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`
|
||||
- Decisions templates / reports:
|
||||
- `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)
|
||||
|
||||
|
||||
@@ -151,6 +151,21 @@ de negoce physique:
|
||||
taux de la provisoire
|
||||
- le wizard final doit calculer son delta depuis la quantite provisoire hors
|
||||
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
|
||||
|
||||
|
||||
@@ -587,6 +587,51 @@ Owner technique: `a completer`
|
||||
- exemple: une ligne achat passee de `10000` a `20000` avec deja `10000`
|
||||
physiques doit afficher `10000` ouverts et `10000` physiques, pas
|
||||
`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:
|
||||
- `structurante`
|
||||
|
||||
|
||||
@@ -331,6 +331,52 @@ class Fee(ModelSQL,ModelView):
|
||||
if lqts:
|
||||
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):
|
||||
quantity = self.quantity
|
||||
if quantity is None:
|
||||
@@ -423,16 +469,17 @@ class Fee(ModelSQL,ModelView):
|
||||
|
||||
return super().copy(fees, default=default)
|
||||
|
||||
def get_fee_lots_qt(self,state_id=0):
|
||||
qt = Decimal(0)
|
||||
FeeLots = Pool().get('fee.lots')
|
||||
fee_lots = FeeLots.search([('fee', '=', self.id)])
|
||||
if fee_lots:
|
||||
qt = sum([e.lot.get_current_quantity_converted(state_id,self.unit) for e in fee_lots])
|
||||
logger.info("GET_FEE_LOTS_QT:%s",qt)
|
||||
return qt
|
||||
|
||||
def adjust_purchase_values(self):
|
||||
def get_fee_lots_qt(self,state_id=0):
|
||||
qt = Decimal(0)
|
||||
lots = self._get_effective_fee_lots()
|
||||
if lots:
|
||||
qt = sum([
|
||||
e.get_current_quantity_converted(state_id,self.unit)
|
||||
for e in lots])
|
||||
logger.info("GET_FEE_LOTS_QT:%s",qt)
|
||||
return qt
|
||||
|
||||
def adjust_purchase_values(self):
|
||||
Purchase = Pool().get('purchase.purchase')
|
||||
PurchaseLine = Pool().get('purchase.line')
|
||||
logger.info("ADJUST_PURCHASE_VALUES:%s",self)
|
||||
@@ -662,13 +709,45 @@ class Fee(ModelSQL,ModelView):
|
||||
lines=[move_line,move_line_],
|
||||
)
|
||||
|
||||
class FeeLots(ModelSQL,ModelView):
|
||||
class FeeLots(ModelSQL,ModelView):
|
||||
|
||||
"Fee lots"
|
||||
__name__ = 'fee.lots'
|
||||
|
||||
fee = fields.Many2One('fee.fee',"Fee",required=True, ondelete='CASCADE')
|
||||
lot = fields.Many2One('lot.lot',"Lot",required=True, ondelete='CASCADE')
|
||||
fee = fields.Many2One('fee.fee',"Fee",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(
|
||||
ModelSQL, ModelView):
|
||||
|
||||
@@ -869,14 +869,15 @@ class Lot(metaclass=PoolMeta):
|
||||
def delete(cls, lots):
|
||||
pool = Pool()
|
||||
LL = pool.get('lot.lot')
|
||||
FeeLots = pool.get('fee.lots')
|
||||
lines_to_update = set()
|
||||
sale_lines_to_update = set()
|
||||
logger.info("DELETE_FROM_LOTS:%s",lots)
|
||||
for lot in lots:
|
||||
if lot.lot_type == 'physic':
|
||||
if lot.line:
|
||||
lines_to_update.add(lot.line.id)
|
||||
FeeLots = pool.get('fee.lots')
|
||||
lines_to_update = set()
|
||||
sale_lines_to_update = set()
|
||||
fees_to_sync = set()
|
||||
logger.info("DELETE_FROM_LOTS:%s",lots)
|
||||
for lot in lots:
|
||||
if lot.lot_type == 'physic':
|
||||
if lot.line:
|
||||
lines_to_update.add(lot.line.id)
|
||||
if lot.sale_line:
|
||||
sale_lines_to_update.add(lot.sale_line.id)
|
||||
if lot.lot_parent:
|
||||
@@ -886,20 +887,27 @@ class Lot(metaclass=PoolMeta):
|
||||
pa.lot_quantity += lot.lot_quantity
|
||||
pa.lot_gross_quantity += lot.lot_gross_quantity
|
||||
LL.save([pa])
|
||||
logger.info("DELETE_FROM_LOTS2:%s",lot)
|
||||
fls = FeeLots.search(['lot','=',lot.id])
|
||||
if fls:
|
||||
FeeLots.delete(fls)
|
||||
|
||||
super(Lot, cls).delete(lots)
|
||||
logger.info("DELETE_FROM_LOTS2:%s",lot)
|
||||
fls = FeeLots.search(['lot','=',lot.id])
|
||||
if fls:
|
||||
fees_to_sync.update(
|
||||
fl.fee.id for fl in fls if getattr(fl, 'fee', None))
|
||||
FeeLots.delete(fls)
|
||||
|
||||
super(Lot, cls).delete(lots)
|
||||
|
||||
for line_id in lines_to_update:
|
||||
cls._recompute_virtual_lot(Pool().get('purchase.line')(line_id))
|
||||
cls._recalc_line_quantity(line_id,True)
|
||||
|
||||
for line_id in sale_lines_to_update:
|
||||
cls._recompute_virtual_lot(Pool().get('sale.line')(line_id))
|
||||
cls._recalc_line_quantity(line_id,False)
|
||||
for line_id in sale_lines_to_update:
|
||||
cls._recompute_virtual_lot(Pool().get('sale.line')(line_id))
|
||||
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
|
||||
def _recalc_line_quantity(cls, line_id,purchase=True):
|
||||
@@ -1117,37 +1125,40 @@ class LotQt(
|
||||
if newlot.line.fees:
|
||||
for f in newlot.line.fees:
|
||||
exist = FeeLots.search([('fee','=',f.id),('lot','=',newlot.id)])
|
||||
if not exist:
|
||||
fl =FeeLots()
|
||||
fl.fee = f
|
||||
fl.lot = newlot
|
||||
fl.line = newlot.line
|
||||
FeeLots.save([fl])
|
||||
#update ordered fee with physical lot quantity
|
||||
f.adjust_purchase_values()
|
||||
if not exist:
|
||||
fl =FeeLots()
|
||||
fl.fee = f
|
||||
fl.lot = newlot
|
||||
fl.line = newlot.line
|
||||
FeeLots.save([fl])
|
||||
#update ordered fee with physical lot quantity
|
||||
f.sync_quantity_from_lots()
|
||||
f.adjust_purchase_values()
|
||||
|
||||
if newlot.sale_line and newlot.sale_line.fees:
|
||||
for f in newlot.sale_line.fees:
|
||||
exist = FeeLots.search([('fee','=',f.id),('lot','=',newlot.id)])
|
||||
if not exist:
|
||||
fl =FeeLots()
|
||||
fl.fee = f
|
||||
fl.lot = newlot
|
||||
fl.sale_line = newlot.sale_line
|
||||
FeeLots.save([fl])
|
||||
#update ordered fee with physical lot quantity
|
||||
f.adjust_purchase_values()
|
||||
if not exist:
|
||||
fl =FeeLots()
|
||||
fl.fee = f
|
||||
fl.lot = newlot
|
||||
fl.sale_line = newlot.sale_line
|
||||
FeeLots.save([fl])
|
||||
#update ordered fee with physical lot quantity
|
||||
f.sync_quantity_from_lots()
|
||||
f.adjust_purchase_values()
|
||||
|
||||
if newlot.lot_shipment_in and newlot.lot_shipment_in.fees:
|
||||
for f in newlot.lot_shipment_in.fees:
|
||||
exist = FeeLots.search([('fee','=',f.id),('lot','=',newlot.id)])
|
||||
if not exist:
|
||||
fl =FeeLots()
|
||||
fl.fee = f
|
||||
fl.lot = newlot
|
||||
FeeLots.save([fl])
|
||||
#update ordered fee with physical lot quantity
|
||||
f.adjust_purchase_values()
|
||||
if not exist:
|
||||
fl =FeeLots()
|
||||
fl.fee = f
|
||||
fl.lot = newlot
|
||||
FeeLots.save([fl])
|
||||
#update ordered fee with physical lot quantity
|
||||
f.sync_quantity_from_lots()
|
||||
f.adjust_purchase_values()
|
||||
|
||||
return newlot, newlot.get_current_quantity_converted()
|
||||
|
||||
@@ -3139,13 +3150,14 @@ class LotWeighing(Wizard):
|
||||
l.lot.lot_state = self.w.lot_state
|
||||
Lot.save([l.lot])
|
||||
diff = round(Decimal(l.lot.get_current_quantity_converted() - quantity),5)
|
||||
if diff != 0 :
|
||||
#need to update virtual part
|
||||
l.lot.updateVirtualPart(-diff,l.lot.lot_shipment_origin,l.lot.getVlot_s())
|
||||
#adjuts fee ordered with new quantity
|
||||
fees = FeeLots.search(['lot','=',l.id])
|
||||
for f in fees:
|
||||
f.fee.adjust_purchase_values()
|
||||
if diff != 0 :
|
||||
#need to update virtual part
|
||||
l.lot.updateVirtualPart(-diff,l.lot.lot_shipment_origin,l.lot.getVlot_s())
|
||||
#adjuts fee ordered with new quantity
|
||||
fees = FeeLots.search(['lot','=',l.lot.id])
|
||||
for f in fees:
|
||||
f.fee.sync_quantity_from_lots()
|
||||
f.fee.adjust_purchase_values()
|
||||
|
||||
return 'end'
|
||||
|
||||
|
||||
@@ -1565,6 +1565,9 @@ class Line(metaclass=PoolMeta):
|
||||
if target_quantity < 0:
|
||||
raise UserError("Please unlink or unmatch lot")
|
||||
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
|
||||
def _sync_open_lot_quantity(cls, line, vlot, target_quantity):
|
||||
|
||||
@@ -1681,6 +1681,9 @@ class SaleLine(metaclass=PoolMeta):
|
||||
if target_quantity < 0:
|
||||
raise UserError("Please unlink or unmatch lot")
|
||||
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
|
||||
def _sync_open_lot_quantity(cls, line, vlot, target_quantity):
|
||||
|
||||
@@ -242,6 +242,84 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
|
||||
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):
|
||||
'purchase rate fee amount uses the financing delta as absolute period'
|
||||
Fee = Pool().get('fee.fee')
|
||||
@@ -1166,6 +1244,47 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
lot_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):
|
||||
'purchase line write does not re-add quantity when initializing contractual qty'
|
||||
PurchaseLine = Pool().get('purchase.line')
|
||||
|
||||
@@ -697,6 +697,32 @@ class ValuationBase(ModelSQL):
|
||||
def _fee_amount_or_zero(cls, fee):
|
||||
amount = fee.get_amount() if hasattr(fee, 'get_amount') else fee.amount
|
||||
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
|
||||
def create_pnl_fee_from_line(cls, line):
|
||||
@@ -725,13 +751,20 @@ class ValuationBase(ModelSQL):
|
||||
if not fl:
|
||||
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):
|
||||
sign = -1 if sf.p_r == 'pay' else 1
|
||||
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':
|
||||
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':
|
||||
price = sf.price
|
||||
amount = sf.price * sign
|
||||
@@ -787,13 +820,18 @@ class ValuationBase(ModelSQL):
|
||||
fees = [
|
||||
e.fee for e in fl
|
||||
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):
|
||||
sign = -1 if sf.p_r == 'pay' else 1
|
||||
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':
|
||||
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':
|
||||
price = sf.price
|
||||
amount = sf.price * sign
|
||||
|
||||
@@ -180,3 +180,17 @@ elle existe, par exemple:
|
||||
`Please unlink or unmatch lot`.
|
||||
- Cas de reference: une ligne achat passee de `10000` a `20000` avec deja
|
||||
`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