From fbfa73110fc663a36eaa2f37cb1c1ee95ac00aa4 Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Sun, 26 Apr 2026 14:35:07 +0200 Subject: [PATCH] padding acc --- modules/purchase_trade/AGENTS.md | 18 ++ modules/purchase_trade/__init__.py | 8 +- modules/purchase_trade/account.py | 57 +++- modules/purchase_trade/account.xml | 6 + modules/purchase_trade/docs/business-rules.md | 17 +- .../docs/padding-invoice-accounting.md | 294 ++++++++++++++++++ modules/purchase_trade/invoice.py | 174 +++++++++++ modules/purchase_trade/stock.py | 2 +- .../view/account_configuration_form.xml | 10 + .../view/invoice_line_tree_sequence.xml | 2 +- notes/template_business_rules.md | 16 + 11 files changed, 590 insertions(+), 14 deletions(-) create mode 100644 modules/purchase_trade/docs/padding-invoice-accounting.md create mode 100644 modules/purchase_trade/view/account_configuration_form.xml diff --git a/modules/purchase_trade/AGENTS.md b/modules/purchase_trade/AGENTS.md index 8433c0e..150d34a 100644 --- a/modules/purchase_trade/AGENTS.md +++ b/modules/purchase_trade/AGENTS.md @@ -45,6 +45,8 @@ de negoce physique: - `modules/purchase_trade/docs/template-rules.md` - Catalogue des proprietes templates: - `modules/purchase_trade/docs/template-properties.md` +- Padding facture provisoire vente / validation comptable: + - `modules/purchase_trade/docs/padding-invoice-accounting.md` ## 4) Invariants metier a preserver @@ -131,6 +133,19 @@ de negoce physique: - a la validation d'une `sale.line`, si un lot virtuel est cree et qu'aucun matching purchase n'existe, il faut lancer `generate_from_sale_line()` pour alimenter le PnL sale-first +- En padding de facture provisoire vente: + - le padding est saisi globalement dans `lot.invoice` + - il est uniquement applique aux factures provisoires cote vente + - il est reparti par lot et stocke dans `lot.sale_invoice_padding` + - la ligne facture affiche `Inc. padding` depuis le lot + - le padding augmente `account.invoice.line.quantity`, donc le move principal + de facture inclut deja le montant padding + - les comptes de padding viennent de `account.configuration`: + `Default Sale Padding` et `Default Accrual Padding` + - la provisoire doit passer le couple sale/accrual, et la finale doit passer + l'inverse pour le montant exact comptabilise en provisoire + - preferer stocker ce montant au moment de la provisoire plutot que le + recalculer depuis la finale ## 5) Conventions de modification @@ -174,6 +189,9 @@ de negoce physique: - attribution du `number` - `Post` ne doit pas reintroduire une difference de session/fresh login cote client +- Pour le padding provisoire vente, ne pas changer la quantite physique du lot: + l'ecart doit rester porte par `lot.sale_invoice_padding` et visible sur la + facture via `Inc. padding`. ## 7) Definition of done (module `purchase_trade`) diff --git a/modules/purchase_trade/__init__.py b/modules/purchase_trade/__init__.py index afa8840..496f674 100755 --- a/modules/purchase_trade/__init__.py +++ b/modules/purchase_trade/__init__.py @@ -190,9 +190,11 @@ def register(): dimension.AnalyticDimensionAssignment, weight_report.WeightReport, module='purchase', type_='model') - Pool.register( - account.PhysicalTradeIFRS, - module='purchase_trade', type_='model') + Pool.register( + account.Configuration, + account.ConfigurationDefaultAccount, + account.PhysicalTradeIFRS, + module='purchase_trade', type_='model') Pool.register( invoice.Invoice, invoice.InvoiceLine, diff --git a/modules/purchase_trade/account.py b/modules/purchase_trade/account.py index b686fa6..7c213b9 100644 --- a/modules/purchase_trade/account.py +++ b/modules/purchase_trade/account.py @@ -1,12 +1,63 @@ -# account.py from trytond.model import ModelSQL, ModelView, fields -from trytond.pool import PoolMeta +from trytond.pool import Pool, PoolMeta from trytond.pyson import Eval -__all__ = ['PhysicalTradeIFRS'] +__all__ = [ + 'Configuration', + 'ConfigurationDefaultAccount', + 'PhysicalTradeIFRS', + ] __metaclass__ = PoolMeta +class Configuration(metaclass=PoolMeta): + __name__ = 'account.configuration' + + default_sale_padding_account = fields.MultiValue(fields.Many2One( + 'account.account', "Default Sale Padding", + domain=[ + ('closed', '!=', True), + ('type.revenue', '=', True), + ('company', '=', Eval('context', {}).get('company', -1)), + ])) + default_accrual_padding_account = fields.MultiValue(fields.Many2One( + 'account.account', "Default Accrual Padding", + domain=[ + ('closed', '!=', True), + ('type.statement', '=', 'balance'), + ('company', '=', Eval('context', {}).get('company', -1)), + ])) + + @classmethod + def multivalue_model(cls, field): + pool = Pool() + if field in { + 'default_sale_padding_account', + 'default_accrual_padding_account', + }: + return pool.get('account.configuration.default_account') + return super().multivalue_model(field) + + +class ConfigurationDefaultAccount(metaclass=PoolMeta): + __name__ = 'account.configuration.default_account' + + default_sale_padding_account = fields.Many2One( + 'account.account', "Default Sale Padding", + domain=[ + ('closed', '!=', True), + ('type.revenue', '=', True), + ('company', '=', Eval('company', -1)), + ]) + default_accrual_padding_account = fields.Many2One( + 'account.account', "Default Accrual Padding", + domain=[ + ('closed', '!=', True), + ('type.statement', '=', 'balance'), + ('company', '=', Eval('company', -1)), + ]) + + class PhysicalTradeIFRS(ModelSQL, ModelView): 'Physical Trade - IFRS Adjustment' __name__ = 'account.physical_trade_ifrs' diff --git a/modules/purchase_trade/account.xml b/modules/purchase_trade/account.xml index 40efb3f..23844f2 100644 --- a/modules/purchase_trade/account.xml +++ b/modules/purchase_trade/account.xml @@ -1,6 +1,12 @@ + + account.configuration + + account_configuration_form + + Physical Trade IFRS diff --git a/modules/purchase_trade/docs/business-rules.md b/modules/purchase_trade/docs/business-rules.md index af933f8..4deabe4 100644 --- a/modules/purchase_trade/docs/business-rules.md +++ b/modules/purchase_trade/docs/business-rules.md @@ -1,8 +1,8 @@ # Business Rules - Purchase Trade Statut: `draft` -Version: `v0.6` -Derniere mise a jour: `2026-04-23` +Version: `v0.7` +Derniere mise a jour: `2026-04-26` Owner metier: `a completer` Owner technique: `a completer` @@ -426,11 +426,16 @@ Owner technique: `a completer` - deux lots factures ensemble avec un padding global de `1000` recoivent chacun `500` de padding - la ligne facture affiche la quantite augmentee - - la ligne facture expose `Included padding` + - la ligne facture expose `Inc. padding` - le lot conserve sa part de padding dans `sale_invoice_padding` -- Hors scope: - - les ecritures comptables specifiques au padding seront traitees dans un - second temps +- Validation comptable: + - au `Validate`, le move principal de facture inclut deja le padding car il + est integre a `account.invoice.line.quantity` + - la provisoire doit utiliser le couple de comptes configure + `Default Sale Padding` / `Default Accrual Padding` + - la finale doit passer l'ecriture inverse pour exactement le montant padding + comptabilise lors de la provisoire + - voir `modules/purchase_trade/docs/padding-invoice-accounting.md` - Priorite: - `importante` diff --git a/modules/purchase_trade/docs/padding-invoice-accounting.md b/modules/purchase_trade/docs/padding-invoice-accounting.md new file mode 100644 index 0000000..b7704d0 --- /dev/null +++ b/modules/purchase_trade/docs/padding-invoice-accounting.md @@ -0,0 +1,294 @@ +# Padding facture provisoire vente - notes comptables + +Statut: `draft` +Derniere mise a jour: `2026-04-26` +Scope: `lot.invoice` -> `account.invoice` -> validation comptable. + +## 1) Objectif metier + +Le padding sert a augmenter temporairement la quantite facturee sur une facture +provisoire cote vente, afin de constituer une provision avant la facture finale. + +Exemple: + +- 2 lots factures ensemble +- padding global saisi dans le wizard: `1000` +- repartition actuelle: `500` par lot +- chaque ligne facture affiche: + - `quantity = quantite reelle du lot + padding du lot` + - `Inc. padding = padding du lot` + +Le padding ne modifie pas la quantite physique du lot. Il doit rester visible et +traçable comme ecart entre la facture provisoire et le lot. + +## 2) Donnees creees ou utilisees + +### `lot.lot` + +- Champ: `sale_invoice_padding` +- Role: memoire de la part de padding appliquee au lot pour la facture + provisoire vente. +- UI: readonly sur le lot. + +### `account.invoice.line` + +- Champ fonctionnel: `included_padding` +- Label UI: `Inc. padding` +- Role: affichage de `line.lot.sale_invoice_padding` sur la ligne facture. +- La valeur n'est pas stockee sur la ligne; elle lit le padding du lot. + +### Wizard `lot.invoice` + +- Champ: `sale_padding` (`Global padding`) +- Visible seulement pour: + - `type = sale` + - `action = prov` +- Repartition actuelle: + - padding global / nombre de lots selectionnes + +## 3) Flux de creation de facture avec padding + +1. L'utilisateur ouvre le wizard `lot.invoice` depuis les lots physiques. +2. Le wizard prepare deux listes: + - `lot_p` pour achat + - `lot_s` pour vente +3. En mode vente provisoire, l'utilisateur saisit `Global padding`. +4. Au clic `Invoice`, le wizard: + - calcule la part de padding par lot + - ecrit cette part dans `lot.sale_invoice_padding` + - appelle `sale.sale._process_invoice(..., action='prov')` +5. `sale.line.get_invoice_line(lots, action)` cree une ligne facture par lot + physique. +6. Pour chaque ligne positive `out` / `Pro forma`, le module ajoute le padding + du lot a `invoice_line.quantity`. +7. L'amount de la ligne augmente naturellement via: + - `quantity * unit_price` + +## 4) Ce qui se passe actuellement lors de `Validate` d'une facture + +Bouton UI: `Validate` sur `account.invoice`. + +Code principal: + +- `modules/account_invoice/invoice.py` + - `Invoice.validate_invoice` + - `Invoice.get_move` + - `InvoiceLine.get_move_lines` + - `Invoice.do_lot_invoicing` + - `Invoice.cleanMoves` + +### Etapes de `validate_invoice` + +1. Verifie les taxes avec `_check_taxes`. +2. Attribue le numero via `set_number`. +3. Stocke les caches de montants via `_store_cache`. +4. Pour chaque facture: + - appelle `invoice.get_move()` + - affecte le move a `invoice.move` s'il est nouveau + - appelle `invoice.do_lot_invoicing()` +5. Sauvegarde les moves crees. +6. Nettoie certaines lignes de move via `cleanMoves`. +7. Sauvegarde les factures. + +Note importante deja actee dans le projet: + +- `Validate` cree deja le `account.move` et attribue le `number` aussi pour les + factures client (`type = out`). +- `Post` ne doit plus etre le premier moment ou ces donnees apparaissent cote + client. + +### `get_move()` + +`get_move()` construit le move comptable principal de la facture: + +- appelle `update_taxes(exception=True)` +- parcourt `invoice.lines` +- pour chaque ligne, appelle `line.get_move_lines()` +- ajoute les lignes de taxes via `tax.get_move_lines()` +- ajoute la ligne tiers / receivable-payable via `_get_move_line` +- ajoute eventuellement une ligne de change via `_get_exchange_move_line` +- cree un `account.move` avec: + - `journal = invoice.journal` + - `period` trouve depuis la date comptable + - `origin = invoice` + - `lines = move_lines` + +### `account.invoice.line.get_move_lines()` + +Pour une ligne facture `type = line`: + +- cree une ligne `account.move.line` +- rattache `line.lot = self.invoice.lines[0].lot` + - attention: comportement existant, pas forcement la ligne courante +- convertit le montant si devise differente +- pour une facture client (`type = out`): + - montant positif => credit du compte de revenu de la ligne + - montant negatif => debit +- met `line.account = self.account` +- met `line.origin = self` +- calcule les lignes de taxe + +Pour le padding actuel, comme il augmente `invoice_line.quantity`, il est deja +inclus dans le montant de revenu du move principal. + +### `do_lot_invoicing()` + +`do_lot_invoicing()` traite des ajustements lies aux lots: + +- groupe les lignes facture par `i.lot` +- calcule: + - `var_price` + - `var_qt` +- retrouve le mouvement stock: + - fournisseur pour `type = in` + - client pour `type = out` +- declenche des actions stock / shipment si necessaire +- calcule le COG du lot +- prepare des lignes d'ajustement via `_get_move_lines` +- actuellement, le move d'ajustement n'est cree/poste que si: + - `adjust_move_lines` existe + - `self.type == 'in'` + +Consequence importante pour le padding: + +- les ajustements de quantite/prix existants sont principalement effectifs cote + achat (`type = in`) +- une facture client provisoire avec padding augmente deja le move principal de + vente, mais ne genere pas encore d'ecritures specifiques padding separees + +### `_get_move_lines(gl, amount, drop, IsUsd=False, stock_move=None)` + +Helper existant pour creer des paires de lignes de move liees au stock/COG: + +- rattache `lot`, `fee`, `origin` +- gere la conversion devise / second currency +- choisit les comptes stock / stock out / stock in / COG selon le signe, fee et + contexte +- peut creer une variante `drop` + +Ce helper est un candidat possible pour reutilisation partielle, mais il est +aujourd'hui pense pour des ajustements stock/COG, pas pour isoler un padding de +facture provisoire vente. + +## 5) Question comptable a trancher pour le padding + +Le besoin restant est: au `Validate` d'une facture provisoire vente avec padding, +generer des ecritures propres a ce padding. + +Points a definir avant implementation: + +- Le move padding doit-il etre: + - integre dans le move principal de facture ? + - cree comme `additional_move` ? + - cree comme move separe avec `origin = invoice` ? +- Quels comptes utiliser ? + - compte de revenu temporaire / provision ? + - compte de contrepartie padding ? + - compte lie au produit ? + - compte de configuration dedie ? +- Faut-il neutraliser une partie du revenu principal deja augmente par la + quantity, ou seulement ajouter une ecriture analytique/stock parallele ? +- Le padding doit-il impacter: + - revenu client ? + - stock out ? + - COG ? + - PnL / valuation ? +- Que doit faire la facture finale ? + - reprendre le padding provisoire ? + - le contrepasser ? + - ne rien faire si la finale facture la vraie quantite ? +- Le montant padding a utiliser est: + - `lot.sale_invoice_padding * invoice_line.unit_price` + - dans la devise de facture + - a convertir si devise societe differente selon la logique standard + +## 5.1) Decision comptable cible + +Comptes de configuration ajoutes sur `account.configuration`: + +- `default_sale_padding_account` + - label UI: `Default Sale Padding` + - compte attendu metier: `80021` + - nature: revenu +- `default_accrual_padding_account` + - label UI: `Default Accrual Padding` + - compte attendu metier: `42021` + - nature: bilan / accrual + +Facture provisoire vente: + +- condition: + - `invoice.type = out` + - `invoice.reference = Provisional` + - `invoice_line.description = Pro forma` + - `invoice_line.lot.sale_invoice_padding > 0` +- montant: + - `padding_amount = lot.sale_invoice_padding * invoice_line.unit_price` + - le prix est celui de la facture provisoire, pas un prix recalcule plus tard +- ecriture attendue: + - couple `80021 / 42021` + - rattachement au lot obligatoire sur les lignes si possible + +Facture finale vente: + +- condition: + - lot deja facture en provisoire avec padding + - la finale doit inverser le padding deja provisionne +- montant: + - exactement le montant comptabilise lors de la provisoire + - ne pas recalculer avec le prix de la finale +- ecriture attendue: + - couple inverse `42021 / 80021` + +Decision technique retenue: + +- ne pas ajouter de nouveau champ montant pour l'instant +- utiliser le lien deja ecrit sur le lot: + - `lot.sale_invoice_line_prov` +- au moment de la finale, retrouver la ligne provisoire via ce champ +- calculer le montant a extourner avec: + - `lot.sale_invoice_padding * lot.sale_invoice_line_prov.unit_price` +- utiliser la devise, le taux et la date de la facture provisoire pour obtenir + le meme montant societe que l'ecriture initiale +- garder le rattachement comptable par `account.move.line.lot` + +Raison: + +- evite les ecarts si le prix final change +- reutilise le lien direct deja maintenu entre le lot et la ligne provisoire +- rend la reprise finale audit-friendly +- garde le pivot comptable lot coherent + +## 6) Invariants a preserver + +- Le padding ne change pas la quantite physique du lot. +- Le padding est uniquement pour facture provisoire vente: + - `invoice.type = out` + - `invoice.reference = Provisional` + - lignes `description = Pro forma` +- L'utilisateur doit voir clairement l'ecart: + - `Quantity` + - `Inc. padding` +- Le lien principal reste: + - `account.invoice.line.lot` + - `lot.sale_invoice_padding` +- Les futures ecritures doivent etre rattachees au lot autant que possible pour + rester visibles dans le pivot comptable du lot. + +## 7) Tests a prevoir pour la suite + +- Validation d'une facture provisoire vente avec padding: + - move principal existe + - montant facture inclut le padding + - ecritures specifiques padding creees selon la regle retenue + - lignes rattachees au bon lot +- Facture provisoire vente sans padding: + - aucun move padding specifique +- Facture finale vente: + - aucun nouveau padding applique automatiquement + - comportement de reprise/contrepassation selon decision metier +- Facture achat: + - aucun impact padding vente +- Multi-lots: + - padding reparti correctement + - ecritures detaillees par lot si la regle comptable l'exige diff --git a/modules/purchase_trade/invoice.py b/modules/purchase_trade/invoice.py index e35306a..ff05741 100644 --- a/modules/purchase_trade/invoice.py +++ b/modules/purchase_trade/invoice.py @@ -15,6 +15,180 @@ from trytond.modules.purchase.purchase import ( class Invoice(metaclass=PoolMeta): __name__ = 'account.invoice' + def do_lot_invoicing(self): + super().do_lot_invoicing() + self._create_sale_padding_moves() + + @classmethod + def _post(cls, invoices): + pool = Pool() + Move = pool.get('account.move') + + super()._post(invoices) + + padding_moves = [] + for invoice in invoices: + padding_moves.extend(invoice._create_sale_padding_moves()) + padding_moves.extend([ + move for move in (invoice.additional_moves or []) + if move.description in { + invoice._get_sale_padding_move_description(False), + invoice._get_sale_padding_move_description(True), + } + and move.state != 'posted' + ]) + if padding_moves: + cls.save(invoices) + Move.post([m for m in padding_moves if m.state != 'posted']) + + def _get_sale_padding_accounts(self): + Configuration = Pool().get('account.configuration') + config = Configuration(1) + sale_account = config.get_multivalue( + 'default_sale_padding_account', company=self.company.id) + accrual_account = config.get_multivalue( + 'default_accrual_padding_account', company=self.company.id) + if not sale_account or not accrual_account: + raise UserError( + 'Default Sale Padding and Default Accrual Padding ' + 'accounts must be configured.') + return sale_account, accrual_account + + def _has_sale_padding_move(self, reversal=False): + description = self._get_sale_padding_move_description(reversal) + return any( + move.description == description + for move in (self.additional_moves or [])) + + @staticmethod + def _get_sale_padding_move_description(reversal=False): + if reversal: + return 'Sale padding reversal' + return 'Sale padding accrual' + + def _get_padding_company_amount(self, invoice_line, padding): + Currency = Pool().get('currency.currency') + invoice = invoice_line.invoice + amount = Decimal(str(padding or 0)) * Decimal( + str(invoice_line.unit_price or 0)) + amount = invoice.currency.round(amount) + if invoice.currency == invoice.company.currency: + return amount, amount + if invoice.rate: + company_amount = invoice.company.currency.round( + amount / invoice.rate) + else: + with Transaction().set_context(date=invoice.currency_date): + company_amount = Currency.compute( + invoice.currency, amount, invoice.company.currency) + return amount, company_amount + + def _get_sale_padding_entries(self): + if self.type != 'out': + return [] + + entries = [] + for line in self.lines or []: + if getattr(line, 'type', None) != 'line': + continue + lot = getattr(line, 'lot', None) + if not lot: + continue + padding = Decimal(str( + getattr(lot, 'sale_invoice_padding', 0) or 0)) + if padding <= 0: + continue + + if self.reference == 'Provisional' and line.description == 'Pro forma': + entries.append((lot, line, padding, False)) + elif self.reference == 'Final' and line.description == 'Final': + provisional_line = getattr(lot, 'sale_invoice_line_prov', None) + if provisional_line: + entries.append((lot, provisional_line, padding, True)) + return entries + + def _get_sale_padding_move_lines(self, entries): + MoveLine = Pool().get('account.move.line') + sale_account, accrual_account = self._get_sale_padding_accounts() + + move_lines = [] + for lot, invoice_line, padding, reversal in entries: + padding_amount, company_amount = self._get_padding_company_amount( + invoice_line, padding) + if not company_amount: + continue + + sale_line = MoveLine() + accrual_line = MoveLine() + for move_line in (sale_line, accrual_line): + move_line.lot = lot + move_line.origin = invoice_line + if move_line.account and move_line.account.party_required: + move_line.party = self.party + + if not reversal: + sale_line.account = sale_account + sale_line.debit = company_amount + sale_line.credit = Decimal(0) + accrual_line.account = accrual_account + accrual_line.debit = Decimal(0) + accrual_line.credit = company_amount + else: + accrual_line.account = accrual_account + accrual_line.debit = company_amount + accrual_line.credit = Decimal(0) + sale_line.account = sale_account + sale_line.debit = Decimal(0) + sale_line.credit = company_amount + + if self.currency != self.company.currency: + sale_line.second_currency = self.currency + accrual_line.second_currency = self.currency + sale_line.amount_second_currency = padding_amount.copy_sign( + sale_line.debit - sale_line.credit) + accrual_line.amount_second_currency = padding_amount.copy_sign( + accrual_line.debit - accrual_line.credit) + + if sale_line.account.party_required: + sale_line.party = self.party + if accrual_line.account.party_required: + accrual_line.party = self.party + move_lines.extend([sale_line, accrual_line]) + return move_lines + + def _create_sale_padding_moves(self): + pool = Pool() + Move = pool.get('account.move') + Period = pool.get('account.period') + Date = pool.get('ir.date') + + entries = self._get_sale_padding_entries() + if not entries: + return [] + reversal = entries[0][3] + if self._has_sale_padding_move(reversal): + return [] + + move_lines = self._get_sale_padding_move_lines(entries) + if not move_lines: + return [] + + with Transaction().set_context(company=self.company.id): + today = Date.today() + accounting_date = self.accounting_date or self.invoice_date or today + period = Period.find(self.company, date=accounting_date) + move = Move() + move.journal = self.journal + move.period = period + move.date = accounting_date + move.origin = self + move.company = self.company + move.description = self._get_sale_padding_move_description(reversal) + move.lines = move_lines + Move.save([move]) + self.additional_moves = tuple(self.additional_moves or ()) + (move,) + return [move] + @staticmethod def _format_report_number(value, digits='0.0000', keep_trailing_decimal=False, strip_trailing_zeros=True): diff --git a/modules/purchase_trade/stock.py b/modules/purchase_trade/stock.py index 8fd1543..1c8b285 100755 --- a/modules/purchase_trade/stock.py +++ b/modules/purchase_trade/stock.py @@ -106,7 +106,7 @@ class InvoiceLine(metaclass=PoolMeta): lot = fields.Many2One('lot.lot',"Lot") fee = fields.Many2One('fee.fee',"Fee") included_padding = fields.Function( - fields.Numeric("Included padding", digits=(1, 5)), + fields.Numeric("Inc. padding", digits=(1, 5)), 'get_included_padding') def get_included_padding(self, name): diff --git a/modules/purchase_trade/view/account_configuration_form.xml b/modules/purchase_trade/view/account_configuration_form.xml new file mode 100644 index 0000000..8454452 --- /dev/null +++ b/modules/purchase_trade/view/account_configuration_form.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/modules/purchase_trade/view/invoice_line_tree_sequence.xml b/modules/purchase_trade/view/invoice_line_tree_sequence.xml index 729a5aa..9c6383e 100644 --- a/modules/purchase_trade/view/invoice_line_tree_sequence.xml +++ b/modules/purchase_trade/view/invoice_line_tree_sequence.xml @@ -1,6 +1,6 @@ - + diff --git a/notes/template_business_rules.md b/notes/template_business_rules.md index 56d6877..5c978a3 100644 --- a/notes/template_business_rules.md +++ b/notes/template_business_rules.md @@ -94,3 +94,19 @@ Scope: templates Relatorio + ponts `report_*` Python. - `operator` filtre sur `OPERATOR`. - les quotas/pricings doivent fallback sur `quantity` si `quantity_theorical` est vide. - `sale.line` / `purchase.line`: en mode `basis`, si aucun `price_component` n'est defini, le prix et la progression doivent remonter depuis la ligne `Summary` / `pricing.summary` sans component. + +## 6) Session 2026-04-26 - Padding facture provisoire vente + +- Le wizard `lot.invoice` peut appliquer un padding global uniquement sur les + factures provisoires cote vente. +- Le padding global est reparti par lot et conserve sur `lot.lot` dans + `sale_invoice_padding`. +- La ligne facture affiche ce montant via `Inc. padding`, avec la meme unite que + `Quantity`. +- La quantite facturee est augmentee du padding, mais la quantite physique du + lot ne doit pas changer. +- Au `Validate` d'une facture, le move principal inclut deja le padding car il + est integre dans `account.invoice.line.quantity`. +- Les ecritures comptables specifiques au padding restent a definir; point + d'entree documente: + `modules/purchase_trade/docs/padding-invoice-accounting.md`.