MukeshKapoor25's picture
chore(database): add comprehensive database schema and migration scripts
69f2a47
-- FUNCTION: trans.apply_bulk_stock_movements(jsonb)
-- DROP FUNCTION IF EXISTS trans.apply_bulk_stock_movements(jsonb);
CREATE OR REPLACE FUNCTION trans.apply_bulk_stock_movements(
p_movements jsonb)
RETURNS jsonb
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
AS $BODY$
DECLARE
v_movement JSONB;
v_results JSONB := '[]'::JSONB;
v_result JSONB;
v_success BOOLEAN := TRUE;
v_error_msg TEXT;
BEGIN
-- Process each movement in the array
FOR v_movement IN SELECT * FROM jsonb_array_elements(p_movements)
LOOP
BEGIN
-- Apply individual stock movement using the 9-parameter function
PERFORM trans.apply_stock_movement(
(v_movement->>'merchant_id')::TEXT,
(v_movement->>'warehouse_id')::TEXT,
(v_movement->>'sku')::TEXT,
(v_movement->>'batch_no')::TEXT,
(v_movement->>'catalogue_id')::TEXT,
(v_movement->>'expiry_date')::DATE,
(v_movement->>'uom')::TEXT,
(v_movement->>'qty')::NUMERIC,
(v_movement->>'txn_type')::TEXT,
(v_movement->>'ref_type')::TEXT,
(v_movement->>'ref_id')::UUID,
(v_movement->>'created_by')::TEXT,
(v_movement->>'ref_no')::TEXT,
(v_movement->>'remarks')::TEXT
);
-- Build success result object
v_result := jsonb_build_object(
'ledger_id', gen_random_uuid(),
'sku', v_movement->>'sku',
'qty', v_movement->>'qty',
'success', true,
'error', null
);
EXCEPTION WHEN OTHERS THEN
-- Handle individual movement errors
v_success := FALSE;
v_error_msg := SQLERRM;
v_result := jsonb_build_object(
'ledger_id', null,
'sku', v_movement->>'sku',
'qty', v_movement->>'qty',
'success', false,
'error', v_error_msg
);
END;
-- Add to results array
v_results := v_results || v_result;
END LOOP;
-- If any movement failed, rollback the entire transaction
IF NOT v_success THEN
RAISE EXCEPTION 'One or more stock movements failed. Transaction rolled back.';
END IF;
RETURN v_results;
END;
$BODY$;
ALTER FUNCTION trans.apply_bulk_stock_movements(jsonb)
OWNER TO trans_owner;
-- FUNCTION: trans.apply_stock_movement(text, text, text, text, text, date, text, numeric, text, text, uuid, text, text, text)
DROP FUNCTION IF EXISTS trans.apply_stock_movement(text, text, text, text, text, date, text, numeric, text, text, uuid, text);
DROP FUNCTION IF EXISTS trans.apply_stock_movement(text, text, text, text, text, date, text, numeric, text, text, uuid, text, text, text);
CREATE OR REPLACE FUNCTION trans.apply_stock_movement(
p_merchant_id text,
p_warehouse_id text,
p_sku text,
p_batch_no text,
p_catalogue_id text,
p_expiry_date date,
p_uom text,
p_qty numeric,
p_txn_type text,
p_ref_type text,
p_ref_id uuid,
p_user text,
p_ref_no text DEFAULT NULL,
p_remarks text DEFAULT NULL)
RETURNS void
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
AS $BODY$
DECLARE
v_ledger_id uuid := gen_random_uuid();
v_qty_change numeric;
BEGIN
----------------------------------------------------------------
-- Idempotency check
----------------------------------------------------------------
IF EXISTS (
SELECT 1
FROM trans.scm_stock_ledger
WHERE ref_type = p_ref_type
AND ref_id = p_ref_id
AND sku = p_sku
AND batch_no = p_batch_no
) THEN
RETURN;
END IF;
----------------------------------------------------------------
-- Determine quantity change based on transaction type
----------------------------------------------------------------
CASE p_txn_type
WHEN 'GRN_IN' THEN
v_qty_change := p_qty; -- Add stock
WHEN 'ADJUST_IN' THEN
v_qty_change := p_qty; -- Add stock
WHEN 'RETURN_IN' THEN
v_qty_change := p_qty; -- Add stock
WHEN 'TRANSFER_IN' THEN
v_qty_change := p_qty; -- Add stock
WHEN 'ADJUST_OUT' THEN
v_qty_change := -p_qty; -- Subtract stock (negate the positive qty)
WHEN 'SALE_OUT' THEN
v_qty_change := -p_qty; -- Subtract stock
WHEN 'TRANSFER_OUT' THEN
v_qty_change := -p_qty; -- Subtract stock
ELSE
RAISE EXCEPTION 'Unknown transaction type: %', p_txn_type;
END CASE;
Raise Notice 'in';
RAISE NOTICE 'Transaction type: %, Input qty: %, Calculated change: %',
p_txn_type, p_qty, v_qty_change;
RAISE NOTICE 'Inserting ledger: %, %, %, %, %, %, %, %, %, %, %',
v_ledger_id,
p_merchant_id,
p_warehouse_id,
p_sku,
p_batch_no,
p_txn_type,
v_qty_change, -- Store the calculated change in ledger
p_ref_type,
p_ref_id,
p_user,
NOW();
----------------------------------------------------------------
-- Ledger insert (store the ACTUAL qty change with proper sign)
----------------------------------------------------------------
INSERT INTO trans.scm_stock_ledger (
ledger_id,
merchant_id,
warehouse_id,
catalogue_id,
sku,
batch_no,
txn_type,
qty,
uom,
ref_type,
ref_id,
ref_no,
remarks,
created_by,
created_at
) VALUES (
v_ledger_id,
p_merchant_id,
p_warehouse_id,
p_catalogue_id,
p_sku,
p_batch_no,
p_txn_type,
v_qty_change, -- Store with correct sign
p_uom,
p_ref_type,
p_ref_id,
p_ref_no,
p_remarks,
p_user,
NOW()
);
RAISE NOTICE 'Stock ledger done';
----------------------------------------------------------------
-- Stock snapshot upsert (ALL TABLE COLUMNS)
----------------------------------------------------------------
INSERT INTO trans.scm_stock (
merchant_id,
warehouse_id,
sku,
batch_no,
catalogue_id,
uom,
qty_on_hand,
qty_reserved,
qty_available,
cost_price,
expiry_date,
ledger_id,
created_at,
updated_at
) VALUES (
p_merchant_id,
p_warehouse_id,
p_sku,
p_batch_no,
p_catalogue_id,
p_uom,
GREATEST(v_qty_change, 0), -- Use calculated change (don't allow negative on insert)
0, -- qty_reserved
GREATEST(v_qty_change, 0), -- qty_available
NULL, -- cost_price
p_expiry_date, -- expiry_date
v_ledger_id,
NOW(),
NOW()
)
ON CONFLICT (merchant_id, warehouse_id, catalogue_id, batch_no)
DO UPDATE SET
qty_on_hand = GREATEST(trans.scm_stock.qty_on_hand + v_qty_change, 0),
qty_available = GREATEST((trans.scm_stock.qty_on_hand + v_qty_change) - trans.scm_stock.qty_reserved, 0),
ledger_id = v_ledger_id,
updated_at = NOW();
RAISE NOTICE 'Stock updated. Final qty change applied: %', v_qty_change;
END;
$BODY$;
ALTER FUNCTION trans.apply_stock_movement(text, text, text, text, text, date, text, numeric, text, text, uuid, text, text, text)
OWNER TO trans_owner;
-- FUNCTION: trans.fn_get_po_item_details(uuid)
-- DROP FUNCTION IF EXISTS trans.fn_get_po_item_details(uuid);
CREATE OR REPLACE FUNCTION trans.fn_get_po_item_details(
p_po_id uuid)
RETURNS TABLE(po_id uuid, catalogue_code text, catalogue_name text, hsn_code text, gst_rate numeric, ord_qty numeric, dispatched_qty numeric, rcvd_qty numeric, rejected_qty numeric, returned_qty numeric, net_accepted_qty numeric, unit_price numeric, taxable_amount numeric, gst_amount numeric, final_amount numeric)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
ROWS 1000
AS $BODY$
BEGIN
RETURN QUERY
SELECT
poi.po_id,
cr.catalogue_code,
cr.catalogue_name,
cr.hsn_code,
cr.gst_rate,
poi.ord_qty,
poi.dispatched_qty,
poi.rcvd_qty,
poi.rejected_qty,
poi.returned_qty,
(poi.rcvd_qty - poi.rejected_qty - poi.returned_qty) AS net_accepted_qty,
poi.unit_price,
poi.line_amt AS taxable_amount,
poi.tax_amt AS gst_amount,
(poi.line_amt + poi.tax_amt) AS final_amount
FROM trans.scm_po_item poi
JOIN trans.catalogue_ref cr
ON cr.catalogue_id = poi.catalogue_id
WHERE poi.po_id = p_po_id;
END;
$BODY$;
ALTER FUNCTION trans.fn_get_po_item_details(uuid)
OWNER TO trans_owner;
-- FUNCTION: trans.fn_get_po_item_details(uuid)
-- DROP FUNCTION IF EXISTS trans.fn_get_po_item_details(uuid);
CREATE OR REPLACE FUNCTION trans.fn_get_po_item_details(
p_po_id uuid)
RETURNS TABLE(po_id uuid, catalogue_code text, catalogue_name text, hsn_code text, gst_rate numeric, ord_qty numeric, dispatched_qty numeric, rcvd_qty numeric, rejected_qty numeric, returned_qty numeric, net_accepted_qty numeric, unit_price numeric, taxable_amount numeric, gst_amount numeric, final_amount numeric)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
ROWS 1000
AS $BODY$
BEGIN
RETURN QUERY
SELECT
poi.po_id,
cr.catalogue_code,
cr.catalogue_name,
cr.hsn_code,
cr.gst_rate,
poi.ord_qty,
poi.dispatched_qty,
poi.rcvd_qty,
poi.rejected_qty,
poi.returned_qty,
(poi.rcvd_qty - poi.rejected_qty - poi.returned_qty) AS net_accepted_qty,
poi.unit_price,
poi.line_amt AS taxable_amount,
poi.tax_amt AS gst_amount,
(poi.line_amt + poi.tax_amt) AS final_amount
FROM trans.scm_po_item poi
JOIN trans.catalogue_ref cr
ON cr.catalogue_id = poi.catalogue_id
WHERE poi.po_id = p_po_id;
END;
$BODY$;
ALTER FUNCTION trans.fn_get_po_item_details(uuid)
OWNER TO trans_owner;
-- FUNCTION: trans.fn_get_po_items_for_grn(uuid)
-- DROP FUNCTION IF EXISTS trans.fn_get_po_items_for_grn(uuid);
CREATE OR REPLACE FUNCTION trans.fn_get_po_items_for_grn(
p_po_id uuid)
RETURNS TABLE(po_item_id uuid, catalogue_id uuid, catalogue_name text, ordered_qty numeric, received_qty numeric, rejected_qty numeric, pending_grn numeric, cost_price numeric)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
ROWS 1000
AS $BODY$
BEGIN
RETURN QUERY
SELECT
po.po_item_id AS po_item_id,
po.catalogue_id,
cr.catalogue_name,
po.ord_qty AS ordered_qty,
po.rcvd_qty AS received_qty,
po.rejected_qty,
(po.ord_qty - po.rcvd_qty - po.rejected_qty) AS pending_grn,
po.unit_price AS cost_price
FROM trans.scm_po_item po
JOIN trans.catalogue_ref cr
ON po.catalogue_id = cr.catalogue_id::uuid
WHERE po.po_id = p_po_id
AND (po.ord_qty - po.rcvd_qty - po.rejected_qty) > 0
ORDER BY cr.catalogue_name;
END;
$BODY$;
ALTER FUNCTION trans.fn_get_po_items_for_grn(uuid)
OWNER TO trans_owner;
-- FUNCTION: trans.fn_get_po_items_for_order_process(uuid, text, text)
-- DROP FUNCTION IF EXISTS trans.fn_get_po_items_for_order_process(uuid, text, text);
CREATE OR REPLACE FUNCTION trans.fn_get_po_items_for_order_process(
p_po_id uuid,
p_warehouse_id text,
p_mode text)
RETURNS TABLE(po_item_id uuid, catalogue_id uuid, catalogue_name text, ordered_qty numeric, received_qty numeric, dispatched_qty numeric, pending_qty numeric, qty_available numeric, batch_no character varying, expiry_date date, unit_price numeric)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
ROWS 1000
AS $BODY$
BEGIN
IF p_mode = 'DISPATCH' THEN
RETURN QUERY
SELECT
po.po_item_id,
po.catalogue_id,
cr.catalogue_name,
po.ord_uom_qty,
po.rcvd_qty,
0::numeric AS dispatched_qty,
(po.ord_uom_qty - 0::numeric) AS pending_qty,
st.qty_available,
st.batch_no,
st.expiry_date,
po.unit_price
FROM trans.scm_po_item po
JOIN trans.catalogue_ref cr
ON po.catalogue_id = cr.catalogue_id
JOIN trans.scm_stock st
ON st.catalogue_id = po.catalogue_id
AND st.warehouse_id = p_warehouse_id
WHERE po.po_id = p_po_id
AND (po.ord_uom_qty - 0::numeric) > 0
AND st.qty_available > 0
ORDER BY cr.catalogue_name, st.expiry_date;
ELSIF p_mode = 'INVOICE' THEN
RETURN QUERY
SELECT
po.po_item_id,
po.catalogue_id,
cr.catalogue_name,
po.ord_uom_qty,
po.rcvd_qty,
0::numeric AS dispatched_qty,
(po.ord_uom_qty - 0::numeric) AS pending_qty,
NULL::numeric AS qty_available,
NULL::character varying(50) AS batch_no,
NULL::date AS expiry_date,
po.unit_price
FROM trans.scm_po_item po
JOIN trans.catalogue_ref cr
ON po.catalogue_id = cr.catalogue_id
WHERE po.po_id = p_po_id
AND (po.ord_uom_qty - po.returned_qty) > 0
ORDER BY cr.catalogue_name;
ELSE
RAISE EXCEPTION 'Invalid p_mode: %', p_mode;
END IF;
END;
$BODY$;
ALTER FUNCTION trans.fn_get_po_items_for_order_process(uuid, text, text)
OWNER TO trans_owner;
-- FUNCTION: trans.fn_get_po_items_for_purchase_return(uuid, character varying)
-- DROP FUNCTION IF EXISTS trans.fn_get_po_items_for_purchase_return(uuid, character varying);
-- DROP FUNCTION IF EXISTS trans.fn_get_po_items_for_purchase_return(uuid, uuid);
CREATE OR REPLACE FUNCTION trans.fn_get_po_items_for_purchase_return(
p_po_id uuid,
p_warehouse_id character varying)
RETURNS TABLE(
po_item_id uuid,
catalogue_id uuid,
catalogue_name text,
sku text,
ordered_qty numeric,
received_qty numeric,
returned_qty numeric,
dispatched_qty numeric,
returnable_qty numeric,
batch_no text,
expiry_date date,
qty_available numeric,
unit_price numeric
)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
ROWS 1000
AS $BODY$
BEGIN
RETURN QUERY
SELECT
pi.po_item_id,
pi.catalogue_id,
cr.catalogue_name,
pi.sku::text,
pi.ord_qty,
pi.rcvd_qty,
pi.returned_qty,
pi.dispatched_qty,
(
COALESCE(pi.rcvd_qty, 0)
- COALESCE(pi.rejected_qty, 0)
- COALESCE(pi.returned_qty, 0)
- COALESCE(pi.dispatched_qty, 0)
) AS returnable_qty,
st.batch_no::text,
st.expiry_date,
st.qty_available,
pi.unit_price
FROM trans.scm_po_item pi
JOIN trans.catalogue_ref cr
ON cr.catalogue_id = pi.catalogue_id
JOIN trans.scm_stock st
ON st.catalogue_id::uuid = pi.catalogue_id
AND st.warehouse_id = p_warehouse_id
WHERE pi.po_id = p_po_id
AND st.qty_available > 0
ORDER BY cr.catalogue_name, st.expiry_date;
END;
$BODY$;
ALTER FUNCTION trans.fn_get_po_items_for_purchase_return(uuid, character varying)
OWNER TO trans_owner;
-- FUNCTION: trans.fn_get_po_ready_for_action(text, character varying)
-- DROP FUNCTION IF EXISTS trans.fn_get_po_ready_for_action(text, character varying);
CREATE OR REPLACE FUNCTION trans.fn_get_po_ready_for_action(
p_mode text,
p_client_id character varying DEFAULT NULL::character varying)
RETURNS TABLE(po_id uuid, po_no character varying, supplier_id character varying, client_id character varying, shipment_count integer, latest_shipment_date date, shipment_no text, lr_no text, transporter text)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
ROWS 1000
AS $BODY$
BEGIN
IF p_mode = 'INVOICE' THEN
RETURN QUERY
SELECT
po.po_id,
po.po_no,
po.supplier_id::text,
po.buyer_id::text,
COUNT(DISTINCT sh.shipment_id)::integer AS shipment_count,
MAX(sh.shipment_date)::date AS latest_shipment_date,
MAX(sh.shipment_no) AS shipment_no,
MAX(sh.lr_no) AS lr_no,
MAX(sh.transporter) AS transporter
FROM trans.scm_po po
JOIN trans.scm_trade_shipment sh
ON sh.order_id = po.po_id
AND sh.status IN ('shipped', 'closed')
WHERE po.status = 'APPROVED'
AND (p_client_id IS NULL OR po.buyer_id::text = p_client_id)
GROUP BY
po.po_id,
po.po_no,
po.supplier_id,
po.buyer_id
ORDER BY latest_shipment_date DESC;
ELSIF p_mode = 'DISPATCH' THEN
RETURN QUERY
SELECT
po.po_id,
po.po_no,
po.supplier_id::text,
po.buyer_id::text,
COUNT(DISTINCT sh.shipment_id)::integer AS shipment_count,
MAX(sh.shipment_date)::date AS latest_shipment_date,
MAX(sh.shipment_no) AS shipment_no,
MAX(sh.lr_no) AS lr_no,
MAX(sh.transporter) AS transporter
FROM trans.scm_po po
LEFT JOIN trans.scm_trade_shipment sh
ON sh.order_id = po.po_id
WHERE po.status = 'APPROVED'
AND (p_client_id IS NULL OR po.buyer_id::text = p_client_id)
GROUP BY
po.po_id,
po.po_no,
po.supplier_id,
po.buyer_id
ORDER BY latest_shipment_date DESC NULLS FIRST, po.po_no DESC;
ELSE
RAISE EXCEPTION 'Invalid p_mode: % (expected DISPATCH or INVOICE)', p_mode;
END IF;
END;
$BODY$;
ALTER FUNCTION trans.fn_get_po_ready_for_action(text, character varying)
OWNER TO trans_owner;
-- FUNCTION: trans.fn_get_po_ready_for_purchase_return(character varying)
-- DROP FUNCTION IF EXISTS trans.fn_get_po_ready_for_purchase_return(character varying);
CREATE OR REPLACE FUNCTION trans.fn_get_po_ready_for_purchase_return(
p_client_id character varying DEFAULT NULL::character varying)
RETURNS TABLE(po_id uuid, po_no character varying, supplier_id uuid, client_id uuid, returnable_qty numeric, last_grn_date date)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
ROWS 1000
AS $BODY$
BEGIN
RETURN QUERY
SELECT
po.po_id,
po.po_no,
po.supplier_id,
po.buyer_id AS client_id,
SUM(
pi.rcvd_qty
- pi.rejected_qty
- pi.returned_qty
- pi.dispatched_qty
) AS returnable_qty,
MAX(grn.recv_dt) AS last_grn_date
FROM trans.scm_po po
JOIN trans.scm_po_item pi
ON pi.po_id = po.po_id
LEFT JOIN trans.scm_grn grn
ON grn.po_id = po.po_id
WHERE po.status = 'APPROVED'
AND (p_client_id IS NULL OR po.buyer_id::text = p_client_id)
GROUP BY
po.po_id,
po.po_no,
po.supplier_id,
po.buyer_id
HAVING
SUM(
pi.rcvd_qty
- pi.rejected_qty
- pi.returned_qty
- pi.dispatched_qty
) > 0
ORDER BY last_grn_date DESC;
END;
$BODY$;
ALTER FUNCTION trans.fn_get_po_ready_for_purchase_return(character varying)
OWNER TO trans_owner;
-- FUNCTION: trans.get_stock_ledger(character varying, character varying, character varying, integer)
-- DROP FUNCTION IF EXISTS trans.get_stock_ledger(character varying, character varying, character varying, integer);
CREATE OR REPLACE FUNCTION trans.get_stock_ledger(
p_merchant_id character varying,
p_sku character varying DEFAULT NULL::character varying,
p_batch_no character varying DEFAULT NULL::character varying,
p_limit integer DEFAULT 100)
RETURNS TABLE(ledger_id uuid, warehouse_id character varying, catalogue_id uuid, sku character varying, batch_no character varying, txn_type character varying, qty numeric, uom character varying, ref_type character varying, ref_id uuid, ref_no character varying, remarks text, created_by character varying, created_at timestamp without time zone)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
ROWS 1000
AS $BODY$
BEGIN
RETURN QUERY
SELECT l.ledger_id, l.warehouse_id, l.catalogue_id, l.sku, l.batch_no,
l.txn_type, l.qty, l.uom, l.ref_type, l.ref_id, l.ref_no,
l.remarks, l.created_by, l.created_at
FROM trans.scm_stock_ledger l
WHERE l.merchant_id = p_merchant_id
AND (p_sku IS NULL OR l.sku = p_sku)
AND (p_batch_no IS NULL OR l.batch_no = p_batch_no)
ORDER BY l.created_at DESC
LIMIT p_limit;
END;
$BODY$;
ALTER FUNCTION trans.get_stock_ledger(character varying, character varying, character varying, integer)
OWNER TO trans_owner;
-- FUNCTION: trans.get_stock_summary(character varying, character varying, character varying)
-- DROP FUNCTION IF EXISTS trans.get_stock_summary(character varying, character varying, character varying);
CREATE OR REPLACE FUNCTION trans.get_stock_summary(
p_merchant_id character varying,
p_warehouse_id character varying DEFAULT NULL::character varying,
p_sku character varying DEFAULT NULL::character varying)
RETURNS TABLE(stock_id uuid, warehouse_id character varying, catalogue_id uuid, sku character varying, batch_no character varying, qty_on_hand numeric, qty_reserved numeric, qty_available numeric, uom character varying, updated_at timestamp without time zone)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
ROWS 1000
AS $BODY$
BEGIN
RETURN QUERY
SELECT s.stock_id, s.warehouse_id, s.catalogue_id, s.sku, s.batch_no,
s.qty_on_hand, s.qty_reserved, s.qty_available, s.uom, s.updated_at
FROM trans.scm_stock s
WHERE s.merchant_id = p_merchant_id
AND (p_warehouse_id IS NULL OR s.warehouse_id = p_warehouse_id)
AND (p_sku IS NULL OR s.sku = p_sku)
ORDER BY s.sku, s.batch_no;
END;
$BODY$;
ALTER FUNCTION trans.get_stock_summary(character varying, character varying, character varying)
OWNER TO trans_owner;
-- FUNCTION: trans.get_trade_sales_analytics(character varying, character varying, date, date)
-- DROP FUNCTION IF EXISTS trans.get_trade_sales_analytics(character varying, character varying, date, date);
CREATE OR REPLACE FUNCTION trans.get_trade_sales_analytics(
p_supplier_id character varying DEFAULT NULL::character varying,
p_client_id character varying DEFAULT NULL::character varying,
p_date_from date DEFAULT NULL::date,
p_date_to date DEFAULT NULL::date)
RETURNS TABLE(metric_name character varying, metric_value numeric, metric_unit character varying)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
ROWS 1000
AS $BODY$
BEGIN
-- Total shipments
RETURN QUERY
SELECT
'total_shipments'::VARCHAR(50) as metric_name,
COUNT(*)::NUMERIC as metric_value,
'count'::VARCHAR(20) as metric_unit
FROM trans.scm_trade_shipment ts
WHERE (p_supplier_id IS NULL OR ts.supplier_id = p_supplier_id)
AND (p_client_id IS NULL OR ts.client_id = p_client_id)
AND (p_date_from IS NULL OR ts.shipment_date >= p_date_from)
AND (p_date_to IS NULL OR ts.shipment_date <= p_date_to);
-- Total quantity shipped
RETURN QUERY
SELECT
'total_qty_shipped'::VARCHAR(50) as metric_name,
COALESCE(SUM(tsi.shipped_qty), 0)::NUMERIC as metric_value,
'units'::VARCHAR(20) as metric_unit
FROM trans.scm_trade_shipment ts
JOIN trans.scm_trade_shipment_item tsi ON ts.shipment_id = tsi.shipment_id
WHERE (p_supplier_id IS NULL OR ts.supplier_id = p_supplier_id)
AND (p_client_id IS NULL OR ts.client_id = p_client_id)
AND (p_date_from IS NULL OR ts.shipment_date >= p_date_from)
AND (p_date_to IS NULL OR ts.shipment_date <= p_date_to);
-- Unique SKUs shipped
RETURN QUERY
SELECT
'unique_skus'::VARCHAR(50) as metric_name,
COUNT(DISTINCT tsi.sku)::NUMERIC as metric_value,
'count'::VARCHAR(20) as metric_unit
FROM trans.scm_trade_shipment ts
JOIN trans.scm_trade_shipment_item tsi ON ts.shipment_id = tsi.shipment_id
WHERE (p_supplier_id IS NULL OR ts.supplier_id = p_supplier_id)
AND (p_client_id IS NULL OR ts.client_id = p_client_id)
AND (p_date_from IS NULL OR ts.shipment_date >= p_date_from)
AND (p_date_to IS NULL OR ts.shipment_date <= p_date_to);
-- Average shipment size
RETURN QUERY
SELECT
'avg_shipment_size'::VARCHAR(50) as metric_name,
COALESCE(AVG(shipment_totals.total_qty), 0)::NUMERIC as metric_value,
'units'::VARCHAR(20) as metric_unit
FROM (
SELECT ts.shipment_id, SUM(tsi.shipped_qty) as total_qty
FROM trans.scm_trade_shipment ts
JOIN trans.scm_trade_shipment_item tsi ON ts.shipment_id = tsi.shipment_id
WHERE (p_supplier_id IS NULL OR ts.supplier_id = p_supplier_id)
AND (p_client_id IS NULL OR ts.client_id = p_client_id)
AND (p_date_from IS NULL OR ts.shipment_date >= p_date_from)
AND (p_date_to IS NULL OR ts.shipment_date <= p_date_to)
GROUP BY ts.shipment_id
) shipment_totals;
END;
$BODY$;
ALTER FUNCTION trans.get_trade_sales_analytics(character varying, character varying, date, date)
OWNER TO trans_owner;
-- FUNCTION: trans.release_stock_reservation(character varying, character varying, character varying, character varying, numeric, uuid, character varying)
-- DROP FUNCTION IF EXISTS trans.release_stock_reservation(character varying, character varying, character varying, character varying, numeric, uuid, character varying);
CREATE OR REPLACE FUNCTION trans.release_stock_reservation(
p_merchant_id character varying,
p_warehouse_id character varying,
p_sku character varying,
p_batch_no character varying,
p_qty numeric,
p_ref_id uuid,
p_created_by character varying)
RETURNS boolean
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
AS $BODY$
BEGIN
-- Update stock reservation
UPDATE trans.scm_stock
SET qty_reserved = GREATEST(qty_reserved - p_qty, 0),
qty_available = qty_on_hand - GREATEST(qty_reserved - p_qty, 0),
updated_at = NOW()
WHERE merchant_id = p_merchant_id
AND warehouse_id = p_warehouse_id
AND sku = p_sku
AND batch_no = p_batch_no;
RETURN TRUE;
END;
$BODY$;
ALTER FUNCTION trans.release_stock_reservation(character varying, character varying, character varying, character varying, numeric, uuid, character varying)
OWNER TO trans_owner;
-- FUNCTION: trans.reserve_stock(character varying, character varying, character varying, character varying, numeric, uuid, character varying)
-- DROP FUNCTION IF EXISTS trans.reserve_stock(character varying, character varying, character varying, character varying, numeric, uuid, character varying);
CREATE OR REPLACE FUNCTION trans.reserve_stock(
p_merchant_id character varying,
p_warehouse_id character varying,
p_sku character varying,
p_batch_no character varying,
p_qty numeric,
p_ref_id uuid,
p_created_by character varying)
RETURNS boolean
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
AS $BODY$
DECLARE
v_available_qty NUMERIC(14,3);
BEGIN
-- Get current available quantity
SELECT qty_available
INTO v_available_qty
FROM trans.scm_stock
WHERE merchant_id = p_merchant_id
AND warehouse_id = p_warehouse_id
AND sku = p_sku
AND batch_no = p_batch_no;
-- Check if sufficient stock available
IF v_available_qty IS NULL OR v_available_qty < p_qty THEN
RAISE EXCEPTION 'Insufficient available stock for SKU: %. Available: %, Required: %',
p_sku, COALESCE(v_available_qty, 0), p_qty;
END IF;
-- Update stock reservation
UPDATE trans.scm_stock
SET qty_reserved = qty_reserved + p_qty,
qty_available = qty_available - p_qty,
updated_at = NOW()
WHERE merchant_id = p_merchant_id
AND warehouse_id = p_warehouse_id
AND sku = p_sku
AND batch_no = p_batch_no;
RETURN TRUE;
END;
$BODY$;
ALTER FUNCTION trans.reserve_stock(character varying, character varying, character varying, character varying, numeric, uuid, character varying)
OWNER TO trans_owner;
-- FUNCTION: trans.update_adjustment_note_timestamp()
CREATE OR REPLACE FUNCTION trans.update_adjustment_note_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
ALTER FUNCTION trans.update_adjustment_note_timestamp()
OWNER TO trans_owner;
-- FUNCTION: trans.update_trade_shipment_updated_at()
CREATE OR REPLACE FUNCTION trans.update_trade_shipment_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
ALTER FUNCTION trans.update_trade_shipment_updated_at()
OWNER TO trans_owner;
-- FUNCTION: trans.get_next_scheme_sequence(integer)
CREATE OR REPLACE FUNCTION trans.get_next_scheme_sequence(scheme_year INTEGER)
RETURNS INTEGER AS $$
DECLARE
next_seq INTEGER;
BEGIN
SELECT COALESCE(MAX(
CAST(SUBSTRING(scheme_code FROM 'SCH-[0-9]{4}-([0-9]{6})') AS INTEGER)
), 0) + 1
INTO next_seq
FROM trans.trade_schemes
WHERE scheme_code LIKE 'SCH-' || scheme_year || '-%';
RETURN next_seq;
END;
$$ LANGUAGE plpgsql;
ALTER FUNCTION trans.get_next_scheme_sequence(integer)
OWNER TO trans_owner;