Compare commits

..

3 commits

Author SHA1 Message Date
snt
b31df7b9d8 [DOC] product_pricelist_total_margin: Update docs and version to 18.0.1.2.0
Changelog:
- Document global_margin_type feature for independent global limits calculation
- Update version from 18.0.1.1.0 to 18.0.1.2.0
- Update test coverage count (11 → 13 tests)
- Update manifest summary to include global limits enforcement
2026-02-21 19:19:25 +01:00
snt
449bb75bb6 [IMP] product_pricelist_total_margin: Add global_margin_type for min/max limits
- 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)
2026-02-21 19:16:16 +01:00
snt
07cc0eb517 [DOC] product_pricelist_total_margin: Update documentation for v1.1.0
- Update README with margin_type field documentation
- Add detailed explanations for both calculation methods:
  * Markup (on cost): PVP = Cost × (1 + markup%)
  * Commercial Margin (on PVP): PVP = Cost / (1 - margin%)
- Add examples comparing both methods
- Update features list with new capabilities
- Update configuration steps to include margin_type selector
- Update technical details with new field specifications
- Update test coverage section (11 tests)
- Add logging examples for both methods
- Update changelog with v18.0.1.1.0 release notes
- Bump version to 18.0.1.1.0 in __manifest__.py
- Update summary to reflect new features
2026-02-21 19:11:01 +01:00
6 changed files with 333 additions and 39 deletions

View file

@ -22,17 +22,36 @@ Pricelist B: 25% markup → 4.446 × 1.25 = 5.5575€
With this module, you can calculate the **total margin** by summing percentages: With this module, you can calculate the **total margin** by summing percentages:
#### Option 1: Markup (on cost)
``` ```
Base price: 4.68€ Base price: 4.68€
Total margin: -5% + 25% = 20% Total margin: -5% + 25% = 20%
Final price: 4.68 × 1.20 = 5.616€ Final price: 4.68 × 1.20 = 5.616€
Effective margin: (5.616 - 4.68) / 5.616 = 16.67%
``` ```
**Result:** 5.62€ (effective margin: 20%) **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 ## Features
- ✅ **Additive margin calculation** across chained pricelists - ✅ **Additive margin calculation** across chained pricelists
- ✅ **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 - ✅ **Opt-in via checkbox** - doesn't affect existing pricelists
- ✅ **Compatible with custom bases** (`last_purchase_price` from `product_sale_price_from_pricelist`) - ✅ **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) - ✅ **Supports all formula extras** (price_round, price_surcharge, price_min/max_margin)
@ -84,9 +103,29 @@ Create a pricelist that chains to the base one:
- **Based on:** Other Pricelist → Select "Base Pricelist - Last Purchase Price" - **Based on:** Other Pricelist → Select "Base Pricelist - Last Purchase Price"
- **Price Computation:** Formula - **Price Computation:** Formula
- **Discount:** -25% (negative = 25% markup) - **Discount:** -25% (negative = 25% markup)
- **☑️ Use Total Margin:** Check this box! - **☑️ 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%)`
### 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** - For automatic price calculation, configure the pricelist in **Settings > Sales > Automatic Price Configuration**
- Or assign the pricelist to specific customers/partners - Or assign the pricelist to specific customers/partners
@ -113,19 +152,62 @@ After product category (+25%): 5.5575€ ❌ Wrong effective margin: 18.8%
Product: Flour (cesta básica, repostería) Product: Flour (cesta básica, repostería)
Purchase price: 4.68€ Purchase price: 4.68€
Total margin: -5% + 25% = 20% Total margin: -5% + 25% = 20%
Final price: 5.616€ ✅ Correct effective margin: 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 ## Technical Details
### New Field ### New Fields
- **Model:** `product.pricelist.item` - **Model:** `product.pricelist.item`
- **Field:** `use_total_margin` (Boolean)
#### `use_total_margin` (Boolean)
- **Default:** False (opt-in) - **Default:** False (opt-in)
- **Visibility:** Only shown when: - **Visibility:** Only shown when:
- `compute_price = 'formula'` - `compute_price = 'formula'`
- `base = 'pricelist'` (chained pricelist) - `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
### 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 ### Methods
@ -153,15 +235,37 @@ Applies additional formula options:
- `price_min_margin`: Enforce minimum margin - `price_min_margin`: Enforce minimum margin
- `price_max_margin`: Enforce maximum 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] #### `_compute_price(product, quantity, uom, date, currency)` [OVERRIDE]
Main override that: Main override that:
1. Checks if `use_total_margin=True` and conditions are met 1. Checks if `use_total_margin=True` and conditions are met
2. Calls helper methods to get base price and margins 2. Calls helper methods to get base price and margins
3. Sums margins additively: `total_margin = sum(margins)` 3. Sums margins additively: `total_margin = sum(margins)`
4. Applies total margin: `price = base_price * (1 + total_margin / 100)` 4. Applies total margin using item's selected method:
5. Applies formula extras - **Markup:** `price = base_price * (1 + total_margin / 100)`
6. Falls back to standard behavior if conditions not met - **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
**Safety:** Commercial margin >= 100% is capped at 99% to avoid division by zero
### Logging ### Logging
@ -170,7 +274,12 @@ All calculations are logged with `[TOTAL MARGIN]` prefix for easy debugging:
```python ```python
_logger.info("[TOTAL MARGIN] Item %s: base=%s, margin=%.2f%%", ...) _logger.info("[TOTAL MARGIN] Item %s: base=%s, margin=%.2f%%", ...)
_logger.info("[TOTAL MARGIN] Margins: ['5.0%', '25.0%'] = 20.0% total") _logger.info("[TOTAL MARGIN] Margins: ['5.0%', '25.0%'] = 20.0% total")
_logger.info("[TOTAL MARGIN] Base price 4.68 * (1 + 20.0%) = 5.616")
# 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: View logs:
@ -189,12 +298,18 @@ docker-compose run odoo odoo -d odoo --test-enable --stop-after-init -u product_
### Test Coverage ### Test Coverage
- ✅ Compound margin (default behavior preserved) - ✅ Compound margin (default behavior preserved)
- ✅ Total margin (additive calculation) - ✅ Total margin with Markup calculation
- ✅ Total margin with Commercial Margin calculation
- ✅ Formula extras (round, surcharge, min/max) - ✅ Formula extras (round, surcharge, min/max)
- ✅ 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 - ✅ 3-level pricelist chains
- ✅ Different base types (last_purchase_price, list_price) - ✅ Different base types (last_purchase_price, list_price)
- ✅ Currency conversions - ✅ Currency conversions
**Total:** 13 tests, all passing
## Compatibility ## Compatibility
- **Odoo Version:** 18.0 - **Odoo Version:** 18.0
@ -271,10 +386,34 @@ AGPL-3.0 or later
## Changelog ## 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
- **Markup (on cost):** `PVP = Cost × (1 + markup%)`
- **Commercial Margin (on PVP):** `PVP = Cost / (1 - margin%)`
- 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) ### 18.0.1.0.0 (2026-02-21)
- Initial implementation - Initial implementation
- Support for additive margin calculation in chained pricelists - Support for additive margin calculation in chained pricelists
- Compatible with `last_purchase_price` custom base - Compatible with `last_purchase_price` custom base
- Comprehensive test suite - Global min/max margin limits configuration
- Comprehensive test suite (9 tests)
- Detailed logging for debugging - Detailed logging for debugging

