# 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 | `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. | | 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 > **Gap à confirmer** > Le contrôle complet de tolérance restante dans `LotQt.add_physical_lots` / > `LotQt.add_physical_lot` reste à confirmer. > | Point | Règle cible | | --- | --- | | Niveau | Tolérance globale sur ligne ou contrat. | | Transport | Pas de tolérance indépendante par transport. | | Surconsommation | Consomme la tolérance restante. | | Sous-consommation | Restitue de la tolérance restante. | ## 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` ### 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`. ### 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()` - 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 ; - weighing. ### 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.