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_theoricalsum(non-zero lot.qt) = max(virtual lot, 0)sum(non-zero lot.qt) >= max(virtual lot, 0)lot.qt stops at zero, but the virtual lot absorbs the whole physical lot.lot.qt greater than virtual lotsomme(lots physiques) + lot virtuel = quantity_theoricalsomme(lot.qt non zéro) = max(lot virtuel, 0)somme(lot.qt non zéro) >= max(lot virtuel, 0)lot.qt s'arrête à zéro, mais le lot virtuel absorbe tout le physique.lot.qt supérieur au virtuel\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),