From c6d7b0da2b627f08c62cd065c3cb98fa93221712 Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Mon, 25 May 2026 14:17:08 +0200 Subject: [PATCH] Fee rules --- modules/purchase_trade/fee.py | 23 +++++++++++++++++ modules/purchase_trade/fee.xml | 18 +++++++------ modules/purchase_trade/tests/test_module.py | 25 +++++++++++++++++++ modules/purchase_trade/view/fee_rule_form.xml | 2 ++ modules/purchase_trade/view/fee_rule_tree.xml | 1 + 5 files changed, 62 insertions(+), 7 deletions(-) diff --git a/modules/purchase_trade/fee.py b/modules/purchase_trade/fee.py index 677ecc0..8706284 100755 --- a/modules/purchase_trade/fee.py +++ b/modules/purchase_trade/fee.py @@ -992,6 +992,8 @@ class FeeRule(ModelSQL, ModelView): auto_apply = fields.Boolean("Auto Apply") party = fields.Many2One('party.party', "Counterparty") + party_category = fields.Many2One( + 'party.category', "Counterparty Category") line_product = fields.Many2One('product.product', "Line Product") incoterm_code = fields.Char("Incoterm Code") from_location = fields.Many2One('stock.location', "From Location") @@ -1054,6 +1056,24 @@ class FeeRule(ModelSQL, ModelView): left and right and getattr(left, 'id', left) == getattr(right, 'id', right)) + @classmethod + def _category_matches(cls, category, target): + while target: + if cls._same_record(category, target): + return True + target = getattr(target, 'parent', None) + return False + + @classmethod + def _party_has_category(cls, party, category): + if not category: + return True + if not party: + return False + return any( + cls._category_matches(category, party_category) + for party_category in (getattr(party, 'categories', []) or [])) + @classmethod def _rules_for_lines(cls, apply_on, auto_only=False): domain = [ @@ -1077,6 +1097,9 @@ class FeeRule(ModelSQL, ModelView): if any(rule_value and not self._same_record(rule_value, actual) for rule_value, actual in checks): return False + if not self._party_has_category( + getattr(contract, 'party', None), self.party_category): + return False if self.incoterm_code: incoterm = getattr(contract, 'incoterm', None) if (getattr(incoterm, 'code', '') or '').upper() != ( diff --git a/modules/purchase_trade/fee.xml b/modules/purchase_trade/fee.xml index 1d6ed50..5f9480f 100755 --- a/modules/purchase_trade/fee.xml +++ b/modules/purchase_trade/fee.xml @@ -148,16 +148,20 @@ this repository contains the full copyright notices and license terms. --> + name="Fees" + sequence="99" + id="menu_fees"/> + diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 5931533..2e57ab9 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -1812,6 +1812,31 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertTrue(rule._matches_line(line, 'purchase_line')) + def test_fee_rule_matches_counterparty_category_parent(self): + 'fee rule can match a counterparty by parent category' + parent_category = Mock(id=1, parent=None) + child_category = Mock(id=2, parent=parent_category) + party = Mock(id=3, categories=[child_category]) + contract = Mock( + party=party, + incoterm=None, + from_location=None, + to_location=None, + plan=None, + ) + line = Mock(product=None, purchase=contract, sale=None) + rule = fee_module.FeeRule() + rule.apply_on = 'purchase_line' + rule.party = None + rule.party_category = parent_category + rule.line_product = None + rule.incoterm_code = None + rule.from_location = None + rule.to_location = None + rule.execution_plan = None + + self.assertTrue(rule._matches_line(line, 'purchase_line')) + def test_fee_rule_builds_budgeted_sale_fee_values(self): 'fee rule builds budgeted fee values for a sale line' currency = Mock(id=1) diff --git a/modules/purchase_trade/view/fee_rule_form.xml b/modules/purchase_trade/view/fee_rule_form.xml index 7c4dd85..b5bd07b 100644 --- a/modules/purchase_trade/view/fee_rule_form.xml +++ b/modules/purchase_trade/view/fee_rule_form.xml @@ -12,6 +12,8 @@