#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Standalone integration test runner for vendor_bill_editable_totals module. This script simulates integration test scenarios without requiring a full Odoo database. For full integration tests with Odoo, use: python3 odoo/odoo-bin -c odoo.conf --test-enable --stop-after-init \ -d odoo17 -u vendor_bill_editable_totals --log-level=test Usage: python3 run_integration_tests.py """ import sys import os class MockEnv: """Mock Odoo environment for testing""" def __getitem__(self, key): if key == 'decimal.precision': return MockDecimalPrecision() elif key == 'res.partner': return MockPartnerModel() elif key == 'res.product': return MockProductModel() elif key == 'account.move': return MockMoveModel() elif key == 'account.move.line': return MockLineModel() elif key == 'account.tax': return MockTaxModel() return self def precision_get(self, name): return 2 # Default precision for Product Price class MockDecimalPrecision: def precision_get(self, name): return 2 class MockPartnerModel: def create(self, vals): return MockPartner(vals) class MockProductModel: def create(self, vals): return MockProduct(vals) class MockMoveModel: def create(self, vals): return MockMove(vals) class MockLineModel: def create(self, vals): return MockLine(vals) class MockTaxModel: def create(self, vals): return MockTax(vals) class MockPartner: def __init__(self, vals): self.id = 1 self.name = vals.get('name', 'Test Partner') class MockProduct: def __init__(self, vals): self.id = 1 self.name = vals.get('name', 'Test Product') class MockMove: def __init__(self, vals): self.id = 1 self.move_type = vals.get('move_type', 'in_invoice') self.state = 'draft' self.partner_id = vals.get('partner_id') def flush_recordset(self): """Simulate flushing to database""" pass class MockTax: """Mock tax record""" def __init__(self, vals): self.id = 1 self.name = vals.get('name', 'Tax') self.amount = vals.get('amount', 0.0) self.price_include = vals.get('price_include', False) def compute_all(self, price_unit, currency, quantity, product, partner): """Compute tax amounts""" total_excluded = price_unit * quantity if self.price_include: total_included = total_excluded else: tax_amount = total_excluded * (self.amount / 100.0) total_included = total_excluded + tax_amount return { 'total_excluded': total_excluded, 'total_included': total_included, } class MockTaxIds: """Mock tax_ids recordset""" def __init__(self, taxes): self.taxes = taxes if taxes else [] def __iter__(self): return iter(self.taxes) def __bool__(self): return len(self.taxes) > 0 def compute_all(self, price_unit, currency, quantity, product, partner): """Compute combined tax amounts for multiple taxes""" total_excluded = price_unit * quantity total_included = total_excluded for tax in self.taxes: if not tax.price_include: tax_amount = total_excluded * (tax.amount / 100.0) total_included += tax_amount return { 'total_excluded': total_excluded, 'total_included': total_included, } class MockLine: """Mock account.move.line for integration testing""" def __init__(self, vals): self.id = 1 self.move_id = vals.get('move_id') self.product_id = vals.get('product_id') self.name = vals.get('name', 'Test Line') self.quantity = vals.get('quantity', 1.0) self.price_unit = vals.get('price_unit', 0.0) self.price_subtotal = self.quantity * self.price_unit self.price_total = self.price_subtotal self.env = MockEnv() self.currency_id = None # Handle tax_ids tax_ids_val = vals.get('tax_ids', []) if tax_ids_val and tax_ids_val[0][0] == 6: # Format: [(6, 0, [tax_id1, tax_id2])] tax_list = tax_ids_val[0][2] if len(tax_ids_val[0]) > 2 else [] # For simplicity, create mock taxes self.tax_ids = MockTaxIds([]) else: self.tax_ids = MockTaxIds([]) def set_taxes(self, taxes): """Set tax_ids for the line""" self.tax_ids = MockTaxIds(taxes) def _compute_price_subtotal(self): """Simulate Odoo's compute method""" self.price_subtotal = self.quantity * self.price_unit # Calculate price_total with taxes if self.tax_ids: tax_results = self.tax_ids.compute_all( price_unit=self.price_unit, currency=self.currency_id, quantity=self.quantity, product=self.product_id, partner=None ) self.price_total = tax_results['total_included'] else: self.price_total = self.price_subtotal def _onchange_price_subtotal(self): """Onchange method for price_subtotal""" if self.move_id.move_type not in ('in_invoice', 'in_refund'): return if self.quantity == 0: raise ValueError("Cannot calculate unit price: quantity must be greater than zero") new_price_unit = self.price_subtotal / self.quantity precision = self.env['decimal.precision'].precision_get('Product Price') self.price_unit = round(new_price_unit, precision) def _onchange_price_total(self): """Onchange method for price_total""" if self.move_id.move_type not in ('in_invoice', 'in_refund'): return if self.quantity == 0: raise ValueError("Cannot calculate unit price: quantity must be greater than zero") if not self.tax_ids: new_price_unit = self.price_total / self.quantity precision = self.env['decimal.precision'].precision_get('Product Price') self.price_unit = round(new_price_unit, precision) return has_price_included_tax = any(tax.price_include for tax in self.tax_ids) if has_price_included_tax: new_price_unit = self.price_total / self.quantity precision = self.env['decimal.precision'].precision_get('Product Price') self.price_unit = round(new_price_unit, precision) else: tax_results = self.tax_ids.compute_all( price_unit=1.0, currency=self.currency_id, quantity=1.0, product=self.product_id, partner=None ) if tax_results['total_excluded'] != 0: tax_factor = tax_results['total_included'] / tax_results['total_excluded'] else: tax_factor = 1.0 derived_price_subtotal = self.price_total / tax_factor new_price_unit = derived_price_subtotal / self.quantity precision = self.env['decimal.precision'].precision_get('Product Price') self.price_unit = round(new_price_unit, precision) def flush_recordset(self): """Simulate flushing to database""" pass def invalidate_recordset(self): """Simulate invalidating cache""" pass def test_full_workflow_modify_price_subtotal(): """ Test full workflow: create vendor bill → modify price_subtotal → save → verify. Requirement 4.3 """ print("\nTest: Full workflow - modify price_subtotal") print("-" * 70) env = MockEnv() # Create vendor vendor = env['res.partner'].create({ 'name': 'Integration Test Vendor', 'supplier_rank': 1, }) # Create product product = env['res.product'].create({ 'name': 'Product A', 'type': 'service', 'list_price': 100.0, }) # Create vendor bill vendor_bill = env['account.move'].create({ 'move_type': 'in_invoice', 'partner_id': vendor.id, 'invoice_date': '2024-01-15', }) # Create tax tax_10 = MockTax({ 'name': 'Tax 10%', 'amount': 10.0, 'price_include': False, }) # Create invoice line line = env['account.move.line'].create({ 'move_id': vendor_bill, 'product_id': product, 'name': 'Product A - Test', 'quantity': 5.0, 'price_unit': 100.0, }) line.set_taxes([tax_10]) # Modify price_subtotal line.price_subtotal = 600.0 line._onchange_price_subtotal() # Verify price_unit was recalculated expected_price_unit = 120.0 precision = 2 if abs(line.price_unit - expected_price_unit) < 10 ** (-precision): print(f"✓ price_unit correctly recalculated: {line.price_unit}") else: return False, f"price_unit incorrect: expected {expected_price_unit}, got {line.price_unit}" # Trigger recomputation line._compute_price_subtotal() # Verify price_subtotal is maintained if abs(line.price_subtotal - 600.0) < 0.01: print(f"✓ price_subtotal maintained: {line.price_subtotal}") else: return False, f"price_subtotal not maintained: expected 600.0, got {line.price_subtotal}" # Verify price_total is correct expected_price_total = 660.0 if abs(line.price_total - expected_price_total) < 0.01: print(f"✓ price_total correct: {line.price_total}") else: return False, f"price_total incorrect: expected {expected_price_total}, got {line.price_total}" # Save vendor_bill.flush_recordset() line.flush_recordset() return True, "Full workflow with price_subtotal completed successfully" def test_full_workflow_modify_price_total(): """ Test full workflow: create vendor bill → modify price_total → save → verify. Requirement 4.3 """ print("\nTest: Full workflow - modify price_total") print("-" * 70) env = MockEnv() # Create vendor vendor = env['res.partner'].create({ 'name': 'Integration Test Vendor', 'supplier_rank': 1, }) # Create product product = env['res.product'].create({ 'name': 'Product B', 'type': 'consu', 'list_price': 250.0, }) # Create vendor bill vendor_bill = env['account.move'].create({ 'move_type': 'in_invoice', 'partner_id': vendor.id, 'invoice_date': '2024-01-20', }) # Create tax tax_20 = MockTax({ 'name': 'Tax 20%', 'amount': 20.0, 'price_include': False, }) # Create invoice line line = env['account.move.line'].create({ 'move_id': vendor_bill, 'product_id': product, 'name': 'Product B - Test', 'quantity': 10.0, 'price_unit': 250.0, }) line.set_taxes([tax_20]) # Modify price_total line.price_total = 3600.0 line._onchange_price_total() # Verify price_unit was recalculated expected_price_unit = 300.0 precision = 2 if abs(line.price_unit - expected_price_unit) < 10 ** (-precision): print(f"✓ price_unit correctly recalculated: {line.price_unit}") else: return False, f"price_unit incorrect: expected {expected_price_unit}, got {line.price_unit}" # Trigger recomputation line._compute_price_subtotal() # Verify price_subtotal is correct expected_price_subtotal = 3000.0 if abs(line.price_subtotal - expected_price_subtotal) < 0.01: print(f"✓ price_subtotal correct: {line.price_subtotal}") else: return False, f"price_subtotal incorrect: expected {expected_price_subtotal}, got {line.price_subtotal}" # Verify price_total is maintained if abs(line.price_total - 3600.0) < 0.01: print(f"✓ price_total maintained: {line.price_total}") else: return False, f"price_total not maintained: expected 3600.0, got {line.price_total}" # Save vendor_bill.flush_recordset() line.flush_recordset() return True, "Full workflow with price_total completed successfully" def test_multiple_lines_workflow(): """ Test workflow with multiple invoice lines being modified. Requirement 4.3 """ print("\nTest: Multiple lines workflow") print("-" * 70) env = MockEnv() # Create vendor and products vendor = env['res.partner'].create({'name': 'Test Vendor'}) product_a = env['res.product'].create({'name': 'Product A'}) product_b = env['res.product'].create({'name': 'Product B'}) # Create vendor bill vendor_bill = env['account.move'].create({ 'move_type': 'in_invoice', 'partner_id': vendor.id, }) # Create taxes tax_10 = MockTax({'name': 'Tax 10%', 'amount': 10.0}) tax_20 = MockTax({'name': 'Tax 20%', 'amount': 20.0}) # Create lines line1 = env['account.move.line'].create({ 'move_id': vendor_bill, 'product_id': product_a, 'name': 'Line 1', 'quantity': 3.0, 'price_unit': 100.0, }) line1.set_taxes([tax_10]) line2 = env['account.move.line'].create({ 'move_id': vendor_bill, 'product_id': product_b, 'name': 'Line 2', 'quantity': 2.0, 'price_unit': 250.0, }) line2.set_taxes([tax_20]) # Modify line 1's price_subtotal line1.price_subtotal = 450.0 line1._onchange_price_subtotal() # Modify line 2's price_total line2.price_total = 720.0 line2._onchange_price_total() # Verify line 1 expected_price_unit_1 = 150.0 if abs(line1.price_unit - expected_price_unit_1) < 0.01: print(f"✓ Line 1 price_unit correct: {line1.price_unit}") else: return False, f"Line 1 incorrect: expected {expected_price_unit_1}, got {line1.price_unit}" # Verify line 2 expected_price_unit_2 = 300.0 if abs(line2.price_unit - expected_price_unit_2) < 0.01: print(f"✓ Line 2 price_unit correct: {line2.price_unit}") else: return False, f"Line 2 incorrect: expected {expected_price_unit_2}, got {line2.price_unit}" return True, "Multiple lines workflow completed successfully" def test_compatibility_with_standard_validations(): """ Test that standard Odoo validations still work correctly. Requirement 4.3 """ print("\nTest: Compatibility with standard validations") print("-" * 70) env = MockEnv() # Create vendor and product vendor = env['res.partner'].create({'name': 'Test Vendor'}) product = env['res.product'].create({'name': 'Test Product'}) # Create vendor bill vendor_bill = env['account.move'].create({ 'move_type': 'in_invoice', 'partner_id': vendor.id, }) # Create tax tax_10 = MockTax({'name': 'Tax 10%', 'amount': 10.0}) # Create line line = env['account.move.line'].create({ 'move_id': vendor_bill, 'product_id': product, 'name': 'Test Line', 'quantity': 5.0, 'price_unit': 100.0, }) line.set_taxes([tax_10]) # Trigger standard computation line._compute_price_subtotal() expected_subtotal = 500.0 if abs(line.price_subtotal - expected_subtotal) < 0.01: print(f"✓ Standard computation works: {line.price_subtotal}") else: return False, f"Standard computation failed: expected {expected_subtotal}, got {line.price_subtotal}" # Modify via onchange line.price_subtotal = 600.0 line._onchange_price_subtotal() # Verify we can still save vendor_bill.flush_recordset() print("✓ Can save after modification") return True, "Compatibility with standard validations verified" def test_refund_workflow(): """ Test workflow with vendor refund (credit note). Requirement 4.3 """ print("\nTest: Refund workflow") print("-" * 70) env = MockEnv() # Create vendor and product vendor = env['res.partner'].create({'name': 'Test Vendor'}) product = env['res.product'].create({'name': 'Test Product'}) # Create vendor refund vendor_refund = env['account.move'].create({ 'move_type': 'in_refund', 'partner_id': vendor.id, }) # Create tax tax_10 = MockTax({'name': 'Tax 10%', 'amount': 10.0}) # Create line with negative values line = env['account.move.line'].create({ 'move_id': vendor_refund, 'product_id': product, 'name': 'Refund Line', 'quantity': 5.0, 'price_unit': -100.0, }) line.set_taxes([tax_10]) # Modify price_subtotal with negative value line.price_subtotal = -600.0 line._onchange_price_subtotal() # Verify calculation expected_price_unit = -120.0 if abs(line.price_unit - expected_price_unit) < 0.01: print(f"✓ Refund calculation correct: {line.price_unit}") else: return False, f"Refund calculation failed: expected {expected_price_unit}, got {line.price_unit}" # Save vendor_refund.flush_recordset() print("✓ Refund saved successfully") return True, "Refund workflow completed successfully" def test_no_interference_with_other_move_types(): """ Test that the module doesn't interfere with non-vendor-bill move types. Requirement 4.2 """ print("\nTest: No interference with other move types") print("-" * 70) env = MockEnv() # Create customer and product customer = env['res.partner'].create({'name': 'Test Customer'}) product = env['res.product'].create({'name': 'Test Product'}) # Create customer invoice customer_invoice = env['account.move'].create({ 'move_type': 'out_invoice', 'partner_id': customer.id, }) # Create line line = env['account.move.line'].create({ 'move_id': customer_invoice, 'product_id': product, 'name': 'Customer Line', 'quantity': 5.0, 'price_unit': 100.0, }) # Store original price_unit original_price_unit = line.price_unit # Try to modify price_subtotal (should be skipped) line.price_subtotal = 600.0 line._onchange_price_subtotal() # Verify price_unit was NOT changed if line.price_unit == original_price_unit: print(f"✓ Customer invoice not affected: {line.price_unit}") else: return False, f"Customer invoice was affected: {line.price_unit} != {original_price_unit}" return True, "No interference with other move types verified" if __name__ == '__main__': print("=" * 70) print("Integration Tests for vendor-bill-editable-totals") print("=" * 70) tests = [ ("Full workflow - modify price_subtotal", test_full_workflow_modify_price_subtotal), ("Full workflow - modify price_total", test_full_workflow_modify_price_total), ("Multiple lines workflow", test_multiple_lines_workflow), ("Compatibility with standard validations", test_compatibility_with_standard_validations), ("Refund workflow", test_refund_workflow), ("No interference with other move types", test_no_interference_with_other_move_types), ] passed = 0 failed = 0 for test_name, test_func in tests: try: success, message = test_func() if success: print(f"✓ PASSED: {message}\n") passed += 1 else: print(f"✗ FAILED: {message}\n") failed += 1 except Exception as e: print(f"✗ FAILED with exception: {e}\n") failed += 1 print("=" * 70) print(f"Results: {passed} passed, {failed} failed out of {len(tests)} tests") print("=" * 70) if failed == 0: print("\n✓ All integration tests passed!") print("\nNote: These are standalone integration tests.") print("For full Odoo integration tests, run:") print(" python3 odoo/odoo-bin -c odoo.conf --test-enable --stop-after-init \\") print(" -d odoo17 -u vendor_bill_editable_totals --log-level=test") sys.exit(0) else: print("\n✗ Some integration tests failed!") sys.exit(1)