View file

@ -2,9 +2,9 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{ # noqa: B018 { # noqa: B018
"name": "Product Pricelist Total Margin", "name": "Product Pricelist Total Margin",
"version": "18.0.1.0.0", "version": "18.0.1.2.0",
"category": "Sales/Products", "category": "Sales/Products",
"summary": "Calculate total margin additively instead of compounding in chained pricelists", "summary": "Calculate total margin additively with Markup or Commercial Margin methods, enforce global limits",
"author": "Odoo Community Association (OCA), Criptomart", "author": "Odoo Community Association (OCA), Criptomart",
"website": "https://git.criptomart.net/criptomart/addons-cm", "website": "https://git.criptomart.net/criptomart/addons-cm",
"license": "AGPL-3", "license": "AGPL-3",

View file

@ -274,16 +274,21 @@ class ProductPricelistItem(models.Model):
return price return price
def _apply_global_margin_limits(self, total_margin, base_price): def _apply_global_margin_limits(self, price, base_price):
""" """
Apply global minimum and maximum margin limits from configuration. Apply global minimum and maximum margin limits from configuration.
Instead of adjusting the margin percentage, this method checks if the
calculated price respects the global limits and adjusts the price directly
if needed. The limits are interpreted according to the global_margin_type
configuration.
Args: Args:
total_margin: The calculated total margin percentage price: The calculated price
base_price: The base price (for logging) base_price: The base price (cost)
Returns: Returns:
float: The adjusted margin percentage float: The adjusted price
""" """
# Get global margin limits from configuration # Get global margin limits from configuration
IrConfigParam = self.env["ir.config_parameter"].sudo() IrConfigParam = self.env["ir.config_parameter"].sudo()
@ -297,32 +302,64 @@ class ProductPricelistItem(models.Model):
"product_pricelist_total_margin.max_percent", default="0.0" "product_pricelist_total_margin.max_percent", default="0.0"
) )
) )
global_margin_type = IrConfigParam.get_param(
"product_pricelist_total_margin.global_margin_type", default="markup"
)
original_margin = total_margin if min_margin <= 0.0 and max_margin <= 0.0:
# No limits configured
return price
# Apply minimum margin if configured (> 0) # Calculate min/max prices based on global margin type
if min_margin > 0.0 and total_margin < min_margin: if global_margin_type == "margin":
total_margin = min_margin # Commercial Margin: PVP = Cost / (1 - margin%)
if min_margin > 0.0:
if min_margin >= 100:
min_margin = 99.0
min_price = base_price / (1 - min_margin / 100)
else:
min_price = 0.0
if max_margin > 0.0:
if max_margin >= 100:
max_margin = 99.0
max_price = base_price / (1 - max_margin / 100)
else:
max_price = float("inf")
else:
# Markup: PVP = Cost × (1 + markup%)
min_price = base_price * (1 + min_margin / 100) if min_margin > 0.0 else 0.0
max_price = (
base_price * (1 + max_margin / 100)
if max_margin > 0.0
else float("inf")
)
original_price = price
# Apply minimum price limit
if min_margin > 0.0 and price < min_price:
price = min_price
_logger.info( _logger.info(
"[TOTAL MARGIN] Applied global minimum margin: %.2f%% -> %.2f%% " "[TOTAL MARGIN] Applied global minimum (%s %.2f%%): %.2f -> %.2f",
"(configured min: %.2f%%)", global_margin_type,
original_margin,
total_margin,
min_margin, min_margin,
original_price,
price,
) )
# Apply maximum margin if configured (> 0) # Apply maximum price limit
if max_margin > 0.0 and total_margin > max_margin: if max_margin > 0.0 and price > max_price:
total_margin = max_margin price = max_price
_logger.info( _logger.info(
"[TOTAL MARGIN] Applied global maximum margin: %.2f%% -> %.2f%% " "[TOTAL MARGIN] Applied global maximum (%s %.2f%%): %.2f -> %.2f",
"(configured max: %.2f%%)", global_margin_type,
original_margin,
total_margin,
max_margin, max_margin,
original_price,
price,
) )
return total_margin return price
def _compute_price( def _compute_price(
self, self,
@ -378,9 +415,6 @@ class ProductPricelistItem(models.Model):
total_margin, total_margin,
) )
# Apply global min/max margin limits
total_margin = self._apply_global_margin_limits(total_margin, base_price)
# Apply total margin to base price using selected margin type # Apply total margin to base price using selected margin type
margin_type = self.margin_type or "markup" margin_type = self.margin_type or "markup"
if margin_type == "margin": if margin_type == "margin":
@ -408,6 +442,9 @@ class ProductPricelistItem(models.Model):
price, price,
) )
# Apply global min/max margin limits (checks and adjusts price)
price = self._apply_global_margin_limits(price, base_price)
# Apply formula extras (round, surcharge, min/max margins) # Apply formula extras (round, surcharge, min/max margins)
price = self._apply_formula_extras(price, base_price, currency) price = self._apply_formula_extras(price, base_price, currency)

