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 9785d12..bf6a291 100644 --- a/modules/purchase_trade/docs/business/lots-and-quantities.en.md +++ b/modules/purchase_trade/docs/business/lots-and-quantities.en.md @@ -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 diff --git a/modules/purchase_trade/docs/business/lots-and-quantities.md b/modules/purchase_trade/docs/business/lots-and-quantities.md index e72dd12..b75503e 100644 --- a/modules/purchase_trade/docs/business/lots-and-quantities.md +++ b/modules/purchase_trade/docs/business/lots-and-quantities.md @@ -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 diff --git a/modules/purchase_trade/docs/business/sql/README.md b/modules/purchase_trade/docs/business/sql/README.md index 25b5631..2952d5e 100644 --- a/modules/purchase_trade/docs/business/sql/README.md +++ b/modules/purchase_trade/docs/business/sql/README.md @@ -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. 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 54c6cd4..1627e61 100644 --- a/modules/purchase_trade/docs/business/sql/quantity_consistency_checks.sql +++ b/modules/purchase_trade/docs/business/sql/quantity_consistency_checks.sql @@ -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; diff --git a/modules/purchase_trade/lot.py b/modules/purchase_trade/lot.py index 009f64b..783c22f 100755 --- a/modules/purchase_trade/lot.py +++ b/modules/purchase_trade/lot.py @@ -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: