add country to sla cost

This commit is contained in:
2026-05-01 16:32:37 +02:00
parent 0f218374a7
commit 199caa2632
5 changed files with 150 additions and 10 deletions

View File

@@ -166,6 +166,16 @@ de negoce physique:
virtuel, puis uniquement les physiques. virtuel, puis uniquement les physiques.
- detail durable: - detail durable:
`modules/purchase_trade/docs/business-rules.md` BR-PT-020 / BR-PT-021. `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 ## 5) Conventions de modification

View File

@@ -346,6 +346,34 @@ Owner technique: `a completer`
- Priorite: - Priorite:
- `importante` - `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 ### 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 - Intent: separer la creation du `weight.report` global et l'export detaille

View File

@@ -92,6 +92,7 @@ class PartyExecutionPlace(ModelSQL,ModelView):
__name__ = 'party.execution.place' __name__ = 'party.execution.place'
pes = fields.Many2One('party.execution.sla',"Sla") pes = fields.Many2One('party.execution.sla',"Sla")
country = fields.Many2One('country.country',"Country")
location = fields.Many2One('stock.location',"Location") location = fields.Many2One('stock.location',"Location")
cost = fields.Numeric("Cost",digits=(16,4)) cost = fields.Numeric("Cost",digits=(16,4))
mode = fields.Selection([ mode = fields.Selection([
@@ -139,11 +140,38 @@ class Party(metaclass=PoolMeta):
best_rule = execution best_rule = execution
return best_gap, best_rule return best_gap, best_rule
def get_sla_cost(self,location): def get_sla_cost(self, location):
if self.sla: if self.sla:
country = getattr(location, 'country', None)
for sla in self.sla: for sla in self.sla:
SlaPlace = Pool().get('party.execution.place') 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: if sp:
return sp[0].cost,sp[0].mode,sp[0].currency,sp[0].unit return sp[0].cost,sp[0].mode,sp[0].currency,sp[0].unit
return None, None, None, None return None, None, None, None

View File

@@ -662,6 +662,79 @@ class PurchaseTradeTestCase(ModuleTestCase):
party.get_sla_cost(Mock()), party.get_sla_cost(Mock()),
(None, None, None, None)) (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): def test_get_party_by_name_adds_missing_category_to_existing_party(self):
'existing parties found by automation gain the requested category when missing' 'existing parties found by automation gain the requested category when missing'
Party = Pool().get('party.party') Party = Pool().get('party.party')

View File

@@ -1,4 +1,5 @@
<tree editable="1"> <tree editable="1">
<field name="country"/>
<field name="location"/> <field name="location"/>
<field name="mode"/> <field name="mode"/>
<field name="cost"/> <field name="cost"/>