View file

@ -15,7 +15,7 @@ class ResConfigSettings(models.TransientModel):
"Total Margin Mode. If the calculated margin is below this value, " "Total Margin Mode. If the calculated margin is below this value, "
"the price will be adjusted to meet the minimum.\n\n" "the price will be adjusted to meet the minimum.\n\n"
"Example: If set to 10%, a product with base price 100€ will have " "Example: If set to 10%, a product with base price 100€ will have "
"a minimum final price of 110€, regardless of calculated margins.\n\n" "a minimum final price of 110€ (markup) or 111.11€ (commercial margin).\n\n"
"Set to 0 to disable minimum margin control.", "Set to 0 to disable minimum margin control.",
config_parameter="product_pricelist_total_margin.min_percent", config_parameter="product_pricelist_total_margin.min_percent",
) )
@ -27,7 +27,23 @@ class ResConfigSettings(models.TransientModel):
"Total Margin Mode. If the calculated margin exceeds this value, " "Total Margin Mode. If the calculated margin exceeds this value, "
"the price will be adjusted to meet the maximum.\n\n" "the price will be adjusted to meet the maximum.\n\n"
"Example: If set to 50%, a product with base price 100€ will have " "Example: If set to 50%, a product with base price 100€ will have "
"a maximum final price of 150€, regardless of calculated margins.\n\n" "a maximum final price of 150€ (markup) or 200€ (commercial margin).\n\n"
"Set to 0 to disable maximum margin control.", "Set to 0 to disable maximum margin control.",
config_parameter="product_pricelist_total_margin.max_percent", config_parameter="product_pricelist_total_margin.max_percent",
) )
global_margin_type = fields.Selection(
selection=[
("markup", "Markup (on cost)"),
("margin", "Commercial Margin (on list price)"),
],
string="Global Limits Calculation",
default="markup",
help="Calculation method for global minimum and maximum margin limits:\n"
"- Markup: Price = Cost × (1 + margin%). Applied limits are calculated as markup.\n"
" Example: Min 10% → Price >= Cost × 1.10\n\n"
"- Commercial Margin: Price = Cost / (1 - margin%). Applied limits are commercial margin.\n"
" Example: Min 10% → Price >= Cost / 0.90\n\n"
"This affects how the minimum and maximum percentages above are interpreted.",
config_parameter="product_pricelist_total_margin.global_margin_type",
)

