-- Active: 1778691597138@@72.61.163.139@5433@tradon -- Active: 1778686654594@@72.61.163.139@5435@tradon /* Purchase Trade quantity consistency diagnostics. Read-only script. It returns rows only when a consistency issue is detected. Rules checked: 1. For each trade line with one virtual lot: sum(physical lots current quantity) + virtual lot current quantity = line quantity_theorical. 2. For each virtual purchase 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(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 completely ignored by the checks. They may be the memory of an open quantity consumed by a physical lot. This also matters when the virtual lot becomes negative to compensate the difference between theoretical and executed quantity: lot_qt represents usable open forecast and stops at zero. Notes: - The script uses the lot current state, i.e. lot_lot.lot_state joined to lot_qt_hist.quantity_type. - Quantities are converted with product_uom.factor when source and target units are in the same category. - Cross-category conversions are reported separately because the Python code sometimes passes an explicit factor/rate and SQL cannot infer that business context safely. */ WITH params AS ( SELECT 0.00001::numeric AS epsilon ), uom AS ( SELECT id, name, symbol, category, factor::numeric AS factor FROM product_uom ), lot_current AS ( SELECT lot.id AS lot_id, lot.line AS purchase_line, lot.sale_line AS sale_line, lot.lot_type, lot.lot_state, lot.lot_unit_line, hist.quantity_type, COALESCE(hist.quantity, 0)::numeric AS current_quantity FROM lot_lot lot LEFT JOIN lot_qt_hist hist ON hist.lot = lot.id AND hist.quantity_type = lot.lot_state ), 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, 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, 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, base.virtual_count, base.virtual_lot_id, COALESCE(SUM( CASE WHEN phys_uom.category = line_uom.category THEN phys.current_quantity * phys_uom.factor / line_uom.factor ELSE NULL END), 0)::numeric AS physical_quantity, MAX( CASE WHEN vlot_uom.category = line_uom.category THEN vlot.current_quantity * vlot_uom.factor / line_uom.factor ELSE NULL END)::numeric AS virtual_quantity, COUNT(*) FILTER ( WHERE phys.lot_id IS NOT NULL AND phys_uom.category IS DISTINCT FROM line_uom.category ) AS physical_cross_category_count, CASE WHEN vlot.lot_id IS NOT NULL AND vlot_uom.category IS DISTINCT FROM line_uom.category THEN 1 ELSE 0 END AS virtual_cross_category_count FROM purchase_line_base base LEFT JOIN uom line_uom ON line_uom.id = base.line_unit LEFT JOIN lot_current vlot ON vlot.lot_id = base.virtual_lot_id LEFT JOIN uom vlot_uom ON vlot_uom.id = vlot.lot_unit_line LEFT JOIN lot_current phys ON phys.purchase_line = base.line_id AND phys.lot_type = 'physic' LEFT JOIN uom phys_uom ON phys_uom.id = phys.lot_unit_line GROUP BY 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 ), sale_line_quantities AS ( SELECT 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, COALESCE(SUM( CASE WHEN phys_uom.category = line_uom.category THEN phys.current_quantity * phys_uom.factor / line_uom.factor ELSE NULL END), 0)::numeric AS physical_quantity, MAX( CASE WHEN vlot_uom.category = line_uom.category THEN vlot.current_quantity * vlot_uom.factor / line_uom.factor ELSE NULL END)::numeric AS virtual_quantity, COUNT(*) FILTER ( WHERE phys.lot_id IS NOT NULL AND phys_uom.category IS DISTINCT FROM line_uom.category ) AS physical_cross_category_count, CASE WHEN vlot.lot_id IS NOT NULL AND vlot_uom.category IS DISTINCT FROM line_uom.category THEN 1 ELSE 0 END AS virtual_cross_category_count FROM sale_line_base base LEFT JOIN uom line_uom ON line_uom.id = base.line_unit LEFT JOIN lot_current vlot ON vlot.lot_id = base.virtual_lot_id LEFT JOIN uom vlot_uom ON vlot_uom.id = vlot.lot_unit_line LEFT JOIN lot_current phys ON phys.sale_line = base.line_id AND phys.lot_type = 'physic' LEFT JOIN uom phys_uom ON phys_uom.id = phys.lot_unit_line GROUP BY 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 ), 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 THEN COALESCE(lqt.lot_quantity, 0)::numeric * lqt_uom.factor / line_uom.factor ELSE NULL END), 0)::numeric AS lot_qt_quantity, MAX( CASE WHEN vlot_uom.category = line_uom.category THEN current.current_quantity * vlot_uom.factor / line_uom.factor ELSE NULL END)::numeric AS virtual_quantity, COUNT(*) FILTER ( WHERE lqt.id IS NOT NULL AND lqt_uom.category IS DISTINCT FROM line_uom.category ) 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 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, 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 THEN COALESCE(lqt.lot_quantity, 0)::numeric * lqt_uom.factor / line_uom.factor ELSE NULL END), 0)::numeric AS lot_qt_quantity, MAX( CASE WHEN vlot_uom.category = line_uom.category THEN current.current_quantity * vlot_uom.factor / line_uom.factor ELSE NULL END)::numeric AS virtual_quantity, COUNT(*) FILTER ( WHERE lqt.id IS NOT NULL AND lqt_uom.category IS DISTINCT FROM line_uom.category ) 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 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, 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, 1::numeric AS expected_value, (virtual_count - 1)::numeric AS diff, 'purchase.line must have exactly one virtual lot once initialized' AS detail FROM purchase_line_base WHERE virtual_count <> 1 UNION ALL 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, 1::numeric AS expected_value, (virtual_count - 1)::numeric AS diff, 'sale.line must have exactly one virtual lot once initialized' AS detail FROM sale_line_base WHERE virtual_count <> 1 UNION ALL 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) AS observed_value, ROUND(q.theoretical_quantity, 5) AS expected_value, ROUND( q.physical_quantity + COALESCE(q.virtual_quantity, 0) - q.theoretical_quantity, 5) AS diff, 'physical lots + virtual lot must equal purchase quantity_theorical' AS detail FROM purchase_line_quantities q, params p WHERE q.virtual_count = 1 AND q.physical_cross_category_count = 0 AND q.virtual_cross_category_count = 0 AND ABS( q.physical_quantity + COALESCE(q.virtual_quantity, 0) - q.theoretical_quantity) > p.epsilon UNION ALL 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) AS observed_value, ROUND(q.theoretical_quantity, 5) AS expected_value, ROUND( q.physical_quantity + COALESCE(q.virtual_quantity, 0) - q.theoretical_quantity, 5) AS diff, 'physical lots + virtual lot must equal sale quantity_theorical' AS detail FROM sale_line_quantities q, params p WHERE q.virtual_count = 1 AND q.physical_cross_category_count = 0 AND q.virtual_cross_category_count = 0 AND ABS( q.physical_quantity + COALESCE(q.virtual_quantity, 0) - q.theoretical_quantity) > p.epsilon UNION ALL 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, ROUND(GREATEST(COALESCE(b.virtual_quantity, 0), 0), 5) AS expected_value, 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' AS detail FROM purchase_lot_qt_balance b, params p WHERE b.lot_qt_cross_category_count = 0 AND ABS( b.lot_qt_quantity - GREATEST(COALESCE(b.virtual_quantity, 0), 0)) > p.epsilon UNION ALL 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, ROUND(GREATEST(COALESCE(b.virtual_quantity, 0), 0), 5) AS expected_value, 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' AS detail FROM sale_lot_qt_balance b, params p WHERE b.lot_qt_cross_category_count = 0 AND ABS( b.lot_qt_quantity - GREATEST(COALESCE(b.virtual_quantity, 0), 0)) > p.epsilon UNION ALL 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, 0::numeric, (q.physical_cross_category_count + q.virtual_cross_category_count)::numeric, 'line has lot quantities in another UoM category; review with Python conversion context' AS detail FROM purchase_line_quantities q WHERE q.physical_cross_category_count + q.virtual_cross_category_count > 0 UNION ALL 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, 0::numeric, (q.physical_cross_category_count + q.virtual_cross_category_count)::numeric, 'line has lot quantities in another UoM category; review with Python conversion context' AS detail FROM sale_line_quantities q WHERE q.physical_cross_category_count + q.virtual_cross_category_count > 0 UNION ALL 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, 0::numeric, b.lot_qt_cross_category_count::numeric, 'lot_qt contains quantities in another UoM category; review with Python conversion context' AS detail FROM purchase_lot_qt_balance b WHERE b.lot_qt_cross_category_count > 0 UNION ALL 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, 0::numeric, b.lot_qt_cross_category_count::numeric, 'lot_qt contains quantities in another UoM category; review with Python conversion context' 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, 'non-zero lot_qt row has neither lot_p nor lot_s' AS detail FROM lot_qt lqt WHERE lqt.lot_p IS NULL AND lqt.lot_s IS NULL AND COALESCE(lqt.lot_quantity, 0) <> 0 ) issues ORDER BY check_name, contract_number, line_id, virtual_lot_id;