diff --git a/ITSA/Data Reconciliation/Contracts and Costs.xlsx b/ITSA/Data Reconciliation/Contracts and Costs.xlsx new file mode 100644 index 0000000..b2afc11 Binary files /dev/null and b/ITSA/Data Reconciliation/Contracts and Costs.xlsx differ diff --git a/ITSA/Data Reconciliation/ITSA_Forward_Position_Comparison.xlsx b/ITSA/Data Reconciliation/ITSA_Forward_Position_Comparison.xlsx new file mode 100644 index 0000000..45738d9 Binary files /dev/null and b/ITSA/Data Reconciliation/ITSA_Forward_Position_Comparison.xlsx differ diff --git a/ITSA/Data Reconciliation/update_vw_bi_itsa_physical_open_costs_v2.sql b/ITSA/Data Reconciliation/update_vw_bi_itsa_physical_open_costs_v2.sql new file mode 100644 index 0000000..aa1ed18 --- /dev/null +++ b/ITSA/Data Reconciliation/update_vw_bi_itsa_physical_open_costs_v2.sql @@ -0,0 +1,267 @@ +CREATE OR REPLACE VIEW public.vw_utility_physical_pricing_definition AS +SELECT + 'Purchase'::text AS "strContractType", + pc.line AS "intContractLineId", + pc.id AS "intPricingComponentId", + pc.price_index AS "intPriceCurveId", + vpc."strPriceDescription", + vpc."strPriceIndex", + vpc."strCurrency", + vpc."strUnit", + vpc."strPriceCurveType", + vpc."strPriceArea", + vpc."strPriceCalendar", + vpc."strPricingType", + vpc."dtmPricingStartDate", + vpc."dtmPricingEndDate", + vpc."strPricingMonth" +FROM pricing_component pc +LEFT JOIN vw_utility_price_curve vpc + ON vpc."intPriceCurveId" = pc.price_index +WHERE pc.line IS NOT NULL + +UNION ALL + +SELECT + 'Sale'::text AS "strContractType", + pc.sale_line AS "intContractLineId", + pc.id AS "intPricingComponentId", + pc.price_index AS "intPriceCurveId", + vpc."strPriceDescription", + vpc."strPriceIndex", + vpc."strCurrency", + vpc."strUnit", + vpc."strPriceCurveType", + vpc."strPriceArea", + vpc."strPriceCalendar", + vpc."strPricingType", + vpc."dtmPricingStartDate", + vpc."dtmPricingEndDate", + vpc."strPricingMonth" +FROM pricing_component pc +LEFT JOIN vw_utility_price_curve vpc + ON vpc."intPriceCurveId" = pc.price_index +WHERE pc.sale_line IS NOT NULL; + +CREATE OR REPLACE VIEW public.vw_bi_itsa_physical_open_costs_v2 AS +WITH valuation_info AS ( + SELECT + pvl."intContractId", + pvl."intContractLineId", + pvl."strState", + avg(pvl."dblPrice") AS "avgPrice", + avg(pvl."dblMtmPrice")::numeric(18, 2) AS "avgMtmPrice" + FROM vw_utility_physical_valuation_line pvl + GROUP BY + pvl."intContractId", + pvl."intContractLineId", + pvl."strState" +), +utility_open_quantity AS ( + SELECT + CASE + WHEN uop."strContractSide" = 'purchase'::text THEN 'Purchase'::text + WHEN uop."strContractSide" = 'sale'::text THEN 'Sale'::text + ELSE NULL::text + END AS "TradeType", + uop."intContractLineId" AS "TradeLineId", + sum(uop."dblOpenQuantity")::numeric(18, 3) AS "OpenQuantitySigned" + FROM vw_utility_open_position uop + GROUP BY + CASE + WHEN uop."strContractSide" = 'purchase'::text THEN 'Purchase'::text + WHEN uop."strContractSide" = 'sale'::text THEN 'Sale'::text + ELSE NULL::text + END, + uop."intContractLineId" +), +contract_open_quantity AS ( + SELECT + 'Purchase'::text AS "TradeType", + pc."intPurchaseLineId" AS "TradeLineId", + abs(COALESCE(uop."OpenQuantitySigned", 0::numeric))::numeric(18, 3) AS "OpenQuantity", + COALESCE(uop."OpenQuantitySigned", 0::numeric)::numeric(18, 3) AS "OpenQuantitySigned", + COALESCE(vi."avgMtmPrice", 0::numeric(18, 2)) AS "AvgMtmPrice", + pc."dblTheoriticalQuantity" AS "TheoriticalQuantitySigned" + FROM vw_utility_purchase_physical_contract pc + LEFT JOIN utility_open_quantity uop + ON uop."TradeType" = 'Purchase'::text + AND uop."TradeLineId" = pc."intPurchaseLineId" + LEFT JOIN valuation_info vi + ON pc."intPurchaseLineId" = vi."intContractLineId" + + UNION ALL + + SELECT + 'Sale'::text AS "TradeType", + pc."intSaleLineId" AS "TradeLineId", + abs(COALESCE(uop."OpenQuantitySigned", 0::numeric))::numeric(18, 3) AS "OpenQuantity", + COALESCE(uop."OpenQuantitySigned", 0::numeric)::numeric(18, 3) AS "OpenQuantitySigned", + COALESCE(vi."avgMtmPrice", 0::numeric(18, 2)) AS "AvgMtmPrice", + pc."dblTheoreticalQuantity" * '-1'::integer::numeric AS "TheoriticalQuantitySigned" + FROM vw_utility_sale_physical_contract pc + LEFT JOIN utility_open_quantity uop + ON uop."TradeType" = 'Sale'::text + AND uop."TradeLineId" = pc."intSaleLineId" + LEFT JOIN valuation_info vi + ON pc."intSaleLineId" = vi."intContractLineId" +) +SELECT + 1 AS "Group Order", + 'Price'::text AS "Group Type", + ctr."intPurchaseLineId" AS "intTradeLineId", + CASE + WHEN ctr."strPricingType"::text = 'basis'::text THEN COALESCE(prc."strPriceDescription", 'Basis Price Curve Missing'::character varying) + ELSE 'Fixed Price'::character varying + END AS "Line Description", + pur."dblUnitPrice" AS "Price", + opq."OpenQuantitySigned" AS "Open Quantity Signed", + opq."OpenQuantitySigned" * pur."dblUnitPrice" * '-1'::integer::numeric AS "Open Cost Amount", + 1 AS "intPriceGroupId", + ctr."dtmEstimatedBLDate" AS "Estimated BL Date", + opq."TheoriticalQuantitySigned" AS "Contract Quantity Signed", + 'Price'::text AS "Line Group" +FROM vw_utility_dim_physical_purchase_contract ctr +JOIN vw_utility_purchase_physical_contract pur + ON ctr."intPurchaseLineId" = pur."intPurchaseLineId" +JOIN contract_open_quantity opq + ON ctr."intPurchaseLineId" = opq."TradeLineId" + AND opq."TradeType" = 'Purchase'::text +LEFT JOIN vw_utility_physical_pricing_definition prc + ON prc."strContractType" = 'Purchase'::text + AND prc."intContractLineId" = ctr."intPurchaseLineId" +WHERE + ctr."strPricingType"::text IN ('priced'::text, 'basis'::text) + AND opq."OpenQuantity" <> 0::numeric + +UNION ALL + +SELECT + 2 AS "Group Order", + 'MTM'::text AS "Group Type", + ctr."intPurchaseLineId" AS "intTradeLineId", + mtm."strComponentCurve" AS "Line Description", + opq."AvgMtmPrice" AS "Price", + opq."OpenQuantitySigned" AS "Open Quantity Signed", + opq."OpenQuantitySigned" * opq."AvgMtmPrice" AS "Open Cost Amount", + 3 AS "intPriceGroupId", + ctr."dtmEstimatedBLDate" AS "Estimated BL Date", + opq."TheoriticalQuantitySigned" AS "Contract Quantity Signed", + 'MTM'::text AS "Line Group" +FROM vw_utility_physical_mtm_definition mtm +JOIN vw_utility_dim_physical_purchase_contract ctr + ON mtm."intContractLineId" = ctr."intPurchaseLineId" +JOIN contract_open_quantity opq + ON ctr."intPurchaseLineId" = opq."TradeLineId" + AND opq."TradeType" = 'Purchase'::text +WHERE + mtm."strContractType" = 'Purchase'::text + AND opq."OpenQuantity" <> 0::numeric + +UNION ALL + +SELECT + 3 AS "Group Order", + 'Fee'::text AS "Group Type", + fees."intContractLineId" AS "intTradeLineId", + fees."strProduct" AS "Line Description", + fees."dblPrice" AS "Price", + opq."OpenQuantitySigned" AS "Open Quantity Signed", + CASE + WHEN fees."strPayOrRec"::text = 'pay'::text THEN 1 + ELSE '-1'::integer + END::numeric * opq."OpenQuantity" * fees."dblPrice" * '-1'::integer::numeric AS "Open Cost Amount", + 2 AS "intPriceGroupId", + ctr."dtmEstimatedBLDate" AS "Estimated BL Date", + opq."TheoriticalQuantitySigned" AS "Contract Quantity Signed", + fees."strProductGroup" AS "Line Group" +FROM vw_utility_contract_fees fees +JOIN vw_utility_dim_physical_purchase_contract ctr + ON fees."intContractLineId" = ctr."intPurchaseLineId" +JOIN contract_open_quantity opq + ON ctr."intPurchaseLineId" = opq."TradeLineId" + AND opq."TradeType" = 'Purchase'::text +WHERE + fees."strContractType" = 'Purchase'::text + AND opq."OpenQuantity" <> 0::numeric + +UNION ALL + +SELECT + 1 AS "Group Order", + 'Price'::text AS "Group Type", + ctr."intSaleLineId" AS "intTradeLineId", + CASE + WHEN ctr."strPricingType"::text = 'basis'::text THEN COALESCE(prc."strPriceDescription", 'Basis Price Curve Missing'::character varying) + ELSE 'Fixed Price'::character varying + END AS "Line Description", + sale."dblUnitPrice" AS "Price", + opq."OpenQuantitySigned" AS "Open Quantity Signed", + opq."OpenQuantitySigned" * sale."dblUnitPrice" * '-1'::integer::numeric AS "Open Cost Amount", + 1 AS "intPriceGroupId", + ctr."dtmEstimatedBLDate" AS "Estimated BL Date", + opq."TheoriticalQuantitySigned" AS "Contract Quantity Signed", + 'Price'::text AS "Line Group" +FROM vw_utility_dim_physical_sale_contract ctr +JOIN vw_utility_sale_physical_contract sale + ON ctr."intSaleLineId" = sale."intSaleLineId" +JOIN contract_open_quantity opq + ON ctr."intSaleLineId" = opq."TradeLineId" + AND opq."TradeType" = 'Sale'::text +LEFT JOIN vw_utility_physical_pricing_definition prc + ON prc."strContractType" = 'Sale'::text + AND prc."intContractLineId" = ctr."intSaleLineId" +WHERE + ctr."strPricingType"::text IN ('priced'::text, 'basis'::text) + AND opq."OpenQuantity" <> 0::numeric + +UNION ALL + +SELECT + 2 AS "Group Order", + 'MTM'::text AS "Group Type", + ctr."intSaleLineId" AS "intTradeLineId", + mtm."strComponentCurve" AS "Line Description", + opq."AvgMtmPrice" AS "Price", + opq."OpenQuantitySigned" AS "Open Quantity Signed", + opq."OpenQuantitySigned" * opq."AvgMtmPrice" AS "Open Cost Amount", + 3 AS "intPriceGroupId", + ctr."dtmEstimatedBLDate" AS "Estimated BL Date", + opq."TheoriticalQuantitySigned" AS "Contract Quantity Signed", + 'MTM'::text AS "Line Group" +FROM vw_utility_physical_mtm_definition mtm +JOIN vw_utility_dim_physical_sale_contract ctr + ON mtm."intContractLineId" = ctr."intSaleLineId" +JOIN contract_open_quantity opq + ON ctr."intSaleLineId" = opq."TradeLineId" + AND opq."TradeType" = 'Sale'::text +WHERE + mtm."strContractType" = 'Sale'::text + AND opq."OpenQuantity" <> 0::numeric + +UNION ALL + +SELECT + 3 AS "Group Order", + 'Fee'::text AS "Group Type", + fees."intContractLineId" AS "intTradeLineId", + fees."strProduct" AS "Line Description", + fees."dblPrice" AS "Price", + opq."OpenQuantitySigned" AS "Open Quantity Signed", + CASE + WHEN fees."strPayOrRec"::text = 'pay'::text THEN 1 + ELSE '-1'::integer + END::numeric * opq."OpenQuantity" * fees."dblPrice" * '-1'::integer::numeric AS "Open Cost Amount", + 2 AS "intPriceGroupId", + ctr."dtmEstimatedBLDate" AS "Estimated BL Date", + opq."TheoriticalQuantitySigned" AS "Contract Quantity Signed", + fees."strProductGroup" AS "Line Group" +FROM vw_utility_contract_fees fees +JOIN vw_utility_dim_physical_sale_contract ctr + ON fees."intContractLineId" = ctr."intSaleLineId" +JOIN contract_open_quantity opq + ON ctr."intSaleLineId" = opq."TradeLineId" + AND opq."TradeType" = 'Sale'::text +WHERE + fees."strContractType" = 'Sale'::text + AND opq."OpenQuantity" <> 0::numeric; diff --git a/ITSA/Data Reconciliation/update_vw_utility_open_position.sql b/ITSA/Data Reconciliation/update_vw_utility_open_position.sql new file mode 100644 index 0000000..885bee8 --- /dev/null +++ b/ITSA/Data Reconciliation/update_vw_utility_open_position.sql @@ -0,0 +1,154 @@ +CREATE OR REPLACE VIEW public.vw_utility_open_position AS +WITH physical_lot_quantity AS ( + SELECT + 'purchase'::text AS side, + l.line AS contract_line_id, + SUM(COALESCE(qh.quantity, 0::numeric)) AS physical_quantity + FROM lot_lot l + LEFT JOIN lot_qt_hist qh + ON qh.lot = l.id + AND qh.quantity_type = l.lot_state + WHERE + l.lot_type::text = 'physic'::text + AND l.line IS NOT NULL + GROUP BY + l.line + + UNION ALL + + SELECT + 'sale'::text AS side, + l.sale_line AS contract_line_id, + SUM(COALESCE(qh.quantity, 0::numeric)) AS physical_quantity + FROM lot_lot l + LEFT JOIN lot_qt_hist qh + ON qh.lot = l.id + AND qh.quantity_type = l.lot_state + WHERE + l.lot_type::text = 'physic'::text + AND l.sale_line IS NOT NULL + GROUP BY + l.sale_line +), +matched_virtual_quantity AS ( + SELECT + 'purchase'::text AS side, + lp.line AS contract_line_id, + SUM(lqt.lot_quantity) AS matched_quantity + FROM lot_qt lqt + JOIN lot_lot lp + ON lp.id = lqt.lot_p + WHERE + lp.lot_type::text = 'virtual'::text + AND lp.line IS NOT NULL + AND lqt.lot_s IS NOT NULL + AND lqt.lot_quantity > 0::numeric + GROUP BY + lp.line + + UNION ALL + + SELECT + 'sale'::text AS side, + ls.sale_line AS contract_line_id, + SUM(lqt.lot_quantity) AS matched_quantity + FROM lot_qt lqt + JOIN lot_lot ls + ON ls.id = lqt.lot_s + WHERE + ls.lot_type::text = 'virtual'::text + AND ls.sale_line IS NOT NULL + AND lqt.lot_p IS NOT NULL + AND lqt.lot_quantity > 0::numeric + GROUP BY + ls.sale_line +), +open_virtual_lots AS ( + SELECT + 'purchase'::text AS side, + lqt.id AS int_lot_qt_id, + lp.id AS int_lot_id, + pl.id AS int_contract_line_id, + pp.party AS int_counterparty_id, + pl.product AS int_product_id, + uom.name AS uom, + lqt.lot_quantity AS open_quantity, + pl.from_del AS period_start, + pl.to_del AS period_end, + COALESCE(plq.physical_quantity, 0::numeric) AS physical_lot_quantity, + pl.quantity_theorical - COALESCE(plq.physical_quantity, 0::numeric) + - COALESCE(mvq.matched_quantity, 0::numeric) + AS open_targeted_quantity + FROM lot_qt lqt + JOIN lot_lot lp + ON lp.id = lqt.lot_p + JOIN purchase_line pl + ON pl.id = lp.line + JOIN purchase_purchase pp + ON pp.id = pl.purchase + LEFT JOIN product_uom uom + ON uom.id = lqt.lot_unit + LEFT JOIN physical_lot_quantity plq + ON plq.side = 'purchase'::text + AND plq.contract_line_id = pl.id + LEFT JOIN matched_virtual_quantity mvq + ON mvq.side = 'purchase'::text + AND mvq.contract_line_id = pl.id + WHERE + lp.lot_type::text = 'virtual'::text + AND lqt.lot_s IS NULL + AND COALESCE(pl.finished, false) = false + AND lqt.lot_quantity > 0::numeric + + UNION ALL + + SELECT + 'sale'::text AS side, + lqt.id AS int_lot_qt_id, + ls.id AS int_lot_id, + sl.id AS int_contract_line_id, + ss.party AS int_counterparty_id, + sl.product AS int_product_id, + uom.name AS uom, + -lqt.lot_quantity AS open_quantity, + sl.from_del AS period_start, + sl.to_del AS period_end, + COALESCE(plq.physical_quantity, 0::numeric) AS physical_lot_quantity, + sl.quantity_theorical - COALESCE(plq.physical_quantity, 0::numeric) + - COALESCE(mvq.matched_quantity, 0::numeric) + AS open_targeted_quantity + FROM lot_qt lqt + JOIN lot_lot ls + ON ls.id = lqt.lot_s + JOIN sale_line sl + ON sl.id = ls.sale_line + JOIN sale_sale ss + ON ss.id = sl.sale + LEFT JOIN product_uom uom + ON uom.id = lqt.lot_unit + LEFT JOIN physical_lot_quantity plq + ON plq.side = 'sale'::text + AND plq.contract_line_id = sl.id + LEFT JOIN matched_virtual_quantity mvq + ON mvq.side = 'sale'::text + AND mvq.contract_line_id = sl.id + WHERE + ls.lot_type::text = 'virtual'::text + AND lqt.lot_p IS NULL + AND COALESCE(sl.finished, false) = false + AND lqt.lot_quantity > 0::numeric +) +SELECT + side AS "strContractSide", + int_lot_qt_id AS "intLotQtId", + int_lot_id AS "intLotId", + int_contract_line_id AS "intContractLineId", + int_counterparty_id AS "intCounterpartyId", + int_product_id AS "intProductId", + uom AS "strUom", + open_quantity AS "dblOpenQuantity", + period_start AS "dtmPeriodStart", + period_end AS "dtmPeriodEnd", + physical_lot_quantity AS "dblPhysicalLotQuantity", + open_targeted_quantity AS "dblOpenTargetedQuantity" +FROM open_virtual_lots; diff --git a/ITSA/Data Reconciliation/update_vw_utility_shipment_fee_allocation.sql b/ITSA/Data Reconciliation/update_vw_utility_shipment_fee_allocation.sql new file mode 100644 index 0000000..3bd0f7c --- /dev/null +++ b/ITSA/Data Reconciliation/update_vw_utility_shipment_fee_allocation.sql @@ -0,0 +1,495 @@ +CREATE OR REPLACE VIEW public.vw_utility_shipment_fee_allocation AS +WITH physical_shipment_lots AS ( + SELECT + l.lot_shipment_in AS shipment_id, + l.id AS lot_id, + l.lot_type, + l.line AS purchase_line_id, + l.sale_line AS sale_line_id, + qh.quantity AS shipment_quantity, + l.lot_unit_line AS shipment_unit_id + FROM lot_lot l + LEFT JOIN lot_qt_hist qh + ON qh.lot = l.id + AND qh.quantity_type = l.lot_state + WHERE + l.lot_type::text = 'physic'::text + AND l.lot_shipment_in IS NOT NULL +), +virtual_shipment_lots AS ( + SELECT + q.lot_shipment_in AS shipment_id, + vp.id AS lot_id, + vp.lot_type, + vp.line AS purchase_line_id, + COALESCE(vp.sale_line, ps.sale_line) AS sale_line_id, + q.lot_quantity AS shipment_quantity, + q.lot_unit AS shipment_unit_id + FROM lot_qt q + JOIN lot_lot vp + ON vp.id = q.lot_p + AND vp.lot_type::text = 'virtual'::text + LEFT JOIN lot_lot ps + ON ps.id = q.lot_s + WHERE + q.lot_shipment_in IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM physical_shipment_lots pl + WHERE + pl.shipment_id = q.lot_shipment_in + AND pl.purchase_line_id = vp.line + AND ( + pl.sale_line_id = COALESCE(vp.sale_line, ps.sale_line) + OR COALESCE(vp.sale_line, ps.sale_line) IS NULL + ) + ) +), +shipment_lots AS ( + SELECT + physical_shipment_lots.shipment_id, + physical_shipment_lots.lot_id, + physical_shipment_lots.lot_type, + physical_shipment_lots.purchase_line_id, + physical_shipment_lots.sale_line_id, + physical_shipment_lots.shipment_quantity, + physical_shipment_lots.shipment_unit_id + FROM physical_shipment_lots + + UNION ALL + + SELECT + virtual_shipment_lots.shipment_id, + virtual_shipment_lots.lot_id, + virtual_shipment_lots.lot_type, + virtual_shipment_lots.purchase_line_id, + virtual_shipment_lots.sale_line_id, + virtual_shipment_lots.shipment_quantity, + virtual_shipment_lots.shipment_unit_id + FROM virtual_shipment_lots +), +shipment_context AS ( + SELECT + shipment_lots.shipment_id, + shipment_lots.lot_id, + shipment_lots.purchase_line_id, + shipment_lots.sale_line_id, + shipment_lots.shipment_unit_id, + sum(COALESCE(shipment_lots.shipment_quantity, 0::numeric)) AS shipment_quantity + FROM shipment_lots + WHERE shipment_lots.shipment_id IS NOT NULL + GROUP BY + shipment_lots.shipment_id, + shipment_lots.lot_id, + shipment_lots.purchase_line_id, + shipment_lots.sale_line_id, + shipment_lots.shipment_unit_id +), +shipment_base AS ( + SELECT + shipment_context.shipment_id, + sum(shipment_context.shipment_quantity) AS shipment_quantity + FROM shipment_context + GROUP BY shipment_context.shipment_id +), +shipment_fee_candidates AS ( + SELECT + f.id AS fee_id, + 'Shipment'::text AS fee_source, + f.type AS fee_type, + CASE + WHEN f.type::text = 'ordered'::text THEN 1 + WHEN f.type::text = 'scheduled'::text THEN 2 + ELSE NULL::integer + END AS priority, + sb.shipment_id, + f.product AS product_id, + f.supplier AS supplier_id, + f.mode AS packaging, + f.p_r AS pay_or_rec, + f.state, + f.weight_type, + COALESCE(f.quantity, 0::numeric) AS requested_quantity, + sb.shipment_quantity, + f.price AS fee_price, + f.currency AS currency_id, + f.unit AS unit_id, + CASE + WHEN upper(f.p_r::text) = 'REC'::text THEN 1 + ELSE '-1'::integer + END AS sign_multiplier, + NULL::text AS forced_cost_group + FROM shipment_base sb + JOIN fee_fee f + ON f.shipment_in = sb.shipment_id + WHERE f.type::text = ANY (ARRAY['ordered'::character varying::text, 'scheduled'::character varying::text]) +), +shipment_fee_limits AS ( + SELECT + c.*, + LEAST( + GREATEST(c.requested_quantity, 0::numeric), + GREATEST( + c.shipment_quantity + - COALESCE( + sum(c.requested_quantity) OVER ( + PARTITION BY c.shipment_id, c.product_id, c.supplier_id + ORDER BY c.priority, c.fee_id + ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING + ), + 0::numeric + ), + 0::numeric + ) + ) AS fee_allocatable_quantity + FROM shipment_fee_candidates c +), +shipment_fee_allocated AS ( + SELECT + fl.fee_id, + fl.fee_source, + fl.fee_type, + fl.priority, + fl.shipment_id, + sc.lot_id, + sc.purchase_line_id, + sc.sale_line_id, + fl.product_id, + fl.supplier_id, + fl.packaging, + fl.pay_or_rec, + fl.state, + fl.weight_type, + fl.requested_quantity, + fl.shipment_quantity, + fl.fee_price, + fl.currency_id, + COALESCE(fl.unit_id, sc.shipment_unit_id) AS unit_id, + fl.sign_multiplier, + fl.forced_cost_group, + LEAST( + sc.shipment_quantity, + GREATEST( + fl.fee_allocatable_quantity + - COALESCE( + sum(sc.shipment_quantity) OVER ( + PARTITION BY fl.fee_id + ORDER BY sc.lot_id, sc.purchase_line_id, sc.sale_line_id + ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING + ), + 0::numeric + ), + 0::numeric + ) + ) AS allocated_quantity + FROM shipment_fee_limits fl + JOIN shipment_context sc + ON sc.shipment_id = fl.shipment_id +), +shipment_fee_coverage AS ( + SELECT + shipment_fee_allocated.shipment_id, + shipment_fee_allocated.product_id, + shipment_fee_allocated.supplier_id, + max(shipment_fee_allocated.shipment_quantity) AS shipment_quantity, + sum(shipment_fee_allocated.allocated_quantity) AS allocated_quantity + FROM shipment_fee_allocated + GROUP BY + shipment_fee_allocated.shipment_id, + shipment_fee_allocated.product_id, + shipment_fee_allocated.supplier_id +), +contract_budgeted_fee_candidates AS ( + SELECT + f.id AS fee_id, + 'Purchase Contract'::text AS fee_source, + f.type AS fee_type, + 3 AS priority, + sc.shipment_id, + sc.lot_id, + sc.purchase_line_id, + sc.sale_line_id, + f.product AS product_id, + f.supplier AS supplier_id, + f.mode AS packaging, + f.p_r AS pay_or_rec, + f.state, + f.weight_type, + sc.shipment_quantity, + f.price AS fee_price, + f.currency AS currency_id, + COALESCE(f.unit, sc.shipment_unit_id) AS unit_id, + CASE + WHEN upper(f.p_r::text) = 'REC'::text THEN '-1'::integer + ELSE 1 + END AS sign_multiplier, + NULL::text AS forced_cost_group + FROM shipment_context sc + JOIN fee_fee f + ON f.line = sc.purchase_line_id + WHERE + f.type::text = 'budgeted'::text + AND sc.purchase_line_id IS NOT NULL + + UNION ALL + + SELECT + f.id AS fee_id, + 'Sale Contract'::text AS fee_source, + f.type AS fee_type, + 3 AS priority, + sc.shipment_id, + sc.lot_id, + sc.purchase_line_id, + sc.sale_line_id, + f.product AS product_id, + f.supplier AS supplier_id, + f.mode AS packaging, + f.p_r AS pay_or_rec, + f.state, + f.weight_type, + sc.shipment_quantity, + f.price AS fee_price, + f.currency AS currency_id, + COALESCE(f.unit, sc.shipment_unit_id) AS unit_id, + CASE + WHEN upper(f.p_r::text) = 'REC'::text THEN 1 + ELSE '-1'::integer + END AS sign_multiplier, + NULL::text AS forced_cost_group + FROM shipment_context sc + JOIN fee_fee f + ON f.sale_line = sc.sale_line_id + WHERE + f.type::text = 'budgeted'::text + AND sc.sale_line_id IS NOT NULL +), +contract_budgeted_allocated AS ( + SELECT + c.fee_id, + c.fee_source, + c.fee_type, + c.priority, + c.shipment_id, + c.lot_id, + c.purchase_line_id, + c.sale_line_id, + c.product_id, + c.supplier_id, + c.packaging, + c.pay_or_rec, + c.state, + c.weight_type, + c.shipment_quantity, + c.fee_price, + c.currency_id, + c.unit_id, + c.sign_multiplier, + c.forced_cost_group, + LEAST( + c.shipment_quantity, + GREATEST( + sum(c.shipment_quantity) OVER ( + PARTITION BY c.shipment_id, c.product_id, c.supplier_id, c.fee_id + ) + - COALESCE(fc.allocated_quantity, 0::numeric) + - COALESCE( + sum(c.shipment_quantity) OVER ( + PARTITION BY c.shipment_id, c.product_id, c.supplier_id, c.fee_id + ORDER BY c.lot_id + ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING + ), + 0::numeric + ), + 0::numeric + ) + ) AS allocated_quantity + FROM contract_budgeted_fee_candidates c + LEFT JOIN shipment_fee_coverage fc + ON fc.shipment_id = c.shipment_id + AND fc.product_id = c.product_id + AND fc.supplier_id = c.supplier_id +), +price_rows AS ( + SELECT + NULL::integer AS fee_id, + 'Purchase Price'::text AS fee_source, + 'price'::text AS fee_type, + 4 AS priority, + sc.shipment_id, + sc.lot_id, + sc.purchase_line_id, + sc.sale_line_id, + pl.product AS product_id, + NULL::integer AS supplier_id, + NULL::character varying AS packaging, + NULL::character varying AS pay_or_rec, + NULL::character varying AS state, + NULL::character varying AS weight_type, + sc.shipment_quantity AS allocated_quantity, + pl.unit_price AS fee_price, + pl.currency_ AS currency_id, + COALESCE(pl.unit, sc.shipment_unit_id) AS unit_id, + 1 AS sign_multiplier, + 'PurchasePrice'::text AS forced_cost_group + FROM shipment_context sc + JOIN purchase_line pl + ON pl.id = sc.purchase_line_id + WHERE sc.purchase_line_id IS NOT NULL + + UNION ALL + + SELECT + NULL::integer AS fee_id, + 'Sale Price'::text AS fee_source, + 'price'::text AS fee_type, + 5 AS priority, + sc.shipment_id, + sc.lot_id, + sc.purchase_line_id, + sc.sale_line_id, + sl.product AS product_id, + NULL::integer AS supplier_id, + NULL::character varying AS packaging, + NULL::character varying AS pay_or_rec, + NULL::character varying AS state, + NULL::character varying AS weight_type, + sc.shipment_quantity AS allocated_quantity, + sl.unit_price AS fee_price, + ss.currency AS currency_id, + COALESCE(sl.unit, sc.shipment_unit_id) AS unit_id, + '-1'::integer AS sign_multiplier, + 'SalePrice'::text AS forced_cost_group + FROM shipment_context sc + JOIN sale_line sl + ON sl.id = sc.sale_line_id + JOIN sale_sale ss + ON ss.id = sl.sale + WHERE sc.sale_line_id IS NOT NULL +), +final_fees AS ( + SELECT + shipment_fee_allocated.fee_id, + shipment_fee_allocated.fee_source, + shipment_fee_allocated.fee_type, + shipment_fee_allocated.priority, + shipment_fee_allocated.shipment_id, + shipment_fee_allocated.lot_id, + shipment_fee_allocated.purchase_line_id, + shipment_fee_allocated.sale_line_id, + shipment_fee_allocated.product_id, + shipment_fee_allocated.supplier_id, + shipment_fee_allocated.packaging, + shipment_fee_allocated.pay_or_rec, + shipment_fee_allocated.state, + shipment_fee_allocated.weight_type, + shipment_fee_allocated.allocated_quantity AS fee_quantity, + shipment_fee_allocated.fee_price, + shipment_fee_allocated.currency_id, + shipment_fee_allocated.unit_id, + shipment_fee_allocated.sign_multiplier, + shipment_fee_allocated.forced_cost_group + FROM shipment_fee_allocated + WHERE shipment_fee_allocated.allocated_quantity > 0::numeric + + UNION ALL + + SELECT + contract_budgeted_allocated.fee_id, + contract_budgeted_allocated.fee_source, + contract_budgeted_allocated.fee_type, + contract_budgeted_allocated.priority, + contract_budgeted_allocated.shipment_id, + contract_budgeted_allocated.lot_id, + contract_budgeted_allocated.purchase_line_id, + contract_budgeted_allocated.sale_line_id, + contract_budgeted_allocated.product_id, + contract_budgeted_allocated.supplier_id, + contract_budgeted_allocated.packaging, + contract_budgeted_allocated.pay_or_rec, + contract_budgeted_allocated.state, + contract_budgeted_allocated.weight_type, + contract_budgeted_allocated.allocated_quantity AS fee_quantity, + contract_budgeted_allocated.fee_price, + contract_budgeted_allocated.currency_id, + contract_budgeted_allocated.unit_id, + contract_budgeted_allocated.sign_multiplier, + contract_budgeted_allocated.forced_cost_group + FROM contract_budgeted_allocated + WHERE contract_budgeted_allocated.allocated_quantity > 0::numeric + + UNION ALL + + SELECT + price_rows.fee_id, + price_rows.fee_source, + price_rows.fee_type, + price_rows.priority, + price_rows.shipment_id, + price_rows.lot_id, + price_rows.purchase_line_id, + price_rows.sale_line_id, + price_rows.product_id, + price_rows.supplier_id, + price_rows.packaging, + price_rows.pay_or_rec, + price_rows.state, + price_rows.weight_type, + price_rows.allocated_quantity AS fee_quantity, + price_rows.fee_price, + price_rows.currency_id, + price_rows.unit_id, + price_rows.sign_multiplier, + price_rows.forced_cost_group + FROM price_rows + WHERE price_rows.allocated_quantity > 0::numeric +) +SELECT + f.shipment_id AS "intShipmentId", + f.lot_id AS "intLotId", + COALESCE(f.fee_id, 0) AS "intFeeId", + f.fee_source AS "strFeeSource", + f.fee_type AS "strFeeType", + f.priority AS "intPriority", + COALESCE(f.purchase_line_id, 0) AS "intPurchaseLineId", + COALESCE(f.sale_line_id, 0) AS "intSaleLineId", + COALESCE(f.product_id, 0) AS "intProductId", + COALESCE(p.code, ''::character varying) AS "strFee", + COALESCE(f.supplier_id, 0) AS "intSupplierId", + COALESCE(sup.name, ''::character varying) AS "strSupplier", + COALESCE(f.packaging, ''::character varying) AS "strPackaging", + COALESCE(f.pay_or_rec, ''::character varying) AS "strPayOrRec", + COALESCE(f.state, ''::character varying) AS "strState", + CASE + WHEN upper(f.weight_type::text) = 'BRUT'::text THEN 'Gross'::text + ELSE 'Net'::text + END AS "strWeighingType", + f.fee_quantity AS "dblQuantity", + f.fee_price AS "dblPrice", + COALESCE(cur.name, ''::character varying) AS "strCurrency", + COALESCE(uom.name, 'Mt'::character varying) AS "strUnit", + f.fee_quantity * f.fee_price AS "dblAmount", + COALESCE( + f.forced_cost_group, + CASE + WHEN upper(p.code::text) ~~ '%FREIGHT%'::text THEN 'Freight'::text + WHEN upper(p.code::text) ~~ '%PROFIT SHARING%'::text THEN 'Profit Sharing'::text + ELSE 'Other Costs'::text + END + ) AS "strCostGroup", + CASE + WHEN f.pay_or_rec::text = 'pay'::text THEN - 1::numeric + WHEN f.pay_or_rec::text = 'rec'::text THEN 1::numeric + WHEN f.fee_source = 'Purchase Price'::text AND f.fee_type::text = 'price'::text THEN - 1::numeric + WHEN f.fee_source = 'Sale Price'::text AND f.fee_type::text = 'price'::text THEN 1::numeric + ELSE - 1::numeric + END AS "intSignMultiplier" +FROM final_fees f +JOIN product_product p + ON p.id = f.product_id +LEFT JOIN party_party sup + ON sup.id = f.supplier_id +LEFT JOIN currency_currency cur + ON cur.id = f.currency_id +LEFT JOIN product_uom uom + ON uom.id = f.unit_id; diff --git a/ITSA/Data Reconciliation/update_vw_utility_trade_pnl_quantities.sql b/ITSA/Data Reconciliation/update_vw_utility_trade_pnl_quantities.sql new file mode 100644 index 0000000..a53400d --- /dev/null +++ b/ITSA/Data Reconciliation/update_vw_utility_trade_pnl_quantities.sql @@ -0,0 +1,382 @@ +CREATE OR REPLACE VIEW public.vw_utility_trade_pnl_quantities AS +WITH lot_current_qty AS ( + SELECT + l.id AS lot_id, + round( + COALESCE(lh.quantity, 0::numeric) + * COALESCE(lot_uom.factor, 1::double precision)::numeric + / NULLIF(COALESCE(line_uom.factor, 1::double precision)::numeric, 0::numeric), + 5 + ) AS pnl_quantity, + lot_uom.name AS quantity_uom + FROM lot_lot l + LEFT JOIN lot_qt_hist lh ON lh.lot = l.id AND lh.quantity_type = l.lot_state + LEFT JOIN product_uom lot_uom ON lot_uom.id = l.lot_unit_line + LEFT JOIN purchase_line pl ON pl.id = l.line + LEFT JOIN sale_line sl ON sl.id = l.sale_line + LEFT JOIN product_uom line_uom ON line_uom.id = COALESCE(pl.unit, sl.unit) +), purchase_pnl_lots AS ( + SELECT + 'purchase'::text AS pnl_side, + p.id AS purchase_id, + p.number AS purchase_number, + p.reference AS purchase_reference, + pl.id AS purchase_line_id, + pl.finished AS purchase_line_finished, + s.id AS sale_id, + s.number AS sale_number, + s.reference AS sale_reference, + sl.id AS sale_line_id, + sl.finished AS sale_line_finished, + l.id AS valued_lot_id, + l.lot_type AS valued_lot_type, + NULL::integer AS lot_qt_id, + lcq.pnl_quantity, + lcq.quantity_uom AS quantity_unit, + pl.product AS product_id, + COALESCE(shi.from_location, sint.from_location, sho.from_location, p.from_location) AS from_location_id, + COALESCE(shi.to_location, sint.to_location, sho.to_location, p.to_location) AS to_location_id, + shi.id AS shipment_id, + shi.bl_date, + pl.from_del AS delivery_date_from, + pl.to_del AS delivery_date_to + FROM purchase_line pl + JOIN purchase_purchase p ON p.id = pl.purchase + JOIN lot_lot l ON l.line = pl.id + JOIN lot_current_qty lcq ON lcq.lot_id = l.id + LEFT JOIN sale_line sl ON sl.id = l.sale_line + LEFT JOIN sale_sale s ON s.id = sl.sale + LEFT JOIN stock_shipment_in shi ON shi.id = l.lot_shipment_in + LEFT JOIN stock_shipment_internal sint ON sint.id = l.lot_shipment_internal + LEFT JOIN stock_shipment_out sho ON sho.id = l.lot_shipment_out + WHERE pl.type::text = 'line'::text + AND (l.lot_type::text = ANY (ARRAY['virtual'::character varying::text, 'physic'::character varying::text])) +), sale_pnl_direct_lots AS ( + SELECT + 'sale'::text AS pnl_side, + p.id AS purchase_id, + p.number AS purchase_number, + p.reference AS purchase_reference, + pl.id AS purchase_line_id, + pl.finished AS purchase_line_finished, + s.id AS sale_id, + s.number AS sale_number, + s.reference AS sale_reference, + sl.id AS sale_line_id, + sl.finished AS sale_line_finished, + l.id AS valued_lot_id, + l.lot_type AS valued_lot_type, + NULL::integer AS lot_qt_id, + lcq.pnl_quantity, + lcq.quantity_uom AS quantity_unit, + sl.product AS product_id, + COALESCE(shi.from_location, sint.from_location, sho.from_location, s.from_location) AS from_location_id, + COALESCE(shi.to_location, sint.to_location, sho.to_location, s.to_location) AS to_location_id, + shi.id AS shipment_id, + shi.bl_date, + sl.from_del AS delivery_date_from, + sl.to_del AS delivery_date_to + FROM purchase_line pl + JOIN purchase_purchase p ON p.id = pl.purchase + JOIN lot_lot l ON l.line = pl.id AND l.sale_line IS NOT NULL + JOIN sale_line sl ON sl.id = l.sale_line + JOIN sale_sale s ON s.id = sl.sale + JOIN lot_current_qty lcq ON lcq.lot_id = l.id + LEFT JOIN stock_shipment_in shi ON shi.id = l.lot_shipment_in + LEFT JOIN stock_shipment_internal sint ON sint.id = l.lot_shipment_internal + LEFT JOIN stock_shipment_out sho ON sho.id = l.lot_shipment_out + WHERE pl.type::text = 'line'::text + AND l.lot_type::text = 'physic'::text + AND NOT EXISTS ( + SELECT 1 + FROM purchase_pnl_lots ppl + WHERE ppl.valued_lot_id = l.id + AND ppl.sale_line_id = sl.id + ) +), sale_pnl_matched_virtual_lots AS ( + SELECT + 'sale'::text AS pnl_side, + p.id AS purchase_id, + p.number AS purchase_number, + p.reference AS purchase_reference, + pl.id AS purchase_line_id, + pl.finished AS purchase_line_finished, + s.id AS sale_id, + s.number AS sale_number, + s.reference AS sale_reference, + sl.id AS sale_line_id, + sl.finished AS sale_line_finished, + sale_lot.id AS valued_lot_id, + sale_lot.lot_type AS valued_lot_type, + lqt.id AS lot_qt_id, + lcq.pnl_quantity, + lcq.quantity_uom AS quantity_unit, + sl.product AS product_id, + COALESCE(shi.from_location, sint.from_location, sho.from_location, s.from_location) AS from_location_id, + COALESCE(shi.to_location, sint.to_location, sho.to_location, s.to_location) AS to_location_id, + shi.id AS shipment_id, + shi.bl_date, + sl.from_del AS delivery_date_from, + sl.to_del AS delivery_date_to + FROM purchase_line pl + JOIN purchase_purchase p ON p.id = pl.purchase + JOIN lot_lot purchase_lot ON purchase_lot.line = pl.id + AND purchase_lot.lot_type::text = 'virtual'::text + AND purchase_lot.sale_line IS NULL + JOIN lot_qt lqt ON lqt.lot_p = purchase_lot.id + AND lqt.lot_s IS NOT NULL + AND lqt.lot_quantity > 0::numeric + JOIN lot_lot sale_lot ON sale_lot.id = lqt.lot_s + AND sale_lot.lot_type::text = 'virtual'::text + JOIN sale_line sl ON sl.id = sale_lot.sale_line + JOIN sale_sale s ON s.id = sl.sale + JOIN lot_current_qty lcq ON lcq.lot_id = sale_lot.id + LEFT JOIN stock_shipment_in shi ON shi.id = lqt.lot_shipment_in + LEFT JOIN stock_shipment_internal sint ON sint.id = lqt.lot_shipment_internal + LEFT JOIN stock_shipment_out sho ON sho.id = lqt.lot_shipment_out + WHERE pl.type::text = 'line'::text +), sale_pnl_unmatched_lots AS ( + SELECT + 'sale'::text AS pnl_side, + NULL::integer AS purchase_id, + NULL::character varying AS purchase_number, + NULL::character varying AS purchase_reference, + NULL::integer AS purchase_line_id, + NULL::boolean AS purchase_line_finished, + s.id AS sale_id, + s.number AS sale_number, + s.reference AS sale_reference, + sl.id AS sale_line_id, + sl.finished AS sale_line_finished, + l.id AS valued_lot_id, + l.lot_type AS valued_lot_type, + NULL::integer AS lot_qt_id, + lcq.pnl_quantity, + lcq.quantity_uom AS quantity_unit, + sl.product AS product_id, + COALESCE(shi.from_location, sint.from_location, sho.from_location, s.from_location) AS from_location_id, + COALESCE(shi.to_location, sint.to_location, sho.to_location, s.to_location) AS to_location_id, + shi.id AS shipment_id, + shi.bl_date, + sl.from_del AS delivery_date_from, + sl.to_del AS delivery_date_to + FROM sale_line sl + JOIN sale_sale s ON s.id = sl.sale + JOIN lot_lot l ON l.sale_line = sl.id + AND l.line IS NULL + AND (l.lot_type::text = ANY (ARRAY['virtual'::character varying::text, 'physic'::character varying::text])) + JOIN lot_current_qty lcq ON lcq.lot_id = l.id + LEFT JOIN stock_shipment_in shi ON shi.id = l.lot_shipment_in + LEFT JOIN stock_shipment_internal sint ON sint.id = l.lot_shipment_internal + LEFT JOIN stock_shipment_out sho ON sho.id = l.lot_shipment_out + WHERE sl.type::text = 'line'::text + AND NOT EXISTS ( + SELECT 1 + FROM lot_qt lqt + WHERE lqt.lot_s = l.id + AND lqt.lot_p IS NOT NULL + AND lqt.lot_quantity > 0::numeric + ) +), pnl_lot_rows AS ( + SELECT + purchase_pnl_lots.pnl_side, + purchase_pnl_lots.purchase_id, + purchase_pnl_lots.purchase_number, + purchase_pnl_lots.purchase_reference, + purchase_pnl_lots.purchase_line_id, + purchase_pnl_lots.purchase_line_finished, + purchase_pnl_lots.sale_id, + purchase_pnl_lots.sale_number, + purchase_pnl_lots.sale_reference, + purchase_pnl_lots.sale_line_id, + purchase_pnl_lots.sale_line_finished, + purchase_pnl_lots.valued_lot_id, + purchase_pnl_lots.valued_lot_type, + purchase_pnl_lots.lot_qt_id, + purchase_pnl_lots.pnl_quantity, + purchase_pnl_lots.quantity_unit, + purchase_pnl_lots.product_id, + purchase_pnl_lots.from_location_id, + purchase_pnl_lots.to_location_id, + purchase_pnl_lots.shipment_id, + purchase_pnl_lots.bl_date, + purchase_pnl_lots.delivery_date_from, + purchase_pnl_lots.delivery_date_to + FROM purchase_pnl_lots + UNION ALL + SELECT + sale_pnl_direct_lots.pnl_side, + sale_pnl_direct_lots.purchase_id, + sale_pnl_direct_lots.purchase_number, + sale_pnl_direct_lots.purchase_reference, + sale_pnl_direct_lots.purchase_line_id, + sale_pnl_direct_lots.purchase_line_finished, + sale_pnl_direct_lots.sale_id, + sale_pnl_direct_lots.sale_number, + sale_pnl_direct_lots.sale_reference, + sale_pnl_direct_lots.sale_line_id, + sale_pnl_direct_lots.sale_line_finished, + sale_pnl_direct_lots.valued_lot_id, + sale_pnl_direct_lots.valued_lot_type, + sale_pnl_direct_lots.lot_qt_id, + sale_pnl_direct_lots.pnl_quantity, + sale_pnl_direct_lots.quantity_unit, + sale_pnl_direct_lots.product_id, + sale_pnl_direct_lots.from_location_id, + sale_pnl_direct_lots.to_location_id, + sale_pnl_direct_lots.shipment_id, + sale_pnl_direct_lots.bl_date, + sale_pnl_direct_lots.delivery_date_from, + sale_pnl_direct_lots.delivery_date_to + FROM sale_pnl_direct_lots + UNION ALL + SELECT + sale_pnl_matched_virtual_lots.pnl_side, + sale_pnl_matched_virtual_lots.purchase_id, + sale_pnl_matched_virtual_lots.purchase_number, + sale_pnl_matched_virtual_lots.purchase_reference, + sale_pnl_matched_virtual_lots.purchase_line_id, + sale_pnl_matched_virtual_lots.purchase_line_finished, + sale_pnl_matched_virtual_lots.sale_id, + sale_pnl_matched_virtual_lots.sale_number, + sale_pnl_matched_virtual_lots.sale_reference, + sale_pnl_matched_virtual_lots.sale_line_id, + sale_pnl_matched_virtual_lots.sale_line_finished, + sale_pnl_matched_virtual_lots.valued_lot_id, + sale_pnl_matched_virtual_lots.valued_lot_type, + sale_pnl_matched_virtual_lots.lot_qt_id, + sale_pnl_matched_virtual_lots.pnl_quantity, + sale_pnl_matched_virtual_lots.quantity_unit, + sale_pnl_matched_virtual_lots.product_id, + sale_pnl_matched_virtual_lots.from_location_id, + sale_pnl_matched_virtual_lots.to_location_id, + sale_pnl_matched_virtual_lots.shipment_id, + sale_pnl_matched_virtual_lots.bl_date, + sale_pnl_matched_virtual_lots.delivery_date_from, + sale_pnl_matched_virtual_lots.delivery_date_to + FROM sale_pnl_matched_virtual_lots + UNION ALL + SELECT + sale_pnl_unmatched_lots.pnl_side, + sale_pnl_unmatched_lots.purchase_id, + sale_pnl_unmatched_lots.purchase_number, + sale_pnl_unmatched_lots.purchase_reference, + sale_pnl_unmatched_lots.purchase_line_id, + sale_pnl_unmatched_lots.purchase_line_finished, + sale_pnl_unmatched_lots.sale_id, + sale_pnl_unmatched_lots.sale_number, + sale_pnl_unmatched_lots.sale_reference, + sale_pnl_unmatched_lots.sale_line_id, + sale_pnl_unmatched_lots.sale_line_finished, + sale_pnl_unmatched_lots.valued_lot_id, + sale_pnl_unmatched_lots.valued_lot_type, + sale_pnl_unmatched_lots.lot_qt_id, + sale_pnl_unmatched_lots.pnl_quantity, + sale_pnl_unmatched_lots.quantity_unit, + sale_pnl_unmatched_lots.product_id, + sale_pnl_unmatched_lots.from_location_id, + sale_pnl_unmatched_lots.to_location_id, + sale_pnl_unmatched_lots.shipment_id, + sale_pnl_unmatched_lots.bl_date, + sale_pnl_unmatched_lots.delivery_date_from, + sale_pnl_unmatched_lots.delivery_date_to + FROM sale_pnl_unmatched_lots +), pnl_lot_rows_with_finished AS ( + SELECT + pnl_lot_rows.pnl_side, + pnl_lot_rows.purchase_id, + pnl_lot_rows.purchase_number, + pnl_lot_rows.purchase_reference, + pnl_lot_rows.purchase_line_id, + pnl_lot_rows.purchase_line_finished, + pnl_lot_rows.sale_id, + pnl_lot_rows.sale_number, + pnl_lot_rows.sale_reference, + pnl_lot_rows.sale_line_id, + pnl_lot_rows.sale_line_finished, + pnl_lot_rows.valued_lot_id, + pnl_lot_rows.valued_lot_type, + pnl_lot_rows.lot_qt_id, + pnl_lot_rows.pnl_quantity, + pnl_lot_rows.quantity_unit, + pnl_lot_rows.product_id, + pnl_lot_rows.from_location_id, + pnl_lot_rows.to_location_id, + pnl_lot_rows.shipment_id, + pnl_lot_rows.bl_date, + pnl_lot_rows.delivery_date_from, + pnl_lot_rows.delivery_date_to, + CASE + WHEN pnl_lot_rows.pnl_side = 'purchase'::text THEN pnl_lot_rows.purchase_line_finished + WHEN pnl_lot_rows.pnl_side = 'sale'::text THEN pnl_lot_rows.sale_line_finished + ELSE false + END AS valued_line_finished, + CASE + WHEN pnl_lot_rows.valued_lot_type::text = 'virtual'::text + AND ( + pnl_lot_rows.pnl_side = 'purchase'::text + AND pnl_lot_rows.purchase_line_finished IS TRUE + OR pnl_lot_rows.pnl_side = 'sale'::text + AND pnl_lot_rows.sale_line_finished IS TRUE + ) + THEN true + ELSE false + END AS ignored_by_finished_rule + FROM pnl_lot_rows +) +SELECT + pnl_side, + purchase_id, + purchase_number, + purchase_reference, + purchase_line_id, + purchase_line_finished, + sale_id, + sale_number, + sale_reference, + sale_line_id, + sale_line_finished, + valued_lot_id, + valued_lot_type, + valued_line_finished, + ignored_by_finished_rule, + sum(pnl_quantity) AS pnl_quantity_raw, + sum( + CASE + WHEN ignored_by_finished_rule THEN 0::numeric + ELSE pnl_quantity + END + ) AS pnl_quantity_after_finished_rule, + quantity_unit, + product_id, + from_location_id, + to_location_id, + shipment_id, + bl_date, + delivery_date_from, + delivery_date_to +FROM pnl_lot_rows_with_finished +GROUP BY + pnl_side, + purchase_id, + purchase_number, + purchase_reference, + purchase_line_id, + purchase_line_finished, + sale_id, + sale_number, + sale_reference, + sale_line_id, + sale_line_finished, + valued_lot_type, + valued_lot_id, + valued_line_finished, + ignored_by_finished_rule, + quantity_unit, + product_id, + from_location_id, + to_location_id, + shipment_id, + bl_date, + delivery_date_from, + delivery_date_to; diff --git a/ITSA/Data Reconciliation/vw_utility_shipment_fee_allocation_current.sql b/ITSA/Data Reconciliation/vw_utility_shipment_fee_allocation_current.sql new file mode 100644 index 0000000..990d0b3 --- /dev/null +++ b/ITSA/Data Reconciliation/vw_utility_shipment_fee_allocation_current.sql @@ -0,0 +1,361 @@ + WITH physical_shipment_lots AS ( + SELECT l.lot_shipment_in AS shipment_id, + l.id AS lot_id, + l.lot_type, + l.line AS purchase_line_id, + l.sale_line AS sale_line_id, + qh.quantity AS shipment_quantity, + l.lot_unit_line AS shipment_unit_id + FROM lot_lot l + LEFT JOIN lot_qt_hist qh ON qh.lot = l.id AND qh.quantity_type = l.lot_state + WHERE l.lot_type::text = 'physic'::text AND l.lot_shipment_in IS NOT NULL + ), virtual_shipment_lots AS ( + SELECT q.lot_shipment_in AS shipment_id, + vp.id AS lot_id, + vp.lot_type, + vp.line AS purchase_line_id, + COALESCE(vp.sale_line, ps.sale_line) AS sale_line_id, + q.lot_quantity AS shipment_quantity, + q.lot_unit AS shipment_unit_id + FROM lot_qt q + JOIN lot_lot vp ON vp.id = q.lot_p AND vp.lot_type::text = 'virtual'::text + LEFT JOIN lot_lot ps ON ps.id = q.lot_s + WHERE q.lot_shipment_in IS NOT NULL AND NOT (EXISTS ( SELECT 1 + FROM physical_shipment_lots pl + WHERE pl.shipment_id = q.lot_shipment_in AND pl.purchase_line_id = vp.line AND (pl.sale_line_id = COALESCE(vp.sale_line, ps.sale_line) OR COALESCE(vp.sale_line, ps.sale_line) IS NULL))) + ), shipment_lots AS ( + SELECT physical_shipment_lots.shipment_id, + physical_shipment_lots.lot_id, + physical_shipment_lots.lot_type, + physical_shipment_lots.purchase_line_id, + physical_shipment_lots.sale_line_id, + physical_shipment_lots.shipment_quantity, + physical_shipment_lots.shipment_unit_id + FROM physical_shipment_lots + UNION ALL + SELECT virtual_shipment_lots.shipment_id, + virtual_shipment_lots.lot_id, + virtual_shipment_lots.lot_type, + virtual_shipment_lots.purchase_line_id, + virtual_shipment_lots.sale_line_id, + virtual_shipment_lots.shipment_quantity, + virtual_shipment_lots.shipment_unit_id + FROM virtual_shipment_lots + ), shipment_context AS ( + SELECT shipment_lots.shipment_id, + shipment_lots.lot_id, + shipment_lots.purchase_line_id, + shipment_lots.sale_line_id, + shipment_lots.shipment_unit_id, + sum(COALESCE(shipment_lots.shipment_quantity, 0::numeric)) AS shipment_quantity + FROM shipment_lots + WHERE shipment_lots.shipment_id IS NOT NULL + GROUP BY shipment_lots.shipment_id, shipment_lots.lot_id, shipment_lots.purchase_line_id, shipment_lots.sale_line_id, shipment_lots.shipment_unit_id + ), shipment_base AS ( + SELECT shipment_context.shipment_id, + sum(shipment_context.shipment_quantity) AS shipment_quantity + FROM shipment_context + GROUP BY shipment_context.shipment_id + ), shipment_fee_candidates AS ( + SELECT f_1.id AS fee_id, + 'Shipment'::text AS fee_source, + f_1.type AS fee_type, + CASE + WHEN f_1.type::text = 'ordered'::text THEN 1 + WHEN f_1.type::text = 'scheduled'::text THEN 2 + ELSE NULL::integer + END AS priority, + sb.shipment_id, + NULL::integer AS lot_id, + NULL::integer AS purchase_line_id, + NULL::integer AS sale_line_id, + f_1.product AS product_id, + f_1.supplier AS supplier_id, + f_1.mode AS packaging, + f_1.p_r AS pay_or_rec, + f_1.state, + f_1.weight_type, + COALESCE(f_1.quantity, 0::numeric) AS requested_quantity, + sb.shipment_quantity, + f_1.price AS fee_price, + f_1.currency AS currency_id, + f_1.unit AS unit_id, + CASE + WHEN upper(f_1.p_r::text) = 'REC'::text THEN 1 + ELSE '-1'::integer + END AS sign_multiplier, + NULL::text AS forced_cost_group + FROM shipment_base sb + JOIN fee_fee f_1 ON f_1.shipment_in = sb.shipment_id + WHERE f_1.type::text = ANY (ARRAY['ordered'::character varying::text, 'scheduled'::character varying::text]) + ), shipment_fee_allocated AS ( + SELECT c.fee_id, + c.fee_source, + c.fee_type, + c.priority, + c.shipment_id, + c.lot_id, + c.purchase_line_id, + c.sale_line_id, + c.product_id, + c.supplier_id, + c.packaging, + c.pay_or_rec, + c.state, + c.weight_type, + c.requested_quantity, + c.shipment_quantity, + c.fee_price, + c.currency_id, + c.unit_id, + c.sign_multiplier, + c.forced_cost_group, + LEAST(GREATEST(c.requested_quantity, 0::numeric), GREATEST(c.shipment_quantity - COALESCE(sum(c.requested_quantity) OVER (PARTITION BY c.shipment_id, c.product_id, c.supplier_id ORDER BY c.priority, c.fee_id ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING), 0::numeric), 0::numeric)) AS allocated_quantity + FROM shipment_fee_candidates c + ), shipment_fee_coverage AS ( + SELECT shipment_fee_allocated.shipment_id, + shipment_fee_allocated.product_id, + shipment_fee_allocated.supplier_id, + max(shipment_fee_allocated.shipment_quantity) AS shipment_quantity, + sum(shipment_fee_allocated.allocated_quantity) AS allocated_quantity + FROM shipment_fee_allocated + GROUP BY shipment_fee_allocated.shipment_id, shipment_fee_allocated.product_id, shipment_fee_allocated.supplier_id + ), contract_budgeted_fee_candidates AS ( + SELECT f_1.id AS fee_id, + 'Purchase Contract'::text AS fee_source, + f_1.type AS fee_type, + 3 AS priority, + sc.shipment_id, + sc.lot_id, + sc.purchase_line_id, + sc.sale_line_id, + f_1.product AS product_id, + f_1.supplier AS supplier_id, + f_1.mode AS packaging, + f_1.p_r AS pay_or_rec, + f_1.state, + f_1.weight_type, + sc.shipment_quantity, + f_1.price AS fee_price, + f_1.currency AS currency_id, + COALESCE(f_1.unit, sc.shipment_unit_id) AS unit_id, + CASE + WHEN upper(f_1.p_r::text) = 'REC'::text THEN '-1'::integer + ELSE 1 + END AS sign_multiplier, + NULL::text AS forced_cost_group + FROM shipment_context sc + JOIN fee_fee f_1 ON f_1.line = sc.purchase_line_id + WHERE f_1.type::text = 'budgeted'::text AND sc.purchase_line_id IS NOT NULL + UNION ALL + SELECT f_1.id AS fee_id, + 'Sale Contract'::text AS fee_source, + f_1.type AS fee_type, + 3 AS priority, + sc.shipment_id, + sc.lot_id, + sc.purchase_line_id, + sc.sale_line_id, + f_1.product AS product_id, + f_1.supplier AS supplier_id, + f_1.mode AS packaging, + f_1.p_r AS pay_or_rec, + f_1.state, + f_1.weight_type, + sc.shipment_quantity, + f_1.price AS fee_price, + f_1.currency AS currency_id, + COALESCE(f_1.unit, sc.shipment_unit_id) AS unit_id, + CASE + WHEN upper(f_1.p_r::text) = 'REC'::text THEN 1 + ELSE '-1'::integer + END AS sign_multiplier, + NULL::text AS forced_cost_group + FROM shipment_context sc + JOIN fee_fee f_1 ON f_1.sale_line = sc.sale_line_id + WHERE f_1.type::text = 'budgeted'::text AND sc.sale_line_id IS NOT NULL + ), contract_budgeted_allocated AS ( + SELECT c.fee_id, + c.fee_source, + c.fee_type, + c.priority, + c.shipment_id, + c.lot_id, + c.purchase_line_id, + c.sale_line_id, + c.product_id, + c.supplier_id, + c.packaging, + c.pay_or_rec, + c.state, + c.weight_type, + c.shipment_quantity, + c.fee_price, + c.currency_id, + c.unit_id, + c.sign_multiplier, + c.forced_cost_group, + LEAST(c.shipment_quantity, GREATEST(sum(c.shipment_quantity) OVER (PARTITION BY c.shipment_id, c.product_id, c.supplier_id, c.fee_id) - COALESCE(fc.allocated_quantity, 0::numeric) - COALESCE(sum(c.shipment_quantity) OVER (PARTITION BY c.shipment_id, c.product_id, c.supplier_id, c.fee_id ORDER BY c.lot_id ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING), 0::numeric), 0::numeric)) AS allocated_quantity + FROM contract_budgeted_fee_candidates c + LEFT JOIN shipment_fee_coverage fc ON fc.shipment_id = c.shipment_id AND fc.product_id = c.product_id AND fc.supplier_id = c.supplier_id + ), price_rows AS ( + SELECT NULL::integer AS fee_id, + 'Purchase Price'::text AS fee_source, + 'price'::text AS fee_type, + 4 AS priority, + sc.shipment_id, + sc.lot_id, + sc.purchase_line_id, + sc.sale_line_id, + pl.product AS product_id, + NULL::integer AS supplier_id, + NULL::character varying AS packaging, + NULL::character varying AS pay_or_rec, + NULL::character varying AS state, + NULL::character varying AS weight_type, + sc.shipment_quantity AS allocated_quantity, + pl.unit_price AS fee_price, + pl.currency_ AS currency_id, + COALESCE(pl.unit, sc.shipment_unit_id) AS unit_id, + 1 AS sign_multiplier, + 'PurchasePrice'::text AS forced_cost_group + FROM shipment_context sc + JOIN purchase_line pl ON pl.id = sc.purchase_line_id + WHERE sc.purchase_line_id IS NOT NULL + UNION ALL + SELECT NULL::integer AS fee_id, + 'Sale Price'::text AS fee_source, + 'price'::text AS fee_type, + 5 AS priority, + sc.shipment_id, + sc.lot_id, + sc.purchase_line_id, + sc.sale_line_id, + sl.product AS product_id, + NULL::integer AS supplier_id, + NULL::character varying AS packaging, + NULL::character varying AS pay_or_rec, + NULL::character varying AS state, + NULL::character varying AS weight_type, + sc.shipment_quantity AS allocated_quantity, + sl.unit_price AS fee_price, + ss.currency AS currency_id, + COALESCE(sl.unit, sc.shipment_unit_id) AS unit_id, + '-1'::integer AS sign_multiplier, + 'SalePrice'::text AS forced_cost_group + FROM shipment_context sc + JOIN sale_line sl ON sl.id = sc.sale_line_id + JOIN sale_sale ss ON ss.id = sl.sale + WHERE sc.sale_line_id IS NOT NULL + ), final_fees AS ( + SELECT shipment_fee_allocated.fee_id, + shipment_fee_allocated.fee_source, + shipment_fee_allocated.fee_type, + shipment_fee_allocated.priority, + shipment_fee_allocated.shipment_id, + shipment_fee_allocated.lot_id, + shipment_fee_allocated.purchase_line_id, + shipment_fee_allocated.sale_line_id, + shipment_fee_allocated.product_id, + shipment_fee_allocated.supplier_id, + shipment_fee_allocated.packaging, + shipment_fee_allocated.pay_or_rec, + shipment_fee_allocated.state, + shipment_fee_allocated.weight_type, + shipment_fee_allocated.allocated_quantity AS fee_quantity, + shipment_fee_allocated.fee_price, + shipment_fee_allocated.currency_id, + shipment_fee_allocated.unit_id, + shipment_fee_allocated.sign_multiplier, + shipment_fee_allocated.forced_cost_group + FROM shipment_fee_allocated + WHERE shipment_fee_allocated.allocated_quantity > 0::numeric + UNION ALL + SELECT contract_budgeted_allocated.fee_id, + contract_budgeted_allocated.fee_source, + contract_budgeted_allocated.fee_type, + contract_budgeted_allocated.priority, + contract_budgeted_allocated.shipment_id, + contract_budgeted_allocated.lot_id, + contract_budgeted_allocated.purchase_line_id, + contract_budgeted_allocated.sale_line_id, + contract_budgeted_allocated.product_id, + contract_budgeted_allocated.supplier_id, + contract_budgeted_allocated.packaging, + contract_budgeted_allocated.pay_or_rec, + contract_budgeted_allocated.state, + contract_budgeted_allocated.weight_type, + contract_budgeted_allocated.allocated_quantity AS fee_quantity, + contract_budgeted_allocated.fee_price, + contract_budgeted_allocated.currency_id, + contract_budgeted_allocated.unit_id, + contract_budgeted_allocated.sign_multiplier, + contract_budgeted_allocated.forced_cost_group + FROM contract_budgeted_allocated + WHERE contract_budgeted_allocated.allocated_quantity > 0::numeric + UNION ALL + SELECT price_rows.fee_id, + price_rows.fee_source, + price_rows.fee_type, + price_rows.priority, + price_rows.shipment_id, + price_rows.lot_id, + price_rows.purchase_line_id, + price_rows.sale_line_id, + price_rows.product_id, + price_rows.supplier_id, + price_rows.packaging, + price_rows.pay_or_rec, + price_rows.state, + price_rows.weight_type, + price_rows.allocated_quantity AS fee_quantity, + price_rows.fee_price, + price_rows.currency_id, + price_rows.unit_id, + price_rows.sign_multiplier, + price_rows.forced_cost_group + FROM price_rows + WHERE price_rows.allocated_quantity > 0::numeric + ) + SELECT f.shipment_id AS "intShipmentId", + f.lot_id AS "intLotId", + COALESCE(f.fee_id, 0) AS "intFeeId", + f.fee_source AS "strFeeSource", + f.fee_type AS "strFeeType", + f.priority AS "intPriority", + COALESCE(f.purchase_line_id, 0) AS "intPurchaseLineId", + COALESCE(f.sale_line_id, 0) AS "intSaleLineId", + COALESCE(f.product_id, 0) AS "intProductId", + COALESCE(p.code, ''::character varying) AS "strFee", + COALESCE(f.supplier_id, 0) AS "intSupplierId", + COALESCE(sup.name, ''::character varying) AS "strSupplier", + COALESCE(f.packaging, ''::character varying) AS "strPackaging", + COALESCE(f.pay_or_rec, ''::character varying) AS "strPayOrRec", + COALESCE(f.state, ''::character varying) AS "strState", + CASE + WHEN upper(f.weight_type::text) = 'BRUT'::text THEN 'Gross'::text + ELSE 'Net'::text + END AS "strWeighingType", + f.fee_quantity AS "dblQuantity", + f.fee_price AS "dblPrice", + COALESCE(cur.name, ''::character varying) AS "strCurrency", + COALESCE(uom.name, 'Mt'::character varying) AS "strUnit", + f.fee_quantity * f.fee_price AS "dblAmount", + COALESCE(f.forced_cost_group, + CASE + WHEN upper(p.code::text) ~~ '%FREIGHT%'::text THEN 'Freight'::text + WHEN upper(p.code::text) ~~ '%PROFIT SHARING%'::text THEN 'Profit Sharing'::text + ELSE 'Other Costs'::text + END) AS "strCostGroup", + CASE + WHEN f.pay_or_rec::text = 'pay'::text THEN - 1::numeric + WHEN f.pay_or_rec::text = 'rec'::text THEN 1::numeric + WHEN f.fee_source = 'Purchase Price'::text AND f.fee_type::text = 'price'::text THEN - 1::numeric + WHEN f.fee_source = 'Sale Price'::text AND f.fee_type::text = 'price'::text THEN 1::numeric + ELSE - 1::numeric + END AS "intSignMultiplier" + FROM final_fees f + JOIN product_product p ON p.id = f.product_id + LEFT JOIN party_party sup ON sup.id = f.supplier_id + LEFT JOIN currency_currency cur ON cur.id = f.currency_id + LEFT JOIN product_uom uom ON uom.id = f.unit_id; diff --git a/modules/trade_finance/facility.py b/modules/trade_finance/facility.py index 8cff530..036faf7 100644 --- a/modules/trade_finance/facility.py +++ b/modules/trade_finance/facility.py @@ -1,7 +1,7 @@ # This file is part of Tradon. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. -from trytond.model import ModelSQL, ModelView, fields +from trytond.model import ModelSQL, ModelView, fields, tree from trytond.model import sequence_ordered from trytond.pool import Pool from trytond.pyson import Eval, Bool @@ -184,7 +184,7 @@ class FacilityCovenant(ModelSQL, ModelView): # Facility Limit (Global Limit + Sub-Limits in one model) # --------------------------------------------------------------------------- -class FacilityLimit(ModelSQL, ModelView): +class FacilityLimit(tree(), ModelSQL, ModelView): 'Facility Limit' __name__ = 'trade_finance.facility_limit' _rec_name = 'name' @@ -194,24 +194,41 @@ class FacilityLimit(ModelSQL, ModelView): states={'readonly': Bool(Eval('parent'))}, depends=['parent']) parent = fields.Many2One('trade_finance.facility_limit', 'Parent Limit', + left='left', right='right', ondelete='RESTRICT', domain=[('facility', '=', Eval('facility'))], depends=['facility'], help='Leave empty for Global Limit (root node)') children = fields.One2Many('trade_finance.facility_limit', 'parent', 'Sub-Limits') + left = fields.Integer('Left', required=True) + right = fields.Integer('Right', required=True) name = fields.Char('Name', required=True) alternative_name = fields.Char('Limit Alternative Name') financing_type = fields.Many2One('trade_finance.financing_type', 'Financing Type', ondelete='RESTRICT') + currency = fields.Many2One('currency.currency', 'Currency', + ondelete='RESTRICT', + states={'readonly': Bool(Eval('parent'))}, + depends=['parent'], + help='Currency of the global limit. Sub-limits inherit it from ' + 'their parent.') amount = fields.Numeric('Amount', digits=(16, 2), required=True) tenor = fields.Integer('Tenor (days)', help='Maximum duration of financing from drawdown to repayment') + date_from = fields.Date('Valid From') + date_to = fields.Date('Valid To') sequence = fields.Integer('Sequence') + notes = fields.Text('Notes') _order = [('sequence', 'ASC'), ('id', 'ASC')] + @classmethod + def __register__(cls, module_name): + super().__register__(module_name) + cls._rebuild_tree('parent', None, 0) + haircuts = fields.One2Many('trade_finance.facility_limit_haircut', 'limit', 'Haircuts') currencies = fields.One2Many('trade_finance.facility_limit_currency', @@ -230,6 +247,38 @@ class FacilityLimit(ModelSQL, ModelView): def default_sequence(): return 10 + @staticmethod + def default_left(): + return 0 + + @staticmethod + def default_right(): + return 0 + + @fields.depends('facility', 'currency', 'date_from', 'date_to', 'parent', + '_parent_parent.facility', '_parent_parent.currency', + '_parent_parent.date_from', '_parent_parent.date_to') + def on_change_parent(self): + if self.parent: + self.facility = self.parent.facility + self.currency = self.parent.currency + if not self.date_from: + self.date_from = self.parent.date_from + if not self.date_to: + self.date_to = self.parent.date_to + + @fields.depends('facility', 'currency', 'date_from', 'date_to', 'parent', + '_parent_facility.currency', '_parent_facility.date_from', + '_parent_facility.date_to') + def on_change_facility(self): + if self.facility and not self.parent: + if not self.currency: + self.currency = self.facility.currency + if not self.date_from: + self.date_from = self.facility.date_from + if not self.date_to: + self.date_to = self.facility.date_to + @classmethod def create(cls, vlist): vlist = [v.copy() for v in vlist] @@ -237,13 +286,73 @@ class FacilityLimit(ModelSQL, ModelView): if values.get('parent') and not values.get('facility'): parent = cls(values['parent']) values['facility'] = parent.facility.id + if values.get('parent'): + parent = cls(values['parent']) + values['currency'] = ( + parent.currency.id if parent.currency else None) + values.setdefault('date_from', parent.date_from) + values.setdefault('date_to', parent.date_to) + elif values.get('facility'): + facility = Pool().get('trade_finance.facility')( + values['facility']) + values.setdefault('currency', + facility.currency.id if facility.currency else None) + values.setdefault('date_from', facility.date_from) + values.setdefault('date_to', facility.date_to) return super().create(vlist) + @classmethod + def write(cls, *args): + args = list(args) + actions = iter(args) + new_args = [] + check_descendants = [] + sync_currency = [] + for limits, values in zip(actions, actions): + values = values.copy() + if values.get('parent'): + parent = cls(values['parent']) + values['facility'] = parent.facility.id + values['currency'] = ( + parent.currency.id if parent.currency else None) + if {'parent', 'currency'} & set(values): + sync_currency.extend(limits) + if {'parent', 'currency', 'date_from', 'date_to'} & set(values): + check_descendants.extend(limits) + new_args.extend((limits, values)) + result = super().write(*new_args) + cls._sync_descendant_currencies(sync_currency) + cls._check_descendants(check_descendants) + return result + + @classmethod + def _sync_descendant_currencies(cls, limits): + for limit in limits: + limit = cls(limit.id) + if not limit.currency: + continue + descendants = cls.search([('parent', 'child_of', [limit.id])]) + if descendants: + super(FacilityLimit, cls).write( + descendants, {'currency': limit.currency.id}) + + @classmethod + def _check_descendants(cls, limits): + descendants = [] + for limit in limits: + descendants.extend( + cls.search([('parent', 'child_of', [limit.id])])) + if descendants: + cls.validate(descendants) + @classmethod def validate(cls, limits): super().validate(limits) for limit in limits: limit.check_single_root() + limit.check_required_values() + limit.check_parent_consistency() + limit.check_dates_within_parent() limit.check_amount_vs_parent() limit.check_children_amounts() @@ -260,6 +369,49 @@ class FacilityLimit(ModelSQL, ModelView): f"Limit ('{roots[0].name}'). Only one root limit is " f"allowed per facility.") + def check_required_values(self): + if not self.currency: + raise UserError( + f"Limit '{self.name}' must define or inherit a currency.") + if not self.date_from or not self.date_to: + raise UserError( + f"Limit '{self.name}' must define valid from and valid to " + f"dates.") + if self.date_from > self.date_to: + raise UserError( + f"Limit '{self.name}' valid from date cannot be after " + f"its valid to date.") + + def check_parent_consistency(self): + if not self.parent: + return + if self.facility != self.parent.facility: + raise UserError( + f"Sub-limit '{self.name}' must belong to the same facility " + f"as parent limit '{self.parent.name}'.") + if self.currency != self.parent.currency: + raise UserError( + f"Sub-limit '{self.name}' must use the same currency as " + f"parent limit '{self.parent.name}'.") + + def check_dates_within_parent(self): + if self.date_from and self.date_to and self.date_from > self.date_to: + raise UserError( + f"Limit '{self.name}' valid from date cannot be after its " + f"valid to date.") + if not self.parent: + return + if (self.date_from and self.parent.date_from + and self.date_from < self.parent.date_from): + raise UserError( + f"Sub-limit '{self.name}' valid from date cannot be before " + f"parent limit '{self.parent.name}' valid from date.") + if (self.date_to and self.parent.date_to + and self.date_to > self.parent.date_to): + raise UserError( + f"Sub-limit '{self.name}' valid to date cannot be after " + f"parent limit '{self.parent.name}' valid to date.") + def check_amount_vs_parent(self): if self.parent and self.amount > self.parent.amount: raise UserError( diff --git a/modules/trade_finance/tests/__init__.py b/modules/trade_finance/tests/__init__.py new file mode 100644 index 0000000..303e6e3 --- /dev/null +++ b/modules/trade_finance/tests/__init__.py @@ -0,0 +1,2 @@ +# This file is part of Tradon. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. diff --git a/modules/trade_finance/tests/test_module.py b/modules/trade_finance/tests/test_module.py new file mode 100644 index 0000000..e297e22 --- /dev/null +++ b/modules/trade_finance/tests/test_module.py @@ -0,0 +1,117 @@ +# This file is part of Tradon. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. + +from datetime import date +from decimal import Decimal + +from trytond.exceptions import UserError +from trytond.modules.currency.tests import create_currency +from trytond.pool import Pool +from trytond.tests.test_tryton import ModuleTestCase, with_transaction + + +class TradeFinanceTestCase(ModuleTestCase): + 'Test Trade Finance module' + module = 'trade_finance' + + def create_facility(self): + pool = Pool() + Bank = pool.get('bank') + Facility = pool.get('trade_finance.facility') + Party = pool.get('party.party') + + currency = create_currency('USD') + party = Party(name='Test Bank') + party.save() + bank = Bank(party=party) + bank.save() + facility = Facility( + name='Test Facility', + tfe=bank, + currency=currency, + commitment_status='committed', + date_from=date(2026, 1, 1), + date_to=date(2026, 12, 31)) + facility.save() + return facility, currency + + @with_transaction() + def test_sublimit_inherits_currency_and_dates(self): + 'Test sublimit inherits currency and dates from parent' + pool = Pool() + Limit = pool.get('trade_finance.facility_limit') + + facility, currency = self.create_facility() + root, = Limit.create([{ + 'facility': facility.id, + 'name': 'Global limit', + 'currency': currency.id, + 'amount': Decimal('100.00'), + 'date_from': date(2026, 1, 1), + 'date_to': date(2026, 12, 31), + }]) + child, = Limit.create([{ + 'parent': root.id, + 'name': 'Trading limit', + 'amount': Decimal('50.00'), + }]) + + self.assertEqual(child.facility, facility) + self.assertEqual(child.currency, currency) + self.assertEqual(child.date_from, root.date_from) + self.assertEqual(child.date_to, root.date_to) + + @with_transaction() + def test_sublimit_dates_must_stay_within_parent(self): + 'Test sublimit dates stay within parent dates' + pool = Pool() + Limit = pool.get('trade_finance.facility_limit') + + facility, currency = self.create_facility() + root, = Limit.create([{ + 'facility': facility.id, + 'name': 'Global limit', + 'currency': currency.id, + 'amount': Decimal('100.00'), + 'date_from': date(2026, 1, 1), + 'date_to': date(2026, 12, 31), + }]) + + with self.assertRaises(UserError): + Limit.create([{ + 'parent': root.id, + 'name': 'Late limit', + 'amount': Decimal('50.00'), + 'date_from': date(2026, 1, 1), + 'date_to': date(2027, 1, 1), + }]) + + @with_transaction() + def test_currency_change_cascades_to_sublimits(self): + 'Test root currency changes cascade to sublimits' + pool = Pool() + Limit = pool.get('trade_finance.facility_limit') + + facility, currency = self.create_facility() + other_currency = create_currency('EUR') + root, = Limit.create([{ + 'facility': facility.id, + 'name': 'Global limit', + 'currency': currency.id, + 'amount': Decimal('100.00'), + 'date_from': date(2026, 1, 1), + 'date_to': date(2026, 12, 31), + }]) + child, = Limit.create([{ + 'parent': root.id, + 'name': 'Trading limit', + 'amount': Decimal('50.00'), + }]) + + Limit.write([root], {'currency': other_currency.id}) + child = Limit(child.id) + + self.assertEqual(child.currency, other_currency) + + +del ModuleTestCase diff --git a/modules/trade_finance/view/facility_form.xml b/modules/trade_finance/view/facility_form.xml index 88b9a09..b2e296a 100644 --- a/modules/trade_finance/view/facility_form.xml +++ b/modules/trade_finance/view/facility_form.xml @@ -30,9 +30,10 @@ - + + domain="[('parent', '=', None)]" + view_ids="trade_finance.facility_limit_view_tree,trade_finance.facility_limit_view_form"/> diff --git a/modules/trade_finance/view/facility_limit_form.xml b/modules/trade_finance/view/facility_limit_form.xml index c49dbfe..5208e2e 100644 --- a/modules/trade_finance/view/facility_limit_form.xml +++ b/modules/trade_finance/view/facility_limit_form.xml @@ -6,8 +6,14 @@