188 lines
7.2 KiB
Python
188 lines
7.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from odoo.tests import TransactionCase
|
|
from hypothesis import given, settings, strategies as st
|
|
from decimal import Decimal
|
|
|
|
|
|
class TestPriceTotalProperty(TransactionCase):
|
|
"""
|
|
Property-based tests for price_total calculation with taxes.
|
|
|
|
**Feature: vendor-bill-editable-totals, Property 4: Price total to unit price calculation with taxes**
|
|
"""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
# Create a vendor partner
|
|
self.vendor = self.env['res.partner'].create({
|
|
'name': 'Test Vendor',
|
|
'supplier_rank': 1,
|
|
})
|
|
|
|
# Create a product
|
|
self.product = self.env['res.product'].create({
|
|
'name': 'Test Product',
|
|
'type': 'service',
|
|
'list_price': 100.0,
|
|
})
|
|
|
|
# Create various tax configurations for testing
|
|
# Tax 1: 10% tax excluded
|
|
self.tax_10 = self.env['account.tax'].create({
|
|
'name': 'Tax 10%',
|
|
'amount': 10.0,
|
|
'amount_type': 'percent',
|
|
'type_tax_use': 'purchase',
|
|
'price_include': False,
|
|
})
|
|
|
|
# Tax 2: 5% tax excluded
|
|
self.tax_5 = self.env['account.tax'].create({
|
|
'name': 'Tax 5%',
|
|
'amount': 5.0,
|
|
'amount_type': 'percent',
|
|
'type_tax_use': 'purchase',
|
|
'price_include': False,
|
|
})
|
|
|
|
# Tax 3: 20% tax excluded
|
|
self.tax_20 = self.env['account.tax'].create({
|
|
'name': 'Tax 20%',
|
|
'amount': 20.0,
|
|
'amount_type': 'percent',
|
|
'type_tax_use': 'purchase',
|
|
'price_include': False,
|
|
})
|
|
|
|
# Tax 4: 15% tax included
|
|
self.tax_15_included = self.env['account.tax'].create({
|
|
'name': 'Tax 15% Included',
|
|
'amount': 15.0,
|
|
'amount_type': 'percent',
|
|
'type_tax_use': 'purchase',
|
|
'price_include': True,
|
|
})
|
|
|
|
# Get decimal precision for Product Price
|
|
self.precision = self.env['decimal.precision'].precision_get('Product Price')
|
|
|
|
def _create_vendor_bill_line(self, quantity, price_unit, tax_ids):
|
|
"""Helper method to create a vendor bill line with specified parameters."""
|
|
vendor_bill = self.env['account.move'].create({
|
|
'move_type': 'in_invoice',
|
|
'partner_id': self.vendor.id,
|
|
'invoice_date': '2024-01-01',
|
|
})
|
|
|
|
line = self.env['account.move.line'].create({
|
|
'move_id': vendor_bill.id,
|
|
'product_id': self.product.id,
|
|
'name': 'Test Line',
|
|
'quantity': quantity,
|
|
'price_unit': price_unit,
|
|
'tax_ids': [(6, 0, tax_ids)],
|
|
})
|
|
|
|
return line
|
|
|
|
@given(
|
|
price_total=st.floats(min_value=0.01, max_value=1000000.0, allow_nan=False, allow_infinity=False),
|
|
quantity=st.floats(min_value=1.0, max_value=10000.0, allow_nan=False, allow_infinity=False),
|
|
tax_config=st.integers(min_value=0, max_value=5)
|
|
)
|
|
@settings(max_examples=100, deadline=None)
|
|
def test_property_price_total_with_taxes(self, price_total, quantity, tax_config):
|
|
"""
|
|
**Feature: vendor-bill-editable-totals, Property 4: Price total to unit price calculation with taxes**
|
|
**Validates: Requirements 2.2, 2.3, 2.4**
|
|
|
|
Property: For any invoice line with non-zero quantity, configured taxes, and a
|
|
user-modified price_total value, the recalculated price_unit should result in a
|
|
recomputed price_total that matches the user input within the configured decimal precision.
|
|
"""
|
|
# Map tax_config to different tax configurations
|
|
# 0: No taxes
|
|
# 1: Single tax (10%)
|
|
# 2: Single tax (20%)
|
|
# 3: Multiple taxes (10% + 5%)
|
|
# 4: Multiple taxes (20% + 5%)
|
|
# 5: Tax included (15%)
|
|
tax_ids = []
|
|
if tax_config == 0:
|
|
tax_ids = []
|
|
elif tax_config == 1:
|
|
tax_ids = [self.tax_10.id]
|
|
elif tax_config == 2:
|
|
tax_ids = [self.tax_20.id]
|
|
elif tax_config == 3:
|
|
tax_ids = [self.tax_10.id, self.tax_5.id]
|
|
elif tax_config == 4:
|
|
tax_ids = [self.tax_20.id, self.tax_5.id]
|
|
elif tax_config == 5:
|
|
tax_ids = [self.tax_15_included.id]
|
|
|
|
# Create an invoice line
|
|
line = self._create_vendor_bill_line(quantity, 1.0, tax_ids)
|
|
|
|
# Store the user input price_total
|
|
user_input_total = price_total
|
|
|
|
# Step 1: Set the price_total to trigger the onchange
|
|
line.price_total = user_input_total
|
|
line._onchange_price_total()
|
|
|
|
# Step 2: Calculate what the recomputed price_total should be
|
|
# using Odoo's tax computation with the new price_unit
|
|
if not line.tax_ids:
|
|
# No taxes: price_total should equal price_subtotal
|
|
recomputed_total = line.price_unit * line.quantity
|
|
else:
|
|
# With taxes: use Odoo's tax computation API
|
|
tax_results = line.tax_ids.compute_all(
|
|
price_unit=line.price_unit,
|
|
currency=line.currency_id,
|
|
quantity=line.quantity,
|
|
product=line.product_id,
|
|
partner=line.move_id.partner_id
|
|
)
|
|
recomputed_total = tax_results['total_included']
|
|
|
|
# Calculate the tolerance based on decimal precision
|
|
# The tolerance accounts for:
|
|
# 1. Rounding of price_unit to 'precision' decimal places
|
|
# 2. Cumulative effect when multiplied by quantity
|
|
# 3. Tax calculations that may introduce additional rounding
|
|
unit_rounding_error = 0.5 * (10 ** (-self.precision))
|
|
base_tolerance = quantity * unit_rounding_error
|
|
|
|
# For tax calculations, we need a more lenient tolerance because:
|
|
# - Tax calculations involve division and multiplication
|
|
# - Multiple taxes compound the rounding errors
|
|
# - Tax-included calculations are more complex
|
|
if tax_ids:
|
|
# With taxes, allow for additional rounding in tax calculations
|
|
# The tolerance is proportional to the price_total
|
|
tolerance = max(base_tolerance * 2, user_input_total * 0.0001) # 0.01% or base tolerance
|
|
else:
|
|
# Without taxes, use the base tolerance
|
|
tolerance = base_tolerance * 1.1 # 10% margin for floating point
|
|
|
|
# Verify the property: recomputed price_total should match user input
|
|
# within the configured decimal precision
|
|
difference = abs(recomputed_total - user_input_total)
|
|
|
|
self.assertLessEqual(
|
|
difference,
|
|
tolerance,
|
|
msg=f"Round-trip accuracy failed for tax_config={tax_config}: "
|
|
f"user input={user_input_total:.{self.precision}f}, "
|
|
f"price_unit={line.price_unit:.{self.precision}f}, "
|
|
f"quantity={quantity:.2f}, "
|
|
f"recomputed={recomputed_total:.{self.precision}f}, "
|
|
f"difference={difference:.{self.precision+2}f}, "
|
|
f"tolerance={tolerance:.{self.precision+2}f}"
|
|
)
|