diff --git a/account_invoice_triple_discount_readonly/tests/test_purchase_order.py b/account_invoice_triple_discount_readonly/tests/test_purchase_order.py index cd12fe5..d4a5e1e 100644 --- a/account_invoice_triple_discount_readonly/tests/test_purchase_order.py +++ b/account_invoice_triple_discount_readonly/tests/test_purchase_order.py @@ -7,6 +7,16 @@ from odoo.tests.common import TransactionCase @tagged("post_install", "-at_install") class TestPurchaseOrder(TransactionCase): + """Purchase lines and the vendor bills created from them. + + In 18.0 ``purchase.order.line`` does not use ``triple.discount.mixin`` but + ``purchase.triple.discount.mixin`` (purchase_triple_discount), where the + total discount is a stored compute of discount1..3: the accumulation bug + this addon fixes does not happen there. What matters on the purchase side + is that the three discounts reach the bill untouched, and the bill lines do + use the mixin this addon patches. + """ + @classmethod def setUpClass(cls): super().setUpClass() @@ -20,13 +30,15 @@ class TestPurchaseOrder(TransactionCase): } ) - # Create a product template first, then get the variant + # Create a product template first, then get the variant. Billed on + # ordered quantities, so the bill can be created without a receipt. cls.product_template = cls.env["product.template"].create( { "name": "Test Product PO", "type": "consu", "list_price": 150.0, "standard_price": 80.0, + "purchase_method": "purchase", } ) # Get the auto-created product variant @@ -52,16 +64,24 @@ class TestPurchaseOrder(TransactionCase): } ) - def test_po_line_discount_readonly(self): - """Test that discount field is readonly in PO lines""" + def _create_bill_line(self): + self.purchase_order.button_confirm() + self.purchase_order.action_create_invoice() + return self.purchase_order.invoice_ids.invoice_line_ids.filtered( + lambda line: line.product_id == self.product + ) + + def test_po_line_discount_is_computed(self): + """The total discount of a PO line is computed from discount1..3""" field = self.po_line._fields["discount"] - self.assertTrue(field.readonly, "Discount field should be readonly in PO lines") + self.assertTrue(field.compute) + expected = 100 - (100 * 0.90 * 0.95 * 0.98) + self.assertAlmostEqual(self.po_line.discount, expected, places=2) def test_po_line_write_with_explicit_discounts(self): """Test writing PO line with explicit discounts""" self.po_line.write( { - "discount": 25.0, # Should be ignored "discount1": 12.0, "discount2": 8.0, "discount3": 4.0, @@ -71,22 +91,11 @@ class TestPurchaseOrder(TransactionCase): self.assertEqual(self.po_line.discount1, 12.0) self.assertEqual(self.po_line.discount2, 8.0) self.assertEqual(self.po_line.discount3, 4.0) - - def test_po_line_legacy_discount(self): - """Test legacy discount behavior in PO lines""" - self.po_line.write( - { - "discount": 18.0, - } - ) - - # Should map to discount1 and reset others - self.assertEqual(self.po_line.discount1, 18.0) - self.assertEqual(self.po_line.discount2, 0.0) - self.assertEqual(self.po_line.discount3, 0.0) + expected = 100 - (100 * 0.88 * 0.92 * 0.96) + self.assertAlmostEqual(self.po_line.discount, expected, places=2) def test_po_line_price_calculation(self): - """Test that price subtotal is calculated correctly with triple discount""" + """The PO subtotal applies the total discount""" self.po_line.write( { "discount1": 15.0, @@ -95,11 +104,9 @@ class TestPurchaseOrder(TransactionCase): } ) - # Base: 10 * 150 = 1500 - # After 15% discount: 1275 - # After 10% discount: 1147.5 - # After 5% discount: 1090.125 - expected_subtotal = 10 * 150 * 0.85 * 0.90 * 0.95 + # The PO line keeps the total discount with the "Discount" precision + # (27.33 %, not 27.3325 %), so the subtotal is computed from it. + expected_subtotal = 10 * 150 * (1 - self.po_line.discount / 100) self.assertAlmostEqual(self.po_line.price_subtotal, expected_subtotal, places=2) def test_multiple_po_lines(self): @@ -117,7 +124,7 @@ class TestPurchaseOrder(TransactionCase): ) # Verify both lines have correct discounts - self.assertEqual(self.po_line.discount1, 15.0) + self.assertEqual(self.po_line.discount1, 10.0) self.assertEqual(line2.discount1, 20.0) self.assertEqual(line2.discount2, 15.0) self.assertEqual(line2.discount3, 10.0) @@ -174,40 +181,8 @@ class TestPurchaseOrder(TransactionCase): expected = 10 * 150 self.assertEqual(self.po_line.price_subtotal, expected) - def test_po_line_combined_operations(self): - """Test combined operations on PO line""" - # Update multiple fields at once - self.po_line.write( - { - "product_qty": 15, - "price_unit": 175.0, - "discount1": 18.0, - "discount2": 12.0, - "discount3": 6.0, - } - ) - - # All fields should be updated correctly - self.assertEqual(self.po_line.product_qty, 15) - self.assertEqual(self.po_line.price_unit, 175.0) - self.assertEqual(self.po_line.discount1, 18.0) - self.assertEqual(self.po_line.discount2, 12.0) - self.assertEqual(self.po_line.discount3, 6.0) - - # Calculate expected subtotal - expected = 15 * 175 * 0.82 * 0.88 * 0.94 - self.assertAlmostEqual(self.po_line.price_subtotal, expected, places=2) - def test_po_confirm_with_discounts(self): """Test confirming PO doesn't alter discounts""" - self.po_line.write( - { - "discount1": 10.0, - "discount2": 5.0, - "discount3": 2.0, - } - ) - # Confirm the purchase order self.purchase_order.button_confirm() @@ -215,3 +190,25 @@ class TestPurchaseOrder(TransactionCase): self.assertEqual(self.po_line.discount1, 10.0) self.assertEqual(self.po_line.discount2, 5.0) self.assertEqual(self.po_line.discount3, 2.0) + + def test_bill_keeps_the_three_discounts(self): + """The bill line gets discount1..3 of the PO line, not their sum""" + bill_line = self._create_bill_line() + + self.assertEqual(len(bill_line), 1) + self.assertEqual(bill_line.discount1, 10.0) + self.assertEqual(bill_line.discount2, 5.0) + self.assertEqual(bill_line.discount3, 2.0) + # The bill line computes the total discount without rounding it. + expected = 10 * 150 * 0.90 * 0.95 * 0.98 + self.assertAlmostEqual(bill_line.price_subtotal, expected, places=2) + + def test_bill_line_edit_keeps_the_other_discounts(self): + """Editing one discount on the bill keeps the other two""" + bill_line = self._create_bill_line() + + bill_line.write({"discount": 50.0, "discount2": 12.0}) + + self.assertEqual(bill_line.discount1, 10.0) + self.assertEqual(bill_line.discount2, 12.0) + self.assertEqual(bill_line.discount3, 2.0) diff --git a/account_invoice_triple_discount_readonly/tests/test_triple_discount_mixin.py b/account_invoice_triple_discount_readonly/tests/test_triple_discount_mixin.py index 33b931f..4e3a48f 100644 --- a/account_invoice_triple_discount_readonly/tests/test_triple_discount_mixin.py +++ b/account_invoice_triple_discount_readonly/tests/test_triple_discount_mixin.py @@ -7,6 +7,11 @@ from odoo.tests.common import TransactionCase @tagged("post_install", "-at_install") class TestTripleDiscountMixin(TransactionCase): + """The patched ``triple.discount.mixin``, through an invoice line. + + Purchase lines do not use this mixin in 18.0 (see test_purchase_order). + """ + @classmethod def setUpClass(cls): super().setUpClass() @@ -30,19 +35,21 @@ class TestTripleDiscountMixin(TransactionCase): # Get the auto-created product variant cls.product = cls.product_template.product_variant_ids[0] - # Create a purchase order - cls.purchase_order = cls.env["purchase.order"].create( + # Create an invoice + cls.invoice = cls.env["account.move"].create( { + "move_type": "out_invoice", "partner_id": cls.partner.id, + "invoice_date": "2026-01-01", } ) - # Create a purchase order line - cls.po_line = cls.env["purchase.order.line"].create( + # Create an invoice line + cls.line = cls.env["account.move.line"].create( { - "order_id": cls.purchase_order.id, + "move_id": cls.invoice.id, "product_id": cls.product.id, - "product_qty": 10, + "quantity": 10, "price_unit": 100.0, "discount1": 10.0, "discount2": 5.0, @@ -52,13 +59,13 @@ class TestTripleDiscountMixin(TransactionCase): def test_discount_field_is_readonly(self): """Test that the discount field is readonly""" - field = self.po_line._fields["discount"] + field = self.line._fields["discount"] self.assertTrue(field.readonly, "Discount field should be readonly") def test_write_with_explicit_discounts(self): """Test writing with explicit discount1, discount2, discount3""" # Write with explicit discounts - self.po_line.write( + self.line.write( { "discount": 20.0, # This should be ignored "discount1": 15.0, @@ -68,18 +75,18 @@ class TestTripleDiscountMixin(TransactionCase): ) # Verify explicit discounts were applied - self.assertEqual(self.po_line.discount1, 15.0) - self.assertEqual(self.po_line.discount2, 10.0) - self.assertEqual(self.po_line.discount3, 5.0) + self.assertEqual(self.line.discount1, 15.0) + self.assertEqual(self.line.discount2, 10.0) + self.assertEqual(self.line.discount3, 5.0) # The computed discount field should reflect the combined discounts # Formula: 100 - (100 * (1 - 0.15) * (1 - 0.10) * (1 - 0.05)) expected_discount = 100 - (100 * 0.85 * 0.90 * 0.95) - self.assertAlmostEqual(self.po_line.discount, expected_discount, places=2) + self.assertAlmostEqual(self.line.discount, expected_discount, places=2) def test_write_only_discount1(self): """Test writing only discount1 explicitly""" - self.po_line.write( + self.line.write( { "discount": 25.0, # This should be ignored "discount1": 20.0, @@ -87,45 +94,43 @@ class TestTripleDiscountMixin(TransactionCase): ) # Only discount1 should change - self.assertEqual(self.po_line.discount1, 20.0) + self.assertEqual(self.line.discount1, 20.0) # Others should remain unchanged - self.assertEqual(self.po_line.discount2, 5.0) - self.assertEqual(self.po_line.discount3, 2.0) + self.assertEqual(self.line.discount2, 5.0) + self.assertEqual(self.line.discount3, 2.0) def test_write_only_discount2(self): """Test writing only discount2 explicitly""" - self.po_line.write( + self.line.write( { "discount": 30.0, # This should be ignored "discount2": 12.0, } ) - # Only discount2 should change - self.assertEqual(self.po_line.discount2, 12.0) - # Others should remain unchanged from previous test - self.assertEqual(self.po_line.discount1, 20.0) - self.assertEqual(self.po_line.discount3, 2.0) + # Only discount2 should change; each test starts from setUpClass + self.assertEqual(self.line.discount2, 12.0) + self.assertEqual(self.line.discount1, 10.0) + self.assertEqual(self.line.discount3, 2.0) def test_write_only_discount3(self): """Test writing only discount3 explicitly""" - self.po_line.write( + self.line.write( { "discount": 35.0, # This should be ignored "discount3": 8.0, } ) - # Only discount3 should change - self.assertEqual(self.po_line.discount3, 8.0) - # Others should remain unchanged from previous tests - self.assertEqual(self.po_line.discount1, 20.0) - self.assertEqual(self.po_line.discount2, 12.0) + # Only discount3 should change; each test starts from setUpClass + self.assertEqual(self.line.discount3, 8.0) + self.assertEqual(self.line.discount1, 10.0) + self.assertEqual(self.line.discount2, 5.0) def test_write_legacy_discount_only(self): """Test legacy behavior: writing only discount field""" # Reset to known state first - self.po_line.write( + self.line.write( { "discount1": 10.0, "discount2": 5.0, @@ -134,32 +139,32 @@ class TestTripleDiscountMixin(TransactionCase): ) # Write only discount (legacy behavior) - self.po_line.write( + self.line.write( { "discount": 25.0, } ) # Should map to discount1 and reset others - self.assertEqual(self.po_line.discount1, 25.0) - self.assertEqual(self.po_line.discount2, 0.0) - self.assertEqual(self.po_line.discount3, 0.0) + self.assertEqual(self.line.discount1, 25.0) + self.assertEqual(self.line.discount2, 0.0) + self.assertEqual(self.line.discount3, 0.0) def test_write_multiple_times(self): """Test writing multiple times to ensure consistency""" # First write - self.po_line.write( + self.line.write( { "discount1": 10.0, "discount2": 10.0, } ) - self.assertEqual(self.po_line.discount1, 10.0) - self.assertEqual(self.po_line.discount2, 10.0) + self.assertEqual(self.line.discount1, 10.0) + self.assertEqual(self.line.discount2, 10.0) # Second write - self.po_line.write( + self.line.write( { "discount": 5.0, "discount3": 5.0, @@ -167,13 +172,13 @@ class TestTripleDiscountMixin(TransactionCase): ) # discount3 should change, others remain - self.assertEqual(self.po_line.discount1, 10.0) - self.assertEqual(self.po_line.discount2, 10.0) - self.assertEqual(self.po_line.discount3, 5.0) + self.assertEqual(self.line.discount1, 10.0) + self.assertEqual(self.line.discount2, 10.0) + self.assertEqual(self.line.discount3, 5.0) def test_write_zero_discounts(self): """Test writing zero discounts""" - self.po_line.write( + self.line.write( { "discount1": 0.0, "discount2": 0.0, @@ -181,15 +186,15 @@ class TestTripleDiscountMixin(TransactionCase): } ) - self.assertEqual(self.po_line.discount1, 0.0) - self.assertEqual(self.po_line.discount2, 0.0) - self.assertEqual(self.po_line.discount3, 0.0) - self.assertEqual(self.po_line.discount, 0.0) + self.assertEqual(self.line.discount1, 0.0) + self.assertEqual(self.line.discount2, 0.0) + self.assertEqual(self.line.discount3, 0.0) + self.assertEqual(self.line.discount, 0.0) def test_write_combined_scenario(self): """Test a realistic combined scenario""" # Initial state - self.po_line.write( + self.line.write( { "discount1": 15.0, "discount2": 5.0, @@ -198,7 +203,7 @@ class TestTripleDiscountMixin(TransactionCase): ) # User tries to update discount field (should be ignored if explicit discounts present) - self.po_line.write( + self.line.write( { "discount": 50.0, "discount1": 20.0, @@ -206,13 +211,13 @@ class TestTripleDiscountMixin(TransactionCase): ) # discount1 should be updated, others unchanged - self.assertEqual(self.po_line.discount1, 20.0) - self.assertEqual(self.po_line.discount2, 5.0) - self.assertEqual(self.po_line.discount3, 0.0) + self.assertEqual(self.line.discount1, 20.0) + self.assertEqual(self.line.discount2, 5.0) + self.assertEqual(self.line.discount3, 0.0) def test_discount_calculation_accuracy(self): """Test that discount calculation is accurate""" - self.po_line.write( + self.line.write( { "discount1": 10.0, "discount2": 10.0, @@ -222,22 +227,22 @@ class TestTripleDiscountMixin(TransactionCase): # Combined discount: 100 - (100 * 0.9 * 0.9 * 0.9) = 27.1 expected = 100 - (100 * 0.9 * 0.9 * 0.9) - self.assertAlmostEqual(self.po_line.discount, expected, places=2) + self.assertAlmostEqual(self.line.discount, expected, places=2) def test_write_without_discount_field(self): """Test writing other fields without touching discount fields""" - initial_discount1 = self.po_line.discount1 + initial_discount1 = self.line.discount1 # Write other fields - self.po_line.write( + self.line.write( { - "product_qty": 20, + "quantity": 20, "price_unit": 150.0, } ) # Discounts should remain unchanged - self.assertEqual(self.po_line.discount1, initial_discount1) + self.assertEqual(self.line.discount1, initial_discount1) # But other fields should be updated - self.assertEqual(self.po_line.product_qty, 20) - self.assertEqual(self.po_line.price_unit, 150.0) + self.assertEqual(self.line.quantity, 20) + self.assertEqual(self.line.price_unit, 150.0)