View file

@ -411,3 +411,97 @@ class TestTotalMargin(TransactionCase):
places=4, places=4,
msg=f"Commercial margin should be 20%, got {commercial_margin * 100}%", msg=f"Commercial margin should be 20%, got {commercial_margin * 100}%",
) )
def test_global_minimum_with_commercial_margin_type(self):
"""Test global minimum margin with commercial margin calculation."""
# Set global minimum to 25% and type to commercial margin
self.env["ir.config_parameter"].sudo().set_param(
"product_pricelist_total_margin.min_percent", "25.0"
)
self.env["ir.config_parameter"].sudo().set_param(
"product_pricelist_total_margin.global_margin_type", "margin"
)
# Enable total margin (calculated: -5% + 25% = 20%, below min 25%)
self.item_chained.use_total_margin = True
self.item_chained.margin_type = "markup" # Item uses markup
price = self.pricelist_chained._get_product_price(
product=self.product,
quantity=1.0,
)
# Expected: Global limit is interpreted as commercial margin
# Min 25% commercial margin: PVP = Cost / (1 - 0.25) = 4.68 / 0.75 = 6.24
# Item calculates 20% markup: 4.68 * 1.20 = 5.616, but global min forces 6.24
expected_price = 4.68 / 0.75
self.assertAlmostEqual(
price,
expected_price,
places=2,
msg=f"Global minimum (commercial margin 25%) should give {expected_price}, got {price}",
)
# Verify the actual commercial margin is 25%
commercial_margin = (price - 4.68) / price
self.assertAlmostEqual(
commercial_margin,
0.25,
places=4,
msg=f"Commercial margin should be 25%, got {commercial_margin * 100}%",
)
# Clean up
self.env["ir.config_parameter"].sudo().set_param(
"product_pricelist_total_margin.min_percent", "0.0"
)
self.env["ir.config_parameter"].sudo().set_param(
"product_pricelist_total_margin.global_margin_type", "markup"
)
def test_global_maximum_with_commercial_margin_type(self):
"""Test global maximum margin with commercial margin calculation."""
# Set global maximum to 15% and type to commercial margin
self.env["ir.config_parameter"].sudo().set_param(
"product_pricelist_total_margin.max_percent", "15.0"
)
self.env["ir.config_parameter"].sudo().set_param(
"product_pricelist_total_margin.global_margin_type", "margin"
)
# Enable total margin (calculated: -5% + 25% = 20%, above max 15%)
self.item_chained.use_total_margin = True
self.item_chained.margin_type = "margin" # Item uses commercial margin
price = self.pricelist_chained._get_product_price(
product=self.product,
quantity=1.0,
)
# Expected: Global limit is interpreted as commercial margin
# Max 15% commercial margin: PVP = Cost / (1 - 0.15) = 4.68 / 0.85 = 5.506
# Item calculates 20% margin: 4.68 / 0.80 = 5.85, but global max limits to 5.506
expected_price = 4.68 / 0.85
self.assertAlmostEqual(
price,
expected_price,
places=2,
msg=f"Global maximum (commercial margin 15%) should give {expected_price}, got {price}",
)
# Verify the actual commercial margin is 15%
commercial_margin = (price - 4.68) / price
self.assertAlmostEqual(
commercial_margin,
0.15,
places=4,
msg=f"Commercial margin should be 15%, got {commercial_margin * 100}%",
)
# Clean up
self.env["ir.config_parameter"].sudo().set_param(
"product_pricelist_total_margin.max_percent", "0.0"
)
self.env["ir.config_parameter"].sudo().set_param(
"product_pricelist_total_margin.global_margin_type", "markup"
)

View file

@ -25,6 +25,14 @@
/> />
<field name="total_margin_max_percent" class="oe_inline" /> <field name="total_margin_max_percent" class="oe_inline" />
</div> </div>
<div class="row">
<label
string="Calculation Method"
for="global_margin_type"
class="col-lg-3 o_light_label"
/>
<field name="global_margin_type" class="oe_inline" />
</div>
</div> </div>
</setting> </setting>
</xpath> </xpath>