From 0d034816f7aeb2091b75e1d28449030447c1a6bf Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Thu, 14 May 2026 10:25:39 +0200 Subject: [PATCH] docs --- .../docs/business/lots-and-quantities.en.md | 553 +++++++---------- .../docs/business/lots-and-quantities.md | 565 +++++++----------- 2 files changed, 462 insertions(+), 656 deletions(-) diff --git a/modules/purchase_trade/docs/business/lots-and-quantities.en.md b/modules/purchase_trade/docs/business/lots-and-quantities.en.md index 8281e59..ae27555 100644 --- a/modules/purchase_trade/docs/business/lots-and-quantities.en.md +++ b/modules/purchase_trade/docs/business/lots-and-quantities.en.md @@ -5,382 +5,291 @@ Mirror page: [lots-and-quantities.md](lots-and-quantities.md) Status: `partial migration` Last code verification: `2026-05-13` -This page consolidates the former rules `BR-PT-LOT-001`, -`BR-PT-LOT-002`, and `BR-PT-LOT-003` around one functional rule: the life -cycle of open, forecasted, and physical quantities. +This page consolidates `BR-PT-LOT-001`, `BR-PT-LOT-002`, and +`BR-PT-LOT-003`. +Goal: drive quantity rules from a readable business definition, then secure +them with Python guards and SQL diagnostics. -## Consultant Business Rule +## Key Points -### BR-PT-LOT-001 - Lot and quantity life cycle +- A trade line has exactly one virtual lot. +- The virtual lot represents the global open balance of the line. +- `lot.qt` represents the operational forecast: matching, forecast sale, + transport, shipment. +- Physical lots consume a `lot.qt` line. +- `quantity_theorical` is the business quantity entered by the user. +- `quantity` is a read-only technical counter. +- The line amount uses theoretical quantity while the line is not finished. +- A finished line may switch back to physical execution, possibly using the + Weight basis quantity state. +- Invoicing chooses its own quantity states from `lot.qt.hist`. +- Invariants are enforced by Python and auditable with SQL. -When a purchase line or sale line is created, the system creates one virtual -lot linked to that line. This virtual lot represents the contract quantity that -is still open. Its initial quantity is the contractual or theoretical quantity -defined on the line. +## Consultant Rules -At the same time, the system adds one open line in `lot.qt`. This line is the -basis for commercial and logistics forecasts: forecast sale, future matching, -planned transport, shipment, and so on. `lot.qt` therefore carries the -operational forecast, while the virtual lot remains the global representation -of the open balance in `lot.lot`. +### BR-PT-LOT-001 - Virtual lot / forecast / physical life cycle -Example: an open quantity `P1` can be progressively subdivided in `lot.qt` to -forecast several sales or transports: +- When a `purchase line` or `sale line` is created: + - one virtual lot is created; + - its initial quantity is the contractual/theoretical quantity; + - one open `lot.qt` line is created. +- Virtual lot = global open balance. +- `lot.qt` = forecast allocation of that balance. +- Example split of an open balance `P1`: + - `P1S1T1` + - `P1S1T2` + - `P1S2T3` + - `P1S2T4` +- These splits do not create several virtual lots. +- A physical lot is created from a specific `lot.qt` line. +- The physical lot consumes that forecast. +- The `lot.qt` line decreases. +- The virtual lot is recalculated. -- `P1S1T1` -- `P1S1T2` -- `P1S2T3` -- `P1S2T4` +### BR-PT-LOT-002 - Contractual quantity, counter quantity, finished line -These subdivisions do not create several virtual lots for `P1`. They only -describe the forecast allocation of the open quantity. +- `quantity_theorical` is the business quantity. +- `quantity` is not a business input. +- Before any physical lot: + - `quantity` is initialized from `quantity_theorical`. +- After physical lots are added: + - `quantity` reflects physical execution. +- While `finished = False`: + - the line amount uses `quantity_theorical`. +- When `finished = True`: + - the user accepts ignoring the open balance; + - the line amount may switch back to physical execution; + - the initial contract trace remains unchanged; + - physical lots and their PnL remain visible. +- If a Weight basis exists on the contract: + - purchase: state from `purchase.purchase.wb.qt_type`; + - sale: state from `sale.sale.wb.qt_type`; + - the same physical lot may be read as BL on purchase and LR on sale. +- Invoicing remains independent: + - it may select a specific state in `lot.qt.hist`; + - example: BL on purchase, Weight Report or LR on sale. -When physical lots are added, they are created from a specific Lots Management -line, therefore from a `lot.qt` line. If the user selects `P1S1T1`, the -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. +### BR-PT-LOT-003 - Quantity Invariants -The first immutable quantity consistency rule is: +Conservation invariant: ```text -sum of the line's physical lots + the line's virtual lot -= the line's contractual/theoretical quantity +sum(physical lots) + virtual lot quantity = quantity_theorical ``` -The second immutable quantity consistency rule is: +Open forecast invariant: ```text -sum of lot.qt.lot_quantity for the line's virtual lot_p -= lot.qt.hist.quantity of the line's virtual lot +sum(non-zero lot.qt of the virtual lot) = max(virtual lot quantity, 0) ``` -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. +Details: -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 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. +- Purchase: sum all `lot.qt` where `lot_p = virtual lot`, with or without + `lot_s`. +- Sale: sum all `lot.qt` where `lot_s = virtual lot`, with or without `lot_p`. +- `lot.qt = 0` rows are ignored: + - they may keep the memory of a consumed forecast; + - `lot.qt` never goes below zero. +- The virtual lot may become negative: + - it compensates the gap between `quantity_theorical` and physical execution; + - in that case expected open forecast remains zero. +- A non-zero `lot.qt` without `lot_p` and without `lot_s` is forbidden. -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 -the action. The removal restores the quantity into the `lot.qt` line that -carried the forecast, matching, or transport context used to create the -physical lot. +### BR-PT-LOT-004 - Quantity history and weighing -The quantity history of a lot is never a direct data-entry area. The different -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. +- `lot.qt.hist` carries lot quantity states. +- The `lot.lot` form is not a direct entry area for states. +- Updates are allowed only through `Do weighing`. +- A virtual lot does not receive manual packing: + - `lot_qt` is not editable; + - `lot_unit` is not editable. -### BR-PT-LOT-002 - Contractual quantity, physical execution, and finished line +### Tolerances -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, -within the contract tolerance limits. Tolerance must be read as a global -tolerance on the line or contract, not as an independent tolerance per -transport. - -If a physical lot consumes more or less than its forecast, this must -dynamically reduce or increase the remaining tolerance for the next physical -lots of the same line. No add, weighing, deletion, or contractual adjustment -action may break the global quantity consistency rule. +- Tolerance is global on the line or contract. +- It is not independent per transport. +- A physical lot may consume more or less than its forecast. +- This over/under execution consumes or restores remaining tolerance. +- Current gap: full remaining-tolerance control in `LotQt.add_physical_lots` / + `LotQt.add_physical_lot` still needs confirmation. ## Developer Section -### Main models and fields +### Key Fields -- Purchase line: `purchase.line`. -- Sale line: `sale.line`. -- Lot: `lot.lot`. -- Forecast / open quantity: `lot.qt`. -- Lot quantity history: `lot.qt.hist`. -- Purchase contractual quantity: `purchase.line.quantity_theorical` - (`Contractual Qt`). -- Sale contractual quantity: `sale.line.quantity_theorical` - (`Th. 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`. -- Shipment links: `lot_shipment_in`, `lot_shipment_internal`, - `lot_shipment_out`. -- Purchase/sale forecast matching: `lot.qt.lot_p`, `lot.qt.lot_s`. -- 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`. -- Lot packing fields: `lot.lot.lot_qt`, `lot.lot.lot_unit`. +- Purchase line: `purchase.line` +- Sale line: `sale.line` +- Lot: `lot.lot` +- Forecast: `lot.qt` +- History: `lot.qt.hist` +- Purchase business quantity: `purchase.line.quantity_theorical` +- Sale business quantity: `sale.line.quantity_theorical` +- Technical counter: `quantity` +- Finished line: `purchase.line.finished`, `sale.line.finished` +- Virtual / physical lot: `lot.lot.lot_type = virtual / physic` +- Purchase link: `lot.lot.line` +- Sale link: `lot.lot.sale_line` +- Purchase forecast: `lot.qt.lot_p` +- Sale forecast: `lot.qt.lot_s` +- Forecast quantity: `lot.qt.lot_quantity` +- Weight basis: `purchase.purchase.wb`, `sale.sale.wb` +- Weight basis state: `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` -### Line and virtual lot creation +### Line / Virtual Lot Creation -- 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_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. +- Purchase: `purchase.py`, `Line.validate` +- Sale: `sale.py`, `SaleLine.validate` +- If `quantity_theorical` is entered and `quantity` is empty or zero: + - `quantity` is initialized from `quantity_theorical`; + - only if no physical lot exists. +- If the line is eligible: + - not `created_by_code`; + - no lot yet; + - non-service product; + - `quantity_theorical != 0`; + - create one `virtual` lot. +- The virtual lot receives a first `lot.qt.hist` entry. +- `Lot.validate` creates the open `lot.qt` through `createVirtualPart`. -### Contractual quantity update +### Updating `quantity_theorical` -Verified code state: - -- Purchase: `purchase.py`, `Line.write`. -- Sale: `sale.py`, `SaleLine.write`. -- Updating `quantity_theorical` recalculates a target virtual quantity: +- Purchase: `purchase.py`, `Line.write` +- Sale: `sale.py`, `SaleLine.write` +- Virtual lot target: ```text target_quantity = quantity_theorical - sum(converted physical lots) ``` -- If this target quantity becomes negative, the code blocks with - `Please unlink or unmatch lot`. -- The free `lot.qt` is then resynchronized while taking into account `lot.qt` - lines that are already allocated, meaning already matched or linked to a - shipment: +- If `target_quantity < 0`: + - block with `Please unlink or unmatch lot`. +- Free `lot.qt` target: ```text free_quantity = target_quantity - sum(already matched or shipped lot.qt) ``` -- If `free_quantity` becomes negative, the code blocks with - `Please unlink or unmatch lot`. -- If the virtual lot does not already carry the target quantity, the code calls - `vlot.set_current_quantity(target_quantity, target_quantity, 1)`. -- If a free `lot.qt` exists, its quantity is replaced with `free_quantity`. -- If no free `lot.qt` exists and `free_quantity > 0`, a new free `lot.qt` is - created. -- The line fees are resynchronized after the update. +- If `free_quantity < 0`: + - block with `Please unlink or unmatch lot`. +- If a free `lot.qt` exists: + - replace its quantity. +- If no free `lot.qt` exists and `free_quantity > 0`: + - create a new `lot.qt`. +- Line fees are resynchronized. -Invariants to preserve after every update: +### Adding Physical Lots -- `sum(physical lots) + virtual lot current quantity = quantity_theorical`; -- `sum(non-zero lot.qt.lot_quantity where lot_p = virtual lot) = - max(virtual lot current quantity, 0)`. -- For a purchase virtual lot, the second invariant sums all `lot.qt` lines - where `lot_p = virtual lot`, whether they are matched to a `lot_s` or not. -- For a sale virtual lot, the second invariant sums all `lot.qt` lines where - `lot_s = virtual lot`, whether they are linked to a `lot_p` or not. -- A zero `lot.qt` row is completely ignored by this check: it is the memory of - a consumed forecast and `lot.qt` never goes below zero. -- If the virtual lot becomes negative to compensate the difference between - `quantity_theorical` and physical execution, the expected `lot.qt` therefore - remains zero. -- A non-zero `lot.qt` row without both `lot_p` and `lot_s` is blocked by - `lot.qt.validate`. -- The technical guard is centralized in - `lot.lot.assert_lines_quantity_consistency()` and must be called at the end - of flows that modify lots, matching, transport, weighing, or the contractual - quantity. +- Wizard: `lot.add` +- Methods: + - `LotQt.add_physical_lots` + - `LotQt.add_physical_lot` +- Mandatory source: one `lot.qt` line. +- Direct add from a physical lot is refused. +- Physical add on sale side through this wizard is refused: use + `Apply matching`. +- The physical lot inherits: + - purchase line; + - matched sale, if any; + - shipment; + - product; + - unit; + - quantities; + - premium; + - chunk key. +- After creation: + - source `lot.qt` is reduced; + - `lot.qt` cannot become negative; + - virtual lot is recalculated; + - `quantity` is recalculated; + - moves and fees are updated when needed. -Purchase-specific point: +### Removing Physical Lots -- If `quantity_theorical` was empty during initialization, the code uses the - current virtual lot quantity as the baseline to avoid doubling the open - `lot.qt`. +- Wizard: `lot.remove` +- Open lot: removal forbidden. +- Lot with `stock.move`: + - move must be `draft`. +- Matched or shipped lot: + - confirmable warning. +- Effects: + - draft move deletion; + - restore quantity into `lot.qt`; + - restore context through shipment, `getVlot_p()`, `getVlot_s()`; + - recalculate virtual lot, `quantity`, fees. -Sale-specific point: +### Weighing / Quantity States -- If the previous `quantity_theorical` is empty, `SaleLine.write` does not yet - run this resynchronization. +- Wizard: `lot.weighing` +- UI action: `Do weighing` +- Writes or updates `lot.qt.hist`. +- May update `lot_state`. +- Synchronizes: + - lot; + - open quantities; + - fees. +- `lot.qt.hist` views are consultative. -### Adding physical lots +### Quantity Counter `quantity` -- Wizard: `modules/purchase_trade/lot.py`, `lot.add`. -- Main method: `LotQt.add_physical_lots`. -- Unit creation method: `LotQt.add_physical_lot`. -- Adding starts from a `lot.qt` line selected in Lots Management. -- The code refuses direct addition from a physical lot. -- The code refuses physical addition on the sale side through this wizard and - asks the user to use `Apply matching`. -- The new physical lot inherits the context of the `lot.qt` line: purchase - line, matched sale if present, shipment, product, unit, quantities, premium, - and chunk key. -- After creation, the code reduces the source `lot.qt` quantity by the total - physical quantity created and never lets the line go below zero. -- Saving/validating the physical lot recalculates: - - the line's virtual lot through `_recompute_virtual_lot`; - - the line quantity counter through `_recalc_line_quantity`; - - linked stock moves when needed; - - fees linked to the lot, line, or shipment. +- Method: `Lot._recalc_line_quantity` +- Without physical lots: + - `quantity` follows the virtual lot. +- With physical lots: + - `quantity` sums physical lots only. +- `quantity` is readonly on trade lines. -### Removing physical lots +### Line Amount -- Wizard: `modules/purchase_trade/lot.py`, `lot.remove`. -- Removing an open lot is forbidden. -- If the physical lot has a `stock.move`, that move must be in `draft` state. -- If the lot is matched or shipped, a confirmable warning is displayed. -- The code first deletes the draft move when present. -- The code restores the physical quantity into `lot.qt` while reusing the - context: - - original shipment through `lot.lot_shipment_origin`; - - sale virtual lot through `getVlot_s()` if the lot was matched; - - purchase virtual lot through `getVlot_p()` in `updateVirtualPart`. -- If a compatible `lot.qt` line already exists, it is incremented. -- Otherwise, a new `lot.qt` line is created. -- Deleting the physical lot then triggers recalculation of the virtual lot, - the line quantity counter, and fees. +- Purchase: `purchase.line.on_change_with_amount()` +- Sale: `sale.line.on_change_with_amount()` +- Helpers: + - `_get_amount_quantity()` + - `_get_weight_basis_quantity()` +- Priorities: + - `finished = False`: `quantity_theorical` + - `finished = True` + usable Weight basis: physical sum in that state + - `finished = True` without usable Weight basis: `quantity` + - legacy fallback: `quantity` if `quantity_theorical` is empty -### Quantity history and weighing +### Python Guards -- History is carried by `lot.qt.hist`. -- `lot.qt.hist` represents the different quantity states of a lot. -- Historical quantities must not be changed directly from the `lot.lot` form. -- The `lot.lot` view displays `lot_hist` as read-only. -- `lot.qt.hist` views are consultative: no direct editing from tree or form. -- The functional change path is the `lot.weighing` wizard, exposed by the - `Do weighing` action. -- The wizard writes or updates the `lot.qt.hist` entry for the selected - `lot_state`, then synchronizes the lot, open quantities, and fees. -- A virtual lot must not receive packing data entry from the lot form: - `lot_qt` and `lot_unit` are not editable for `lot_type = virtual`. +- Central check: + - `lot.lot.assert_lines_quantity_consistency()` +- Non-zero orphan `lot.qt` block: + - `lot.qt.validate` +- Called after: + - `quantity_theorical` update; + - physical lot creation / deletion; + - matching / unmatching; + - shipping / unshipping; + - weighing. -### Line quantity counter +### SQL Diagnostic -- Method: `lot.py`, `Lot._recalc_line_quantity`. -- If the line has only one lot, `quantity` takes the current quantity of that - lot. -- If the line has several lots, `quantity` sums physical lots only. -- This matches the functional rule: - - without a physical lot: `quantity` follows the virtual lot; - - with physical lots: `quantity` reflects executed physical quantity. +- Script: + - [sql/quantity_consistency_checks.sql](sql/quantity_consistency_checks.sql) +- Use: + - test database audit; + - historical data audit; + - qualification before repair. +- The script completely ignores `lot.qt = 0`. -### 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: - -- Purchase: `purchase.purchase.tol_min`, `purchase.purchase.tol_max`, - `purchase.line.tol_min`, `purchase.line.tol_max`. -- Sale: `sale.sale.tol_min`, `sale.sale.tol_max`, `sale.line.tol_min`, - `sale.line.tol_max`. -- Display functions: `get_tol_min`, `get_tol_max`. -- Add wizard: `lot.add.line.tol_min`, `lot.add.line.tol_max`, with defaults - from the source contract. - -To confirm / potential gap: - -- In `LotQt.add_physical_lots` and `LotQt.add_physical_lot`, I do not yet see - a complete control that computes remaining global tolerance while taking - already created physical lots into account. -- The consultant rule above about dynamic global tolerance must therefore be - considered a target business rule to verify/implement before marking it - `active`. - -### Nearby tests +## 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` - - `test_purchase_line_write_syncs_virtual_fee_quantity` - - `test_purchase_line_write_initial_theorical_qty_does_not_double_open_lot` - -To add if this rule becomes regression-sensitive: - -- verify that `lot_hist` is read-only on the `lot.lot` form; -- verify that `Do weighing` can still create or update a quantity state; -- verify that a virtual lot does not allow direct entry of `lot_qt` and - `lot_unit`. +- Existing coverage: + - readonly `quantity`; + - initialization from `quantity_theorical`; + - protection when physical lots exist; + - amount on theoretical / physical / Weight basis; + - virtual lot resynchronization; + - blocking when open quantity is not enough. +- Tests to add: + - readonly `lot_hist`; + - `Do weighing` creates or updates a state; + - virtual lot without direct `lot_qt` / `lot_unit` entry; + - SQL checks replayed on inconsistent datasets. diff --git a/modules/purchase_trade/docs/business/lots-and-quantities.md b/modules/purchase_trade/docs/business/lots-and-quantities.md index b44231d..1311b7d 100644 --- a/modules/purchase_trade/docs/business/lots-and-quantities.md +++ b/modules/purchase_trade/docs/business/lots-and-quantities.md @@ -1,398 +1,295 @@ # 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` +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 les anciennes règles `BR-PT-LOT-001`, -`BR-PT-LOT-002` et `BR-PT-LOT-003` autour d'une règle fonctionnelle unique : -le cycle de vie des quantités ouvertes, forecastées et physiques. +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. -## Règle business consultant +## À retenir -### BR-PT-LOT-001 - Cycle de vie des lots et des quantités +- Une ligne trade possède un seul lot virtuel. +- Le lot virtuel représente le solde ouvert global de la ligne. +- `lot.qt` représente le forecast opérationnel : matching, vente prévue, + transport, shipment. +- Les lots physiques consomment une ligne `lot.qt`. +- `quantity_theorical` est la quantité métier saisie. +- `quantity` est un compteur technique non éditable. +- Le montant de ligne utilise la quantité théorique tant que la ligne n'est pas + finie. +- Une ligne finie peut revenir à l'exécuté physique, éventuellement dans l'état + de quantité du Weight basis. +- La facturation choisit ses propres états de quantité dans `lot.qt.hist`. +- Les invariants sont contrôlés par Python et auditables en SQL. -Lors de la création d'une ligne d'achat ou de vente, le système crée un lot -virtuel associé à cette ligne. Ce lot virtuel représente la quantité encore -ouverte du contrat. Sa quantité initiale reprend la quantité contractuelle ou -théorique de la ligne. +## Règles consultant -En parallèle, le système ajoute une ligne ouverte dans `lot.qt`. Cette ligne -sert de base aux prévisions commerciales et logistiques : vente prévisionnelle, -matching futur, transport planifié, shipment, etc. `lot.qt` porte donc le -forecast opérationnel, tandis que le lot virtuel reste la représentation -globale du solde ouvert dans `lot.lot`. +### BR-PT-LOT-001 - Cycle de vie lot virtuel / forecast / physique -Exemple : une quantité ouverte `P1` peut être progressivement subdivisée dans -`lot.qt` pour prévoir plusieurs ventes ou transports : +- À la création d'une `purchase line` ou `sale line` : + - un lot virtuel est créé ; + - sa quantité initiale reprend la quantité contractuelle/théorique ; + - une ligne ouverte est créée dans `lot.qt`. +- Le lot virtuel = solde ouvert global. +- `lot.qt` = découpage prévisionnel de ce solde. +- Exemple de découpage d'un solde `P1` : + - `P1S1T1` + - `P1S1T2` + - `P1S2T3` + - `P1S2T4` +- Ces découpages ne créent pas plusieurs lots virtuels. +- Un lot physique est créé depuis une ligne `lot.qt` précise. +- Le lot physique consomme cette prévision. +- La ligne `lot.qt` diminue. +- Le lot virtuel est recalculé. -- `P1S1T1` -- `P1S1T2` -- `P1S2T3` -- `P1S2T4` +### BR-PT-LOT-002 - Quantité contractuelle, quantité compteur, ligne finie -Ces subdivisions ne créent pas plusieurs lots virtuels pour `P1`. Elles -décrivent seulement la répartition prévisionnelle de la quantité ouverte. +- `quantity_theorical` est la quantité métier. +- `quantity` n'est pas une saisie métier. +- Avant lot physique : + - `quantity` est initialisée depuis `quantity_theorical`. +- Après ajout de lots physiques : + - `quantity` reflète l'exécuté physique. +- Tant que `finished = False` : + - le montant de ligne utilise `quantity_theorical`. +- Quand `finished = True` : + - l'utilisateur accepte d'ignorer le reliquat ouvert ; + - le montant de ligne peut revenir à l'exécuté physique ; + - la trace contractuelle initiale reste intacte ; + - les lots physiques et leur PnL restent visibles. +- Si un Weight basis existe sur le contrat : + - achat : état via `purchase.purchase.wb.qt_type` ; + - vente : état via `sale.sale.wb.qt_type` ; + - le même lot physique peut être lu en BL côté achat et en LR côté vente. +- La facturation reste indépendante : + - elle peut choisir un état précis dans `lot.qt.hist` ; + - exemple : BL à l'achat, Weight Report ou LR à la vente. -Quand des lots physiques sont ajoutés, ils sont créés depuis une ligne précise -de Lots Management, donc depuis une ligne `lot.qt`. Si l'utilisateur choisit -`P1S1T1`, le lot physique créé consomme ce forecast particulier. La ligne -`lot.qt` choisie est réduite, puis le lot virtuel de la ligne est recalculé -pour représenter seulement le reliquat encore ouvert. +### BR-PT-LOT-003 - Invariants de quantité -La première règle immuable de cohérence est : +Invariant de conservation : ```text -somme des lots physiques de la ligne + lot virtuel de la ligne -= quantité contractuelle/théorique de la ligne +somme(lots physiques) + quantité du lot virtuel = quantity_theorical ``` -La deuxième règle immuable de cohérence est : +Invariant du forecast ouvert : ```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 +somme(lot.qt non zéro du lot virtuel) = max(quantité du lot virtuel, 0) ``` -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. +Précisions : -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 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. +- Achat : sommer tous les `lot.qt` où `lot_p = lot virtuel`, avec ou sans + `lot_s`. +- Vente : sommer tous les `lot.qt` où `lot_s = lot virtuel`, avec ou sans + `lot_p`. +- Les `lot.qt = 0` sont ignorés : + - ils peuvent mémoriser une prévision vidée ; + - `lot.qt` ne descend jamais sous zéro. +- Le lot virtuel peut devenir négatif : + - il compense l'écart entre `quantity_theorical` et l'exécuté physique ; + - dans ce cas le forecast ouvert attendu reste zéro. +- Un `lot.qt` non zéro sans `lot_p` ni `lot_s` est interdit. -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 -shipment, l'utilisateur doit confirmer l'action. Le retrait restaure la -quantité dans la ligne `lot.qt` qui portait le contexte forecast, matching ou -transport ayant permis de créer le lot physique. +### BR-PT-LOT-004 - Historique de quantité et weighing -L'historique des quantités d'un lot n'est jamais une zone de saisie directe. -Les différents états de quantité d'un lot doivent être modifiés uniquement par -une action métier dédiée, aujourd'hui `Do weighing`. Cette règle garantit que -les recalculs de quantité ouverte, de lot virtuel, de fees et de mouvements -stock restent synchronisés. +- `lot.qt.hist` porte les états de quantité d'un lot. +- La fiche `lot.lot` n'est pas une zone de saisie directe des états. +- Modification autorisée uniquement via `Do weighing`. +- Un lot virtuel ne reçoit pas de packing manuel : + - `lot_qt` non éditable ; + - `lot_unit` non éditable. -### BR-PT-LOT-002 - Quantité contractuelle, exécuté physique et ligne finie +### Tolérances -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, -dans les limites de tolérance du contrat. La tolérance doit se lire comme une -tolérance globale sur la ligne ou le contrat, pas comme une tolérance -indépendante par transport. - -Si un lot physique consomme plus ou moins que son forecast, cela doit réduire -ou augmenter dynamiquement la tolérance restante pour les prochains lots -physiques de la même ligne. Aucune action d'ajout, de weighing, de suppression -ou d'ajustement contractuel ne doit permettre de sortir de la cohérence globale -des quantités. +- La tolérance est globale sur la ligne ou le contrat. +- Elle n'est pas indépendante par transport. +- Un lot physique peut dépasser ou sous-consommer son forecast. +- Ce dépassement consomme ou restitue de la tolérance restante. +- Gap actuel : le contrôle complet de tolérance restante dans + `LotQt.add_physical_lots` / `LotQt.add_physical_lot` reste à confirmer. ## Section développeur -### Modèles et champs principaux +### Champs clés -- Ligne achat : `purchase.line`. -- Ligne vente : `sale.line`. -- Lot : `lot.lot`. -- Forecast / quantité ouverte : `lot.qt`. -- Historique des quantités de lot : `lot.qt.hist`. -- Quantité contractuelle achat : `purchase.line.quantity_theorical` - (`Contractual Qt`). -- Quantité contractuelle vente : `sale.line.quantity_theorical` - (`Th. 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`. -- Liens shipment : `lot_shipment_in`, `lot_shipment_internal`, - `lot_shipment_out`. -- Matching forecast achat/vente : `lot.qt.lot_p`, `lot.qt.lot_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`. -- Champs de packing du lot : `lot.lot.lot_qt`, `lot.lot.lot_unit`. +- 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 de la ligne et du lot virtuel +### Création ligne / lot virtuel -- 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_theorical != 0`, un lot - `virtual` est créé. +- 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`. -- 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. +- `Lot.validate` crée le `lot.qt` ouvert via `createVirtualPart`. -### Modification de la quantité contractuelle +### Modification de `quantity_theorical` -État du code vérifié : - -- Achat : `purchase.py`, `Line.write`. -- Vente : `sale.py`, `SaleLine.write`. -- La modification de `quantity_theorical` recalcule une quantité virtuelle - cible : +- Achat : `purchase.py`, `Line.write` +- Vente : `sale.py`, `SaleLine.write` +- Cible lot virtuel : ```text target_quantity = quantity_theorical - somme(lots physiques convertis) ``` -- Si cette quantité cible devient négative, le code bloque avec - `Please unlink or unmatch lot`. -- Le `lot.qt` libre est ensuite resynchronisé en tenant compte des lignes - `lot.qt` déjà allouées, c'est-à-dire déjà matchées ou rattachées à un - shipment : +- 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` devient négative, le code bloque avec - `Please unlink or unmatch lot`. -- Si le lot virtuel ne porte pas déjà la quantité cible, le code appelle - `vlot.set_current_quantity(target_quantity, target_quantity, 1)`. -- Si un `lot.qt` libre existe, sa quantité est remplacée par `free_quantity`. -- Si aucun `lot.qt` libre n'existe et que `free_quantity > 0`, un nouveau - `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 non zéro where lot_p = virtual lot) = - max(virtual lot current quantity, 0)`. -- Pour un lot virtuel achat, le second invariant somme toutes les lignes - `lot.qt` où `lot_p = virtual lot`, qu'elles soient matchées à un `lot_s` ou - non. -- Pour un lot virtuel vente, le second invariant somme toutes les lignes - `lot.qt` où `lot_s = virtual lot`, qu'elles soient liées à un `lot_p` ou non. -- Une ligne `lot.qt` à zéro est totalement ignorée par ce check : elle sert de - mémoire d'une prévision vidée et `lot.qt` ne descend jamais sous zéro. -- Si le lot virtuel devient négatif pour compenser l'écart entre - `quantity_theorical` et l'exécuté physique, le `lot.qt` attendu reste donc - zéro. -- Une ligne `lot.qt` non zéro sans `lot_p` ni `lot_s` est bloquée par - `lot.qt.validate`. -- Le garde-fou technique est centralisé dans - `lot.lot.assert_lines_quantity_consistency()` et doit être appelé à la fin - des flux qui modifient des lots, du matching, du transport, du weighing ou la - quantité contractuelle. - -Point spécifique achat : - -- Si `quantity_theorical` était vide au moment de l'initialisation, le code - prend comme baseline la quantité courante du lot virtuel pour éviter de - doubler le `lot.qt` ouvert. - -Point spécifique vente : - -- Si l'ancienne `quantity_theorical` est vide, `SaleLine.write` ne lance pas - encore cette resynchronisation. +- 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 : `modules/purchase_trade/lot.py`, `lot.add`. -- Méthode principale : `LotQt.add_physical_lots`. -- Création unitaire : `LotQt.add_physical_lot`. -- L'ajout part obligatoirement d'une ligne `lot.qt` issue de Lots Management. -- Le code refuse l'ajout direct depuis un lot physique. -- Le code refuse l'ajout physique côté vente par ce wizard et demande - d'utiliser `Apply matching`. -- Le nouveau lot physique reprend le contexte de la ligne `lot.qt` : - ligne achat, vente matchée si présente, shipment, produit, unité, - quantités, premium et chunk key. -- Après création, le code réduit la quantité de la ligne `lot.qt` source du - total physique créé et ne laisse pas la ligne descendre sous zéro. -- La sauvegarde/validation du lot physique recalcule : - - le lot virtuel de la ligne via `_recompute_virtual_lot` ; - - la quantité compteur de la ligne via `_recalc_line_quantity` ; - - les mouvements stock liés si nécessaire ; - - les fees rattachés au lot, à la ligne ou au shipment. +- 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 : `modules/purchase_trade/lot.py`, `lot.remove`. -- Le retrait d'un lot ouvert est interdit. -- Si le lot physique possède un `stock.move`, ce move doit être en état - `draft`. -- Si le lot est matché ou shippé, un warning confirmable est affiché. -- Le code supprime d'abord le move draft si présent. -- Le code restaure la quantité physique dans `lot.qt` en réutilisant le - contexte : - - shipment d'origine via `lot.lot_shipment_origin` ; - - lot virtuel sale via `getVlot_s()` si le lot était matché ; - - lot virtuel purchase via `getVlot_p()` dans `updateVirtualPart`. -- Si une ligne `lot.qt` compatible existe déjà, elle est incrémentée. -- Sinon une nouvelle ligne `lot.qt` est créée. -- La suppression du lot physique déclenche ensuite le recalcul du lot virtuel, - de la quantité compteur de ligne et des fees. +- 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. -### Historique de quantité et weighing +### Weighing / états de quantité -- L'historique est porté par `lot.qt.hist`. -- `lot.qt.hist` représente les différents états de quantité d'un lot. -- Les quantités historiques ne doivent pas être modifiées directement depuis - la fiche `lot.lot`. -- La vue `lot.lot` affiche `lot_hist` en lecture seule. -- Les vues `lot.qt.hist` sont consultatives: pas d'édition directe depuis - l'arbre ou le formulaire. -- Le chemin fonctionnel de modification est le wizard `lot.weighing`, exposé - par l'action `Do weighing`. -- Le wizard écrit ou met à jour l'entrée `lot.qt.hist` correspondant au - `lot_state` choisi, puis synchronise le lot, les quantités ouvertes et les - fees. -- Un lot virtuel ne doit pas recevoir de saisie de packing depuis la fiche - lot: les champs `lot_qt` et `lot_unit` sont non éditables pour - `lot_type = virtual`. +- 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 de ligne +### Quantité compteur `quantity` -- Méthode : `lot.py`, `Lot._recalc_line_quantity`. -- Si la ligne n'a qu'un seul lot, `quantity` reprend la quantité courante de ce - lot. -- Si la ligne a plusieurs lots, `quantity` somme uniquement les lots physiques. -- Cette logique correspond à la règle fonctionnelle : - - sans physique : `quantity` suit le virtuel ; - - avec physiques : `quantity` reflète l'exécuté physique. +- 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()`. -- 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. +- 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 -### Tolérance : état actuel et point à confirmer +### Garde-fous Python -Le code expose déjà les tolérances sur contrats et lignes : +- 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. -- Achat : `purchase.purchase.tol_min`, `purchase.purchase.tol_max`, - `purchase.line.tol_min`, `purchase.line.tol_max`. -- Vente : `sale.sale.tol_min`, `sale.sale.tol_max`, `sale.line.tol_min`, - `sale.line.tol_max`. -- Fonctions d'affichage : `get_tol_min`, `get_tol_max`. -- Wizard d'ajout : `lot.add.line.tol_min`, `lot.add.line.tol_max`, avec - défauts depuis le contrat source. +### Diagnostic SQL -À confirmer / gap potentiel : +- 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`. -- Dans `LotQt.add_physical_lots` et `LotQt.add_physical_lot`, je ne vois pas - encore de contrôle complet qui calcule une tolérance restante globale en - tenant compte des lots physiques déjà créés. -- La règle consultant ci-dessus de tolérance globale dynamique doit donc être - considérée comme cible métier à vérifier/implémenter avant de la marquer - `active`. - -### Tests proches +## 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` - - `test_purchase_line_write_syncs_virtual_fee_quantity` - - `test_purchase_line_write_initial_theorical_qty_does_not_double_open_lot` - -À ajouter si cette règle devient sensible côté régression: - -- vérifier que `lot_hist` est en lecture seule sur la fiche `lot.lot`; -- vérifier que `Do weighing` reste capable de créer ou mettre à jour un état de - quantité; -- vérifier qu'un lot virtuel ne permet pas la saisie directe de `lot_qt` et - `lot_unit`. +- 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.