# Lots and Quantities Language: `en` Mirror page: [lots-and-quantities.md](lots-and-quantities.md) Status: `partial migration` Last code verification: `2026-05-13` 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. ## Key Points > **Operational Summary** > A trade line has exactly one virtual lot. The virtual lot carries the > global open balance, `lot.qt` carries the operational forecast, and > physical lots consume that forecast. > | Topic | Short Rule | | --- | --- | | Entered quantity | `quantity_theorical` is the business quantity. | | Standard quantity | `quantity` is a read-only technical counter. | | Before physical lots | `quantity` follows `quantity_theorical`. | | After physical lots | `quantity` reflects physical execution. | | Open line | Line amount uses `quantity_theorical`. | | Finished line | Line amount may switch back to physical execution. | | Weight basis | Purchase and sale may read two different states of the same lot. | | Invoicing | It chooses states from `lot.qt.hist`. | | Controls | Invariants are blocked in Python and auditable with SQL. | ```text quantity_theorical | v virtual lot P1 ---> lot.qt forecast ---> physical lot ^ | | v +------ recalculation after consumption ``` ## Consultant Rules ### BR-PT-LOT-001 - Virtual lot / forecast / physical life cycle | Moment | Business Effect | | --- | --- | | Line creation | One unique virtual lot is created. | | Initialization | The virtual lot takes `quantity_theorical`. | | Forecast | One open line is created in `lot.qt`. | | Planning | `lot.qt` may be split by sale, matching, transport, or shipment. | | Physical add | The physical lot consumes a precise `lot.qt` line. | | After add | `lot.qt` decreases and the virtual lot is recalculated. | > **Split of an open P1 balance** > `P1S1T1`, `P1S1T2`, `P1S2T3`, `P1S2T4` > > **Key Point** > These splits do not create several virtual lots. They only describe the > forecast allocation of the open balance. > ### BR-PT-LOT-002 - Contractual quantity, counter quantity, finished line | Situation | Reference Quantity | | --- | --- | | User entry | `quantity_theorical` | | No physical lot | `quantity = quantity_theorical` | | Physical lots exist | `quantity = sum of physical lots` | | `finished = False` | Amount based on `quantity_theorical` | | `finished = True` | Amount based on physical execution | | Weight basis available | Amount based on the contract `wb.qt_type` state | > **What `finished` Does Not Do** > `finished` does not erase the contractual quantity, delete physical lots, > or hide their PnL. It only means that the open balance may be ignored for > execution calculations. > | Reading of the Same Physical Lot | Possible State | | --- | --- | | Purchase | BL through `purchase.purchase.wb.qt_type` | | Sale | LR or Weight Report through `sale.sale.wb.qt_type` | | Invoicing | Independent choice in `lot.qt.hist` | ### BR-PT-LOT-003 - Quantity Invariants | Invariant | Formula | | --- | --- | | Conservation | `sum(physical lots) + virtual lot = quantity_theorical` | | Open forecast | `sum(non-zero lot.qt) = max(virtual lot, 0)` | | Case | Rule | | --- | --- | | Purchase virtual lot | Sum all `lot.qt` where `lot_p = virtual lot`, with or without `lot_s`. | | Sale virtual lot | Sum all `lot.qt` where `lot_s = virtual lot`, with or without `lot_p`. | | `lot.qt = 0` | Ignored by checks; possible memory of a consumed forecast. | | Negative virtual lot | Allowed to compensate theoretical / executed gap; expected forecast = zero. | | Non-zero orphan `lot.qt` | Forbidden if neither `lot_p` nor `lot_s` is set. | ### BR-PT-LOT-004 - Quantity history and weighing | Element | Rule | | --- | --- | | `lot.qt.hist` | Carries lot quantity states. | | `lot.lot` form | No direct state entry. | | Update | Only through `Do weighing`. | | Virtual lot | No manual packing. | | Virtual packing fields | `lot_qt` and `lot_unit` are not editable. | ### Tolerances > **Gap to Confirm** > Full remaining-tolerance control in `LotQt.add_physical_lots` / > `LotQt.add_physical_lot` still needs confirmation. > | Point | Target Rule | | --- | --- | | Level | Global tolerance on line or contract. | | Transport | No independent tolerance per transport. | | Over-execution | Consumes remaining tolerance. | | Under-execution | Restores remaining tolerance. | ## Developer Section ### Key Fields - 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 / Virtual Lot Creation - 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`. ### Updating `quantity_theorical` - Purchase: `purchase.py`, `Line.write` - Sale: `sale.py`, `SaleLine.write` - Virtual lot target: ```text target_quantity = quantity_theorical - sum(converted physical lots) ``` - 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 < 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. ### Adding Physical Lots - 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. ### Removing Physical Lots - 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. ### Weighing / Quantity States - 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. ### Quantity Counter `quantity` - 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. ### Line Amount - 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 ### Python Guards - 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. ### SQL Diagnostic - 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`. ## Nearby Tests - `modules/purchase_trade/tests/test_module.py` - 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.