docs
This commit is contained in:
@@ -3,8 +3,14 @@
|
|||||||
Statut: `migration partielle`
|
Statut: `migration partielle`
|
||||||
Dernière mise à jour: `2026-05-13`
|
Dernière mise à jour: `2026-05-13`
|
||||||
|
|
||||||
Ce dossier devient la source de lecture thématique pour les règles business du
|
Ce dossier devient la source de lecture thématique publiée dans le wiki pour les
|
||||||
module `purchase_trade`.
|
règles business du module `purchase_trade`.
|
||||||
|
|
||||||
|
Certaines pages peuvent être générées depuis une source de vérité plus sobre,
|
||||||
|
rangée hors du dossier wiki dans `modules/purchase_trade/docs_source/`. Dans ce
|
||||||
|
cas, la page publiée dans `modules/purchase_trade/docs/` porte un commentaire
|
||||||
|
`Generated from ...` en tête de fichier et ne doit pas être modifiée
|
||||||
|
directement.
|
||||||
|
|
||||||
Chaque page doit rester lisible par deux publics:
|
Chaque page doit rester lisible par deux publics:
|
||||||
|
|
||||||
@@ -31,6 +37,21 @@ Toute modification d'une règle business, d'un statut, d'un champ technique ou
|
|||||||
d'un point de vigilance doit être reportée dans les deux pages au même moment.
|
d'un point de vigilance doit être reportée dans les deux pages au même moment.
|
||||||
Les deux pages doivent indiquer leur page miroir en en-tête.
|
Les deux pages doivent indiquer leur page miroir en en-tête.
|
||||||
|
|
||||||
|
## Convention source / wiki
|
||||||
|
|
||||||
|
Pour les pages qui ont besoin d'une présentation riche dans MkDocs:
|
||||||
|
|
||||||
|
- éditer la source de vérité dans `modules/purchase_trade/docs_source/`;
|
||||||
|
- régénérer la version wiki avec:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python modules/purchase_trade/docs/tools/render_business_docs.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Le rendu wiki privilégie du HTML simple et portable plutôt que des extensions
|
||||||
|
MkDocs optionnelles. Cela évite d'exposer dans le wiki des marqueurs non rendus
|
||||||
|
comme `!!!` ou `:material-...:`.
|
||||||
|
|
||||||
## Convention de rédaction
|
## Convention de rédaction
|
||||||
|
|
||||||
Pour chaque règle durable, utiliser autant que possible ce format:
|
Pour chaque règle durable, utiliser autant que possible ce format:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
# :material-scale-balance: Lots and Quantities
|
<!-- Generated from docs_source/business by docs/tools/render_business_docs.py. -->
|
||||||
|
|
||||||
|
# Lots and Quantities
|
||||||
|
|
||||||
Language: `en`
|
Language: `en`
|
||||||
Mirror page: [lots-and-quantities.md](lots-and-quantities.md)
|
Mirror page: [lots-and-quantities.md](lots-and-quantities.md)
|
||||||
@@ -10,116 +12,287 @@ This page consolidates `BR-PT-LOT-001`, `BR-PT-LOT-002`, and
|
|||||||
Goal: drive quantity rules from a readable business definition, then secure
|
Goal: drive quantity rules from a readable business definition, then secure
|
||||||
them with Python guards and SQL diagnostics.
|
them with Python guards and SQL diagnostics.
|
||||||
|
|
||||||
## :material-bookmark-check: Key Points
|
## Key Points
|
||||||
|
|
||||||
!!! abstract ":material-compass-outline: Operational Summary"
|
<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>
|
||||||
A trade line has exactly one virtual lot. The virtual lot carries the
|
|
||||||
global open balance, `lot.qt` carries the operational forecast, and
|
|
||||||
physical lots consume that forecast.
|
|
||||||
|
|
||||||
| Marker | Topic | Short Rule |
|
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||||
| --- | --- |
|
<thead>
|
||||||
| :material-pencil: | Entered quantity | `quantity_theorical` is the business quantity. |
|
<tr style="background:#eef3ff;">
|
||||||
| :material-counter: | Standard quantity | `quantity` is a read-only technical counter. |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Topic</th>
|
||||||
| :material-timeline-clock-outline: | Before physical lots | `quantity` follows `quantity_theorical`. |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Short Rule</th>
|
||||||
| :material-package-variant-closed: | After physical lots | `quantity` reflects physical execution. |
|
</tr>
|
||||||
| :material-lock-open-variant-outline: | Open line | Line amount uses `quantity_theorical`. |
|
</thead>
|
||||||
| :material-check-circle-outline: | Finished line | Line amount may switch back to physical execution. |
|
<tbody>
|
||||||
| :material-weight: | Weight basis | Purchase and sale may read two different states of the same lot. |
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
| :material-file-document-check-outline: | Invoicing | It chooses states from `lot.qt.hist`. |
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Entered quantity</td>
|
||||||
| :material-shield-check-outline: | Controls | Invariants are blocked in Python and auditable with SQL. |
|
<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>
|
||||||
|
|
||||||
```text
|
<pre style="background:#263238; color:#eef7ff; padding:1rem; border-radius:0.35rem; overflow:auto;"><code>quantity_theorical
|
||||||
quantity_theorical
|
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
virtual lot P1 ---> lot.qt forecast ---> physical lot
|
virtual lot P1 ---> lot.qt forecast ---> physical lot
|
||||||
^ |
|
^ |
|
||||||
| v
|
| v
|
||||||
+------ recalculation after consumption
|
+------ recalculation after consumption</code></pre>
|
||||||
```
|
|
||||||
|
|
||||||
## :material-account-tie: Consultant Rules
|
## Consultant Rules
|
||||||
|
|
||||||
### BR-PT-LOT-001 - Virtual lot / forecast / physical life cycle
|
### BR-PT-LOT-001 - Virtual lot / forecast / physical life cycle
|
||||||
|
|
||||||
| Step | Moment | Business Effect |
|
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||||
| --- | --- |
|
<thead>
|
||||||
| :material-plus-box-outline: | Line creation | One unique virtual lot is created. |
|
<tr style="background:#eef3ff;">
|
||||||
| :material-play-circle-outline: | Initialization | The virtual lot takes `quantity_theorical`. |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Moment</th>
|
||||||
| :material-map-marker-path: | Forecast | One open line is created in `lot.qt`. |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Business Effect</th>
|
||||||
| :material-source-branch: | Planning | `lot.qt` may be split by sale, matching, transport, or shipment. |
|
</tr>
|
||||||
| :material-package-variant: | Physical add | The physical lot consumes a precise `lot.qt` line. |
|
</thead>
|
||||||
| :material-refresh: | After add | `lot.qt` decreases and the virtual lot is recalculated. |
|
<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>
|
||||||
|
|
||||||
!!! example ":material-source-branch: Split of an open P1 balance"
|
<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>
|
||||||
`P1S1T1`, `P1S1T2`, `P1S2T3`, `P1S2T4`
|
|
||||||
|
|
||||||
!!! note ":material-lightbulb-outline: Key Point"
|
<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>
|
||||||
These splits do not create several virtual lots. They only describe the
|
|
||||||
forecast allocation of the open balance.
|
|
||||||
|
|
||||||
### BR-PT-LOT-002 - Contractual quantity, counter quantity, finished line
|
### BR-PT-LOT-002 - Contractual quantity, counter quantity, finished line
|
||||||
|
|
||||||
| Marker | Situation | Reference Quantity |
|
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||||
| --- | --- |
|
<thead>
|
||||||
| :material-pencil: | User entry | `quantity_theorical` |
|
<tr style="background:#eef3ff;">
|
||||||
| :material-package-variant-remove: | No physical lot | `quantity = quantity_theorical` |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Situation</th>
|
||||||
| :material-package-variant-closed: | Physical lots exist | `quantity = sum of physical lots` |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Reference Quantity</th>
|
||||||
| :material-lock-open-variant-outline: | `finished = False` | Amount based on `quantity_theorical` |
|
</tr>
|
||||||
| :material-check-circle-outline: | `finished = True` | Amount based on physical execution |
|
</thead>
|
||||||
| :material-weight: | Weight basis available | Amount based on the contract `wb.qt_type` state |
|
<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>
|
||||||
|
|
||||||
!!! warning ":material-alert-outline: What `finished` Does Not Do"
|
<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>
|
||||||
`finished` does not erase the contractual quantity, delete physical lots,
|
|
||||||
or hide their PnL. It only means that the open balance may be ignored for
|
|
||||||
execution calculations.
|
|
||||||
|
|
||||||
| Marker | Reading of the Same Physical Lot | Possible State |
|
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||||
| --- | --- |
|
<thead>
|
||||||
| :material-cart-arrow-down: | Purchase | BL through `purchase.purchase.wb.qt_type` |
|
<tr style="background:#eef3ff;">
|
||||||
| :material-cart-arrow-up: | Sale | LR or Weight Report through `sale.sale.wb.qt_type` |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Reading of the Same Physical Lot</th>
|
||||||
| :material-file-document-check-outline: | Invoicing | Independent choice in `lot.qt.hist` |
|
<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
|
### BR-PT-LOT-003 - Quantity Invariants
|
||||||
|
|
||||||
| Marker | Invariant | Formula |
|
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||||
| --- | --- |
|
<thead>
|
||||||
| :material-shield-check-outline: | Conservation | `sum(physical lots) + virtual lot = quantity_theorical` |
|
<tr style="background:#eef3ff;">
|
||||||
| :material-chart-timeline-variant: | Open forecast | `sum(non-zero lot.qt) = max(virtual lot, 0)` |
|
<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>
|
||||||
|
|
||||||
| Marker | Case | Rule |
|
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||||
| --- | --- |
|
<thead>
|
||||||
| :material-cart-arrow-down: | Purchase virtual lot | Sum all `lot.qt` where `lot_p = virtual lot`, with or without `lot_s`. |
|
<tr style="background:#eef3ff;">
|
||||||
| :material-cart-arrow-up: | Sale virtual lot | Sum all `lot.qt` where `lot_s = virtual lot`, with or without `lot_p`. |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Case</th>
|
||||||
| :material-numeric-0-box-outline: | `lot.qt = 0` | Ignored by checks; possible memory of a consumed forecast. |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Rule</th>
|
||||||
| :material-minus-circle-outline: | Negative virtual lot | Allowed to compensate theoretical / executed gap; expected forecast = zero. |
|
</tr>
|
||||||
| :material-alert-octagon-outline: | Non-zero orphan `lot.qt` | Forbidden if neither `lot_p` nor `lot_s` is set. |
|
</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
|
### BR-PT-LOT-004 - Quantity history and weighing
|
||||||
|
|
||||||
| Marker | Element | Rule |
|
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||||
| --- | --- |
|
<thead>
|
||||||
| :material-history: | `lot.qt.hist` | Carries lot quantity states. |
|
<tr style="background:#eef3ff;">
|
||||||
| :material-form-textbox: | `lot.lot` form | No direct state entry. |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Element</th>
|
||||||
| :material-scale: | Update | Only through `Do weighing`. |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Rule</th>
|
||||||
| :material-cloud-outline: | Virtual lot | No manual packing. |
|
</tr>
|
||||||
| :material-lock-outline: | Virtual packing fields | `lot_qt` and `lot_unit` are not editable. |
|
</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
|
### Tolerances
|
||||||
|
|
||||||
!!! warning ":material-alert-outline: Gap to Confirm"
|
<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>
|
||||||
Full remaining-tolerance control in `LotQt.add_physical_lots` /
|
|
||||||
`LotQt.add_physical_lot` still needs confirmation.
|
|
||||||
|
|
||||||
| Marker | Point | Target Rule |
|
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||||
| --- | --- |
|
<thead>
|
||||||
| :material-target: | Level | Global tolerance on line or contract. |
|
<tr style="background:#eef3ff;">
|
||||||
| :material-truck-outline: | Transport | No independent tolerance per transport. |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Point</th>
|
||||||
| :material-arrow-up-bold-outline: | Over-execution | Consumes remaining tolerance. |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Target Rule</th>
|
||||||
| :material-arrow-down-bold-outline: | Under-execution | Restores remaining tolerance. |
|
</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>
|
||||||
|
|
||||||
## :material-code-braces: Developer Section
|
## Developer Section
|
||||||
|
|
||||||
### Key Fields
|
### Key Fields
|
||||||
|
|
||||||
@@ -166,17 +339,13 @@ quantity_theorical
|
|||||||
- Sale: `sale.py`, `SaleLine.write`
|
- Sale: `sale.py`, `SaleLine.write`
|
||||||
- Virtual lot target:
|
- Virtual lot target:
|
||||||
|
|
||||||
```text
|
<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>
|
||||||
target_quantity = quantity_theorical - sum(converted physical lots)
|
|
||||||
```
|
|
||||||
|
|
||||||
- If `target_quantity < 0`:
|
- If `target_quantity < 0`:
|
||||||
- block with `Please unlink or unmatch lot`.
|
- block with `Please unlink or unmatch lot`.
|
||||||
- Free `lot.qt` target:
|
- Free `lot.qt` target:
|
||||||
|
|
||||||
```text
|
<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>
|
||||||
free_quantity = target_quantity - sum(already matched or shipped lot.qt)
|
|
||||||
```
|
|
||||||
|
|
||||||
- If `free_quantity < 0`:
|
- If `free_quantity < 0`:
|
||||||
- block with `Please unlink or unmatch lot`.
|
- block with `Please unlink or unmatch lot`.
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
# :material-scale-balance: Lots et quantités
|
<!-- Generated from docs_source/business by docs/tools/render_business_docs.py. -->
|
||||||
|
|
||||||
|
# Lots et quantités
|
||||||
|
|
||||||
Langue : `fr`
|
Langue : `fr`
|
||||||
Page miroir : [lots-and-quantities.en.md](lots-and-quantities.en.md)
|
Page miroir : [lots-and-quantities.en.md](lots-and-quantities.en.md)
|
||||||
@@ -9,116 +11,287 @@ 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,
|
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.
|
puis les sécuriser par des checks Python et des diagnostics SQL.
|
||||||
|
|
||||||
## :material-bookmark-check: À retenir
|
## À retenir
|
||||||
|
|
||||||
!!! abstract ":material-compass-outline: Résumé opérationnel"
|
<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;">Résumé opérationnel</strong><span>Une ligne trade possède un seul lot virtuel. Le lot virtuel porte le solde ouvert global, <code>lot.qt</code> porte le forecast opérationnel, et les lots physiques consomment ce forecast.</span></div>
|
||||||
Une ligne trade possède un seul lot virtuel. Le lot virtuel porte le solde
|
|
||||||
ouvert global, `lot.qt` porte le forecast opérationnel, et les lots
|
|
||||||
physiques consomment ce forecast.
|
|
||||||
|
|
||||||
| Repère | Sujet | Règle courte |
|
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||||
| --- | --- |
|
<thead>
|
||||||
| :material-pencil: | Quantité saisie | `quantity_theorical` est la quantité métier. |
|
<tr style="background:#eef3ff;">
|
||||||
| :material-counter: | Quantité standard | `quantity` est un compteur technique non éditable. |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Sujet</th>
|
||||||
| :material-timeline-clock-outline: | Avant physique | `quantity` suit `quantity_theorical`. |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Règle courte</th>
|
||||||
| :material-package-variant-closed: | Après physique | `quantity` reflète l'exécuté physique. |
|
</tr>
|
||||||
| :material-lock-open-variant-outline: | Ligne non finie | Le montant de ligne utilise `quantity_theorical`. |
|
</thead>
|
||||||
| :material-check-circle-outline: | Ligne finie | Le montant peut revenir à l'exécuté physique. |
|
<tbody>
|
||||||
| :material-weight: | Weight basis | Achat et vente peuvent lire deux états différents du même lot. |
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
| :material-file-document-check-outline: | Facturation | Elle choisit ses états dans `lot.qt.hist`. |
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Quantité saisie</td>
|
||||||
| :material-shield-check-outline: | Contrôles | Invariants bloqués en Python et auditables en SQL. |
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>quantity_theorical</code> est la quantité métier.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Quantité standard</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>quantity</code> est un compteur technique non éditable.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Avant physique</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>quantity</code> suit <code>quantity_theorical</code>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Après physique</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>quantity</code> reflète l'exécuté physique.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Ligne non finie</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Le montant de ligne utilise <code>quantity_theorical</code>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Ligne finie</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Le montant peut revenir à l'exécuté physique.</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;">Achat et vente peuvent lire deux états différents du même lot.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Facturation</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Elle choisit ses états dans <code>lot.qt.hist</code>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Contrôles</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Invariants bloqués en Python et auditables en SQL.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
```text
|
<pre style="background:#263238; color:#eef7ff; padding:1rem; border-radius:0.35rem; overflow:auto;"><code>quantity_theorical
|
||||||
quantity_theorical
|
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
lot virtuel P1 ---> lot.qt forecast ---> lot physique
|
lot virtuel P1 ---> lot.qt forecast ---> lot physique
|
||||||
^ |
|
^ |
|
||||||
| v
|
| v
|
||||||
+------ recalcul après consommation
|
+------ recalcul après consommation</code></pre>
|
||||||
```
|
|
||||||
|
|
||||||
## :material-account-tie: Règles consultant
|
## Règles consultant
|
||||||
|
|
||||||
### BR-PT-LOT-001 - Cycle de vie lot virtuel / forecast / physique
|
### BR-PT-LOT-001 - Cycle de vie lot virtuel / forecast / physique
|
||||||
|
|
||||||
| Étape | Moment | Effet métier |
|
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||||
| --- | --- |
|
<thead>
|
||||||
| :material-plus-box-outline: | Création de ligne | Création d'un lot virtuel unique. |
|
<tr style="background:#eef3ff;">
|
||||||
| :material-play-circle-outline: | Initialisation | Le lot virtuel reprend `quantity_theorical`. |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Moment</th>
|
||||||
| :material-map-marker-path: | Forecast | Une ligne ouverte est créée dans `lot.qt`. |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Effet métier</th>
|
||||||
| :material-source-branch: | Planification | `lot.qt` peut être subdivisé par vente, matching, transport ou shipment. |
|
</tr>
|
||||||
| :material-package-variant: | Ajout physique | Le lot physique consomme une ligne `lot.qt` précise. |
|
</thead>
|
||||||
| :material-refresh: | Après ajout | `lot.qt` diminue et le lot virtuel est recalculé. |
|
<tbody>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Création de ligne</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Création d'un lot virtuel unique.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Initialisation</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Le lot virtuel reprend <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;">Une ligne ouverte est créée dans <code>lot.qt</code>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Planification</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>lot.qt</code> peut être subdivisé par vente, matching, transport ou shipment.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Ajout physique</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Le lot physique consomme une ligne <code>lot.qt</code> précise.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Après ajout</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>lot.qt</code> diminue et le lot virtuel est recalculé.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
!!! example ":material-source-branch: Découpage d'un solde P1"
|
<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;">Découpage d'un solde P1</strong><span><code>P1S1T1</code>, <code>P1S1T2</code>, <code>P1S2T3</code>, <code>P1S2T4</code></span></div>
|
||||||
`P1S1T1`, `P1S1T2`, `P1S2T3`, `P1S2T4`
|
|
||||||
|
|
||||||
!!! note ":material-lightbulb-outline: Point clé"
|
<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;">Point clé</strong><span>Ces découpages ne créent pas plusieurs lots virtuels. Ils décrivent seulement la répartition prévisionnelle du solde ouvert.</span></div>
|
||||||
Ces découpages ne créent pas plusieurs lots virtuels. Ils décrivent
|
|
||||||
seulement la répartition prévisionnelle du solde ouvert.
|
|
||||||
|
|
||||||
### BR-PT-LOT-002 - Quantité contractuelle, compteur, ligne finie
|
### BR-PT-LOT-002 - Quantité contractuelle, compteur, ligne finie
|
||||||
|
|
||||||
| Repère | Situation | Quantité de référence |
|
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||||
| --- | --- |
|
<thead>
|
||||||
| :material-pencil: | Saisie utilisateur | `quantity_theorical` |
|
<tr style="background:#eef3ff;">
|
||||||
| :material-package-variant-remove: | Aucun lot physique | `quantity = quantity_theorical` |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Situation</th>
|
||||||
| :material-package-variant-closed: | Lots physiques présents | `quantity = somme des lots physiques` |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Quantité de référence</th>
|
||||||
| :material-lock-open-variant-outline: | `finished = False` | Montant basé sur `quantity_theorical` |
|
</tr>
|
||||||
| :material-check-circle-outline: | `finished = True` | Montant basé sur l'exécuté physique |
|
</thead>
|
||||||
| :material-weight: | Weight basis disponible | Montant basé sur l'état `wb.qt_type` du contrat |
|
<tbody>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Saisie utilisateur</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;">Aucun lot physique</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;">Lots physiques présents</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>quantity = somme des lots physiques</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;">Montant basé sur <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;">Montant basé sur l'exécuté physique</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Weight basis disponible</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Montant basé sur l'état <code>wb.qt_type</code> du contrat</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
!!! warning ":material-alert-outline: Ce que `finished` ne fait pas"
|
<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;">Ce que <code>finished</code> ne fait pas</strong><span><code>finished</code> n'efface pas la quantité contractuelle, ne supprime pas les lots physiques et ne masque pas leur PnL. Il signifie seulement que le reliquat ouvert peut être ignoré pour les calculs d'exécution.</span></div>
|
||||||
`finished` n'efface pas la quantité contractuelle, ne supprime pas les lots
|
|
||||||
physiques et ne masque pas leur PnL. Il signifie seulement que le reliquat
|
|
||||||
ouvert peut être ignoré pour les calculs d'exécution.
|
|
||||||
|
|
||||||
| Repère | Lecture du même lot physique | État possible |
|
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||||
| --- | --- |
|
<thead>
|
||||||
| :material-cart-arrow-down: | Achat | BL via `purchase.purchase.wb.qt_type` |
|
<tr style="background:#eef3ff;">
|
||||||
| :material-cart-arrow-up: | Vente | LR ou Weight Report via `sale.sale.wb.qt_type` |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Lecture du même lot physique</th>
|
||||||
| :material-file-document-check-outline: | Facturation | Choix indépendant dans `lot.qt.hist` |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">État possible</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Achat</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">BL via <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;">Vente</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">LR ou Weight Report via <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;">Facturation</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Choix indépendant dans <code>lot.qt.hist</code></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
### BR-PT-LOT-003 - Invariants de quantité
|
### BR-PT-LOT-003 - Invariants de quantité
|
||||||
|
|
||||||
| Repère | Invariant | Formule |
|
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||||
| --- | --- |
|
<thead>
|
||||||
| :material-shield-check-outline: | Conservation | `somme(lots physiques) + lot virtuel = quantity_theorical` |
|
<tr style="background:#eef3ff;">
|
||||||
| :material-chart-timeline-variant: | Forecast ouvert | `somme(lot.qt non zéro) = max(lot virtuel, 0)` |
|
<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;">Formule</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>somme(lots physiques) + lot virtuel = quantity_theorical</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Forecast ouvert</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>somme(lot.qt non zéro) = max(lot virtuel, 0)</code></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
| Repère | Cas | Règle |
|
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||||
| --- | --- |
|
<thead>
|
||||||
| :material-cart-arrow-down: | Lot virtuel achat | Sommer tous les `lot.qt` où `lot_p = lot virtuel`, avec ou sans `lot_s`. |
|
<tr style="background:#eef3ff;">
|
||||||
| :material-cart-arrow-up: | Lot virtuel vente | Sommer tous les `lot.qt` où `lot_s = lot virtuel`, avec ou sans `lot_p`. |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Cas</th>
|
||||||
| :material-numeric-0-box-outline: | `lot.qt = 0` | Ignoré par les checks ; mémoire possible d'une prévision vidée. |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Règle</th>
|
||||||
| :material-minus-circle-outline: | Lot virtuel négatif | Autorisé pour compenser l'écart théorique / exécuté ; forecast attendu = zéro. |
|
</tr>
|
||||||
| :material-alert-octagon-outline: | `lot.qt` non zéro orphelin | Interdit si ni `lot_p` ni `lot_s` n'est renseigné. |
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Lot virtuel achat</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Sommer tous les <code>lot.qt</code> où <code>lot_p = lot virtuel</code>, avec ou sans <code>lot_s</code>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Lot virtuel vente</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Sommer tous les <code>lot.qt</code> où <code>lot_s = lot virtuel</code>, avec ou sans <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;">Ignoré par les checks ; mémoire possible d'une prévision vidée.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Lot virtuel négatif</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Autorisé pour compenser l'écart théorique / exécuté ; forecast attendu = zéro.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>lot.qt</code> non zéro orphelin</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Interdit si ni <code>lot_p</code> ni <code>lot_s</code> n'est renseigné.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
### BR-PT-LOT-004 - Historique de quantité et weighing
|
### BR-PT-LOT-004 - Historique de quantité et weighing
|
||||||
|
|
||||||
| Repère | Élément | Règle |
|
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||||
| --- | --- |
|
<thead>
|
||||||
| :material-history: | `lot.qt.hist` | Porte les états de quantité d'un lot. |
|
<tr style="background:#eef3ff;">
|
||||||
| :material-form-textbox: | Fiche `lot.lot` | Pas de saisie directe des états. |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Élément</th>
|
||||||
| :material-scale: | Modification | Uniquement via `Do weighing`. |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Règle</th>
|
||||||
| :material-cloud-outline: | Lot virtuel | Pas de packing manuel. |
|
</tr>
|
||||||
| :material-lock-outline: | Champs packing virtuel | `lot_qt` et `lot_unit` non éditables. |
|
</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;">Porte les états de quantité d'un lot.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Fiche <code>lot.lot</code></td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Pas de saisie directe des états.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Modification</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Uniquement via <code>Do weighing</code>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Lot virtuel</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Pas de packing manuel.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Champs packing virtuel</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>lot_qt</code> et <code>lot_unit</code> non éditables.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
### Tolérances
|
### Tolérances
|
||||||
|
|
||||||
!!! warning ":material-alert-outline: Gap à confirmer"
|
<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 à confirmer</strong><span>Le contrôle complet de tolérance restante dans <code>LotQt.add_physical_lots</code> / <code>LotQt.add_physical_lot</code> reste à confirmer.</span></div>
|
||||||
Le contrôle complet de tolérance restante dans `LotQt.add_physical_lots` /
|
|
||||||
`LotQt.add_physical_lot` reste à confirmer.
|
|
||||||
|
|
||||||
| Repère | Point | Règle cible |
|
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||||
| --- | --- |
|
<thead>
|
||||||
| :material-target: | Niveau | Tolérance globale sur ligne ou contrat. |
|
<tr style="background:#eef3ff;">
|
||||||
| :material-truck-outline: | Transport | Pas de tolérance indépendante par transport. |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Point</th>
|
||||||
| :material-arrow-up-bold-outline: | Surconsommation | Consomme la tolérance restante. |
|
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Règle cible</th>
|
||||||
| :material-arrow-down-bold-outline: | Sous-consommation | Restitue de la tolérance restante. |
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Niveau</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Tolérance globale sur ligne ou contrat.</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;">Pas de tolérance indépendante par transport.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Surconsommation</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Consomme la tolérance restante.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Sous-consommation</td>
|
||||||
|
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Restitue de la tolérance restante.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
## :material-code-braces: Section développeur
|
## Section développeur
|
||||||
|
|
||||||
### Champs clés
|
### Champs clés
|
||||||
|
|
||||||
@@ -165,17 +338,13 @@ quantity_theorical
|
|||||||
- Vente : `sale.py`, `SaleLine.write`
|
- Vente : `sale.py`, `SaleLine.write`
|
||||||
- Cible lot virtuel :
|
- Cible lot virtuel :
|
||||||
|
|
||||||
```text
|
<pre style="background:#263238; color:#eef7ff; padding:1rem; border-radius:0.35rem; overflow:auto;"><code>target_quantity = quantity_theorical - somme(lots physiques convertis)</code></pre>
|
||||||
target_quantity = quantity_theorical - somme(lots physiques convertis)
|
|
||||||
```
|
|
||||||
|
|
||||||
- Si `target_quantity < 0` :
|
- Si `target_quantity < 0` :
|
||||||
- blocage : `Please unlink or unmatch lot`.
|
- blocage : `Please unlink or unmatch lot`.
|
||||||
- Cible `lot.qt` libre :
|
- Cible `lot.qt` libre :
|
||||||
|
|
||||||
```text
|
<pre style="background:#263238; color:#eef7ff; padding:1rem; border-radius:0.35rem; overflow:auto;"><code>free_quantity = target_quantity - somme(lot.qt déjà matchés ou shippés)</code></pre>
|
||||||
free_quantity = target_quantity - somme(lot.qt déjà matchés ou shippés)
|
|
||||||
```
|
|
||||||
|
|
||||||
- Si `free_quantity < 0` :
|
- Si `free_quantity < 0` :
|
||||||
- blocage : `Please unlink or unmatch lot`.
|
- blocage : `Please unlink or unmatch lot`.
|
||||||
|
|||||||
275
modules/purchase_trade/docs/tools/render_business_docs.py
Normal file
275
modules/purchase_trade/docs/tools/render_business_docs.py
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
"""Render purchase_trade business docs for the MkDocs wiki.
|
||||||
|
|
||||||
|
The files in ``docs_source/business`` are the source of truth. The generated
|
||||||
|
files are written back to ``docs/business`` because the wiki already points to
|
||||||
|
``docs``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import html
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
BUSINESS = ROOT / "business"
|
||||||
|
SOURCE = ROOT.parent / "docs_source" / "business"
|
||||||
|
TARGETS = (
|
||||||
|
"lots-and-quantities.md",
|
||||||
|
"lots-and-quantities.en.md",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
MATERIAL_ICON = re.compile(r":material-[a-z0-9-]+:\s*")
|
||||||
|
|
||||||
|
|
||||||
|
def clean_inline(text: str) -> str:
|
||||||
|
text = MATERIAL_ICON.sub("", text)
|
||||||
|
text = re.sub(r"\s{2,}", " ", text)
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def split_table_row(line: str) -> list[str]:
|
||||||
|
return [cell.strip() for cell in line.strip().strip("|").split("|")]
|
||||||
|
|
||||||
|
|
||||||
|
def is_separator_row(line: str) -> bool:
|
||||||
|
cells = split_table_row(line)
|
||||||
|
return bool(cells) and all(re.fullmatch(r":?-{3,}:?", cell) for cell in cells)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_table(lines: list[str], index: int) -> tuple[list[str], int]:
|
||||||
|
rows = []
|
||||||
|
while index < len(lines) and lines[index].lstrip().startswith("|"):
|
||||||
|
rows.append(lines[index])
|
||||||
|
index += 1
|
||||||
|
if len(rows) < 2 or not is_separator_row(rows[1]):
|
||||||
|
return rows, index
|
||||||
|
|
||||||
|
table = [split_table_row(row) for row in rows]
|
||||||
|
header = [clean_inline(cell) for cell in table[0]]
|
||||||
|
drop_first = header and header[0].lower() in {
|
||||||
|
"repere",
|
||||||
|
"repère",
|
||||||
|
"marker",
|
||||||
|
"etape",
|
||||||
|
"étape",
|
||||||
|
"step",
|
||||||
|
}
|
||||||
|
if drop_first:
|
||||||
|
table = [row[1:] for row in table]
|
||||||
|
|
||||||
|
cleaned = []
|
||||||
|
for row_index, row in enumerate(table):
|
||||||
|
if row_index == 1 and is_separator_row("| " + " | ".join(row) + " |"):
|
||||||
|
row = ["---"] * len(table[0])
|
||||||
|
cleaned.append("| " + " | ".join(clean_inline(cell) for cell in row) + " |")
|
||||||
|
return cleaned, index
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_source(text: str) -> str:
|
||||||
|
lines = text.splitlines()
|
||||||
|
out: list[str] = []
|
||||||
|
i = 0
|
||||||
|
in_code = False
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i]
|
||||||
|
|
||||||
|
if line.startswith("```"):
|
||||||
|
in_code = not in_code
|
||||||
|
out.append(line)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_code:
|
||||||
|
out.append(line)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.startswith("!!! "):
|
||||||
|
match = re.match(r"!!!\s+\w+\s+\"(.+)\"", line)
|
||||||
|
title = clean_inline(match.group(1) if match else "Note")
|
||||||
|
out.append(f"> **{title}**")
|
||||||
|
i += 1
|
||||||
|
while i < len(lines) and (
|
||||||
|
lines[i].startswith(" ") or not lines[i].strip()
|
||||||
|
):
|
||||||
|
if lines[i].strip():
|
||||||
|
out.append("> " + clean_inline(lines[i].strip()))
|
||||||
|
else:
|
||||||
|
out.append(">")
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.lstrip().startswith("|"):
|
||||||
|
table, i = normalize_table(lines, i)
|
||||||
|
if len(table) > 1 and is_separator_row(table[1]):
|
||||||
|
header_len = len(split_table_row(table[0]))
|
||||||
|
table[1] = "| " + " | ".join(["---"] * header_len) + " |"
|
||||||
|
out.extend(table)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not line.strip():
|
||||||
|
out.append("")
|
||||||
|
elif line.startswith("#"):
|
||||||
|
out.append(clean_inline(line))
|
||||||
|
else:
|
||||||
|
leading = re.match(r"^\s*", line).group(0)
|
||||||
|
out.append(leading + clean_inline(line.strip()))
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return "\n".join(out).strip() + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def render_inline(text: str) -> str:
|
||||||
|
placeholders: list[str] = []
|
||||||
|
|
||||||
|
def keep_link(match: re.Match[str]) -> str:
|
||||||
|
placeholders.append(
|
||||||
|
f'<a href="{html.escape(match.group(2), quote=True)}">{html.escape(match.group(1))}</a>'
|
||||||
|
)
|
||||||
|
return f"@@LINK{len(placeholders) - 1}@@"
|
||||||
|
|
||||||
|
text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", keep_link, text)
|
||||||
|
text = html.escape(text)
|
||||||
|
text = re.sub(r"`([^`]+)`", r"<code>\1</code>", text)
|
||||||
|
for idx, value in enumerate(placeholders):
|
||||||
|
text = text.replace(f"@@LINK{idx}@@", value)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def render_table(rows: list[str]) -> str:
|
||||||
|
parsed = [split_table_row(row) for row in rows]
|
||||||
|
header = parsed[0]
|
||||||
|
body = parsed[2:]
|
||||||
|
out = [
|
||||||
|
'<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">',
|
||||||
|
"<thead>",
|
||||||
|
'<tr style="background:#eef3ff;">',
|
||||||
|
]
|
||||||
|
for cell in header:
|
||||||
|
out.append(
|
||||||
|
'<th style="text-align:left; padding:0.55rem 0.7rem; '
|
||||||
|
'border-bottom:2px solid #4051b5;">'
|
||||||
|
f"{render_inline(cell)}</th>"
|
||||||
|
)
|
||||||
|
out.extend(["</tr>", "</thead>", "<tbody>"])
|
||||||
|
for row in body:
|
||||||
|
out.append('<tr style="border-bottom:1px solid #e0e0e0;">')
|
||||||
|
for cell in row:
|
||||||
|
out.append(
|
||||||
|
'<td style="vertical-align:top; padding:0.5rem 0.7rem;">'
|
||||||
|
f"{render_inline(cell)}</td>"
|
||||||
|
)
|
||||||
|
out.append("</tr>")
|
||||||
|
out.extend(["</tbody>", "</table>"])
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def render_callout(lines: list[str]) -> str:
|
||||||
|
title = ""
|
||||||
|
body = []
|
||||||
|
for line in lines:
|
||||||
|
content = line[1:].strip()
|
||||||
|
if content.startswith("**") and content.endswith("**") and not title:
|
||||||
|
title = content.strip("*")
|
||||||
|
elif content:
|
||||||
|
body.append(content)
|
||||||
|
title_html = (
|
||||||
|
f'<strong style="display:block; margin-bottom:0.35rem;">{render_inline(title)}</strong>'
|
||||||
|
if title
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
body_html = " ".join(render_inline(line) for line in body)
|
||||||
|
return (
|
||||||
|
'<div style="border-left:0.28rem solid #4051b5; background:#f5f7ff; '
|
||||||
|
'padding:0.85rem 1rem; margin:1rem 0 1.4rem 0; border-radius:0.35rem;">'
|
||||||
|
f"{title_html}<span>{body_html}</span></div>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def render_source(text: str) -> str:
|
||||||
|
lines = text.splitlines()
|
||||||
|
out = [
|
||||||
|
"<!-- Generated from docs_source/business by docs/tools/render_business_docs.py. -->",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
in_code = False
|
||||||
|
code_lines: list[str] = []
|
||||||
|
i = 0
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i]
|
||||||
|
|
||||||
|
if line.startswith("```"):
|
||||||
|
if in_code:
|
||||||
|
out.append(
|
||||||
|
'<pre style="background:#263238; color:#eef7ff; padding:1rem; '
|
||||||
|
'border-radius:0.35rem; overflow:auto;"><code>'
|
||||||
|
+ html.escape("\n".join(code_lines))
|
||||||
|
+ "</code></pre>"
|
||||||
|
)
|
||||||
|
code_lines = []
|
||||||
|
in_code = False
|
||||||
|
else:
|
||||||
|
in_code = True
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if in_code:
|
||||||
|
code_lines.append(line)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.startswith(">"):
|
||||||
|
block = []
|
||||||
|
while i < len(lines) and lines[i].startswith(">"):
|
||||||
|
if not lines[i][1:].strip():
|
||||||
|
i += 1
|
||||||
|
break
|
||||||
|
block.append(lines[i])
|
||||||
|
i += 1
|
||||||
|
out.append(render_callout(block))
|
||||||
|
out.append("")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.lstrip().startswith("|"):
|
||||||
|
table = []
|
||||||
|
while i < len(lines) and lines[i].lstrip().startswith("|"):
|
||||||
|
table.append(lines[i])
|
||||||
|
i += 1
|
||||||
|
if len(table) > 1 and is_separator_row(table[1]):
|
||||||
|
out.append(render_table(table))
|
||||||
|
else:
|
||||||
|
out.extend(table)
|
||||||
|
continue
|
||||||
|
|
||||||
|
out.append(line)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return "\n".join(out).strip() + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def render_all(normalize: bool = False) -> None:
|
||||||
|
SOURCE.mkdir(parents=True, exist_ok=True)
|
||||||
|
for name in TARGETS:
|
||||||
|
source = SOURCE / name
|
||||||
|
target = BUSINESS / name
|
||||||
|
if normalize:
|
||||||
|
source.write_text(normalize_source(source.read_text(encoding="utf-8")), encoding="utf-8")
|
||||||
|
target.write_text(render_source(source.read_text(encoding="utf-8")), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument(
|
||||||
|
"--normalize-source",
|
||||||
|
action="store_true",
|
||||||
|
help="Clean existing source files from wiki-only syntax before rendering.",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
render_all(normalize=args.normalize_source)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
# Lots and Quantities
|
||||||
|
|
||||||
|
Language: `en`
|
||||||
|
Mirror page: [lots-and-quantities.md](lots-and-quantities.md)
|
||||||
|
Status: `partial migration`
|
||||||
|
Last code verification: `2026-05-13`
|
||||||
|
|
||||||
|
This page consolidates `BR-PT-LOT-001`, `BR-PT-LOT-002`, and
|
||||||
|
`BR-PT-LOT-003`.
|
||||||
|
Goal: drive quantity rules from a readable business definition, then secure
|
||||||
|
them with Python guards and SQL diagnostics.
|
||||||
|
|
||||||
|
## Key Points
|
||||||
|
|
||||||
|
> **Operational Summary**
|
||||||
|
> A trade line has exactly one virtual lot. The virtual lot carries the
|
||||||
|
> global open balance, `lot.qt` carries the operational forecast, and
|
||||||
|
> physical lots consume that forecast.
|
||||||
|
>
|
||||||
|
| Topic | Short Rule |
|
||||||
|
| --- | --- |
|
||||||
|
| Entered quantity | `quantity_theorical` is the business quantity. |
|
||||||
|
| Standard quantity | `quantity` is a read-only technical counter. |
|
||||||
|
| Before physical lots | `quantity` follows `quantity_theorical`. |
|
||||||
|
| After physical lots | `quantity` reflects physical execution. |
|
||||||
|
| Open line | Line amount uses `quantity_theorical`. |
|
||||||
|
| Finished line | Line amount may switch back to physical execution. |
|
||||||
|
| Weight basis | Purchase and sale may read two different states of the same lot. |
|
||||||
|
| Invoicing | It chooses states from `lot.qt.hist`. |
|
||||||
|
| Controls | Invariants are blocked in Python and auditable with SQL. |
|
||||||
|
|
||||||
|
```text
|
||||||
|
quantity_theorical
|
||||||
|
|
|
||||||
|
v
|
||||||
|
virtual lot P1 ---> lot.qt forecast ---> physical lot
|
||||||
|
^ |
|
||||||
|
| v
|
||||||
|
+------ recalculation after consumption
|
||||||
|
```
|
||||||
|
|
||||||
|
## Consultant Rules
|
||||||
|
|
||||||
|
### BR-PT-LOT-001 - Virtual lot / forecast / physical life cycle
|
||||||
|
|
||||||
|
| Moment | Business Effect |
|
||||||
|
| --- | --- |
|
||||||
|
| Line creation | One unique virtual lot is created. |
|
||||||
|
| Initialization | The virtual lot takes `quantity_theorical`. |
|
||||||
|
| Forecast | One open line is created in `lot.qt`. |
|
||||||
|
| Planning | `lot.qt` may be split by sale, matching, transport, or shipment. |
|
||||||
|
| Physical add | The physical lot consumes a precise `lot.qt` line. |
|
||||||
|
| After add | `lot.qt` decreases and the virtual lot is recalculated. |
|
||||||
|
|
||||||
|
> **Split of an open P1 balance**
|
||||||
|
> `P1S1T1`, `P1S1T2`, `P1S2T3`, `P1S2T4`
|
||||||
|
>
|
||||||
|
> **Key Point**
|
||||||
|
> These splits do not create several virtual lots. They only describe the
|
||||||
|
> forecast allocation of the open balance.
|
||||||
|
>
|
||||||
|
### BR-PT-LOT-002 - Contractual quantity, counter quantity, finished line
|
||||||
|
|
||||||
|
| Situation | Reference Quantity |
|
||||||
|
| --- | --- |
|
||||||
|
| User entry | `quantity_theorical` |
|
||||||
|
| No physical lot | `quantity = quantity_theorical` |
|
||||||
|
| Physical lots exist | `quantity = sum of physical lots` |
|
||||||
|
| `finished = False` | Amount based on `quantity_theorical` |
|
||||||
|
| `finished = True` | Amount based on physical execution |
|
||||||
|
| Weight basis available | Amount based on the contract `wb.qt_type` state |
|
||||||
|
|
||||||
|
> **What `finished` Does Not Do**
|
||||||
|
> `finished` does not erase the contractual quantity, delete physical lots,
|
||||||
|
> or hide their PnL. It only means that the open balance may be ignored for
|
||||||
|
> execution calculations.
|
||||||
|
>
|
||||||
|
| Reading of the Same Physical Lot | Possible State |
|
||||||
|
| --- | --- |
|
||||||
|
| Purchase | BL through `purchase.purchase.wb.qt_type` |
|
||||||
|
| Sale | LR or Weight Report through `sale.sale.wb.qt_type` |
|
||||||
|
| Invoicing | Independent choice in `lot.qt.hist` |
|
||||||
|
|
||||||
|
### BR-PT-LOT-003 - Quantity Invariants
|
||||||
|
|
||||||
|
| Invariant | Formula |
|
||||||
|
| --- | --- |
|
||||||
|
| Conservation | `sum(physical lots) + virtual lot = quantity_theorical` |
|
||||||
|
| Open forecast | `sum(non-zero lot.qt) = max(virtual lot, 0)` |
|
||||||
|
|
||||||
|
| Case | Rule |
|
||||||
|
| --- | --- |
|
||||||
|
| Purchase virtual lot | Sum all `lot.qt` where `lot_p = virtual lot`, with or without `lot_s`. |
|
||||||
|
| Sale virtual lot | Sum all `lot.qt` where `lot_s = virtual lot`, with or without `lot_p`. |
|
||||||
|
| `lot.qt = 0` | Ignored by checks; possible memory of a consumed forecast. |
|
||||||
|
| Negative virtual lot | Allowed to compensate theoretical / executed gap; expected forecast = zero. |
|
||||||
|
| Non-zero orphan `lot.qt` | Forbidden if neither `lot_p` nor `lot_s` is set. |
|
||||||
|
|
||||||
|
### BR-PT-LOT-004 - Quantity history and weighing
|
||||||
|
|
||||||
|
| Element | Rule |
|
||||||
|
| --- | --- |
|
||||||
|
| `lot.qt.hist` | Carries lot quantity states. |
|
||||||
|
| `lot.lot` form | No direct state entry. |
|
||||||
|
| Update | Only through `Do weighing`. |
|
||||||
|
| Virtual lot | No manual packing. |
|
||||||
|
| Virtual packing fields | `lot_qt` and `lot_unit` are not editable. |
|
||||||
|
|
||||||
|
### Tolerances
|
||||||
|
|
||||||
|
> **Gap to Confirm**
|
||||||
|
> Full remaining-tolerance control in `LotQt.add_physical_lots` /
|
||||||
|
> `LotQt.add_physical_lot` still needs confirmation.
|
||||||
|
>
|
||||||
|
| Point | Target Rule |
|
||||||
|
| --- | --- |
|
||||||
|
| Level | Global tolerance on line or contract. |
|
||||||
|
| Transport | No independent tolerance per transport. |
|
||||||
|
| Over-execution | Consumes remaining tolerance. |
|
||||||
|
| Under-execution | Restores remaining tolerance. |
|
||||||
|
|
||||||
|
## Developer Section
|
||||||
|
|
||||||
|
### Key Fields
|
||||||
|
|
||||||
|
- Purchase line: `purchase.line`
|
||||||
|
- Sale line: `sale.line`
|
||||||
|
- Lot: `lot.lot`
|
||||||
|
- Forecast: `lot.qt`
|
||||||
|
- History: `lot.qt.hist`
|
||||||
|
- Purchase business quantity: `purchase.line.quantity_theorical`
|
||||||
|
- Sale business quantity: `sale.line.quantity_theorical`
|
||||||
|
- Technical counter: `quantity`
|
||||||
|
- Finished line: `purchase.line.finished`, `sale.line.finished`
|
||||||
|
- Virtual / physical lot: `lot.lot.lot_type = virtual / physic`
|
||||||
|
- Purchase link: `lot.lot.line`
|
||||||
|
- Sale link: `lot.lot.sale_line`
|
||||||
|
- Purchase forecast: `lot.qt.lot_p`
|
||||||
|
- Sale forecast: `lot.qt.lot_s`
|
||||||
|
- Forecast quantity: `lot.qt.lot_quantity`
|
||||||
|
- Weight basis: `purchase.purchase.wb`, `sale.sale.wb`
|
||||||
|
- Weight basis state: `purchase.weight.basis.qt_type`
|
||||||
|
- Packing: `lot.lot.lot_qt`, `lot.lot.lot_unit`
|
||||||
|
- Tolerances: `tol_min`, `tol_max`, `tol_min_qt`, `tol_max_qt`,
|
||||||
|
`tol_min_v`, `tol_max_v`
|
||||||
|
|
||||||
|
### Line / Virtual Lot Creation
|
||||||
|
|
||||||
|
- Purchase: `purchase.py`, `Line.validate`
|
||||||
|
- Sale: `sale.py`, `SaleLine.validate`
|
||||||
|
- If `quantity_theorical` is entered and `quantity` is empty or zero:
|
||||||
|
- `quantity` is initialized from `quantity_theorical`;
|
||||||
|
- only if no physical lot exists.
|
||||||
|
- If the line is eligible:
|
||||||
|
- not `created_by_code`;
|
||||||
|
- no lot yet;
|
||||||
|
- non-service product;
|
||||||
|
- `quantity_theorical != 0`;
|
||||||
|
- create one `virtual` lot.
|
||||||
|
- The virtual lot receives a first `lot.qt.hist` entry.
|
||||||
|
- `Lot.validate` creates the open `lot.qt` through `createVirtualPart`.
|
||||||
|
|
||||||
|
### Updating `quantity_theorical`
|
||||||
|
|
||||||
|
- Purchase: `purchase.py`, `Line.write`
|
||||||
|
- Sale: `sale.py`, `SaleLine.write`
|
||||||
|
- Virtual lot target:
|
||||||
|
|
||||||
|
```text
|
||||||
|
target_quantity = quantity_theorical - sum(converted physical lots)
|
||||||
|
```
|
||||||
|
|
||||||
|
- If `target_quantity < 0`:
|
||||||
|
- block with `Please unlink or unmatch lot`.
|
||||||
|
- Free `lot.qt` target:
|
||||||
|
|
||||||
|
```text
|
||||||
|
free_quantity = target_quantity - sum(already matched or shipped lot.qt)
|
||||||
|
```
|
||||||
|
|
||||||
|
- If `free_quantity < 0`:
|
||||||
|
- block with `Please unlink or unmatch lot`.
|
||||||
|
- If a free `lot.qt` exists:
|
||||||
|
- replace its quantity.
|
||||||
|
- If no free `lot.qt` exists and `free_quantity > 0`:
|
||||||
|
- create a new `lot.qt`.
|
||||||
|
- Line fees are resynchronized.
|
||||||
|
|
||||||
|
### Adding Physical Lots
|
||||||
|
|
||||||
|
- Wizard: `lot.add`
|
||||||
|
- Methods:
|
||||||
|
- `LotQt.add_physical_lots`
|
||||||
|
- `LotQt.add_physical_lot`
|
||||||
|
- Mandatory source: one `lot.qt` line.
|
||||||
|
- Direct add from a physical lot is refused.
|
||||||
|
- Physical add on sale side through this wizard is refused: use
|
||||||
|
`Apply matching`.
|
||||||
|
- The physical lot inherits:
|
||||||
|
- purchase line;
|
||||||
|
- matched sale, if any;
|
||||||
|
- shipment;
|
||||||
|
- product;
|
||||||
|
- unit;
|
||||||
|
- quantities;
|
||||||
|
- premium;
|
||||||
|
- chunk key.
|
||||||
|
- After creation:
|
||||||
|
- source `lot.qt` is reduced;
|
||||||
|
- `lot.qt` cannot become negative;
|
||||||
|
- virtual lot is recalculated;
|
||||||
|
- `quantity` is recalculated;
|
||||||
|
- moves and fees are updated when needed.
|
||||||
|
|
||||||
|
### Removing Physical Lots
|
||||||
|
|
||||||
|
- Wizard: `lot.remove`
|
||||||
|
- Open lot: removal forbidden.
|
||||||
|
- Lot with `stock.move`:
|
||||||
|
- move must be `draft`.
|
||||||
|
- Matched or shipped lot:
|
||||||
|
- confirmable warning.
|
||||||
|
- Effects:
|
||||||
|
- draft move deletion;
|
||||||
|
- restore quantity into `lot.qt`;
|
||||||
|
- restore context through shipment, `getVlot_p()`, `getVlot_s()`;
|
||||||
|
- recalculate virtual lot, `quantity`, fees.
|
||||||
|
|
||||||
|
### Weighing / Quantity States
|
||||||
|
|
||||||
|
- Wizard: `lot.weighing`
|
||||||
|
- UI action: `Do weighing`
|
||||||
|
- Writes or updates `lot.qt.hist`.
|
||||||
|
- May update `lot_state`.
|
||||||
|
- Synchronizes:
|
||||||
|
- lot;
|
||||||
|
- open quantities;
|
||||||
|
- fees.
|
||||||
|
- `lot.qt.hist` views are consultative.
|
||||||
|
|
||||||
|
### Quantity Counter `quantity`
|
||||||
|
|
||||||
|
- Method: `Lot._recalc_line_quantity`
|
||||||
|
- Without physical lots:
|
||||||
|
- `quantity` follows the virtual lot.
|
||||||
|
- With physical lots:
|
||||||
|
- `quantity` sums physical lots only.
|
||||||
|
- `quantity` is readonly on trade lines.
|
||||||
|
|
||||||
|
### Line Amount
|
||||||
|
|
||||||
|
- Purchase: `purchase.line.on_change_with_amount()`
|
||||||
|
- Sale: `sale.line.on_change_with_amount()`
|
||||||
|
- Helpers:
|
||||||
|
- `_get_amount_quantity()`
|
||||||
|
- `_get_weight_basis_quantity()`
|
||||||
|
- Priorities:
|
||||||
|
- `finished = False`: `quantity_theorical`
|
||||||
|
- `finished = True` + usable Weight basis: physical sum in that state
|
||||||
|
- `finished = True` without usable Weight basis: `quantity`
|
||||||
|
- legacy fallback: `quantity` if `quantity_theorical` is empty
|
||||||
|
|
||||||
|
### Python Guards
|
||||||
|
|
||||||
|
- Central check:
|
||||||
|
- `lot.lot.assert_lines_quantity_consistency()`
|
||||||
|
- Non-zero orphan `lot.qt` block:
|
||||||
|
- `lot.qt.validate`
|
||||||
|
- Called after:
|
||||||
|
- `quantity_theorical` update;
|
||||||
|
- physical lot creation / deletion;
|
||||||
|
- matching / unmatching;
|
||||||
|
- shipping / unshipping;
|
||||||
|
- weighing.
|
||||||
|
|
||||||
|
### SQL Diagnostic
|
||||||
|
|
||||||
|
- Script:
|
||||||
|
- [sql/quantity_consistency_checks.sql](sql/quantity_consistency_checks.sql)
|
||||||
|
- Use:
|
||||||
|
- test database audit;
|
||||||
|
- historical data audit;
|
||||||
|
- qualification before repair.
|
||||||
|
- The script completely ignores `lot.qt = 0`.
|
||||||
|
|
||||||
|
## Nearby Tests
|
||||||
|
|
||||||
|
- `modules/purchase_trade/tests/test_module.py`
|
||||||
|
- Existing coverage:
|
||||||
|
- readonly `quantity`;
|
||||||
|
- initialization from `quantity_theorical`;
|
||||||
|
- protection when physical lots exist;
|
||||||
|
- amount on theoretical / physical / Weight basis;
|
||||||
|
- virtual lot resynchronization;
|
||||||
|
- blocking when open quantity is not enough.
|
||||||
|
- Tests to add:
|
||||||
|
- readonly `lot_hist`;
|
||||||
|
- `Do weighing` creates or updates a state;
|
||||||
|
- virtual lot without direct `lot_qt` / `lot_unit` entry;
|
||||||
|
- SQL checks replayed on inconsistent datasets.
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
# 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`
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## À retenir
|
||||||
|
|
||||||
|
> **Résumé opérationnel**
|
||||||
|
> Une ligne trade possède un seul lot virtuel. Le lot virtuel porte le solde
|
||||||
|
> ouvert global, `lot.qt` porte le forecast opérationnel, et les lots
|
||||||
|
> physiques consomment ce forecast.
|
||||||
|
>
|
||||||
|
| Sujet | Règle courte |
|
||||||
|
| --- | --- |
|
||||||
|
| Quantité saisie | `quantity_theorical` est la quantité métier. |
|
||||||
|
| Quantité standard | `quantity` est un compteur technique non éditable. |
|
||||||
|
| Avant physique | `quantity` suit `quantity_theorical`. |
|
||||||
|
| Après physique | `quantity` reflète l'exécuté physique. |
|
||||||
|
| Ligne non finie | Le montant de ligne utilise `quantity_theorical`. |
|
||||||
|
| Ligne finie | Le montant peut revenir à l'exécuté physique. |
|
||||||
|
| Weight basis | Achat et vente peuvent lire deux états différents du même lot. |
|
||||||
|
| Facturation | Elle choisit ses états dans `lot.qt.hist`. |
|
||||||
|
| Contrôles | Invariants bloqués en Python et auditables en SQL. |
|
||||||
|
|
||||||
|
```text
|
||||||
|
quantity_theorical
|
||||||
|
|
|
||||||
|
v
|
||||||
|
lot virtuel P1 ---> lot.qt forecast ---> lot physique
|
||||||
|
^ |
|
||||||
|
| v
|
||||||
|
+------ recalcul après consommation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Règles consultant
|
||||||
|
|
||||||
|
### BR-PT-LOT-001 - Cycle de vie lot virtuel / forecast / physique
|
||||||
|
|
||||||
|
| Moment | Effet métier |
|
||||||
|
| --- | --- |
|
||||||
|
| Création de ligne | Création d'un lot virtuel unique. |
|
||||||
|
| Initialisation | Le lot virtuel reprend `quantity_theorical`. |
|
||||||
|
| Forecast | Une ligne ouverte est créée dans `lot.qt`. |
|
||||||
|
| Planification | `lot.qt` peut être subdivisé par vente, matching, transport ou shipment. |
|
||||||
|
| Ajout physique | Le lot physique consomme une ligne `lot.qt` précise. |
|
||||||
|
| Après ajout | `lot.qt` diminue et le lot virtuel est recalculé. |
|
||||||
|
|
||||||
|
> **Découpage d'un solde P1**
|
||||||
|
> `P1S1T1`, `P1S1T2`, `P1S2T3`, `P1S2T4`
|
||||||
|
>
|
||||||
|
> **Point clé**
|
||||||
|
> Ces découpages ne créent pas plusieurs lots virtuels. Ils décrivent
|
||||||
|
> seulement la répartition prévisionnelle du solde ouvert.
|
||||||
|
>
|
||||||
|
### BR-PT-LOT-002 - Quantité contractuelle, compteur, ligne finie
|
||||||
|
|
||||||
|
| Situation | Quantité de référence |
|
||||||
|
| --- | --- |
|
||||||
|
| Saisie utilisateur | `quantity_theorical` |
|
||||||
|
| Aucun lot physique | `quantity = quantity_theorical` |
|
||||||
|
| Lots physiques présents | `quantity = somme des lots physiques` |
|
||||||
|
| `finished = False` | Montant basé sur `quantity_theorical` |
|
||||||
|
| `finished = True` | Montant basé sur l'exécuté physique |
|
||||||
|
| Weight basis disponible | Montant basé sur l'état `wb.qt_type` du contrat |
|
||||||
|
|
||||||
|
> **Ce que `finished` ne fait pas**
|
||||||
|
> `finished` n'efface pas la quantité contractuelle, ne supprime pas les lots
|
||||||
|
> physiques et ne masque pas leur PnL. Il signifie seulement que le reliquat
|
||||||
|
> ouvert peut être ignoré pour les calculs d'exécution.
|
||||||
|
>
|
||||||
|
| Lecture du même lot physique | État possible |
|
||||||
|
| --- | --- |
|
||||||
|
| Achat | BL via `purchase.purchase.wb.qt_type` |
|
||||||
|
| Vente | LR ou Weight Report via `sale.sale.wb.qt_type` |
|
||||||
|
| Facturation | Choix indépendant dans `lot.qt.hist` |
|
||||||
|
|
||||||
|
### BR-PT-LOT-003 - Invariants de quantité
|
||||||
|
|
||||||
|
| Invariant | Formule |
|
||||||
|
| --- | --- |
|
||||||
|
| Conservation | `somme(lots physiques) + lot virtuel = quantity_theorical` |
|
||||||
|
| Forecast ouvert | `somme(lot.qt non zéro) = max(lot virtuel, 0)` |
|
||||||
|
|
||||||
|
| Cas | Règle |
|
||||||
|
| --- | --- |
|
||||||
|
| Lot virtuel achat | Sommer tous les `lot.qt` où `lot_p = lot virtuel`, avec ou sans `lot_s`. |
|
||||||
|
| Lot virtuel vente | Sommer tous les `lot.qt` où `lot_s = lot virtuel`, avec ou sans `lot_p`. |
|
||||||
|
| `lot.qt = 0` | Ignoré par les checks ; mémoire possible d'une prévision vidée. |
|
||||||
|
| Lot virtuel négatif | Autorisé pour compenser l'écart théorique / exécuté ; forecast attendu = zéro. |
|
||||||
|
| `lot.qt` non zéro orphelin | Interdit si ni `lot_p` ni `lot_s` n'est renseigné. |
|
||||||
|
|
||||||
|
### BR-PT-LOT-004 - Historique de quantité et weighing
|
||||||
|
|
||||||
|
| Élément | Règle |
|
||||||
|
| --- | --- |
|
||||||
|
| `lot.qt.hist` | Porte les états de quantité d'un lot. |
|
||||||
|
| Fiche `lot.lot` | Pas de saisie directe des états. |
|
||||||
|
| Modification | Uniquement via `Do weighing`. |
|
||||||
|
| Lot virtuel | Pas de packing manuel. |
|
||||||
|
| Champs packing virtuel | `lot_qt` et `lot_unit` non éditables. |
|
||||||
|
|
||||||
|
### Tolérances
|
||||||
|
|
||||||
|
> **Gap à confirmer**
|
||||||
|
> Le contrôle complet de tolérance restante dans `LotQt.add_physical_lots` /
|
||||||
|
> `LotQt.add_physical_lot` reste à confirmer.
|
||||||
|
>
|
||||||
|
| Point | Règle cible |
|
||||||
|
| --- | --- |
|
||||||
|
| Niveau | Tolérance globale sur ligne ou contrat. |
|
||||||
|
| Transport | Pas de tolérance indépendante par transport. |
|
||||||
|
| Surconsommation | Consomme la tolérance restante. |
|
||||||
|
| Sous-consommation | Restitue de la tolérance restante. |
|
||||||
|
|
||||||
|
## Section développeur
|
||||||
|
|
||||||
|
### Champs clés
|
||||||
|
|
||||||
|
- 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 ligne / lot virtuel
|
||||||
|
|
||||||
|
- 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`.
|
||||||
|
- `Lot.validate` crée le `lot.qt` ouvert via `createVirtualPart`.
|
||||||
|
|
||||||
|
### Modification de `quantity_theorical`
|
||||||
|
|
||||||
|
- Achat : `purchase.py`, `Line.write`
|
||||||
|
- Vente : `sale.py`, `SaleLine.write`
|
||||||
|
- Cible lot virtuel :
|
||||||
|
|
||||||
|
```text
|
||||||
|
target_quantity = quantity_theorical - somme(lots physiques convertis)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 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 < 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 : `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 : `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.
|
||||||
|
|
||||||
|
### Weighing / états de quantité
|
||||||
|
|
||||||
|
- 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 `quantity`
|
||||||
|
|
||||||
|
- 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()`
|
||||||
|
- 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
|
||||||
|
|
||||||
|
### Garde-fous Python
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
### Diagnostic SQL
|
||||||
|
|
||||||
|
- 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`.
|
||||||
|
|
||||||
|
## Tests proches
|
||||||
|
|
||||||
|
- `modules/purchase_trade/tests/test_module.py`
|
||||||
|
- 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