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 @@
+
+
+
+
+
+
@@ -17,10 +23,13 @@
+
+
-
+
diff --git a/modules/trade_finance/view/facility_limit_tree.xml b/modules/trade_finance/view/facility_limit_tree.xml
index 551d4e8..4a9b90c 100644
--- a/modules/trade_finance/view/facility_limit_tree.xml
+++ b/modules/trade_finance/view/facility_limit_tree.xml
@@ -1,7 +1,11 @@
-
-
-
+
+
+
+
+
+
+