Lots and Quantities
Language: en
Mirror page: 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 SummaryA 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. |
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 balanceP1S1T1, P1S1T2, P1S2T3, P1S2T4
Key PointThese 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 Dofinished 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 |
| Minimum 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. |
| Forecast overconsumption |
Allowed after tolerance confirmation: lot.qt stops at zero, but the virtual lot absorbs the whole physical lot. |
lot.qt greater than virtual lot |
Accepted when remaining open forecast exceeds the positive virtual lot because of physical overconsumption. |
| 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
| Point |
Rule |
| Level |
Tolerance is defined on the purchase / sale header. |
| Inheritance |
Lines inherit the header tolerance by default. |
| Transport |
No independent tolerance per transport. |
| Physical lot addition |
The check is performed during Add physical lots. |
| Matched forecast |
If lot_qt carries both lot_p and lot_s, the check applies to both purchase and sale sides. |
| Line excess |
Confirmable warning in English when the projected physical quantity exceeds the current line tolerance. The message names the blocking side: purchase, sale, or both if both checks are triggered successively. |
| Global envelope |
A line excess is accepted after confirmation and consumes the contract-level envelope. |
| Remaining tolerance |
Contract lines are recalculated with inherit_tol = False. |
| Over-execution |
A line exceeding the header tolerance keeps a + tolerance at least equal to its actual excess. |
| Other lines |
Their + tolerance is reduced according to the remaining envelope. |
| Under-execution |
Mechanically restores tolerance available to other lines on the next recalculation. |
| Open matching |
Go to matching may match above the strict open balance if the projected quantity remains within Qt max. |
| Line gauge |
On a line with physical lots, the gauge uses the sum of physical lots; without physical lots, it uses the sum of linked lot.qt. |
| Header gauge |
The purchase/sale gauge is the weighted average of line gauges, weighted by quantity_theorical. |
| Matching gauge |
In Go to matching, the gauge projects already matched + Qt to match against quantity_theorical, with -tol_min / tol_max bounds. |
| Line gauge layout |
On purchase.line / sale.line forms, place the gauge and targeted_qt directly in the main grid, without a colspan="4" subgroup, and use xalign="0" to avoid a double right shift. |
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, tolerance_used, tolerance_min, tolerance_max
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.
- If the line is
created_by_code:
- it comes from a business workflow (
Create contracts, matching, etc.);
Lot.validate does not create an automatic open lot.qt;
- the workflow attaches or splits its source
lot.qt;
- the blocking check applies only after the workflow is stable.
Updating quantity_theorical
- Purchase:
purchase.py, Line.write
- Sale:
sale.py, SaleLine.write
- Virtual lot target:
target_quantity = quantity_theorical - sum(converted physical lots)
- If
target_quantity < 0:
- block with
Please unlink or unmatch lot.
- Free
lot.qt target:
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:
- If no free
lot.qt exists and free_quantity > 0:
- 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:
- Matched or shipped lot:
- 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()
- Trigger rule:
- block inconsistent final states;
- do not block internal transient workflow states;
- use
Lot.skip_quantity_consistency() only around a sequence that restores the invariants afterwards;
- call an explicit final check after the sequence.
- Non-zero orphan
lot.qt block:
- Called after:
quantity_theorical update;
- physical lot creation / deletion;
- matching / unmatching;
- shipping / unshipping;
- matched
Create contracts;
- weighing.
- The
created_by_code case is intentionally excluded from the immediate Lot.validate check:
- the virtual lot is saved before the matched
lot.qt is attached;
- the invariant is checked by the final workflow guard.
SQL Diagnostic
- Script:
- 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.