- Add global_margin_type field in res.config.settings * Options: 'markup' (default) or 'margin' (commercial margin) * Determines how global min/max percentages are interpreted - Refactor _apply_global_margin_limits(): * Now receives price instead of margin percentage * Calculates min/max prices based on global_margin_type * Returns adjusted price instead of adjusted margin * Supports both markup and commercial margin formulas - Update _compute_price() to apply limits after price calculation - Update res_config_settings_views.xml to show global_margin_type selector - Update help texts with examples for both calculation methods - Add 2 new tests to validate global limits with commercial margin type: * test_global_minimum_with_commercial_margin_type * test_global_maximum_with_commercial_margin_type - All 13 tests passing (11 existing + 2 new) Example with global min 25%: - Markup: Min price = Cost × 1.25 - Commercial Margin: Min price = Cost / 0.75 (ensures 25% margin on PVP) |
||
|---|---|---|
| .. | ||
| models | ||
| tests | ||
| views | ||
| __init__.py | ||
| __manifest__.py | ||
| README.md | ||
Product Pricelist Total Margin
Overview
This module solves the problem of compounded margins when using chained pricelists in Odoo. By default, Odoo applies each pricelist rule's margin on top of the previous result, leading to compounded percentages. This module provides an option to calculate margins additively instead.
Problem Description
Standard Odoo Behavior (Compound Margins)
When you chain pricelists (Pricelist A → Pricelist B), Odoo applies margins in cascade:
Base price: 4.68€
Pricelist A: -5% discount → 4.68 × 0.95 = 4.446€
Pricelist B: 25% markup → 4.446 × 1.25 = 5.5575€
Result: 5.56€ (effective margin: 18.8%)
Desired Behavior (Total/Additive Margins)
With this module, you can calculate the total margin by summing percentages:
Option 1: Markup (on cost)
Base price: 4.68€
Total margin: -5% + 25% = 20%
Final price: 4.68 × 1.20 = 5.616€
Effective margin: (5.616 - 4.68) / 5.616 = 16.67%
Result: 5.62€ (20% markup on cost)
Option 2: Commercial Margin (on PVP)
Base price: 4.68€
Total margin: -5% + 25% = 20%
Final price: 4.68 / 0.80 = 5.85€
Effective margin: (5.85 - 4.68) / 5.85 = 20%
Result: 5.85€ (20% commercial margin on PVP)
Features
- ✅ Additive margin calculation across chained pricelists
- ✅ Two calculation methods:
- Markup (on cost):
PVP = Cost × (1 + markup%) - Commercial Margin (on PVP):
PVP = Cost / (1 - margin%)
- Markup (on cost):
- ✅ Opt-in via checkbox - doesn't affect existing pricelists
- ✅ Compatible with custom bases (
last_purchase_pricefromproduct_sale_price_from_pricelist) - ✅ Supports all formula extras (price_round, price_surcharge, price_min/max_margin)
- ✅ Global min/max margin limits (configurable in Settings > Sales)
- ✅ Multi-level chains - works with 2+ pricelists in sequence
- ✅ Currency conversion - handles multi-currency scenarios
- ✅ Detailed logging - debug pricing calculations easily
Installation
-
Install dependencies:
# Ensure these modules are installed: - product - product_price_category - product_sale_price_from_pricelist -
Install module:
docker-compose exec odoo odoo -d odoo -u product_pricelist_total_margin --stop-after-init -
Restart Odoo:
docker-compose restart odoo
Configuration
Step 1: Create Base Pricelist
Create a pricelist that defines your base pricing logic:
- Go to Sales > Configuration > Pricelists
- Create a new pricelist: "Base Pricelist - Last Purchase Price"
- Add a rule:
- Apply On: All Products
- Based on: Last Purchase Price (or List Price, Standard Price, etc.)
- Price Computation: Formula
- Discount: 5% (for "cesta básica" category example)
Step 2: Create Chained Pricelist
Create a pricelist that chains to the base one:
- Create a new pricelist: "Category Margin - Repostería"
- Add a rule:
- Apply On: All Products (or specific category)
- Based on: Other Pricelist → Select "Base Pricelist - Last Purchase Price"
- Price Computation: Formula
- Discount: -25% (negative = 25% markup)
- ☑️ Total Margin Mode: Check this box!
- Calculation Method: Choose between:
- Markup (on cost) - Default, calculates
PVP = Cost × (1 + markup%) - Commercial Margin (on PVP) - Calculates
PVP = Cost / (1 - margin%)
- Markup (on cost) - Default, calculates
Step 3: Assign to Products
- For automatic price calculation, configure the pricelist in Settings > Sales > Automatic Price Configuration
- Or assign the pricelist to specific customers/partners
Usage Example
Scenario: Cooperative Pricing System
Your cooperative has two margin rules:
- Price Category Discount: "Cesta Básica" products get -5% (to make them affordable)
- Product Category Markup: "Repostería" products get +25% (higher margin category)
Without this module (compound):
Product: Flour (cesta básica, repostería)
Purchase price: 4.68€
After price category (-5%): 4.446€
After product category (+25%): 5.5575€ ❌ Wrong effective margin: 18.8%
With this module (total margin enabled):
Product: Flour (cesta básica, repostería)
Purchase price: 4.68€
Total margin: -5% + 25% = 20%
Option 1 - Markup:
Final price: 4.68 × 1.20 = 5.616€ ✅ Markup 20% on cost
Option 2 - Commercial Margin:
Final price: 4.68 / 0.80 = 5.85€ ✅ Margin 20% on PVP
Which method to choose?
- Markup (on cost): Traditional markup calculation, margin on cost base
- Commercial Margin (on PVP): Retail/commercial margin, ensures exact margin percentage on final price
Technical Details
New Fields
- Model:
product.pricelist.item
use_total_margin (Boolean)
- Default: False (opt-in)
- Visibility: Only shown when:
compute_price = 'formula'base = 'pricelist'(chained pricelist)
- Purpose: Enable additive margin calculation instead of compound
margin_type (Selection)
- Default:
'markup' - Options:
'markup'- Markup (on cost):PVP = Cost × (1 + markup%)'margin'- Commercial Margin (on PVP):PVP = Cost / (1 - margin%)
- Visibility: Only shown when
use_total_margin = True - Purpose: Choose the calculation method for the total margin
Methods
_get_base_price_and_margins(product, quantity, uom, date, currency)
Traverses the pricelist chain backwards to:
- Find the original base price (before any margins)
- Collect all margin percentages along the chain
Returns: (base_price: float, margins: list[float])
_compute_original_base_price(item, product, quantity, uom, date, currency)
Computes the base price from the bottom item of the chain, supporting:
last_purchase_price(custom fromproduct_sale_price_from_pricelist)list_price(standard)standard_price(cost)- Currency conversions
_apply_formula_extras(price, base_price, currency)
Applies additional formula options:
price_round: Round to nearest valueprice_surcharge: Add fixed amountprice_min_margin: Enforce minimum marginprice_max_margin: Enforce maximum margin
_compute_price(product, quantity, uom, date, currency) [OVERRIDE]
Main override that:
- Checks if
use_total_margin=Trueand conditions are met - Calls helper methods to get base price and margins
- Sums margins additively:
total_margin = sum(margins) - Applies global min/max margin limits if configured
- Applies total margin using selected method:
- Markup:
price = base_price * (1 + total_margin / 100) - Commercial Margin:
price = base_price / (1 - total_margin / 100)
- Markup:
- Applies formula extras
- Falls back to standard behavior if conditions not met
Safety: Commercial margin >= 100% is capped at 99% to avoid division by zero
Logging
All calculations are logged with [TOTAL MARGIN] prefix for easy debugging:
_logger.info("[TOTAL MARGIN] Item %s: base=%s, margin=%.2f%%", ...)
_logger.info("[TOTAL MARGIN] Margins: ['5.0%', '25.0%'] = 20.0% total")
# Markup method:
_logger.info("[TOTAL MARGIN] Markup: Base 4.68 * (1 + 20.0%) = 5.616")
# Commercial Margin method:
_logger.info("[TOTAL MARGIN] Commercial Margin: Base 4.68 / (1 - 20.0%) = 5.85")
View logs:
docker-compose logs -f odoo | grep "TOTAL MARGIN"
Testing
Run Tests
docker-compose run odoo odoo -d odoo --test-enable --stop-after-init -u product_pricelist_total_margin
Test Coverage
- ✅ Compound margin (default behavior preserved)
- ✅ Total margin with Markup calculation
- ✅ Total margin with Commercial Margin calculation
- ✅ Formula extras (round, surcharge, min/max)
- ✅ Global min/max margin limits
- ✅ 3-level pricelist chains
- ✅ Different base types (last_purchase_price, list_price)
- ✅ Currency conversions
Total: 11 tests, all passing
Compatibility
- Odoo Version: 18.0
- Python Version: 3.10+
- Dependencies:
product(core)product_price_category(OCA)product_sale_price_from_pricelist(custom)
Limitations
- Only works with
compute_price='formula': Fixed prices and percentage-based rules are not affected - Circular references: The module detects and breaks circular pricelist chains, but logs a warning
- Performance: Traversing long pricelist chains may impact performance (though minimal in practice)
Troubleshooting
Margins still compound even with checkbox enabled
Check:
- Is
compute_priceset to "Formula"? - Is
baseset to "Other Pricelist"? - Is the checkbox actually checked and saved?
- Check logs for
[TOTAL MARGIN]entries to see if logic is being triggered
Price is incorrect
Debug:
- Enable developer mode
- Check logs:
docker-compose logs -f odoo | grep "TOTAL MARGIN" - Verify:
- Base price is correct
- All margins are collected
- Currency conversions are applied
- Formula extras (round, surcharge) are expected
Checkbox not visible
Possible causes:
compute_priceis not "Formula" (must be formula-based)baseis not "Other Pricelist" (no chain to traverse)- View not properly loaded (try reloading page or clearing browser cache)
Development
File Structure
product_pricelist_total_margin/
├── __init__.py
├── __manifest__.py
├── README.md
├── models/
│ ├── __init__.py
│ └── product_pricelist_item.py
├── tests/
│ ├── __init__.py
│ └── test_total_margin.py
└── views/
└── product_pricelist_item_views.xml
Contributing
Follow OCA guidelines and project conventions defined in .github/copilot-instructions.md.
License
AGPL-3.0 or later
Author
Kidekoop - 2026
Changelog
18.0.1.1.0 (2026-02-21)
- [IMP] Add
margin_typefield to choose between calculation methods- Markup (on cost):
PVP = Cost × (1 + markup%) - Commercial Margin (on PVP):
PVP = Cost / (1 - margin%)
- Markup (on cost):
- Add 2 new tests for both calculation methods
- Add safety cap for commercial margin >= 100% (caps at 99%)
- Improve logging to show which calculation method is used
- Total: 11 tests passing
18.0.1.0.0 (2026-02-21)
- Initial implementation
- Support for additive margin calculation in chained pricelists
- Compatible with
last_purchase_pricecustom base - Global min/max margin limits configuration
- Comprehensive test suite (9 tests)
- Detailed logging for debugging