From 199caa263266665c1751cb64de460d211b33cfee Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Fri, 1 May 2026 16:32:37 +0200 Subject: [PATCH] add country to sla cost --- modules/purchase_trade/AGENTS.md | 10 +++ modules/purchase_trade/docs/business-rules.md | 28 +++++++ modules/purchase_trade/party.py | 46 +++++++++--- modules/purchase_trade/tests/test_module.py | 73 +++++++++++++++++++ .../view/party_exec_place_tree.xml | 3 +- 5 files changed, 150 insertions(+), 10 deletions(-) diff --git a/modules/purchase_trade/AGENTS.md b/modules/purchase_trade/AGENTS.md index c27ee59..91e50fa 100644 --- a/modules/purchase_trade/AGENTS.md +++ b/modules/purchase_trade/AGENTS.md @@ -166,6 +166,16 @@ de negoce physique: virtuel, puis uniquement les physiques. - detail durable: `modules/purchase_trade/docs/business-rules.md` BR-PT-020 / BR-PT-021. +- En execution controller / SLA: + - les objectifs de repartition controller utilisent `party.execution.area` + (`country.region`). + - les couts SLA utilisent `party.execution.place` et peuvent matcher par + `country`, par `location`, ou par couple `country + location`. + - la creation du fee controller depuis shipment part de + `shipment.to_location`; le pays de matching est + `shipment.to_location.country`. + - priorite cout SLA: couple pays+location, puis location seule, puis pays + seul. ## 5) Conventions de modification diff --git a/modules/purchase_trade/docs/business-rules.md b/modules/purchase_trade/docs/business-rules.md index ed10264..99b95df 100644 --- a/modules/purchase_trade/docs/business-rules.md +++ b/modules/purchase_trade/docs/business-rules.md @@ -346,6 +346,34 @@ Owner technique: `a completer` - Priorite: - `importante` +### BR-PT-014-bis - Les couts SLA controller peuvent cibler pays et/ou lieu + +- Intent: permettre de definir le cout d'un controller soit pour un pays, soit + pour une location, soit pour un couple pays + location. +- Description: + - Dans l'onglet `Execution` de `party.party`, les lignes SLA + (`party.execution.place`) peuvent porter: + - `country` + - `location` + - ou les deux. + - Lors de la creation automatique du fee controller sur un + `stock.shipment.in`, le systeme continue de partir de + `shipment.to_location`. + - Le pays utilise pour le matching est `shipment.to_location.country`. +- Priorite de matching: + - couple `country + location` + - puis `location` seule + - puis `country` seul +- Resultat attendu: + - un cout defini uniquement sur un pays s'applique a toutes les destinations + de ce pays. + - un cout defini uniquement sur une location s'applique a cette destination, + quel que soit le pays porte par la location. + - un cout defini sur le couple pays + location est le plus specifique et + prime les deux autres. +- Priorite: + - `importante` + ### BR-PT-015 - Les weight reports distants par lot partent du weight report global attache au shipment - Intent: separer la creation du `weight.report` global et l'export detaille diff --git a/modules/purchase_trade/party.py b/modules/purchase_trade/party.py index b84512f..5822337 100755 --- a/modules/purchase_trade/party.py +++ b/modules/purchase_trade/party.py @@ -87,13 +87,14 @@ class PartyExecutionSla(ModelSQL,ModelView): date_to = fields.Date("To") places = fields.One2Many('party.execution.place','pes',"") -class PartyExecutionPlace(ModelSQL,ModelView): - "Party Sla Place" - __name__ = 'party.execution.place' - - pes = fields.Many2One('party.execution.sla',"Sla") - location = fields.Many2One('stock.location',"Location") - cost = fields.Numeric("Cost",digits=(16,4)) +class PartyExecutionPlace(ModelSQL,ModelView): + "Party Sla Place" + __name__ = 'party.execution.place' + + pes = fields.Many2One('party.execution.sla',"Sla") + country = fields.Many2One('country.country',"Country") + location = fields.Many2One('stock.location',"Location") + cost = fields.Numeric("Cost",digits=(16,4)) mode = fields.Selection([ ('lumpsum', 'Lump sum'), ('perqt', 'Per qt'), @@ -139,11 +140,38 @@ class Party(metaclass=PoolMeta): best_rule = execution return best_gap, best_rule - def get_sla_cost(self,location): + def get_sla_cost(self, location): if self.sla: + country = getattr(location, 'country', None) for sla in self.sla: SlaPlace = Pool().get('party.execution.place') - sp = SlaPlace.search([('pes','=', sla.id),('location','=',location)]) + domain = [ + ('pes', '=', sla.id), + [ + 'OR', + ('location', '=', location), + ('location', '=', None), + ], + ] + if country: + domain.append([ + 'OR', + ('country', '=', country), + ('country', '=', None), + ]) + else: + domain.append(('country', '=', None)) + sp = SlaPlace.search(domain) + sp = [ + place for place in sp + if place.location or place.country + ] + sp.sort( + key=lambda place: ( + 0 if place.location and place.country else + 1 if place.location else + 2 if place.country else + 3)) if sp: return sp[0].cost,sp[0].mode,sp[0].currency,sp[0].unit return None, None, None, None diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index fef2bd1..75c0d19 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -662,6 +662,79 @@ class PurchaseTradeTestCase(ModuleTestCase): party.get_sla_cost(Mock()), (None, None, None, None)) + def test_get_sla_cost_matches_country_without_location(self): + 'controller sla helper can match a destination country' + Party = Pool().get('party.party') + party = Party() + sla = Mock(id=1) + party.sla = [sla] + country = Mock(id=10) + location = Mock(country=country) + country_place = Mock( + country=country, + location=None, + cost=Decimal('12'), + mode='ppack', + currency=Mock(id=1), + unit=Mock(id=2), + ) + place_model = Mock() + place_model.search.return_value = [country_place] + + with patch('trytond.modules.purchase_trade.party.Pool') as PoolMock: + PoolMock.return_value.get.return_value = place_model + + self.assertEqual( + party.get_sla_cost(location), + ( + country_place.cost, + country_place.mode, + country_place.currency, + country_place.unit, + )) + + def test_get_sla_cost_prioritizes_country_location_pair(self): + 'controller sla helper prefers country and location over either alone' + Party = Pool().get('party.party') + party = Party() + sla = Mock(id=1) + party.sla = [sla] + country = Mock(id=10) + location = Mock(country=country) + country_only = Mock( + country=country, + location=None, + cost=Decimal('12'), + mode='ppack', + currency=Mock(id=1), + unit=Mock(id=2), + ) + location_only = Mock( + country=None, + location=location, + cost=Decimal('14'), + mode='perqt', + currency=Mock(id=3), + unit=Mock(id=4), + ) + pair = Mock( + country=country, + location=location, + cost=Decimal('16'), + mode='rate', + currency=Mock(id=5), + unit=Mock(id=6), + ) + place_model = Mock() + place_model.search.return_value = [country_only, location_only, pair] + + with patch('trytond.modules.purchase_trade.party.Pool') as PoolMock: + PoolMock.return_value.get.return_value = place_model + + self.assertEqual( + party.get_sla_cost(location), + (pair.cost, pair.mode, pair.currency, pair.unit)) + def test_get_party_by_name_adds_missing_category_to_existing_party(self): 'existing parties found by automation gain the requested category when missing' Party = Pool().get('party.party') diff --git a/modules/purchase_trade/view/party_exec_place_tree.xml b/modules/purchase_trade/view/party_exec_place_tree.xml index 5f48d9c..a2e3dd7 100644 --- a/modules/purchase_trade/view/party_exec_place_tree.xml +++ b/modules/purchase_trade/view/party_exec_place_tree.xml @@ -1,7 +1,8 @@ + - \ No newline at end of file +