Files
tradon/modules/purchase_trade/docs_source/business/lots-and-quantities.md
2026-05-14 10:55:47 +02:00

299 lines
10 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 | `somme(lot.qt non zéro) = max(lot virtuel, 0)` |
| Cas | Règle |
| --- | --- |
| Lot virtuel achat | Sommer tous les `lot.qt``lot_p = lot virtuel`, avec ou sans `lot_s`. |
| Lot virtuel vente | Sommer tous les `lot.qt``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.