# 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.
