lot quantity logic
This commit is contained in:
@@ -24,6 +24,7 @@ Statut: `migration partielle`
|
||||
- `BR-PT-CON-002`: delivery period coherent.
|
||||
- `BR-PT-CON-003`: lieux stock propages dans Create Contracts.
|
||||
- `BR-PT-LOT-001`: cycle de vie des lots et des quantites.
|
||||
- `BR-PT-LOT-002`: quantity contractuelle, execute physique et ligne finie.
|
||||
- `BR-PT-MAT-001`: Create Contracts multi-lots.
|
||||
- `BR-PT-SHP-001`: affectation controller.
|
||||
- `BR-PT-SHP-002`: couts SLA controller.
|
||||
|
||||
@@ -41,18 +41,30 @@ physical lot created consumes that specific forecast. The selected `lot.qt`
|
||||
line is reduced, then the line's virtual lot is recalculated so that it only
|
||||
represents the remaining open balance.
|
||||
|
||||
The immutable quantity consistency rule is:
|
||||
The first immutable quantity consistency rule is:
|
||||
|
||||
```text
|
||||
sum of the line's physical lots + the line's virtual lot
|
||||
= the line's contractual/theoretical quantity
|
||||
```
|
||||
|
||||
The second immutable quantity consistency rule is:
|
||||
|
||||
```text
|
||||
sum of lot.qt.lot_quantity for the line's virtual lot_p
|
||||
= lot.qt.hist.quantity of the line's virtual lot
|
||||
```
|
||||
|
||||
Each physical lot addition must subtract the same quantity from both sides of
|
||||
this equality: the forecast `lot.qt` line decreases, and the virtual lot
|
||||
quantity history decreases as well.
|
||||
|
||||
This consistency must remain true even if the contractual quantity is adjusted
|
||||
along the way. The contractual/theoretical quantity is the only quantity that
|
||||
the user enters on the line. The standard line quantity is a counter: it
|
||||
follows the contractual quantity as long as there is no physical lot, then it
|
||||
reflects the sum of the executed physical lots.
|
||||
the user enters on the line. The standard line quantity is a read-only
|
||||
technical counter: it is initialized from the contractual quantity as long as
|
||||
there is no physical lot, then it reflects the sum of the executed physical
|
||||
lots.
|
||||
|
||||
Removing a physical lot is allowed only while its stock move is not finalized.
|
||||
If the lot was already matched or linked to a shipment, the user must confirm
|
||||
@@ -65,6 +77,53 @@ quantity states of a lot must be changed only through a dedicated business
|
||||
action, currently `Do weighing`. This rule ensures that open quantity, virtual
|
||||
lot, fee, and stock move recalculations remain synchronized.
|
||||
|
||||
### BR-PT-LOT-002 - Contractual quantity, physical execution, and finished line
|
||||
|
||||
The contractual/theoretical quantity remains the contract basis as long as the
|
||||
line is not marked as finished. It is therefore the basis for the commercial
|
||||
line amount, even if physical lots have already been added and the technical
|
||||
`quantity` field only reflects the sum of those physical lots.
|
||||
|
||||
The `quantity` field is not a business input. It is a technical counter:
|
||||
|
||||
- before any physical lot exists, it is initialized from the
|
||||
contractual/theoretical quantity;
|
||||
- as soon as physical lots exist, it reflects the sum of executed physical
|
||||
lots;
|
||||
- it must not drive the contractual amount while the line is not marked as
|
||||
finished.
|
||||
|
||||
The `Mark as finished` checkbox means that the user accepts ignoring the open
|
||||
balance still carried by the virtual lot. It does not modify the historical
|
||||
contractual quantity. It does not delete physical lots. It only means that
|
||||
calculations that should no longer include the open balance may switch to the
|
||||
physical execution.
|
||||
|
||||
Strict rules:
|
||||
|
||||
- while `finished = False`, the commercial line amount must be calculated on
|
||||
`quantity_theorical`;
|
||||
- when `finished = True`, the remaining open/virtual balance is ignored for
|
||||
execution calculations and the line amount may therefore use the physical
|
||||
counter `quantity` again;
|
||||
- if the finished line has physical lots, the line amount must use the
|
||||
quantity state defined by the Weight basis of the relevant contract, when
|
||||
that state exists on the lots;
|
||||
- `finished` must never erase the initial contract trace;
|
||||
- `finished` must never hide or delete physical lot PnL.
|
||||
|
||||
This rule applies to the amount carried by the contract line. Invoicing remains
|
||||
a separate flow: it may select a specific quantity state from `lot_qt_hist`
|
||||
depending on the contract, for example a bill of lading weight on the purchase
|
||||
side and a Weight Report on the sale side. The line amount must therefore not
|
||||
try to replace the invoicing quantity selection logic.
|
||||
|
||||
The same physical lot can therefore have two different commercial readings: on
|
||||
the purchase contract, it may be valued with the BL state; on the sale
|
||||
contract, once matched, it may be valued with another state, for example LR for
|
||||
Landing Report. The state used always depends on the displayed contract, not on
|
||||
one global lot state.
|
||||
|
||||
### Tolerance and physical quantities
|
||||
|
||||
A physical lot may have a quantity that differs from the initial forecast,
|
||||
@@ -90,7 +149,8 @@ action may break the global quantity consistency rule.
|
||||
(`Contractual Qt`).
|
||||
- Sale contractual quantity: `sale.line.quantity_theorical`
|
||||
(`Th. quantity`).
|
||||
- Purchase/sale quantity counter: `quantity`.
|
||||
- Purchase/sale quantity counter: `quantity`, not editable by the user on
|
||||
trade lines.
|
||||
- Lot type: `lot.lot.lot_type`, values `virtual` and `physic`.
|
||||
- Purchase link: `lot.lot.line`.
|
||||
- Sale link: `lot.lot.sale_line`.
|
||||
@@ -100,6 +160,10 @@ action may break the global quantity consistency rule.
|
||||
- Forecast quantity: `lot.qt.lot_quantity`.
|
||||
- Contract/line tolerances: `tol_min`, `tol_max`, `tol_min_qt`,
|
||||
`tol_max_qt`, `tol_min_v`, `tol_max_v`.
|
||||
- Finished line flag: `purchase.line.finished`, `sale.line.finished`.
|
||||
- Purchase/sale Weight basis: `purchase.purchase.wb`, `sale.sale.wb`.
|
||||
- Final quantity state associated with the Weight basis:
|
||||
`purchase.weight.basis.qt_type`.
|
||||
- Quantity state history: `lot.lot.lot_hist` to `lot.qt.hist`.
|
||||
- History fields: `lot.qt.hist.quantity_type`, `lot.qt.hist.quantity`,
|
||||
`lot.qt.hist.gross_quantity`.
|
||||
@@ -109,14 +173,19 @@ action may break the global quantity consistency rule.
|
||||
|
||||
- Purchase: `modules/purchase_trade/purchase.py`, `Line.validate`.
|
||||
- Sale: `modules/purchase_trade/sale.py`, `SaleLine.validate`.
|
||||
- On creation and when `quantity_theorical` is entered, if `quantity` is empty
|
||||
or equal to zero and no physical lot exists, `quantity` is initialized from
|
||||
`quantity_theorical`.
|
||||
- If the line is not `created_by_code`, has no lot yet, the product is not a
|
||||
service, and `quantity != 0`, a `virtual` lot is created.
|
||||
service, and `quantity_theorical != 0`, a `virtual` lot is created.
|
||||
- The virtual lot receives its first `lot.qt.hist` entry.
|
||||
- The open `lot.qt` creation is triggered by
|
||||
`modules/purchase_trade/lot.py`, `Lot.validate`, through
|
||||
`createVirtualPart` when no `lot.qt` exists yet for the virtual lot.
|
||||
- On purchase, the virtual lot feeds `lot.qt.lot_p`.
|
||||
- On sale, the virtual lot feeds `lot.qt.lot_s` with `lot_p = None`.
|
||||
- Legacy flows that do not yet have `quantity_theorical` may still use
|
||||
`quantity` as a fallback, but it is no longer the business input field.
|
||||
|
||||
### Contractual quantity update
|
||||
|
||||
@@ -149,6 +218,12 @@ free_quantity = target_quantity - sum(already matched or shipped lot.qt)
|
||||
created.
|
||||
- The line fees are resynchronized after the update.
|
||||
|
||||
Invariants to preserve after every update:
|
||||
|
||||
- `sum(physical lots) + virtual lot current quantity = quantity_theorical`;
|
||||
- `sum(lot.qt.lot_quantity where lot_p = virtual lot) =
|
||||
virtual lot current quantity`.
|
||||
|
||||
Purchase-specific point:
|
||||
|
||||
- If `quantity_theorical` was empty during initialization, the code uses the
|
||||
@@ -221,6 +296,32 @@ Sale-specific point:
|
||||
- without a physical lot: `quantity` follows the virtual lot;
|
||||
- with physical lots: `quantity` reflects executed physical quantity.
|
||||
|
||||
### Line amount
|
||||
|
||||
- Purchase: `purchase.line.on_change_with_amount()`.
|
||||
- Sale: `sale.line.on_change_with_amount()`.
|
||||
- While `finished = False`, the amount quantity basis is
|
||||
`quantity_theorical`.
|
||||
- When `finished = True`, the amount quantity basis may switch back to
|
||||
physical execution, because the user has accepted ignoring the remaining open
|
||||
virtual balance.
|
||||
- If the finished line has physical lots and the contract has a Weight basis
|
||||
with `qt_type`, the amount sums physical quantities in that `lot.qt.hist`
|
||||
state, converted into the line unit.
|
||||
- Purchase resolves this state through `purchase.purchase.wb.qt_type`.
|
||||
- Sale resolves this state through `sale.sale.wb.qt_type`.
|
||||
- The same matched physical lot may therefore contribute to a purchase amount
|
||||
in BL state and to a sale amount in LR state.
|
||||
- If `quantity_theorical` is empty in a legacy flow, the code keeps a fallback
|
||||
to `quantity`.
|
||||
- If `quantity` is empty on a finished line, the code keeps a fallback to
|
||||
`quantity_theorical`.
|
||||
- If the Weight basis state does not exist yet on a lot, the code keeps the
|
||||
previous fallback instead of treating the amount as final.
|
||||
- Invoicing remains independent from this line amount rule: invoice lines may
|
||||
use a selected quantity state from `lot.qt.hist` according to the
|
||||
contractual invoicing rule.
|
||||
|
||||
### Tolerance: current state and point to confirm
|
||||
|
||||
The code already exposes tolerances on contracts and lines:
|
||||
@@ -245,6 +346,17 @@ To confirm / potential gap:
|
||||
### Nearby tests
|
||||
|
||||
- `modules/purchase_trade/tests/test_module.py`
|
||||
- `test_trade_line_quantity_is_readonly`
|
||||
- `test_purchase_line_initial_quantity_uses_theoretical_quantity`
|
||||
- `test_sale_line_initial_quantity_uses_theoretical_quantity`
|
||||
- `test_trade_line_initial_quantity_does_not_override_existing_counter`
|
||||
- `test_trade_line_initial_quantity_does_not_run_with_physical_lot`
|
||||
- `test_purchase_line_amount_uses_theoretical_quantity_until_finished`
|
||||
- `test_purchase_line_amount_uses_physical_counter_when_finished`
|
||||
- `test_purchase_line_amount_uses_purchase_weight_basis_when_finished`
|
||||
- `test_sale_line_amount_uses_theoretical_quantity_until_finished`
|
||||
- `test_sale_line_amount_uses_physical_counter_when_finished`
|
||||
- `test_sale_line_amount_uses_sale_weight_basis_when_finished`
|
||||
- `test_sale_line_write_updates_virtual_lot_when_theorical_qty_increases`
|
||||
- `test_sale_line_write_blocks_theorical_qty_decrease_when_no_open_quantity`
|
||||
- `test_purchase_line_write_syncs_open_lot_qt_with_physical_lots`
|
||||
|
||||
@@ -41,18 +41,30 @@ de Lots Management, donc depuis une ligne `lot.qt`. Si l'utilisateur choisit
|
||||
`lot.qt` choisie est réduite, puis le lot virtuel de la ligne est recalculé
|
||||
pour représenter seulement le reliquat encore ouvert.
|
||||
|
||||
La règle immuable de cohérence est :
|
||||
La première règle immuable de cohérence est :
|
||||
|
||||
```text
|
||||
somme des lots physiques de la ligne + lot virtuel de la ligne
|
||||
= quantité contractuelle/théorique de la ligne
|
||||
```
|
||||
|
||||
La deuxième règle immuable de cohérence est :
|
||||
|
||||
```text
|
||||
somme des lot.qt.lot_quantity pour le lot_p virtuel de la ligne
|
||||
= lot.qt.hist.quantity du lot virtuel de la ligne
|
||||
```
|
||||
|
||||
Chaque ajout de lot physique doit retrancher la même quantité des deux côtés
|
||||
de cette égalité : la ligne `lot.qt` forecastée diminue, et l'historique de
|
||||
quantité du lot virtuel diminue aussi.
|
||||
|
||||
Cette cohérence doit rester vraie même si la quantité contractuelle est
|
||||
ajustée en cours de route. La quantité contractuelle/théorique est la seule
|
||||
quantité saisie par l'utilisateur sur la ligne. La quantité standard de la
|
||||
ligne est un compteur : elle reprend la quantité contractuelle tant qu'il n'y a
|
||||
pas de lot physique, puis elle reflète la somme des lots physiques exécutés.
|
||||
ligne est un compteur technique en lecture seule : elle est initialisée depuis
|
||||
la quantité contractuelle tant qu'il n'y a pas de lot physique, puis elle
|
||||
reflète la somme des lots physiques exécutés.
|
||||
|
||||
Le retrait d'un lot physique est autorisé seulement tant que son mouvement
|
||||
stock n'est pas finalisé. Si ce lot était déjà matché ou rattaché à un
|
||||
@@ -66,6 +78,53 @@ une action métier dédiée, aujourd'hui `Do weighing`. Cette règle garantit qu
|
||||
les recalculs de quantité ouverte, de lot virtuel, de fees et de mouvements
|
||||
stock restent synchronisés.
|
||||
|
||||
### BR-PT-LOT-002 - Quantité contractuelle, exécuté physique et ligne finie
|
||||
|
||||
La quantité contractuelle/théorique reste la base du contrat tant que la ligne
|
||||
n'est pas marquée comme finie. Elle sert donc de base au montant commercial de
|
||||
la ligne, même si des lots physiques ont déjà été ajoutés et que la quantité
|
||||
technique `quantity` reflète seulement la somme de ces lots physiques.
|
||||
|
||||
Le champ `quantity` n'est pas une saisie métier. Il est un compteur technique :
|
||||
|
||||
- avant tout lot physique, il est initialisé depuis la quantité
|
||||
contractuelle/théorique ;
|
||||
- dès qu'il existe des lots physiques, il reflète la somme des lots physiques
|
||||
exécutés ;
|
||||
- il ne doit pas piloter le montant contractuel tant que la ligne n'est pas
|
||||
marquée comme finie.
|
||||
|
||||
La coche `Mark as finished` indique que l'utilisateur accepte d'ignorer le
|
||||
reliquat ouvert encore porté par le lot virtuel. Elle ne modifie pas la
|
||||
quantité contractuelle historique. Elle ne supprime pas les lots physiques. Elle
|
||||
signifie seulement que les calculs qui ne doivent plus tenir compte du reliquat
|
||||
ouvert peuvent basculer sur l'exécuté physique.
|
||||
|
||||
Règles strictes :
|
||||
|
||||
- tant que `finished = False`, le montant commercial de la ligne doit être
|
||||
calculé sur `quantity_theorical` ;
|
||||
- quand `finished = True`, le reliquat ouvert/virtuel restant est ignoré pour
|
||||
les calculs d'exécution et le montant de ligne peut donc revenir au compteur
|
||||
physique `quantity` ;
|
||||
- si la ligne finie possède des lots physiques, le montant de ligne doit
|
||||
utiliser l'état de quantité défini par le Weight basis du contrat concerné,
|
||||
quand cet état existe sur les lots ;
|
||||
- `finished` ne doit jamais effacer la trace du contrat initial ;
|
||||
- `finished` ne doit jamais masquer ou supprimer le PnL des lots physiques.
|
||||
|
||||
Cette règle concerne le montant porté par la ligne de contrat. La facturation
|
||||
reste un flux distinct : elle peut choisir un état de quantité précis dans
|
||||
`lot_qt_hist` selon le contrat, par exemple un poids BL à l'achat et un Weight
|
||||
Report à la vente. Le montant de ligne ne doit donc pas essayer de remplacer la
|
||||
logique de sélection des quantités de facturation.
|
||||
|
||||
Le même lot physique peut donc avoir deux lectures commerciales différentes :
|
||||
sur le contrat d'achat, il peut être valorisé avec l'état BL ; sur le contrat de
|
||||
vente, une fois matché, il peut être valorisé avec un autre état, par exemple
|
||||
LR pour Landing Report. L'état utilisé dépend toujours du contrat affiché, pas
|
||||
d'un état global unique du lot.
|
||||
|
||||
### Tolérance et quantités physiques
|
||||
|
||||
Un lot physique peut avoir une quantité différente de la prévision initiale,
|
||||
@@ -92,7 +151,8 @@ des quantités.
|
||||
(`Contractual Qt`).
|
||||
- Quantité contractuelle vente : `sale.line.quantity_theorical`
|
||||
(`Th. quantity`).
|
||||
- Quantité compteur achat/vente : `quantity`.
|
||||
- Quantité compteur achat/vente : `quantity`, non éditable par l'utilisateur
|
||||
dans les lignes trade.
|
||||
- Type de lot : `lot.lot.lot_type`, valeurs `virtual` et `physic`.
|
||||
- Lien achat : `lot.lot.line`.
|
||||
- Lien vente : `lot.lot.sale_line`.
|
||||
@@ -102,6 +162,10 @@ des quantités.
|
||||
- Quantité forecast : `lot.qt.lot_quantity`.
|
||||
- Tolérances contrat/ligne : `tol_min`, `tol_max`, `tol_min_qt`,
|
||||
`tol_max_qt`, `tol_min_v`, `tol_max_v`.
|
||||
- Ligne finie : `purchase.line.finished`, `sale.line.finished`.
|
||||
- Weight basis achat/vente : `purchase.purchase.wb`, `sale.sale.wb`.
|
||||
- État de quantité final associé au Weight basis :
|
||||
`purchase.weight.basis.qt_type`.
|
||||
- Historique des états de quantité : `lot.lot.lot_hist` vers `lot.qt.hist`.
|
||||
- Champs d'historique : `lot.qt.hist.quantity_type`,
|
||||
`lot.qt.hist.quantity`, `lot.qt.hist.gross_quantity`.
|
||||
@@ -111,15 +175,21 @@ des quantités.
|
||||
|
||||
- Achat : `modules/purchase_trade/purchase.py`, `Line.validate`.
|
||||
- Vente : `modules/purchase_trade/sale.py`, `SaleLine.validate`.
|
||||
- À la création et lors de la saisie de `quantity_theorical`, si `quantity` est
|
||||
vide ou égale à zéro et qu'aucun lot physique n'existe, `quantity` est
|
||||
initialisée depuis `quantity_theorical`.
|
||||
- Si la ligne n'est pas `created_by_code`, qu'elle n'a pas encore de lot, que
|
||||
le produit n'est pas un service et que `quantity != 0`, un lot `virtual` est
|
||||
créé.
|
||||
le produit n'est pas un service et que `quantity_theorical != 0`, un lot
|
||||
`virtual` est créé.
|
||||
- Le lot virtuel reçoit une première entrée `lot.qt.hist`.
|
||||
- La création du `lot.qt` ouvert est déclenchée par
|
||||
`modules/purchase_trade/lot.py`, `Lot.validate`, via `createVirtualPart`
|
||||
quand aucun `lot.qt` n'existe encore pour le lot virtuel.
|
||||
- Pour l'achat, le lot virtuel alimente `lot.qt.lot_p`.
|
||||
- Pour la vente, le lot virtuel alimente `lot.qt.lot_s` avec `lot_p = None`.
|
||||
- Les anciens flux qui n'ont pas encore `quantity_theorical` peuvent encore
|
||||
utiliser `quantity` comme fallback legacy, mais ce n'est plus le champ de
|
||||
saisie métier.
|
||||
|
||||
### Modification de la quantité contractuelle
|
||||
|
||||
@@ -153,6 +223,12 @@ free_quantity = target_quantity - somme(lot.qt déjà matchés ou shippés)
|
||||
`lot.qt` libre est créé.
|
||||
- Les fees de la ligne sont resynchronisés après modification.
|
||||
|
||||
Invariants à préserver après chaque modification :
|
||||
|
||||
- `sum(physical lots) + virtual lot current quantity = quantity_theorical` ;
|
||||
- `sum(lot.qt.lot_quantity where lot_p = virtual lot) =
|
||||
virtual lot current quantity`.
|
||||
|
||||
Point spécifique achat :
|
||||
|
||||
- Si `quantity_theorical` était vide au moment de l'initialisation, le code
|
||||
@@ -230,6 +306,32 @@ Point spécifique vente :
|
||||
- sans physique : `quantity` suit le virtuel ;
|
||||
- avec physiques : `quantity` reflète l'exécuté physique.
|
||||
|
||||
### Montant de ligne
|
||||
|
||||
- Achat : `purchase.line.on_change_with_amount()`.
|
||||
- Vente : `sale.line.on_change_with_amount()`.
|
||||
- Tant que `finished = False`, la quantité de base du montant est
|
||||
`quantity_theorical`.
|
||||
- Quand `finished = True`, la quantité de base du montant peut revenir à
|
||||
l'exécuté physique, car l'utilisateur a accepté d'ignorer le reliquat
|
||||
virtuel ouvert.
|
||||
- Si la ligne finie possède des lots physiques et que le contrat porte un
|
||||
Weight basis avec `qt_type`, le montant somme les quantités physiques dans
|
||||
cet état `lot.qt.hist`, converties dans l'unité de la ligne.
|
||||
- L'achat résout cet état via `purchase.purchase.wb.qt_type`.
|
||||
- La vente résout cet état via `sale.sale.wb.qt_type`.
|
||||
- Le même lot physique matché peut donc contribuer à un montant achat en état
|
||||
BL et à un montant vente en état LR.
|
||||
- Si `quantity_theorical` est vide dans un ancien flux, le code conserve un
|
||||
fallback sur `quantity`.
|
||||
- Si `quantity` est vide sur une ligne finie, le code conserve un fallback sur
|
||||
`quantity_theorical`.
|
||||
- Si l'état du Weight basis n'existe pas encore sur un lot, le code conserve le
|
||||
fallback précédent au lieu de considérer le montant comme final.
|
||||
- La facturation reste indépendante de cette règle de montant de ligne :
|
||||
les lignes de facture peuvent utiliser un état de quantité choisi dans
|
||||
`lot.qt.hist` selon la règle contractuelle de facturation.
|
||||
|
||||
### Tolérance : état actuel et point à confirmer
|
||||
|
||||
Le code expose déjà les tolérances sur contrats et lignes :
|
||||
@@ -254,6 +356,17 @@ Le code expose déjà les tolérances sur contrats et lignes :
|
||||
### Tests proches
|
||||
|
||||
- `modules/purchase_trade/tests/test_module.py`
|
||||
- `test_trade_line_quantity_is_readonly`
|
||||
- `test_purchase_line_initial_quantity_uses_theoretical_quantity`
|
||||
- `test_sale_line_initial_quantity_uses_theoretical_quantity`
|
||||
- `test_trade_line_initial_quantity_does_not_override_existing_counter`
|
||||
- `test_trade_line_initial_quantity_does_not_run_with_physical_lot`
|
||||
- `test_purchase_line_amount_uses_theoretical_quantity_until_finished`
|
||||
- `test_purchase_line_amount_uses_physical_counter_when_finished`
|
||||
- `test_purchase_line_amount_uses_purchase_weight_basis_when_finished`
|
||||
- `test_sale_line_amount_uses_theoretical_quantity_until_finished`
|
||||
- `test_sale_line_amount_uses_physical_counter_when_finished`
|
||||
- `test_sale_line_amount_uses_sale_weight_basis_when_finished`
|
||||
- `test_sale_line_write_updates_virtual_lot_when_theorical_qty_increases`
|
||||
- `test_sale_line_write_blocks_theorical_qty_decrease_when_no_open_quantity`
|
||||
- `test_purchase_line_write_syncs_open_lot_qt_with_physical_lots`
|
||||
|
||||
Reference in New Issue
Block a user