From 51ced23ab806e806f0fa2917808f96c86793406b Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Thu, 2 Apr 2026 12:12:49 +0200 Subject: [PATCH] 02.04.26 --- modules/account_invoice/invoice.xml | 4 +- modules/purchase_trade/AGENTS.md | 4 ++ modules/purchase_trade/docs/business-rules.md | 22 ++++++ modules/purchase_trade/docs/template-rules.md | 11 +++ modules/purchase_trade/invoice.py | 20 ++++++ modules/purchase_trade/sale.py | 72 ++++++++++++++----- modules/purchase_trade/tests/test_module.py | 46 ++++++++++++ 7 files changed, 158 insertions(+), 21 deletions(-) diff --git a/modules/account_invoice/invoice.xml b/modules/account_invoice/invoice.xml index cc704bc..228a1a3 100755 --- a/modules/account_invoice/invoice.xml +++ b/modules/account_invoice/invoice.xml @@ -288,7 +288,7 @@ this repository contains the full copyright notices and license terms. --> - Provisional Invoice + Invoice account.invoice account.invoice account_invoice/invoice.fodt @@ -314,7 +314,7 @@ this repository contains the full copyright notices and license terms. --> - Final Invoice + CN/DN account.invoice account.invoice account_invoice/invoice_ict_final.fodt diff --git a/modules/purchase_trade/AGENTS.md b/modules/purchase_trade/AGENTS.md index 7489422..18509c8 100644 --- a/modules/purchase_trade/AGENTS.md +++ b/modules/purchase_trade/AGENTS.md @@ -61,6 +61,10 @@ de negoce physique: - ne pas multiplier des chemins d'acces concurrents - Le `FREIGHT VALUE` d'un template facture vient du `fee.fee` du shipment dont le produit est `Maritime freight`. +- 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 + creations automatiques de lots dans les validations. - En valuation / PnL: - la valeur stockee dans `type` est la cle technique (`pur. priced`, `sale priced`, `pur. fee`, etc.), pas le label affiche dans l'UI diff --git a/modules/purchase_trade/docs/business-rules.md b/modules/purchase_trade/docs/business-rules.md index 0c0ea9b..975ae0d 100644 --- a/modules/purchase_trade/docs/business-rules.md +++ b/modules/purchase_trade/docs/business-rules.md @@ -283,6 +283,28 @@ Owner technique: `a completer` - Priorite: - `importante` +### BR-PT-013 - Create Contracts multi-lots doit conserver un matching par lot source + +- Intent: permettre la creation d'un seul contrat mirror a partir de plusieurs + open quantities sans perdre le lien lot-a-lot. +- Description: + - Le wizard `Create contracts` peut etre lance avec plusieurs `lot.qt` + selectionnes. + - En creation `matched`, le systeme doit creer un seul contrat avec une ligne + par lot source selectionne, et chaque ligne doit etre matchee avec son lot + d'origine. +- Resultat attendu: + - la quantite totale du wizard = somme des open quantities selectionnees + - le contrat cree porte plusieurs lignes si plusieurs lots source sont + selectionnes + - chaque ligne creee reutilise le `shipment_origin` et le lot source qui lui + correspondent + - `created_by_code` doit rester positionne sur les lignes creees par wizard + pour eviter la recreation automatique de lots virtuels dans les `validate` + de `purchase.line`, `sale.line` et `lot.lot` +- Priorite: + - `importante` + ## 4) Exemples concrets ### Exemple E1 - Augmentation simple diff --git a/modules/purchase_trade/docs/template-rules.md b/modules/purchase_trade/docs/template-rules.md index 1a140ce..082006e 100644 --- a/modules/purchase_trade/docs/template-rules.md +++ b/modules/purchase_trade/docs/template-rules.md @@ -138,6 +138,17 @@ Derniere mise a jour: `2026-03-27` - `purchase.report_delivery_period_description` - `invoice.report_delivery_period_description` +### TR-010 - En template, un contrat `basis` affiche le premium comme prix + +- Pour les templates commerciaux/facture (`sale_ict`, `invoice_ict`, etc.), + le prix affiche d'une ligne `basis` ne doit pas etre le prix economique total + (`unit_price`, `linked_price` ou prix basis brut). +- La valeur a afficher est uniquement le `premium`: + - en devise/unite liee si `linked currency` est active + - sinon dans la devise/unite native de la ligne +- Le texte de curve / pricing (`ON ICE ...`) reste affiche a cote, mais la + valeur numerique et sa version en lettres doivent representer le premium. + ## 4) Workflow recommande pour corriger un template en erreur 1. Identifier le placeholder exact qui provoque l'erreur Relatorio. diff --git a/modules/purchase_trade/invoice.py b/modules/purchase_trade/invoice.py index 1985548..084c27f 100644 --- a/modules/purchase_trade/invoice.py +++ b/modules/purchase_trade/invoice.py @@ -1,6 +1,7 @@ from decimal import Decimal from trytond.pool import Pool, PoolMeta +from trytond.modules.purchase_trade.numbers_to_words import amount_to_currency_words class Invoice(metaclass=PoolMeta): @@ -298,6 +299,10 @@ class Invoice(metaclass=PoolMeta): sale = self._get_report_sale() if sale and sale.report_gross != '': return sale.report_gross + if self.lines: + return sum( + Decimal(str(getattr(line, 'quantity', 0) or 0)) + for line in self._get_report_invoice_lines()) line = self._get_report_trade_line() if line and line.lots: return sum( @@ -311,6 +316,10 @@ class Invoice(metaclass=PoolMeta): trade = self._get_report_trade() if trade and getattr(trade, 'report_net', '') != '': return trade.report_net + if self.lines: + return sum( + Decimal(str(getattr(line, 'quantity', 0) or 0)) + for line in self._get_report_invoice_lines()) line = self._get_report_trade_line() if line and line.lots: return sum( @@ -463,6 +472,11 @@ class InvoiceLine(metaclass=PoolMeta): @property def report_rate_value(self): + origin = self._get_report_trade_line() + if origin and getattr(origin, 'price_type', None) == 'basis': + if getattr(origin, 'enable_linked_currency', False) and getattr(origin, 'linked_currency', None): + return Decimal(str(origin.premium or 0)) + return Decimal(str(origin._get_premium_price() or 0)) return self.unit_price if self.unit_price is not None else '' @property @@ -475,6 +489,12 @@ class InvoiceLine(metaclass=PoolMeta): @property def report_rate_price_words(self): + origin = self._get_report_trade_line() + if origin and getattr(origin, 'price_type', None) == 'basis': + value = self.report_rate_value + if self.report_rate_currency_upper == 'USC': + return amount_to_currency_words(value, 'USC', 'USC') + return amount_to_currency_words(value) trade = self._get_report_trade() if trade and getattr(trade, 'report_price', None): return trade.report_price diff --git a/modules/purchase_trade/sale.py b/modules/purchase_trade/sale.py index 1b21b4d..56ea13d 100755 --- a/modules/purchase_trade/sale.py +++ b/modules/purchase_trade/sale.py @@ -343,19 +343,43 @@ class Sale(metaclass=PoolMeta): return text or '0' def _format_report_price_words(self, line): + value = self._get_report_display_price_value(line) + currency = self._get_report_display_currency(line) + if currency and (currency.rec_name or '').upper() == 'USC': + return amount_to_currency_words(value, 'USC', 'USC') + return amount_to_currency_words(value) + + def _get_report_display_currency(self, line): + if getattr(line, 'price_type', None) == 'basis': + if getattr(line, 'enable_linked_currency', False) and getattr(line, 'linked_currency', None): + return line.linked_currency + return self.currency + return getattr(line, 'linked_currency', None) or self.currency + + def _get_report_display_unit(self, line): + if getattr(line, 'price_type', None) == 'basis': + if getattr(line, 'enable_linked_currency', False) and getattr(line, 'linked_unit', None): + return line.linked_unit + return getattr(line, 'unit', None) + return getattr(line, 'linked_unit', None) or getattr(line, 'unit', None) + + def _get_report_display_price_value(self, line): + if getattr(line, 'price_type', None) == 'basis': + if getattr(line, 'enable_linked_currency', False) and getattr(line, 'linked_currency', None): + return Decimal(str(line.premium or 0)) + return Decimal(str(line._get_premium_price() or 0)) if getattr(line, 'linked_price', None): - return amount_to_currency_words(line.linked_price, 'USC', 'USC') - return amount_to_currency_words(line.unit_price) + return Decimal(str(line.linked_price or 0)) + return Decimal(str(line.unit_price or 0)) def _format_report_price_line(self, line): - currency = getattr(line, 'linked_currency', None) or self.currency - unit = getattr(line, 'linked_unit', None) or getattr(line, 'unit', None) + currency = self._get_report_display_currency(line) + unit = self._get_report_display_unit(line) pricing_text = getattr(line, 'get_pricing_text', '') or '' parts = [ (currency.rec_name.upper() if currency and currency.rec_name else '').strip(), self._format_report_number( - line.linked_price if getattr(line, 'linked_price', None) - else line.unit_price, + self._get_report_display_price_value(line), strip_trailing_zeros=False), 'PER', (unit.rec_name.upper() if unit and unit.rec_name else '').strip(), @@ -374,16 +398,32 @@ class Sale(metaclass=PoolMeta): @property def report_gross(self): - line = self._get_report_first_line() - if line: - return sum([l.get_current_gross_quantity() for l in line.lots if l.lot_type == 'physic']) + lines = self._get_report_lines() + if lines: + total = Decimal(0) + for line in lines: + phys_lots = [l for l in line.lots if l.lot_type == 'physic'] + if phys_lots: + total += sum(Decimal(str(l.get_current_gross_quantity() or 0)) + for l in phys_lots) + else: + total += Decimal(str(line.quantity or 0)) + return total return '' @property def report_net(self): - line = self._get_report_first_line() - if line: - return sum([l.get_current_quantity() for l in line.lots if l.lot_type == 'physic']) + lines = self._get_report_lines() + if lines: + total = Decimal(0) + for line in lines: + phys_lots = [l for l in line.lots if l.lot_type == 'physic'] + if phys_lots: + total += sum(Decimal(str(l.get_current_quantity() or 0)) + for l in phys_lots) + else: + total += Decimal(str(line.quantity or 0)) + return total return '' @property @@ -474,13 +514,7 @@ class Sale(metaclass=PoolMeta): def report_price(self): line = self._get_report_first_line() if line: - if line.price_type == 'priced': - if line.linked_price: - return amount_to_currency_words(line.linked_price,'USC','USC') - else: - return amount_to_currency_words(line.unit_price) - elif line.price_type == 'basis': - return amount_to_currency_words(line.unit_price) + ' ' + line.get_pricing_text + return self._format_report_price_words(line) return '' @property diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 9fbbe1a..85bfd07 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -233,5 +233,51 @@ class PurchaseTradeTestCase(ModuleTestCase): with self.assertRaises(UserError): ContractFactory._get_line_sources(contract_detail, sources, ct) + def test_sale_report_price_lines_basis_displays_premium_only(self): + 'basis report pricing displays only the premium in templates' + Sale = Pool().get('sale.sale') + + line = Mock() + line.type = 'line' + line.price_type = 'basis' + line.enable_linked_currency = True + line.linked_currency = Mock(rec_name='USC') + line.linked_unit = Mock(rec_name='POUND') + line.unit = Mock(rec_name='MT') + line.unit_price = Decimal('1598.3495') + line.linked_price = Decimal('72.5000') + line.premium = Decimal('8.3000') + line.get_pricing_text = 'ON ICE Cotton #2 MARCH 2026' + + sale = Sale() + sale.currency = Mock(rec_name='USD') + sale.lines = [line] + + self.assertEqual( + sale.report_price_lines, + 'USC 8.3000 PER POUND (EIGHT USC AND THIRTY CENTS) ON ICE Cotton #2 MARCH 2026') + + def test_sale_report_net_and_gross_sum_all_lines(self): + 'sale report totals aggregate every line instead of the first one only' + Sale = Pool().get('sale.sale') + + def make_lot(quantity): + lot = Mock() + lot.lot_type = 'physic' + lot.get_current_quantity.return_value = Decimal(quantity) + lot.get_current_gross_quantity.return_value = Decimal(quantity) + return lot + + line_a = Mock(type='line', quantity=Decimal('1000')) + line_a.lots = [make_lot('1000')] + line_b = Mock(type='line', quantity=Decimal('1000')) + line_b.lots = [make_lot('1000')] + + sale = Sale() + sale.lines = [line_a, line_b] + + self.assertEqual(sale.report_net, Decimal('2000')) + self.assertEqual(sale.report_gross, Decimal('2000')) + del ModuleTestCase