diff --git a/account_invoice_triple_discount_readonly/__manifest__.py b/account_invoice_triple_discount_readonly/__manifest__.py index a9ce583..bdd4c26 100644 --- a/account_invoice_triple_discount_readonly/__manifest__.py +++ b/account_invoice_triple_discount_readonly/__manifest__.py @@ -2,17 +2,12 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { "name": "Account Invoice Triple Discount Readonly", - "version": "16.0.1.0.0", + "version": "18.0.1.0.0", "summary": "Make total discount readonly and fix discount2/discount3 write issue", "license": "AGPL-3", "author": "Criptomart", "website": "https://github.com/OCA/account-invoicing", "depends": ["account_invoice_triple_discount", "purchase_triple_discount"], - "data": [ - "views/product_supplierinfo_view.xml", - "views/res_partner_view.xml", - "views/purchase_order_view.xml", - "views/account_move_view.xml", - ], + "data": [], "installable": True, } diff --git a/account_invoice_triple_discount_readonly/oca_dependencies.txt b/account_invoice_triple_discount_readonly/oca_dependencies.txt new file mode 100644 index 0000000..fece1fc --- /dev/null +++ b/account_invoice_triple_discount_readonly/oca_dependencies.txt @@ -0,0 +1,5 @@ +# OCA Dependencies for account_invoice_triple_discount_readonly +# Format: repository_name branch + +account-invoicing https://github.com/OCA/account-invoicing.git 18.0 +purchase-workflow https://github.com/OCA/purchase-workflow.git 18.0 diff --git a/account_invoice_triple_discount_readonly/tests/__init__.py b/account_invoice_triple_discount_readonly/tests/__init__.py new file mode 100644 index 0000000..4c7cb7d --- /dev/null +++ b/account_invoice_triple_discount_readonly/tests/__init__.py @@ -0,0 +1,3 @@ +from . import test_triple_discount_mixin +from . import test_account_move +from . import test_purchase_order diff --git a/account_invoice_triple_discount_readonly/tests/test_account_move.py b/account_invoice_triple_discount_readonly/tests/test_account_move.py new file mode 100644 index 0000000..4b86fa5 --- /dev/null +++ b/account_invoice_triple_discount_readonly/tests/test_account_move.py @@ -0,0 +1,187 @@ +# Copyright (C) 2025: Criptomart (https://criptomart.net) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + + +@tagged("post_install", "-at_install") +class TestAccountMove(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + # Create a partner + cls.partner = cls.env["res.partner"].create({ + "name": "Test Customer", + "email": "customer@test.com", + }) + + # Create a product + cls.product = cls.env["product.product"].create({ + "name": "Test Product Invoice", + "type": "consu", + "list_price": 200.0, + "standard_price": 100.0, + }) + + # Create tax + cls.tax = cls.env["account.tax"].create({ + "name": "Test Tax 10%", + "amount": 10.0, + "amount_type": "percent", + "type_tax_use": "sale", + }) + + # 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 invoice line + cls.invoice_line = cls.env["account.move.line"].create({ + "move_id": cls.invoice.id, + "product_id": cls.product.id, + "quantity": 5, + "price_unit": 200.0, + "discount1": 10.0, + "discount2": 5.0, + "discount3": 2.0, + "tax_ids": [(6, 0, [cls.tax.id])], + }) + + def test_invoice_line_discount_readonly(self): + """Test that discount field is readonly in invoice lines""" + field = self.invoice_line._fields["discount"] + self.assertTrue(field.readonly, "Discount field should be readonly in invoice lines") + + def test_invoice_line_write_with_explicit_discounts(self): + """Test writing invoice line with explicit discounts""" + self.invoice_line.write({ + "discount": 30.0, # Should be ignored + "discount1": 15.0, + "discount2": 10.0, + "discount3": 5.0, + }) + + self.assertEqual(self.invoice_line.discount1, 15.0) + self.assertEqual(self.invoice_line.discount2, 10.0) + self.assertEqual(self.invoice_line.discount3, 5.0) + + def test_invoice_line_legacy_discount(self): + """Test legacy discount behavior in invoice lines""" + self.invoice_line.write({ + "discount": 20.0, + }) + + # Should map to discount1 and reset others + self.assertEqual(self.invoice_line.discount1, 20.0) + self.assertEqual(self.invoice_line.discount2, 0.0) + self.assertEqual(self.invoice_line.discount3, 0.0) + + def test_invoice_line_price_calculation(self): + """Test that price subtotal is calculated correctly with triple discount""" + self.invoice_line.write({ + "discount1": 10.0, + "discount2": 5.0, + "discount3": 0.0, + }) + + # Base: 5 * 200 = 1000 + # After 10% discount: 900 + # After 5% discount: 855 + expected_subtotal = 5 * 200 * 0.9 * 0.95 + self.assertAlmostEqual( + self.invoice_line.price_subtotal, expected_subtotal, places=2 + ) + + def test_multiple_invoice_lines(self): + """Test multiple invoice lines with different discounts""" + line2 = self.env["account.move.line"].create({ + "move_id": self.invoice.id, + "product_id": self.product.id, + "quantity": 3, + "price_unit": 150.0, + "discount1": 20.0, + "discount2": 10.0, + "discount3": 5.0, + "tax_ids": [(6, 0, [self.tax.id])], + }) + + # Verify both lines have correct discounts + self.assertEqual(self.invoice_line.discount1, 10.0) + self.assertEqual(line2.discount1, 20.0) + self.assertEqual(line2.discount2, 10.0) + self.assertEqual(line2.discount3, 5.0) + + def test_invoice_line_update_quantity(self): + """Test updating quantity doesn't affect discounts""" + initial_discount1 = self.invoice_line.discount1 + initial_discount2 = self.invoice_line.discount2 + + self.invoice_line.write({ + "quantity": 10, + }) + + # Discounts should remain unchanged + self.assertEqual(self.invoice_line.discount1, initial_discount1) + self.assertEqual(self.invoice_line.discount2, initial_discount2) + # Quantity should be updated + self.assertEqual(self.invoice_line.quantity, 10) + + def test_invoice_line_update_price(self): + """Test updating price doesn't affect discounts""" + initial_discount1 = self.invoice_line.discount1 + + self.invoice_line.write({ + "price_unit": 250.0, + }) + + # Discount should remain unchanged + self.assertEqual(self.invoice_line.discount1, initial_discount1) + # Price should be updated + self.assertEqual(self.invoice_line.price_unit, 250.0) + + def test_invoice_with_zero_discounts(self): + """Test invoice line with all zero discounts""" + self.invoice_line.write({ + "discount1": 0.0, + "discount2": 0.0, + "discount3": 0.0, + }) + + # All discounts should be zero + self.assertEqual(self.invoice_line.discount, 0.0) + self.assertEqual(self.invoice_line.discount1, 0.0) + self.assertEqual(self.invoice_line.discount2, 0.0) + self.assertEqual(self.invoice_line.discount3, 0.0) + + # Subtotal should be quantity * price + expected = 5 * 200 + self.assertEqual(self.invoice_line.price_subtotal, expected) + + def test_invoice_line_combined_operations(self): + """Test combined operations on invoice line""" + # Update multiple fields at once + self.invoice_line.write({ + "quantity": 8, + "price_unit": 180.0, + "discount1": 12.0, + "discount2": 6.0, + "discount3": 0.0, # Reset discount3 explicitly + }) + + # All fields should be updated correctly + self.assertEqual(self.invoice_line.quantity, 8) + self.assertEqual(self.invoice_line.price_unit, 180.0) + self.assertEqual(self.invoice_line.discount1, 12.0) + self.assertEqual(self.invoice_line.discount2, 6.0) + self.assertEqual(self.invoice_line.discount3, 0.0) + + # Calculate expected subtotal: 8 * 180 * (1-0.12) * (1-0.06) + expected = 8 * 180 * 0.88 * 0.94 + self.assertAlmostEqual( + self.invoice_line.price_subtotal, expected, places=2 + ) diff --git a/account_invoice_triple_discount_readonly/tests/test_purchase_order.py b/account_invoice_triple_discount_readonly/tests/test_purchase_order.py new file mode 100644 index 0000000..fc22dd0 --- /dev/null +++ b/account_invoice_triple_discount_readonly/tests/test_purchase_order.py @@ -0,0 +1,193 @@ +# Copyright (C) 2025: Criptomart (https://criptomart.net) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + + +@tagged("post_install", "-at_install") +class TestPurchaseOrder(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + # Create a supplier + cls.supplier = cls.env["res.partner"].create({ + "name": "Test Supplier", + "email": "supplier@test.com", + "supplier_rank": 1, + }) + + # Create a product + cls.product = cls.env["product.product"].create({ + "name": "Test Product PO", + "type": "product", + "list_price": 150.0, + "standard_price": 80.0, + }) + + # Create a purchase order + cls.purchase_order = cls.env["purchase.order"].create({ + "partner_id": cls.supplier.id, + }) + + # Create purchase order line + cls.po_line = cls.env["purchase.order.line"].create({ + "order_id": cls.purchase_order.id, + "product_id": cls.product.id, + "product_qty": 10, + "price_unit": 150.0, + "discount1": 10.0, + "discount2": 5.0, + "discount3": 2.0, + }) + + def test_po_line_discount_readonly(self): + """Test that discount field is readonly in PO lines""" + field = self.po_line._fields["discount"] + self.assertTrue(field.readonly, "Discount field should be readonly in PO lines") + + 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, + }) + + 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) + + def test_po_line_price_calculation(self): + """Test that price subtotal is calculated correctly with triple discount""" + self.po_line.write({ + "discount1": 15.0, + "discount2": 10.0, + "discount3": 5.0, + }) + + # 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 + self.assertAlmostEqual( + self.po_line.price_subtotal, expected_subtotal, places=2 + ) + + def test_multiple_po_lines(self): + """Test multiple PO lines with different discounts""" + line2 = self.env["purchase.order.line"].create({ + "order_id": self.purchase_order.id, + "product_id": self.product.id, + "product_qty": 5, + "price_unit": 120.0, + "discount1": 20.0, + "discount2": 15.0, + "discount3": 10.0, + }) + + # Verify both lines have correct discounts + self.assertEqual(self.po_line.discount1, 15.0) + self.assertEqual(line2.discount1, 20.0) + self.assertEqual(line2.discount2, 15.0) + self.assertEqual(line2.discount3, 10.0) + + def test_po_line_update_quantity(self): + """Test updating quantity doesn't affect discounts""" + initial_discount1 = self.po_line.discount1 + initial_discount2 = self.po_line.discount2 + + self.po_line.write({ + "product_qty": 20, + }) + + # Discounts should remain unchanged + self.assertEqual(self.po_line.discount1, initial_discount1) + self.assertEqual(self.po_line.discount2, initial_discount2) + # Quantity should be updated + self.assertEqual(self.po_line.product_qty, 20) + + def test_po_line_update_price(self): + """Test updating price doesn't affect discounts""" + initial_discount1 = self.po_line.discount1 + + self.po_line.write({ + "price_unit": 200.0, + }) + + # Discount should remain unchanged + self.assertEqual(self.po_line.discount1, initial_discount1) + # Price should be updated + self.assertEqual(self.po_line.price_unit, 200.0) + + def test_po_with_zero_discounts(self): + """Test PO line with all zero discounts""" + self.po_line.write({ + "discount1": 0.0, + "discount2": 0.0, + "discount3": 0.0, + }) + + # All discounts should be zero + self.assertEqual(self.po_line.discount, 0.0) + self.assertEqual(self.po_line.discount1, 0.0) + self.assertEqual(self.po_line.discount2, 0.0) + self.assertEqual(self.po_line.discount3, 0.0) + + # Subtotal should be quantity * price + 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() + + # Discounts should remain unchanged after confirmation + self.assertEqual(self.po_line.discount1, 10.0) + self.assertEqual(self.po_line.discount2, 5.0) + self.assertEqual(self.po_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 new file mode 100644 index 0000000..f4600a2 --- /dev/null +++ b/account_invoice_triple_discount_readonly/tests/test_triple_discount_mixin.py @@ -0,0 +1,207 @@ +# Copyright (C) 2025: Criptomart (https://criptomart.net) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + + +@tagged("post_install", "-at_install") +class TestTripleDiscountMixin(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + # Create a partner + cls.partner = cls.env["res.partner"].create({ + "name": "Test Partner", + }) + + # Create a product + cls.product = cls.env["product.product"].create({ + "name": "Test Product", + "type": "product", + "list_price": 100.0, + "standard_price": 50.0, + }) + + # Create a purchase order + cls.purchase_order = cls.env["purchase.order"].create({ + "partner_id": cls.partner.id, + }) + + # Create a purchase order line + cls.po_line = cls.env["purchase.order.line"].create({ + "order_id": cls.purchase_order.id, + "product_id": cls.product.id, + "product_qty": 10, + "price_unit": 100.0, + "discount1": 10.0, + "discount2": 5.0, + "discount3": 2.0, + }) + + def test_discount_field_is_readonly(self): + """Test that the discount field is readonly""" + field = self.po_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({ + "discount": 20.0, # This should be ignored + "discount1": 15.0, + "discount2": 10.0, + "discount3": 5.0, + }) + + # 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) + + # 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) + + def test_write_only_discount1(self): + """Test writing only discount1 explicitly""" + self.po_line.write({ + "discount": 25.0, # This should be ignored + "discount1": 20.0, + }) + + # Only discount1 should change + self.assertEqual(self.po_line.discount1, 20.0) + # Others should remain unchanged + self.assertEqual(self.po_line.discount2, 5.0) + self.assertEqual(self.po_line.discount3, 2.0) + + def test_write_only_discount2(self): + """Test writing only discount2 explicitly""" + self.po_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) + + def test_write_only_discount3(self): + """Test writing only discount3 explicitly""" + self.po_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) + + def test_write_legacy_discount_only(self): + """Test legacy behavior: writing only discount field""" + # Reset to known state first + self.po_line.write({ + "discount1": 10.0, + "discount2": 5.0, + "discount3": 2.0, + }) + + # Write only discount (legacy behavior) + self.po_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) + + def test_write_multiple_times(self): + """Test writing multiple times to ensure consistency""" + # First write + self.po_line.write({ + "discount1": 10.0, + "discount2": 10.0, + }) + + self.assertEqual(self.po_line.discount1, 10.0) + self.assertEqual(self.po_line.discount2, 10.0) + + # Second write + self.po_line.write({ + "discount": 5.0, + "discount3": 5.0, + }) + + # 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) + + def test_write_zero_discounts(self): + """Test writing zero discounts""" + self.po_line.write({ + "discount1": 0.0, + "discount2": 0.0, + "discount3": 0.0, + }) + + 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) + + def test_write_combined_scenario(self): + """Test a realistic combined scenario""" + # Initial state + self.po_line.write({ + "discount1": 15.0, + "discount2": 5.0, + "discount3": 0.0, + }) + + # User tries to update discount field (should be ignored if explicit discounts present) + self.po_line.write({ + "discount": 50.0, + "discount1": 20.0, + }) + + # 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) + + def test_discount_calculation_accuracy(self): + """Test that discount calculation is accurate""" + self.po_line.write({ + "discount1": 10.0, + "discount2": 10.0, + "discount3": 10.0, + }) + + # 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) + + def test_write_without_discount_field(self): + """Test writing other fields without touching discount fields""" + initial_discount1 = self.po_line.discount1 + + # Write other fields + self.po_line.write({ + "product_qty": 20, + "price_unit": 150.0, + }) + + # Discounts should remain unchanged + self.assertEqual(self.po_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) diff --git a/account_invoice_triple_discount_readonly/views/account_move_view.xml b/account_invoice_triple_discount_readonly/views/account_move_view.xml index b44c4d2..c3e1fa0 100644 --- a/account_invoice_triple_discount_readonly/views/account_move_view.xml +++ b/account_invoice_triple_discount_readonly/views/account_move_view.xml @@ -1,7 +1,7 @@ - + account.invoice.triple.discount.readonly account.move @@ -10,12 +10,7 @@ ref="account_invoice_triple_discount.invoice_triple_discount_form_view" /> - - 1 - + - - - product.supplierinfo - - - - 1 - - - - - - - product.supplierinfo - - - - 1 - - - + diff --git a/account_invoice_triple_discount_readonly/views/purchase_order_view.xml b/account_invoice_triple_discount_readonly/views/purchase_order_view.xml index c082397..6ea6258 100644 --- a/account_invoice_triple_discount_readonly/views/purchase_order_view.xml +++ b/account_invoice_triple_discount_readonly/views/purchase_order_view.xml @@ -1,28 +1,6 @@ - - - purchase.order.triple.discount.readonly - purchase.order - - - - 1 - - - 1 - - - + diff --git a/account_invoice_triple_discount_readonly/views/res_partner_view.xml b/account_invoice_triple_discount_readonly/views/res_partner_view.xml index 8a18830..20426de 100644 --- a/account_invoice_triple_discount_readonly/views/res_partner_view.xml +++ b/account_invoice_triple_discount_readonly/views/res_partner_view.xml @@ -1,18 +1,6 @@ - - - res.partner - - - - 1 - - - +