diff --git a/product_sale_price_from_pricelist/__manifest__.py b/product_sale_price_from_pricelist/__manifest__.py index a4bfec3..3a2f170 100644 --- a/product_sale_price_from_pricelist/__manifest__.py +++ b/product_sale_price_from_pricelist/__manifest__.py @@ -12,7 +12,7 @@ "depends": [ "product_get_price_helper", "account_invoice_triple_discount_readonly", - "sale", + "sale_management", "purchase", "account", "stock_account", diff --git a/product_sale_price_from_pricelist/models/product_pricelist.py b/product_sale_price_from_pricelist/models/product_pricelist.py index d16d8ca..6668aad 100644 --- a/product_sale_price_from_pricelist/models/product_pricelist.py +++ b/product_sale_price_from_pricelist/models/product_pricelist.py @@ -2,26 +2,39 @@ # @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, qty, uom=None, date=False, **kwargs): + def _compute_price_rule(self, products, quantity, 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, - qty=qty, + quantity, 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(): @@ -29,10 +42,26 @@ 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": - price = ProductProduct.browse( - product_id - ).last_purchase_price_received + 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 = (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_product.py b/product_sale_price_from_pricelist/models/product_product.py index f867604..425a8a2 100644 --- a/product_sale_price_from_pricelist/models/product_product.py +++ b/product_sale_price_from_pricelist/models/product_product.py @@ -1,9 +1,16 @@ -from odoo import models +from odoo import fields, 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 9de30a9..79c0720 100644 --- a/product_sale_price_from_pricelist/models/product_template.py +++ b/product_sale_price_from_pricelist/models/product_template.py @@ -5,8 +5,11 @@ 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" @@ -56,6 +59,15 @@ 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 @@ -65,6 +77,14 @@ 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( @@ -73,23 +93,33 @@ class ProductTemplate(models.Model): ) % template.name ) - tax_price = template.taxes_id.compute_all( - partial_price[template.product_variant_id.id]["value"] or 0.0, - handle_price_include=False, + + 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, ) - price_with_taxes = ( - tax_price["taxes"][0]["amount"] - + partial_price[template.product_variant_id.id]["value"] + + # 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, ) - # 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": price_with_taxes, + "list_price_theoritical": theoretical_price, "last_purchase_price_updated": ( - price_with_taxes != template.list_price + theoretical_price != template.list_price ), } ) @@ -103,8 +133,19 @@ 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 9865db0..abbaaf5 100644 --- a/product_sale_price_from_pricelist/models/stock_move.py +++ b/product_sale_price_from_pricelist/models/stock_move.py @@ -59,14 +59,16 @@ class StockMove(models.Model): move.product_id.last_purchase_price_received, price_updated, precision_digits=2, - ) and not float_is_zero(move.quantity_done, precision_digits=3): + ) and not float_is_zero(move.quantity, precision_digits=3): _logger.info( - "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, - ) + "[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, ) move.product_id.with_company( move.company_id diff --git a/product_sale_price_from_pricelist/tests/test_product_template.py b/product_sale_price_from_pricelist/tests/test_product_template.py index fb95fb7..bfc2420 100644 --- a/product_sale_price_from_pricelist/tests/test_product_template.py +++ b/product_sale_price_from_pricelist/tests/test_product_template.py @@ -161,3 +161,65 @@ class TestProductTemplate(TransactionCase): 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