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

190 lines
7.4 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 TestDecimalPrecisionProperty(TransactionCase):
"""
Property-based tests for decimal precision compliance.
**Feature: vendor-bill-editable-totals, Property 5: Decimal precision compliance**
"""
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 a vendor bill
self.vendor_bill = self.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': self.vendor.id,
'invoice_date': '2024-01-01',
})
# Create a tax for testing price_total calculations
self.tax_10 = self.env['account.tax'].create({
'name': 'Tax 10%',
'amount': 10.0,
'amount_type': 'percent',
'type_tax_use': 'purchase',
'price_include': False,
})
# Get decimal precision for Product Price
self.precision = self.env['decimal.precision'].precision_get('Product Price')
def _count_decimal_places(self, value):
"""Helper method to count the number of decimal places in a float."""
# Convert to string and count digits after decimal point
value_str = f"{value:.15f}" # Use high precision to capture all decimals
if '.' in value_str:
# Remove trailing zeros
value_str = value_str.rstrip('0').rstrip('.')
if '.' in value_str:
return len(value_str.split('.')[1])
return 0
@given(
price_subtotal=st.floats(
min_value=0.001,
max_value=1000000.0,
allow_nan=False,
allow_infinity=False
).map(lambda x: round(x, 10)), # Generate values with excessive decimal places
quantity=st.floats(min_value=1.0, max_value=10000.0, allow_nan=False, allow_infinity=False)
)
@settings(max_examples=100, deadline=None)
def test_property_decimal_precision_price_subtotal(self, price_subtotal, quantity):
"""
**Feature: vendor-bill-editable-totals, Property 5: Decimal precision compliance**
**Validates: Requirements 3.5**
Property: For any calculated price_unit value from price_subtotal modification,
the system should round the value according to Odoo's configured decimal precision
for the Product Price field.
"""
# Create an invoice line
line = self.env['account.move.line'].create({
'move_id': self.vendor_bill.id,
'product_id': self.product.id,
'name': 'Test Line',
'quantity': quantity,
'price_unit': 1.0, # Initial value, will be recalculated
})
# Set the price_subtotal to trigger the onchange
line.price_subtotal = price_subtotal
line._onchange_price_subtotal()
# Count decimal places in the calculated price_unit
decimal_places = self._count_decimal_places(line.price_unit)
# Verify the property: price_unit should have no more decimal places
# than the configured precision
self.assertLessEqual(
decimal_places,
self.precision,
msg=f"Decimal precision violation: price_unit={line.price_unit} has "
f"{decimal_places} decimal places, but precision is {self.precision}. "
f"Input: price_subtotal={price_subtotal}, quantity={quantity}"
)
# Also verify that the value is properly rounded
expected_rounded = round(price_subtotal / quantity, self.precision)
self.assertEqual(
line.price_unit,
expected_rounded,
msg=f"Price unit not properly rounded: expected {expected_rounded}, "
f"got {line.price_unit}"
)
@given(
price_total=st.floats(
min_value=0.001,
max_value=1000000.0,
allow_nan=False,
allow_infinity=False
).map(lambda x: round(x, 10)), # Generate values with excessive decimal places
quantity=st.floats(min_value=1.0, max_value=10000.0, allow_nan=False, allow_infinity=False),
use_tax=st.booleans()
)
@settings(max_examples=100, deadline=None)
def test_property_decimal_precision_price_total(self, price_total, quantity, use_tax):
"""
**Feature: vendor-bill-editable-totals, Property 5: Decimal precision compliance**
**Validates: Requirements 3.5**
Property: For any calculated price_unit value from price_total modification,
the system should round the value according to Odoo's configured decimal precision
for the Product Price field.
"""
# Create an invoice line with or without tax
tax_ids = [self.tax_10.id] if use_tax else []
line = self.env['account.move.line'].create({
'move_id': self.vendor_bill.id,
'product_id': self.product.id,
'name': 'Test Line',
'quantity': quantity,
'price_unit': 1.0, # Initial value, will be recalculated
'tax_ids': [(6, 0, tax_ids)],
})
# Set the price_total to trigger the onchange
line.price_total = price_total
line._onchange_price_total()
# Count decimal places in the calculated price_unit
decimal_places = self._count_decimal_places(line.price_unit)
# Verify the property: price_unit should have no more decimal places
# than the configured precision
self.assertLessEqual(
decimal_places,
self.precision,
msg=f"Decimal precision violation: price_unit={line.price_unit} has "
f"{decimal_places} decimal places, but precision is {self.precision}. "
f"Input: price_total={price_total}, quantity={quantity}, use_tax={use_tax}"
)
# Calculate expected value based on whether tax is used
if use_tax:
# With tax: need to account for tax factor
tax_results = line.tax_ids.compute_all(
price_unit=1.0,
currency=line.currency_id,
quantity=1.0,
product=line.product_id,
partner=line.move_id.partner_id
)
tax_factor = tax_results['total_included'] / tax_results['total_excluded']
expected_price_unit = (price_total / tax_factor) / quantity
else:
# Without tax: price_total equals price_subtotal
expected_price_unit = price_total / quantity
expected_rounded = round(expected_price_unit, self.precision)
# Verify that the value is properly rounded
self.assertEqual(
line.price_unit,
expected_rounded,
msg=f"Price unit not properly rounded: expected {expected_rounded}, "
f"got {line.price_unit}"
)