Th qt correction
This commit is contained in:
@@ -66,6 +66,17 @@ de negoce physique:
|
||||
- ne pas proratiser depuis le poids (`net` / `gross`)
|
||||
- Le `FREIGHT VALUE` d'un template facture vient du `fee.fee` du shipment
|
||||
dont le produit est `Maritime freight`.
|
||||
- Pour `stock/insurance.fodt`, le `Amount insured` doit venir en priorite de
|
||||
`110%` du total des `incoming_moves` (fallback fee `Insurance` si aucun
|
||||
montant incoming calculable).
|
||||
- Pour le surveyor du certificat d'assurance shipment, la priorite est:
|
||||
`shipment.surveyor` -> `shipment.controller` -> fournisseur du fee
|
||||
`Insurance`.
|
||||
- Pour `payment_order.fodt`, utiliser des proprietes
|
||||
`invoice.report_payment_order_*` plutot que des tokens legacy `<...>`.
|
||||
- Ajouter un champ de template dans `Document Templates` ne rend pas le report
|
||||
visible dans la fiche: il faut aussi l'action `ir.action.report` +
|
||||
`ir.action.keyword` (`form_print`) cote `account.invoice`.
|
||||
- Le wizard `Create contracts` en mode `matched` peut maintenant partir de
|
||||
plusieurs `lot.qt`, mais doit conserver un matching par lot source et laisser
|
||||
`created_by_code = True` sur les lignes creees pour ne pas declencher les
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Template Properties - Purchase Trade
|
||||
|
||||
Statut: `draft`
|
||||
Version: `v0.1`
|
||||
Derniere mise a jour: `2026-03-27`
|
||||
Version: `v0.2`
|
||||
Derniere mise a jour: `2026-04-07`
|
||||
|
||||
## 1) Objectif
|
||||
|
||||
@@ -200,6 +200,81 @@ Source code: `modules/purchase_trade/invoice.py`
|
||||
- Usage: devise du `FREIGHT VALUE`
|
||||
- Source de verite: devise du fee `Maritime freight`, fallback devise facture
|
||||
|
||||
### Payment order
|
||||
|
||||
- `report_payment_order_short_name`
|
||||
- Usage: nom court emetteur du payment order
|
||||
- Source de verite: `invoice.company.party.rec_name`
|
||||
|
||||
- `report_payment_order_document_reference`
|
||||
- Usage: reference du document payment order
|
||||
- Source de verite: `invoice.number`, fallback `invoice.reference`
|
||||
|
||||
- `report_payment_order_from_account_nb`
|
||||
- Usage: compte bancaire emetteur
|
||||
- Source de verite: premier `bank.account` de la societe
|
||||
|
||||
- `report_payment_order_to_bank_name`
|
||||
- Usage: banque destinataire
|
||||
- Source de verite: banque du premier compte bancaire du partenaire facture
|
||||
|
||||
- `report_payment_order_to_bank_city`
|
||||
- Usage: ville banque destinataire
|
||||
- Source de verite: adresse de la banque destinataire
|
||||
|
||||
- `report_payment_order_amount`
|
||||
- Usage: montant payment order
|
||||
- Source de verite: `invoice.total_amount`
|
||||
|
||||
- `report_payment_order_currency_code`
|
||||
- Usage: devise payment order
|
||||
- Source de verite: `invoice.currency` (`code`, fallback `rec_name/symbol`)
|
||||
|
||||
- `report_payment_order_amount_text`
|
||||
- Usage: montant en lettres
|
||||
- Source de verite: conversion `amount_to_currency_words(invoice.total_amount)`
|
||||
|
||||
- `report_payment_order_value_date`
|
||||
- Usage: date valeur
|
||||
- Source de verite: `invoice.payment_term_date`, fallback `invoice.invoice_date`
|
||||
|
||||
- `report_payment_order_company_address`
|
||||
- Usage: bloc beneficiaire
|
||||
- Source de verite: `invoice.invoice_address.full_address`, fallback
|
||||
`invoice.report_address`
|
||||
|
||||
- `report_payment_order_beneficiary_account_nb`
|
||||
- Usage: compte beneficiaire
|
||||
- Source de verite: premier compte bancaire du `invoice.party`
|
||||
|
||||
- `report_payment_order_beneficiary_bank_name`
|
||||
- Usage: banque beneficiaire
|
||||
- Source de verite: banque du compte beneficiaire
|
||||
|
||||
- `report_payment_order_beneficiary_bank_city`
|
||||
- Usage: ville banque beneficiaire
|
||||
- Source de verite: adresse banque beneficiaire
|
||||
|
||||
- `report_payment_order_swift_code`
|
||||
- Usage: swift/bic beneficiaire
|
||||
- Source de verite: `bank.bic`
|
||||
|
||||
- `report_payment_order_other_instructions`
|
||||
- Usage: instructions complementaires
|
||||
- Source de verite: `invoice.description`
|
||||
|
||||
- `report_payment_order_reference`
|
||||
- Usage: reference business de paiement
|
||||
- Source de verite: `invoice.reference`, fallback `invoice.number`
|
||||
|
||||
- `report_payment_order_current_user`
|
||||
- Usage: signataire payment order
|
||||
- Source de verite: utilisateur courant (`res.user`)
|
||||
|
||||
- `report_payment_order_current_user_email`
|
||||
- Usage: email retour swift
|
||||
- Source de verite: contact email du `party` utilisateur, fallback `user.email`
|
||||
|
||||
## 5) Proprietes disponibles sur `account.invoice.line`
|
||||
|
||||
Source code: `modules/purchase_trade/invoice.py`
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Template Rules - Purchase Trade
|
||||
|
||||
Statut: `draft`
|
||||
Version: `v0.3`
|
||||
Derniere mise a jour: `2026-04-02`
|
||||
Version: `v0.4`
|
||||
Derniere mise a jour: `2026-04-07`
|
||||
|
||||
## 1) Scope
|
||||
|
||||
@@ -167,11 +167,48 @@ Derniere mise a jour: `2026-04-02`
|
||||
- numero du certificat: `shipment.bl_number`, sinon `shipment.number`
|
||||
- `insured for account of`: client de la premiere ligne metier retrouvee via
|
||||
lot physique, sinon `shipment.supplier`
|
||||
- `surveyor`: `shipment.controller`, sinon fournisseur du fee `Insurance`
|
||||
- `surveyor`: `shipment.surveyor`, sinon `shipment.controller`, sinon
|
||||
fournisseur du fee `Insurance`
|
||||
- lieu/date d'emission: ville de la societe + date du jour
|
||||
- Si une source differente est decidee plus tard, corriger la propriete Python
|
||||
plutot que complexifier `insurance.fodt`
|
||||
|
||||
### TR-017 - `payment_order.fodt` doit utiliser des proprietes `report_payment_order_*`
|
||||
|
||||
- Pour `modules/account_invoice/payment_order.fodt`, ne pas utiliser des
|
||||
placeholders externes legacy (tokens metier entre `<...>` du systeme source).
|
||||
- Tous les placeholders du template doivent pointer vers des proprietes Python
|
||||
stables exposees sur `account.invoice`:
|
||||
- `report_payment_order_document_reference`
|
||||
- `report_payment_order_from_account_nb`
|
||||
- `report_payment_order_to_bank_name`
|
||||
- `report_payment_order_to_bank_city`
|
||||
- `report_payment_order_amount`
|
||||
- `report_payment_order_currency_code`
|
||||
- `report_payment_order_amount_text`
|
||||
- `report_payment_order_value_date`
|
||||
- `report_payment_order_company_address`
|
||||
- `report_payment_order_beneficiary_account_nb`
|
||||
- `report_payment_order_beneficiary_bank_name`
|
||||
- `report_payment_order_beneficiary_bank_city`
|
||||
- `report_payment_order_swift_code`
|
||||
- `report_payment_order_other_instructions`
|
||||
- `report_payment_order_reference`
|
||||
- `report_payment_order_current_user`
|
||||
- `report_payment_order_current_user_email`
|
||||
- Eviter les marqueurs conditionnels heredites de l'ancien moteur (`++...`):
|
||||
privilegier des placeholders simples avec fallback `or ''`.
|
||||
|
||||
### TR-018 - Un template configure n'apparait dans le form que si une action report existe
|
||||
|
||||
- Ajouter un champ dans `Document Templates` ne suffit pas a rendre un
|
||||
template imprimable depuis la fiche.
|
||||
- Pour afficher l'entree dans `account.invoice`, il faut aussi:
|
||||
- un `ir.action.report` sur `model = account.invoice`
|
||||
- un `ir.action.keyword` `form_print` lie a cette action
|
||||
- Appliquer cette regle pour `Payment Order` comme pour `Invoice`,
|
||||
`Prepayment` et `CN/DN`.
|
||||
|
||||
### TR-007 - Pour une facture trade, privilegier le lot physique comme chemin de navigation
|
||||
|
||||
- Pour remonter d'une facture vers des donnees logistiques ou metier, ne pas dupliquer de chemins differents selon achat/vente.
|
||||
|
||||
@@ -777,7 +777,7 @@ class SaleLine(metaclass=PoolMeta):
|
||||
del_period = fields.Many2One('product.month',"Delivery Period")
|
||||
lots = fields.One2Many('lot.lot','sale_line',"Lots",readonly=True)
|
||||
fees = fields.One2Many('fee.fee', 'sale_line', 'Fees')
|
||||
quantity_theorical = fields.Numeric("Th. quantity", digits='unit', readonly=True)
|
||||
quantity_theorical = fields.Numeric("Th. quantity", digits='unit', readonly=False)
|
||||
premium = fields.Numeric("Premium/Discount",digits='unit')
|
||||
price_type = fields.Selection([
|
||||
('cash', 'Cash Price'),
|
||||
@@ -1245,28 +1245,77 @@ class SaleLine(metaclass=PoolMeta):
|
||||
Pricing.save([p])
|
||||
index += 1
|
||||
|
||||
# @classmethod
|
||||
# def write(cls, records, values):
|
||||
# if 'quantity' in values:
|
||||
# for record in records:
|
||||
# old_qt = record.quantity
|
||||
# new_qt = values['quantity']
|
||||
# logger.info("WRITE_OLD_QT:%s",old_qt)
|
||||
# logger.info("WRITE_NEW_QT:%s",new_qt)
|
||||
# if old_qt != new_qt:
|
||||
# LotQt = Pool().get('lot.qt')
|
||||
# lqts = LotQt.search(['lot_s','=',record.lots[0]])
|
||||
# if len(lqts)>1:
|
||||
# raise UserError("You cannot changed quantity with open quantities defined !")
|
||||
# return
|
||||
# elif len(lqts)==1:
|
||||
# if lqts[0].lot_p or lqts[0].lot_shipment_origin:
|
||||
# raise UserError("You cannot changed quantity with open quantities defined !")
|
||||
# return
|
||||
# lqts[0].lot_quantity = new_qt
|
||||
# LotQt.save(lqts)
|
||||
@classmethod
|
||||
def write(cls, *args):
|
||||
Lot = Pool().get('lot.lot')
|
||||
LotQt = Pool().get('lot.qt')
|
||||
old_values = {}
|
||||
|
||||
# super().write(records, values)
|
||||
for records, values in zip(args[::2], args[1::2]):
|
||||
if 'quantity_theorical' in values:
|
||||
for record in records:
|
||||
old_values[record.id] = record.quantity_theorical
|
||||
|
||||
super().write(*args)
|
||||
|
||||
lines = sum(args[::2], [])
|
||||
for line in lines:
|
||||
if line.id not in old_values:
|
||||
continue
|
||||
old = Decimal(old_values[line.id] or 0)
|
||||
new = Decimal(line.quantity_theorical or 0)
|
||||
delta = new - old
|
||||
if delta == 0:
|
||||
continue
|
||||
|
||||
virtual_lots = [
|
||||
lot for lot in (line.lots or [])
|
||||
if lot.lot_type == 'virtual'
|
||||
]
|
||||
if not virtual_lots:
|
||||
continue
|
||||
|
||||
vlot = virtual_lots[0]
|
||||
lqts = LotQt.search([
|
||||
('lot_s', '=', vlot.id),
|
||||
('lot_p', '=', None),
|
||||
('lot_shipment_in', '=', None),
|
||||
('lot_shipment_internal', '=', None),
|
||||
('lot_shipment_out', '=', None),
|
||||
])
|
||||
|
||||
if delta > 0:
|
||||
new_qty = round(
|
||||
Decimal(vlot.get_current_quantity_converted() or 0) + delta,
|
||||
5)
|
||||
vlot.set_current_quantity(new_qty, new_qty, 1)
|
||||
Lot.save([vlot])
|
||||
if lqts:
|
||||
lqt = lqts[0]
|
||||
lqt.lot_quantity = round(
|
||||
Decimal(lqt.lot_quantity or 0) + delta, 5)
|
||||
LotQt.save([lqt])
|
||||
else:
|
||||
lqt = LotQt()
|
||||
lqt.lot_p = None
|
||||
lqt.lot_s = vlot.id
|
||||
lqt.lot_quantity = round(delta, 5)
|
||||
lqt.lot_unit = line.unit
|
||||
LotQt.save([lqt])
|
||||
elif delta < 0:
|
||||
decrease = abs(delta)
|
||||
if not lqts or Decimal(lqts[0].lot_quantity or 0) < decrease:
|
||||
raise UserError("Please unlink or unmatch lot")
|
||||
new_qty = round(
|
||||
Decimal(vlot.get_current_quantity_converted() or 0)
|
||||
- decrease,
|
||||
5)
|
||||
vlot.set_current_quantity(new_qty, new_qty, 1)
|
||||
Lot.save([vlot])
|
||||
lqt = lqts[0]
|
||||
lqt.lot_quantity = round(
|
||||
Decimal(lqt.lot_quantity or 0) - decrease, 5)
|
||||
LotQt.save([lqt])
|
||||
|
||||
@classmethod
|
||||
def delete(cls, lines):
|
||||
|
||||
@@ -184,6 +184,80 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
self.assertEqual(
|
||||
PurchaseLine.default_pricing_rule(), 'Default pricing rule')
|
||||
|
||||
def test_sale_line_write_updates_virtual_lot_when_theorical_qty_increases(self):
|
||||
'sale line write increases virtual lot and open lot.qt when contractual qty grows'
|
||||
SaleLine = Pool().get('sale.line')
|
||||
line = Mock(id=1, quantity_theorical=Decimal('10'))
|
||||
line.unit = Mock()
|
||||
vlot = Mock(id=99, lot_type='virtual')
|
||||
vlot.get_current_quantity_converted.return_value = Decimal('10')
|
||||
line.lots = [vlot]
|
||||
lqt = Mock(lot_quantity=Decimal('10'))
|
||||
|
||||
lot_model = Mock()
|
||||
lotqt_model = Mock()
|
||||
lotqt_model.search.return_value = [lqt]
|
||||
|
||||
with patch(
|
||||
'trytond.modules.purchase_trade.sale.Pool'
|
||||
) as PoolMock, patch(
|
||||
'trytond.modules.purchase_trade.sale.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
|
||||
|
||||
SaleLine.write([line], {'quantity_theorical': Decimal('12')})
|
||||
|
||||
self.assertEqual(lqt.lot_quantity, Decimal('12'))
|
||||
vlot.set_current_quantity.assert_called_once_with(
|
||||
Decimal('12'), Decimal('12'), 1)
|
||||
lot_model.save.assert_called()
|
||||
lotqt_model.save.assert_called()
|
||||
|
||||
def test_sale_line_write_blocks_theorical_qty_decrease_when_no_open_quantity(self):
|
||||
'sale line write blocks contractual qty decrease when open lot.qt is insufficient'
|
||||
SaleLine = Pool().get('sale.line')
|
||||
line = Mock(id=2, quantity_theorical=Decimal('10'))
|
||||
vlot = Mock(id=100, lot_type='virtual')
|
||||
vlot.get_current_quantity_converted.return_value = Decimal('10')
|
||||
line.lots = [vlot]
|
||||
lqt = Mock(lot_quantity=Decimal('1'))
|
||||
|
||||
lot_model = Mock()
|
||||
lotqt_model = Mock()
|
||||
lotqt_model.search.return_value = [lqt]
|
||||
|
||||
with patch(
|
||||
'trytond.modules.purchase_trade.sale.Pool'
|
||||
) as PoolMock, patch(
|
||||
'trytond.modules.purchase_trade.sale.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
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
SaleLine.write([line], {'quantity_theorical': Decimal('8')})
|
||||
|
||||
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')
|
||||
|
||||
Reference in New Issue
Block a user