[FIX] account_invoice_triple_discount_readonly: tests for 18.0 purchases

In 18.0 purchase.order.line uses purchase.triple.discount.mixin, not the
triple.discount.mixin this addon patches, so the purchase tests checked 16.0
behaviour. test_purchase_order now checks the 18.0 one plus what matters
here: the bill created from the PO keeps discount1..3, and editing one of
them on the bill keeps the others. test_triple_discount_mixin runs on an
invoice line, and no longer expects the values of the previous test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
GitHub Copilot 2026-10-04 15:04:54 +02:00
parent 9e05a279d7
commit ca526d55cc
Signed by: snt
GPG key ID: A9FD34930EADBE71
2 changed files with 117 additions and 115 deletions

View file

@ -7,6 +7,16 @@ from odoo.tests.common import TransactionCase
@tagged("post_install", "-at_install") @tagged("post_install", "-at_install")
class TestPurchaseOrder(TransactionCase): 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 @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() 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( cls.product_template = cls.env["product.template"].create(
{ {
"name": "Test Product PO", "name": "Test Product PO",
"type": "consu", "type": "consu",
"list_price": 150.0, "list_price": 150.0,
"standard_price": 80.0, "standard_price": 80.0,
"purchase_method": "purchase",
} }
) )
# Get the auto-created product variant # Get the auto-created product variant
@ -52,16 +64,24 @@ class TestPurchaseOrder(TransactionCase):
} }
) )
def test_po_line_discount_readonly(self): def _create_bill_line(self):
"""Test that discount field is readonly in PO lines""" 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"] 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): def test_po_line_write_with_explicit_discounts(self):
"""Test writing PO line with explicit discounts""" """Test writing PO line with explicit discounts"""
self.po_line.write( self.po_line.write(
{ {
"discount": 25.0, # Should be ignored
"discount1": 12.0, "discount1": 12.0,
"discount2": 8.0, "discount2": 8.0,
"discount3": 4.0, "discount3": 4.0,
@ -71,22 +91,11 @@ class TestPurchaseOrder(TransactionCase):
self.assertEqual(self.po_line.discount1, 12.0) self.assertEqual(self.po_line.discount1, 12.0)
self.assertEqual(self.po_line.discount2, 8.0) self.assertEqual(self.po_line.discount2, 8.0)
self.assertEqual(self.po_line.discount3, 4.0) self.assertEqual(self.po_line.discount3, 4.0)
expected = 100 - (100 * 0.88 * 0.92 * 0.96)
def test_po_line_legacy_discount(self): self.assertAlmostEqual(self.po_line.discount, expected, places=2)
"""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)
def test_po_line_price_calculation(self): 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( self.po_line.write(
{ {
"discount1": 15.0, "discount1": 15.0,
@ -95,11 +104,9 @@ class TestPurchaseOrder(TransactionCase):
} }
) )
# Base: 10 * 150 = 1500 # The PO line keeps the total discount with the "Discount" precision
# After 15% discount: 1275 # (27.33 %, not 27.3325 %), so the subtotal is computed from it.
# After 10% discount: 1147.5 expected_subtotal = 10 * 150 * (1 - self.po_line.discount / 100)
# After 5% discount: 1090.125
expected_subtotal = 10 * 150 * 0.85 * 0.90 * 0.95
self.assertAlmostEqual(self.po_line.price_subtotal, expected_subtotal, places=2) self.assertAlmostEqual(self.po_line.price_subtotal, expected_subtotal, places=2)
def test_multiple_po_lines(self): def test_multiple_po_lines(self):
@ -117,7 +124,7 @@ class TestPurchaseOrder(TransactionCase):
) )
# Verify both lines have correct discounts # 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.discount1, 20.0)
self.assertEqual(line2.discount2, 15.0) self.assertEqual(line2.discount2, 15.0)
self.assertEqual(line2.discount3, 10.0) self.assertEqual(line2.discount3, 10.0)
@ -174,40 +181,8 @@ class TestPurchaseOrder(TransactionCase):
expected = 10 * 150 expected = 10 * 150
self.assertEqual(self.po_line.price_subtotal, expected) 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): def test_po_confirm_with_discounts(self):
"""Test confirming PO doesn't alter discounts""" """Test confirming PO doesn't alter discounts"""
self.po_line.write(
{
"discount1": 10.0,
"discount2": 5.0,
"discount3": 2.0,
}
)
# Confirm the purchase order # Confirm the purchase order
self.purchase_order.button_confirm() 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.discount1, 10.0)
self.assertEqual(self.po_line.discount2, 5.0) self.assertEqual(self.po_line.discount2, 5.0)
self.assertEqual(self.po_line.discount3, 2.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)

View file

@ -7,6 +7,11 @@ from odoo.tests.common import TransactionCase
@tagged("post_install", "-at_install") @tagged("post_install", "-at_install")
class TestTripleDiscountMixin(TransactionCase): 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 @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
@ -30,19 +35,21 @@ class TestTripleDiscountMixin(TransactionCase):
# Get the auto-created product variant # Get the auto-created product variant
cls.product = cls.product_template.product_variant_ids[0] cls.product = cls.product_template.product_variant_ids[0]
# Create a purchase order # Create an invoice
cls.purchase_order = cls.env["purchase.order"].create( cls.invoice = cls.env["account.move"].create(
{ {
"move_type": "out_invoice",
"partner_id": cls.partner.id, "partner_id": cls.partner.id,
"invoice_date": "2026-01-01",
} }
) )
# Create a purchase order line # Create an invoice line
cls.po_line = cls.env["purchase.order.line"].create( 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_id": cls.product.id,
"product_qty": 10, "quantity": 10,
"price_unit": 100.0, "price_unit": 100.0,
"discount1": 10.0, "discount1": 10.0,
"discount2": 5.0, "discount2": 5.0,
@ -52,13 +59,13 @@ class TestTripleDiscountMixin(TransactionCase):
def test_discount_field_is_readonly(self): def test_discount_field_is_readonly(self):
"""Test that the discount field is readonly""" """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") self.assertTrue(field.readonly, "Discount field should be readonly")
def test_write_with_explicit_discounts(self): def test_write_with_explicit_discounts(self):
"""Test writing with explicit discount1, discount2, discount3""" """Test writing with explicit discount1, discount2, discount3"""
# Write with explicit discounts # Write with explicit discounts
self.po_line.write( self.line.write(
{ {
"discount": 20.0, # This should be ignored "discount": 20.0, # This should be ignored
"discount1": 15.0, "discount1": 15.0,
@ -68,18 +75,18 @@ class TestTripleDiscountMixin(TransactionCase):
) )
# Verify explicit discounts were applied # Verify explicit discounts were applied
self.assertEqual(self.po_line.discount1, 15.0) self.assertEqual(self.line.discount1, 15.0)
self.assertEqual(self.po_line.discount2, 10.0) self.assertEqual(self.line.discount2, 10.0)
self.assertEqual(self.po_line.discount3, 5.0) self.assertEqual(self.line.discount3, 5.0)
# The computed discount field should reflect the combined discounts # The computed discount field should reflect the combined discounts
# Formula: 100 - (100 * (1 - 0.15) * (1 - 0.10) * (1 - 0.05)) # Formula: 100 - (100 * (1 - 0.15) * (1 - 0.10) * (1 - 0.05))
expected_discount = 100 - (100 * 0.85 * 0.90 * 0.95) 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): def test_write_only_discount1(self):
"""Test writing only discount1 explicitly""" """Test writing only discount1 explicitly"""
self.po_line.write( self.line.write(
{ {
"discount": 25.0, # This should be ignored "discount": 25.0, # This should be ignored
"discount1": 20.0, "discount1": 20.0,
@ -87,45 +94,43 @@ class TestTripleDiscountMixin(TransactionCase):
) )
# Only discount1 should change # Only discount1 should change
self.assertEqual(self.po_line.discount1, 20.0) self.assertEqual(self.line.discount1, 20.0)
# Others should remain unchanged # Others should remain unchanged
self.assertEqual(self.po_line.discount2, 5.0) self.assertEqual(self.line.discount2, 5.0)
self.assertEqual(self.po_line.discount3, 2.0) self.assertEqual(self.line.discount3, 2.0)
def test_write_only_discount2(self): def test_write_only_discount2(self):
"""Test writing only discount2 explicitly""" """Test writing only discount2 explicitly"""
self.po_line.write( self.line.write(
{ {
"discount": 30.0, # This should be ignored "discount": 30.0, # This should be ignored
"discount2": 12.0, "discount2": 12.0,
} }
) )
# Only discount2 should change # Only discount2 should change; each test starts from setUpClass
self.assertEqual(self.po_line.discount2, 12.0) self.assertEqual(self.line.discount2, 12.0)
# Others should remain unchanged from previous test self.assertEqual(self.line.discount1, 10.0)
self.assertEqual(self.po_line.discount1, 20.0) self.assertEqual(self.line.discount3, 2.0)
self.assertEqual(self.po_line.discount3, 2.0)
def test_write_only_discount3(self): def test_write_only_discount3(self):
"""Test writing only discount3 explicitly""" """Test writing only discount3 explicitly"""
self.po_line.write( self.line.write(
{ {
"discount": 35.0, # This should be ignored "discount": 35.0, # This should be ignored
"discount3": 8.0, "discount3": 8.0,
} }
) )
# Only discount3 should change # Only discount3 should change; each test starts from setUpClass
self.assertEqual(self.po_line.discount3, 8.0) self.assertEqual(self.line.discount3, 8.0)
# Others should remain unchanged from previous tests self.assertEqual(self.line.discount1, 10.0)
self.assertEqual(self.po_line.discount1, 20.0) self.assertEqual(self.line.discount2, 5.0)
self.assertEqual(self.po_line.discount2, 12.0)
def test_write_legacy_discount_only(self): def test_write_legacy_discount_only(self):
"""Test legacy behavior: writing only discount field""" """Test legacy behavior: writing only discount field"""
# Reset to known state first # Reset to known state first
self.po_line.write( self.line.write(
{ {
"discount1": 10.0, "discount1": 10.0,
"discount2": 5.0, "discount2": 5.0,
@ -134,32 +139,32 @@ class TestTripleDiscountMixin(TransactionCase):
) )
# Write only discount (legacy behavior) # Write only discount (legacy behavior)
self.po_line.write( self.line.write(
{ {
"discount": 25.0, "discount": 25.0,
} }
) )
# Should map to discount1 and reset others # Should map to discount1 and reset others
self.assertEqual(self.po_line.discount1, 25.0) self.assertEqual(self.line.discount1, 25.0)
self.assertEqual(self.po_line.discount2, 0.0) self.assertEqual(self.line.discount2, 0.0)
self.assertEqual(self.po_line.discount3, 0.0) self.assertEqual(self.line.discount3, 0.0)
def test_write_multiple_times(self): def test_write_multiple_times(self):
"""Test writing multiple times to ensure consistency""" """Test writing multiple times to ensure consistency"""
# First write # First write
self.po_line.write( self.line.write(
{ {
"discount1": 10.0, "discount1": 10.0,
"discount2": 10.0, "discount2": 10.0,
} }
) )
self.assertEqual(self.po_line.discount1, 10.0) self.assertEqual(self.line.discount1, 10.0)
self.assertEqual(self.po_line.discount2, 10.0) self.assertEqual(self.line.discount2, 10.0)
# Second write # Second write
self.po_line.write( self.line.write(
{ {
"discount": 5.0, "discount": 5.0,
"discount3": 5.0, "discount3": 5.0,
@ -167,13 +172,13 @@ class TestTripleDiscountMixin(TransactionCase):
) )
# discount3 should change, others remain # discount3 should change, others remain
self.assertEqual(self.po_line.discount1, 10.0) self.assertEqual(self.line.discount1, 10.0)
self.assertEqual(self.po_line.discount2, 10.0) self.assertEqual(self.line.discount2, 10.0)
self.assertEqual(self.po_line.discount3, 5.0) self.assertEqual(self.line.discount3, 5.0)
def test_write_zero_discounts(self): def test_write_zero_discounts(self):
"""Test writing zero discounts""" """Test writing zero discounts"""
self.po_line.write( self.line.write(
{ {
"discount1": 0.0, "discount1": 0.0,
"discount2": 0.0, "discount2": 0.0,
@ -181,15 +186,15 @@ class TestTripleDiscountMixin(TransactionCase):
} }
) )
self.assertEqual(self.po_line.discount1, 0.0) self.assertEqual(self.line.discount1, 0.0)
self.assertEqual(self.po_line.discount2, 0.0) self.assertEqual(self.line.discount2, 0.0)
self.assertEqual(self.po_line.discount3, 0.0) self.assertEqual(self.line.discount3, 0.0)
self.assertEqual(self.po_line.discount, 0.0) self.assertEqual(self.line.discount, 0.0)
def test_write_combined_scenario(self): def test_write_combined_scenario(self):
"""Test a realistic combined scenario""" """Test a realistic combined scenario"""
# Initial state # Initial state
self.po_line.write( self.line.write(
{ {
"discount1": 15.0, "discount1": 15.0,
"discount2": 5.0, "discount2": 5.0,
@ -198,7 +203,7 @@ class TestTripleDiscountMixin(TransactionCase):
) )
# User tries to update discount field (should be ignored if explicit discounts present) # User tries to update discount field (should be ignored if explicit discounts present)
self.po_line.write( self.line.write(
{ {
"discount": 50.0, "discount": 50.0,
"discount1": 20.0, "discount1": 20.0,
@ -206,13 +211,13 @@ class TestTripleDiscountMixin(TransactionCase):
) )
# discount1 should be updated, others unchanged # discount1 should be updated, others unchanged
self.assertEqual(self.po_line.discount1, 20.0) self.assertEqual(self.line.discount1, 20.0)
self.assertEqual(self.po_line.discount2, 5.0) self.assertEqual(self.line.discount2, 5.0)
self.assertEqual(self.po_line.discount3, 0.0) self.assertEqual(self.line.discount3, 0.0)
def test_discount_calculation_accuracy(self): def test_discount_calculation_accuracy(self):
"""Test that discount calculation is accurate""" """Test that discount calculation is accurate"""
self.po_line.write( self.line.write(
{ {
"discount1": 10.0, "discount1": 10.0,
"discount2": 10.0, "discount2": 10.0,
@ -222,22 +227,22 @@ class TestTripleDiscountMixin(TransactionCase):
# Combined discount: 100 - (100 * 0.9 * 0.9 * 0.9) = 27.1 # Combined discount: 100 - (100 * 0.9 * 0.9 * 0.9) = 27.1
expected = 100 - (100 * 0.9 * 0.9 * 0.9) 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): def test_write_without_discount_field(self):
"""Test writing other fields without touching discount fields""" """Test writing other fields without touching discount fields"""
initial_discount1 = self.po_line.discount1 initial_discount1 = self.line.discount1
# Write other fields # Write other fields
self.po_line.write( self.line.write(
{ {
"product_qty": 20, "quantity": 20,
"price_unit": 150.0, "price_unit": 150.0,
} }
) )
# Discounts should remain unchanged # 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 # But other fields should be updated
self.assertEqual(self.po_line.product_qty, 20) self.assertEqual(self.line.quantity, 20)
self.assertEqual(self.po_line.price_unit, 150.0) self.assertEqual(self.line.price_unit, 150.0)