docs
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user