# Lots et quantités Langue : `fr` Page miroir : [lots-and-quantities.en.md](lots-and-quantities.en.md) Statut : `migration partielle` Dernière vérification code : `2026-05-13` Cette page consolide `BR-PT-LOT-001`, `BR-PT-LOT-002` et `BR-PT-LOT-003`. Objectif : piloter les règles de quantité depuis une définition métier lisible, puis les sécuriser par des checks Python et des diagnostics SQL. ## À retenir > **Résumé opérationnel** > Une ligne trade possède un seul lot virtuel. Le lot virtuel porte le solde > ouvert global, `lot.qt` porte le forecast opérationnel, et les lots > physiques consomment ce forecast. > | Sujet | Règle courte | | --- | --- | | Quantité saisie | `quantity_theorical` est la quantité métier. | | Quantité standard | `quantity` est un compteur technique non éditable. | | Avant physique | `quantity` suit `quantity_theorical`. | | Après physique | `quantity` reflète l'exécuté physique. | | Ligne non finie | Le montant de ligne utilise `quantity_theorical`. | | Ligne finie | Le montant peut revenir à l'exécuté physique. | | Weight basis | Achat et vente peuvent lire deux états différents du même lot. | | Facturation | Elle choisit ses états dans `lot.qt.hist`. | | Contrôles | Invariants bloqués en Python et auditables en SQL. | ```text quantity_theorical | v lot virtuel P1 ---> lot.qt forecast ---> lot physique ^ | | v +------ recalcul après consommation ``` ## Règles consultant ### BR-PT-LOT-001 - Cycle de vie lot virtuel / forecast / physique | Moment | Effet métier | | --- | --- | | Création de ligne | Création d'un lot virtuel unique. | | Initialisation | Le lot virtuel reprend `quantity_theorical`. | | Forecast | Une ligne ouverte est créée dans `lot.qt`. | | Planification | `lot.qt` peut être subdivisé par vente, matching, transport ou shipment. | | Ajout physique | Le lot physique consomme une ligne `lot.qt` précise. | | Après ajout | `lot.qt` diminue et le lot virtuel est recalculé. | > **Découpage d'un solde P1** > `P1S1T1`, `P1S1T2`, `P1S2T3`, `P1S2T4` > > **Point clé** > Ces découpages ne créent pas plusieurs lots virtuels. Ils décrivent > seulement la répartition prévisionnelle du solde ouvert. > ### BR-PT-LOT-002 - Quantité contractuelle, compteur, ligne finie | Situation | Quantité de référence | | --- | --- | | Saisie utilisateur | `quantity_theorical` | | Aucun lot physique | `quantity = quantity_theorical` | | Lots physiques présents | `quantity = somme des lots physiques` | | `finished = False` | Montant basé sur `quantity_theorical` | | `finished = True` | Montant basé sur l'exécuté physique | | Weight basis disponible | Montant basé sur l'état `wb.qt_type` du contrat | > **Ce que `finished` ne fait pas** > `finished` n'efface pas la quantité contractuelle, ne supprime pas les lots > physiques et ne masque pas leur PnL. Il signifie seulement que le reliquat > ouvert peut être ignoré pour les calculs d'exécution. > | Lecture du même lot physique | État possible | | --- | --- | | Achat | BL via `purchase.purchase.wb.qt_type` | | Vente | LR ou Weight Report via `sale.sale.wb.qt_type` | | Facturation | Choix indépendant dans `lot.qt.hist` | ### BR-PT-LOT-003 - Invariants de quantité | Invariant | Formule | | --- | --- | | Conservation | `somme(lots physiques) + lot virtuel = quantity_theorical` | | Forecast ouvert minimal | `somme(lot.qt non zéro) >= max(lot virtuel, 0)` | | Cas | Règle | | --- | --- | | Lot virtuel achat | Sommer tous les `lot.qt` où `lot_p = lot virtuel`, avec ou sans `lot_s`. | | Lot virtuel vente | Sommer tous les `lot.qt` où `lot_s = lot virtuel`, avec ou sans `lot_p`. | | `lot.qt = 0` | Ignoré par les checks ; mémoire possible d'une prévision vidée. | | Surconsommation forecast | Autorisée après confirmation de tolérance : `lot.qt` s'arrête à zéro, mais le lot virtuel absorbe tout le physique. | | `lot.qt` supérieur au virtuel | Accepté si le forecast ouvert restant dépasse le virtuel positif à cause d'une surconsommation physique. | | Lot virtuel négatif | Autorisé pour compenser l'écart théorique / exécuté ; forecast attendu = zéro. | | `lot.qt` non zéro orphelin | Interdit si ni `lot_p` ni `lot_s` n'est renseigné. | ### BR-PT-LOT-004 - Historique de quantité et weighing | Élément | Règle | | --- | --- | | `lot.qt.hist` | Porte les états de quantité d'un lot. | | Fiche `lot.lot` | Pas de saisie directe des états. | | Modification | Uniquement via `Do weighing`. | | Lot virtuel | Pas de packing manuel. | | Champs packing virtuel | `lot_qt` et `lot_unit` non éditables. | ### Tolérances | Point | Règle | | --- | --- | | Niveau | La tolérance est définie au header `purchase` / `sale`. | | Héritage | Les lignes héritent de la tolérance header par défaut. | | Transport | Pas de tolérance indépendante par transport. | | Ajout physique | Le contrôle se fait au moment de `Add physical lots`. | | Forecast matché | Si `lot_qt` porte `lot_p` et `lot_s`, le contrôle porte sur les deux côtés achat et vente. | | Dépassement ligne | Warning confirmable en anglais si la quantité physique projetée dépasse la tolérance courante de la ligne. Le message indique le côté bloquant : `purchase`, `sale`, ou les deux si les deux checks déclenchent successivement. | | Enveloppe globale | Le dépassement ponctuel d'une ligne est accepté après confirmation et consomme l'enveloppe globale du contrat. | | Tolérance restante | Les lignes du contrat sont recalculées avec `inherit_tol = False`. | | Surconsommation | Une ligne qui dépasse la tolérance header garde une tolérance `+` au moins égale à son dépassement réel. | | Autres lignes | Leur tolérance `+` est réduite selon l'enveloppe restante. | | Sous-consommation | Restitue mécaniquement de la tolérance disponible aux autres lignes lors du recalcul suivant. | | Matching ouvert | `Go to matching` peut matcher au-delà du solde ouvert strict si la quantité projetée reste dans `Qt max`. | | Jauge ligne | Sur une ligne avec lots physiques, la jauge utilise la somme des physiques ; sans physique, elle utilise la somme des `lot.qt` liés. | | Jauge header | La jauge purchase/sale est la moyenne pondérée des jauges de lignes au prorata de `quantity_theorical`. | | Jauge matching | Dans `Go to matching`, la jauge projette `déjà matché + Qt to match` contre `quantity_theorical`, avec bornes `-tol_min` / `tol_max`. | | Layout jauge ligne | En formulaire `purchase.line` / `sale.line`, placer la jauge et `targeted_qt` directement dans la grille principale, sans sous-groupe `colspan="4"`, et utiliser `xalign="0"` pour éviter un double décalage vers la droite. | ## Section développeur ### Champs clés - Ligne achat : `purchase.line` - Ligne vente : `sale.line` - Lot : `lot.lot` - Forecast : `lot.qt` - Historique : `lot.qt.hist` - Quantité métier achat : `purchase.line.quantity_theorical` - Quantité métier vente : `sale.line.quantity_theorical` - Compteur technique : `quantity` - Ligne finie : `purchase.line.finished`, `sale.line.finished` - Lot virtuel / physique : `lot.lot.lot_type = virtual / physic` - Lien achat : `lot.lot.line` - Lien vente : `lot.lot.sale_line` - Forecast achat : `lot.qt.lot_p` - Forecast vente : `lot.qt.lot_s` - Quantité forecast : `lot.qt.lot_quantity` - Weight basis : `purchase.purchase.wb`, `sale.sale.wb` - État Weight basis : `purchase.weight.basis.qt_type` - Packing : `lot.lot.lot_qt`, `lot.lot.lot_unit` - Tolerances : `tol_min`, `tol_max`, `tol_min_qt`, `tol_max_qt`, `tol_min_v`, `tol_max_v`, `tolerance_used`, `tolerance_min`, `tolerance_max` ### Création ligne / lot virtuel - Achat : `purchase.py`, `Line.validate` - Vente : `sale.py`, `SaleLine.validate` - Si `quantity_theorical` est saisi et que `quantity` est vide ou zéro : - `quantity` est initialisée depuis `quantity_theorical` ; - seulement si aucun lot physique n'existe. - Si la ligne est éligible : - pas `created_by_code` ; - pas encore de lot ; - produit non service ; - `quantity_theorical != 0` ; - création d'un lot `virtual`. - Le lot virtuel reçoit une première entrée `lot.qt.hist`. - `Lot.validate` crée le `lot.qt` ouvert via `createVirtualPart`. - Si la ligne est `created_by_code` : - elle provient d'un workflow métier (`Create contracts`, matching, etc.) ; - `Lot.validate` ne crée pas de `lot.qt` ouvert automatique ; - le workflow rattache ou subdivise son `lot.qt` source ; - le check bloquant s'applique seulement après stabilisation du workflow. ### Modification de `quantity_theorical` - Achat : `purchase.py`, `Line.write` - Vente : `sale.py`, `SaleLine.write` - Cible lot virtuel : ```text target_quantity = quantity_theorical - somme(lots physiques convertis) ``` - Si `target_quantity < 0` : - blocage : `Please unlink or unmatch lot`. - Cible `lot.qt` libre : ```text free_quantity = target_quantity - somme(lot.qt déjà matchés ou shippés) ``` - Si `free_quantity < 0` : - blocage : `Please unlink or unmatch lot`. - Si un `lot.qt` libre existe : - sa quantité est remplacée. - Si aucun `lot.qt` libre n'existe et `free_quantity > 0` : - création d'un nouveau `lot.qt`. - Les fees de ligne sont resynchronisés. ### Ajout de lots physiques - Wizard : `lot.add` - Méthodes : - `LotQt.add_physical_lots` - `LotQt.add_physical_lot` - Source obligatoire : une ligne `lot.qt`. - Ajout direct depuis un lot physique refusé. - Ajout physique côté vente par ce wizard refusé : utiliser `Apply matching`. - Le lot physique reprend : - ligne achat ; - vente matchée si présente ; - shipment ; - produit ; - unité ; - quantités ; - premium ; - chunk key. - Après création : - réduction de la ligne `lot.qt` source ; - pas de quantité `lot.qt` négative ; - recalcul du lot virtuel ; - recalcul de `quantity` ; - mise à jour moves et fees si nécessaire. ### Retrait de lots physiques - Wizard : `lot.remove` - Lot ouvert : retrait interdit. - Lot avec `stock.move` : - move obligatoire en `draft`. - Lot matché ou shippé : - warning confirmable. - Effets : - suppression du move draft ; - restauration de la quantité dans `lot.qt` ; - contexte restauré via shipment, `getVlot_p()`, `getVlot_s()` ; - recalcul lot virtuel, `quantity`, fees. ### Weighing / états de quantité - Wizard : `lot.weighing` - Action UI : `Do weighing` - Écrit ou met à jour `lot.qt.hist`. - Peut mettre à jour `lot_state`. - Synchronise : - lot ; - quantités ouvertes ; - fees. - Les vues `lot.qt.hist` sont consultatives. ### Quantité compteur `quantity` - Méthode : `Lot._recalc_line_quantity` - Sans physique : - `quantity` suit le lot virtuel. - Avec physiques : - `quantity` somme uniquement les lots physiques. - `quantity` est readonly côté ligne trade. ### Montant de ligne - Achat : `purchase.line.on_change_with_amount()` - Vente : `sale.line.on_change_with_amount()` - Helper : - `_get_amount_quantity()` - `_get_weight_basis_quantity()` - Priorités : - `finished = False` : `quantity_theorical` - `finished = True` + Weight basis disponible : somme physique dans cet état - `finished = True` sans Weight basis exploitable : `quantity` - fallback legacy : `quantity` si `quantity_theorical` vide ### Garde-fous Python - Check central : - `lot.lot.assert_lines_quantity_consistency()` - Règle de déclenchement : - bloquer les états finaux incohérents ; - ne pas bloquer les états transitoires internes d'un workflow ; - utiliser `Lot.skip_quantity_consistency()` uniquement autour d'une séquence qui rétablit ensuite les invariants ; - appeler un check final explicite après la séquence. - Blocage `lot.qt` orphelin non zéro : - `lot.qt.validate` - Appels après : - modification `quantity_theorical` ; - création / suppression de lots physiques ; - matching / unmatching ; - shipping / unshipping ; - `Create contracts` en mode matched ; - weighing. - Le cas `created_by_code` est volontairement exclu du check immédiat `Lot.validate` : - le lot virtuel est sauvegardé avant que le `lot.qt` matched soit rattaché ; - l'invariant est contrôlé par le check final du workflow. ### Diagnostic SQL - Script : - [sql/quantity_consistency_checks.sql](sql/quantity_consistency_checks.sql) - Usage : - audit des bases de test ; - audit des données historiques ; - qualification avant correction. - Le script ignore totalement les `lot.qt = 0`. ## Tests proches - `modules/purchase_trade/tests/test_module.py` - Couverture existante : - `quantity` readonly ; - initialisation depuis `quantity_theorical` ; - protection si lots physiques ; - amount sur théorique / physique / Weight basis ; - resynchronisation des lots virtuels ; - blocages quand l'open ne suffit plus. - Tests à ajouter : - `lot_hist` readonly ; - `Do weighing` crée ou met à jour un état ; - lot virtuel sans saisie directe `lot_qt` / `lot_unit` ; - contrôles SQL rejoués sur jeux de données incohérents.