vendor_bill_editable_totals/tests/run_integration_tests.py
2025-11-21 18:02:20 +07:00

683 lines
20 KiB
Python

#!/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)