Spaces:
Runtime error
Runtime error
Commit ·
47cf637
1
Parent(s): 611c7c7
fix(stock,trade_sales): correct stored procedure calls and enhance shipment queries
Browse files- Change CALL to SELECT for apply_stock_movement function since it returns VOID
- Add catalogue_id and uom fields to shipment order query for complete item details
- Update GROUP BY clause to include catalogue_id and uom for proper aggregation
- Remove misleading comment about missing catalogue_id in stock transaction
- Add comprehensive stock management stored procedures documentation with idempotency checks and bulk movement support
- Improve error handling in bulk stock movement operations with transaction rollback on failure
app/inventory/stock/services/service.py
CHANGED
|
@@ -90,9 +90,9 @@ class StockService:
|
|
| 90 |
# Validate transaction data
|
| 91 |
self._validate_transaction(transaction)
|
| 92 |
|
| 93 |
-
# Call simplified stored procedure
|
| 94 |
query = text("""
|
| 95 |
-
|
| 96 |
:merchant_id, :location_id, :sku, :batch_no, :qty,
|
| 97 |
:txn_type, :ref_type, :ref_id, :ref_no, :created_by
|
| 98 |
)
|
|
|
|
| 90 |
# Validate transaction data
|
| 91 |
self._validate_transaction(transaction)
|
| 92 |
|
| 93 |
+
# Call simplified stored procedure - use SELECT since it returns VOID
|
| 94 |
query = text("""
|
| 95 |
+
SELECT apply_stock_movement(
|
| 96 |
:merchant_id, :location_id, :sku, :batch_no, :qty,
|
| 97 |
:txn_type, :ref_type, :ref_id, :ref_no, :created_by
|
| 98 |
)
|
app/trade_sales/services/service.py
CHANGED
|
@@ -411,13 +411,13 @@ class TradeSalesService:
|
|
| 411 |
async with self.db.begin():
|
| 412 |
# Validate order exists and get details
|
| 413 |
order_query = text("""
|
| 414 |
-
SELECT po.*, poi.po_item_id, poi.sku, poi.ord_qty,
|
| 415 |
COALESCE(SUM(tsi.shipped_qty), 0) as already_shipped
|
| 416 |
FROM trans.scm_po po
|
| 417 |
JOIN trans.scm_po_item poi ON po.po_id = poi.po_id
|
| 418 |
LEFT JOIN trans.scm_trade_shipment_item tsi ON poi.po_item_id = tsi.po_item_id
|
| 419 |
WHERE po.po_id = :order_id
|
| 420 |
-
GROUP BY po.po_id, poi.po_item_id, poi.sku, poi.ord_qty
|
| 421 |
""")
|
| 422 |
|
| 423 |
order_result = await self.db.execute(order_query, {"order_id": str(order_id)})
|
|
@@ -491,7 +491,7 @@ class TradeSalesService:
|
|
| 491 |
stock_transaction = StockTransaction(
|
| 492 |
merchant_id=supplier_id,
|
| 493 |
location_id=str(request.warehouse_id),
|
| 494 |
-
catalogue_id=str(order_item.catalogue_id),
|
| 495 |
sku=ship_item.sku,
|
| 496 |
batch_no=ship_item.batch_no,
|
| 497 |
exp_dt=None, # Will be handled by stock service
|
|
|
|
| 411 |
async with self.db.begin():
|
| 412 |
# Validate order exists and get details
|
| 413 |
order_query = text("""
|
| 414 |
+
SELECT po.*, poi.po_item_id, poi.sku, poi.ord_qty, poi.catalogue_id, poi.uom,
|
| 415 |
COALESCE(SUM(tsi.shipped_qty), 0) as already_shipped
|
| 416 |
FROM trans.scm_po po
|
| 417 |
JOIN trans.scm_po_item poi ON po.po_id = poi.po_id
|
| 418 |
LEFT JOIN trans.scm_trade_shipment_item tsi ON poi.po_item_id = tsi.po_item_id
|
| 419 |
WHERE po.po_id = :order_id
|
| 420 |
+
GROUP BY po.po_id, poi.po_item_id, poi.sku, poi.ord_qty, poi.catalogue_id, poi.uom
|
| 421 |
""")
|
| 422 |
|
| 423 |
order_result = await self.db.execute(order_query, {"order_id": str(order_id)})
|
|
|
|
| 491 |
stock_transaction = StockTransaction(
|
| 492 |
merchant_id=supplier_id,
|
| 493 |
location_id=str(request.warehouse_id),
|
| 494 |
+
catalogue_id=str(order_item.catalogue_id),
|
| 495 |
sku=ship_item.sku,
|
| 496 |
batch_no=ship_item.batch_no,
|
| 497 |
exp_dt=None, # Will be handled by stock service
|
docs/database/sql/stored_procedures/stock_management_updated.sql
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- Stock Management Stored Procedures - UPDATED VERSION
|
| 2 |
+
-- Centralized inventory mutation logic in PostgreSQL with proper schema references
|
| 3 |
+
|
| 4 |
+
-- Simplified stock movement function with idempotency and proper schema references
|
| 5 |
+
CREATE OR REPLACE FUNCTION apply_stock_movement(
|
| 6 |
+
p_merchant_id TEXT,
|
| 7 |
+
p_location_id TEXT,
|
| 8 |
+
p_sku TEXT,
|
| 9 |
+
p_batch_no TEXT,
|
| 10 |
+
p_qty NUMERIC,
|
| 11 |
+
p_txn_type TEXT,
|
| 12 |
+
p_ref_type TEXT,
|
| 13 |
+
p_ref_id UUID,
|
| 14 |
+
p_ref_no TEXT,
|
| 15 |
+
p_user TEXT
|
| 16 |
+
) RETURNS VOID AS $$
|
| 17 |
+
BEGIN
|
| 18 |
+
-- Idempotency check - prevent duplicate processing
|
| 19 |
+
IF EXISTS (
|
| 20 |
+
SELECT 1 FROM trans.scm_stock_ledger
|
| 21 |
+
WHERE ref_type = p_ref_type AND ref_id = p_ref_id AND sku = p_sku AND batch_no = p_batch_no
|
| 22 |
+
) THEN
|
| 23 |
+
RETURN;
|
| 24 |
+
END IF;
|
| 25 |
+
|
| 26 |
+
-- Insert ledger entry (immutable audit trail)
|
| 27 |
+
INSERT INTO trans.scm_stock_ledger (
|
| 28 |
+
ledger_id, merchant_id, location_id, sku, batch_no,
|
| 29 |
+
qty, txn_type, ref_type, ref_id, ref_no, created_by, created_at
|
| 30 |
+
) VALUES (
|
| 31 |
+
gen_random_uuid(), p_merchant_id, p_location_id, p_sku, p_batch_no,
|
| 32 |
+
p_qty, p_txn_type, p_ref_type, p_ref_id, p_ref_no, p_user, NOW()
|
| 33 |
+
);
|
| 34 |
+
|
| 35 |
+
-- Stock snapshot upsert (create or update)
|
| 36 |
+
INSERT INTO trans.scm_stock (
|
| 37 |
+
stock_id, merchant_id, location_id, sku, batch_no,
|
| 38 |
+
qty_on_hand, qty_reserved, qty_available, created_at, updated_at, last_updated_at
|
| 39 |
+
) VALUES (
|
| 40 |
+
gen_random_uuid(), p_merchant_id, p_location_id, p_sku, p_batch_no,
|
| 41 |
+
GREATEST(p_qty, 0), 0, GREATEST(p_qty, 0), NOW(), NOW(), NOW()
|
| 42 |
+
)
|
| 43 |
+
ON CONFLICT (merchant_id, location_id, sku, batch_no)
|
| 44 |
+
DO UPDATE SET
|
| 45 |
+
qty_on_hand = trans.scm_stock.qty_on_hand + p_qty,
|
| 46 |
+
qty_available = GREATEST(trans.scm_stock.qty_on_hand + p_qty - trans.scm_stock.qty_reserved, 0),
|
| 47 |
+
updated_at = NOW(),
|
| 48 |
+
last_updated_at = NOW();
|
| 49 |
+
END;
|
| 50 |
+
$$ LANGUAGE plpgsql;
|
| 51 |
+
|
| 52 |
+
-- Enhanced bulk stock movement function with proper error handling
|
| 53 |
+
CREATE OR REPLACE FUNCTION apply_bulk_stock_movements(
|
| 54 |
+
p_movements JSONB
|
| 55 |
+
) RETURNS JSONB
|
| 56 |
+
LANGUAGE plpgsql
|
| 57 |
+
AS $$
|
| 58 |
+
DECLARE
|
| 59 |
+
v_movement JSONB;
|
| 60 |
+
v_results JSONB := '[]'::JSONB;
|
| 61 |
+
v_result JSONB;
|
| 62 |
+
v_success BOOLEAN := TRUE;
|
| 63 |
+
v_error_msg TEXT;
|
| 64 |
+
BEGIN
|
| 65 |
+
-- Process each movement in the array
|
| 66 |
+
FOR v_movement IN SELECT * FROM jsonb_array_elements(p_movements)
|
| 67 |
+
LOOP
|
| 68 |
+
BEGIN
|
| 69 |
+
-- Apply individual stock movement
|
| 70 |
+
PERFORM apply_stock_movement(
|
| 71 |
+
(v_movement->>'merchant_id')::TEXT,
|
| 72 |
+
(v_movement->>'location_id')::TEXT,
|
| 73 |
+
(v_movement->>'sku')::TEXT,
|
| 74 |
+
(v_movement->>'batch_no')::TEXT,
|
| 75 |
+
(v_movement->>'qty')::NUMERIC,
|
| 76 |
+
(v_movement->>'txn_type')::TEXT,
|
| 77 |
+
(v_movement->>'ref_type')::TEXT,
|
| 78 |
+
(v_movement->>'ref_id')::UUID,
|
| 79 |
+
(v_movement->>'ref_no')::TEXT,
|
| 80 |
+
(v_movement->>'created_by')::TEXT
|
| 81 |
+
);
|
| 82 |
+
|
| 83 |
+
-- Build success result object
|
| 84 |
+
v_result := jsonb_build_object(
|
| 85 |
+
'ledger_id', gen_random_uuid(),
|
| 86 |
+
'sku', v_movement->>'sku',
|
| 87 |
+
'qty', v_movement->>'qty',
|
| 88 |
+
'success', true,
|
| 89 |
+
'error', null
|
| 90 |
+
);
|
| 91 |
+
|
| 92 |
+
EXCEPTION WHEN OTHERS THEN
|
| 93 |
+
-- Handle individual movement errors
|
| 94 |
+
v_success := FALSE;
|
| 95 |
+
v_error_msg := SQLERRM;
|
| 96 |
+
|
| 97 |
+
v_result := jsonb_build_object(
|
| 98 |
+
'ledger_id', null,
|
| 99 |
+
'sku', v_movement->>'sku',
|
| 100 |
+
'qty', v_movement->>'qty',
|
| 101 |
+
'success', false,
|
| 102 |
+
'error', v_error_msg
|
| 103 |
+
);
|
| 104 |
+
END;
|
| 105 |
+
|
| 106 |
+
-- Add to results array
|
| 107 |
+
v_results := v_results || v_result;
|
| 108 |
+
END LOOP;
|
| 109 |
+
|
| 110 |
+
-- If any movement failed, rollback the entire transaction
|
| 111 |
+
IF NOT v_success THEN
|
| 112 |
+
RAISE EXCEPTION 'One or more stock movements failed. Transaction rolled back.';
|
| 113 |
+
END IF;
|
| 114 |
+
|
| 115 |
+
RETURN v_results;
|
| 116 |
+
END;
|
| 117 |
+
$$;
|
| 118 |
+
|
| 119 |
+
-- Function to reserve stock (for sales orders) with proper schema
|
| 120 |
+
CREATE OR REPLACE FUNCTION reserve_stock(
|
| 121 |
+
p_merchant_id VARCHAR(64),
|
| 122 |
+
p_location_id VARCHAR(64),
|
| 123 |
+
p_sku VARCHAR(64),
|
| 124 |
+
p_batch_no VARCHAR(50),
|
| 125 |
+
p_qty NUMERIC(14,3),
|
| 126 |
+
p_ref_id UUID,
|
| 127 |
+
p_created_by VARCHAR(64)
|
| 128 |
+
) RETURNS BOOLEAN
|
| 129 |
+
LANGUAGE plpgsql
|
| 130 |
+
AS $$
|
| 131 |
+
DECLARE
|
| 132 |
+
v_available_qty NUMERIC(14,3);
|
| 133 |
+
BEGIN
|
| 134 |
+
-- Get current available quantity
|
| 135 |
+
SELECT qty_available
|
| 136 |
+
INTO v_available_qty
|
| 137 |
+
FROM trans.scm_stock
|
| 138 |
+
WHERE merchant_id = p_merchant_id
|
| 139 |
+
AND location_id = p_location_id
|
| 140 |
+
AND sku = p_sku
|
| 141 |
+
AND batch_no = p_batch_no;
|
| 142 |
+
|
| 143 |
+
-- Check if sufficient stock available
|
| 144 |
+
IF v_available_qty IS NULL OR v_available_qty < p_qty THEN
|
| 145 |
+
RAISE EXCEPTION 'Insufficient available stock for SKU: %. Available: %, Required: %',
|
| 146 |
+
p_sku, COALESCE(v_available_qty, 0), p_qty;
|
| 147 |
+
END IF;
|
| 148 |
+
|
| 149 |
+
-- Update stock reservation
|
| 150 |
+
UPDATE trans.scm_stock
|
| 151 |
+
SET qty_reserved = qty_reserved + p_qty,
|
| 152 |
+
qty_available = qty_available - p_qty,
|
| 153 |
+
updated_at = NOW(),
|
| 154 |
+
last_updated_at = NOW()
|
| 155 |
+
WHERE merchant_id = p_merchant_id
|
| 156 |
+
AND location_id = p_location_id
|
| 157 |
+
AND sku = p_sku
|
| 158 |
+
AND batch_no = p_batch_no;
|
| 159 |
+
|
| 160 |
+
RETURN TRUE;
|
| 161 |
+
END;
|
| 162 |
+
$$;
|
| 163 |
+
|
| 164 |
+
-- Function to release stock reservation with proper schema
|
| 165 |
+
CREATE OR REPLACE FUNCTION release_stock_reservation(
|
| 166 |
+
p_merchant_id VARCHAR(64),
|
| 167 |
+
p_location_id VARCHAR(64),
|
| 168 |
+
p_sku VARCHAR(64),
|
| 169 |
+
p_batch_no VARCHAR(50),
|
| 170 |
+
p_qty NUMERIC(14,3),
|
| 171 |
+
p_ref_id UUID,
|
| 172 |
+
p_created_by VARCHAR(64)
|
| 173 |
+
) RETURNS BOOLEAN
|
| 174 |
+
LANGUAGE plpgsql
|
| 175 |
+
AS $$
|
| 176 |
+
BEGIN
|
| 177 |
+
-- Update stock reservation
|
| 178 |
+
UPDATE trans.scm_stock
|
| 179 |
+
SET qty_reserved = GREATEST(qty_reserved - p_qty, 0),
|
| 180 |
+
qty_available = qty_on_hand - GREATEST(qty_reserved - p_qty, 0),
|
| 181 |
+
updated_at = NOW(),
|
| 182 |
+
last_updated_at = NOW()
|
| 183 |
+
WHERE merchant_id = p_merchant_id
|
| 184 |
+
AND location_id = p_location_id
|
| 185 |
+
AND sku = p_sku
|
| 186 |
+
AND batch_no = p_batch_no;
|
| 187 |
+
|
| 188 |
+
RETURN TRUE;
|
| 189 |
+
END;
|
| 190 |
+
$$;
|
| 191 |
+
|
| 192 |
+
-- Function to get current stock summary with proper schema
|
| 193 |
+
CREATE OR REPLACE FUNCTION get_stock_summary(
|
| 194 |
+
p_merchant_id VARCHAR(64),
|
| 195 |
+
p_location_id VARCHAR(64) DEFAULT NULL,
|
| 196 |
+
p_sku VARCHAR(64) DEFAULT NULL
|
| 197 |
+
) RETURNS TABLE (
|
| 198 |
+
stock_id UUID,
|
| 199 |
+
location_id VARCHAR(64),
|
| 200 |
+
catalogue_id UUID,
|
| 201 |
+
sku VARCHAR(64),
|
| 202 |
+
batch_no VARCHAR(50),
|
| 203 |
+
qty_on_hand NUMERIC(14,3),
|
| 204 |
+
qty_reserved NUMERIC(14,3),
|
| 205 |
+
qty_available NUMERIC(14,3),
|
| 206 |
+
uom VARCHAR(10),
|
| 207 |
+
last_updated_at TIMESTAMP
|
| 208 |
+
)
|
| 209 |
+
LANGUAGE plpgsql
|
| 210 |
+
AS $$
|
| 211 |
+
BEGIN
|
| 212 |
+
RETURN QUERY
|
| 213 |
+
SELECT s.stock_id, s.location_id, s.catalogue_id, s.sku, s.batch_no,
|
| 214 |
+
s.qty_on_hand, s.qty_reserved, s.qty_available, s.uom, s.last_updated_at
|
| 215 |
+
FROM trans.scm_stock s
|
| 216 |
+
WHERE s.merchant_id = p_merchant_id
|
| 217 |
+
AND (p_location_id IS NULL OR s.location_id = p_location_id)
|
| 218 |
+
AND (p_sku IS NULL OR s.sku = p_sku)
|
| 219 |
+
ORDER BY s.sku, s.batch_no;
|
| 220 |
+
END;
|
| 221 |
+
$$;
|
| 222 |
+
|
| 223 |
+
-- Function to get stock ledger history with proper schema
|
| 224 |
+
CREATE OR REPLACE FUNCTION get_stock_ledger(
|
| 225 |
+
p_merchant_id VARCHAR(64),
|
| 226 |
+
p_sku VARCHAR(64) DEFAULT NULL,
|
| 227 |
+
p_batch_no VARCHAR(50) DEFAULT NULL,
|
| 228 |
+
p_limit INTEGER DEFAULT 100
|
| 229 |
+
) RETURNS TABLE (
|
| 230 |
+
ledger_id UUID,
|
| 231 |
+
location_id VARCHAR(64),
|
| 232 |
+
catalogue_id UUID,
|
| 233 |
+
sku VARCHAR(64),
|
| 234 |
+
batch_no VARCHAR(50),
|
| 235 |
+
txn_type VARCHAR(30),
|
| 236 |
+
qty NUMERIC(14,3),
|
| 237 |
+
uom VARCHAR(10),
|
| 238 |
+
ref_type VARCHAR(30),
|
| 239 |
+
ref_id UUID,
|
| 240 |
+
ref_no VARCHAR(50),
|
| 241 |
+
remarks TEXT,
|
| 242 |
+
created_by VARCHAR(64),
|
| 243 |
+
created_at TIMESTAMP
|
| 244 |
+
)
|
| 245 |
+
LANGUAGE plpgsql
|
| 246 |
+
AS $$
|
| 247 |
+
BEGIN
|
| 248 |
+
RETURN QUERY
|
| 249 |
+
SELECT l.ledger_id, l.location_id, l.catalogue_id, l.sku, l.batch_no,
|
| 250 |
+
l.txn_type, l.qty, l.uom, l.ref_type, l.ref_id, l.ref_no,
|
| 251 |
+
l.remarks, l.created_by, l.created_at
|
| 252 |
+
FROM trans.scm_stock_ledger l
|
| 253 |
+
WHERE l.merchant_id = p_merchant_id
|
| 254 |
+
AND (p_sku IS NULL OR l.sku = p_sku)
|
| 255 |
+
AND (p_batch_no IS NULL OR l.batch_no = p_batch_no)
|
| 256 |
+
ORDER BY l.created_at DESC
|
| 257 |
+
LIMIT p_limit;
|
| 258 |
+
END;
|
| 259 |
+
$$;
|