324 lines
13 KiB
Markdown
324 lines
13 KiB
Markdown
# 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.
|