From 90eab7343091064942ef3d4ac6c2272430822077 Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Thu, 9 Apr 2026 22:46:32 +0200 Subject: [PATCH] price component --- AGENTS.md | 12 ++++++ modules/purchase_trade/purchase.py | 26 ++++++++---- modules/purchase_trade/sale.py | 22 +++++++--- modules/purchase_trade/tests/test_module.py | 45 +++++++++++++++++++++ notes/template_business_rules.md | 38 +++++++++++++++++ 5 files changed, 131 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f7bc744..65698ed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -124,3 +124,15 @@ Toujours fournir: 3. Proposer le patch minimal. 4. Implementer + tester cible. 5. Rendre avec le contrat de sortie (section 8). +- Rappels session 2026-04-09: + - `invoice_ict.fodt` / `invoice_ict_final.fodt`: poids et unites depuis `lot.qt.hist` / `lot_unit_line`, priorite lots `physic`, sinon lot `virtual` unique. + - `invoice_ict.fodt` / `invoice_ict_final.fodt`: infos shipment depuis les lots reels des lignes facture; ne rien afficher si plusieurs shipments differents. + - `invoice_ict.fodt` / `invoice_ict_final.fodt`: `S/I` = `shipment.reference`; `NB BALES: 0` => `Unchanged` sur le final. + - `invoice_ict.fodt` / `invoice_ict_final.fodt`: quantites uniformisees a `2` decimales; conversion `LBS` via UoM, jamais via un facteur fixe aveugle. + - `invoice_ict.fodt` / `invoice_ict_final.fodt`: si plusieurs lignes reutilisent le meme lot, les lignes detaillees suivent la quantite facturee convertie, mais le `GROSS` global doit rester le vrai delta historique du lot. + - `sale_ict.fodt`: meme priorite lots; les mots suivent l'unite reelle; le total convertit vers une unite commune, qui est celle du lot virtuel seulement s'il y a un seul lot virtuel sur tout le report. + - `lot.report.r_del_period`: utiliser `sale.line.del_period` pour `lot_s` sans `lot_p`, sinon `purchase.line.del_period`. + - `lot.do_weighing`: `lot_qt` editable et ecrasement direct de `lot.lot_qt`. + - `account.invoice`: `Validate` cree aussi le `account.move` pour les factures client; `Post` ne doit plus forcer une fresh session sur ce flux. + - `pricing.pricing`: saisie manuelle autorisee meme sans composant; `eod_price` non editable et calcule en prix moyen pondere; `last=True` gere par groupe `line + component`, choisi sur la `pricing_date` la plus grande. + - `purchase_trade`: `trader` filtre sur `TRADER`, `operator` sur `OPERATOR`; fallback sur `quantity` si `quantity_theorical` est vide dans les quotas/pricings. diff --git a/modules/purchase_trade/purchase.py b/modules/purchase_trade/purchase.py index 4cf041b..3cd2b5e 100755 --- a/modules/purchase_trade/purchase.py +++ b/modules/purchase_trade/purchase.py @@ -1240,11 +1240,15 @@ class Line(metaclass=PoolMeta): if self.tol_max and self.quantity_theorical: return round((1+(self.tol_max/100))*Decimal(self.quantity_theorical),3) - def get_progress(self,name): - PS = Pool().get('purchase.pricing.summary') - ps = PS.search(['line','=',self.id]) - if ps: - return sum((e.progress if e.progress else 0) * (e.ratio if e.ratio else 0) / 100 for e in ps) + def get_progress(self,name): + PS = Pool().get('purchase.pricing.summary') + ps = PS.search(['line','=',self.id]) + if ps: + if not self.price_components: + manual = [e for e in ps if not e.price_component] + if manual: + return manual[0].progress or 0 + return sum((e.progress if e.progress else 0) * (e.ratio if e.ratio else 0) / 100 for e in ps) def getVirtualLot(self): if self.lots: @@ -1296,8 +1300,16 @@ class Line(metaclass=PoolMeta): for t in self.terms: price += (t.manual_price if t.manual_price else Decimal(0)) else: - for pc in self.price_components: - PP = Pool().get('purchase.pricing.summary') + if not self.price_components: + PP = Pool().get('purchase.pricing.summary') + pp = PP.search([ + ('line', '=', self.id), + ('price_component', '=', None), + ], limit=1) + if pp: + return round(Decimal(pp[0].price or 0), 4) + for pc in self.price_components: + PP = Pool().get('purchase.pricing.summary') pp = PP.search([('price_component','=',pc.id),('line','=',self.id)]) if pp: price += pp[0].price * (pc.ratio / 100) diff --git a/modules/purchase_trade/sale.py b/modules/purchase_trade/sale.py index 76736db..4dc2535 100755 --- a/modules/purchase_trade/sale.py +++ b/modules/purchase_trade/sale.py @@ -1167,11 +1167,15 @@ class SaleLine(metaclass=PoolMeta): if self.tol_max and self.quantity_theorical: return round((1+(self.tol_max/100))*Decimal(self.quantity_theorical),3) - def get_progress(self,name): - PS = Pool().get('sale.pricing.summary') - ps = PS.search(['sale_line','=',self.id]) - if ps: - return sum((e.progress if e.progress else 0) * (e.ratio if e.ratio else 0) / 100 for e in ps) + def get_progress(self,name): + PS = Pool().get('sale.pricing.summary') + ps = PS.search(['sale_line','=',self.id]) + if ps: + if not self.price_components: + manual = [e for e in ps if not e.price_component] + if manual: + return manual[0].progress or 0 + return sum((e.progress if e.progress else 0) * (e.ratio if e.ratio else 0) / 100 for e in ps) def getVirtualLot(self): if self.lots: @@ -1225,6 +1229,14 @@ class SaleLine(metaclass=PoolMeta): def _get_basis_component_price(self): price = Decimal(0) + if not self.price_components: + PP = Pool().get('sale.pricing.summary') + pp = PP.search([ + ('sale_line', '=', self.id), + ('price_component', '=', None), + ], limit=1) + if pp: + return round(Decimal(pp[0].price or 0), 4) for pc in self.price_components: PP = Pool().get('sale.pricing.summary') pp = PP.search([('price_component','=',pc.id),('sale_line','=',self.id)]) diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 268dbda..1f535f0 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -336,6 +336,51 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual( Purchase.operator.domain, [('categories.name', '=', 'OPERATOR')]) + def test_sale_line_basis_price_and_progress_use_manual_summary_without_component(self): + 'sale line basis values use manual summary rows even without a component' + SaleLine = Pool().get('sale.line') + summary_model = Mock() + summary_model.search.side_effect = [ + [Mock(price=Decimal('150'), progress=1, price_component=None)], + [Mock(price=Decimal('150'), progress=1, price_component=None)], + ] + + line = SaleLine() + line.id = 1 + line.price_type = 'basis' + line.price_components = [] + line.enable_linked_currency = False + line.linked_currency = None + + with patch('trytond.modules.purchase_trade.sale.Pool') as PoolMock: + PoolMock.return_value.get.return_value = summary_model + + self.assertEqual(line.get_basis_price(), Decimal('150.0000')) + self.assertEqual(line.get_progress('progress'), 1) + + def test_purchase_line_basis_price_and_progress_use_manual_summary_without_component(self): + 'purchase line basis values use manual summary rows even without a component' + PurchaseLine = Pool().get('purchase.line') + summary_model = Mock() + summary_model.search.side_effect = [ + [Mock(price=Decimal('150'), progress=1, price_component=None)], + [Mock(price=Decimal('150'), progress=1, price_component=None)], + ] + + line = PurchaseLine() + line.id = 1 + line.price_type = 'basis' + line.price_components = [] + line.terms = [] + line.enable_linked_currency = False + line.linked_currency = None + + with patch('trytond.modules.purchase_trade.purchase.Pool') as PoolMock: + PoolMock.return_value.get.return_value = summary_model + + self.assertEqual(line.get_basis_price(), Decimal('150.0000')) + self.assertEqual(line.get_progress('progress'), 1) + 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') diff --git a/notes/template_business_rules.md b/notes/template_business_rules.md index 83a8ef4..7afd4fe 100644 --- a/notes/template_business_rules.md +++ b/notes/template_business_rules.md @@ -49,3 +49,41 @@ Scope: templates Relatorio + ponts `report_*` Python. - La source metier est-elle correcte (compagnie vs client, total vs unit_price, maturity date vs payment term)? - Les formats sont-ils conformes (date, devise, montant en lettres)? - Le template est-il bien expose dans la config + menu d'impression de la forme cible? + +## 5) Session 2026-04-09 - Rappels metier purchase_trade / account_invoice + +- Factures trade: + - `NET` et `GROSS` viennent de `lot.qt.hist` du lot retenu, pas de `invoice.line.quantity`. + - priorite lots: `physic` d'abord, sinon lot `virtual` unique. + - l'unite affichee vient de `lot.lot_unit_line`. + - les infos shipment doivent dependre des lots reels des lignes facture. + - le label `S/I` doit afficher `shipment.reference`. + - `invoice_ict_final.fodt`: si `NB BALES = 0`, afficher `Unchanged`. + - les quantites de `invoice_ict` et `invoice_ict_final` sont uniformisees a `2` decimales. + - la conversion vers `LBS` doit passer par `product.uom.compute_qty`, pas un facteur fixe. + - si plusieurs lignes reutilisent le meme lot, les lignes detaillees utilisent la quantite facturee convertie, mais le `GROSS` global doit continuer a refleter le vrai delta historique du lot. +- CN / DN: + - cote `sale` / `out`: montant negatif => `Credit Note`, montant positif => `Debit Note`. + - cote `purchase` / `in`: logique inverse conservee. +- Report sale: + - meme priorite lots que facture. + - les quantites en lettres suivent l'unite reelle (`MT`, `KILOGRAM`, `LBS`). + - le total convertit les lignes vers une unite commune. + - l'unite commune est celle du lot virtuel seulement s'il y a un seul lot virtuel sur tout le report. +- Lots: + - `lot.report.r_del_period` affiche `sale.line.del_period` pour `lot_s` sans `lot_p`, sinon `purchase.line.del_period`. + - dans `Do weighing`, `lot_qt` doit etre editable et ecraser directement `lot.lot_qt`. +- Factures client / fournisseur: + - `Validate` cree aussi le `account.move` pour les factures client. + - `Post` ne doit plus forcer une fresh session / demande de mot de passe sur ce flux. +- Pricing: + - `pricing.pricing` peut etre saisi manuellement meme sans composant. + - `fixed_qt`, `fixed_qt_price`, `unfixed_qt`, `unfixed_qt_price` sont editables. + - `eod_price` reste non editable et suit le prix moyen pondere. + - le mode auto suit la meme formule. + - `last` est gere par groupe metier (`line + component`), avec un seul `last=True` par groupe. + - la ligne `last=True` est celle de `pricing_date` la plus grande; `id` ne sert qu'en tie-break. +- Divers: + - `trader` filtre sur `TRADER`. + - `operator` filtre sur `OPERATOR`. + - les quotas/pricings doivent fallback sur `quantity` si `quantity_theorical` est vide.