Fee adj when th qt change
This commit is contained in:
@@ -71,6 +71,11 @@ Guide rapide pour les agents qui codent dans ce repository.
|
|||||||
- `modules/purchase_trade/docs/business-rules.md`
|
- `modules/purchase_trade/docs/business-rules.md`
|
||||||
- Decisions templates / reports:
|
- Decisions templates / reports:
|
||||||
- `notes/template_business_rules.md`
|
- `notes/template_business_rules.md`
|
||||||
|
- Regles sensibles `purchase_trade` a relire avant de toucher lots, quantites
|
||||||
|
ou fees:
|
||||||
|
- `modules/purchase_trade/AGENTS.md`
|
||||||
|
- `modules/purchase_trade/docs/business-rules.md` BR-PT-020 / BR-PT-021
|
||||||
|
(`quantity_theorical`, `lot.qt`, lots physiques, fees et PnL fee).
|
||||||
|
|
||||||
## 5) Workflow de modification (obligatoire)
|
## 5) Workflow de modification (obligatoire)
|
||||||
|
|
||||||
|
|||||||
@@ -151,6 +151,21 @@ de negoce physique:
|
|||||||
taux de la provisoire
|
taux de la provisoire
|
||||||
- le wizard final doit calculer son delta depuis la quantite provisoire hors
|
- le wizard final doit calculer son delta depuis la quantite provisoire hors
|
||||||
padding: `sale_invoice_line_prov.quantity - sale_invoice_padding`
|
padding: `sale_invoice_line_prov.quantity - sale_invoice_padding`
|
||||||
|
- En modification de quantite contractuelle:
|
||||||
|
- `quantity_theorical` est la source contractuelle; `quantity` peut refleter
|
||||||
|
l'execute physique.
|
||||||
|
- le `lot.qt` libre doit etre resynchronise sur le solde ouvert reel:
|
||||||
|
`quantity_theorical - lots physiques - lot.qt deja matches/shippes`.
|
||||||
|
- ne jamais ajuster le `lot.qt` libre uniquement par delta si des lots
|
||||||
|
physiques existent deja.
|
||||||
|
- les fees suivent les lots effectifs: physiques s'il en existe dans
|
||||||
|
`fee.lots`, sinon virtuel.
|
||||||
|
- conserver le lien `fee.lots` vers le virtuel comme fallback; ne pas le
|
||||||
|
supprimer lors de la creation d'un physique.
|
||||||
|
- le PnL fee applique la meme regle: full open tant qu'il n'y a que le
|
||||||
|
virtuel, puis uniquement les physiques.
|
||||||
|
- detail durable:
|
||||||
|
`modules/purchase_trade/docs/business-rules.md` BR-PT-020 / BR-PT-021.
|
||||||
|
|
||||||
## 5) Conventions de modification
|
## 5) Conventions de modification
|
||||||
|
|
||||||
|
|||||||
@@ -587,6 +587,51 @@ Owner technique: `a completer`
|
|||||||
- exemple: une ligne achat passee de `10000` a `20000` avec deja `10000`
|
- exemple: une ligne achat passee de `10000` a `20000` avec deja `10000`
|
||||||
physiques doit afficher `10000` ouverts et `10000` physiques, pas
|
physiques doit afficher `10000` ouverts et `10000` physiques, pas
|
||||||
`20000` ouverts plus `10000` physiques.
|
`20000` ouverts plus `10000` physiques.
|
||||||
|
- Impacts fees:
|
||||||
|
- apres toute modification de `quantity_theorical`, les fees de la ligne sont
|
||||||
|
resynchronises avec la regle BR-PT-021.
|
||||||
|
- si le fee est encore uniquement porte par le lot virtuel, sa quantite suit
|
||||||
|
donc la nouvelle quantite virtuelle.
|
||||||
|
- si le fee possede deja des lots physiques, une hausse ou baisse uniquement
|
||||||
|
ouverte n'impacte pas sa quantite.
|
||||||
|
- Priorite:
|
||||||
|
- `structurante`
|
||||||
|
|
||||||
|
### BR-PT-021 - Les fees lies aux lots privilegient les physiques
|
||||||
|
|
||||||
|
- Intent: eviter qu'un fee cree sur un lot virtuel reste calcule sur la
|
||||||
|
quantite contractuelle totale apres creation de lots physiques.
|
||||||
|
- Description:
|
||||||
|
- Le lien `fee.lots` avec le lot virtuel est conserve comme fallback.
|
||||||
|
- Des qu'un fee possede au moins un lot physique dans `fee.lots`, les lots
|
||||||
|
physiques deviennent la base effective du fee et le virtuel est ignore pour
|
||||||
|
la quantite.
|
||||||
|
- Resultat attendu:
|
||||||
|
- si aucun physique n'existe, `fee.quantity` suit le lot virtuel rattache au
|
||||||
|
fee.
|
||||||
|
- si des physiques existent, `fee.quantity` suit uniquement la somme des lots
|
||||||
|
physiques rattaches au fee.
|
||||||
|
- pour `ppack`, la somme se fait sur `lot.lot_qt` des physiques.
|
||||||
|
- pour les modes quantitatifs (`perqt`, `rate`, `pprice`, `pcost`), la somme
|
||||||
|
se fait sur les quantites courantes converties des lots physiques.
|
||||||
|
- supprimer le dernier physique fait retomber le fee sur son lot virtuel,
|
||||||
|
puisque le lien virtuel est conserve.
|
||||||
|
- le PnL fee applique la meme selection: full open tant qu'il n'y a que le
|
||||||
|
virtuel, puis uniquement les lots physiques effectifs.
|
||||||
|
- Points de synchronisation obligatoires:
|
||||||
|
- creation d'un fee lie a une ligne ou a un shipment
|
||||||
|
- ajout d'un lot physique dans `fee.lots`
|
||||||
|
- modification de `purchase.line.quantity_theorical` ou
|
||||||
|
`sale.line.quantity_theorical`
|
||||||
|
- weighing / modification de quantite d'un lot physique
|
||||||
|
- suppression d'un lot physique ou suppression d'un lien `fee.lots`
|
||||||
|
- Regle de conception:
|
||||||
|
- ne pas supprimer le lien `fee.lots` vers le lot virtuel lors de la creation
|
||||||
|
de physiques.
|
||||||
|
- le virtuel reste le fallback permettant de revenir au cas ouvert si tous
|
||||||
|
les physiques sont supprimes.
|
||||||
|
- toute logique metier, comptable ou PnL doit utiliser les lots effectifs du
|
||||||
|
fee: physiques s'il y en a, sinon virtuels.
|
||||||
- Priorite:
|
- Priorite:
|
||||||
- `structurante`
|
- `structurante`
|
||||||
|
|
||||||
|
|||||||
@@ -331,6 +331,52 @@ class Fee(ModelSQL,ModelView):
|
|||||||
if lqts:
|
if lqts:
|
||||||
return Decimal(lqts[0].lot_quantity)
|
return Decimal(lqts[0].lot_quantity)
|
||||||
|
|
||||||
|
def _get_effective_fee_lots(self):
|
||||||
|
FeeLots = Pool().get('fee.lots')
|
||||||
|
fee_lots = FeeLots.search([('fee', '=', self.id)])
|
||||||
|
lots = [fee_lot.lot for fee_lot in fee_lots if fee_lot.lot]
|
||||||
|
physical_lots = [
|
||||||
|
lot for lot in lots
|
||||||
|
if getattr(lot, 'lot_type', None) == 'physic'
|
||||||
|
]
|
||||||
|
if physical_lots:
|
||||||
|
return physical_lots
|
||||||
|
return [
|
||||||
|
lot for lot in lots
|
||||||
|
if getattr(lot, 'lot_type', None) == 'virtual'
|
||||||
|
]
|
||||||
|
|
||||||
|
def is_effective_fee_lot(self, lot):
|
||||||
|
return bool(lot and lot in self._get_effective_fee_lots())
|
||||||
|
|
||||||
|
def _get_effective_fee_lots_quantity(self):
|
||||||
|
lots = self._get_effective_fee_lots()
|
||||||
|
if not lots:
|
||||||
|
return None
|
||||||
|
if self.mode == 'ppack':
|
||||||
|
packing_qty = sum(
|
||||||
|
Decimal(str(getattr(lot, 'lot_qt', 0) or 0))
|
||||||
|
for lot in lots)
|
||||||
|
if packing_qty:
|
||||||
|
return packing_qty
|
||||||
|
return self.quantity
|
||||||
|
return sum(
|
||||||
|
Decimal(lot.get_current_quantity_converted(0, self.unit) or 0)
|
||||||
|
for lot in lots)
|
||||||
|
|
||||||
|
def sync_quantity_from_lots(self):
|
||||||
|
if self.mode == 'lumpsum':
|
||||||
|
return
|
||||||
|
quantity = self._get_effective_fee_lots_quantity()
|
||||||
|
if quantity is None:
|
||||||
|
return
|
||||||
|
quantity = round(Decimal(quantity), 5)
|
||||||
|
if self.mode == 'ppack':
|
||||||
|
quantity = quantity.to_integral_value(rounding=ROUND_UP)
|
||||||
|
if Decimal(self.quantity or 0) != quantity:
|
||||||
|
self.quantity = quantity
|
||||||
|
self.__class__.save([self])
|
||||||
|
|
||||||
def _get_amount_quantity(self):
|
def _get_amount_quantity(self):
|
||||||
quantity = self.quantity
|
quantity = self.quantity
|
||||||
if quantity is None:
|
if quantity is None:
|
||||||
@@ -425,10 +471,11 @@ class Fee(ModelSQL,ModelView):
|
|||||||
|
|
||||||
def get_fee_lots_qt(self,state_id=0):
|
def get_fee_lots_qt(self,state_id=0):
|
||||||
qt = Decimal(0)
|
qt = Decimal(0)
|
||||||
FeeLots = Pool().get('fee.lots')
|
lots = self._get_effective_fee_lots()
|
||||||
fee_lots = FeeLots.search([('fee', '=', self.id)])
|
if lots:
|
||||||
if fee_lots:
|
qt = sum([
|
||||||
qt = sum([e.lot.get_current_quantity_converted(state_id,self.unit) for e in fee_lots])
|
e.get_current_quantity_converted(state_id,self.unit)
|
||||||
|
for e in lots])
|
||||||
logger.info("GET_FEE_LOTS_QT:%s",qt)
|
logger.info("GET_FEE_LOTS_QT:%s",qt)
|
||||||
return qt
|
return qt
|
||||||
|
|
||||||
@@ -670,6 +717,38 @@ class FeeLots(ModelSQL,ModelView):
|
|||||||
fee = fields.Many2One('fee.fee',"Fee",required=True, ondelete='CASCADE')
|
fee = fields.Many2One('fee.fee',"Fee",required=True, ondelete='CASCADE')
|
||||||
lot = fields.Many2One('lot.lot',"Lot",required=True, ondelete='CASCADE')
|
lot = fields.Many2One('lot.lot',"Lot",required=True, ondelete='CASCADE')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, vlist):
|
||||||
|
records = super().create(vlist)
|
||||||
|
Fee = Pool().get('fee.fee')
|
||||||
|
fee_ids = {
|
||||||
|
record.fee.id for record in records if getattr(record, 'fee', None)}
|
||||||
|
for fee in Fee.browse(list(fee_ids)):
|
||||||
|
fee.sync_quantity_from_lots()
|
||||||
|
fee.adjust_purchase_values()
|
||||||
|
return records
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def write(cls, *args):
|
||||||
|
super().write(*args)
|
||||||
|
records = sum(args[::2], [])
|
||||||
|
Fee = Pool().get('fee.fee')
|
||||||
|
fee_ids = {
|
||||||
|
record.fee.id for record in records if getattr(record, 'fee', None)}
|
||||||
|
for fee in Fee.browse(list(fee_ids)):
|
||||||
|
fee.sync_quantity_from_lots()
|
||||||
|
fee.adjust_purchase_values()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def delete(cls, records):
|
||||||
|
Fee = Pool().get('fee.fee')
|
||||||
|
fee_ids = {
|
||||||
|
record.fee.id for record in records if getattr(record, 'fee', None)}
|
||||||
|
super().delete(records)
|
||||||
|
for fee in Fee.browse(list(fee_ids)):
|
||||||
|
fee.sync_quantity_from_lots()
|
||||||
|
fee.adjust_purchase_values()
|
||||||
|
|
||||||
class FeeReport(
|
class FeeReport(
|
||||||
ModelSQL, ModelView):
|
ModelSQL, ModelView):
|
||||||
"Fee Report"
|
"Fee Report"
|
||||||
|
|||||||
@@ -872,6 +872,7 @@ class Lot(metaclass=PoolMeta):
|
|||||||
FeeLots = pool.get('fee.lots')
|
FeeLots = pool.get('fee.lots')
|
||||||
lines_to_update = set()
|
lines_to_update = set()
|
||||||
sale_lines_to_update = set()
|
sale_lines_to_update = set()
|
||||||
|
fees_to_sync = set()
|
||||||
logger.info("DELETE_FROM_LOTS:%s",lots)
|
logger.info("DELETE_FROM_LOTS:%s",lots)
|
||||||
for lot in lots:
|
for lot in lots:
|
||||||
if lot.lot_type == 'physic':
|
if lot.lot_type == 'physic':
|
||||||
@@ -889,6 +890,8 @@ class Lot(metaclass=PoolMeta):
|
|||||||
logger.info("DELETE_FROM_LOTS2:%s",lot)
|
logger.info("DELETE_FROM_LOTS2:%s",lot)
|
||||||
fls = FeeLots.search(['lot','=',lot.id])
|
fls = FeeLots.search(['lot','=',lot.id])
|
||||||
if fls:
|
if fls:
|
||||||
|
fees_to_sync.update(
|
||||||
|
fl.fee.id for fl in fls if getattr(fl, 'fee', None))
|
||||||
FeeLots.delete(fls)
|
FeeLots.delete(fls)
|
||||||
|
|
||||||
super(Lot, cls).delete(lots)
|
super(Lot, cls).delete(lots)
|
||||||
@@ -901,6 +904,11 @@ class Lot(metaclass=PoolMeta):
|
|||||||
cls._recompute_virtual_lot(Pool().get('sale.line')(line_id))
|
cls._recompute_virtual_lot(Pool().get('sale.line')(line_id))
|
||||||
cls._recalc_line_quantity(line_id,False)
|
cls._recalc_line_quantity(line_id,False)
|
||||||
|
|
||||||
|
Fee = Pool().get('fee.fee')
|
||||||
|
for fee in Fee.browse(list(fees_to_sync)):
|
||||||
|
fee.sync_quantity_from_lots()
|
||||||
|
fee.adjust_purchase_values()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _recalc_line_quantity(cls, line_id,purchase=True):
|
def _recalc_line_quantity(cls, line_id,purchase=True):
|
||||||
PL = Pool().get('purchase.line')
|
PL = Pool().get('purchase.line')
|
||||||
@@ -1124,6 +1132,7 @@ class LotQt(
|
|||||||
fl.line = newlot.line
|
fl.line = newlot.line
|
||||||
FeeLots.save([fl])
|
FeeLots.save([fl])
|
||||||
#update ordered fee with physical lot quantity
|
#update ordered fee with physical lot quantity
|
||||||
|
f.sync_quantity_from_lots()
|
||||||
f.adjust_purchase_values()
|
f.adjust_purchase_values()
|
||||||
|
|
||||||
if newlot.sale_line and newlot.sale_line.fees:
|
if newlot.sale_line and newlot.sale_line.fees:
|
||||||
@@ -1136,6 +1145,7 @@ class LotQt(
|
|||||||
fl.sale_line = newlot.sale_line
|
fl.sale_line = newlot.sale_line
|
||||||
FeeLots.save([fl])
|
FeeLots.save([fl])
|
||||||
#update ordered fee with physical lot quantity
|
#update ordered fee with physical lot quantity
|
||||||
|
f.sync_quantity_from_lots()
|
||||||
f.adjust_purchase_values()
|
f.adjust_purchase_values()
|
||||||
|
|
||||||
if newlot.lot_shipment_in and newlot.lot_shipment_in.fees:
|
if newlot.lot_shipment_in and newlot.lot_shipment_in.fees:
|
||||||
@@ -1147,6 +1157,7 @@ class LotQt(
|
|||||||
fl.lot = newlot
|
fl.lot = newlot
|
||||||
FeeLots.save([fl])
|
FeeLots.save([fl])
|
||||||
#update ordered fee with physical lot quantity
|
#update ordered fee with physical lot quantity
|
||||||
|
f.sync_quantity_from_lots()
|
||||||
f.adjust_purchase_values()
|
f.adjust_purchase_values()
|
||||||
|
|
||||||
return newlot, newlot.get_current_quantity_converted()
|
return newlot, newlot.get_current_quantity_converted()
|
||||||
@@ -3143,8 +3154,9 @@ class LotWeighing(Wizard):
|
|||||||
#need to update virtual part
|
#need to update virtual part
|
||||||
l.lot.updateVirtualPart(-diff,l.lot.lot_shipment_origin,l.lot.getVlot_s())
|
l.lot.updateVirtualPart(-diff,l.lot.lot_shipment_origin,l.lot.getVlot_s())
|
||||||
#adjuts fee ordered with new quantity
|
#adjuts fee ordered with new quantity
|
||||||
fees = FeeLots.search(['lot','=',l.id])
|
fees = FeeLots.search(['lot','=',l.lot.id])
|
||||||
for f in fees:
|
for f in fees:
|
||||||
|
f.fee.sync_quantity_from_lots()
|
||||||
f.fee.adjust_purchase_values()
|
f.fee.adjust_purchase_values()
|
||||||
|
|
||||||
return 'end'
|
return 'end'
|
||||||
|
|||||||
@@ -1565,6 +1565,9 @@ class Line(metaclass=PoolMeta):
|
|||||||
if target_quantity < 0:
|
if target_quantity < 0:
|
||||||
raise UserError("Please unlink or unmatch lot")
|
raise UserError("Please unlink or unmatch lot")
|
||||||
cls._sync_open_lot_quantity(line, vlot, target_quantity)
|
cls._sync_open_lot_quantity(line, vlot, target_quantity)
|
||||||
|
for fee in line.fees or []:
|
||||||
|
fee.sync_quantity_from_lots()
|
||||||
|
fee.adjust_purchase_values()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _sync_open_lot_quantity(cls, line, vlot, target_quantity):
|
def _sync_open_lot_quantity(cls, line, vlot, target_quantity):
|
||||||
|
|||||||
@@ -1681,6 +1681,9 @@ class SaleLine(metaclass=PoolMeta):
|
|||||||
if target_quantity < 0:
|
if target_quantity < 0:
|
||||||
raise UserError("Please unlink or unmatch lot")
|
raise UserError("Please unlink or unmatch lot")
|
||||||
cls._sync_open_lot_quantity(line, vlot, target_quantity)
|
cls._sync_open_lot_quantity(line, vlot, target_quantity)
|
||||||
|
for fee in line.fees or []:
|
||||||
|
fee.sync_quantity_from_lots()
|
||||||
|
fee.adjust_purchase_values()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _sync_open_lot_quantity(cls, line, vlot, target_quantity):
|
def _sync_open_lot_quantity(cls, line, vlot, target_quantity):
|
||||||
|
|||||||
@@ -242,6 +242,84 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
|
|
||||||
self.assertEqual(values[0]['amount'], Decimal('0'))
|
self.assertEqual(values[0]['amount'], Decimal('0'))
|
||||||
|
|
||||||
|
def test_fee_quantity_sync_uses_physical_lots_when_present(self):
|
||||||
|
'fee quantity sync ignores virtual lot once physical lots exist'
|
||||||
|
Fee = Pool().get('fee.fee')
|
||||||
|
fee = Fee()
|
||||||
|
fee.id = 1
|
||||||
|
fee.mode = 'perqt'
|
||||||
|
fee.quantity = Decimal('100')
|
||||||
|
fee.unit = Mock()
|
||||||
|
virtual = Mock(lot_type='virtual')
|
||||||
|
virtual.get_current_quantity_converted.return_value = Decimal('100')
|
||||||
|
physical = Mock(lot_type='physic')
|
||||||
|
physical.get_current_quantity_converted.return_value = Decimal('40')
|
||||||
|
fee_lots = Mock()
|
||||||
|
fee_lots.search.return_value = [
|
||||||
|
Mock(lot=virtual),
|
||||||
|
Mock(lot=physical),
|
||||||
|
]
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
'trytond.modules.purchase_trade.fee.Pool'
|
||||||
|
) as PoolMock, patch.object(Fee, 'save') as save:
|
||||||
|
PoolMock.return_value.get.return_value = fee_lots
|
||||||
|
|
||||||
|
fee.sync_quantity_from_lots()
|
||||||
|
|
||||||
|
self.assertEqual(fee.quantity, Decimal('40.00000'))
|
||||||
|
save.assert_called_once_with([fee])
|
||||||
|
|
||||||
|
def test_purchase_fee_pnl_ignores_virtual_fee_lot_when_physical_exists(self):
|
||||||
|
'purchase fee pnl uses physical fee lots instead of the residual virtual'
|
||||||
|
Valuation = Pool().get('valuation.valuation')
|
||||||
|
currency = Mock(id=1)
|
||||||
|
unit = Mock(id=2)
|
||||||
|
product = Mock(id=3, name='Handling')
|
||||||
|
supplier = Mock(id=4)
|
||||||
|
virtual = Mock(id=5, sale_line=None, lot_type='virtual')
|
||||||
|
virtual.get_current_quantity_converted.return_value = Decimal('60')
|
||||||
|
physical = Mock(id=6, sale_line=None, lot_type='physic')
|
||||||
|
physical.get_current_quantity_converted.return_value = Decimal('40')
|
||||||
|
fee = Mock(
|
||||||
|
product=product,
|
||||||
|
supplier=supplier,
|
||||||
|
type='budgeted',
|
||||||
|
p_r='pay',
|
||||||
|
mode='perqt',
|
||||||
|
price=Decimal('2'),
|
||||||
|
currency=currency,
|
||||||
|
shipment_in=None,
|
||||||
|
sale_line=None,
|
||||||
|
unit=unit,
|
||||||
|
)
|
||||||
|
fee.is_effective_fee_lot.side_effect = lambda lot: lot is physical
|
||||||
|
fee.get_price_per_qt.return_value = Decimal('2')
|
||||||
|
line = Mock(
|
||||||
|
id=7,
|
||||||
|
lots=[virtual, physical],
|
||||||
|
get_matched_lines=Mock(return_value=[]),
|
||||||
|
purchase=Mock(id=8, currency=currency),
|
||||||
|
unit=unit,
|
||||||
|
)
|
||||||
|
fee_lots = Mock()
|
||||||
|
fee_lots.search.return_value = [Mock(fee=fee)]
|
||||||
|
|
||||||
|
with patch('trytond.modules.purchase_trade.valuation.Pool') as PoolMock:
|
||||||
|
PoolMock.return_value.get.side_effect = lambda name: {
|
||||||
|
'ir.date': Mock(today=Mock(return_value=datetime.date(2026, 4, 23))),
|
||||||
|
'currency.currency': Mock(),
|
||||||
|
'fee.lots': fee_lots,
|
||||||
|
'lot.qt': Mock(),
|
||||||
|
}[name]
|
||||||
|
|
||||||
|
values = Valuation.create_pnl_fee_from_line(line)
|
||||||
|
|
||||||
|
self.assertEqual(len(values), 1)
|
||||||
|
self.assertEqual(values[0]['lot'], physical.id)
|
||||||
|
self.assertEqual(values[0]['quantity'], Decimal('40.00000'))
|
||||||
|
self.assertEqual(values[0]['amount'], Decimal('-80.00'))
|
||||||
|
|
||||||
def test_purchase_rate_fee_amount_uses_virtual_lot_quantity(self):
|
def test_purchase_rate_fee_amount_uses_virtual_lot_quantity(self):
|
||||||
'purchase rate fee amount uses the financing delta as absolute period'
|
'purchase rate fee amount uses the financing delta as absolute period'
|
||||||
Fee = Pool().get('fee.fee')
|
Fee = Pool().get('fee.fee')
|
||||||
@@ -1166,6 +1244,47 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
lot_model.save.assert_not_called()
|
lot_model.save.assert_not_called()
|
||||||
lotqt_model.save.assert_not_called()
|
lotqt_model.save.assert_not_called()
|
||||||
|
|
||||||
|
def test_purchase_line_write_syncs_virtual_fee_quantity(self):
|
||||||
|
'purchase line write updates fee quantity when only virtual lot exists'
|
||||||
|
PurchaseLine = Pool().get('purchase.line')
|
||||||
|
fee = Mock()
|
||||||
|
line = Mock(id=5, quantity_theorical=Decimal('20000'))
|
||||||
|
line.unit = Mock()
|
||||||
|
line.fees = [fee]
|
||||||
|
vlot = Mock(id=103, lot_type='virtual')
|
||||||
|
vlot.get_current_quantity_converted.return_value = Decimal('20000')
|
||||||
|
line.lots = [vlot]
|
||||||
|
lqt = Mock(lot_quantity=Decimal('20000'))
|
||||||
|
|
||||||
|
lot_model = Mock()
|
||||||
|
lotqt_model = Mock()
|
||||||
|
lotqt_model.search.side_effect = [[lqt], []]
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
'trytond.modules.purchase_trade.purchase.Pool'
|
||||||
|
) as PoolMock, patch(
|
||||||
|
'trytond.modules.purchase_trade.purchase.super'
|
||||||
|
) as super_mock:
|
||||||
|
PoolMock.return_value.get.side_effect = lambda name: {
|
||||||
|
'lot.lot': lot_model,
|
||||||
|
'lot.qt': lotqt_model,
|
||||||
|
}[name]
|
||||||
|
|
||||||
|
def fake_super_write(*args):
|
||||||
|
for records, values in zip(args[::2], args[1::2]):
|
||||||
|
if 'quantity_theorical' in values:
|
||||||
|
for record in records:
|
||||||
|
record.quantity_theorical = values['quantity_theorical']
|
||||||
|
|
||||||
|
super_mock.return_value.write.side_effect = fake_super_write
|
||||||
|
|
||||||
|
PurchaseLine.write(
|
||||||
|
[line], {'quantity_theorical': Decimal('10000')})
|
||||||
|
|
||||||
|
self.assertEqual(lqt.lot_quantity, Decimal('10000.00000'))
|
||||||
|
fee.sync_quantity_from_lots.assert_called_once_with()
|
||||||
|
fee.adjust_purchase_values.assert_called_once_with()
|
||||||
|
|
||||||
def test_purchase_line_write_initial_theorical_qty_does_not_double_open_lot(self):
|
def test_purchase_line_write_initial_theorical_qty_does_not_double_open_lot(self):
|
||||||
'purchase line write does not re-add quantity when initializing contractual qty'
|
'purchase line write does not re-add quantity when initializing contractual qty'
|
||||||
PurchaseLine = Pool().get('purchase.line')
|
PurchaseLine = Pool().get('purchase.line')
|
||||||
|
|||||||
@@ -698,6 +698,32 @@ class ValuationBase(ModelSQL):
|
|||||||
amount = fee.get_amount() if hasattr(fee, 'get_amount') else fee.amount
|
amount = fee.get_amount() if hasattr(fee, 'get_amount') else fee.amount
|
||||||
return Decimal(amount or 0)
|
return Decimal(amount or 0)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _fee_amount_for_lot_or_zero(cls, fee, lot):
|
||||||
|
if fee.mode == 'ppack' and getattr(lot, 'lot_qt', None):
|
||||||
|
return round(
|
||||||
|
Decimal(fee.price or 0)
|
||||||
|
* Decimal(str(lot.lot_qt or 0)),
|
||||||
|
2)
|
||||||
|
if fee.mode == 'rate':
|
||||||
|
line = fee.line or getattr(fee, 'sale_line', None)
|
||||||
|
if line and line.estimated_date:
|
||||||
|
est_lines = [
|
||||||
|
dd for dd in line.estimated_date
|
||||||
|
if dd.trigger == 'bldate']
|
||||||
|
est_line = est_lines[0] if est_lines else None
|
||||||
|
if est_line and est_line.fin_int_delta:
|
||||||
|
factor = (
|
||||||
|
Decimal(fee.price or 0) / Decimal(100)
|
||||||
|
* Decimal(est_line.fin_int_delta) / Decimal(360))
|
||||||
|
quantity = Decimal(
|
||||||
|
lot.get_current_quantity_converted(0, fee.unit) or 0)
|
||||||
|
return round(
|
||||||
|
factor * Decimal(line.unit_price or 0) * quantity,
|
||||||
|
2)
|
||||||
|
return Decimal(0)
|
||||||
|
return cls._fee_amount_or_zero(fee)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_pnl_fee_from_line(cls, line):
|
def create_pnl_fee_from_line(cls, line):
|
||||||
fee_lines = []
|
fee_lines = []
|
||||||
@@ -725,13 +751,20 @@ class ValuationBase(ModelSQL):
|
|||||||
if not fl:
|
if not fl:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
fees = [e.fee for e in fl]
|
fees = [
|
||||||
|
e.fee for e in fl
|
||||||
|
if e.fee and (
|
||||||
|
not hasattr(e.fee, 'is_effective_fee_lot')
|
||||||
|
or e.fee.is_effective_fee_lot(lot))
|
||||||
|
]
|
||||||
for sf in cls.group_fees_by_type_supplier(line, fees):
|
for sf in cls.group_fees_by_type_supplier(line, fees):
|
||||||
sign = -1 if sf.p_r == 'pay' else 1
|
sign = -1 if sf.p_r == 'pay' else 1
|
||||||
qty = round(lot.get_current_quantity_converted(), 5)
|
qty = round(lot.get_current_quantity_converted(), 5)
|
||||||
|
if sf.mode == 'ppack' and getattr(lot, 'lot_qt', None):
|
||||||
|
qty = Decimal(str(lot.lot_qt or 0))
|
||||||
if sf.mode == 'ppack' or sf.mode == 'rate':
|
if sf.mode == 'ppack' or sf.mode == 'rate':
|
||||||
price = sf.price
|
price = sf.price
|
||||||
amount = cls._fee_amount_or_zero(sf) * sign
|
amount = cls._fee_amount_for_lot_or_zero(sf, lot) * sign
|
||||||
elif sf.mode == 'lumpsum':
|
elif sf.mode == 'lumpsum':
|
||||||
price = sf.price
|
price = sf.price
|
||||||
amount = sf.price * sign
|
amount = sf.price * sign
|
||||||
@@ -787,13 +820,18 @@ class ValuationBase(ModelSQL):
|
|||||||
fees = [
|
fees = [
|
||||||
e.fee for e in fl
|
e.fee for e in fl
|
||||||
if e.fee and (not e.fee.sale_line or e.fee.sale_line.id == sale_line.id)
|
if e.fee and (not e.fee.sale_line or e.fee.sale_line.id == sale_line.id)
|
||||||
|
and (
|
||||||
|
not hasattr(e.fee, 'is_effective_fee_lot')
|
||||||
|
or e.fee.is_effective_fee_lot(lot))
|
||||||
]
|
]
|
||||||
for sf in cls.group_fees_by_type_supplier(sale_line, fees):
|
for sf in cls.group_fees_by_type_supplier(sale_line, fees):
|
||||||
sign = -1 if sf.p_r == 'pay' else 1
|
sign = -1 if sf.p_r == 'pay' else 1
|
||||||
qty = round(lot.get_current_quantity_converted(), 5)
|
qty = round(lot.get_current_quantity_converted(), 5)
|
||||||
|
if sf.mode == 'ppack' and getattr(lot, 'lot_qt', None):
|
||||||
|
qty = Decimal(str(lot.lot_qt or 0))
|
||||||
if sf.mode == 'ppack' or sf.mode == 'rate':
|
if sf.mode == 'ppack' or sf.mode == 'rate':
|
||||||
price = sf.price
|
price = sf.price
|
||||||
amount = cls._fee_amount_or_zero(sf) * sign
|
amount = cls._fee_amount_for_lot_or_zero(sf, lot) * sign
|
||||||
elif sf.mode == 'lumpsum':
|
elif sf.mode == 'lumpsum':
|
||||||
price = sf.price
|
price = sf.price
|
||||||
amount = sf.price * sign
|
amount = sf.price * sign
|
||||||
|
|||||||
@@ -180,3 +180,17 @@ elle existe, par exemple:
|
|||||||
`Please unlink or unmatch lot`.
|
`Please unlink or unmatch lot`.
|
||||||
- Cas de reference: une ligne achat passee de `10000` a `20000` avec deja
|
- Cas de reference: une ligne achat passee de `10000` a `20000` avec deja
|
||||||
`10000` physiques doit rester a `10000` ouverts et `10000` physiques.
|
`10000` physiques doit rester a `10000` ouverts et `10000` physiques.
|
||||||
|
|
||||||
|
### `fee.fee` / `fee.lots`
|
||||||
|
|
||||||
|
- Le lien entre un fee et son lot virtuel est conserve comme fallback.
|
||||||
|
- Des qu'un fee possede au moins un lot physique dans `fee.lots`, les lots
|
||||||
|
physiques deviennent la base effective de `fee.quantity`.
|
||||||
|
- Une hausse purement ouverte de `quantity_theorical` n'impacte donc pas un fee
|
||||||
|
deja porte par des lots physiques.
|
||||||
|
- En `ppack`, `fee.quantity` suit la somme des `lot_qt` physiques; dans les
|
||||||
|
autres modes quantitatifs, elle suit la somme des quantites courantes des
|
||||||
|
lots physiques.
|
||||||
|
- A la suppression du dernier physique, le fee retombe sur son lot virtuel.
|
||||||
|
- Le PnL fee suit la meme logique: full open tant que seul le virtuel existe,
|
||||||
|
puis uniquement les lots physiques effectifs.
|
||||||
|
|||||||
Reference in New Issue
Block a user