Bug check qt

This commit is contained in:
2026-05-14 21:52:10 +02:00
parent 07ddb212e3
commit 60f26ddc9d
8 changed files with 54 additions and 19 deletions

View File

@@ -191,8 +191,8 @@ them with Python guards and SQL diagnostics.
<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>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Minimum open forecast</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>sum(non-zero lot.qt) &gt;= max(virtual lot, 0)</code></td>
</tr>
</tbody>
</table>
@@ -218,6 +218,14 @@ them with Python guards and SQL diagnostics.
<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;">Forecast overconsumption</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Allowed after tolerance confirmation: <code>lot.qt</code> stops at zero, but the virtual lot absorbs the whole physical lot.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>lot.qt</code> greater than virtual lot</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Accepted when remaining open forecast exceeds the positive virtual lot because of physical overconsumption.</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>

View File

@@ -190,8 +190,8 @@ puis les sécuriser par des checks Python et des diagnostics SQL.
<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>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Forecast ouvert minimal</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>somme(lot.qt non zéro) &gt;= max(lot virtuel, 0)</code></td>
</tr>
</tbody>
</table>
@@ -217,6 +217,14 @@ puis les sécuriser par des checks Python et des diagnostics SQL.
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Ignoré par les checks ; mémoire possible d&#x27;une prévision vidée.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Surconsommation forecast</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Autorisée après confirmation de tolérance : <code>lot.qt</code> s&#x27;arrête à zéro, mais le lot virtuel absorbe tout le physique.</td>
</tr>
<tr style="border-bottom:1px solid #e0e0e0;">
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>lot.qt</code> supérieur au virtuel</td>
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Accepté si le forecast ouvert restant dépasse le virtuel positif à cause d&#x27;une surconsommation physique.</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&#x27;écart théorique / exécuté ; forecast attendu = zéro.</td>
</tr>

View File

@@ -23,6 +23,12 @@ lot may become negative to compensate the difference between theoretical and
executed quantity. A non-zero `lot_qt` row without both `lot_p` and `lot_s` is
reported as anomalous.
When a physical lot overconsumes the selected forecast after a tolerance
confirmation, `lot_qt` stops at zero but the virtual lot absorbs the full
physical quantity. The SQL diagnostic therefore reports only open forecast
lower than the positive virtual lot. Open forecast greater than the virtual lot
is accepted as a consequence of this overconsumption rule.
Run it on a restored test database:
<pre style="background:#263238; color:#eef7ff; padding:1rem; border-radius:0.35rem; overflow:auto;"><code>\i modules/purchase_trade/docs/business/sql/quantity_consistency_checks.sql</code></pre>

View File

