18 KiB
Lots et quantités
Langue: fr
Page miroir: lots-and-quantities.en.md
Statut: migration partielle
Dernière vérification code: 2026-05-13
Cette page consolide les anciennes règles BR-PT-LOT-001,
BR-PT-LOT-002 et BR-PT-LOT-003 autour d'une règle fonctionnelle unique :
le cycle de vie des quantités ouvertes, forecastées et physiques.
Règle business consultant
BR-PT-LOT-001 - Cycle de vie des lots et des quantités
Lors de la création d'une ligne d'achat ou de vente, le système crée un lot virtuel associé à cette ligne. Ce lot virtuel représente la quantité encore ouverte du contrat. Sa quantité initiale reprend la quantité contractuelle ou théorique de la ligne.
En parallèle, le système ajoute une ligne ouverte dans lot.qt. Cette ligne
sert de base aux prévisions commerciales et logistiques : vente prévisionnelle,
matching futur, transport planifié, shipment, etc. lot.qt porte donc le
forecast opérationnel, tandis que le lot virtuel reste la représentation
globale du solde ouvert dans lot.lot.
Exemple : une quantité ouverte P1 peut être progressivement subdivisée dans
lot.qt pour prévoir plusieurs ventes ou transports :
P1S1T1P1S1T2P1S2T3P1S2T4
Ces subdivisions ne créent pas plusieurs lots virtuels pour P1. Elles
décrivent seulement la répartition prévisionnelle de la quantité ouverte.
Quand des lots physiques sont ajoutés, ils sont créés depuis une ligne précise
de Lots Management, donc depuis une ligne lot.qt. Si l'utilisateur choisit
P1S1T1, le lot physique créé consomme ce forecast particulier. La ligne
lot.qt choisie est réduite, puis le lot virtuel de la ligne est recalculé
pour représenter seulement le reliquat encore ouvert.
La première règle immuable de cohérence est :
somme des lots physiques de la ligne + lot virtuel de la ligne
= quantité contractuelle/théorique de la ligne
La deuxième règle immuable de cohérence est :
somme des lot.qt.lot_quantity pour le lot_p virtuel de la ligne
= lot.qt.hist.quantity du lot virtuel de la ligne
Chaque ajout de lot physique doit retrancher la même quantité des deux côtés
de cette égalité : la ligne lot.qt forecastée diminue, et l'historique de
quantité du lot virtuel diminue aussi.
Cette cohérence doit rester vraie même si la quantité contractuelle est ajustée en cours de route. La quantité contractuelle/théorique est la seule quantité saisie par l'utilisateur sur la ligne. La quantité standard de la ligne est un compteur technique en lecture seule : elle est initialisée depuis la quantité contractuelle tant qu'il n'y a pas de lot physique, puis elle reflète la somme des lots physiques exécutés.
Le retrait d'un lot physique est autorisé seulement tant que son mouvement
stock n'est pas finalisé. Si ce lot était déjà matché ou rattaché à un
shipment, l'utilisateur doit confirmer l'action. Le retrait restaure la
quantité dans la ligne lot.qt qui portait le contexte forecast, matching ou
transport ayant permis de créer le lot physique.
L'historique des quantités d'un lot n'est jamais une zone de saisie directe.
Les différents états de quantité d'un lot doivent être modifiés uniquement par
une action métier dédiée, aujourd'hui Do weighing. Cette règle garantit que
les recalculs de quantité ouverte, de lot virtuel, de fees et de mouvements
stock restent synchronisés.
BR-PT-LOT-002 - Quantité contractuelle, exécuté physique et ligne finie
La quantité contractuelle/théorique reste la base du contrat tant que la ligne
n'est pas marquée comme finie. Elle sert donc de base au montant commercial de
la ligne, même si des lots physiques ont déjà été ajoutés et que la quantité
technique quantity reflète seulement la somme de ces lots physiques.
Le champ quantity n'est pas une saisie métier. Il est un compteur technique :
- avant tout lot physique, il est initialisé depuis la quantité contractuelle/théorique ;
- dès qu'il existe des lots physiques, il reflète la somme des lots physiques exécutés ;
- il ne doit pas piloter le montant contractuel tant que la ligne n'est pas marquée comme finie.
La coche Mark as finished indique que l'utilisateur accepte d'ignorer le
reliquat ouvert encore porté par le lot virtuel. Elle ne modifie pas la
quantité contractuelle historique. Elle ne supprime pas les lots physiques. Elle
signifie seulement que les calculs qui ne doivent plus tenir compte du reliquat
ouvert peuvent basculer sur l'exécuté physique.
Règles strictes :
- tant que
finished = False, le montant commercial de la ligne doit être calculé surquantity_theorical; - quand
finished = True, le reliquat ouvert/virtuel restant est ignoré pour les calculs d'exécution et le montant de ligne peut donc revenir au compteur physiquequantity; - si la ligne finie possède des lots physiques, le montant de ligne doit utiliser l'état de quantité défini par le Weight basis du contrat concerné, quand cet état existe sur les lots ;
finishedne doit jamais effacer la trace du contrat initial ;finishedne doit jamais masquer ou supprimer le PnL des lots physiques.
Cette règle concerne le montant porté par la ligne de contrat. La facturation
reste un flux distinct : elle peut choisir un état de quantité précis dans
lot_qt_hist selon le contrat, par exemple un poids BL à l'achat et un Weight
Report à la vente. Le montant de ligne ne doit donc pas essayer de remplacer la
logique de sélection des quantités de facturation.
Le même lot physique peut donc avoir deux lectures commerciales différentes : sur le contrat d'achat, il peut être valorisé avec l'état BL ; sur le contrat de vente, une fois matché, il peut être valorisé avec un autre état, par exemple LR pour Landing Report. L'état utilisé dépend toujours du contrat affiché, pas d'un état global unique du lot.
Tolérance et quantités physiques
Un lot physique peut avoir une quantité différente de la prévision initiale, dans les limites de tolérance du contrat. La tolérance doit se lire comme une tolérance globale sur la ligne ou le contrat, pas comme une tolérance indépendante par transport.
Si un lot physique consomme plus ou moins que son forecast, cela doit réduire ou augmenter dynamiquement la tolérance restante pour les prochains lots physiques de la même ligne. Aucune action d'ajout, de weighing, de suppression ou d'ajustement contractuel ne doit permettre de sortir de la cohérence globale des quantités.
Section développeur
Modèles et champs principaux
- Ligne achat :
purchase.line. - Ligne vente :
sale.line. - Lot :
lot.lot. - Forecast / quantité ouverte :
lot.qt. - Historique des quantités de lot :
lot.qt.hist. - Quantité contractuelle achat :
purchase.line.quantity_theorical(Contractual Qt). - Quantité contractuelle vente :
sale.line.quantity_theorical(Th. quantity). - Quantité compteur achat/vente :
quantity, non éditable par l'utilisateur dans les lignes trade. - Type de lot :
lot.lot.lot_type, valeursvirtualetphysic. - Lien achat :
lot.lot.line. - Lien vente :
lot.lot.sale_line. - Liens shipment :
lot_shipment_in,lot_shipment_internal,lot_shipment_out. - Matching forecast achat/vente :
lot.qt.lot_p,lot.qt.lot_s. - Quantité forecast :
lot.qt.lot_quantity. - Tolérances contrat/ligne :
tol_min,tol_max,tol_min_qt,tol_max_qt,tol_min_v,tol_max_v. - Ligne finie :
purchase.line.finished,sale.line.finished. - Weight basis achat/vente :
purchase.purchase.wb,sale.sale.wb. - État de quantité final associé au Weight basis :
purchase.weight.basis.qt_type. - Historique des états de quantité :
lot.lot.lot_histverslot.qt.hist. - Champs d'historique :
lot.qt.hist.quantity_type,lot.qt.hist.quantity,lot.qt.hist.gross_quantity. - Champs de packing du lot :
lot.lot.lot_qt,lot.lot.lot_unit.
Création de la ligne et du lot virtuel
- Achat :
modules/purchase_trade/purchase.py,Line.validate. - Vente :
modules/purchase_trade/sale.py,SaleLine.validate. - À la création et lors de la saisie de
quantity_theorical, siquantityest vide ou égale à zéro et qu'aucun lot physique n'existe,quantityest initialisée depuisquantity_theorical. - Si la ligne n'est pas
created_by_code, qu'elle n'a pas encore de lot, que le produit n'est pas un service et quequantity_theorical != 0, un lotvirtualest créé. - Le lot virtuel reçoit une première entrée
lot.qt.hist. - La création du
lot.qtouvert est déclenchée parmodules/purchase_trade/lot.py,Lot.validate, viacreateVirtualPartquand aucunlot.qtn'existe encore pour le lot virtuel. - Pour l'achat, le lot virtuel alimente
lot.qt.lot_p. - Pour la vente, le lot virtuel alimente
lot.qt.lot_saveclot_p = None. - Les anciens flux qui n'ont pas encore
quantity_theoricalpeuvent encore utiliserquantitycomme fallback legacy, mais ce n'est plus le champ de saisie métier.
Modification de la quantité contractuelle
État du code vérifié :
- Achat :
purchase.py,Line.write. - Vente :
sale.py,SaleLine.write. - La modification de
quantity_theoricalrecalcule une quantité virtuelle cible :
target_quantity = quantity_theorical - somme(lots physiques convertis)
- Si cette quantité cible devient négative, le code bloque avec
Please unlink or unmatch lot. - Le
lot.qtlibre est ensuite resynchronisé en tenant compte des ligneslot.qtdéjà allouées, c'est-à-dire déjà matchées ou rattachées à un shipment :
free_quantity = target_quantity - somme(lot.qt déjà matchés ou shippés)
- Si
free_quantitydevient négative, le code bloque avecPlease unlink or unmatch lot. - Si le lot virtuel ne porte pas déjà la quantité cible, le code appelle
vlot.set_current_quantity(target_quantity, target_quantity, 1). - Si un
lot.qtlibre existe, sa quantité est remplacée parfree_quantity. - Si aucun
lot.qtlibre n'existe et quefree_quantity > 0, un nouveaulot.qtlibre est créé. - Les fees de la ligne sont resynchronisés après modification.
Invariants à préserver après chaque modification :
sum(physical lots) + virtual lot current quantity = quantity_theorical;sum(lot.qt.lot_quantity where lot_p = virtual lot) = virtual lot current quantity.- Pour un lot virtuel achat, le second invariant somme toutes les lignes
lot.qtoùlot_p = virtual lot, qu'elles soient matchées à unlot_sou non. - Pour un lot virtuel vente, le second invariant somme toutes les lignes
lot.qtoùlot_s = virtual lot, qu'elles soient liées à unlot_pou non. - Le garde-fou technique est centralisé dans
lot.lot.assert_lines_quantity_consistency()et doit être appelé à la fin des flux qui modifient des lots, du matching, du transport, du weighing ou la quantité contractuelle.
Point spécifique achat :
- Si
quantity_theoricalétait vide au moment de l'initialisation, le code prend comme baseline la quantité courante du lot virtuel pour éviter de doubler lelot.qtouvert.
Point spécifique vente :
- Si l'ancienne
quantity_theoricalest vide,SaleLine.writene lance pas encore cette resynchronisation.
Ajout de lots physiques
- Wizard :
modules/purchase_trade/lot.py,lot.add. - Méthode principale :
LotQt.add_physical_lots. - Création unitaire :
LotQt.add_physical_lot. - L'ajout part obligatoirement d'une ligne
lot.qtissue de Lots Management. - Le code refuse l'ajout direct depuis un lot physique.
- Le code refuse l'ajout physique côté vente par ce wizard et demande
d'utiliser
Apply matching. - Le nouveau lot physique reprend le contexte de la ligne
lot.qt: ligne achat, vente matchée si présente, shipment, produit, unité, quantités, premium et chunk key. - Après création, le code réduit la quantité de la ligne
lot.qtsource du total physique créé et ne laisse pas la ligne descendre sous zéro. - La sauvegarde/validation du lot physique recalcule :
- le lot virtuel de la ligne via
_recompute_virtual_lot; - la quantité compteur de la ligne via
_recalc_line_quantity; - les mouvements stock liés si nécessaire ;
- les fees rattachés au lot, à la ligne ou au shipment.
- le lot virtuel de la ligne via
Retrait de lots physiques
- Wizard :
modules/purchase_trade/lot.py,lot.remove. - Le retrait d'un lot ouvert est interdit.
- Si le lot physique possède un
stock.move, ce move doit être en étatdraft. - Si le lot est matché ou shippé, un warning confirmable est affiché.
- Le code supprime d'abord le move draft si présent.
- Le code restaure la quantité physique dans
lot.qten réutilisant le contexte :- shipment d'origine via
lot.lot_shipment_origin; - lot virtuel sale via
getVlot_s()si le lot était matché ; - lot virtuel purchase via
getVlot_p()dansupdateVirtualPart.
- shipment d'origine via
- Si une ligne
lot.qtcompatible existe déjà, elle est incrémentée. - Sinon une nouvelle ligne
lot.qtest créée. - La suppression du lot physique déclenche ensuite le recalcul du lot virtuel, de la quantité compteur de ligne et des fees.
Historique de quantité et weighing
- L'historique est porté par
lot.qt.hist. lot.qt.histreprésente les différents états de quantité d'un lot.- Les quantités historiques ne doivent pas être modifiées directement depuis
la fiche
lot.lot. - La vue
lot.lotaffichelot_histen lecture seule. - Les vues
lot.qt.histsont consultatives: pas d'édition directe depuis l'arbre ou le formulaire. - Le chemin fonctionnel de modification est le wizard
lot.weighing, exposé par l'actionDo weighing. - Le wizard écrit ou met à jour l'entrée
lot.qt.histcorrespondant aulot_statechoisi, puis synchronise le lot, les quantités ouvertes et les fees. - Un lot virtuel ne doit pas recevoir de saisie de packing depuis la fiche
lot: les champs
lot_qtetlot_unitsont non éditables pourlot_type = virtual.
Quantité compteur de ligne
- Méthode :
lot.py,Lot._recalc_line_quantity. - Si la ligne n'a qu'un seul lot,
quantityreprend la quantité courante de ce lot. - Si la ligne a plusieurs lots,
quantitysomme uniquement les lots physiques. - Cette logique correspond à la règle fonctionnelle :
- sans physique :
quantitysuit le virtuel ; - avec physiques :
quantityreflète l'exécuté physique.
- sans physique :
Montant de ligne
- Achat :
purchase.line.on_change_with_amount(). - Vente :
sale.line.on_change_with_amount(). - Tant que
finished = False, la quantité de base du montant estquantity_theorical. - Quand
finished = True, la quantité de base du montant peut revenir à l'exécuté physique, car l'utilisateur a accepté d'ignorer le reliquat virtuel ouvert. - Si la ligne finie possède des lots physiques et que le contrat porte un
Weight basis avec
qt_type, le montant somme les quantités physiques dans cet étatlot.qt.hist, converties dans l'unité de la ligne. - L'achat résout cet état via
purchase.purchase.wb.qt_type. - La vente résout cet état via
sale.sale.wb.qt_type. - Le même lot physique matché peut donc contribuer à un montant achat en état BL et à un montant vente en état LR.
- Si
quantity_theoricalest vide dans un ancien flux, le code conserve un fallback surquantity. - Si
quantityest vide sur une ligne finie, le code conserve un fallback surquantity_theorical. - Si l'état du Weight basis n'existe pas encore sur un lot, le code conserve le fallback précédent au lieu de considérer le montant comme final.
- La facturation reste indépendante de cette règle de montant de ligne :
les lignes de facture peuvent utiliser un état de quantité choisi dans
lot.qt.histselon la règle contractuelle de facturation.
Tolérance : état actuel et point à confirmer
Le code expose déjà les tolérances sur contrats et lignes :
- Achat :
purchase.purchase.tol_min,purchase.purchase.tol_max,purchase.line.tol_min,purchase.line.tol_max. - Vente :
sale.sale.tol_min,sale.sale.tol_max,sale.line.tol_min,sale.line.tol_max. - Fonctions d'affichage :
get_tol_min,get_tol_max. - Wizard d'ajout :
lot.add.line.tol_min,lot.add.line.tol_max, avec défauts depuis le contrat source.
À confirmer / gap potentiel :
- Dans
LotQt.add_physical_lotsetLotQt.add_physical_lot, je ne vois pas encore de contrôle complet qui calcule une tolérance restante globale en tenant compte des lots physiques déjà créés. - La règle consultant ci-dessus de tolérance globale dynamique doit donc être
considérée comme cible métier à vérifier/implémenter avant de la marquer
active.
Tests proches
modules/purchase_trade/tests/test_module.pytest_trade_line_quantity_is_readonlytest_purchase_line_initial_quantity_uses_theoretical_quantitytest_sale_line_initial_quantity_uses_theoretical_quantitytest_trade_line_initial_quantity_does_not_override_existing_countertest_trade_line_initial_quantity_does_not_run_with_physical_lottest_purchase_line_amount_uses_theoretical_quantity_until_finishedtest_purchase_line_amount_uses_physical_counter_when_finishedtest_purchase_line_amount_uses_purchase_weight_basis_when_finishedtest_sale_line_amount_uses_theoretical_quantity_until_finishedtest_sale_line_amount_uses_physical_counter_when_finishedtest_sale_line_amount_uses_sale_weight_basis_when_finishedtest_sale_line_write_updates_virtual_lot_when_theorical_qty_increasestest_sale_line_write_blocks_theorical_qty_decrease_when_no_open_quantitytest_purchase_line_write_syncs_open_lot_qt_with_physical_lotstest_purchase_line_write_syncs_virtual_fee_quantitytest_purchase_line_write_initial_theorical_qty_does_not_double_open_lot
À ajouter si cette règle devient sensible côté régression:
- vérifier que
lot_histest en lecture seule sur la fichelot.lot; - vérifier que
Do weighingreste capable de créer ou mettre à jour un état de quantité; - vérifier qu'un lot virtuel ne permet pas la saisie directe de
lot_qtetlot_unit.