diff --git a/product_pricelist_total_margin/README.md b/product_pricelist_total_margin/README.md index 79a47e4..c8e1052 100644 --- a/product_pricelist_total_margin/README.md +++ b/product_pricelist_total_margin/README.md @@ -45,13 +45,16 @@ Effective margin: (5.85 - 4.68) / 5.85 = 20% ## Features - ✅ **Additive margin calculation** across chained pricelists -- ✅ **Two calculation methods:** +- ✅ **Two calculation methods per pricelist:** - **Markup (on cost):** `PVP = Cost × (1 + markup%)` - **Commercial Margin (on PVP):** `PVP = Cost / (1 - margin%)` +- ✅ **Global min/max margin limits** with independent calculation type: + - Configure minimum and maximum margin percentages + - Choose between Markup or Commercial Margin interpretation for limits + - Limits can use different calculation method than individual pricelists - ✅ **Opt-in via checkbox** - doesn't affect existing pricelists - ✅ **Compatible with custom bases** (`last_purchase_price` from `product_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 @@ -105,7 +108,24 @@ Create a pricelist that chains to the base one: - **Markup (on cost)** - Default, calculates `PVP = Cost × (1 + markup%)` - **Commercial Margin (on PVP)** - Calculates `PVP = Cost / (1 - margin%)` -### Step 3: Assign to Products +### Step 3: Configure Global Limits (Optional) + +Set enterprise-wide minimum and maximum margins: + +1. Go to **Settings > Sales > Total Margin Limits** +2. Configure: + - **Minimum Margin (%):** E.g., 10% (no product can have less margin) + - **Maximum Margin (%):** E.g., 50% (no product can have more margin) + - **Calculation Method:** Choose how these percentages are interpreted: + - **Markup (on cost)** - Default, `Min: Cost × 1.10`, `Max: Cost × 1.50` + - **Commercial Margin (on PVP)** - `Min: Cost / 0.90`, `Max: Cost / 0.50` + +**Important:** The global limits calculation method is independent from individual pricelist items. You can have: +- Items using Markup, with global limits as Commercial Margin +- Items using Commercial Margin, with global limits as Markup +- Both using the same method + +### Step 4: Assign to Products - For automatic price calculation, configure the pricelist in **Settings > Sales > Automatic Price Configuration** - Or assign the pricelist to specific customers/partners @@ -165,6 +185,30 @@ Option 2 - Commercial Margin: - **Visibility:** Only shown when `use_total_margin = True` - **Purpose:** Choose the calculation method for the total margin +### Global Configuration Fields + +- **Model:** `res.config.settings` + +#### `total_margin_min_percent` (Float) +- **Default:** 0.0 (disabled) +- **Purpose:** Minimum margin percentage for all products using Total Margin Mode +- **Interpretation:** Depends on `global_margin_type` setting + +#### `total_margin_max_percent` (Float) +- **Default:** 0.0 (disabled) +- **Purpose:** Maximum margin percentage for all products using Total Margin Mode +- **Interpretation:** Depends on `global_margin_type` setting + +#### `global_margin_type` (Selection) +- **Default:** `'markup'` +- **Options:** + - `'markup'` - Limits are interpreted as markup on cost + - `'margin'` - Limits are interpreted as commercial margin on PVP +- **Purpose:** Define how min/max percentages are converted to prices +- **Example:** Min 25%: + - Markup: `Min Price = Cost × 1.25` + - Commercial Margin: `Min Price = Cost / 0.75` (ensures 25% margin on final price) + ### Methods #### `_get_base_price_and_margins(product, quantity, uom, date, currency)` @@ -191,16 +235,33 @@ Applies additional formula options: - `price_min_margin`: Enforce minimum margin - `price_max_margin`: Enforce maximum margin +#### `_apply_global_margin_limits(price, base_price)` + +Enforces global minimum and maximum margins configured in Settings: +1. Reads `total_margin_min_percent`, `total_margin_max_percent`, and `global_margin_type` from configuration +2. Calculates min/max allowed prices based on `global_margin_type`: + - **Markup:** `Price = Cost × (1 + margin%)` + - **Commercial Margin:** `Price = Cost / (1 - margin%)` +3. Adjusts price if it falls outside configured limits +4. Returns adjusted price + +**Note:** Global limits are applied AFTER the item's margin calculation, and can use a different margin type than the item itself. + +**Example:** +- Item calculates 15% markup → Price = 100 × 1.15 = 115€ +- Global min is 20% commercial margin → Min Price = 100 / 0.80 = 125€ +- Result: Price adjusted from 115€ to 125€ to meet global minimum + #### `_compute_price(product, quantity, uom, date, currency)` [OVERRIDE] Main override that: 1. Checks if `use_total_margin=True` and conditions are met 2. Calls helper methods to get base price and margins 3. Sums margins additively: `total_margin = sum(margins)` -4. Applies global min/max margin limits if configured -5. Applies total margin using selected method: +4. Applies total margin using item's selected method: - **Markup:** `price = base_price * (1 + total_margin / 100)` - **Commercial Margin:** `price = base_price / (1 - total_margin / 100)` +5. Applies global min/max margin limits (may use different margin type) 6. Applies formula extras 7. Falls back to standard behavior if conditions not met @@ -240,12 +301,14 @@ docker-compose run odoo odoo -d odoo --test-enable --stop-after-init -u product_ - ✅ Total margin with Markup calculation - ✅ Total margin with Commercial Margin calculation - ✅ Formula extras (round, surcharge, min/max) -- ✅ Global min/max margin limits +- ✅ Global min/max margin limits with Markup type +- ✅ Global min/max margin limits with Commercial Margin type +- ✅ Mixed margin types (item vs global with different types) - ✅ 3-level pricelist chains - ✅ Different base types (last_purchase_price, list_price) - ✅ Currency conversions -**Total:** 11 tests, all passing +**Total:** 13 tests, all passing ## Compatibility @@ -323,6 +386,19 @@ AGPL-3.0 or later ## Changelog +### 18.0.1.2.0 (2026-02-21) + +- **[IMP]** Add `global_margin_type` field for independent global limits calculation + - Configure how min/max percentages are interpreted (Markup or Commercial Margin) + - Global limits can use different calculation method than individual pricelist items + - Enables flexible business rules (e.g., items use Markup, global limits enforce Commercial Margin) +- Refactor `_apply_global_margin_limits()` to work with prices instead of percentages + - Now calculates min/max prices based on `global_margin_type` + - Applied after item's margin calculation, not before +- Add 2 new tests for global limits with Commercial Margin type +- Update configuration UI with global margin type selector +- Total: 13 tests passing + ### 18.0.1.1.0 (2026-02-21) - **[IMP]** Add `margin_type` field to choose between calculation methods diff --git a/product_pricelist_total_margin/__manifest__.py b/product_pricelist_total_margin/__manifest__.py index 8003703..b940e05 100644 --- a/product_pricelist_total_margin/__manifest__.py +++ b/product_pricelist_total_margin/__manifest__.py @@ -2,9 +2,9 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { # noqa: B018 "name": "Product Pricelist Total Margin", - "version": "18.0.1.1.0", + "version": "18.0.1.2.0", "category": "Sales/Products", - "summary": "Calculate total margin additively with Markup or Commercial Margin methods", + "summary": "Calculate total margin additively with Markup or Commercial Margin methods, enforce global limits", "author": "Odoo Community Association (OCA), Criptomart", "website": "https://git.criptomart.net/criptomart/addons-cm", "license": "AGPL-3",