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 @@
+
+
diff --git a/modules/purchase_trade/view/fee_rule_tree.xml b/modules/purchase_trade/view/fee_rule_tree.xml
index d6847e8..b5d1a56 100644
--- a/modules/purchase_trade/view/fee_rule_tree.xml
+++ b/modules/purchase_trade/view/fee_rule_tree.xml
@@ -5,6 +5,7 @@
+