Hide field ITSA
This commit is contained in:
@@ -629,6 +629,8 @@ Owner technique: `a completer`
|
|||||||
|
|
||||||
- Intent: eviter qu'un fee cree sur un lot virtuel reste calcule sur la
|
- Intent: eviter qu'un fee cree sur un lot virtuel reste calcule sur la
|
||||||
quantite contractuelle totale apres creation de lots physiques.
|
quantite contractuelle totale apres creation de lots physiques.
|
||||||
|
- Detail implementation / PnL:
|
||||||
|
- voir `modules/purchase_trade/docs/fees.md`
|
||||||
- Description:
|
- Description:
|
||||||
- Le lien `fee.lots` avec le lot virtuel est conserve comme fallback.
|
- 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
|
- Des qu'un fee possede au moins un lot physique dans `fee.lots`, les lots
|
||||||
|
|||||||
51
modules/purchase_trade/docs/fees.md
Normal file
51
modules/purchase_trade/docs/fees.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Fees - regles de synchronisation PnL
|
||||||
|
|
||||||
|
## PnL apres ajout ou modification d'un fee
|
||||||
|
|
||||||
|
Un changement sur un `fee.fee` doit relancer le PnL des lignes commerciales
|
||||||
|
impactees, limite aux types de valuation fees:
|
||||||
|
|
||||||
|
- `pur. fee`
|
||||||
|
- `sale fee`
|
||||||
|
- `shipment fee`
|
||||||
|
- `line fee`
|
||||||
|
|
||||||
|
Le recalcul utilise `valuation_type = 'fees'` pour ne pas reecrire les lignes de
|
||||||
|
prix, derives ou MTM.
|
||||||
|
|
||||||
|
## Lignes impactees
|
||||||
|
|
||||||
|
Les lignes a recalculer sont retrouvees depuis les lots lies au fee:
|
||||||
|
|
||||||
|
- `lot.line` pour la `purchase.line`
|
||||||
|
- `lot.sale_line` pour la `sale.line`
|
||||||
|
|
||||||
|
Pour un fee de `stock.shipment.in`, si le lien direct par `fee.lots` ne suffit
|
||||||
|
pas, le shipment sert de fallback:
|
||||||
|
|
||||||
|
- `shipment.incoming_moves[].lot`
|
||||||
|
- `shipment.lotqt[].lot_p`
|
||||||
|
- `shipment.lotqt[].lot_s`
|
||||||
|
|
||||||
|
Ce fallback couvre les shipments encore ouverts ou le fee est pose sur des
|
||||||
|
`lot.qt` avant creation de lots physiques.
|
||||||
|
|
||||||
|
## Lots effectifs du fee
|
||||||
|
|
||||||
|
Le PnL des fees suit la regle BR-PT-021:
|
||||||
|
|
||||||
|
- tant qu'il n'existe que des lots virtuels, le fee valorise l'ouvert;
|
||||||
|
- des qu'un lot physique est lie au fee, les lots physiques deviennent la base
|
||||||
|
effective;
|
||||||
|
- le lien virtuel peut rester present comme fallback.
|
||||||
|
|
||||||
|
## Points de declenchement
|
||||||
|
|
||||||
|
Le recalcul est declenche apres:
|
||||||
|
|
||||||
|
- creation, modification ou suppression d'un `fee.fee`;
|
||||||
|
- creation, modification ou suppression d'un lien `fee.lots`.
|
||||||
|
|
||||||
|
`stock.shipment.in.validate()` recalcule deja le PnL depuis les lots du
|
||||||
|
shipment, mais ce workflow ne couvre pas les changements de fee saisis apres
|
||||||
|
coup dans l'onglet Fees.
|
||||||
@@ -449,14 +449,6 @@ class Fee(ModelSQL,ModelView):
|
|||||||
if lqts:
|
if lqts:
|
||||||
return round(self.price * Decimal(lqts[0].lot_p.get_lot_price()) / 100 * lqts[0].lot_quantity * sign,2)
|
return round(self.price * Decimal(lqts[0].lot_p.get_lot_price()) / 100 * lqts[0].lot_quantity * sign,2)
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def write(cls, *args):
|
|
||||||
super().write(*args)
|
|
||||||
fees = sum(args[::2], [])
|
|
||||||
for fee in fees:
|
|
||||||
fee.adjust_purchase_values()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def copy(cls, fees, default=None):
|
def copy(cls, fees, default=None):
|
||||||
if default is None:
|
if default is None:
|
||||||
@@ -506,6 +498,73 @@ class Fee(ModelSQL,ModelView):
|
|||||||
self.purchase.currency = self.currency
|
self.purchase.currency = self.currency
|
||||||
Purchase.save([self.purchase])
|
Purchase.save([self.purchase])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _add_lot_pnl_lines(cls, lot, purchase_lines, sale_lines):
|
||||||
|
if not lot:
|
||||||
|
return
|
||||||
|
if getattr(lot, 'line', None):
|
||||||
|
purchase_lines[lot.line.id] = lot.line
|
||||||
|
if getattr(lot, 'sale_line', None):
|
||||||
|
sale_lines[lot.sale_line.id] = lot.sale_line
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _collect_pnl_lines(cls, fees=None, lots=None):
|
||||||
|
purchase_lines = {}
|
||||||
|
sale_lines = {}
|
||||||
|
|
||||||
|
for lot in lots or []:
|
||||||
|
cls._add_lot_pnl_lines(lot, purchase_lines, sale_lines)
|
||||||
|
|
||||||
|
for fee in fees or []:
|
||||||
|
if getattr(fee, 'line', None):
|
||||||
|
purchase_lines[fee.line.id] = fee.line
|
||||||
|
if getattr(fee, 'sale_line', None):
|
||||||
|
sale_lines[fee.sale_line.id] = fee.sale_line
|
||||||
|
|
||||||
|
for lot in getattr(fee, 'lots', None) or []:
|
||||||
|
cls._add_lot_pnl_lines(lot, purchase_lines, sale_lines)
|
||||||
|
|
||||||
|
shipment = getattr(fee, 'shipment_in', None)
|
||||||
|
if not shipment:
|
||||||
|
continue
|
||||||
|
for move in getattr(shipment, 'incoming_moves', None) or []:
|
||||||
|
cls._add_lot_pnl_lines(
|
||||||
|
getattr(move, 'lot', None), purchase_lines, sale_lines)
|
||||||
|
for lqt in getattr(shipment, 'lotqt', None) or []:
|
||||||
|
cls._add_lot_pnl_lines(
|
||||||
|
getattr(lqt, 'lot_p', None), purchase_lines, sale_lines)
|
||||||
|
cls._add_lot_pnl_lines(
|
||||||
|
getattr(lqt, 'lot_s', None), purchase_lines, sale_lines)
|
||||||
|
|
||||||
|
return purchase_lines.values(), sale_lines.values()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _regenerate_fee_pnl(cls, fees=None, lots=None):
|
||||||
|
purchase_lines, sale_lines = cls._collect_pnl_lines(fees, lots)
|
||||||
|
cls._regenerate_fee_pnl_for_lines(purchase_lines, sale_lines)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _regenerate_fee_pnl_for_lines(cls, purchase_lines, sale_lines):
|
||||||
|
purchase_lines = cls._unique_records(purchase_lines)
|
||||||
|
sale_lines = cls._unique_records(sale_lines)
|
||||||
|
if not purchase_lines and not sale_lines:
|
||||||
|
return
|
||||||
|
Valuation = Pool().get('valuation.valuation')
|
||||||
|
for line in purchase_lines:
|
||||||
|
Valuation.generate(line, valuation_type='fees')
|
||||||
|
for sale_line in sale_lines:
|
||||||
|
Valuation.generate_from_sale_line(
|
||||||
|
sale_line, valuation_type='fees')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _unique_records(records):
|
||||||
|
unique = {}
|
||||||
|
for record in records:
|
||||||
|
if not record:
|
||||||
|
continue
|
||||||
|
unique[getattr(record, 'id', id(record))] = record
|
||||||
|
return list(unique.values())
|
||||||
|
|
||||||
# @classmethod
|
# @classmethod
|
||||||
# def validate(cls, fees):
|
# def validate(cls, fees):
|
||||||
# super(Fee, cls).validate(fees)
|
# super(Fee, cls).validate(fees)
|
||||||
@@ -556,6 +615,16 @@ class Fee(ModelSQL,ModelView):
|
|||||||
lqts = LotQt.search(['lot_shipment_in','=',fee.shipment_in.id])
|
lqts = LotQt.search(['lot_shipment_in','=',fee.shipment_in.id])
|
||||||
if lqts:
|
if lqts:
|
||||||
for l in lqts:
|
for l in lqts:
|
||||||
|
if l.lot_p:
|
||||||
|
exist = FeeLots.search([
|
||||||
|
('fee','=',fee.id),
|
||||||
|
('lot','=',l.lot_p.id),
|
||||||
|
])
|
||||||
|
if not exist:
|
||||||
|
fl = FeeLots()
|
||||||
|
fl.fee = fee.id
|
||||||
|
fl.lot = l.lot_p.id
|
||||||
|
FeeLots.save([fl])
|
||||||
qt_sh += l.lot_p.get_current_quantity_converted()
|
qt_sh += l.lot_p.get_current_quantity_converted()
|
||||||
unit = l.lot_p.line.unit
|
unit = l.lot_p.line.unit
|
||||||
else:
|
else:
|
||||||
@@ -619,8 +688,30 @@ class Fee(ModelSQL,ModelView):
|
|||||||
account_move = fee._get_account_move_fee(fl.lot)
|
account_move = fee._get_account_move_fee(fl.lot)
|
||||||
AccountMove.save([account_move])
|
AccountMove.save([account_move])
|
||||||
|
|
||||||
|
cls._regenerate_fee_pnl(fees=fees)
|
||||||
return fees
|
return fees
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def write(cls, *args):
|
||||||
|
old_purchase_lines, old_sale_lines = cls._collect_pnl_lines(
|
||||||
|
fees=sum(args[::2], []))
|
||||||
|
old_purchase_lines = list(old_purchase_lines)
|
||||||
|
old_sale_lines = list(old_sale_lines)
|
||||||
|
super().write(*args)
|
||||||
|
fees = sum(args[::2], [])
|
||||||
|
for fee in fees:
|
||||||
|
fee.adjust_purchase_values()
|
||||||
|
purchase_lines, sale_lines = cls._collect_pnl_lines(fees=fees)
|
||||||
|
cls._regenerate_fee_pnl_for_lines(
|
||||||
|
old_purchase_lines + list(purchase_lines),
|
||||||
|
old_sale_lines + list(sale_lines))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def delete(cls, fees):
|
||||||
|
purchase_lines, sale_lines = cls._collect_pnl_lines(fees=fees)
|
||||||
|
super().delete(fees)
|
||||||
|
cls._regenerate_fee_pnl_for_lines(purchase_lines, sale_lines)
|
||||||
|
|
||||||
def _get_account_move_fee(self,lot,in_out='in',amt = None):
|
def _get_account_move_fee(self,lot,in_out='in',amt = None):
|
||||||
pool = Pool()
|
pool = Pool()
|
||||||
AccountMove = pool.get('account.move')
|
AccountMove = pool.get('account.move')
|
||||||
@@ -726,10 +817,16 @@ class FeeLots(ModelSQL,ModelView):
|
|||||||
for fee in Fee.browse(list(fee_ids)):
|
for fee in Fee.browse(list(fee_ids)):
|
||||||
fee.sync_quantity_from_lots()
|
fee.sync_quantity_from_lots()
|
||||||
fee.adjust_purchase_values()
|
fee.adjust_purchase_values()
|
||||||
|
Fee._regenerate_fee_pnl(
|
||||||
|
fees=Fee.browse(list(fee_ids)),
|
||||||
|
lots=[record.lot for record in records if getattr(record, 'lot', None)])
|
||||||
return records
|
return records
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def write(cls, *args):
|
def write(cls, *args):
|
||||||
|
old_lots = [
|
||||||
|
record.lot for record in sum(args[::2], [])
|
||||||
|
if getattr(record, 'lot', None)]
|
||||||
super().write(*args)
|
super().write(*args)
|
||||||
records = sum(args[::2], [])
|
records = sum(args[::2], [])
|
||||||
Fee = Pool().get('fee.fee')
|
Fee = Pool().get('fee.fee')
|
||||||
@@ -738,16 +835,24 @@ class FeeLots(ModelSQL,ModelView):
|
|||||||
for fee in Fee.browse(list(fee_ids)):
|
for fee in Fee.browse(list(fee_ids)):
|
||||||
fee.sync_quantity_from_lots()
|
fee.sync_quantity_from_lots()
|
||||||
fee.adjust_purchase_values()
|
fee.adjust_purchase_values()
|
||||||
|
Fee._regenerate_fee_pnl(
|
||||||
|
fees=Fee.browse(list(fee_ids)),
|
||||||
|
lots=old_lots + [
|
||||||
|
record.lot for record in records
|
||||||
|
if getattr(record, 'lot', None)])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def delete(cls, records):
|
def delete(cls, records):
|
||||||
Fee = Pool().get('fee.fee')
|
Fee = Pool().get('fee.fee')
|
||||||
fee_ids = {
|
fee_ids = {
|
||||||
record.fee.id for record in records if getattr(record, 'fee', None)}
|
record.fee.id for record in records if getattr(record, 'fee', None)}
|
||||||
|
lots = [record.lot for record in records if getattr(record, 'lot', None)]
|
||||||
super().delete(records)
|
super().delete(records)
|
||||||
for fee in Fee.browse(list(fee_ids)):
|
for fee in Fee.browse(list(fee_ids)):
|
||||||
fee.sync_quantity_from_lots()
|
fee.sync_quantity_from_lots()
|
||||||
fee.adjust_purchase_values()
|
fee.adjust_purchase_values()
|
||||||
|
Fee._regenerate_fee_pnl(
|
||||||
|
fees=Fee.browse(list(fee_ids)), lots=lots)
|
||||||
|
|
||||||
class FeeReport(
|
class FeeReport(
|
||||||
ModelSQL, ModelView):
|
ModelSQL, ModelView):
|
||||||
|
|||||||
@@ -308,13 +308,14 @@ class Purchase(metaclass=PoolMeta):
|
|||||||
company_visible = fields.Function(
|
company_visible = fields.Function(
|
||||||
fields.Boolean("Visible"), 'on_change_with_company_visible')
|
fields.Boolean("Visible"), 'on_change_with_company_visible')
|
||||||
lc_date = fields.Date("LC date")
|
lc_date = fields.Date("LC date")
|
||||||
product_origin = fields.Char("Origin")
|
product_origin = fields.Char(
|
||||||
|
"Origin", states={'invisible': Eval('company_visible'),})
|
||||||
|
|
||||||
@fields.depends('company', '_parent_company.party')
|
@fields.depends('company', '_parent_company.party')
|
||||||
def on_change_with_company_visible(self, name=None):
|
def on_change_with_company_visible(self, name=None):
|
||||||
return bool(
|
return bool(
|
||||||
self.company and self.company.party
|
self.company and self.company.party
|
||||||
and self.company.party.name == 'MELYA')
|
and self.company.party.name in {'MELYA', 'ITSA'})
|
||||||
|
|
||||||
def _get_default_bank_account(self):
|
def _get_default_bank_account(self):
|
||||||
if not self.party or not self.party.bank_accounts:
|
if not self.party or not self.party.bank_accounts:
|
||||||
|
|||||||
@@ -272,13 +272,14 @@ class Sale(metaclass=PoolMeta):
|
|||||||
company_visible = fields.Function(
|
company_visible = fields.Function(
|
||||||
fields.Boolean("Visible"), 'on_change_with_company_visible')
|
fields.Boolean("Visible"), 'on_change_with_company_visible')
|
||||||
lc_date = fields.Date("LC date")
|
lc_date = fields.Date("LC date")
|
||||||
product_origin = fields.Char("Origin")
|
product_origin = fields.Char(
|
||||||
|
"Origin", states={'invisible': Eval('company_visible'),})
|
||||||
|
|
||||||
@fields.depends('company', '_parent_company.party')
|
@fields.depends('company', '_parent_company.party')
|
||||||
def on_change_with_company_visible(self, name=None):
|
def on_change_with_company_visible(self, name=None):
|
||||||
return bool(
|
return bool(
|
||||||
self.company and self.company.party
|
self.company and self.company.party
|
||||||
and self.company.party.name == 'MELYA')
|
and self.company.party.name in {'MELYA', 'ITSA'})
|
||||||
|
|
||||||
def _get_default_bank_account(self):
|
def _get_default_bank_account(self):
|
||||||
if not self.party or not self.party.bank_accounts:
|
if not self.party or not self.party.bank_accounts:
|
||||||
|
|||||||
@@ -272,6 +272,46 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
self.assertEqual(fee.quantity, Decimal('40.00000'))
|
self.assertEqual(fee.quantity, Decimal('40.00000'))
|
||||||
save.assert_called_once_with([fee])
|
save.assert_called_once_with([fee])
|
||||||
|
|
||||||
|
def test_fee_pnl_regeneration_uses_shipment_lotqt_purchase_line(self):
|
||||||
|
'fee pnl regeneration follows shipment lot.qt back to purchase line'
|
||||||
|
Fee = Pool().get('fee.fee')
|
||||||
|
purchase_line = Mock(id=1)
|
||||||
|
lot_p = Mock(line=purchase_line, sale_line=None)
|
||||||
|
shipment = Mock(
|
||||||
|
incoming_moves=[],
|
||||||
|
lotqt=[Mock(lot_p=lot_p, lot_s=None)],
|
||||||
|
)
|
||||||
|
fee = Mock(line=None, sale_line=None, lots=[], shipment_in=shipment)
|
||||||
|
valuation = Mock()
|
||||||
|
|
||||||
|
with patch('trytond.modules.purchase_trade.fee.Pool') as PoolMock:
|
||||||
|
PoolMock.return_value.get.return_value = valuation
|
||||||
|
|
||||||
|
Fee._regenerate_fee_pnl(fees=[fee])
|
||||||
|
|
||||||
|
valuation.generate.assert_called_once_with(
|
||||||
|
purchase_line, valuation_type='fees')
|
||||||
|
valuation.generate_from_sale_line.assert_not_called()
|
||||||
|
|
||||||
|
def test_fee_pnl_regeneration_updates_purchase_and_sale_lot_lines(self):
|
||||||
|
'fee pnl regeneration follows physical fee lots to purchase and sale'
|
||||||
|
Fee = Pool().get('fee.fee')
|
||||||
|
purchase_line = Mock(id=1)
|
||||||
|
sale_line = Mock(id=2)
|
||||||
|
lot = Mock(line=purchase_line, sale_line=sale_line)
|
||||||
|
fee = Mock(line=None, sale_line=None, lots=[lot], shipment_in=None)
|
||||||
|
valuation = Mock()
|
||||||
|
|
||||||
|
with patch('trytond.modules.purchase_trade.fee.Pool') as PoolMock:
|
||||||
|
PoolMock.return_value.get.return_value = valuation
|
||||||
|
|
||||||
|
Fee._regenerate_fee_pnl(fees=[fee])
|
||||||
|
|
||||||
|
valuation.generate.assert_called_once_with(
|
||||||
|
purchase_line, valuation_type='fees')
|
||||||
|
valuation.generate_from_sale_line.assert_called_once_with(
|
||||||
|
sale_line, valuation_type='fees')
|
||||||
|
|
||||||
def test_purchase_fee_pnl_ignores_virtual_fee_lot_when_physical_exists(self):
|
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'
|
'purchase fee pnl uses physical fee lots instead of the residual virtual'
|
||||||
Valuation = Pool().get('valuation.valuation')
|
Valuation = Pool().get('valuation.valuation')
|
||||||
|
|||||||
Reference in New Issue
Block a user