190 lines
7.4 KiB
Python
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}"
|
|
)
|