-- 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 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.
- 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(COALESCE(b.virtual_quantity, 0), 5) AS expected_value,
        ROUND(b.lot_qt_quantity - COALESCE(b.virtual_quantity, 0), 5) AS diff,
        'sum lot_qt where lot_p = virtual lot must equal 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 - COALESCE(b.virtual_quantity, 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(COALESCE(b.virtual_quantity, 0), 5) AS expected_value,
        ROUND(b.lot_qt_quantity - COALESCE(b.virtual_quantity, 0), 5) AS diff,
        'sum lot_qt where lot_s = virtual lot must equal 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 - COALESCE(b.virtual_quantity, 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,
        '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, contract_number, line_id, virtual_lot_id;
