481 lines
17 KiB
SQL
481 lines
17 KiB
SQL
-- 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;
|