diff --git a/modules/purchase_trade/docs/business/lots-and-quantities.en.md b/modules/purchase_trade/docs/business/lots-and-quantities.en.md index b363a3a..8d487a7 100644 --- a/modules/purchase_trade/docs/business/lots-and-quantities.en.md +++ b/modules/purchase_trade/docs/business/lots-and-quantities.en.md @@ -191,8 +191,8 @@ them with Python guards and SQL diagnostics. 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) @@ -218,6 +218,14 @@ them with Python guards and SQL diagnostics. 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. diff --git a/modules/purchase_trade/docs/business/lots-and-quantities.md b/modules/purchase_trade/docs/business/lots-and-quantities.md index fbb4f85..124cfbd 100644 --- a/modules/purchase_trade/docs/business/lots-and-quantities.md +++ b/modules/purchase_trade/docs/business/lots-and-quantities.md @@ -190,8 +190,8 @@ puis les sécuriser par des checks Python et des diagnostics SQL. 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) @@ -217,6 +217,14 @@ puis les sécuriser par des checks Python et des diagnostics SQL. 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. diff --git a/modules/purchase_trade/docs/business/sql/README.md b/modules/purchase_trade/docs/business/sql/README.md index 80b63ad..4abfe1d 100644 --- a/modules/purchase_trade/docs/business/sql/README.md +++ b/modules/purchase_trade/docs/business/sql/README.md @@ -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:
\i modules/purchase_trade/docs/business/sql/quantity_consistency_checks.sql
diff --git a/modules/purchase_trade/docs/business/sql/quantity_consistency_checks.sql b/modules/purchase_trade/docs/business/sql/quantity_consistency_checks.sql index 4bb3205..c0f38f7 100644 --- a/modules/purchase_trade/docs/business/sql/quantity_consistency_checks.sql +++ b/modules/purchase_trade/docs/business/sql/quantity_consistency_checks.sql @@ -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 diff --git a/modules/purchase_trade/docs_source/business/lots-and-quantities.en.md b/modules/purchase_trade/docs_source/business/lots-and-quantities.en.md index b0110d3..6edcc2b 100644 --- a/modules/purchase_trade/docs_source/business/lots-and-quantities.en.md +++ b/modules/purchase_trade/docs_source/business/lots-and-quantities.en.md @@ -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. | diff --git a/modules/purchase_trade/docs_source/business/lots-and-quantities.md b/modules/purchase_trade/docs_source/business/lots-and-quantities.md index e54c573..c4772ca 100644 --- a/modules/purchase_trade/docs_source/business/lots-and-quantities.md +++ b/modules/purchase_trade/docs_source/business/lots-and-quantities.md @@ -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` 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. | +| 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é. | diff --git a/modules/purchase_trade/docs_source/business/sql/README.md b/modules/purchase_trade/docs_source/business/sql/README.md index 645894b..b11061a 100644 --- a/modules/purchase_trade/docs_source/business/sql/README.md +++ b/modules/purchase_trade/docs_source/business/sql/README.md @@ -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 diff --git a/modules/purchase_trade/lot.py b/modules/purchase_trade/lot.py index 1e956fa..2e10970 100755 --- a/modules/purchase_trade/lot.py +++ b/modules/purchase_trade/lot.py @@ -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),