diff --git a/product_pricelist_total_margin/__manifest__.py b/product_pricelist_total_margin/__manifest__.py
index adb3e30..4f714db 100644
--- a/product_pricelist_total_margin/__manifest__.py
+++ b/product_pricelist_total_margin/__manifest__.py
@@ -5,8 +5,8 @@
"version": "18.0.1.0.0",
"category": "Sales/Products",
"summary": "Calculate total margin additively instead of compounding in chained pricelists",
- "author": "Odoo Community Association (OCA), Kidekoop",
- "website": "https://github.com/kidekoop",
+ "author": "Odoo Community Association (OCA), Criptomart",
+ "website": "https://git.criptomart.net/criptomart/addons-cm",
"license": "AGPL-3",
"depends": [
"product",
diff --git a/product_pricelist_total_margin/models/product_pricelist_item.py b/product_pricelist_total_margin/models/product_pricelist_item.py
index 6f1f218..9b1981f 100644
--- a/product_pricelist_total_margin/models/product_pricelist_item.py
+++ b/product_pricelist_total_margin/models/product_pricelist_item.py
@@ -23,6 +23,20 @@ class ProductPricelistItem(models.Model):
"- Total (this option): 100 * (1 + 0.20) = 120€",
)
+ margin_type = fields.Selection(
+ selection=[
+ ("markup", "Markup (on cost)"),
+ ("margin", "Commercial Margin (on PVP)"),
+ ],
+ string="Calculation Method",
+ default="markup",
+ help="Type of margin calculation:\n"
+ "- Markup: PVP = Cost × (1 + markup%). Margin is calculated on cost.\n"
+ " Example: Cost 100€, Markup 25% → PVP = 125€\n\n"
+ "- Commercial Margin: PVP = Cost / (1 - margin%). Margin is calculated on PVP.\n"
+ " Example: Cost 100€, Margin 20% → PVP = 125€ (margin = 25€/125€ = 20%)",
+ )
+
def _get_base_price_and_margins(self, product, quantity, uom, date, currency):
"""
Traverse the pricelist chain to get the original base price and collect
@@ -367,14 +381,32 @@ class ProductPricelistItem(models.Model):
# Apply global min/max margin limits
total_margin = self._apply_global_margin_limits(total_margin, base_price)
- # Apply total margin to base price
- price = base_price * (1 + total_margin / 100)
- _logger.info(
- "[TOTAL MARGIN] Base price %.2f * (1 + %.2f%%) = %.2f",
- base_price,
- total_margin,
- price,
- )
+ # Apply total margin to base price using selected margin type
+ margin_type = self.margin_type or "markup"
+ if margin_type == "margin":
+ # Commercial Margin: PVP = Cost / (1 - margin%)
+ if total_margin >= 100:
+ _logger.warning(
+ "[TOTAL MARGIN] Commercial margin %.2f%% >= 100%%, capping at 99%%",
+ total_margin,
+ )
+ total_margin = 99.0
+ price = base_price / (1 - total_margin / 100)
+ _logger.info(
+ "[TOTAL MARGIN] Commercial Margin: Base %.2f / (1 - %.2f%%) = %.2f",
+ base_price,
+ total_margin,
+ price,
+ )
+ else:
+ # Markup: PVP = Cost × (1 + markup%)
+ price = base_price * (1 + total_margin / 100)
+ _logger.info(
+ "[TOTAL MARGIN] Markup: Base %.2f * (1 + %.2f%%) = %.2f",
+ base_price,
+ total_margin,
+ price,
+ )
# Apply formula extras (round, surcharge, min/max margins)
price = self._apply_formula_extras(price, base_price, currency)
diff --git a/product_pricelist_total_margin/tests/test_total_margin.py b/product_pricelist_total_margin/tests/test_total_margin.py
index 06128f1..2929015 100644
--- a/product_pricelist_total_margin/tests/test_total_margin.py
+++ b/product_pricelist_total_margin/tests/test_total_margin.py
@@ -351,3 +351,63 @@ class TestTotalMargin(TransactionCase):
self.env["ir.config_parameter"].sudo().set_param(
"product_pricelist_total_margin.max_percent", "0.0"
)
+
+ def test_total_margin_markup_type(self):
+ """Test total margin with Markup calculation (on cost)."""
+ # Enable total margin and set to markup
+ self.item_base.use_total_margin = True
+ self.item_chained.use_total_margin = True
+ self.item_chained.margin_type = "markup"
+
+ price = self.pricelist_chained._get_product_price(
+ product=self.product,
+ quantity=1.0,
+ )
+
+ # Expected calculation (Markup):
+ # Base: 4.68
+ # Total margin: -5% + 25% = 20%
+ # Markup formula: PVP = Cost × (1 + markup%)
+ # Final: 4.68 * (1 + 0.20) = 5.616
+ expected_price = 4.68 * 1.20
+ self.assertAlmostEqual(
+ price,
+ expected_price,
+ places=2,
+ msg=f"Markup calculation should give {expected_price}, got {price}",
+ )
+
+ def test_total_margin_commercial_margin_type(self):
+ """Test total margin with Commercial Margin calculation (on PVP)."""
+ # Enable total margin and set to commercial margin
+ self.item_base.use_total_margin = True
+ self.item_chained.use_total_margin = True
+ self.item_chained.margin_type = "margin"
+
+ price = self.pricelist_chained._get_product_price(
+ product=self.product,
+ quantity=1.0,
+ )
+
+ # Expected calculation (Commercial Margin):
+ # Base: 4.68
+ # Total margin: -5% + 25% = 20%
+ # Commercial margin formula: PVP = Cost / (1 - margin%)
+ # Final: 4.68 / (1 - 0.20) = 5.85
+ expected_price = 4.68 / 0.80
+ self.assertAlmostEqual(
+ price,
+ expected_price,
+ places=2,
+ msg=f"Commercial margin calculation should give {expected_price}, got {price}",
+ )
+
+ # Verify the commercial margin is indeed 20%
+ # Commercial margin = (PVP - Cost) / PVP
+ commercial_margin = (price - 4.68) / price
+ self.assertAlmostEqual(
+ commercial_margin,
+ 0.20,
+ places=4,
+ msg=f"Commercial margin should be 20%, got {commercial_margin * 100}%",
+ )
diff --git a/product_pricelist_total_margin/views/product_pricelist_item_views.xml b/product_pricelist_total_margin/views/product_pricelist_item_views.xml
index 2a7d19d..ff77a18 100644
--- a/product_pricelist_total_margin/views/product_pricelist_item_views.xml
+++ b/product_pricelist_total_margin/views/product_pricelist_item_views.xml
@@ -5,12 +5,16 @@
product.pricelist.item
-
+
+