683 lines
20 KiB
Python
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)
|