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

470 lines
20 KiB
Markdown

<!-- Generated from docs_source/business by docs/tools/render_business_docs.py. -->
# 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
<div style="border-left:0.28rem solid #4051b5; background:#f5f7ff; padding:0.85rem 1rem; margin:1rem 0 1.4rem 0; border-radius:0.35rem;"><strong style="display:block; margin-bottom:0.35rem;">Operational Summary</strong><span>A trade line has exactly one virtual lot. The virtual lot carries the global open balance, <code>lot.qt</code> carries the operational forecast, and physical lots consume that forecast.</span></div>
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
<thead>
<tr style="background:#eef3ff;">
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Topic</th>
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Short Rule</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Entered quantity</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>quantity_theorical</code> is the business quantity.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Standard quantity</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>quantity</code> is a read-only technical counter.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Before physical lots</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>quantity</code> follows <code>quantity_theorical</code>.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">After physical lots</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>quantity</code> reflects physical execution.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Open line</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Line amount uses <code>quantity_theorical</code>.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Finished line</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Line amount may switch back to physical execution.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Weight basis</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Purchase and sale may read two different states of the same lot.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Invoicing</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">It chooses states from <code>lot.qt.hist</code>.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Controls</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Invariants are blocked in Python and auditable with SQL.</td>
</tr>
</tbody>
</table>
<pre style="background:#263238; color:#eef7ff; padding:1rem; border-radius:0.35rem; overflow:auto;"><code>quantity_theorical
|
v
virtual lot P1 ---&gt; lot.qt forecast ---&gt; physical lot
^ |
| v
+------ recalculation after consumption</code></pre>
## Consultant Rules
### BR-PT-LOT-001 - Virtual lot / forecast / physical life cycle
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
<thead>
<tr style="background:#eef3ff;">
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Moment</th>
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Business Effect</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Line creation</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">One unique virtual lot is created.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Initialization</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">The virtual lot takes <code>quantity_theorical</code>.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Forecast</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">One open line is created in <code>lot.qt</code>.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Planning</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>lot.qt</code> may be split by sale, matching, transport, or shipment.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Physical add</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">The physical lot consumes a precise <code>lot.qt</code> line.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">After add</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>lot.qt</code> decreases and the virtual lot is recalculated.</td>
</tr>
</tbody>
</table>
<div style="border-left:0.28rem solid #4051b5; background:#f5f7ff; padding:0.85rem 1rem; margin:1rem 0 1.4rem 0; border-radius:0.35rem;"><strong style="display:block; margin-bottom:0.35rem;">Split of an open P1 balance</strong><span><code>P1S1T1</code>, <code>P1S1T2</code>, <code>P1S2T3</code>, <code>P1S2T4</code></span></div>
<div style="border-left:0.28rem solid #4051b5; background:#f5f7ff; padding:0.85rem 1rem; margin:1rem 0 1.4rem 0; border-radius:0.35rem;"><strong style="display:block; margin-bottom:0.35rem;">Key Point</strong><span>These splits do not create several virtual lots. They only describe the forecast allocation of the open balance.</span></div>
### BR-PT-LOT-002 - Contractual quantity, counter quantity, finished line
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
<thead>
<tr style="background:#eef3ff;">
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Situation</th>
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Reference Quantity</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">User entry</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>quantity_theorical</code></td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">No physical lot</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>quantity = quantity_theorical</code></td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Physical lots exist</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>quantity = sum of physical lots</code></td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>finished = False</code></td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Amount based on <code>quantity_theorical</code></td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>finished = True</code></td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Amount based on physical execution</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Weight basis available</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Amount based on the contract <code>wb.qt_type</code> state</td>
</tr>
</tbody>
</table>
<div style="border-left:0.28rem solid #4051b5; background:#f5f7ff; padding:0.85rem 1rem; margin:1rem 0 1.4rem 0; border-radius:0.35rem;"><strong style="display:block; margin-bottom:0.35rem;">What <code>finished</code> Does Not Do</strong><span><code>finished</code> 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.</span></div>
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
<thead>
<tr style="background:#eef3ff;">
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Reading of the Same Physical Lot</th>
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Possible State</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Purchase</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">BL through <code>purchase.purchase.wb.qt_type</code></td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Sale</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">LR or Weight Report through <code>sale.sale.wb.qt_type</code></td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Invoicing</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Independent choice in <code>lot.qt.hist</code></td>
</tr>
</tbody>
</table>
### BR-PT-LOT-003 - Quantity Invariants
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
<thead>
<tr style="background:#eef3ff;">
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Invariant</th>
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Formula</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Conservation</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>sum(physical lots) + virtual lot = quantity_theorical</code></td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Open forecast</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>sum(non-zero lot.qt) = max(virtual lot, 0)</code></td>
</tr>
</tbody>
</table>
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
<thead>
<tr style="background:#eef3ff;">
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Case</th>
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Rule</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Purchase virtual lot</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Sum all <code>lot.qt</code> where <code>lot_p = virtual lot</code>, with or without <code>lot_s</code>.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Sale virtual lot</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Sum all <code>lot.qt</code> where <code>lot_s = virtual lot</code>, with or without <code>lot_p</code>.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>lot.qt = 0</code></td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Ignored by checks; possible memory of a consumed forecast.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Negative virtual lot</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Allowed to compensate theoretical / executed gap; expected forecast = zero.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Non-zero orphan <code>lot.qt</code></td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Forbidden if neither <code>lot_p</code> nor <code>lot_s</code> is set.</td>
</tr>
</tbody>
</table>
### BR-PT-LOT-004 - Quantity history and weighing
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
<thead>
<tr style="background:#eef3ff;">
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Element</th>
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Rule</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>lot.qt.hist</code></td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Carries lot quantity states.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>lot.lot</code> form</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">No direct state entry.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Update</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Only through <code>Do weighing</code>.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Virtual lot</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">No manual packing.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Virtual packing fields</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>lot_qt</code> and <code>lot_unit</code> are not editable.</td>
</tr>
</tbody>
</table>
### Tolerances
<div style="border-left:0.28rem solid #4051b5; background:#f5f7ff; padding:0.85rem 1rem; margin:1rem 0 1.4rem 0; border-radius:0.35rem;"><strong style="display:block; margin-bottom:0.35rem;">Gap to Confirm</strong><span>Full remaining-tolerance control in <code>LotQt.add_physical_lots</code> / <code>LotQt.add_physical_lot</code> still needs confirmation.</span></div>
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
<thead>
<tr style="background:#eef3ff;">
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Point</th>
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Target Rule</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Level</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Global tolerance on line or contract.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Transport</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">No independent tolerance per transport.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Over-execution</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Consumes remaining tolerance.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Under-execution</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Restores remaining tolerance.</td>
</tr>
</tbody>
</table>
## 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:
<pre style="background:#263238; color:#eef7ff; padding:1rem; border-radius:0.35rem; overflow:auto;"><code>target_quantity = quantity_theorical - sum(converted physical lots)</code></pre>
- If `target_quantity < 0`:
- block with `Please unlink or unmatch lot`.
- Free `lot.qt` target:
<pre style="background:#263238; color:#eef7ff; padding:1rem; border-radius:0.35rem; overflow:auto;"><code>free_quantity = target_quantity - sum(already matched or shipped lot.qt)</code></pre>
- 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.