@@ -12,12 +12,15 @@ Rules checked:
2. For each virtual purchase lot:
sum(non-zero lot_qt.lot_quantity where lot_p = virtual lot)
= virtual lot current quantity.
must not be lower than the positive virtual lot current quantity.
A higher sum is accepted because lot_qt stops at zero when a physical lot
overconsumes a forecast, while the virtual lot absorbs the full physical
quantity.
Matched and unmatched lot_qt rows are both included.
3. For each virtual sale lot:
sum(non-zero lot_qt.lot_quantity where lot_s = virtual lot)
= virtual lot current quantity.
must not be lower than the positive virtual lot current quantity.
Rows linked to a purchase lot and rows without purchase lot are both
included.
@@ -349,7 +352,7 @@ FROM (
UNION ALL
SELECT
'purchase_lot_qt_equals_virtual' AS check_name,
'purchase_lot_qt_below_virtual' AS check_name,
'purchase.purchase' AS contract_model,
b.contract_id,
b.contract_number,
@@ -360,18 +363,18 @@ FROM (
ROUND(
b.lot_qt_quantity
- GREATEST(COALESCE(b.virtual_quantity, 0), 0), 5) AS diff,
'sum non-zero lot_qt where lot_p = virtual lot must equal positive virtual lot quantity'
'sum non-zero lot_qt where lot_p = virtual lot must not be lower than positive virtual lot quantity'
AS detail
FROM purchase_lot_qt_balance b, params p
WHERE b.lot_qt_cross_category_count = 0
AND ABS(
AND (
b.lot_qt_quantity
- GREATEST(COALESCE(b.virtual_quantity, 0), 0)) > p.epsilon
- GREATEST(COALESCE(b.virtual_quantity, 0), 0)) < -p.epsilon
UNION ALL
SELECT
'sale_lot_qt_equals_virtual' AS check_name,
'sale_lot_qt_below_virtual' AS check_name,
'sale.sale' AS contract_model,
b.contract_id,
b.contract_number,
@@ -382,13 +385,13 @@ FROM (
ROUND(
b.lot_qt_quantity
- GREATEST(COALESCE(b.virtual_quantity, 0), 0), 5) AS diff,
'sum non-zero lot_qt where lot_s = virtual lot must equal positive virtual lot quantity'
'sum non-zero lot_qt where lot_s = virtual lot must not be lower than positive virtual lot quantity'
AS detail
FROM sale_lot_qt_balance b, params p
WHERE b.lot_qt_cross_category_count = 0
AND ABS(
AND (
b.lot_qt_quantity
- GREATEST(COALESCE(b.virtual_quantity, 0), 0)) > p.epsilon
- GREATEST(COALESCE(b.virtual_quantity, 0), 0)) < -p.epsilon
UNION ALL

View File

@@ -86,13 +86,15 @@ quantity_theorical
| Invariant | Formula |
| --- | --- |
| Conservation | `sum(physical lots) + virtual lot = quantity_theorical` |
| Open forecast | `sum(non-zero lot.qt) = max(virtual lot, 0)` |
| Minimum open forecast | `sum(non-zero lot.qt) >= max(virtual lot, 0)` |
| Case | Rule |
| --- | --- |
| Purchase virtual lot | Sum all `lot.qt` where `lot_p = virtual lot`, with or without `lot_s`. |
| Sale virtual lot | Sum all `lot.qt` where `lot_s = virtual lot`, with or without `lot_p`. |
| `lot.qt = 0` | Ignored by checks; possible memory of a consumed forecast. |
| Forecast overconsumption | Allowed after tolerance confirmation: `lot.qt` stops at zero, but the virtual lot absorbs the whole physical lot. |
| `lot.qt` greater than virtual lot | Accepted when remaining open forecast exceeds the positive virtual lot because of physical overconsumption. |
| Negative virtual lot | Allowed to compensate theoretical / executed gap; expected forecast = zero. |
| Non-zero orphan `lot.qt` | Forbidden if neither `lot_p` nor `lot_s` is set. |

View File

@@ -85,13 +85,15 @@ quantity_theorical
| Invariant | Formule |
| --- | --- |
| Conservation | `somme(lots physiques) + lot virtuel = quantity_theorical` |
| Forecast ouvert | `somme(lot.qt non zéro) = max(lot virtuel, 0)` |
| Forecast ouvert minimal | `somme(lot.qt non zéro) >= max(lot virtuel, 0)` |
| Cas | Règle |
| --- | --- |
| Lot virtuel achat | Sommer tous les `lot.qt``lot_p = lot virtuel`, avec ou sans `lot_s`. |
| Lot virtuel vente | Sommer tous les `lot.qt``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. |
| Surconsommation forecast | Autorisée après confirmation de tolérance : `lot.qt` s'arrête à zéro, mais le lot virtuel absorbe tout le physique. |
| `lot.qt` supérieur au virtuel | Accepté si le forecast ouvert restant dépasse le virtuel positif à cause d'une surconsommation physique. |
| 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é. |

View File

@@ -21,6 +21,12 @@ lot may become negative to compensate the difference between theoretical and
executed quantity. A non-zero `lot_qt` row without both `lot_p` and `lot_s` is
reported as anomalous.
When a physical lot overconsumes the selected forecast after a tolerance
confirmation, `lot_qt` stops at zero but the virtual lot absorbs the full
physical quantity. The SQL diagnostic therefore reports only open forecast
lower than the positive virtual lot. Open forecast greater than the virtual lot
is accepted as a consequence of this overconsumption rule.
Run it on a restored test database:
```sql

View File

@@ -182,12 +182,12 @@ class Lot(metaclass=PoolMeta):
diff = (
cls._round_consistency_quantity(open_quantity)
- cls._round_consistency_quantity(expected_open_quantity))
if diff:
if diff < 0:
raise UserError(
cls._format_consistency_error(
line,
"lot.qt forecast quantity must equal virtual lot "
"positive current quantity",
"lot.qt forecast quantity must not be lower than "
"virtual lot positive current quantity",
[
('lot_qt', open_quantity),
('virtual', virtual_quantity),