Add counter to controller
This commit is contained in:
@@ -291,6 +291,27 @@ Owner technique: `a completer`
|
|||||||
- les lignes existantes ne sont pas modifiees retroactivement
|
- les lignes existantes ne sont pas modifiees retroactivement
|
||||||
- Priorite:
|
- Priorite:
|
||||||
- `importante`
|
- `importante`
|
||||||
|
|
||||||
|
### BR-PT-014 - L'affectation d'un controller doit suivre l'ecart a l'objectif regional
|
||||||
|
|
||||||
|
- Intent: repartir les controllers selon les cibles definies dans l'onglet
|
||||||
|
`Execution` des `party.party`.
|
||||||
|
- Description:
|
||||||
|
- chaque ligne `party.execution` fixe une cible `% targeted` pour un
|
||||||
|
controller sur une `country.region`
|
||||||
|
- le `% achieved` est calcule a partir des `stock.shipment.in` deja affectes
|
||||||
|
a un controller dans cette zone
|
||||||
|
- la zone d'un shipment est determinee par `shipment.to_location.country`
|
||||||
|
- une region parente couvre aussi ses sous-regions
|
||||||
|
- Resultat attendu:
|
||||||
|
- pour une ligne `party.execution`, `achieved_percent` =
|
||||||
|
`shipments de la zone avec ce controller / shipments controles de la zone`
|
||||||
|
- lors d'un choix automatique de controller, la priorite va a la regle dont
|
||||||
|
l'ecart `targeted - achieved` est le plus eleve
|
||||||
|
- un controller a `80%` cible et `40%` reel doit donc passer avant un
|
||||||
|
controller a `50%` cible et `45%` reel sur la meme zone
|
||||||
|
- Priorite:
|
||||||
|
- `importante`
|
||||||
- Resultat attendu:
|
- Resultat attendu:
|
||||||
- apres creation du lot virtuel, si aucun matching purchase n'existe:
|
- apres creation du lot virtuel, si aucun matching purchase n'existe:
|
||||||
- appeler `Valuation.generate_from_sale_line(line)`
|
- appeler `Valuation.generate_from_sale_line(line)`
|
||||||
|
|||||||
@@ -7,17 +7,71 @@ from decimal import getcontext, Decimal, ROUND_HALF_UP
|
|||||||
from sql import Table
|
from sql import Table
|
||||||
from trytond.pyson import Bool, Eval, Id, If
|
from trytond.pyson import Bool, Eval, Id, If
|
||||||
|
|
||||||
class PartyExecution(ModelSQL,ModelView):
|
class PartyExecution(ModelSQL,ModelView):
|
||||||
"Party Execution"
|
"Party Execution"
|
||||||
__name__ = 'party.execution'
|
__name__ = 'party.execution'
|
||||||
|
|
||||||
party = fields.Many2One('party.party',"Party")
|
party = fields.Many2One('party.party',"Party")
|
||||||
area = fields.Many2One('country.region',"Area")
|
area = fields.Many2One('country.region',"Area")
|
||||||
percent = fields.Numeric("% targeted")
|
percent = fields.Numeric("% targeted")
|
||||||
achieved_percent = fields.Function(fields.Numeric("% achieved"),'get_percent')
|
achieved_percent = fields.Function(fields.Numeric("% achieved"),'get_percent')
|
||||||
|
|
||||||
def get_percent(self,name):
|
@staticmethod
|
||||||
return 2
|
def _to_decimal(value):
|
||||||
|
if value is None:
|
||||||
|
return Decimal('0')
|
||||||
|
if isinstance(value, Decimal):
|
||||||
|
return value
|
||||||
|
return Decimal(str(value))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _round_percent(cls, value):
|
||||||
|
return cls._to_decimal(value).quantize(
|
||||||
|
Decimal('0.01'), rounding=ROUND_HALF_UP)
|
||||||
|
|
||||||
|
def matches_country(self, country):
|
||||||
|
if not self.area or not country or not getattr(country, 'region', None):
|
||||||
|
return False
|
||||||
|
region = country.region
|
||||||
|
while region:
|
||||||
|
if region.id == self.area.id:
|
||||||
|
return True
|
||||||
|
region = getattr(region, 'parent', None)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def matches_shipment(self, shipment):
|
||||||
|
location = getattr(shipment, 'to_location', None)
|
||||||
|
country = getattr(location, 'country', None)
|
||||||
|
return self.matches_country(country)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def compute_achieved_percent_for(cls, party, area):
|
||||||
|
if not party or not area:
|
||||||
|
return Decimal('0')
|
||||||
|
Shipment = Pool().get('stock.shipment.in')
|
||||||
|
shipments = Shipment.search([
|
||||||
|
('controller', '!=', None),
|
||||||
|
('to_location.country', '!=', None),
|
||||||
|
('to_location.country.region', 'child_of', [area.id]),
|
||||||
|
])
|
||||||
|
total = len(shipments)
|
||||||
|
if not total:
|
||||||
|
return Decimal('0')
|
||||||
|
achieved = sum(
|
||||||
|
1 for shipment in shipments
|
||||||
|
if shipment.controller and shipment.controller.id == party.id)
|
||||||
|
return cls._round_percent(
|
||||||
|
(Decimal(achieved) * Decimal('100')) / Decimal(total))
|
||||||
|
|
||||||
|
def compute_achieved_percent(self):
|
||||||
|
return self.__class__.compute_achieved_percent_for(
|
||||||
|
self.party, self.area)
|
||||||
|
|
||||||
|
def get_target_gap(self):
|
||||||
|
return self._to_decimal(self.percent) - self.compute_achieved_percent()
|
||||||
|
|
||||||
|
def get_percent(self,name):
|
||||||
|
return self.compute_achieved_percent()
|
||||||
|
|
||||||
class PartyExecutionSla(ModelSQL,ModelView):
|
class PartyExecutionSla(ModelSQL,ModelView):
|
||||||
"Party Execution Sla"
|
"Party Execution Sla"
|
||||||
@@ -55,8 +109,8 @@ class PartyExecutionPlace(ModelSQL,ModelView):
|
|||||||
'readonly': Eval('mode') != 'ppack',
|
'readonly': Eval('mode') != 'ppack',
|
||||||
})
|
})
|
||||||
|
|
||||||
class Party(metaclass=PoolMeta):
|
class Party(metaclass=PoolMeta):
|
||||||
__name__ = 'party.party'
|
__name__ = 'party.party'
|
||||||
|
|
||||||
tol_min = fields.Numeric("Tol - in %")
|
tol_min = fields.Numeric("Tol - in %")
|
||||||
tol_max = fields.Numeric("Tol + in %")
|
tol_max = fields.Numeric("Tol + in %")
|
||||||
@@ -65,13 +119,25 @@ class Party(metaclass=PoolMeta):
|
|||||||
origin =fields.Char("Origin")
|
origin =fields.Char("Origin")
|
||||||
execution = fields.One2Many('party.execution','party',"")
|
execution = fields.One2Many('party.execution','party',"")
|
||||||
sla = fields.One2Many('party.execution.sla','party', "Sla")
|
sla = fields.One2Many('party.execution.sla','party', "Sla")
|
||||||
initial = fields.Char("Initials")
|
initial = fields.Char("Initials")
|
||||||
|
|
||||||
def IsAvailableForControl(self,sh):
|
def IsAvailableForControl(self,sh):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_sla_cost(self,location):
|
def get_controller_execution_priority(self, shipment):
|
||||||
if self.sla:
|
best_rule = None
|
||||||
|
best_gap = None
|
||||||
|
for execution in self.execution or []:
|
||||||
|
if not execution.matches_shipment(shipment):
|
||||||
|
continue
|
||||||
|
gap = execution.get_target_gap()
|
||||||
|
if best_gap is None or gap > best_gap:
|
||||||
|
best_gap = gap
|
||||||
|
best_rule = execution
|
||||||
|
return best_gap, best_rule
|
||||||
|
|
||||||
|
def get_sla_cost(self,location):
|
||||||
|
if self.sla:
|
||||||
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)])
|
sp = SlaPlace.search([('pes','=', sla.id),('location','=',location)])
|
||||||
@@ -111,4 +177,4 @@ class Party(metaclass=PoolMeta):
|
|||||||
pc.category = cat[0].id
|
pc.category = cat[0].id
|
||||||
PartyCategory.save([pc])
|
PartyCategory.save([pc])
|
||||||
return p
|
return p
|
||||||
|
|
||||||
|
|||||||
@@ -487,16 +487,26 @@ class ShipmentIn(metaclass=PoolMeta):
|
|||||||
fee.price = price
|
fee.price = price
|
||||||
Fee.save([fee])
|
Fee.save([fee])
|
||||||
|
|
||||||
def get_controller(self):
|
def get_controller(self):
|
||||||
ControllerCategory = Pool().get('party.category')
|
ControllerCategory = Pool().get('party.category')
|
||||||
PartyCategory = Pool().get('party.party-party.category')
|
PartyCategory = Pool().get('party.party-party.category')
|
||||||
cc = ControllerCategory.search(['name','=','CONTROLLER'])
|
cc = ControllerCategory.search(['name','=','CONTROLLER'])
|
||||||
if cc:
|
if cc:
|
||||||
cc = cc[0]
|
cc = cc[0]
|
||||||
controllers = PartyCategory.search(['category','=',cc.id])
|
controllers = PartyCategory.search(['category','=',cc.id])
|
||||||
for c in controllers:
|
prioritized = []
|
||||||
if c.party.IsAvailableForControl(self):
|
for c in controllers:
|
||||||
return c.party
|
if not c.party.IsAvailableForControl(self):
|
||||||
|
continue
|
||||||
|
gap, rule = c.party.get_controller_execution_priority(self)
|
||||||
|
prioritized.append((
|
||||||
|
1 if rule else 0,
|
||||||
|
gap if gap is not None else Decimal('-999999'),
|
||||||
|
c.party,
|
||||||
|
))
|
||||||
|
if prioritized:
|
||||||
|
prioritized.sort(key=lambda item: (item[0], item[1]), reverse=True)
|
||||||
|
return prioritized[0][2]
|
||||||
|
|
||||||
def get_instructions_html(self,inv_date,inv_nb):
|
def get_instructions_html(self,inv_date,inv_nb):
|
||||||
vessel = self.vessel.vessel_name if self.vessel else ""
|
vessel = self.vessel.vessel_name if self.vessel else ""
|
||||||
|
|||||||
@@ -184,6 +184,81 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
PurchaseLine.default_pricing_rule(), 'Default pricing rule')
|
PurchaseLine.default_pricing_rule(), 'Default pricing rule')
|
||||||
|
|
||||||
|
def test_party_execution_achieved_percent_uses_real_area_statistics(self):
|
||||||
|
'party execution achieved percent reflects the controller share in its area'
|
||||||
|
PartyExecution = Pool().get('party.execution')
|
||||||
|
execution = PartyExecution()
|
||||||
|
execution.party = Mock(id=1)
|
||||||
|
execution.area = Mock(id=10)
|
||||||
|
|
||||||
|
shipments = [
|
||||||
|
Mock(controller=Mock(id=1)),
|
||||||
|
Mock(controller=Mock(id=2)),
|
||||||
|
Mock(controller=Mock(id=1)),
|
||||||
|
Mock(controller=Mock(id=2)),
|
||||||
|
Mock(controller=Mock(id=1)),
|
||||||
|
]
|
||||||
|
shipment_model = Mock()
|
||||||
|
shipment_model.search.return_value = shipments
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
'trytond.modules.purchase_trade.party.Pool'
|
||||||
|
) as PoolMock:
|
||||||
|
PoolMock.return_value.get.return_value = shipment_model
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
execution.get_percent('achieved_percent'),
|
||||||
|
Decimal('60.00'))
|
||||||
|
|
||||||
|
def test_get_controller_prioritizes_controller_farthest_from_target(self):
|
||||||
|
'shipment controller selection prioritizes the most under-target rule'
|
||||||
|
Shipment = Pool().get('stock.shipment.in')
|
||||||
|
Party = Pool().get('party.party')
|
||||||
|
PartyExecution = Pool().get('party.execution')
|
||||||
|
|
||||||
|
shipment = Shipment()
|
||||||
|
shipment.to_location = Mock(
|
||||||
|
country=Mock(region=Mock(id=20, parent=Mock(id=10, parent=None))))
|
||||||
|
|
||||||
|
party_a = Party()
|
||||||
|
party_a.id = 1
|
||||||
|
rule_a = PartyExecution()
|
||||||
|
rule_a.party = party_a
|
||||||
|
rule_a.area = Mock(id=10)
|
||||||
|
rule_a.percent = Decimal('80')
|
||||||
|
rule_a.compute_achieved_percent = Mock(return_value=Decimal('40'))
|
||||||
|
party_a.execution = [rule_a]
|
||||||
|
|
||||||
|
party_b = Party()
|
||||||
|
party_b.id = 2
|
||||||
|
rule_b = PartyExecution()
|
||||||
|
rule_b.party = party_b
|
||||||
|
rule_b.area = Mock(id=10)
|
||||||
|
rule_b.percent = Decimal('50')
|
||||||
|
rule_b.compute_achieved_percent = Mock(return_value=Decimal('45'))
|
||||||
|
party_b.execution = [rule_b]
|
||||||
|
|
||||||
|
category_model = Mock()
|
||||||
|
category_model.search.return_value = [Mock(id=99)]
|
||||||
|
party_category_model = Mock()
|
||||||
|
party_category_model.search.return_value = [
|
||||||
|
Mock(party=party_b),
|
||||||
|
Mock(party=party_a),
|
||||||
|
]
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
'trytond.modules.purchase_trade.stock.Pool'
|
||||||
|
) as PoolMock:
|
||||||
|
def get_model(name):
|
||||||
|
return {
|
||||||
|
'party.category': category_model,
|
||||||
|
'party.party-party.category': party_category_model,
|
||||||
|
}[name]
|
||||||
|
|
||||||
|
PoolMock.return_value.get.side_effect = get_model
|
||||||
|
|
||||||
|
self.assertIs(shipment.get_controller(), party_a)
|
||||||
|
|
||||||
def test_sale_report_multi_line_helpers_aggregate_all_lines(self):
|
def test_sale_report_multi_line_helpers_aggregate_all_lines(self):
|
||||||
'sale report helpers aggregate quantity, price lines and shipment periods'
|
'sale report helpers aggregate quantity, price lines and shipment periods'
|
||||||
Sale = Pool().get('sale.sale')
|
Sale = Pool().get('sale.sale')
|
||||||
|
|||||||
Reference in New Issue
Block a user