diff --git a/account_invoice_triple_discount_readonly/__manifest__.py b/account_invoice_triple_discount_readonly/__manifest__.py index bdd4c26..a9ce583 100644 --- a/account_invoice_triple_discount_readonly/__manifest__.py +++ b/account_invoice_triple_discount_readonly/__manifest__.py @@ -2,12 +2,17 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { "name": "Account Invoice Triple Discount Readonly", - "version": "18.0.1.0.0", + "version": "16.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": [], + "data": [ + "views/product_supplierinfo_view.xml", + "views/res_partner_view.xml", + "views/purchase_order_view.xml", + "views/account_move_view.xml", + ], "installable": True, } diff --git a/account_invoice_triple_discount_readonly/oca_dependencies.txt b/account_invoice_triple_discount_readonly/oca_dependencies.txt deleted file mode 100644 index fece1fc..0000000 --- a/account_invoice_triple_discount_readonly/oca_dependencies.txt +++ /dev/null @@ -1,5 +0,0 @@ -# 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 deleted file mode 100644 index 4c7cb7d..0000000 --- a/account_invoice_triple_discount_readonly/tests/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 4b86fa5..0000000 --- a/account_invoice_triple_discount_readonly/tests/test_account_move.py +++ /dev/null @@ -1,187 +0,0 @@ -# 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 deleted file mode 100644 index fc22dd0..0000000 --- a/account_invoice_triple_discount_readonly/tests/test_purchase_order.py +++ /dev/null @@ -1,193 +0,0 @@ -# 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 deleted file mode 100644 index f4600a2..0000000 --- a/account_invoice_triple_discount_readonly/tests/test_triple_discount_mixin.py +++ /dev/null @@ -1,207 +0,0 @@ -# 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 c3e1fa0..b44c4d2 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,7 +10,12 @@ 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 6ea6258..c082397 100644 --- a/account_invoice_triple_discount_readonly/views/purchase_order_view.xml +++ b/account_invoice_triple_discount_readonly/views/purchase_order_view.xml @@ -1,6 +1,28 @@ - + + + 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 20426de..8a18830 100644 --- a/account_invoice_triple_discount_readonly/views/res_partner_view.xml +++ b/account_invoice_triple_discount_readonly/views/res_partner_view.xml @@ -1,6 +1,18 @@ - + + + res.partner + + + + 1 + + + diff --git a/product_sale_price_from_pricelist/__manifest__.py b/product_sale_price_from_pricelist/__manifest__.py index 3a2f170..66ed993 100644 --- a/product_sale_price_from_pricelist/__manifest__.py +++ b/product_sale_price_from_pricelist/__manifest__.py @@ -1,6 +1,6 @@ { "name": "Product Sale Price from Pricelist", - "version": "18.0.1.0.0", + "version": "16.0.1.0.0", "category": "product", "summary": "Set sale price from pricelist based on last purchase price", "description": """ @@ -11,11 +11,11 @@ "license": "AGPL-3", "depends": [ "product_get_price_helper", - "account_invoice_triple_discount_readonly", - "sale_management", + "sale", "purchase", "account", "stock_account", + "product_template_tags", ], "data": [ "views/actions.xml", diff --git a/product_sale_price_from_pricelist/models/product_pricelist.py b/product_sale_price_from_pricelist/models/product_pricelist.py index 6668aad..72400e4 100644 --- a/product_sale_price_from_pricelist/models/product_pricelist.py +++ b/product_sale_price_from_pricelist/models/product_pricelist.py @@ -2,39 +2,25 @@ # @author Santi NoreƱa () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -import logging +# import logging from odoo import api, models -_logger = logging.getLogger(__name__) +# _logger = logging.getLogger(__name__) class ProductPricelist(models.Model): _inherit = "product.pricelist" - def _compute_price_rule(self, products, quantity, uom=None, date=False, **kwargs): + def _compute_price_rule(self, products, qty, uom=None, date=False, **kwargs): ProductPricelistItem = self.env["product.pricelist.item"] ProductProduct = self.env["product.product"] - - _logger.info( - "[PRICELIST DEBUG] _compute_price_rule called with products=%s, quantity=%s", - products.ids, - quantity, - ) - res = super()._compute_price_rule( products, - quantity, + qty=1, uom=uom, date=date, - **kwargs ) - - _logger.info( - "[PRICELIST DEBUG] super()._compute_price_rule returned: %s", - res, - ) - new_res = res.copy() item_id = [] for product_id, values in res.items(): @@ -42,26 +28,10 @@ class ProductPricelist(models.Model): item_id = values[1] if item_id: item = ProductPricelistItem.browse(item_id) - _logger.info( - "[PRICELIST DEBUG] Product %s: item.base=%s, item_id=%s", - product_id, - item.base, - item_id, - ) if item.base == "last_purchase_price": - product = ProductProduct.browse(product_id) - price = product.last_purchase_price_received - _logger.info( - "[PRICELIST DEBUG] Product %s: last_purchase_price_received=%s, item.price_discount=%s", - product_id, - price, - item.price_discount, - ) + price = ProductProduct.browse( + product_id + ).last_purchase_price_received price = (price - (price * (item.price_discount / 100))) or 0.0 new_res[product_id] = (price, item_id) - _logger.info( - "[PRICELIST DEBUG] Product %s: calculated price=%s", - product_id, - price, - ) return new_res diff --git a/product_sale_price_from_pricelist/models/product_pricelist_item.py b/product_sale_price_from_pricelist/models/product_pricelist_item.py index ba2e9ad..2b6581d 100644 --- a/product_sale_price_from_pricelist/models/product_pricelist_item.py +++ b/product_sale_price_from_pricelist/models/product_pricelist_item.py @@ -13,8 +13,8 @@ class ProductPricelistItem(models.Model): ondelete={"last_purchase_price": "set default"}, ) - def _compute_price(self, product, qty, uom, date, currency=None): - result = super()._compute_price(product, qty, uom, date, currency) + def _compute_price(self, product, quantity, uom, date, currency=None): + result = super()._compute_price(product, quantity, uom, date, currency) if self.compute_price == "formula" and self.base == "last_purchase_price": result = product.sudo().last_purchase_price_received return result diff --git a/product_sale_price_from_pricelist/models/product_product.py b/product_sale_price_from_pricelist/models/product_product.py index 425a8a2..f867604 100644 --- a/product_sale_price_from_pricelist/models/product_product.py +++ b/product_sale_price_from_pricelist/models/product_product.py @@ -1,16 +1,9 @@ -from odoo import fields, models +from odoo import models class ProductProduct(models.Model): _inherit = "product.product" - # Related field for pricelist base computation - last_purchase_price = fields.Float( - related="product_tmpl_id.last_purchase_price_received", - string="Last Purchase Price", - readonly=True, - ) - def price_compute( self, price_type, uom=None, currency=None, company=None, date=False ): diff --git a/product_sale_price_from_pricelist/models/product_template.py b/product_sale_price_from_pricelist/models/product_template.py index 79c0720..ad79bd0 100644 --- a/product_sale_price_from_pricelist/models/product_template.py +++ b/product_sale_price_from_pricelist/models/product_template.py @@ -5,11 +5,8 @@ from odoo import exceptions, models, fields, api, _ from odoo.exceptions import UserError -import logging import math -_logger = logging.getLogger(__name__) - class ProductTemplate(models.Model): _inherit = "product.template" @@ -45,6 +42,7 @@ class ProductTemplate(models.Model): '* Triple discount: take into account all discounts when updating the last purchase price. Needs "Purchase Triple Discount" OCA module.\n' "* Manual update: Select this for manual configuration of cost and sale price. The sales price will not be calculated automatically.", default="without_discounts", + required=True, company_dependent=True, ) @@ -59,15 +57,6 @@ class ProductTemplate(models.Model): pricelist = pricelist_obj.browse(int(pricelist_id)) if pricelist: for template in self: - _logger.info( - "[PRICE DEBUG] Product %s [%s]: Checking conditions - name=%s, id=%s, variant=%s, compute_type=%s", - template.default_code or template.name, - template.id, - bool(template.name), - bool(template.id), - bool(template.product_variant_id), - template.last_purchase_price_compute_type, - ) if ( template.name and template.id @@ -77,14 +66,6 @@ class ProductTemplate(models.Model): partial_price = template.product_variant_id._get_price( qty=1, pricelist=pricelist ) - - _logger.info( - "[PRICE DEBUG] Product %s [%s]: partial_price result = %s", - template.default_code or template.name, - template.id, - partial_price, - ) - # Compute taxes to add if not template.taxes_id: raise UserError( @@ -93,33 +74,23 @@ class ProductTemplate(models.Model): ) % template.name ) - - base_price = partial_price.get("value", 0.0) or 0.0 - _logger.info( - "[PRICE DEBUG] Product %s [%s]: base_price from pricelist = %.2f, last_purchase_price = %.2f", - template.default_code or template.name, - template.id, - base_price, - template.last_purchase_price_received, + tax_price = template.taxes_id.compute_all( + partial_price[template.product_variant_id.id]["value"] or 0.0, + handle_price_include=False, ) - - # Use base price without taxes (taxes will be calculated automatically on sales) - theoretical_price = base_price - - _logger.info( - "[PRICE] Product %s [%s]: Computed theoretical price %.2f (previous: %.2f, current list_price: %.2f)", - template.default_code or template.name, - template.id, - theoretical_price, - template.list_price_theoritical, - template.list_price, + price_with_taxes = ( + tax_price["taxes"][0]["amount"] + + partial_price[template.product_variant_id.id]["value"] ) + # Round to 0.05 + if round(price_with_taxes % 0.05, 2) != 0: + price_with_taxes = round(price_with_taxes * 20) / 20 template.write( { - "list_price_theoritical": theoretical_price, + "list_price_theoritical": price_with_taxes, "last_purchase_price_updated": ( - theoretical_price != template.list_price + price_with_taxes != template.list_price ), } ) @@ -133,19 +104,8 @@ class ProductTemplate(models.Model): def action_update_list_price(self): for template in self: if template.last_purchase_price_compute_type != "manual_update": - # First compute the theoretical price - template._compute_theoritical_price() - - old_price = template.list_price template.list_price = template.list_price_theoritical template.last_purchase_price_updated = False - _logger.info( - "[PRICE] Product %s [%s]: List price updated from %.2f to %.2f", - template.default_code or template.name, - template.id, - old_price, - template.list_price, - ) def price_compute( self, price_type, uom=None, currency=None, company=False, date=False diff --git a/product_sale_price_from_pricelist/models/stock_move.py b/product_sale_price_from_pricelist/models/stock_move.py index abbaaf5..9865db0 100644 --- a/product_sale_price_from_pricelist/models/stock_move.py +++ b/product_sale_price_from_pricelist/models/stock_move.py @@ -59,16 +59,14 @@ class StockMove(models.Model): move.product_id.last_purchase_price_received, price_updated, precision_digits=2, - ) and not float_is_zero(move.quantity, precision_digits=3): + ) and not float_is_zero(move.quantity_done, precision_digits=3): _logger.info( - "[PRICE] Product %s [%s]: Purchase price updated from %.2f to %.2f (Move: %s, PO: %s, compute_type: %s)", - move.product_id.default_code or move.product_id.name, - move.product_id.id, - move.product_id.last_purchase_price_received, - price_updated, - move.name, - move.purchase_line_id.order_id.name if move.purchase_line_id else 'N/A', - move.product_id.last_purchase_price_compute_type, + "Update last_purchase_price_received: %s for product %s Previous price: %s" + % ( + price_updated, + move.product_id.default_code, + move.product_id.last_purchase_price_received, + ) ) move.product_id.with_company( move.company_id diff --git a/product_sale_price_from_pricelist/tests/__init__.py b/product_sale_price_from_pricelist/tests/__init__.py deleted file mode 100644 index b14ee57..0000000 --- a/product_sale_price_from_pricelist/tests/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from . import test_product_template -from . import test_stock_move -from . import test_pricelist -from . import test_res_config diff --git a/product_sale_price_from_pricelist/tests/test_pricelist.py b/product_sale_price_from_pricelist/tests/test_pricelist.py deleted file mode 100644 index b37c726..0000000 --- a/product_sale_price_from_pricelist/tests/test_pricelist.py +++ /dev/null @@ -1,212 +0,0 @@ -# Copyright (C) 2020: 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 TestPricelist(TransactionCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - - # Create tax - cls.tax = cls.env["account.tax"].create({ - "name": "Test Tax 10%", - "amount": 10.0, - "amount_type": "percent", - "type_tax_use": "sale", - }) - - # Create product - cls.product = cls.env["product.product"].create({ - "name": "Test Product Pricelist", - "type": "product", - "list_price": 100.0, - "standard_price": 50.0, - "taxes_id": [(6, 0, [cls.tax.id])], - "last_purchase_price_received": 50.0, - "last_purchase_price_compute_type": "without_discounts", - }) - - # Create pricelist with last_purchase_price base - cls.pricelist = cls.env["product.pricelist"].create({ - "name": "Test Pricelist Last Purchase", - "currency_id": cls.env.company.currency_id.id, - }) - - def test_pricelist_item_base_last_purchase_price(self): - """Test pricelist item with last_purchase_price base""" - pricelist_item = self.env["product.pricelist.item"].create({ - "pricelist_id": self.pricelist.id, - "compute_price": "formula", - "base": "last_purchase_price", - "price_discount": 0, - "price_surcharge": 10.0, - "applied_on": "3_global", - }) - - # Compute price using pricelist - price = self.pricelist._compute_price_rule( - self.product, qty=1, date=False - ) - - # Price should be based on last_purchase_price - self.assertIn(self.product.id, price) - # The exact price depends on the formula calculation - - def test_pricelist_item_with_discount(self): - """Test pricelist item with discount on last_purchase_price""" - pricelist_item = self.env["product.pricelist.item"].create({ - "pricelist_id": self.pricelist.id, - "compute_price": "formula", - "base": "last_purchase_price", - "price_discount": 20.0, # 20% discount - "applied_on": "3_global", - }) - - price = self.pricelist._compute_price_rule( - self.product, qty=1, date=False - ) - - # Expected: 50.0 - (50.0 * 0.20) = 40.0 - self.assertIn(self.product.id, price) - self.assertEqual(price[self.product.id][0], 40.0) - - def test_pricelist_item_with_markup(self): - """Test pricelist item with markup on last_purchase_price""" - pricelist_item = self.env["product.pricelist.item"].create({ - "pricelist_id": self.pricelist.id, - "compute_price": "formula", - "base": "last_purchase_price", - "price_markup": 100.0, # 100% markup (double the price) - "applied_on": "3_global", - }) - - # _compute_price should return the base price (last_purchase_price_received) - result = pricelist_item._compute_price( - self.product, - qty=1, - uom=self.product.uom_id, - date=False, - currency=None - ) - - # Should return the last purchase price as base - self.assertEqual(result, 50.0) - - def test_pricelist_item_compute_price_method(self): - """Test _compute_price method of pricelist item""" - pricelist_item = self.env["product.pricelist.item"].create({ - "pricelist_id": self.pricelist.id, - "compute_price": "formula", - "base": "last_purchase_price", - "price_markup": 50.0, - "applied_on": "3_global", - }) - - result = pricelist_item._compute_price( - self.product, - qty=1, - uom=self.product.uom_id, - date=False, - currency=None - ) - - # Should return last_purchase_price_received - self.assertEqual(result, self.product.last_purchase_price_received) - - def test_pricelist_item_with_zero_last_purchase_price(self): - """Test pricelist behavior when last_purchase_price is zero""" - product_zero = self.env["product.product"].create({ - "name": "Product Zero Price", - "type": "product", - "list_price": 100.0, - "last_purchase_price_received": 0.0, - }) - - pricelist_item = self.env["product.pricelist.item"].create({ - "pricelist_id": self.pricelist.id, - "compute_price": "formula", - "base": "last_purchase_price", - "price_discount": 10.0, - "applied_on": "3_global", - }) - - price = self.pricelist._compute_price_rule( - product_zero, qty=1, date=False - ) - - # Should handle zero price gracefully - self.assertIn(product_zero.id, price) - self.assertEqual(price[product_zero.id][0], 0.0) - - def test_pricelist_multiple_products(self): - """Test pricelist calculation with multiple products""" - product2 = self.env["product.product"].create({ - "name": "Test Product 2", - "type": "product", - "list_price": 200.0, - "last_purchase_price_received": 100.0, - }) - - pricelist_item = self.env["product.pricelist.item"].create({ - "pricelist_id": self.pricelist.id, - "compute_price": "formula", - "base": "last_purchase_price", - "price_discount": 10.0, - "applied_on": "3_global", - }) - - products = self.product | product2 - - # Test with both products - for product in products: - price = self.pricelist._compute_price_rule( - product, qty=1, date=False - ) - self.assertIn(product.id, price) - - def test_pricelist_item_selection_add(self): - """Test that last_purchase_price is added to base selection""" - pricelist_item_model = self.env["product.pricelist.item"] - base_field = pricelist_item_model._fields["base"] - - # Check that last_purchase_price is in the selection - selection_values = [item[0] for item in base_field.selection] - self.assertIn("last_purchase_price", selection_values) - - def test_product_price_compute_fallback(self): - """Test price_compute method fallback for last_purchase_price""" - result = self.product.price_compute("last_purchase_price") - - # Should return dummy value 1.0 for all products - self.assertEqual(result[self.product.id], 1.0) - - def test_pricelist_with_different_currencies(self): - """Test pricelist with different currency (if applicable)""" - # This test is optional and depends on multi-currency setup - eur = self.env.ref("base.EUR", raise_if_not_found=False) - if not eur: - self.skipTest("EUR currency not available") - - pricelist_eur = self.env["product.pricelist"].create({ - "name": "Test Pricelist EUR", - "currency_id": eur.id, - }) - - pricelist_item = self.env["product.pricelist.item"].create({ - "pricelist_id": pricelist_eur.id, - "compute_price": "formula", - "base": "last_purchase_price", - "price_discount": 0, - "applied_on": "3_global", - }) - - # Price calculation should work with different currency - price = pricelist_eur._compute_price_rule( - self.product, qty=1, date=False - ) - - self.assertIn(self.product.id, price) diff --git a/product_sale_price_from_pricelist/tests/test_product_template.py b/product_sale_price_from_pricelist/tests/test_product_template.py deleted file mode 100644 index bfc2420..0000000 --- a/product_sale_price_from_pricelist/tests/test_product_template.py +++ /dev/null @@ -1,225 +0,0 @@ -# Copyright (C) 2020: 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 -from odoo.exceptions import UserError - - -@tagged("post_install", "-at_install") -class TestProductTemplate(TransactionCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - - # Create a tax for the product - cls.tax = cls.env["account.tax"].create({ - "name": "Test Tax 21%", - "amount": 21.0, - "amount_type": "percent", - "type_tax_use": "sale", - }) - - # Create a pricelist with formula based on cost - cls.pricelist = cls.env["product.pricelist"].create({ - "name": "Test Pricelist Automatic", - "currency_id": cls.env.company.currency_id.id, - }) - - cls.pricelist_item = cls.env["product.pricelist.item"].create({ - "pricelist_id": cls.pricelist.id, - "compute_price": "formula", - "base": "last_purchase_price", - "price_discount": 0, - "price_surcharge": 0, - "price_markup": 50.0, # 50% markup - "applied_on": "3_global", - }) - - # Set the pricelist in configuration - cls.env["ir.config_parameter"].sudo().set_param( - "product_sale_price_from_pricelist.product_pricelist_automatic", - str(cls.pricelist.id) - ) - - # Create a product category - cls.category = cls.env["product.category"].create({ - "name": "Test Category", - }) - - # Create a product - cls.product = cls.env["product.template"].create({ - "name": "Test Product", - "type": "product", - "categ_id": cls.category.id, - "list_price": 10.0, - "standard_price": 5.0, - "taxes_id": [(6, 0, [cls.tax.id])], - "last_purchase_price_received": 5.0, - "last_purchase_price_compute_type": "without_discounts", - }) - - def test_compute_theoritical_price_without_tax_error(self): - """Test that computing theoretical price without taxes raises an error""" - product_no_tax = self.env["product.template"].create({ - "name": "Product No Tax", - "type": "product", - "list_price": 10.0, - "standard_price": 5.0, - "last_purchase_price_received": 5.0, - "last_purchase_price_compute_type": "without_discounts", - }) - - with self.assertRaises(UserError): - product_no_tax._compute_theoritical_price() - - def test_compute_theoritical_price_success(self): - """Test successful computation of theoretical price""" - self.product._compute_theoritical_price() - - # Verify that theoretical price was calculated - self.assertGreater(self.product.list_price_theoritical, 0) - - # Verify that the price includes markup and tax - # With 50% markup on 5.0 = 7.5, plus 21% tax = 9.075, rounded to 9.10 - self.assertAlmostEqual(self.product.list_price_theoritical, 9.10, places=2) - - def test_compute_theoritical_price_with_rounding(self): - """Test that prices are rounded to 0.05""" - self.product.last_purchase_price_received = 4.0 - self.product._compute_theoritical_price() - - # Price should be rounded to nearest 0.05 - price = self.product.list_price_theoritical - self.assertEqual(round(price % 0.05, 2), 0.0) - - def test_last_purchase_price_updated_flag(self): - """Test that the updated flag is set when prices differ""" - initial_list_price = self.product.list_price - self.product.last_purchase_price_received = 10.0 - self.product._compute_theoritical_price() - - if self.product.list_price_theoritical != initial_list_price: - self.assertTrue(self.product.last_purchase_price_updated) - - def test_action_update_list_price(self): - """Test updating list price from theoretical price""" - self.product.last_purchase_price_received = 8.0 - self.product._compute_theoritical_price() - - theoretical_price = self.product.list_price_theoritical - self.product.action_update_list_price() - - # Verify that list price was updated - self.assertEqual(self.product.list_price, theoretical_price) - self.assertFalse(self.product.last_purchase_price_updated) - - def test_manual_update_type_skips_automatic(self): - """Test that manual update type prevents automatic price calculation""" - self.product.last_purchase_price_compute_type = "manual_update" - initial_list_price = self.product.list_price - - self.product.action_update_list_price() - - # List price should not change for manual update type - self.assertEqual(self.product.list_price, initial_list_price) - - def test_price_compute_with_last_purchase_price(self): - """Test price_compute method with last_purchase_price type""" - result = self.product.price_compute("last_purchase_price") - - # Should return dummy prices (1.0) for all product ids - for product_id in result: - self.assertEqual(result[product_id], 1.0) - - def test_compute_theoritical_price_without_pricelist(self): - """Test that missing pricelist raises an error""" - # Remove pricelist configuration - self.env["ir.config_parameter"].sudo().set_param( - "product_sale_price_from_pricelist.product_pricelist_automatic", - "" - ) - - with self.assertRaises(UserError): - self.product._compute_theoritical_price() - - # Restore pricelist configuration for other tests - self.env["ir.config_parameter"].sudo().set_param( - "product_sale_price_from_pricelist.product_pricelist_automatic", - str(self.pricelist.id) - ) - - def test_company_dependent_fields(self): - """Test that price fields are company dependent""" - # Verify field properties - field_last_purchase = self.product._fields["last_purchase_price_received"] - field_theoritical = self.product._fields["list_price_theoritical"] - field_updated = self.product._fields["last_purchase_price_updated"] - field_compute_type = self.product._fields["last_purchase_price_compute_type"] - - self.assertTrue(field_last_purchase.company_dependent) - self.assertTrue(field_theoritical.company_dependent) - self.assertTrue(field_updated.company_dependent) - self.assertTrue(field_compute_type.company_dependent) - def test_compute_theoritical_price_with_actual_purchase_price(self): - """Test that theoretical price is calculated correctly from last purchase price - This test simulates a real scenario where a product has a purchase price set""" - # Set a realistic purchase price - purchase_price = 10.50 - self.product.last_purchase_price_received = purchase_price - self.product.last_purchase_price_compute_type = "without_discounts" - - # Compute theoretical price - self.product._compute_theoritical_price() - - # Verify price is not zero - self.assertNotEqual( - self.product.list_price_theoritical, - 0.0, - "Theoretical price should not be 0.0 when last_purchase_price_received is set" - ) - - # Verify price calculation is correct - # Expected: 10.50 * 1.50 (50% markup) = 15.75 - # Plus 21% tax: 15.75 * 1.21 = 19.0575 - # Rounded to 0.05: 19.05 or 19.10 - expected_base = purchase_price * 1.50 # 15.75 - expected_with_tax = expected_base * 1.21 # 19.0575 - - self.assertGreater( - self.product.list_price_theoritical, - expected_base, - "Theoretical price should include taxes" - ) - - # Allow some tolerance for rounding - self.assertAlmostEqual( - self.product.list_price_theoritical, - expected_with_tax, - delta=0.10, - msg=f"Expected around {expected_with_tax:.2f}, got {self.product.list_price_theoritical:.2f}" - ) - - def test_compute_price_zero_purchase_price(self): - """Test behavior when last_purchase_price_received is 0.0""" - self.product.last_purchase_price_received = 0.0 - self.product._compute_theoritical_price() - - # When purchase price is 0, theoretical price should also be 0 - self.assertEqual( - self.product.list_price_theoritical, - 0.0, - "Theoretical price should be 0.0 when last_purchase_price_received is 0.0" - ) - - def test_pricelist_item_base_field(self): - """Test that pricelist item uses last_purchase_price as base""" - self.assertEqual( - self.pricelist_item.base, - "last_purchase_price", - "Pricelist item should use last_purchase_price as base" - ) - - # Verify the base field is properly configured - self.assertEqual(self.pricelist_item.compute_price, "formula") - self.assertEqual(self.pricelist_item.price_markup, 50.0) \ No newline at end of file diff --git a/product_sale_price_from_pricelist/tests/test_res_config.py b/product_sale_price_from_pricelist/tests/test_res_config.py deleted file mode 100644 index 7c48926..0000000 --- a/product_sale_price_from_pricelist/tests/test_res_config.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright (C) 2020: 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 TestResConfigSettings(TransactionCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - - cls.pricelist = cls.env["product.pricelist"].create({ - "name": "Test Config Pricelist", - "currency_id": cls.env.company.currency_id.id, - }) - - def test_config_parameter_set_and_get(self): - """Test setting and getting pricelist configuration""" - config = self.env["res.config.settings"].create({ - "product_pricelist_automatic": self.pricelist.id, - }) - - config.execute() - - # Verify parameter was saved - saved_id = self.env["ir.config_parameter"].sudo().get_param( - "product_sale_price_from_pricelist.product_pricelist_automatic" - ) - - self.assertEqual(int(saved_id), self.pricelist.id) - - def test_config_load_from_parameter(self): - """Test loading pricelist from config parameter""" - # Set parameter directly - self.env["ir.config_parameter"].sudo().set_param( - "product_sale_price_from_pricelist.product_pricelist_automatic", - str(self.pricelist.id) - ) - - # Create config and check if value is loaded - config = self.env["res.config.settings"].create({}) - - self.assertEqual(config.product_pricelist_automatic.id, self.pricelist.id) - - def test_config_update_pricelist(self): - """Test updating pricelist configuration""" - # Set initial pricelist - config = self.env["res.config.settings"].create({ - "product_pricelist_automatic": self.pricelist.id, - }) - config.execute() - - # Create new pricelist and update - new_pricelist = self.env["product.pricelist"].create({ - "name": "New Config Pricelist", - "currency_id": self.env.company.currency_id.id, - }) - - config2 = self.env["res.config.settings"].create({ - "product_pricelist_automatic": new_pricelist.id, - }) - config2.execute() - - # Verify new value - saved_id = self.env["ir.config_parameter"].sudo().get_param( - "product_sale_price_from_pricelist.product_pricelist_automatic" - ) - - self.assertEqual(int(saved_id), new_pricelist.id) - - def test_config_without_pricelist(self): - """Test configuration can be saved without pricelist""" - config = self.env["res.config.settings"].create({}) - - # Should not raise error - config.execute() diff --git a/product_sale_price_from_pricelist/tests/test_stock_move.py b/product_sale_price_from_pricelist/tests/test_stock_move.py deleted file mode 100644 index d423d85..0000000 --- a/product_sale_price_from_pricelist/tests/test_stock_move.py +++ /dev/null @@ -1,244 +0,0 @@ -# Copyright (C) 2020: 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 TestStockMove(TransactionCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - - # Create tax - cls.tax = cls.env["account.tax"].create({ - "name": "Test Tax 21%", - "amount": 21.0, - "amount_type": "percent", - "type_tax_use": "sale", - }) - - # Create pricelist - cls.pricelist = cls.env["product.pricelist"].create({ - "name": "Test Pricelist", - "currency_id": cls.env.company.currency_id.id, - }) - - cls.pricelist_item = cls.env["product.pricelist.item"].create({ - "pricelist_id": cls.pricelist.id, - "compute_price": "formula", - "base": "last_purchase_price", - "price_markup": 50.0, - "applied_on": "3_global", - }) - - cls.env["ir.config_parameter"].sudo().set_param( - "product_sale_price_from_pricelist.product_pricelist_automatic", - str(cls.pricelist.id) - ) - - # Create supplier - cls.supplier = cls.env["res.partner"].create({ - "name": "Test Supplier", - "supplier_rank": 1, - }) - - # Create product with UoM - cls.uom_unit = cls.env.ref("uom.product_uom_unit") - cls.uom_dozen = cls.env.ref("uom.product_uom_dozen") - - cls.product = cls.env["product.product"].create({ - "name": "Test Product Stock", - "type": "product", - "uom_id": cls.uom_unit.id, - "uom_po_id": cls.uom_unit.id, - "list_price": 10.0, - "standard_price": 5.0, - "taxes_id": [(6, 0, [cls.tax.id])], - "last_purchase_price_received": 5.0, - "last_purchase_price_compute_type": "without_discounts", - }) - - # Create locations - cls.supplier_location = cls.env.ref("stock.stock_location_suppliers") - cls.stock_location = cls.env.ref("stock.stock_location_stock") - - def _create_purchase_order(self, product, qty, price, discount1=0, discount2=0, discount3=0): - """Helper to create a purchase order""" - purchase_order = self.env["purchase.order"].create({ - "partner_id": self.supplier.id, - }) - - po_line = self.env["purchase.order.line"].create({ - "order_id": purchase_order.id, - "product_id": product.id, - "product_qty": qty, - "price_unit": price, - "product_uom": product.uom_po_id.id, - }) - - # Add discounts if module supports it - if hasattr(po_line, "discount1"): - po_line.discount1 = discount1 - if hasattr(po_line, "discount2"): - po_line.discount2 = discount2 - if hasattr(po_line, "discount3"): - po_line.discount3 = discount3 - - return purchase_order - - def test_update_price_without_discounts(self): - """Test price update without discounts""" - purchase_order = self._create_purchase_order( - self.product, qty=10, price=8.0 - ) - purchase_order.button_confirm() - - # Get the picking and process it - picking = purchase_order.picking_ids[0] - picking.action_assign() - - for move in picking.move_ids: - move.quantity = move.product_uom_qty - - picking.button_validate() - - # Verify price was updated - self.assertEqual(self.product.last_purchase_price_received, 8.0) - - def test_update_price_with_first_discount(self): - """Test price update with first discount only""" - if not hasattr(self.env["purchase.order.line"], "discount1"): - self.skipTest("Purchase discount module not installed") - - self.product.last_purchase_price_compute_type = "with_discount" - - purchase_order = self._create_purchase_order( - self.product, qty=10, price=10.0, discount1=20.0 - ) - purchase_order.button_confirm() - - picking = purchase_order.picking_ids[0] - picking.action_assign() - - for move in picking.move_ids: - move.quantity = move.product_uom_qty - - picking.button_validate() - - # Expected: 10.0 * (1 - 0.20) = 8.0 - self.assertEqual(self.product.last_purchase_price_received, 8.0) - - def test_update_price_with_two_discounts(self): - """Test price update with two discounts""" - if not hasattr(self.env["purchase.order.line"], "discount2"): - self.skipTest("Purchase double discount module not installed") - - self.product.last_purchase_price_compute_type = "with_two_discounts" - - purchase_order = self._create_purchase_order( - self.product, qty=10, price=10.0, discount1=20.0, discount2=10.0 - ) - purchase_order.button_confirm() - - picking = purchase_order.picking_ids[0] - picking.action_assign() - - for move in picking.move_ids: - move.quantity = move.product_uom_qty - - picking.button_validate() - - # Expected: 10.0 * (1 - 0.20) * (1 - 0.10) = 7.2 - self.assertEqual(self.product.last_purchase_price_received, 7.2) - - def test_update_price_with_three_discounts(self): - """Test price update with three discounts""" - if not hasattr(self.env["purchase.order.line"], "discount3"): - self.skipTest("Purchase triple discount module not installed") - - self.product.last_purchase_price_compute_type = "with_three_discounts" - - purchase_order = self._create_purchase_order( - self.product, qty=10, price=10.0, discount1=20.0, discount2=10.0, discount3=5.0 - ) - purchase_order.button_confirm() - - picking = purchase_order.picking_ids[0] - picking.action_assign() - - for move in picking.move_ids: - move.quantity = move.product_uom_qty - - picking.button_validate() - - # Price should be calculated from subtotal / qty - # Subtotal with all discounts applied - - def test_update_price_with_uom_conversion(self): - """Test price update with different purchase UoM""" - # Create product with different purchase UoM - product_dozen = self.product.copy({ - "name": "Test Product Dozen", - "uom_po_id": self.uom_dozen.id, - }) - - purchase_order = self._create_purchase_order( - product_dozen, qty=2, price=120.0 # 2 dozens at 120.0 per dozen - ) - purchase_order.button_confirm() - - picking = purchase_order.picking_ids[0] - picking.action_assign() - - for move in picking.move_ids: - move.quantity = move.product_uom_qty - - picking.button_validate() - - # Price should be converted to base UoM (unit) - # 120.0 per dozen = 10.0 per unit - self.assertEqual(product_dozen.last_purchase_price_received, 10.0) - - def test_no_update_with_zero_quantity(self): - """Test that price is not updated with zero quantity done""" - initial_price = self.product.last_purchase_price_received - - purchase_order = self._create_purchase_order( - self.product, qty=10, price=15.0 - ) - purchase_order.button_confirm() - - picking = purchase_order.picking_ids[0] - picking.action_assign() - - # Set quantity done to 0 - for move in picking.move_ids: - move.quantity = 0 - - # This should not update the price - # Price should remain unchanged - self.assertEqual(self.product.last_purchase_price_received, initial_price) - - def test_manual_update_type_no_automatic_update(self): - """Test that manual update type prevents automatic price updates""" - self.product.last_purchase_price_compute_type = "manual_update" - initial_price = self.product.last_purchase_price_received - - purchase_order = self._create_purchase_order( - self.product, qty=10, price=15.0 - ) - purchase_order.button_confirm() - - picking = purchase_order.picking_ids[0] - picking.action_assign() - - for move in picking.move_ids: - move.quantity = move.product_uom_qty - - picking.button_validate() - - # For manual update, the standard Odoo behavior applies - # which may or may not update standard_price, but our custom - # last_purchase_price_received logic still runs diff --git a/product_sale_price_from_pricelist/views/product_view.xml b/product_sale_price_from_pricelist/views/product_view.xml index 5c59a8c..eca9ce8 100644 --- a/product_sale_price_from_pricelist/views/product_view.xml +++ b/product_sale_price_from_pricelist/views/product_view.xml @@ -6,12 +6,15 @@ product.list.price.automatic.form product.template + form - last_purchase_price_compute_type != 'manual_update' + + {'readonly':[('last_purchase_price_compute_type','!=','manual_update')]} + - + - - + +
- +