Check lot quantities
This commit is contained in:
@@ -227,6 +227,10 @@ Invariants to preserve after every update:
|
||||
where `lot_p = virtual lot`, whether they are matched to a `lot_s` or not.
|
||||
- For a sale virtual lot, the second invariant sums all `lot.qt` lines where
|
||||
`lot_s = virtual lot`, whether they are linked to a `lot_p` or not.
|
||||
- A zero `lot.qt` row is accepted if it still keeps at least one `lot_p` or
|
||||
`lot_s` link: it is then the memory of a consumed forecast.
|
||||
- A `lot.qt` row without both `lot_p` and `lot_s` is blocked by
|
||||
`lot.qt.validate`.
|
||||
- The technical guard is centralized in
|
||||
`lot.lot.assert_lines_quantity_consistency()` and must be called at the end
|
||||
of flows that modify lots, matching, transport, weighing, or the contractual
|
||||
|
||||
@@ -233,6 +233,10 @@ Invariants à préserver après chaque modification :
|
||||
non.
|
||||
- Pour un lot virtuel vente, le second invariant somme toutes les lignes
|
||||
`lot.qt` où `lot_s = virtual lot`, qu'elles soient liées à un `lot_p` ou non.
|
||||
- Une ligne `lot.qt` à zéro est acceptée si elle garde au moins un lien
|
||||
`lot_p` ou `lot_s` : elle sert alors de mémoire d'une prévision vidée.
|
||||
- Une ligne `lot.qt` sans `lot_p` ni `lot_s` est bloquée par
|
||||
`lot.qt.validate`.
|
||||
- Le garde-fou technique est centralisé dans
|
||||
`lot.lot.assert_lines_quantity_consistency()` et doit être appelé à la fin
|
||||
des flux qui modifient des lots, du matching, du transport, du weighing ou la
|
||||
|
||||
@@ -7,6 +7,11 @@ These scripts are read-only diagnostics for a PostgreSQL test database.
|
||||
Checks the two core lot quantity invariants documented in
|
||||
`lots-and-quantities.md` and `lots-and-quantities.en.md`.
|
||||
|
||||
Zero `lot_qt` rows are ignored by the quantity sums when they are still linked
|
||||
to at least one virtual lot through `lot_p` or `lot_s`; they are treated as
|
||||
legitimate memory of an open quantity consumed by a physical lot. A `lot_qt`
|
||||
row without both `lot_p` and `lot_s` is reported as anomalous.
|
||||
|
||||
Run it on a restored test database:
|
||||
|
||||
```sql
|
||||
@@ -18,6 +23,9 @@ The script returns rows only when it finds a potential issue.
|
||||
Main columns:
|
||||
|
||||
- `check_name`: invariant or diagnostic that failed.
|
||||
- `contract_model`: `purchase.purchase` or `sale.sale`.
|
||||
- `contract_id`: database id of the contract.
|
||||
- `contract_number`: purchase or sale contract number.
|
||||
- `line_id`: `purchase.line` or `sale.line` id depending on the check.
|
||||
- `virtual_lot_id`: virtual lot involved in the inconsistency.
|
||||
- `observed_value`: value found in the database.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
-- Active: 1778691597138@@72.61.163.139@5433@tradon
|
||||
-- Active: 1778686654594@@72.61.163.139@5435@tradon
|
||||
/*
|
||||
Purchase Trade quantity consistency diagnostics.
|
||||
|
||||
@@ -9,16 +11,20 @@ Rules checked:
|
||||
= line quantity_theorical.
|
||||
|
||||
2. For each virtual purchase lot:
|
||||
sum(lot_qt.lot_quantity where lot_p = virtual lot)
|
||||
sum(non-zero lot_qt.lot_quantity where lot_p = virtual lot)
|
||||
= virtual lot current quantity.
|
||||
Matched and unmatched lot_qt rows are both included.
|
||||
|
||||
3. For each virtual sale lot:
|
||||
sum(lot_qt.lot_quantity where lot_s = virtual lot)
|
||||
sum(non-zero lot_qt.lot_quantity where lot_s = virtual lot)
|
||||
= virtual lot current quantity.
|
||||
Rows linked to a purchase lot and rows without purchase lot are both
|
||||
included.
|
||||
|
||||
Zero lot_qt rows are allowed when they keep the memory of an open quantity
|
||||
consumed by a physical lot, as long as at least lot_p or lot_s is set. Unlinked
|
||||
lot_qt rows are reported as anomalies.
|
||||
|
||||
Notes:
|
||||
- The script uses the lot current state, i.e. lot_lot.lot_state joined to
|
||||
lot_qt_hist.quantity_type.
|
||||
@@ -55,36 +61,46 @@ lot_current AS (
|
||||
purchase_line_base AS (
|
||||
SELECT
|
||||
pl.id AS line_id,
|
||||
pp.id AS contract_id,
|
||||
pp.number AS contract_number,
|
||||
pl.quantity_theorical::numeric AS theoretical_quantity,
|
||||
pl.quantity::numeric AS line_quantity,
|
||||
pl.unit AS line_unit,
|
||||
COUNT(vlot.id) AS virtual_count,
|
||||
MIN(vlot.id) AS virtual_lot_id
|
||||
FROM purchase_line pl
|
||||
LEFT JOIN purchase_purchase pp ON pp.id = pl.purchase
|
||||
LEFT JOIN lot_lot vlot
|
||||
ON vlot.line = pl.id
|
||||
AND vlot.lot_type = 'virtual'
|
||||
WHERE pl.quantity_theorical IS NOT NULL
|
||||
GROUP BY pl.id, pl.quantity_theorical, pl.quantity, pl.unit
|
||||
GROUP BY
|
||||
pl.id, pp.id, pp.number, pl.quantity_theorical, pl.quantity, pl.unit
|
||||
),
|
||||
sale_line_base AS (
|
||||
SELECT
|
||||
sl.id AS line_id,
|
||||
ss.id AS contract_id,
|
||||
ss.number AS contract_number,
|
||||
sl.quantity_theorical::numeric AS theoretical_quantity,
|
||||
sl.quantity::numeric AS line_quantity,
|
||||
sl.unit AS line_unit,
|
||||
COUNT(vlot.id) AS virtual_count,
|
||||
MIN(vlot.id) AS virtual_lot_id
|
||||
FROM sale_line sl
|
||||
LEFT JOIN sale_sale ss ON ss.id = sl.sale
|
||||
LEFT JOIN lot_lot vlot
|
||||
ON vlot.sale_line = sl.id
|
||||
AND vlot.lot_type = 'virtual'
|
||||
WHERE sl.quantity_theorical IS NOT NULL
|
||||
GROUP BY sl.id, sl.quantity_theorical, sl.quantity, sl.unit
|
||||
GROUP BY
|
||||
sl.id, ss.id, ss.number, sl.quantity_theorical, sl.quantity, sl.unit
|
||||
),
|
||||
purchase_line_quantities AS (
|
||||
SELECT
|
||||
base.line_id,
|
||||
base.contract_id,
|
||||
base.contract_number,
|
||||
base.theoretical_quantity,
|
||||
base.line_quantity,
|
||||
base.line_unit,
|
||||
@@ -120,7 +136,8 @@ purchase_line_quantities AS (
|
||||
AND phys.lot_type = 'physic'
|
||||
LEFT JOIN uom phys_uom ON phys_uom.id = phys.lot_unit_line
|
||||
GROUP BY
|
||||
base.line_id, base.theoretical_quantity, base.line_quantity,
|
||||
base.line_id, base.contract_id, base.contract_number,
|
||||
base.theoretical_quantity, base.line_quantity,
|
||||
base.line_unit, base.virtual_count, base.virtual_lot_id,
|
||||
vlot.lot_id, vlot.current_quantity, vlot_uom.category,
|
||||
vlot_uom.factor, line_uom.category, line_uom.factor
|
||||
@@ -128,6 +145,8 @@ purchase_line_quantities AS (
|
||||
sale_line_quantities AS (
|
||||
SELECT
|
||||
base.line_id,
|
||||
base.contract_id,
|
||||
base.contract_number,
|
||||
base.theoretical_quantity,
|
||||
base.line_quantity,
|
||||
base.line_unit,
|
||||
@@ -163,7 +182,8 @@ sale_line_quantities AS (
|
||||
AND phys.lot_type = 'physic'
|
||||
LEFT JOIN uom phys_uom ON phys_uom.id = phys.lot_unit_line
|
||||
GROUP BY
|
||||
base.line_id, base.theoretical_quantity, base.line_quantity,
|
||||
base.line_id, base.contract_id, base.contract_number,
|
||||
base.theoretical_quantity, base.line_quantity,
|
||||
base.line_unit, base.virtual_count, base.virtual_lot_id,
|
||||
vlot.lot_id, vlot.current_quantity, vlot_uom.category,
|
||||
vlot_uom.factor, line_uom.category, line_uom.factor
|
||||
@@ -172,6 +192,8 @@ purchase_lot_qt_balance AS (
|
||||
SELECT
|
||||
vlot.id AS virtual_lot_id,
|
||||
vlot.line AS line_id,
|
||||
pp.id AS contract_id,
|
||||
pp.number AS contract_number,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN lqt_uom.category = line_uom.category
|
||||
@@ -191,18 +213,23 @@ purchase_lot_qt_balance AS (
|
||||
) AS lot_qt_cross_category_count
|
||||
FROM lot_lot vlot
|
||||
JOIN purchase_line pl ON pl.id = vlot.line
|
||||
LEFT JOIN purchase_purchase pp ON pp.id = pl.purchase
|
||||
JOIN uom line_uom ON line_uom.id = pl.unit
|
||||
LEFT JOIN lot_current current ON current.lot_id = vlot.id
|
||||
LEFT JOIN uom vlot_uom ON vlot_uom.id = current.lot_unit_line
|
||||
LEFT JOIN lot_qt lqt ON lqt.lot_p = vlot.id
|
||||
LEFT JOIN lot_qt lqt
|
||||
ON lqt.lot_p = vlot.id
|
||||
AND COALESCE(lqt.lot_quantity, 0) <> 0
|
||||
LEFT JOIN uom lqt_uom ON lqt_uom.id = lqt.lot_unit
|
||||
WHERE vlot.lot_type = 'virtual'
|
||||
GROUP BY vlot.id, vlot.line
|
||||
GROUP BY vlot.id, vlot.line, pp.id, pp.number
|
||||
),
|
||||
sale_lot_qt_balance AS (
|
||||
SELECT
|
||||
vlot.id AS virtual_lot_id,
|
||||
vlot.sale_line AS line_id,
|
||||
ss.id AS contract_id,
|
||||
ss.number AS contract_number,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN lqt_uom.category = line_uom.category
|
||||
@@ -222,18 +249,24 @@ sale_lot_qt_balance AS (
|
||||
) AS lot_qt_cross_category_count
|
||||
FROM lot_lot vlot
|
||||
JOIN sale_line sl ON sl.id = vlot.sale_line
|
||||
LEFT JOIN sale_sale ss ON ss.id = sl.sale
|
||||
JOIN uom line_uom ON line_uom.id = sl.unit
|
||||
LEFT JOIN lot_current current ON current.lot_id = vlot.id
|
||||
LEFT JOIN uom vlot_uom ON vlot_uom.id = current.lot_unit_line
|
||||
LEFT JOIN lot_qt lqt ON lqt.lot_s = vlot.id
|
||||
LEFT JOIN lot_qt lqt
|
||||
ON lqt.lot_s = vlot.id
|
||||
AND COALESCE(lqt.lot_quantity, 0) <> 0
|
||||
LEFT JOIN uom lqt_uom ON lqt_uom.id = lqt.lot_unit
|
||||
WHERE vlot.lot_type = 'virtual'
|
||||
GROUP BY vlot.id, vlot.sale_line
|
||||
GROUP BY vlot.id, vlot.sale_line, ss.id, ss.number
|
||||
)
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT
|
||||
'purchase_line_virtual_count' AS check_name,
|
||||
'purchase.purchase' AS contract_model,
|
||||
contract_id,
|
||||
contract_number,
|
||||
line_id,
|
||||
virtual_lot_id,
|
||||
virtual_count::numeric AS observed_value,
|
||||
@@ -248,6 +281,9 @@ FROM (
|
||||
|
||||
SELECT
|
||||
'sale_line_virtual_count' AS check_name,
|
||||
'sale.sale' AS contract_model,
|
||||
contract_id,
|
||||
contract_number,
|
||||
line_id,
|
||||
virtual_lot_id,
|
||||
virtual_count::numeric AS observed_value,
|
||||
@@ -262,6 +298,9 @@ FROM (
|
||||
|
||||
SELECT
|
||||
'purchase_line_physical_plus_virtual' AS check_name,
|
||||
'purchase.purchase' AS contract_model,
|
||||
q.contract_id,
|
||||
q.contract_number,
|
||||
q.line_id,
|
||||
q.virtual_lot_id,
|
||||
ROUND(q.physical_quantity + COALESCE(q.virtual_quantity, 0), 5)
|
||||
@@ -284,6 +323,9 @@ FROM (
|
||||
|
||||
SELECT
|
||||
'sale_line_physical_plus_virtual' AS check_name,
|
||||
'sale.sale' AS contract_model,
|
||||
q.contract_id,
|
||||
q.contract_number,
|
||||
q.line_id,
|
||||
q.virtual_lot_id,
|
||||
ROUND(q.physical_quantity + COALESCE(q.virtual_quantity, 0), 5)
|
||||
@@ -306,6 +348,9 @@ FROM (
|
||||
|
||||
SELECT
|
||||
'purchase_lot_qt_equals_virtual' AS check_name,
|
||||
'purchase.purchase' AS contract_model,
|
||||
b.contract_id,
|
||||
b.contract_number,
|
||||
b.line_id,
|
||||
b.virtual_lot_id,
|
||||
ROUND(b.lot_qt_quantity, 5) AS observed_value,
|
||||
@@ -321,6 +366,9 @@ FROM (
|
||||
|
||||
SELECT
|
||||
'sale_lot_qt_equals_virtual' AS check_name,
|
||||
'sale.sale' AS contract_model,
|
||||
b.contract_id,
|
||||
b.contract_number,
|
||||
b.line_id,
|
||||
b.virtual_lot_id,
|
||||
ROUND(b.lot_qt_quantity, 5) AS observed_value,
|
||||
@@ -336,6 +384,9 @@ FROM (
|
||||
|
||||
SELECT
|
||||
'purchase_cross_category_manual_review' AS check_name,
|
||||
'purchase.purchase' AS contract_model,
|
||||
q.contract_id,
|
||||
q.contract_number,
|
||||
q.line_id,
|
||||
q.virtual_lot_id,
|
||||
(q.physical_cross_category_count + q.virtual_cross_category_count)::numeric,
|
||||
@@ -350,6 +401,9 @@ FROM (
|
||||
|
||||
SELECT
|
||||
'sale_cross_category_manual_review' AS check_name,
|
||||
'sale.sale' AS contract_model,
|
||||
q.contract_id,
|
||||
q.contract_number,
|
||||
q.line_id,
|
||||
q.virtual_lot_id,
|
||||
(q.physical_cross_category_count + q.virtual_cross_category_count)::numeric,
|
||||
@@ -364,6 +418,9 @@ FROM (
|
||||
|
||||
SELECT
|
||||
'lot_qt_cross_category_manual_review' AS check_name,
|
||||
'purchase.purchase' AS contract_model,
|
||||
b.contract_id,
|
||||
b.contract_number,
|
||||
b.line_id,
|
||||
b.virtual_lot_id,
|
||||
b.lot_qt_cross_category_count::numeric,
|
||||
@@ -378,6 +435,9 @@ FROM (
|
||||
|
||||
SELECT
|
||||
'sale_lot_qt_cross_category_manual_review' AS check_name,
|
||||
'sale.sale' AS contract_model,
|
||||
b.contract_id,
|
||||
b.contract_number,
|
||||
b.line_id,
|
||||
b.virtual_lot_id,
|
||||
b.lot_qt_cross_category_count::numeric,
|
||||
@@ -387,5 +447,23 @@ FROM (
|
||||
AS detail
|
||||
FROM sale_lot_qt_balance b
|
||||
WHERE b.lot_qt_cross_category_count > 0
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'lot_qt_without_lot_link' AS check_name,
|
||||
NULL::text AS contract_model,
|
||||
NULL::integer AS contract_id,
|
||||
NULL::text AS contract_number,
|
||||
NULL::integer AS line_id,
|
||||
NULL::integer AS virtual_lot_id,
|
||||
COALESCE(lqt.lot_quantity, 0)::numeric AS observed_value,
|
||||
0::numeric AS expected_value,
|
||||
COALESCE(lqt.lot_quantity, 0)::numeric AS diff,
|
||||
'lot_qt row has neither lot_p nor lot_s; zero rows are allowed only when linked to at least one virtual lot'
|
||||
AS detail
|
||||
FROM lot_qt lqt
|
||||
WHERE lqt.lot_p IS NULL
|
||||
AND lqt.lot_s IS NULL
|
||||
) issues
|
||||
ORDER BY check_name, line_id, virtual_lot_id;
|
||||
ORDER BY check_name, contract_number, line_id, virtual_lot_id;
|
||||
|
||||
@@ -1391,13 +1391,17 @@ class LotQt(
|
||||
Move.save([move])
|
||||
|
||||
@classmethod
|
||||
def validate(cls, lotqts):
|
||||
super(LotQt, cls).validate(lotqts)
|
||||
Date = Pool().get('ir.date')
|
||||
#Update Move
|
||||
for lqt in lotqts:
|
||||
cls.updateMove(lqt.lot_move)
|
||||
#generate valuation for purchase and sale
|
||||
def validate(cls, lotqts):
|
||||
super(LotQt, cls).validate(lotqts)
|
||||
Date = Pool().get('ir.date')
|
||||
#Update Move
|
||||
for lqt in lotqts:
|
||||
if not lqt.lot_p and not lqt.lot_s:
|
||||
raise UserError(
|
||||
"Lot quantity forecast must be linked to a purchase "
|
||||
"virtual lot or a sale virtual lot.")
|
||||
cls.updateMove(lqt.lot_move)
|
||||
#generate valuation for purchase and sale
|
||||
logger.info("VALIDATE_LQT_LS:%s",lqt.lot_s)
|
||||
logger.info("VALIDATE_LQT_LP:%s",lqt.lot_p)
|
||||
if lqt.lot_p and lqt.lot_quantity > 0:
|
||||
|
||||
Reference in New Issue
Block a user