Th qt correction
This commit is contained in:
@@ -66,6 +66,17 @@ de negoce physique:
|
|||||||
- ne pas proratiser depuis le poids (`net` / `gross`)
|
- ne pas proratiser depuis le poids (`net` / `gross`)
|
||||||
- Le `FREIGHT VALUE` d'un template facture vient du `fee.fee` du shipment
|
- Le `FREIGHT VALUE` d'un template facture vient du `fee.fee` du shipment
|
||||||
dont le produit est `Maritime freight`.
|
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
|
- Le wizard `Create contracts` en mode `matched` peut maintenant partir de
|
||||||
plusieurs `lot.qt`, mais doit conserver un matching par lot source et laisser
|
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
|
`created_by_code = True` sur les lignes creees pour ne pas declencher les
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Template Properties - Purchase Trade
|
# Template Properties - Purchase Trade
|
||||||
|
|
||||||
Statut: `draft`
|
Statut: `draft`
|
||||||
Version: `v0.1`
|
Version: `v0.2`
|
||||||
Derniere mise a jour: `2026-03-27`
|
Derniere mise a jour: `2026-04-07`
|
||||||
|
|
||||||
## 1) Objectif
|
## 1) Objectif
|
||||||
|
|
||||||
@@ -200,6 +200,81 @@ Source code: `modules/purchase_trade/invoice.py`
|
|||||||
- Usage: devise du `FREIGHT VALUE`
|
- Usage: devise du `FREIGHT VALUE`
|
||||||
- Source de verite: devise du fee `Maritime freight`, fallback devise facture
|
- 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`
|
## 5) Proprietes disponibles sur `account.invoice.line`
|
||||||
|
|
||||||
Source code: `modules/purchase_trade/invoice.py`
|
Source code: `modules/purchase_trade/invoice.py`
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Template Rules - Purchase Trade
|
# Template Rules - Purchase Trade
|
||||||
|
|
||||||
Statut: `draft`
|
Statut: `draft`
|
||||||
Version: `v0.3`
|
Version: `v0.4`
|
||||||
Derniere mise a jour: `2026-04-02`
|
Derniere mise a jour: `2026-04-07`
|
||||||
|
|
||||||
## 1) Scope
|
## 1) Scope
|
||||||
|
|
||||||
@@ -167,11 +167,48 @@ Derniere mise a jour: `2026-04-02`
|
|||||||
- numero du certificat: `shipment.bl_number`, sinon `shipment.number`
|
- numero du certificat: `shipment.bl_number`, sinon `shipment.number`
|
||||||
- `insured for account of`: client de la premiere ligne metier retrouvee via
|
- `insured for account of`: client de la premiere ligne metier retrouvee via
|
||||||
lot physique, sinon `shipment.supplier`
|
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
|
- lieu/date d'emission: ville de la societe + date du jour
|
||||||
- Si une source differente est decidee plus tard, corriger la propriete Python
|
- Si une source differente est decidee plus tard, corriger la propriete Python
|
||||||
plutot que complexifier `insurance.fodt`
|
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
|
### 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.
|
- 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")
|
del_period = fields.Many2One('product.month',"Delivery Period")
|
||||||
lots = fields.One2Many('lot.lot','sale_line',"Lots",readonly=True)
|
lots = fields.One2Many('lot.lot','sale_line',"Lots",readonly=True)
|
||||||
fees = fields.One2Many('fee.fee', 'sale_line', 'Fees')
|
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')
|
premium = fields.Numeric("Premium/Discount",digits='unit')
|
||||||
price_type = fields.Selection([
|
price_type = fields.Selection([
|
||||||
('cash', 'Cash Price'),
|
('cash', 'Cash Price'),
|
||||||
@@ -1245,28 +1245,77 @@ class SaleLine(metaclass=PoolMeta):
|
|||||||
Pricing.save([p])
|
Pricing.save([p])
|
||||||
index += 1
|
index += 1
|
||||||
|
|
||||||
# @classmethod
|
@classmethod
|
||||||
# def write(cls, records, values):
|
def write(cls, *args):
|
||||||
# if 'quantity' in values:
|
Lot = Pool().get('lot.lot')
|
||||||
# for record in records:
|
LotQt = Pool().get('lot.qt')
|
||||||
# old_qt = record.quantity
|
old_values = {}
|
||||||
# new_qt = values['quantity']
|
|
||||||
# logger.info("WRITE_OLD_QT:%s",old_qt)
|
for records, values in zip(args[::2], args[1::2]):
|
||||||
# logger.info("WRITE_NEW_QT:%s",new_qt)
|
if 'quantity_theorical' in values:
|
||||||
# if old_qt != new_qt:
|
for record in records:
|
||||||
# LotQt = Pool().get('lot.qt')
|
old_values[record.id] = record.quantity_theorical
|
||||||
# lqts = LotQt.search(['lot_s','=',record.lots[0]])
|
|
||||||
# if len(lqts)>1:
|
super().write(*args)
|
||||||
# raise UserError("You cannot changed quantity with open quantities defined !")
|
|
||||||
# return
|
lines = sum(args[::2], [])
|
||||||
# elif len(lqts)==1:
|
for line in lines:
|
||||||
# if lqts[0].lot_p or lqts[0].lot_shipment_origin:
|
if line.id not in old_values:
|
||||||
# raise UserError("You cannot changed quantity with open quantities defined !")
|
continue
|
||||||
# return
|
old = Decimal(old_values[line.id] or 0)
|
||||||
# lqts[0].lot_quantity = new_qt
|
new = Decimal(line.quantity_theorical or 0)
|
||||||
# LotQt.save(lqts)
|
delta = new - old
|
||||||
|
if delta == 0:
|
||||||
# super().write(records, values)
|
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
|
@classmethod
|
||||||
def delete(cls, lines):
|
def delete(cls, lines):
|
||||||
|
|||||||
@@ -184,6 +184,80 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
PurchaseLine.default_pricing_rule(), 'Default pricing rule')
|
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):
|
def test_party_execution_achieved_percent_uses_real_area_statistics(self):
|
||||||
'party execution achieved percent reflects the controller share in its area'
|
'party execution achieved percent reflects the controller share in its area'
|
||||||
PartyExecution = Pool().get('party.execution')
|
PartyExecution = Pool().get('party.execution')
|
||||||
|
|||||||
Reference in New Issue
Block a user