[ADD] ci: CI de Forgejo, bots y pre-commit en todo el repo #19

Merged
snt merged 7 commits from 18.0-ci-forgejo-bots into 18.0 2026-10-04 17:42:04 +00:00
2 changed files with 117 additions and 115 deletions
Showing only changes of commit ca526d55cc - Show all commits

View file

@ -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)

View file

@ -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)