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
|
||||
- Priorite:
|
||||
- `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:
|
||||
- apres creation du lot virtuel, si aucun matching purchase n'existe:
|
||||
- appeler `Valuation.generate_from_sale_line(line)`
|
||||
|
||||
@@ -7,17 +7,71 @@ from decimal import getcontext, Decimal, ROUND_HALF_UP
|
||||
from sql import Table
|
||||
from trytond.pyson import Bool, Eval, Id, If
|
||||
|
||||
class PartyExecution(ModelSQL,ModelView):
|
||||
"Party Execution"
|
||||
__name__ = 'party.execution'
|
||||
class PartyExecution(ModelSQL,ModelView):
|
||||
"Party Execution"
|
||||
__name__ = 'party.execution'
|
||||
|
||||
party = fields.Many2One('party.party',"Party")
|
||||
area = fields.Many2One('country.region',"Area")
|
||||
percent = fields.Numeric("% targeted")
|
||||
achieved_percent = fields.Function(fields.Numeric("% achieved"),'get_percent')
|
||||
|
||||
def get_percent(self,name):
|
||||
return 2
|
||||
area = fields.Many2One('country.region',"Area")
|
||||
percent = fields.Numeric("% targeted")
|
||||
achieved_percent = fields.Function(fields.Numeric("% achieved"),'get_percent')
|
||||
|
||||
@staticmethod
|
||||
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):
|
||||
"Party Execution Sla"
|
||||
@@ -55,8 +109,8 @@ class PartyExecutionPlace(ModelSQL,ModelView):
|
||||
'readonly': Eval('mode') != 'ppack',
|
||||
})
|
||||
|
||||
class Party(metaclass=PoolMeta):
|
||||
__name__ = 'party.party'
|
||||
class Party(metaclass=PoolMeta):
|
||||
__name__ = 'party.party'
|
||||
|
||||
tol_min = fields.Numeric("Tol - in %")
|
||||
tol_max = fields.Numeric("Tol + in %")
|
||||
@@ -65,13 +119,25 @@ class Party(metaclass=PoolMeta):
|
||||
origin =fields.Char("Origin")
|
||||
execution = fields.One2Many('party.execution','party',"")
|
||||
sla = fields.One2Many('party.execution.sla','party', "Sla")
|
||||
initial = fields.Char("Initials")
|
||||
|
||||
def IsAvailableForControl(self,sh):
|
||||
return True
|
||||
|
||||
def get_sla_cost(self,location):
|
||||
if self.sla:
|
||||
initial = fields.Char("Initials")
|
||||
|
||||
def IsAvailableForControl(self,sh):
|
||||
return True
|
||||
|
||||
def get_controller_execution_priority(self, shipment):
|
||||
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:
|
||||
SlaPlace = Pool().get('party.execution.place')
|
||||
sp = SlaPlace.search([('pes','=', sla.id),('location','=',location)])
|
||||
@@ -111,4 +177,4 @@ class Party(metaclass=PoolMeta):
|
||||
pc.category = cat[0].id
|
||||
PartyCategory.save([pc])
|
||||
return p
|
||||
|
||||
|
||||
|
||||
@@ -487,16 +487,26 @@ class ShipmentIn(metaclass=PoolMeta):
|
||||
fee.price = price
|
||||
Fee.save([fee])
|
||||
|
||||
def get_controller(self):
|
||||
ControllerCategory = Pool().get('party.category')
|
||||
PartyCategory = Pool().get('party.party-party.category')
|
||||
cc = ControllerCategory.search(['name','=','CONTROLLER'])
|
||||
if cc:
|
||||
cc = cc[0]
|
||||
controllers = PartyCategory.search(['category','=',cc.id])
|
||||
for c in controllers:
|
||||
if c.party.IsAvailableForControl(self):
|
||||
return c.party
|
||||
def get_controller(self):
|
||||
ControllerCategory = Pool().get('party.category')
|
||||
PartyCategory = Pool().get('party.party-party.category')
|
||||
cc = ControllerCategory.search(['name','=','CONTROLLER'])
|
||||
if cc:
|
||||
cc = cc[0]
|
||||
controllers = PartyCategory.search(['category','=',cc.id])
|
||||
prioritized = []
|
||||
for c in controllers:
|
||||
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):
|
||||
vessel = self.vessel.vessel_name if self.vessel else ""
|
||||
|
||||
@@ -184,6 +184,81 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
self.assertEqual(
|
||||
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):
|
||||
'sale report helpers aggregate quantity, price lines and shipment periods'
|
||||
Sale = Pool().get('sale.sale')
|
||||
|
||||
Reference in New Issue
Block a user