[IMP] product_pricelist_total_margin: Add margin_type selector (Markup vs Commercial Margin)

- Add margin_type field to choose between calculation methods:
  * Markup (on cost): PVP = Cost × (1 + markup%)
  * Commercial Margin (on PVP): PVP = Cost / (1 - margin%)
- Update _compute_price() to apply correct formula based on margin_type
- Add safety cap for commercial margin >= 100% (caps at 99%)
- Add detailed logging for both calculation types
- Update views to show margin_type field when use_total_margin is enabled
- Add 2 new tests to validate both calculation methods:
  * test_total_margin_markup_type: validates markup formula
  * test_total_margin_commercial_margin_type: validates commercial margin formula
- All 11 tests passing (9 existing + 2 new)

Example with Commercial Margin:
Base: 4.68€, Total Margin: 20%
- Markup: 4.68 × 1.20 = 5.616€ (margin = 16.67% of PVP)
- Commercial Margin: 4.68 / 0.80 = 5.85€ (margin = 20% of PVP) ✓
This commit is contained in:
snt 2026-02-21 18:54:38 +01:00
parent cafa19ffea
commit 0f239601ce
4 changed files with 107 additions and 11 deletions

View file

@ -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="Margin Type",
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)