# -*- 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 TestPriceSubtotalProperty(TransactionCase): """ Property-based tests for price_subtotal calculation. **Feature: vendor-bill-editable-totals, Property 1: Price subtotal to unit price calculation** """ 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', }) # Get decimal precision for Product Price self.precision = self.env['decimal.precision'].precision_get('Product Price') @given( price_subtotal=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) ) @settings(max_examples=100, deadline=None) def test_property_price_subtotal_to_unit_price(self, price_subtotal, quantity): """ **Feature: vendor-bill-editable-totals, Property 1: Price subtotal to unit price calculation** **Validates: Requirements 1.2, 1.4** Property: For any invoice line with non-zero quantity and a user-modified price_subtotal value, the recalculated price_unit should equal price_subtotal divided by quantity. """ # 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() # Calculate expected price_unit expected_price_unit = price_subtotal / quantity expected_price_unit_rounded = round(expected_price_unit, self.precision) # Verify the property: price_unit should equal price_subtotal / quantity self.assertAlmostEqual( line.price_unit, expected_price_unit_rounded, places=self.precision, msg=f"Price unit calculation failed: expected {expected_price_unit_rounded}, got {line.price_unit}" ) @given( price_subtotal=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) ) @settings(max_examples=100, deadline=None) def test_property_quantity_invariance(self, price_subtotal, quantity): """ **Feature: vendor-bill-editable-totals, Property 2: Quantity invariance during price_subtotal modification** **Validates: Requirements 1.3** Property: For any invoice line, when price_subtotal is modified, the quantity value should remain unchanged after the onchange handler executes. """ # 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 }) # Store the initial quantity initial_quantity = line.quantity # Set the price_subtotal to trigger the onchange line.price_subtotal = price_subtotal line._onchange_price_subtotal() # Verify the property: quantity should remain unchanged self.assertEqual( line.quantity, initial_quantity, msg=f"Quantity changed during price_subtotal modification: expected {initial_quantity}, got {line.quantity}" ) @given( price_subtotal=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) ) @settings(max_examples=100, deadline=None) def test_property_price_subtotal_round_trip(self, price_subtotal, quantity): """ **Feature: vendor-bill-editable-totals, Property 3: Price subtotal round-trip accuracy** **Validates: Requirements 1.5** Property: For any invoice line with non-zero quantity, if a user inputs a price_subtotal value, the system calculates price_unit, and then Odoo recomputes price_subtotal from that price_unit and quantity, the final price_subtotal should match the user input within the configured decimal precision. """ # 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 }) # Store the user input price_subtotal user_input_subtotal = price_subtotal # Step 1: Set the price_subtotal to trigger the onchange line.price_subtotal = user_input_subtotal line._onchange_price_subtotal() # Step 2: Let Odoo recompute price_subtotal from price_unit and quantity # This simulates what happens when Odoo's standard compute methods run recomputed_subtotal = line.price_unit * line.quantity # Calculate the tolerance based on decimal precision # We allow a small discrepancy due to rounding tolerance = 10 ** (-self.precision) # Verify the property: recomputed price_subtotal should match user input # within the configured decimal precision self.assertAlmostEqual( recomputed_subtotal, user_input_subtotal, places=self.precision, msg=f"Round-trip accuracy failed: user input={user_input_subtotal:.{self.precision}f}, " f"price_unit={line.price_unit:.{self.precision}f}, " f"recomputed={recomputed_subtotal:.{self.precision}f}, " f"difference={abs(recomputed_subtotal - user_input_subtotal):.{self.precision+2}f}" )