427 lines
14 KiB
Python
427 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Standalone test runner for edge case unit tests.
|
|
This allows running tests without the full Odoo test framework.
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
|
|
|
|
class MockEnv:
|
|
"""Mock Odoo environment for testing"""
|
|
|
|
def __getitem__(self, key):
|
|
if key == 'decimal.precision':
|
|
return MockDecimalPrecision()
|
|
return self
|
|
|
|
def precision_get(self, name):
|
|
return 2 # Default precision for Product Price
|
|
|
|
|
|
class MockDecimalPrecision:
|
|
def precision_get(self, name):
|
|
return 2
|
|
|
|
|
|
class MockTax:
|
|
"""Mock tax record"""
|
|
|
|
def __init__(self, name, amount, price_include=False):
|
|
self.name = name
|
|
self.amount = amount
|
|
self.price_include = price_include
|
|
|
|
def compute_all(self, price_unit, currency, quantity, product, partner):
|
|
"""Compute tax amounts"""
|
|
total_excluded = price_unit * quantity
|
|
|
|
if self.price_include:
|
|
# For price-included taxes, the price_unit already includes tax
|
|
# So total_included = total_excluded
|
|
total_included = total_excluded
|
|
else:
|
|
# For price-excluded taxes, add the tax to get total_included
|
|
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
|
|
|
|
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
|
|
|
|
# Calculate combined tax effect
|
|
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 testing"""
|
|
|
|
def __init__(self, move_type='in_invoice'):
|
|
self.price_subtotal = 0.0
|
|
self.price_total = 0.0
|
|
self.quantity = 1.0
|
|
self.price_unit = 0.0
|
|
self.move_id = MockMove(move_type)
|
|
self.env = MockEnv()
|
|
self.tax_ids = MockTaxIds([])
|
|
self.currency_id = None
|
|
self.product_id = None
|
|
|
|
def set_taxes(self, taxes):
|
|
"""Set tax_ids for the line"""
|
|
self.tax_ids = MockTaxIds(taxes)
|
|
|
|
def _onchange_price_subtotal(self):
|
|
"""Simplified version of the onchange method"""
|
|
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):
|
|
"""Simplified version of the 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")
|
|
|
|
# Handle case with no taxes: price_total equals price_subtotal
|
|
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
|
|
|
|
# Check if any taxes are price-included
|
|
has_price_included_tax = any(tax.price_include for tax in self.tax_ids)
|
|
|
|
if has_price_included_tax:
|
|
# For tax-included taxes, price_unit = price_total / quantity
|
|
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:
|
|
# For tax-excluded taxes, calculate the tax factor
|
|
tax_results = self.tax_ids.compute_all(
|
|
price_unit=1.0,
|
|
currency=self.currency_id,
|
|
quantity=1.0,
|
|
product=self.product_id,
|
|
partner=None
|
|
)
|
|
|
|
# Calculate the tax factor
|
|
if tax_results['total_excluded'] != 0:
|
|
tax_factor = tax_results['total_included'] / tax_results['total_excluded']
|
|
else:
|
|
tax_factor = 1.0
|
|
|
|
# Derive price_subtotal from price_total
|
|
derived_price_subtotal = self.price_total / tax_factor
|
|
|
|
# Calculate price_unit from derived price_subtotal
|
|
new_price_unit = derived_price_subtotal / self.quantity
|
|
|
|
# Apply decimal precision rounding
|
|
precision = self.env['decimal.precision'].precision_get('Product Price')
|
|
self.price_unit = round(new_price_unit, precision)
|
|
|
|
|
|
class MockMove:
|
|
def __init__(self, move_type='in_invoice'):
|
|
self.move_type = move_type
|
|
|
|
|
|
def test_zero_quantity_price_subtotal_raises_error():
|
|
"""
|
|
Test that modifying price_subtotal with zero quantity raises error.
|
|
Requirement 3.1: Division by zero protection
|
|
"""
|
|
line = MockLine()
|
|
line.quantity = 0.0
|
|
line.price_unit = 100.0
|
|
line.price_subtotal = 500.0
|
|
|
|
try:
|
|
line._onchange_price_subtotal()
|
|
return False, "Expected ValueError but no exception was raised"
|
|
except ValueError as e:
|
|
if 'quantity must be greater than zero' in str(e):
|
|
return True, "Correctly raised error for zero quantity"
|
|
else:
|
|
return False, f"Wrong error message: {e}"
|
|
|
|
|
|
def test_zero_quantity_price_total_raises_error():
|
|
"""
|
|
Test that modifying price_total with zero quantity raises error.
|
|
Requirement 3.1: Division by zero protection
|
|
"""
|
|
line = MockLine()
|
|
line.quantity = 0.0
|
|
line.price_unit = 100.0
|
|
line.price_total = 550.0
|
|
|
|
try:
|
|
line._onchange_price_total()
|
|
return False, "Expected ValueError but no exception was raised"
|
|
except ValueError as e:
|
|
if 'quantity must be greater than zero' in str(e):
|
|
return True, "Correctly raised error for zero quantity"
|
|
else:
|
|
return False, f"Wrong error message: {e}"
|
|
|
|
|
|
def test_negative_price_subtotal_credit_note():
|
|
"""
|
|
Test that negative price_subtotal values are handled correctly for credit notes.
|
|
Requirement 3.2: Accept and process negative values correctly
|
|
"""
|
|
line = MockLine(move_type='in_refund')
|
|
line.quantity = 5.0
|
|
line.price_unit = 100.0
|
|
line.price_subtotal = -500.0
|
|
|
|
line._onchange_price_subtotal()
|
|
|
|
expected_price_unit = -100.0
|
|
precision = 2
|
|
|
|
if abs(line.price_unit - expected_price_unit) < 10 ** (-precision):
|
|
return True, f"Correctly calculated negative price_unit: {line.price_unit}"
|
|
else:
|
|
return False, f"Expected {expected_price_unit}, got {line.price_unit}"
|
|
|
|
|
|
def test_negative_price_total_credit_note():
|
|
"""
|
|
Test that negative price_total values are handled correctly for credit notes.
|
|
Requirement 3.2: Accept and process negative values correctly
|
|
"""
|
|
line = MockLine(move_type='in_refund')
|
|
line.quantity = 5.0
|
|
line.price_unit = 100.0
|
|
|
|
# Set up 10% tax
|
|
tax_10 = MockTax('Tax 10%', 10.0, price_include=False)
|
|
line.set_taxes([tax_10])
|
|
|
|
# Set negative price_total
|
|
line.price_total = -550.0
|
|
line._onchange_price_total()
|
|
|
|
expected_price_unit = -100.0
|
|
precision = 2
|
|
|
|
if abs(line.price_unit - expected_price_unit) < 10 ** (-precision):
|
|
return True, f"Correctly calculated negative price_unit with tax: {line.price_unit}"
|
|
else:
|
|
return False, f"Expected {expected_price_unit}, got {line.price_unit}"
|
|
|
|
|
|
def test_no_taxes_price_total_equals_subtotal():
|
|
"""
|
|
Test that when no taxes are configured, price_total is treated as price_subtotal.
|
|
Requirement 3.3: Handle no taxes scenario correctly
|
|
"""
|
|
line = MockLine()
|
|
line.quantity = 10.0
|
|
line.price_unit = 100.0
|
|
line.price_total = 1000.0
|
|
|
|
line._onchange_price_total()
|
|
|
|
expected_price_unit = 100.0
|
|
precision = 2
|
|
|
|
if abs(line.price_unit - expected_price_unit) < 10 ** (-precision):
|
|
return True, f"Correctly handled no taxes scenario: {line.price_unit}"
|
|
else:
|
|
return False, f"Expected {expected_price_unit}, got {line.price_unit}"
|
|
|
|
|
|
def test_single_tax_calculation():
|
|
"""
|
|
Test that a single tax is correctly computed in price_total calculation.
|
|
Requirement 3.4: Correctly compute single tax effect
|
|
"""
|
|
line = MockLine()
|
|
line.quantity = 10.0
|
|
line.price_unit = 100.0
|
|
|
|
# Set up 10% tax
|
|
tax_10 = MockTax('Tax 10%', 10.0, price_include=False)
|
|
line.set_taxes([tax_10])
|
|
|
|
# Set price_total with 10% tax
|
|
line.price_total = 1100.0
|
|
line._onchange_price_total()
|
|
|
|
expected_price_unit = 100.0
|
|
precision = 2
|
|
|
|
if abs(line.price_unit - expected_price_unit) < 10 ** (-precision):
|
|
return True, f"Correctly calculated with single tax: {line.price_unit}"
|
|
else:
|
|
return False, f"Expected {expected_price_unit}, got {line.price_unit}"
|
|
|
|
|
|
def test_single_tax_included_calculation():
|
|
"""
|
|
Test that a single tax-included tax is correctly computed.
|
|
Requirement 3.4: Correctly compute single tax effect with price_include=True
|
|
"""
|
|
line = MockLine()
|
|
line.quantity = 10.0
|
|
line.price_unit = 100.0
|
|
|
|
# Set up 15% tax-included
|
|
tax_15_included = MockTax('Tax 15% Included', 15.0, price_include=True)
|
|
line.set_taxes([tax_15_included])
|
|
|
|
# Set price_total with 15% tax-included
|
|
line.price_total = 1150.0
|
|
line._onchange_price_total()
|
|
|
|
expected_price_unit = 115.0
|
|
precision = 2
|
|
|
|
if abs(line.price_unit - expected_price_unit) < 10 ** (-precision):
|
|
return True, f"Correctly calculated with tax-included: {line.price_unit}"
|
|
else:
|
|
return False, f"Expected {expected_price_unit}, got {line.price_unit}"
|
|
|
|
|
|
def test_multiple_taxes_calculation():
|
|
"""
|
|
Test that multiple taxes are correctly computed in price_total calculation.
|
|
Requirement 3.4: Correctly compute combined tax effect
|
|
"""
|
|
line = MockLine()
|
|
line.quantity = 10.0
|
|
line.price_unit = 100.0
|
|
|
|
# Set up 10% + 5% taxes
|
|
tax_10 = MockTax('Tax 10%', 10.0, price_include=False)
|
|
tax_5 = MockTax('Tax 5%', 5.0, price_include=False)
|
|
line.set_taxes([tax_10, tax_5])
|
|
|
|
# Set price_total with 15% combined tax
|
|
line.price_total = 1150.0
|
|
line._onchange_price_total()
|
|
|
|
expected_price_unit = 100.0
|
|
precision = 2
|
|
|
|
if abs(line.price_unit - expected_price_unit) < 10 ** (-precision):
|
|
return True, f"Correctly calculated with multiple taxes: {line.price_unit}"
|
|
else:
|
|
return False, f"Expected {expected_price_unit}, got {line.price_unit}"
|
|
|
|
|
|
def test_multiple_taxes_with_different_amounts():
|
|
"""
|
|
Test multiple taxes with different amounts to verify correct computation.
|
|
Requirement 3.4: Correctly compute combined tax effect with various amounts
|
|
"""
|
|
line = MockLine()
|
|
line.quantity = 5.0
|
|
line.price_unit = 200.0
|
|
|
|
# Set up 10% + 5% taxes
|
|
tax_10 = MockTax('Tax 10%', 10.0, price_include=False)
|
|
tax_5 = MockTax('Tax 5%', 5.0, price_include=False)
|
|
line.set_taxes([tax_10, tax_5])
|
|
|
|
# Set price_total with 15% combined tax
|
|
line.price_total = 1150.0
|
|
line._onchange_price_total()
|
|
|
|
expected_price_unit = 200.0
|
|
precision = 2
|
|
|
|
if abs(line.price_unit - expected_price_unit) < 10 ** (-precision):
|
|
return True, f"Correctly calculated with multiple taxes (different amounts): {line.price_unit}"
|
|
else:
|
|
return False, f"Expected {expected_price_unit}, got {line.price_unit}"
|
|
|
|
|
|
if __name__ == '__main__':
|
|
print("Running Edge Case Unit Tests for vendor-bill-editable-totals")
|
|
print("=" * 70)
|
|
|
|
tests = [
|
|
("Zero quantity with price_subtotal", test_zero_quantity_price_subtotal_raises_error),
|
|
("Zero quantity with price_total", test_zero_quantity_price_total_raises_error),
|
|
("Negative price_subtotal (credit note)", test_negative_price_subtotal_credit_note),
|
|
("Negative price_total (credit note)", test_negative_price_total_credit_note),
|
|
("No taxes scenario", test_no_taxes_price_total_equals_subtotal),
|
|
("Single tax calculation", test_single_tax_calculation),
|
|
("Single tax-included calculation", test_single_tax_included_calculation),
|
|
("Multiple taxes calculation", test_multiple_taxes_calculation),
|
|
("Multiple taxes with different amounts", test_multiple_taxes_with_different_amounts),
|
|
]
|
|
|
|
passed = 0
|
|
failed = 0
|
|
|
|
for test_name, test_func in tests:
|
|
print(f"\nTest: {test_name}")
|
|
print("-" * 70)
|
|
try:
|
|
success, message = test_func()
|
|
if success:
|
|
print(f"✓ PASSED: {message}")
|
|
passed += 1
|
|
else:
|
|
print(f"✗ FAILED: {message}")
|
|
failed += 1
|
|
except Exception as e:
|
|
print(f"✗ FAILED with exception: {e}")
|
|
failed += 1
|
|
|
|
print("\n" + "=" * 70)
|
|
print(f"Results: {passed} passed, {failed} failed out of {len(tests)} tests")
|
|
|
|
if failed == 0:
|
|
print("✓ All edge case tests passed!")
|
|
sys.exit(0)
|
|
else:
|
|
print("✗ Some edge case tests failed!")
|
|
sys.exit(1)
|