Spaces:
Running
Running
Commit ·
9798804
1
Parent(s): 5ea6287
warehouse_id changes
Browse files- app/catalogues/controllers/router.py +3 -3
- app/catalogues/schemas/schema.py +5 -5
- app/catalogues/services/service.py +6 -6
- app/inventory/adjustments/controllers/router.py +2 -2
- app/inventory/adjustments/schemas/schema.py +6 -6
- app/inventory/adjustments/services/service.py +22 -22
- app/inventory/stock/controllers/router.py +8 -8
- app/inventory/stock/models/model.py +5 -5
- app/inventory/stock/schemas/schema.py +7 -7
- app/inventory/stock/services/service.py +66 -66
- app/inventory/stock_take/controllers/router.py +7 -7
- app/inventory/stock_take/schemas/approval_schema.py +3 -3
- app/inventory/stock_take/schemas/schema.py +3 -3
- app/inventory/stock_take/services/approval_service.py +4 -4
- app/inventory/stock_take/services/service.py +13 -13
- app/purchases/orders/services/service.py +1 -1
- app/sql/apply_bulk_stock_movements.sql +1 -1
- app/sql/apply_stock_movement.sql +7 -7
- app/sql/fn_get_po_items_for_order_process.sql +1 -1
- app/sql/fn_get_po_items_for_purchase_return.sql +1 -1
- app/sql/get_stock_ledger.sql +2 -2
- app/sql/get_stock_summary.sql +4 -4
- app/sql/release_stock_reservation.sql +2 -2
- app/sql/reserve_stock.sql +3 -3
- app/trade_sales/services/service.py +1 -1
- app/transports/controllers/router.py +3 -3
- app/transports/models/model.py +2 -2
- app/transports/schemas/schema.py +5 -5
- app/transports/services/service.py +13 -13
- app/utils/stock_utils.py +10 -10
- docs/database/sql/stored_procedures/stock_management.sql +18 -18
- docs/database/sql/stored_procedures/stock_management_updated.sql +18 -18
- tests/test_merchant_catalogue_list.py +5 -5
app/catalogues/controllers/router.py
CHANGED
|
@@ -571,7 +571,7 @@ async def merchant_stock_batches_list(
|
|
| 571 |
# Auto-fill merchant_id from JWT
|
| 572 |
merchant_id = current_user.merchant_id
|
| 573 |
catalogue_id = payload.catalogue_id
|
| 574 |
-
|
| 575 |
sku = payload.sku
|
| 576 |
skip = payload.skip or 0
|
| 577 |
limit = payload.limit or 100
|
|
@@ -590,7 +590,7 @@ async def merchant_stock_batches_list(
|
|
| 590 |
result, count = await service.list_merchant_stock_batches(
|
| 591 |
merchant_id=merchant_id,
|
| 592 |
catalogue_id=catalogue_id,
|
| 593 |
-
|
| 594 |
sku=sku,
|
| 595 |
skip=skip,
|
| 596 |
limit=limit,
|
|
@@ -602,7 +602,7 @@ async def merchant_stock_batches_list(
|
|
| 602 |
extra={
|
| 603 |
"merchant_id": merchant_id,
|
| 604 |
"catalogue_id": catalogue_id,
|
| 605 |
-
"
|
| 606 |
"sku": sku,
|
| 607 |
"count": count,
|
| 608 |
"duration": time.time() - start_time
|
|
|
|
| 571 |
# Auto-fill merchant_id from JWT
|
| 572 |
merchant_id = current_user.merchant_id
|
| 573 |
catalogue_id = payload.catalogue_id
|
| 574 |
+
warehouse_id = payload.warehouse_id
|
| 575 |
sku = payload.sku
|
| 576 |
skip = payload.skip or 0
|
| 577 |
limit = payload.limit or 100
|
|
|
|
| 590 |
result, count = await service.list_merchant_stock_batches(
|
| 591 |
merchant_id=merchant_id,
|
| 592 |
catalogue_id=catalogue_id,
|
| 593 |
+
warehouse_id=warehouse_id,
|
| 594 |
sku=sku,
|
| 595 |
skip=skip,
|
| 596 |
limit=limit,
|
|
|
|
| 602 |
extra={
|
| 603 |
"merchant_id": merchant_id,
|
| 604 |
"catalogue_id": catalogue_id,
|
| 605 |
+
"warehouse_id": warehouse_id,
|
| 606 |
"sku": sku,
|
| 607 |
"count": count,
|
| 608 |
"duration": time.time() - start_time
|
app/catalogues/schemas/schema.py
CHANGED
|
@@ -1062,7 +1062,7 @@ class MerchantStockBatchesFilter(BaseModel):
|
|
| 1062 |
"""Merchant stock batches list request with PostgreSQL data from scm_stock"""
|
| 1063 |
merchant_id: Optional[str] = Field(None, description="Merchant ID (auto-filled from JWT)")
|
| 1064 |
catalogue_id: Optional[str] = Field(None, description="Filter by catalogue ID")
|
| 1065 |
-
|
| 1066 |
sku: Optional[str] = Field(None, description="Filter by SKU")
|
| 1067 |
skip: int = Field(0, ge=0, description="Number of records to skip")
|
| 1068 |
limit: int = Field(100, ge=1, le=1000, description="Maximum number of records to return")
|
|
@@ -1080,7 +1080,7 @@ class MerchantStockBatchesFilter(BaseModel):
|
|
| 1080 |
# Stock batch fields from scm_stock and catalogue_ref
|
| 1081 |
allowed_fields = {
|
| 1082 |
# Stock fields
|
| 1083 |
-
'stock_id', 'merchant_id', '
|
| 1084 |
'batch_no', 'expiry_date', 'qty_on_hand', 'qty_reserved', 'qty_available',
|
| 1085 |
'cost_price', 'uom', 'created_at', 'updated_at',
|
| 1086 |
# Catalogue fields
|
|
@@ -1099,7 +1099,7 @@ class MerchantStockBatchesFilter(BaseModel):
|
|
| 1099 |
"example": {
|
| 1100 |
"merchant_id": "mch_01HZQX5K3N2P8R6T4V9W",
|
| 1101 |
"catalogue_id": "cat_01HZQX5K3N2P8R6T4V9W",
|
| 1102 |
-
"
|
| 1103 |
"skip": 0,
|
| 1104 |
"limit": 50,
|
| 1105 |
"projection_list": [
|
|
@@ -1118,7 +1118,7 @@ class StockBatchResponse(BaseModel):
|
|
| 1118 |
"""Response schema for stock batch information"""
|
| 1119 |
stock_id: str
|
| 1120 |
merchant_id: str
|
| 1121 |
-
|
| 1122 |
catalogue_id: str
|
| 1123 |
sku: str
|
| 1124 |
batch_no: str
|
|
@@ -1143,7 +1143,7 @@ class StockBatchResponse(BaseModel):
|
|
| 1143 |
"example": {
|
| 1144 |
"stock_id": "stock_01HZQX5K3N2P8R6T4V9W",
|
| 1145 |
"merchant_id": "mch_01HZQX5K3N2P8R6T4V9W",
|
| 1146 |
-
"
|
| 1147 |
"catalogue_id": "cat_01HZQX5K3N2P8R6T4V9W",
|
| 1148 |
"sku": "SHMP-500-001",
|
| 1149 |
"batch_no": "BATCH-2024-001",
|
|
|
|
| 1062 |
"""Merchant stock batches list request with PostgreSQL data from scm_stock"""
|
| 1063 |
merchant_id: Optional[str] = Field(None, description="Merchant ID (auto-filled from JWT)")
|
| 1064 |
catalogue_id: Optional[str] = Field(None, description="Filter by catalogue ID")
|
| 1065 |
+
warehouse_id: Optional[str] = Field(None, description="Filter by warehouse/location ID")
|
| 1066 |
sku: Optional[str] = Field(None, description="Filter by SKU")
|
| 1067 |
skip: int = Field(0, ge=0, description="Number of records to skip")
|
| 1068 |
limit: int = Field(100, ge=1, le=1000, description="Maximum number of records to return")
|
|
|
|
| 1080 |
# Stock batch fields from scm_stock and catalogue_ref
|
| 1081 |
allowed_fields = {
|
| 1082 |
# Stock fields
|
| 1083 |
+
'stock_id', 'merchant_id', 'warehouse_id', 'catalogue_id', 'sku',
|
| 1084 |
'batch_no', 'expiry_date', 'qty_on_hand', 'qty_reserved', 'qty_available',
|
| 1085 |
'cost_price', 'uom', 'created_at', 'updated_at',
|
| 1086 |
# Catalogue fields
|
|
|
|
| 1099 |
"example": {
|
| 1100 |
"merchant_id": "mch_01HZQX5K3N2P8R6T4V9W",
|
| 1101 |
"catalogue_id": "cat_01HZQX5K3N2P8R6T4V9W",
|
| 1102 |
+
"warehouse_id": "loc_warehouse_001",
|
| 1103 |
"skip": 0,
|
| 1104 |
"limit": 50,
|
| 1105 |
"projection_list": [
|
|
|
|
| 1118 |
"""Response schema for stock batch information"""
|
| 1119 |
stock_id: str
|
| 1120 |
merchant_id: str
|
| 1121 |
+
warehouse_id: str
|
| 1122 |
catalogue_id: str
|
| 1123 |
sku: str
|
| 1124 |
batch_no: str
|
|
|
|
| 1143 |
"example": {
|
| 1144 |
"stock_id": "stock_01HZQX5K3N2P8R6T4V9W",
|
| 1145 |
"merchant_id": "mch_01HZQX5K3N2P8R6T4V9W",
|
| 1146 |
+
"warehouse_id": "loc_warehouse_001",
|
| 1147 |
"catalogue_id": "cat_01HZQX5K3N2P8R6T4V9W",
|
| 1148 |
"sku": "SHMP-500-001",
|
| 1149 |
"batch_no": "BATCH-2024-001",
|
app/catalogues/services/service.py
CHANGED
|
@@ -1004,7 +1004,7 @@ class CatalogueService:
|
|
| 1004 |
self,
|
| 1005 |
merchant_id: str,
|
| 1006 |
catalogue_id: str = None,
|
| 1007 |
-
|
| 1008 |
sku: str = None,
|
| 1009 |
skip: int = 0,
|
| 1010 |
limit: int = 100,
|
|
@@ -1043,7 +1043,7 @@ class CatalogueService:
|
|
| 1043 |
SELECT
|
| 1044 |
s.stock_id,
|
| 1045 |
s.merchant_id,
|
| 1046 |
-
s.
|
| 1047 |
s.catalogue_id,
|
| 1048 |
s.sku,
|
| 1049 |
s.batch_no,
|
|
@@ -1076,9 +1076,9 @@ class CatalogueService:
|
|
| 1076 |
where_conditions.append("s.catalogue_id = :catalogue_id")
|
| 1077 |
params["catalogue_id"] = catalogue_id
|
| 1078 |
|
| 1079 |
-
if
|
| 1080 |
-
where_conditions.append("s.
|
| 1081 |
-
params["
|
| 1082 |
|
| 1083 |
if sku:
|
| 1084 |
where_conditions.append("s.sku = :sku")
|
|
@@ -1108,7 +1108,7 @@ class CatalogueService:
|
|
| 1108 |
batch_dict = {
|
| 1109 |
"stock_id": str(row.stock_id),
|
| 1110 |
"merchant_id": row.merchant_id,
|
| 1111 |
-
"
|
| 1112 |
"catalogue_id": str(row.catalogue_id),
|
| 1113 |
"sku": row.sku,
|
| 1114 |
"batch_no": row.batch_no,
|
|
|
|
| 1004 |
self,
|
| 1005 |
merchant_id: str,
|
| 1006 |
catalogue_id: str = None,
|
| 1007 |
+
warehouse_id: str = None,
|
| 1008 |
sku: str = None,
|
| 1009 |
skip: int = 0,
|
| 1010 |
limit: int = 100,
|
|
|
|
| 1043 |
SELECT
|
| 1044 |
s.stock_id,
|
| 1045 |
s.merchant_id,
|
| 1046 |
+
s.warehouse_id,
|
| 1047 |
s.catalogue_id,
|
| 1048 |
s.sku,
|
| 1049 |
s.batch_no,
|
|
|
|
| 1076 |
where_conditions.append("s.catalogue_id = :catalogue_id")
|
| 1077 |
params["catalogue_id"] = catalogue_id
|
| 1078 |
|
| 1079 |
+
if warehouse_id:
|
| 1080 |
+
where_conditions.append("s.warehouse_id = :warehouse_id")
|
| 1081 |
+
params["warehouse_id"] = warehouse_id
|
| 1082 |
|
| 1083 |
if sku:
|
| 1084 |
where_conditions.append("s.sku = :sku")
|
|
|
|
| 1108 |
batch_dict = {
|
| 1109 |
"stock_id": str(row.stock_id),
|
| 1110 |
"merchant_id": row.merchant_id,
|
| 1111 |
+
"warehouse_id": row.warehouse_id,
|
| 1112 |
"catalogue_id": str(row.catalogue_id),
|
| 1113 |
"sku": row.sku,
|
| 1114 |
"batch_no": row.batch_no,
|
app/inventory/adjustments/controllers/router.py
CHANGED
|
@@ -187,7 +187,7 @@ async def list_stock_adjustments(
|
|
| 187 |
**Request Body:**
|
| 188 |
- **filters**: Filter criteria (optional)
|
| 189 |
- merchant_id: Filter by merchant
|
| 190 |
-
-
|
| 191 |
- status: Filter by status (pending, approved, rejected)
|
| 192 |
- adjustment_number: Search by adjustment number (partial match)
|
| 193 |
- **skip**: Number of records to skip (pagination)
|
|
@@ -196,7 +196,7 @@ async def list_stock_adjustments(
|
|
| 196 |
|
| 197 |
**Projection Examples:**
|
| 198 |
- Basic info: ["adjustment_master_id", "adjustment_number", "status", "total_items"]
|
| 199 |
-
- Summary: ["adjustment_number", "
|
| 200 |
- Full details: null (returns all fields)
|
| 201 |
|
| 202 |
**Filter Examples:**
|
|
|
|
| 187 |
**Request Body:**
|
| 188 |
- **filters**: Filter criteria (optional)
|
| 189 |
- merchant_id: Filter by merchant
|
| 190 |
+
- warehouse_id: Filter by location
|
| 191 |
- status: Filter by status (pending, approved, rejected)
|
| 192 |
- adjustment_number: Search by adjustment number (partial match)
|
| 193 |
- **skip**: Number of records to skip (pagination)
|
|
|
|
| 196 |
|
| 197 |
**Projection Examples:**
|
| 198 |
- Basic info: ["adjustment_master_id", "adjustment_number", "status", "total_items"]
|
| 199 |
+
- Summary: ["adjustment_number", "warehouse_id", "total_items", "total_value", "status"]
|
| 200 |
- Full details: null (returns all fields)
|
| 201 |
|
| 202 |
**Filter Examples:**
|
app/inventory/adjustments/schemas/schema.py
CHANGED
|
@@ -42,7 +42,7 @@ class CreateStockAdjustmentRequest(BaseModel):
|
|
| 42 |
class CreateStockAdjustmentRequestSingle(BaseModel):
|
| 43 |
"""Request schema for creating single stock adjustment (legacy support)"""
|
| 44 |
merchant_id: str = Field(..., description="Merchant ID")
|
| 45 |
-
|
| 46 |
sku: str = Field(..., description="SKU code")
|
| 47 |
batch_no: Optional[str] = Field(None, description="Batch number")
|
| 48 |
adj_type: AdjustmentType = Field(..., description="Adjustment type")
|
|
@@ -85,7 +85,7 @@ class StockAdjustmentResponse(BaseModel):
|
|
| 85 |
"""Response schema for stock adjustment (single line item)"""
|
| 86 |
adjustment_id: str
|
| 87 |
merchant_id: str
|
| 88 |
-
|
| 89 |
sku: str
|
| 90 |
batch_no: Optional[str]
|
| 91 |
adj_type: AdjustmentType
|
|
@@ -122,7 +122,7 @@ class StockAdjustmentMasterResponse(BaseModel):
|
|
| 122 |
adjustment_master_id: str
|
| 123 |
adjustment_number: str
|
| 124 |
merchant_id: str
|
| 125 |
-
|
| 126 |
adjustment_date: datetime
|
| 127 |
description: Optional[str]
|
| 128 |
additional_notes: Optional[str]
|
|
@@ -147,7 +147,7 @@ class StockTakeModel(BaseModel):
|
|
| 147 |
"""Stock take model"""
|
| 148 |
stock_take_id: str = Field(..., description="Unique stock take identifier")
|
| 149 |
merchant_id: str = Field(..., description="Merchant ID")
|
| 150 |
-
|
| 151 |
sku: str = Field(..., description="SKU code")
|
| 152 |
batch_no: Optional[str] = Field(None, description="Batch number")
|
| 153 |
system_qty: int = Field(..., ge=0, description="System quantity")
|
|
@@ -177,7 +177,7 @@ class CreateStockTakeRequest(BaseModel):
|
|
| 177 |
class CreateStockTakeRequestSingle(BaseModel):
|
| 178 |
"""Request schema for creating single stock take (legacy support)"""
|
| 179 |
merchant_id: str = Field(..., description="Merchant ID")
|
| 180 |
-
|
| 181 |
sku: str = Field(..., description="SKU code")
|
| 182 |
batch_no: Optional[str] = Field(None, description="Batch number")
|
| 183 |
system_qty: int = Field(..., ge=0, description="System quantity")
|
|
@@ -194,7 +194,7 @@ class StockTakeResponse(BaseModel):
|
|
| 194 |
"""Response schema for stock take"""
|
| 195 |
stock_take_id: str
|
| 196 |
merchant_id: str
|
| 197 |
-
|
| 198 |
sku: str
|
| 199 |
batch_no: Optional[str]
|
| 200 |
system_qty: int
|
|
|
|
| 42 |
class CreateStockAdjustmentRequestSingle(BaseModel):
|
| 43 |
"""Request schema for creating single stock adjustment (legacy support)"""
|
| 44 |
merchant_id: str = Field(..., description="Merchant ID")
|
| 45 |
+
warehouse_id: str = Field(..., description="Location ID")
|
| 46 |
sku: str = Field(..., description="SKU code")
|
| 47 |
batch_no: Optional[str] = Field(None, description="Batch number")
|
| 48 |
adj_type: AdjustmentType = Field(..., description="Adjustment type")
|
|
|
|
| 85 |
"""Response schema for stock adjustment (single line item)"""
|
| 86 |
adjustment_id: str
|
| 87 |
merchant_id: str
|
| 88 |
+
warehouse_id: str
|
| 89 |
sku: str
|
| 90 |
batch_no: Optional[str]
|
| 91 |
adj_type: AdjustmentType
|
|
|
|
| 122 |
adjustment_master_id: str
|
| 123 |
adjustment_number: str
|
| 124 |
merchant_id: str
|
| 125 |
+
warehouse_id: str
|
| 126 |
adjustment_date: datetime
|
| 127 |
description: Optional[str]
|
| 128 |
additional_notes: Optional[str]
|
|
|
|
| 147 |
"""Stock take model"""
|
| 148 |
stock_take_id: str = Field(..., description="Unique stock take identifier")
|
| 149 |
merchant_id: str = Field(..., description="Merchant ID")
|
| 150 |
+
warehouse_id: str = Field(..., description="Location ID")
|
| 151 |
sku: str = Field(..., description="SKU code")
|
| 152 |
batch_no: Optional[str] = Field(None, description="Batch number")
|
| 153 |
system_qty: int = Field(..., ge=0, description="System quantity")
|
|
|
|
| 177 |
class CreateStockTakeRequestSingle(BaseModel):
|
| 178 |
"""Request schema for creating single stock take (legacy support)"""
|
| 179 |
merchant_id: str = Field(..., description="Merchant ID")
|
| 180 |
+
warehouse_id: str = Field(..., description="Location ID")
|
| 181 |
sku: str = Field(..., description="SKU code")
|
| 182 |
batch_no: Optional[str] = Field(None, description="Batch number")
|
| 183 |
system_qty: int = Field(..., ge=0, description="System quantity")
|
|
|
|
| 194 |
"""Response schema for stock take"""
|
| 195 |
stock_take_id: str
|
| 196 |
merchant_id: str
|
| 197 |
+
warehouse_id: str
|
| 198 |
sku: str
|
| 199 |
batch_no: Optional[str]
|
| 200 |
system_qty: int
|
app/inventory/adjustments/services/service.py
CHANGED
|
@@ -42,7 +42,7 @@ class StockAdjustmentService:
|
|
| 42 |
@staticmethod
|
| 43 |
async def _validate_stock_availability(
|
| 44 |
merchant_id: str,
|
| 45 |
-
|
| 46 |
sku: str,
|
| 47 |
batch_no: Optional[str],
|
| 48 |
qty: Decimal
|
|
@@ -53,7 +53,7 @@ class StockAdjustmentService:
|
|
| 53 |
query = select(ScmStock).where(
|
| 54 |
and_(
|
| 55 |
ScmStock.merchant_id == merchant_id,
|
| 56 |
-
ScmStock.
|
| 57 |
ScmStock.sku == sku
|
| 58 |
)
|
| 59 |
)
|
|
@@ -93,7 +93,7 @@ class StockAdjustmentService:
|
|
| 93 |
async def _create_ledger_entry(
|
| 94 |
session,
|
| 95 |
merchant_id: str,
|
| 96 |
-
|
| 97 |
sku: str,
|
| 98 |
batch_no: Optional[str],
|
| 99 |
txn_type: TransactionType,
|
|
@@ -106,7 +106,7 @@ class StockAdjustmentService:
|
|
| 106 |
try:
|
| 107 |
ledger_entry = ScmStockLedger(
|
| 108 |
merchant_id=merchant_id,
|
| 109 |
-
|
| 110 |
sku=sku,
|
| 111 |
batch_no=batch_no,
|
| 112 |
txn_type=txn_type.value,
|
|
@@ -133,7 +133,7 @@ class StockAdjustmentService:
|
|
| 133 |
async def _update_stock_snapshot(
|
| 134 |
session,
|
| 135 |
merchant_id: str,
|
| 136 |
-
|
| 137 |
sku: str,
|
| 138 |
batch_no: Optional[str],
|
| 139 |
qty_change: Decimal
|
|
@@ -143,7 +143,7 @@ class StockAdjustmentService:
|
|
| 143 |
query = select(ScmStock).where(
|
| 144 |
and_(
|
| 145 |
ScmStock.merchant_id == merchant_id,
|
| 146 |
-
ScmStock.
|
| 147 |
ScmStock.sku == sku
|
| 148 |
)
|
| 149 |
)
|
|
@@ -163,7 +163,7 @@ class StockAdjustmentService:
|
|
| 163 |
else:
|
| 164 |
stock = ScmStock(
|
| 165 |
merchant_id=merchant_id,
|
| 166 |
-
|
| 167 |
sku=sku,
|
| 168 |
batch_no=batch_no,
|
| 169 |
qty_on_hand=max(Decimal('0'), qty_change),
|
|
@@ -205,7 +205,7 @@ class StockAdjustmentService:
|
|
| 205 |
master = ScmStockAdjustmentMaster(
|
| 206 |
adjustment_number=adjustment_number,
|
| 207 |
merchant_id=effective_merchant_id,
|
| 208 |
-
|
| 209 |
adjustment_date=payload.adjustment_date or datetime.utcnow(),
|
| 210 |
description=f"Stock adjustment session - {len(payload.entries)} items",
|
| 211 |
additional_notes=payload.additional_notes or "Created via API",
|
|
@@ -261,7 +261,7 @@ class StockAdjustmentService:
|
|
| 261 |
stock_query = select(ScmStock).where(
|
| 262 |
and_(
|
| 263 |
ScmStock.merchant_id == effective_merchant_id,
|
| 264 |
-
ScmStock.
|
| 265 |
ScmStock.sku == entry.sku
|
| 266 |
)
|
| 267 |
)
|
|
@@ -299,7 +299,7 @@ class StockAdjustmentService:
|
|
| 299 |
results.append(StockAdjustmentResponse(
|
| 300 |
adjustment_id=str(detail.adjustment_detail_id),
|
| 301 |
merchant_id=master.merchant_id,
|
| 302 |
-
|
| 303 |
sku=detail.sku,
|
| 304 |
batch_no=detail.batch_no,
|
| 305 |
adj_type=AdjustmentType(detail.adj_type),
|
|
@@ -358,7 +358,7 @@ class StockAdjustmentService:
|
|
| 358 |
if payload.adj_type in [AdjustmentType.DAMAGE, AdjustmentType.EXPIRED, AdjustmentType.SHRINKAGE]:
|
| 359 |
await StockAdjustmentService._validate_stock_availability(
|
| 360 |
payload.merchant_id,
|
| 361 |
-
payload.
|
| 362 |
payload.sku,
|
| 363 |
payload.batch_no,
|
| 364 |
qty_decimal
|
|
@@ -377,7 +377,7 @@ class StockAdjustmentService:
|
|
| 377 |
master = ScmStockAdjustmentMaster(
|
| 378 |
adjustment_number=adjustment_number,
|
| 379 |
merchant_id=payload.merchant_id,
|
| 380 |
-
|
| 381 |
adjustment_date=datetime.utcnow(),
|
| 382 |
description=f"Single adjustment - {payload.sku}",
|
| 383 |
status="pending",
|
|
@@ -393,7 +393,7 @@ class StockAdjustmentService:
|
|
| 393 |
stock_query = select(ScmStock).where(
|
| 394 |
and_(
|
| 395 |
ScmStock.merchant_id == payload.merchant_id,
|
| 396 |
-
ScmStock.
|
| 397 |
ScmStock.sku == payload.sku
|
| 398 |
)
|
| 399 |
)
|
|
@@ -447,7 +447,7 @@ class StockAdjustmentService:
|
|
| 447 |
return StockAdjustmentResponse(
|
| 448 |
adjustment_id=str(detail.adjustment_detail_id),
|
| 449 |
merchant_id=master.merchant_id,
|
| 450 |
-
|
| 451 |
sku=detail.sku,
|
| 452 |
batch_no=detail.batch_no,
|
| 453 |
adj_type=AdjustmentType(detail.adj_type),
|
|
@@ -492,7 +492,7 @@ class StockAdjustmentService:
|
|
| 492 |
stock_query = select(ScmStock).where(
|
| 493 |
and_(
|
| 494 |
ScmStock.merchant_id == master.merchant_id,
|
| 495 |
-
ScmStock.
|
| 496 |
ScmStock.sku == detail.sku,
|
| 497 |
ScmStock.batch_no == detail.batch_no
|
| 498 |
)
|
|
@@ -512,7 +512,7 @@ class StockAdjustmentService:
|
|
| 512 |
# Create stock transaction
|
| 513 |
stock_transaction = StockTransaction(
|
| 514 |
merchant_id=master.merchant_id,
|
| 515 |
-
|
| 516 |
catalogue_id=catalogue_id,
|
| 517 |
sku=detail.sku,
|
| 518 |
batch_no=detail.batch_no,
|
|
@@ -625,7 +625,7 @@ class StockAdjustmentService:
|
|
| 625 |
return StockAdjustmentResponse(
|
| 626 |
adjustment_id=str(detail.adjustment_detail_id),
|
| 627 |
merchant_id=master.merchant_id,
|
| 628 |
-
|
| 629 |
sku=detail.sku,
|
| 630 |
batch_no=detail.batch_no,
|
| 631 |
adj_type=AdjustmentType(detail.adj_type),
|
|
@@ -738,7 +738,7 @@ class StockAdjustmentService:
|
|
| 738 |
return StockAdjustmentResponse(
|
| 739 |
adjustment_id=str(detail.adjustment_detail_id),
|
| 740 |
merchant_id=master.merchant_id,
|
| 741 |
-
|
| 742 |
sku=detail.sku,
|
| 743 |
batch_no=detail.batch_no,
|
| 744 |
adj_type=AdjustmentType(detail.adj_type),
|
|
@@ -851,7 +851,7 @@ class StockAdjustmentService:
|
|
| 851 |
return StockAdjustmentResponse(
|
| 852 |
adjustment_id=str(detail.adjustment_detail_id),
|
| 853 |
merchant_id=master.merchant_id,
|
| 854 |
-
|
| 855 |
sku=detail.sku,
|
| 856 |
batch_no=detail.batch_no,
|
| 857 |
adj_type=AdjustmentType(detail.adj_type),
|
|
@@ -897,8 +897,8 @@ class StockAdjustmentService:
|
|
| 897 |
elif filters.get("merchant_id"):
|
| 898 |
conditions.append(ScmStockAdjustmentMaster.merchant_id == filters["merchant_id"])
|
| 899 |
|
| 900 |
-
if filters.get("
|
| 901 |
-
conditions.append(ScmStockAdjustmentMaster.
|
| 902 |
if filters.get("status"):
|
| 903 |
conditions.append(ScmStockAdjustmentMaster.status == filters["status"])
|
| 904 |
if filters.get("adjustment_number"):
|
|
@@ -941,7 +941,7 @@ class StockAdjustmentService:
|
|
| 941 |
adjustment_master_id=str(master.adjustment_master_id),
|
| 942 |
adjustment_number=master.adjustment_number,
|
| 943 |
merchant_id=master.merchant_id,
|
| 944 |
-
|
| 945 |
adjustment_date=master.adjustment_date,
|
| 946 |
description=master.description,
|
| 947 |
additional_notes=master.additional_notes,
|
|
|
|
| 42 |
@staticmethod
|
| 43 |
async def _validate_stock_availability(
|
| 44 |
merchant_id: str,
|
| 45 |
+
warehouse_id: str,
|
| 46 |
sku: str,
|
| 47 |
batch_no: Optional[str],
|
| 48 |
qty: Decimal
|
|
|
|
| 53 |
query = select(ScmStock).where(
|
| 54 |
and_(
|
| 55 |
ScmStock.merchant_id == merchant_id,
|
| 56 |
+
ScmStock.warehouse_id == warehouse_id,
|
| 57 |
ScmStock.sku == sku
|
| 58 |
)
|
| 59 |
)
|
|
|
|
| 93 |
async def _create_ledger_entry(
|
| 94 |
session,
|
| 95 |
merchant_id: str,
|
| 96 |
+
warehouse_id: str,
|
| 97 |
sku: str,
|
| 98 |
batch_no: Optional[str],
|
| 99 |
txn_type: TransactionType,
|
|
|
|
| 106 |
try:
|
| 107 |
ledger_entry = ScmStockLedger(
|
| 108 |
merchant_id=merchant_id,
|
| 109 |
+
warehouse_id=warehouse_id,
|
| 110 |
sku=sku,
|
| 111 |
batch_no=batch_no,
|
| 112 |
txn_type=txn_type.value,
|
|
|
|
| 133 |
async def _update_stock_snapshot(
|
| 134 |
session,
|
| 135 |
merchant_id: str,
|
| 136 |
+
warehouse_id: str,
|
| 137 |
sku: str,
|
| 138 |
batch_no: Optional[str],
|
| 139 |
qty_change: Decimal
|
|
|
|
| 143 |
query = select(ScmStock).where(
|
| 144 |
and_(
|
| 145 |
ScmStock.merchant_id == merchant_id,
|
| 146 |
+
ScmStock.warehouse_id == warehouse_id,
|
| 147 |
ScmStock.sku == sku
|
| 148 |
)
|
| 149 |
)
|
|
|
|
| 163 |
else:
|
| 164 |
stock = ScmStock(
|
| 165 |
merchant_id=merchant_id,
|
| 166 |
+
warehouse_id=warehouse_id,
|
| 167 |
sku=sku,
|
| 168 |
batch_no=batch_no,
|
| 169 |
qty_on_hand=max(Decimal('0'), qty_change),
|
|
|
|
| 205 |
master = ScmStockAdjustmentMaster(
|
| 206 |
adjustment_number=adjustment_number,
|
| 207 |
merchant_id=effective_merchant_id,
|
| 208 |
+
warehouse_id=payload.warehouse_id,
|
| 209 |
adjustment_date=payload.adjustment_date or datetime.utcnow(),
|
| 210 |
description=f"Stock adjustment session - {len(payload.entries)} items",
|
| 211 |
additional_notes=payload.additional_notes or "Created via API",
|
|
|
|
| 261 |
stock_query = select(ScmStock).where(
|
| 262 |
and_(
|
| 263 |
ScmStock.merchant_id == effective_merchant_id,
|
| 264 |
+
ScmStock.warehouse_id == payload.warehouse_id,
|
| 265 |
ScmStock.sku == entry.sku
|
| 266 |
)
|
| 267 |
)
|
|
|
|
| 299 |
results.append(StockAdjustmentResponse(
|
| 300 |
adjustment_id=str(detail.adjustment_detail_id),
|
| 301 |
merchant_id=master.merchant_id,
|
| 302 |
+
warehouse_id=master.warehouse_id,
|
| 303 |
sku=detail.sku,
|
| 304 |
batch_no=detail.batch_no,
|
| 305 |
adj_type=AdjustmentType(detail.adj_type),
|
|
|
|
| 358 |
if payload.adj_type in [AdjustmentType.DAMAGE, AdjustmentType.EXPIRED, AdjustmentType.SHRINKAGE]:
|
| 359 |
await StockAdjustmentService._validate_stock_availability(
|
| 360 |
payload.merchant_id,
|
| 361 |
+
payload.warehouse_id,
|
| 362 |
payload.sku,
|
| 363 |
payload.batch_no,
|
| 364 |
qty_decimal
|
|
|
|
| 377 |
master = ScmStockAdjustmentMaster(
|
| 378 |
adjustment_number=adjustment_number,
|
| 379 |
merchant_id=payload.merchant_id,
|
| 380 |
+
warehouse_id=payload.warehouse_id,
|
| 381 |
adjustment_date=datetime.utcnow(),
|
| 382 |
description=f"Single adjustment - {payload.sku}",
|
| 383 |
status="pending",
|
|
|
|
| 393 |
stock_query = select(ScmStock).where(
|
| 394 |
and_(
|
| 395 |
ScmStock.merchant_id == payload.merchant_id,
|
| 396 |
+
ScmStock.warehouse_id == payload.warehouse_id,
|
| 397 |
ScmStock.sku == payload.sku
|
| 398 |
)
|
| 399 |
)
|
|
|
|
| 447 |
return StockAdjustmentResponse(
|
| 448 |
adjustment_id=str(detail.adjustment_detail_id),
|
| 449 |
merchant_id=master.merchant_id,
|
| 450 |
+
warehouse_id=master.warehouse_id,
|
| 451 |
sku=detail.sku,
|
| 452 |
batch_no=detail.batch_no,
|
| 453 |
adj_type=AdjustmentType(detail.adj_type),
|
|
|
|
| 492 |
stock_query = select(ScmStock).where(
|
| 493 |
and_(
|
| 494 |
ScmStock.merchant_id == master.merchant_id,
|
| 495 |
+
ScmStock.warehouse_id == master.warehouse_id,
|
| 496 |
ScmStock.sku == detail.sku,
|
| 497 |
ScmStock.batch_no == detail.batch_no
|
| 498 |
)
|
|
|
|
| 512 |
# Create stock transaction
|
| 513 |
stock_transaction = StockTransaction(
|
| 514 |
merchant_id=master.merchant_id,
|
| 515 |
+
warehouse_id=master.warehouse_id,
|
| 516 |
catalogue_id=catalogue_id,
|
| 517 |
sku=detail.sku,
|
| 518 |
batch_no=detail.batch_no,
|
|
|
|
| 625 |
return StockAdjustmentResponse(
|
| 626 |
adjustment_id=str(detail.adjustment_detail_id),
|
| 627 |
merchant_id=master.merchant_id,
|
| 628 |
+
warehouse_id=master.warehouse_id,
|
| 629 |
sku=detail.sku,
|
| 630 |
batch_no=detail.batch_no,
|
| 631 |
adj_type=AdjustmentType(detail.adj_type),
|
|
|
|
| 738 |
return StockAdjustmentResponse(
|
| 739 |
adjustment_id=str(detail.adjustment_detail_id),
|
| 740 |
merchant_id=master.merchant_id,
|
| 741 |
+
warehouse_id=master.warehouse_id,
|
| 742 |
sku=detail.sku,
|
| 743 |
batch_no=detail.batch_no,
|
| 744 |
adj_type=AdjustmentType(detail.adj_type),
|
|
|
|
| 851 |
return StockAdjustmentResponse(
|
| 852 |
adjustment_id=str(detail.adjustment_detail_id),
|
| 853 |
merchant_id=master.merchant_id,
|
| 854 |
+
warehouse_id=master.warehouse_id,
|
| 855 |
sku=detail.sku,
|
| 856 |
batch_no=detail.batch_no,
|
| 857 |
adj_type=AdjustmentType(detail.adj_type),
|
|
|
|
| 897 |
elif filters.get("merchant_id"):
|
| 898 |
conditions.append(ScmStockAdjustmentMaster.merchant_id == filters["merchant_id"])
|
| 899 |
|
| 900 |
+
if filters.get("warehouse_id"):
|
| 901 |
+
conditions.append(ScmStockAdjustmentMaster.warehouse_id == filters["warehouse_id"])
|
| 902 |
if filters.get("status"):
|
| 903 |
conditions.append(ScmStockAdjustmentMaster.status == filters["status"])
|
| 904 |
if filters.get("adjustment_number"):
|
|
|
|
| 941 |
adjustment_master_id=str(master.adjustment_master_id),
|
| 942 |
adjustment_number=master.adjustment_number,
|
| 943 |
merchant_id=master.merchant_id,
|
| 944 |
+
warehouse_id=master.warehouse_id,
|
| 945 |
adjustment_date=master.adjustment_date,
|
| 946 |
description=master.description,
|
| 947 |
additional_notes=master.additional_notes,
|
app/inventory/stock/controllers/router.py
CHANGED
|
@@ -163,7 +163,7 @@ async def get_stock_summary(
|
|
| 163 |
service = StockService(pg_session)
|
| 164 |
result = await service.get_stock_summary_with_projection(
|
| 165 |
merchant_id=current_user.merchant_id,
|
| 166 |
-
|
| 167 |
sku=payload.sku,
|
| 168 |
catalogue_id=payload.catalogue_id,
|
| 169 |
projection_list=payload.projection_list
|
|
@@ -213,7 +213,7 @@ async def get_stock_valuation(
|
|
| 213 |
service = StockService(pg_session)
|
| 214 |
result = await service.get_stock_valuation(
|
| 215 |
merchant_id=current_user.merchant_id,
|
| 216 |
-
|
| 217 |
as_of_date=payload.as_of_date,
|
| 218 |
projection_list=payload.projection_list
|
| 219 |
)
|
|
@@ -238,7 +238,7 @@ async def reserve_stock(
|
|
| 238 |
service = StockService(pg_session)
|
| 239 |
success = await service.reserve_stock(
|
| 240 |
merchant_id=current_user.merchant_id,
|
| 241 |
-
|
| 242 |
sku=payload.sku,
|
| 243 |
batch_no=payload.batch_no,
|
| 244 |
qty=payload.qty,
|
|
@@ -269,7 +269,7 @@ async def release_stock_reservation(
|
|
| 269 |
service = StockService(pg_session)
|
| 270 |
success = await service.release_stock_reservation(
|
| 271 |
merchant_id=current_user.merchant_id,
|
| 272 |
-
|
| 273 |
sku=payload.sku,
|
| 274 |
batch_no=payload.batch_no,
|
| 275 |
qty=payload.qty,
|
|
@@ -305,7 +305,7 @@ async def adjust_stock(
|
|
| 305 |
|
| 306 |
transaction_id, success = await service.process_stock_adjustment(
|
| 307 |
merchant_id=current_user.merchant_id,
|
| 308 |
-
|
| 309 |
sku=payload.sku,
|
| 310 |
batch_no=payload.batch_no,
|
| 311 |
adjustment_qty=payload.adjustment_qty,
|
|
@@ -351,7 +351,7 @@ async def bulk_adjust_stock(
|
|
| 351 |
ref_id = f"{bulk_ref_id}_{adjustment.sku}_{adjustment.batch_no}"
|
| 352 |
transaction_id, success = await service.process_stock_adjustment(
|
| 353 |
merchant_id=current_user.merchant_id,
|
| 354 |
-
|
| 355 |
sku=adjustment.sku,
|
| 356 |
batch_no=adjustment.batch_no,
|
| 357 |
adjustment_qty=adjustment.adjustment_qty,
|
|
@@ -412,8 +412,8 @@ async def transfer_stock(
|
|
| 412 |
|
| 413 |
success = await service.process_stock_transfer(
|
| 414 |
merchant_id=current_user.merchant_id,
|
| 415 |
-
|
| 416 |
-
|
| 417 |
sku=payload.sku,
|
| 418 |
batch_no=payload.batch_no,
|
| 419 |
qty=payload.qty,
|
|
|
|
| 163 |
service = StockService(pg_session)
|
| 164 |
result = await service.get_stock_summary_with_projection(
|
| 165 |
merchant_id=current_user.merchant_id,
|
| 166 |
+
warehouse_id=payload.warehouse_id,
|
| 167 |
sku=payload.sku,
|
| 168 |
catalogue_id=payload.catalogue_id,
|
| 169 |
projection_list=payload.projection_list
|
|
|
|
| 213 |
service = StockService(pg_session)
|
| 214 |
result = await service.get_stock_valuation(
|
| 215 |
merchant_id=current_user.merchant_id,
|
| 216 |
+
warehouse_id=payload.warehouse_id,
|
| 217 |
as_of_date=payload.as_of_date,
|
| 218 |
projection_list=payload.projection_list
|
| 219 |
)
|
|
|
|
| 238 |
service = StockService(pg_session)
|
| 239 |
success = await service.reserve_stock(
|
| 240 |
merchant_id=current_user.merchant_id,
|
| 241 |
+
warehouse_id=payload.warehouse_id,
|
| 242 |
sku=payload.sku,
|
| 243 |
batch_no=payload.batch_no,
|
| 244 |
qty=payload.qty,
|
|
|
|
| 269 |
service = StockService(pg_session)
|
| 270 |
success = await service.release_stock_reservation(
|
| 271 |
merchant_id=current_user.merchant_id,
|
| 272 |
+
warehouse_id=payload.warehouse_id,
|
| 273 |
sku=payload.sku,
|
| 274 |
batch_no=payload.batch_no,
|
| 275 |
qty=payload.qty,
|
|
|
|
| 305 |
|
| 306 |
transaction_id, success = await service.process_stock_adjustment(
|
| 307 |
merchant_id=current_user.merchant_id,
|
| 308 |
+
warehouse_id=payload.warehouse_id,
|
| 309 |
sku=payload.sku,
|
| 310 |
batch_no=payload.batch_no,
|
| 311 |
adjustment_qty=payload.adjustment_qty,
|
|
|
|
| 351 |
ref_id = f"{bulk_ref_id}_{adjustment.sku}_{adjustment.batch_no}"
|
| 352 |
transaction_id, success = await service.process_stock_adjustment(
|
| 353 |
merchant_id=current_user.merchant_id,
|
| 354 |
+
warehouse_id=adjustment.warehouse_id,
|
| 355 |
sku=adjustment.sku,
|
| 356 |
batch_no=adjustment.batch_no,
|
| 357 |
adjustment_qty=adjustment.adjustment_qty,
|
|
|
|
| 412 |
|
| 413 |
success = await service.process_stock_transfer(
|
| 414 |
merchant_id=current_user.merchant_id,
|
| 415 |
+
from_warehouse_id=payload.from_warehouse_id,
|
| 416 |
+
to_warehouse_id=payload.to_warehouse_id,
|
| 417 |
sku=payload.sku,
|
| 418 |
batch_no=payload.batch_no,
|
| 419 |
qty=payload.qty,
|
app/inventory/stock/models/model.py
CHANGED
|
@@ -22,7 +22,7 @@ class ScmStock(Base):
|
|
| 22 |
|
| 23 |
stock_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 24 |
merchant_id = Column(String(64), nullable=False)
|
| 25 |
-
|
| 26 |
sku = Column(String(64), nullable=False)
|
| 27 |
batch_no = Column(String(50), nullable=True)
|
| 28 |
|
|
@@ -53,7 +53,7 @@ class ScmStockLedger(Base):
|
|
| 53 |
|
| 54 |
ledger_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 55 |
merchant_id = Column(String(64), nullable=False)
|
| 56 |
-
|
| 57 |
sku = Column(String(64), nullable=False)
|
| 58 |
batch_no = Column(String(50))
|
| 59 |
|
|
@@ -83,7 +83,7 @@ class ScmStockAdjustmentMaster(Base):
|
|
| 83 |
adjustment_master_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 84 |
adjustment_number = Column(String(50), nullable=False) # Human readable: ADJ-2024-001
|
| 85 |
merchant_id = Column(String(64), nullable=False)
|
| 86 |
-
|
| 87 |
|
| 88 |
# Document level info
|
| 89 |
adjustment_date = Column(TIMESTAMP(timezone=True), nullable=False, default=datetime.utcnow)
|
|
@@ -174,7 +174,7 @@ class ScmStockTakeMaster(Base):
|
|
| 174 |
stock_take_master_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 175 |
stock_take_number = Column(String(50), nullable=False) # Human readable: ST-2024-001
|
| 176 |
merchant_id = Column(String(64), nullable=False)
|
| 177 |
-
|
| 178 |
|
| 179 |
# Document level info
|
| 180 |
stock_take_date = Column(TIMESTAMP(timezone=True), nullable=False)
|
|
@@ -272,7 +272,7 @@ class ScmStockAdjustment(Base):
|
|
| 272 |
|
| 273 |
adjustment_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 274 |
merchant_id = Column(String(64), nullable=False)
|
| 275 |
-
|
| 276 |
sku = Column(String(64), nullable=False)
|
| 277 |
batch_no = Column(String(50))
|
| 278 |
|
|
|
|
| 22 |
|
| 23 |
stock_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 24 |
merchant_id = Column(String(64), nullable=False)
|
| 25 |
+
warehouse_id = Column(String(64), nullable=False)
|
| 26 |
sku = Column(String(64), nullable=False)
|
| 27 |
batch_no = Column(String(50), nullable=True)
|
| 28 |
|
|
|
|
| 53 |
|
| 54 |
ledger_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 55 |
merchant_id = Column(String(64), nullable=False)
|
| 56 |
+
warehouse_id = Column(String(64), nullable=False)
|
| 57 |
sku = Column(String(64), nullable=False)
|
| 58 |
batch_no = Column(String(50))
|
| 59 |
|
|
|
|
| 83 |
adjustment_master_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 84 |
adjustment_number = Column(String(50), nullable=False) # Human readable: ADJ-2024-001
|
| 85 |
merchant_id = Column(String(64), nullable=False)
|
| 86 |
+
warehouse_id = Column(String(64), nullable=False)
|
| 87 |
|
| 88 |
# Document level info
|
| 89 |
adjustment_date = Column(TIMESTAMP(timezone=True), nullable=False, default=datetime.utcnow)
|
|
|
|
| 174 |
stock_take_master_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 175 |
stock_take_number = Column(String(50), nullable=False) # Human readable: ST-2024-001
|
| 176 |
merchant_id = Column(String(64), nullable=False)
|
| 177 |
+
warehouse_id = Column(String(64), nullable=False)
|
| 178 |
|
| 179 |
# Document level info
|
| 180 |
stock_take_date = Column(TIMESTAMP(timezone=True), nullable=False)
|
|
|
|
| 272 |
|
| 273 |
adjustment_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 274 |
merchant_id = Column(String(64), nullable=False)
|
| 275 |
+
warehouse_id = Column(String(64), nullable=False)
|
| 276 |
sku = Column(String(64), nullable=False)
|
| 277 |
batch_no = Column(String(50))
|
| 278 |
|
app/inventory/stock/schemas/schema.py
CHANGED
|
@@ -51,7 +51,7 @@ class StockAdjustmentListFilter(BaseModel):
|
|
| 51 |
|
| 52 |
class StockReservationRequest(BaseModel):
|
| 53 |
"""Stock reservation request"""
|
| 54 |
-
|
| 55 |
sku: str = Field(..., description="SKU code")
|
| 56 |
batch_no: str = Field(..., description="Batch number")
|
| 57 |
qty: Decimal = Field(..., gt=0, description="Quantity to reserve")
|
|
@@ -60,7 +60,7 @@ class StockReservationRequest(BaseModel):
|
|
| 60 |
|
| 61 |
class StockAdjustmentRequest(BaseModel):
|
| 62 |
"""Stock adjustment request"""
|
| 63 |
-
|
| 64 |
sku: str = Field(..., description="SKU code")
|
| 65 |
batch_no: str = Field(..., description="Batch number")
|
| 66 |
adjustment_qty: Decimal = Field(..., description="Adjustment quantity (positive for increase, negative for decrease)")
|
|
@@ -70,8 +70,8 @@ class StockAdjustmentRequest(BaseModel):
|
|
| 70 |
|
| 71 |
class StockTransferRequest(BaseModel):
|
| 72 |
"""Stock transfer request"""
|
| 73 |
-
|
| 74 |
-
|
| 75 |
sku: str = Field(..., description="SKU code")
|
| 76 |
batch_no: str = Field(..., description="Batch number")
|
| 77 |
qty: Decimal = Field(..., gt=0, description="Quantity to transfer")
|
|
@@ -86,7 +86,7 @@ class BulkStockAdjustmentRequest(BaseModel):
|
|
| 86 |
|
| 87 |
class StockSummaryRequest(BaseModel):
|
| 88 |
"""Stock summary request with projection support"""
|
| 89 |
-
|
| 90 |
sku: Optional[str] = Field(None, description="Filter by SKU")
|
| 91 |
catalogue_id: Optional[str] = Field(None, description="Filter by catalogue ID")
|
| 92 |
projection_list: Optional[List[str]] = Field(
|
|
@@ -97,7 +97,7 @@ class StockSummaryRequest(BaseModel):
|
|
| 97 |
|
| 98 |
class StockAvailabilityRequest(BaseModel):
|
| 99 |
"""Stock availability check request"""
|
| 100 |
-
items: List[Dict[str, Any]] = Field(..., description="List of items to check (sku, qty,
|
| 101 |
projection_list: Optional[List[str]] = Field(
|
| 102 |
None,
|
| 103 |
description="List of fields to include in response"
|
|
@@ -106,7 +106,7 @@ class StockAvailabilityRequest(BaseModel):
|
|
| 106 |
|
| 107 |
class StockValuationRequest(BaseModel):
|
| 108 |
"""Stock valuation request"""
|
| 109 |
-
|
| 110 |
as_of_date: Optional[datetime] = Field(None, description="Valuation as of date")
|
| 111 |
projection_list: Optional[List[str]] = Field(
|
| 112 |
None,
|
|
|
|
| 51 |
|
| 52 |
class StockReservationRequest(BaseModel):
|
| 53 |
"""Stock reservation request"""
|
| 54 |
+
warehouse_id: str = Field(..., description="Warehouse/location ID")
|
| 55 |
sku: str = Field(..., description="SKU code")
|
| 56 |
batch_no: str = Field(..., description="Batch number")
|
| 57 |
qty: Decimal = Field(..., gt=0, description="Quantity to reserve")
|
|
|
|
| 60 |
|
| 61 |
class StockAdjustmentRequest(BaseModel):
|
| 62 |
"""Stock adjustment request"""
|
| 63 |
+
warehouse_id: str = Field(..., description="Warehouse/location ID")
|
| 64 |
sku: str = Field(..., description="SKU code")
|
| 65 |
batch_no: str = Field(..., description="Batch number")
|
| 66 |
adjustment_qty: Decimal = Field(..., description="Adjustment quantity (positive for increase, negative for decrease)")
|
|
|
|
| 70 |
|
| 71 |
class StockTransferRequest(BaseModel):
|
| 72 |
"""Stock transfer request"""
|
| 73 |
+
from_warehouse_id: str = Field(..., description="Source location ID")
|
| 74 |
+
to_warehouse_id: str = Field(..., description="Destination location ID")
|
| 75 |
sku: str = Field(..., description="SKU code")
|
| 76 |
batch_no: str = Field(..., description="Batch number")
|
| 77 |
qty: Decimal = Field(..., gt=0, description="Quantity to transfer")
|
|
|
|
| 86 |
|
| 87 |
class StockSummaryRequest(BaseModel):
|
| 88 |
"""Stock summary request with projection support"""
|
| 89 |
+
warehouse_id: Optional[str] = Field(None, description="Filter by location")
|
| 90 |
sku: Optional[str] = Field(None, description="Filter by SKU")
|
| 91 |
catalogue_id: Optional[str] = Field(None, description="Filter by catalogue ID")
|
| 92 |
projection_list: Optional[List[str]] = Field(
|
|
|
|
| 97 |
|
| 98 |
class StockAvailabilityRequest(BaseModel):
|
| 99 |
"""Stock availability check request"""
|
| 100 |
+
items: List[Dict[str, Any]] = Field(..., description="List of items to check (sku, qty, warehouse_id)")
|
| 101 |
projection_list: Optional[List[str]] = Field(
|
| 102 |
None,
|
| 103 |
description="List of fields to include in response"
|
|
|
|
| 106 |
|
| 107 |
class StockValuationRequest(BaseModel):
|
| 108 |
"""Stock valuation request"""
|
| 109 |
+
warehouse_id: Optional[str] = Field(None, description="Filter by location")
|
| 110 |
as_of_date: Optional[datetime] = Field(None, description="Valuation as of date")
|
| 111 |
projection_list: Optional[List[str]] = Field(
|
| 112 |
None,
|
app/inventory/stock/services/service.py
CHANGED
|
@@ -43,7 +43,7 @@ class StockTransaction:
|
|
| 43 |
def __init__(
|
| 44 |
self,
|
| 45 |
merchant_id: str,
|
| 46 |
-
|
| 47 |
catalogue_id: str,
|
| 48 |
sku: str,
|
| 49 |
batch_no: str,
|
|
@@ -58,7 +58,7 @@ class StockTransaction:
|
|
| 58 |
created_by: str = "system"
|
| 59 |
):
|
| 60 |
self.merchant_id = merchant_id
|
| 61 |
-
self.
|
| 62 |
self.catalogue_id = catalogue_id
|
| 63 |
self.sku = sku
|
| 64 |
self.batch_no = batch_no
|
|
@@ -93,14 +93,14 @@ class StockService:
|
|
| 93 |
# # Call stored procedure with correct parameters (10 parameters as per procedure definition)
|
| 94 |
# query = text("""
|
| 95 |
# SELECT trans.apply_stock_movement(
|
| 96 |
-
# :merchant_id, :
|
| 97 |
# :txn_type, :ref_type, :ref_id, :ref_no, :created_by
|
| 98 |
# )
|
| 99 |
# """)
|
| 100 |
|
| 101 |
# await self.db.execute(query, {
|
| 102 |
# "merchant_id": transaction.merchant_id,
|
| 103 |
-
# "
|
| 104 |
# "sku": transaction.sku,
|
| 105 |
# "batch_no": transaction.batch_no,
|
| 106 |
# "qty": float(transaction.qty), # Convert Decimal to float for PostgreSQL NUMERIC
|
|
@@ -145,7 +145,7 @@ class StockService:
|
|
| 145 |
for transaction in transactions:
|
| 146 |
movement = {
|
| 147 |
"merchant_id": transaction.merchant_id,
|
| 148 |
-
"
|
| 149 |
"sku": transaction.sku,
|
| 150 |
"batch_no": transaction.batch_no,
|
| 151 |
"catalogue_id": transaction.catalogue_id,
|
|
@@ -161,7 +161,7 @@ class StockService:
|
|
| 161 |
|
| 162 |
# Call bulk stored procedure
|
| 163 |
query = text("SELECT trans.apply_bulk_stock_movements(:movements) as results")
|
| 164 |
-
logger.info(f"merchant_id: {transaction.merchant_id},
|
| 165 |
|
| 166 |
result = await self.db.execute(query, {
|
| 167 |
"movements": json.dumps(movements)
|
|
@@ -202,8 +202,8 @@ class StockService:
|
|
| 202 |
if not transaction.merchant_id:
|
| 203 |
raise ValueError("merchant_id is required")
|
| 204 |
|
| 205 |
-
if not transaction.
|
| 206 |
-
raise ValueError("
|
| 207 |
|
| 208 |
if not transaction.sku:
|
| 209 |
raise ValueError("sku is required")
|
|
@@ -255,7 +255,7 @@ class StockService:
|
|
| 255 |
for item in grn_items:
|
| 256 |
transaction = StockTransaction(
|
| 257 |
merchant_id=item.merchant_id,
|
| 258 |
-
|
| 259 |
catalogue_id=str(item.catalogue_id),
|
| 260 |
sku=item.sku,
|
| 261 |
batch_no=item.batch_no,
|
|
@@ -284,7 +284,7 @@ class StockService:
|
|
| 284 |
async def process_stock_adjustment(
|
| 285 |
self,
|
| 286 |
merchant_id: str,
|
| 287 |
-
|
| 288 |
sku: str,
|
| 289 |
batch_no: str,
|
| 290 |
adjustment_qty: Decimal,
|
|
@@ -305,13 +305,13 @@ class StockService:
|
|
| 305 |
# Get catalogue_id and uom from existing stock
|
| 306 |
stock_query = text("""
|
| 307 |
SELECT catalogue_id, uom FROM trans.scm_stock
|
| 308 |
-
WHERE merchant_id = :merchant_id AND
|
| 309 |
AND sku = :sku AND batch_no = :batch_no
|
| 310 |
""")
|
| 311 |
|
| 312 |
result = await self.db.execute(stock_query, {
|
| 313 |
"merchant_id": merchant_id,
|
| 314 |
-
"
|
| 315 |
"sku": sku,
|
| 316 |
"batch_no": batch_no
|
| 317 |
})
|
|
@@ -329,7 +329,7 @@ class StockService:
|
|
| 329 |
|
| 330 |
transaction = StockTransaction(
|
| 331 |
merchant_id=merchant_id,
|
| 332 |
-
|
| 333 |
catalogue_id=catalogue_id,
|
| 334 |
sku=sku,
|
| 335 |
batch_no=batch_no,
|
|
@@ -352,15 +352,15 @@ class StockService:
|
|
| 352 |
async def get_stock_summary(
|
| 353 |
self,
|
| 354 |
merchant_id: str,
|
| 355 |
-
|
| 356 |
sku: Optional[str] = None
|
| 357 |
) -> List[Dict[str, Any]]:
|
| 358 |
"""Get current stock summary using stored procedure"""
|
| 359 |
try:
|
| 360 |
-
query = text("SELECT * FROM get_stock_summary(:merchant_id, :
|
| 361 |
result = await self.db.execute(query, {
|
| 362 |
"merchant_id": merchant_id,
|
| 363 |
-
"
|
| 364 |
"sku": sku
|
| 365 |
})
|
| 366 |
|
|
@@ -369,7 +369,7 @@ class StockService:
|
|
| 369 |
return [
|
| 370 |
{
|
| 371 |
"stock_id": str(stock.stock_id),
|
| 372 |
-
"
|
| 373 |
"catalogue_id": str(stock.catalogue_id),
|
| 374 |
"sku": stock.sku,
|
| 375 |
"batch_no": stock.batch_no,
|
|
@@ -408,7 +408,7 @@ class StockService:
|
|
| 408 |
return [
|
| 409 |
{
|
| 410 |
"ledger_id": str(entry.ledger_id),
|
| 411 |
-
"
|
| 412 |
"catalogue_id": str(entry.catalogue_id),
|
| 413 |
"sku": entry.sku,
|
| 414 |
"batch_no": entry.batch_no,
|
|
@@ -441,7 +441,7 @@ class StockService:
|
|
| 441 |
# Build base query
|
| 442 |
base_query = """
|
| 443 |
SELECT
|
| 444 |
-
stock_id, merchant_id,
|
| 445 |
exp_dt, qty_on_hand, qty_reserved, qty_available, uom,
|
| 446 |
created_at, updated_at, last_updated_at
|
| 447 |
FROM trans.scm_stock
|
|
@@ -456,9 +456,9 @@ class StockService:
|
|
| 456 |
if key == "merchant_id":
|
| 457 |
where_conditions.append("merchant_id = :merchant_id")
|
| 458 |
params["merchant_id"] = value
|
| 459 |
-
elif key == "
|
| 460 |
-
where_conditions.append("
|
| 461 |
-
params["
|
| 462 |
elif key == "sku":
|
| 463 |
where_conditions.append("sku ILIKE :sku")
|
| 464 |
params["sku"] = f"%{value}%"
|
|
@@ -494,7 +494,7 @@ class StockService:
|
|
| 494 |
row_dict = {
|
| 495 |
"stock_id": str(row.stock_id),
|
| 496 |
"merchant_id": row.merchant_id,
|
| 497 |
-
"
|
| 498 |
"catalogue_id": str(row.catalogue_id),
|
| 499 |
"sku": row.sku,
|
| 500 |
"batch_no": row.batch_no,
|
|
@@ -541,7 +541,7 @@ class StockService:
|
|
| 541 |
# Build base query
|
| 542 |
base_query = """
|
| 543 |
SELECT
|
| 544 |
-
ledger_id, merchant_id,
|
| 545 |
exp_dt, qty, uom, txn_type, ref_type, ref_id, ref_no,
|
| 546 |
remarks, created_by, created_at
|
| 547 |
FROM trans.scm_stock_ledger
|
|
@@ -556,9 +556,9 @@ class StockService:
|
|
| 556 |
if key == "merchant_id":
|
| 557 |
where_conditions.append("merchant_id = :merchant_id")
|
| 558 |
params["merchant_id"] = value
|
| 559 |
-
elif key == "
|
| 560 |
-
where_conditions.append("
|
| 561 |
-
params["
|
| 562 |
elif key == "sku":
|
| 563 |
where_conditions.append("sku ILIKE :sku")
|
| 564 |
params["sku"] = f"%{value}%"
|
|
@@ -594,7 +594,7 @@ class StockService:
|
|
| 594 |
row_dict = {
|
| 595 |
"ledger_id": str(row.ledger_id),
|
| 596 |
"merchant_id": row.merchant_id,
|
| 597 |
-
"
|
| 598 |
"catalogue_id": str(row.catalogue_id),
|
| 599 |
"sku": row.sku,
|
| 600 |
"batch_no": row.batch_no,
|
|
@@ -643,7 +643,7 @@ class StockService:
|
|
| 643 |
# Build base query
|
| 644 |
base_query = """
|
| 645 |
SELECT
|
| 646 |
-
adjustment_id, merchant_id,
|
| 647 |
adjustment_qty, adjustment_type, reason, status, approved_by,
|
| 648 |
approved_at, created_by, created_at, updated_at
|
| 649 |
FROM trans.scm_stock_adjustment
|
|
@@ -658,9 +658,9 @@ class StockService:
|
|
| 658 |
if key == "merchant_id":
|
| 659 |
where_conditions.append("merchant_id = :merchant_id")
|
| 660 |
params["merchant_id"] = value
|
| 661 |
-
elif key == "
|
| 662 |
-
where_conditions.append("
|
| 663 |
-
params["
|
| 664 |
elif key == "sku":
|
| 665 |
where_conditions.append("sku ILIKE :sku")
|
| 666 |
params["sku"] = f"%{value}%"
|
|
@@ -693,7 +693,7 @@ class StockService:
|
|
| 693 |
row_dict = {
|
| 694 |
"adjustment_id": str(row.adjustment_id),
|
| 695 |
"merchant_id": row.merchant_id,
|
| 696 |
-
"
|
| 697 |
"catalogue_id": str(row.catalogue_id),
|
| 698 |
"sku": row.sku,
|
| 699 |
"batch_no": row.batch_no,
|
|
@@ -732,7 +732,7 @@ class StockService:
|
|
| 732 |
async def reserve_stock(
|
| 733 |
self,
|
| 734 |
merchant_id: str,
|
| 735 |
-
|
| 736 |
sku: str,
|
| 737 |
batch_no: str,
|
| 738 |
qty: Decimal,
|
|
@@ -742,12 +742,12 @@ class StockService:
|
|
| 742 |
"""Reserve stock using stored procedure"""
|
| 743 |
try:
|
| 744 |
query = text("""
|
| 745 |
-
SELECT reserve_stock(:merchant_id, :
|
| 746 |
""")
|
| 747 |
|
| 748 |
result = await self.db.execute(query, {
|
| 749 |
"merchant_id": merchant_id,
|
| 750 |
-
"
|
| 751 |
"sku": sku,
|
| 752 |
"batch_no": batch_no,
|
| 753 |
"qty": float(qty),
|
|
@@ -769,7 +769,7 @@ class StockService:
|
|
| 769 |
async def release_stock_reservation(
|
| 770 |
self,
|
| 771 |
merchant_id: str,
|
| 772 |
-
|
| 773 |
sku: str,
|
| 774 |
batch_no: str,
|
| 775 |
qty: Decimal,
|
|
@@ -779,12 +779,12 @@ class StockService:
|
|
| 779 |
"""Release stock reservation using stored procedure"""
|
| 780 |
try:
|
| 781 |
query = text("""
|
| 782 |
-
SELECT release_stock_reservation(:merchant_id, :
|
| 783 |
""")
|
| 784 |
|
| 785 |
result = await self.db.execute(query, {
|
| 786 |
"merchant_id": merchant_id,
|
| 787 |
-
"
|
| 788 |
"sku": sku,
|
| 789 |
"batch_no": batch_no,
|
| 790 |
"qty": float(qty),
|
|
@@ -806,7 +806,7 @@ class StockService:
|
|
| 806 |
async def get_stock_summary_with_projection(
|
| 807 |
self,
|
| 808 |
merchant_id: str,
|
| 809 |
-
|
| 810 |
sku: Optional[str] = None,
|
| 811 |
catalogue_id: Optional[str] = None,
|
| 812 |
projection_list: Optional[List[str]] = None
|
|
@@ -814,17 +814,17 @@ class StockService:
|
|
| 814 |
"""Get stock summary with optional projection"""
|
| 815 |
try:
|
| 816 |
# Build query with optional filters
|
| 817 |
-
query_parts = ["SELECT * FROM get_stock_summary(:merchant_id, :
|
| 818 |
params = {
|
| 819 |
"merchant_id": merchant_id,
|
| 820 |
-
"
|
| 821 |
"sku": sku
|
| 822 |
}
|
| 823 |
|
| 824 |
# If catalogue_id is provided, add it as a filter
|
| 825 |
if catalogue_id:
|
| 826 |
query_parts[0] = """
|
| 827 |
-
SELECT s.* FROM get_stock_summary(:merchant_id, :
|
| 828 |
WHERE s.catalogue_id = :catalogue_id
|
| 829 |
"""
|
| 830 |
params["catalogue_id"] = catalogue_id
|
|
@@ -837,7 +837,7 @@ class StockService:
|
|
| 837 |
for stock in stocks:
|
| 838 |
stock_dict = {
|
| 839 |
"stock_id": str(stock.stock_id),
|
| 840 |
-
"
|
| 841 |
"catalogue_id": str(stock.catalogue_id),
|
| 842 |
"sku": stock.sku,
|
| 843 |
"batch_no": stock.batch_no,
|
|
@@ -877,7 +877,7 @@ class StockService:
|
|
| 877 |
for item in items:
|
| 878 |
sku = item.get("sku")
|
| 879 |
required_qty = item.get("qty", 0)
|
| 880 |
-
|
| 881 |
|
| 882 |
if not sku:
|
| 883 |
continue
|
|
@@ -885,20 +885,20 @@ class StockService:
|
|
| 885 |
# Query available stock
|
| 886 |
query = text("""
|
| 887 |
SELECT
|
| 888 |
-
sku,
|
| 889 |
catalogue_id, uom
|
| 890 |
FROM trans.scm_stock
|
| 891 |
WHERE merchant_id = :merchant_id
|
| 892 |
AND sku = :sku
|
| 893 |
AND qty_available > 0
|
| 894 |
-
AND (:
|
| 895 |
ORDER BY exp_dt ASC NULLS LAST, created_at ASC
|
| 896 |
""")
|
| 897 |
|
| 898 |
result = await self.db.execute(query, {
|
| 899 |
"merchant_id": merchant_id,
|
| 900 |
"sku": sku,
|
| 901 |
-
"
|
| 902 |
})
|
| 903 |
|
| 904 |
stock_rows = result.fetchall()
|
|
@@ -912,7 +912,7 @@ class StockService:
|
|
| 912 |
"required_qty": required_qty,
|
| 913 |
"total_available": total_available,
|
| 914 |
"is_available": is_available,
|
| 915 |
-
"
|
| 916 |
"batches": [
|
| 917 |
{
|
| 918 |
"batch_no": row.batch_no,
|
|
@@ -920,7 +920,7 @@ class StockService:
|
|
| 920 |
"exp_dt": row.exp_dt.isoformat() if row.exp_dt else None,
|
| 921 |
"catalogue_id": str(row.catalogue_id),
|
| 922 |
"uom": row.uom,
|
| 923 |
-
"
|
| 924 |
}
|
| 925 |
for row in stock_rows
|
| 926 |
]
|
|
@@ -945,7 +945,7 @@ class StockService:
|
|
| 945 |
async def get_stock_valuation(
|
| 946 |
self,
|
| 947 |
merchant_id: str,
|
| 948 |
-
|
| 949 |
as_of_date: Optional[datetime] = None,
|
| 950 |
projection_list: Optional[List[str]] = None
|
| 951 |
) -> List[Dict[str, Any]]:
|
|
@@ -954,7 +954,7 @@ class StockService:
|
|
| 954 |
# Build query for stock valuation
|
| 955 |
query = text("""
|
| 956 |
SELECT
|
| 957 |
-
s.stock_id, s.
|
| 958 |
s.qty_on_hand, s.uom, s.exp_dt,
|
| 959 |
cr.mrp, cr.base_price,
|
| 960 |
(s.qty_on_hand * COALESCE(cr.base_price, cr.mrp, 0)) as valuation_amount
|
|
@@ -962,14 +962,14 @@ class StockService:
|
|
| 962 |
LEFT JOIN trans.catalogue_ref cr ON s.catalogue_id = cr.catalogue_id
|
| 963 |
WHERE s.merchant_id = :merchant_id
|
| 964 |
AND s.qty_on_hand > 0
|
| 965 |
-
AND (:
|
| 966 |
AND (:as_of_date IS NULL OR s.created_at <= :as_of_date)
|
| 967 |
ORDER BY s.sku, s.batch_no
|
| 968 |
""")
|
| 969 |
|
| 970 |
params = {
|
| 971 |
"merchant_id": merchant_id,
|
| 972 |
-
"
|
| 973 |
"as_of_date": as_of_date
|
| 974 |
}
|
| 975 |
|
|
@@ -986,7 +986,7 @@ class StockService:
|
|
| 986 |
|
| 987 |
valuation_dict = {
|
| 988 |
"stock_id": str(row.stock_id),
|
| 989 |
-
"
|
| 990 |
"catalogue_id": str(row.catalogue_id),
|
| 991 |
"sku": row.sku,
|
| 992 |
"batch_no": row.batch_no,
|
|
@@ -1032,7 +1032,7 @@ class StockService:
|
|
| 1032 |
try:
|
| 1033 |
query = text("""
|
| 1034 |
SELECT
|
| 1035 |
-
s.stock_id, s.merchant_id, s.
|
| 1036 |
s.exp_dt, s.qty_on_hand, s.qty_reserved, s.qty_available, s.uom,
|
| 1037 |
s.created_at, s.updated_at, s.last_updated_at,
|
| 1038 |
cr.catalogue_name, cr.mrp, cr.base_price
|
|
@@ -1053,7 +1053,7 @@ class StockService:
|
|
| 1053 |
stock_dict = {
|
| 1054 |
"stock_id": str(row.stock_id),
|
| 1055 |
"merchant_id": row.merchant_id,
|
| 1056 |
-
"
|
| 1057 |
"catalogue_id": str(row.catalogue_id),
|
| 1058 |
"sku": row.sku,
|
| 1059 |
"batch_no": row.batch_no,
|
|
@@ -1092,8 +1092,8 @@ class StockService:
|
|
| 1092 |
async def process_stock_transfer(
|
| 1093 |
self,
|
| 1094 |
merchant_id: str,
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
sku: str,
|
| 1098 |
batch_no: str,
|
| 1099 |
qty: Decimal,
|
|
@@ -1106,13 +1106,13 @@ class StockService:
|
|
| 1106 |
# Get catalogue_id and uom from existing stock
|
| 1107 |
stock_query = text("""
|
| 1108 |
SELECT catalogue_id, uom FROM trans.scm_stock
|
| 1109 |
-
WHERE merchant_id = :merchant_id AND
|
| 1110 |
AND sku = :sku AND batch_no = :batch_no AND qty_available >= :qty
|
| 1111 |
""")
|
| 1112 |
|
| 1113 |
result = await self.db.execute(stock_query, {
|
| 1114 |
"merchant_id": merchant_id,
|
| 1115 |
-
"
|
| 1116 |
"sku": sku,
|
| 1117 |
"batch_no": batch_no,
|
| 1118 |
"qty": float(qty)
|
|
@@ -1128,7 +1128,7 @@ class StockService:
|
|
| 1128 |
# Create transfer OUT transaction
|
| 1129 |
transfer_out = StockTransaction(
|
| 1130 |
merchant_id=merchant_id,
|
| 1131 |
-
|
| 1132 |
catalogue_id=catalogue_id,
|
| 1133 |
sku=sku,
|
| 1134 |
batch_no=batch_no,
|
|
@@ -1138,14 +1138,14 @@ class StockService:
|
|
| 1138 |
txn_type=TransactionType.TRANSFER_OUT,
|
| 1139 |
ref_type=ReferenceType.TRANSFER,
|
| 1140 |
ref_id=ref_id,
|
| 1141 |
-
remarks=f"Transfer to {
|
| 1142 |
created_by=created_by
|
| 1143 |
)
|
| 1144 |
|
| 1145 |
# Create transfer IN transaction
|
| 1146 |
transfer_in = StockTransaction(
|
| 1147 |
merchant_id=merchant_id,
|
| 1148 |
-
|
| 1149 |
catalogue_id=catalogue_id,
|
| 1150 |
sku=sku,
|
| 1151 |
batch_no=batch_no,
|
|
@@ -1155,7 +1155,7 @@ class StockService:
|
|
| 1155 |
txn_type=TransactionType.TRANSFER_IN,
|
| 1156 |
ref_type=ReferenceType.TRANSFER,
|
| 1157 |
ref_id=ref_id,
|
| 1158 |
-
remarks=f"Transfer from {
|
| 1159 |
created_by=created_by
|
| 1160 |
)
|
| 1161 |
|
|
@@ -1167,7 +1167,7 @@ class StockService:
|
|
| 1167 |
success = all(result[1] for result in results)
|
| 1168 |
|
| 1169 |
if success:
|
| 1170 |
-
logger.info(f"Stock transfer completed: {sku} from {
|
| 1171 |
|
| 1172 |
return success
|
| 1173 |
|
|
|
|
| 43 |
def __init__(
|
| 44 |
self,
|
| 45 |
merchant_id: str,
|
| 46 |
+
warehouse_id: str,
|
| 47 |
catalogue_id: str,
|
| 48 |
sku: str,
|
| 49 |
batch_no: str,
|
|
|
|
| 58 |
created_by: str = "system"
|
| 59 |
):
|
| 60 |
self.merchant_id = merchant_id
|
| 61 |
+
self.warehouse_id = warehouse_id
|
| 62 |
self.catalogue_id = catalogue_id
|
| 63 |
self.sku = sku
|
| 64 |
self.batch_no = batch_no
|
|
|
|
| 93 |
# # Call stored procedure with correct parameters (10 parameters as per procedure definition)
|
| 94 |
# query = text("""
|
| 95 |
# SELECT trans.apply_stock_movement(
|
| 96 |
+
# :merchant_id, :warehouse_id, :sku, :batch_no, :qty,
|
| 97 |
# :txn_type, :ref_type, :ref_id, :ref_no, :created_by
|
| 98 |
# )
|
| 99 |
# """)
|
| 100 |
|
| 101 |
# await self.db.execute(query, {
|
| 102 |
# "merchant_id": transaction.merchant_id,
|
| 103 |
+
# "warehouse_id": transaction.warehouse_id,
|
| 104 |
# "sku": transaction.sku,
|
| 105 |
# "batch_no": transaction.batch_no,
|
| 106 |
# "qty": float(transaction.qty), # Convert Decimal to float for PostgreSQL NUMERIC
|
|
|
|
| 145 |
for transaction in transactions:
|
| 146 |
movement = {
|
| 147 |
"merchant_id": transaction.merchant_id,
|
| 148 |
+
"warehouse_id": transaction.warehouse_id,
|
| 149 |
"sku": transaction.sku,
|
| 150 |
"batch_no": transaction.batch_no,
|
| 151 |
"catalogue_id": transaction.catalogue_id,
|
|
|
|
| 161 |
|
| 162 |
# Call bulk stored procedure
|
| 163 |
query = text("SELECT trans.apply_bulk_stock_movements(:movements) as results")
|
| 164 |
+
logger.info(f"merchant_id: {transaction.merchant_id},warehouse_id: {transaction.warehouse_id},sku: {transaction.sku},batch_no: {transaction.batch_no},catalogue_id: {transaction.catalogue_id} ,uom: {transaction.uom}, expiry_date: {transaction.exp_dt} ,qty: {str(transaction.qty)},txn_type: {transaction.txn_type.value} ,ref_type: {transaction.ref_type.value} , ref_id: {str(transaction.ref_id)} ,created_by: {transaction.created_by}")
|
| 165 |
|
| 166 |
result = await self.db.execute(query, {
|
| 167 |
"movements": json.dumps(movements)
|
|
|
|
| 202 |
if not transaction.merchant_id:
|
| 203 |
raise ValueError("merchant_id is required")
|
| 204 |
|
| 205 |
+
if not transaction.warehouse_id:
|
| 206 |
+
raise ValueError("warehouse_id is required")
|
| 207 |
|
| 208 |
if not transaction.sku:
|
| 209 |
raise ValueError("sku is required")
|
|
|
|
| 255 |
for item in grn_items:
|
| 256 |
transaction = StockTransaction(
|
| 257 |
merchant_id=item.merchant_id,
|
| 258 |
+
warehouse_id=item.wh_location or "default",
|
| 259 |
catalogue_id=str(item.catalogue_id),
|
| 260 |
sku=item.sku,
|
| 261 |
batch_no=item.batch_no,
|
|
|
|
| 284 |
async def process_stock_adjustment(
|
| 285 |
self,
|
| 286 |
merchant_id: str,
|
| 287 |
+
warehouse_id: str,
|
| 288 |
sku: str,
|
| 289 |
batch_no: str,
|
| 290 |
adjustment_qty: Decimal,
|
|
|
|
| 305 |
# Get catalogue_id and uom from existing stock
|
| 306 |
stock_query = text("""
|
| 307 |
SELECT catalogue_id, uom FROM trans.scm_stock
|
| 308 |
+
WHERE merchant_id = :merchant_id AND warehouse_id = :warehouse_id
|
| 309 |
AND sku = :sku AND batch_no = :batch_no
|
| 310 |
""")
|
| 311 |
|
| 312 |
result = await self.db.execute(stock_query, {
|
| 313 |
"merchant_id": merchant_id,
|
| 314 |
+
"warehouse_id": warehouse_id,
|
| 315 |
"sku": sku,
|
| 316 |
"batch_no": batch_no
|
| 317 |
})
|
|
|
|
| 329 |
|
| 330 |
transaction = StockTransaction(
|
| 331 |
merchant_id=merchant_id,
|
| 332 |
+
warehouse_id=warehouse_id,
|
| 333 |
catalogue_id=catalogue_id,
|
| 334 |
sku=sku,
|
| 335 |
batch_no=batch_no,
|
|
|
|
| 352 |
async def get_stock_summary(
|
| 353 |
self,
|
| 354 |
merchant_id: str,
|
| 355 |
+
warehouse_id: Optional[str] = None,
|
| 356 |
sku: Optional[str] = None
|
| 357 |
) -> List[Dict[str, Any]]:
|
| 358 |
"""Get current stock summary using stored procedure"""
|
| 359 |
try:
|
| 360 |
+
query = text("SELECT * FROM get_stock_summary(:merchant_id, :warehouse_id, :sku)")
|
| 361 |
result = await self.db.execute(query, {
|
| 362 |
"merchant_id": merchant_id,
|
| 363 |
+
"warehouse_id": warehouse_id,
|
| 364 |
"sku": sku
|
| 365 |
})
|
| 366 |
|
|
|
|
| 369 |
return [
|
| 370 |
{
|
| 371 |
"stock_id": str(stock.stock_id),
|
| 372 |
+
"warehouse_id": stock.warehouse_id,
|
| 373 |
"catalogue_id": str(stock.catalogue_id),
|
| 374 |
"sku": stock.sku,
|
| 375 |
"batch_no": stock.batch_no,
|
|
|
|
| 408 |
return [
|
| 409 |
{
|
| 410 |
"ledger_id": str(entry.ledger_id),
|
| 411 |
+
"warehouse_id": entry.warehouse_id,
|
| 412 |
"catalogue_id": str(entry.catalogue_id),
|
| 413 |
"sku": entry.sku,
|
| 414 |
"batch_no": entry.batch_no,
|
|
|
|
| 441 |
# Build base query
|
| 442 |
base_query = """
|
| 443 |
SELECT
|
| 444 |
+
stock_id, merchant_id, warehouse_id, catalogue_id, sku, batch_no,
|
| 445 |
exp_dt, qty_on_hand, qty_reserved, qty_available, uom,
|
| 446 |
created_at, updated_at, last_updated_at
|
| 447 |
FROM trans.scm_stock
|
|
|
|
| 456 |
if key == "merchant_id":
|
| 457 |
where_conditions.append("merchant_id = :merchant_id")
|
| 458 |
params["merchant_id"] = value
|
| 459 |
+
elif key == "warehouse_id":
|
| 460 |
+
where_conditions.append("warehouse_id = :warehouse_id")
|
| 461 |
+
params["warehouse_id"] = value
|
| 462 |
elif key == "sku":
|
| 463 |
where_conditions.append("sku ILIKE :sku")
|
| 464 |
params["sku"] = f"%{value}%"
|
|
|
|
| 494 |
row_dict = {
|
| 495 |
"stock_id": str(row.stock_id),
|
| 496 |
"merchant_id": row.merchant_id,
|
| 497 |
+
"warehouse_id": row.warehouse_id,
|
| 498 |
"catalogue_id": str(row.catalogue_id),
|
| 499 |
"sku": row.sku,
|
| 500 |
"batch_no": row.batch_no,
|
|
|
|
| 541 |
# Build base query
|
| 542 |
base_query = """
|
| 543 |
SELECT
|
| 544 |
+
ledger_id, merchant_id, warehouse_id, catalogue_id, sku, batch_no,
|
| 545 |
exp_dt, qty, uom, txn_type, ref_type, ref_id, ref_no,
|
| 546 |
remarks, created_by, created_at
|
| 547 |
FROM trans.scm_stock_ledger
|
|
|
|
| 556 |
if key == "merchant_id":
|
| 557 |
where_conditions.append("merchant_id = :merchant_id")
|
| 558 |
params["merchant_id"] = value
|
| 559 |
+
elif key == "warehouse_id":
|
| 560 |
+
where_conditions.append("warehouse_id = :warehouse_id")
|
| 561 |
+
params["warehouse_id"] = value
|
| 562 |
elif key == "sku":
|
| 563 |
where_conditions.append("sku ILIKE :sku")
|
| 564 |
params["sku"] = f"%{value}%"
|
|
|
|
| 594 |
row_dict = {
|
| 595 |
"ledger_id": str(row.ledger_id),
|
| 596 |
"merchant_id": row.merchant_id,
|
| 597 |
+
"warehouse_id": row.warehouse_id,
|
| 598 |
"catalogue_id": str(row.catalogue_id),
|
| 599 |
"sku": row.sku,
|
| 600 |
"batch_no": row.batch_no,
|
|
|
|
| 643 |
# Build base query
|
| 644 |
base_query = """
|
| 645 |
SELECT
|
| 646 |
+
adjustment_id, merchant_id, warehouse_id, catalogue_id, sku, batch_no,
|
| 647 |
adjustment_qty, adjustment_type, reason, status, approved_by,
|
| 648 |
approved_at, created_by, created_at, updated_at
|
| 649 |
FROM trans.scm_stock_adjustment
|
|
|
|
| 658 |
if key == "merchant_id":
|
| 659 |
where_conditions.append("merchant_id = :merchant_id")
|
| 660 |
params["merchant_id"] = value
|
| 661 |
+
elif key == "warehouse_id":
|
| 662 |
+
where_conditions.append("warehouse_id = :warehouse_id")
|
| 663 |
+
params["warehouse_id"] = value
|
| 664 |
elif key == "sku":
|
| 665 |
where_conditions.append("sku ILIKE :sku")
|
| 666 |
params["sku"] = f"%{value}%"
|
|
|
|
| 693 |
row_dict = {
|
| 694 |
"adjustment_id": str(row.adjustment_id),
|
| 695 |
"merchant_id": row.merchant_id,
|
| 696 |
+
"warehouse_id": row.warehouse_id,
|
| 697 |
"catalogue_id": str(row.catalogue_id),
|
| 698 |
"sku": row.sku,
|
| 699 |
"batch_no": row.batch_no,
|
|
|
|
| 732 |
async def reserve_stock(
|
| 733 |
self,
|
| 734 |
merchant_id: str,
|
| 735 |
+
warehouse_id: str,
|
| 736 |
sku: str,
|
| 737 |
batch_no: str,
|
| 738 |
qty: Decimal,
|
|
|
|
| 742 |
"""Reserve stock using stored procedure"""
|
| 743 |
try:
|
| 744 |
query = text("""
|
| 745 |
+
SELECT reserve_stock(:merchant_id, :warehouse_id, :sku, :batch_no, :qty, :ref_id, :created_by)
|
| 746 |
""")
|
| 747 |
|
| 748 |
result = await self.db.execute(query, {
|
| 749 |
"merchant_id": merchant_id,
|
| 750 |
+
"warehouse_id": warehouse_id,
|
| 751 |
"sku": sku,
|
| 752 |
"batch_no": batch_no,
|
| 753 |
"qty": float(qty),
|
|
|
|
| 769 |
async def release_stock_reservation(
|
| 770 |
self,
|
| 771 |
merchant_id: str,
|
| 772 |
+
warehouse_id: str,
|
| 773 |
sku: str,
|
| 774 |
batch_no: str,
|
| 775 |
qty: Decimal,
|
|
|
|
| 779 |
"""Release stock reservation using stored procedure"""
|
| 780 |
try:
|
| 781 |
query = text("""
|
| 782 |
+
SELECT release_stock_reservation(:merchant_id, :warehouse_id, :sku, :batch_no, :qty, :ref_id, :created_by)
|
| 783 |
""")
|
| 784 |
|
| 785 |
result = await self.db.execute(query, {
|
| 786 |
"merchant_id": merchant_id,
|
| 787 |
+
"warehouse_id": warehouse_id,
|
| 788 |
"sku": sku,
|
| 789 |
"batch_no": batch_no,
|
| 790 |
"qty": float(qty),
|
|
|
|
| 806 |
async def get_stock_summary_with_projection(
|
| 807 |
self,
|
| 808 |
merchant_id: str,
|
| 809 |
+
warehouse_id: Optional[str] = None,
|
| 810 |
sku: Optional[str] = None,
|
| 811 |
catalogue_id: Optional[str] = None,
|
| 812 |
projection_list: Optional[List[str]] = None
|
|
|
|
| 814 |
"""Get stock summary with optional projection"""
|
| 815 |
try:
|
| 816 |
# Build query with optional filters
|
| 817 |
+
query_parts = ["SELECT * FROM get_stock_summary(:merchant_id, :warehouse_id, :sku)"]
|
| 818 |
params = {
|
| 819 |
"merchant_id": merchant_id,
|
| 820 |
+
"warehouse_id": warehouse_id,
|
| 821 |
"sku": sku
|
| 822 |
}
|
| 823 |
|
| 824 |
# If catalogue_id is provided, add it as a filter
|
| 825 |
if catalogue_id:
|
| 826 |
query_parts[0] = """
|
| 827 |
+
SELECT s.* FROM get_stock_summary(:merchant_id, :warehouse_id, :sku) s
|
| 828 |
WHERE s.catalogue_id = :catalogue_id
|
| 829 |
"""
|
| 830 |
params["catalogue_id"] = catalogue_id
|
|
|
|
| 837 |
for stock in stocks:
|
| 838 |
stock_dict = {
|
| 839 |
"stock_id": str(stock.stock_id),
|
| 840 |
+
"warehouse_id": stock.warehouse_id,
|
| 841 |
"catalogue_id": str(stock.catalogue_id),
|
| 842 |
"sku": stock.sku,
|
| 843 |
"batch_no": stock.batch_no,
|
|
|
|
| 877 |
for item in items:
|
| 878 |
sku = item.get("sku")
|
| 879 |
required_qty = item.get("qty", 0)
|
| 880 |
+
warehouse_id = item.get("warehouse_id")
|
| 881 |
|
| 882 |
if not sku:
|
| 883 |
continue
|
|
|
|
| 885 |
# Query available stock
|
| 886 |
query = text("""
|
| 887 |
SELECT
|
| 888 |
+
sku, warehouse_id, batch_no, qty_available, exp_dt,
|
| 889 |
catalogue_id, uom
|
| 890 |
FROM trans.scm_stock
|
| 891 |
WHERE merchant_id = :merchant_id
|
| 892 |
AND sku = :sku
|
| 893 |
AND qty_available > 0
|
| 894 |
+
AND (:warehouse_id IS NULL OR warehouse_id = :warehouse_id)
|
| 895 |
ORDER BY exp_dt ASC NULLS LAST, created_at ASC
|
| 896 |
""")
|
| 897 |
|
| 898 |
result = await self.db.execute(query, {
|
| 899 |
"merchant_id": merchant_id,
|
| 900 |
"sku": sku,
|
| 901 |
+
"warehouse_id": warehouse_id
|
| 902 |
})
|
| 903 |
|
| 904 |
stock_rows = result.fetchall()
|
|
|
|
| 912 |
"required_qty": required_qty,
|
| 913 |
"total_available": total_available,
|
| 914 |
"is_available": is_available,
|
| 915 |
+
"warehouse_id": warehouse_id,
|
| 916 |
"batches": [
|
| 917 |
{
|
| 918 |
"batch_no": row.batch_no,
|
|
|
|
| 920 |
"exp_dt": row.exp_dt.isoformat() if row.exp_dt else None,
|
| 921 |
"catalogue_id": str(row.catalogue_id),
|
| 922 |
"uom": row.uom,
|
| 923 |
+
"warehouse_id": row.warehouse_id
|
| 924 |
}
|
| 925 |
for row in stock_rows
|
| 926 |
]
|
|
|
|
| 945 |
async def get_stock_valuation(
|
| 946 |
self,
|
| 947 |
merchant_id: str,
|
| 948 |
+
warehouse_id: Optional[str] = None,
|
| 949 |
as_of_date: Optional[datetime] = None,
|
| 950 |
projection_list: Optional[List[str]] = None
|
| 951 |
) -> List[Dict[str, Any]]:
|
|
|
|
| 954 |
# Build query for stock valuation
|
| 955 |
query = text("""
|
| 956 |
SELECT
|
| 957 |
+
s.stock_id, s.warehouse_id, s.catalogue_id, s.sku, s.batch_no,
|
| 958 |
s.qty_on_hand, s.uom, s.exp_dt,
|
| 959 |
cr.mrp, cr.base_price,
|
| 960 |
(s.qty_on_hand * COALESCE(cr.base_price, cr.mrp, 0)) as valuation_amount
|
|
|
|
| 962 |
LEFT JOIN trans.catalogue_ref cr ON s.catalogue_id = cr.catalogue_id
|
| 963 |
WHERE s.merchant_id = :merchant_id
|
| 964 |
AND s.qty_on_hand > 0
|
| 965 |
+
AND (:warehouse_id IS NULL OR s.warehouse_id = :warehouse_id)
|
| 966 |
AND (:as_of_date IS NULL OR s.created_at <= :as_of_date)
|
| 967 |
ORDER BY s.sku, s.batch_no
|
| 968 |
""")
|
| 969 |
|
| 970 |
params = {
|
| 971 |
"merchant_id": merchant_id,
|
| 972 |
+
"warehouse_id": warehouse_id,
|
| 973 |
"as_of_date": as_of_date
|
| 974 |
}
|
| 975 |
|
|
|
|
| 986 |
|
| 987 |
valuation_dict = {
|
| 988 |
"stock_id": str(row.stock_id),
|
| 989 |
+
"warehouse_id": row.warehouse_id,
|
| 990 |
"catalogue_id": str(row.catalogue_id),
|
| 991 |
"sku": row.sku,
|
| 992 |
"batch_no": row.batch_no,
|
|
|
|
| 1032 |
try:
|
| 1033 |
query = text("""
|
| 1034 |
SELECT
|
| 1035 |
+
s.stock_id, s.merchant_id, s.warehouse_id, s.catalogue_id, s.sku, s.batch_no,
|
| 1036 |
s.exp_dt, s.qty_on_hand, s.qty_reserved, s.qty_available, s.uom,
|
| 1037 |
s.created_at, s.updated_at, s.last_updated_at,
|
| 1038 |
cr.catalogue_name, cr.mrp, cr.base_price
|
|
|
|
| 1053 |
stock_dict = {
|
| 1054 |
"stock_id": str(row.stock_id),
|
| 1055 |
"merchant_id": row.merchant_id,
|
| 1056 |
+
"warehouse_id": row.warehouse_id,
|
| 1057 |
"catalogue_id": str(row.catalogue_id),
|
| 1058 |
"sku": row.sku,
|
| 1059 |
"batch_no": row.batch_no,
|
|
|
|
| 1092 |
async def process_stock_transfer(
|
| 1093 |
self,
|
| 1094 |
merchant_id: str,
|
| 1095 |
+
from_warehouse_id: str,
|
| 1096 |
+
to_warehouse_id: str,
|
| 1097 |
sku: str,
|
| 1098 |
batch_no: str,
|
| 1099 |
qty: Decimal,
|
|
|
|
| 1106 |
# Get catalogue_id and uom from existing stock
|
| 1107 |
stock_query = text("""
|
| 1108 |
SELECT catalogue_id, uom FROM trans.scm_stock
|
| 1109 |
+
WHERE merchant_id = :merchant_id AND warehouse_id = :from_warehouse_id
|
| 1110 |
AND sku = :sku AND batch_no = :batch_no AND qty_available >= :qty
|
| 1111 |
""")
|
| 1112 |
|
| 1113 |
result = await self.db.execute(stock_query, {
|
| 1114 |
"merchant_id": merchant_id,
|
| 1115 |
+
"from_warehouse_id": from_warehouse_id,
|
| 1116 |
"sku": sku,
|
| 1117 |
"batch_no": batch_no,
|
| 1118 |
"qty": float(qty)
|
|
|
|
| 1128 |
# Create transfer OUT transaction
|
| 1129 |
transfer_out = StockTransaction(
|
| 1130 |
merchant_id=merchant_id,
|
| 1131 |
+
warehouse_id=from_warehouse_id,
|
| 1132 |
catalogue_id=catalogue_id,
|
| 1133 |
sku=sku,
|
| 1134 |
batch_no=batch_no,
|
|
|
|
| 1138 |
txn_type=TransactionType.TRANSFER_OUT,
|
| 1139 |
ref_type=ReferenceType.TRANSFER,
|
| 1140 |
ref_id=ref_id,
|
| 1141 |
+
remarks=f"Transfer to {to_warehouse_id}: {remarks}" if remarks else f"Transfer to {to_warehouse_id}",
|
| 1142 |
created_by=created_by
|
| 1143 |
)
|
| 1144 |
|
| 1145 |
# Create transfer IN transaction
|
| 1146 |
transfer_in = StockTransaction(
|
| 1147 |
merchant_id=merchant_id,
|
| 1148 |
+
warehouse_id=to_warehouse_id,
|
| 1149 |
catalogue_id=catalogue_id,
|
| 1150 |
sku=sku,
|
| 1151 |
batch_no=batch_no,
|
|
|
|
| 1155 |
txn_type=TransactionType.TRANSFER_IN,
|
| 1156 |
ref_type=ReferenceType.TRANSFER,
|
| 1157 |
ref_id=ref_id,
|
| 1158 |
+
remarks=f"Transfer from {from_warehouse_id}: {remarks}" if remarks else f"Transfer from {from_warehouse_id}",
|
| 1159 |
created_by=created_by
|
| 1160 |
)
|
| 1161 |
|
|
|
|
| 1167 |
success = all(result[1] for result in results)
|
| 1168 |
|
| 1169 |
if success:
|
| 1170 |
+
logger.info(f"Stock transfer completed: {sku} from {from_warehouse_id} to {to_warehouse_id}")
|
| 1171 |
|
| 1172 |
return success
|
| 1173 |
|
app/inventory/stock_take/controllers/router.py
CHANGED
|
@@ -140,7 +140,7 @@ async def get_stock_take(
|
|
| 140 |
{
|
| 141 |
"stock_take_id": "detail-uuid",
|
| 142 |
"merchant_id": "merchant_123",
|
| 143 |
-
"
|
| 144 |
"sku": "SKU_001",
|
| 145 |
"system_qty": 100,
|
| 146 |
"physical_qty": 98,
|
|
@@ -182,7 +182,7 @@ async def get_stock_take_session(
|
|
| 182 |
"stock_take_master_id": "uuid",
|
| 183 |
"stock_take_number": "ST-2024-12-001",
|
| 184 |
"merchant_id": "merchant_123",
|
| 185 |
-
"
|
| 186 |
"status": "completed",
|
| 187 |
"total_items": 3,
|
| 188 |
"total_variance_value": -45.50,
|
|
@@ -220,7 +220,7 @@ async def list_stock_takes(
|
|
| 220 |
**Request Body:**
|
| 221 |
- **filters**: Filter criteria (optional)
|
| 222 |
- merchant_id: Filter by merchant
|
| 223 |
-
-
|
| 224 |
- status: Filter by status (draft, completed, applied)
|
| 225 |
- stock_take_number: Search by stock take number (partial match)
|
| 226 |
- **skip**: Number of records to skip (pagination)
|
|
@@ -229,7 +229,7 @@ async def list_stock_takes(
|
|
| 229 |
|
| 230 |
**Projection Examples:**
|
| 231 |
- Basic info: ["stock_take_master_id", "stock_take_number", "status", "total_items"]
|
| 232 |
-
- Summary: ["stock_take_number", "
|
| 233 |
- Full details: null (returns all fields)
|
| 234 |
|
| 235 |
**Filter Examples:**
|
|
@@ -439,21 +439,21 @@ async def list_pending_approvals(
|
|
| 439 |
|
| 440 |
**Request Body:**
|
| 441 |
- **filters**: Additional filter criteria (optional)
|
| 442 |
-
-
|
| 443 |
- created_by: Filter by creator
|
| 444 |
- **skip**: Number of records to skip (pagination)
|
| 445 |
- **limit**: Maximum records to return (1-1000)
|
| 446 |
- **projection_list**: List of fields to include in response (optional)
|
| 447 |
|
| 448 |
**Useful Projections:**
|
| 449 |
-
- Dashboard: ["stock_take_number", "
|
| 450 |
- Summary: ["stock_take_master_id", "stock_take_number", "status", "total_variance_value"]
|
| 451 |
|
| 452 |
**Example:**
|
| 453 |
```json
|
| 454 |
{
|
| 455 |
"filters": {
|
| 456 |
-
"
|
| 457 |
},
|
| 458 |
"skip": 0,
|
| 459 |
"limit": 50,
|
|
|
|
| 140 |
{
|
| 141 |
"stock_take_id": "detail-uuid",
|
| 142 |
"merchant_id": "merchant_123",
|
| 143 |
+
"warehouse_id": "WH_001",
|
| 144 |
"sku": "SKU_001",
|
| 145 |
"system_qty": 100,
|
| 146 |
"physical_qty": 98,
|
|
|
|
| 182 |
"stock_take_master_id": "uuid",
|
| 183 |
"stock_take_number": "ST-2024-12-001",
|
| 184 |
"merchant_id": "merchant_123",
|
| 185 |
+
"warehouse_id": "WH_001",
|
| 186 |
"status": "completed",
|
| 187 |
"total_items": 3,
|
| 188 |
"total_variance_value": -45.50,
|
|
|
|
| 220 |
**Request Body:**
|
| 221 |
- **filters**: Filter criteria (optional)
|
| 222 |
- merchant_id: Filter by merchant
|
| 223 |
+
- warehouse_id: Filter by location
|
| 224 |
- status: Filter by status (draft, completed, applied)
|
| 225 |
- stock_take_number: Search by stock take number (partial match)
|
| 226 |
- **skip**: Number of records to skip (pagination)
|
|
|
|
| 229 |
|
| 230 |
**Projection Examples:**
|
| 231 |
- Basic info: ["stock_take_master_id", "stock_take_number", "status", "total_items"]
|
| 232 |
+
- Summary: ["stock_take_number", "warehouse_id", "total_items", "total_variance_value", "status"]
|
| 233 |
- Full details: null (returns all fields)
|
| 234 |
|
| 235 |
**Filter Examples:**
|
|
|
|
| 439 |
|
| 440 |
**Request Body:**
|
| 441 |
- **filters**: Additional filter criteria (optional)
|
| 442 |
+
- warehouse_id: Filter by location
|
| 443 |
- created_by: Filter by creator
|
| 444 |
- **skip**: Number of records to skip (pagination)
|
| 445 |
- **limit**: Maximum records to return (1-1000)
|
| 446 |
- **projection_list**: List of fields to include in response (optional)
|
| 447 |
|
| 448 |
**Useful Projections:**
|
| 449 |
+
- Dashboard: ["stock_take_number", "warehouse_id", "total_items", "total_variance_value", "created_by", "created_at"]
|
| 450 |
- Summary: ["stock_take_master_id", "stock_take_number", "status", "total_variance_value"]
|
| 451 |
|
| 452 |
**Example:**
|
| 453 |
```json
|
| 454 |
{
|
| 455 |
"filters": {
|
| 456 |
+
"warehouse_id": "WH_001"
|
| 457 |
},
|
| 458 |
"skip": 0,
|
| 459 |
"limit": 50,
|
app/inventory/stock_take/schemas/approval_schema.py
CHANGED
|
@@ -38,7 +38,7 @@ class StockTakeMasterRead(BaseModel):
|
|
| 38 |
stock_take_master_id: UUID
|
| 39 |
stock_take_number: str
|
| 40 |
merchant_id: str
|
| 41 |
-
|
| 42 |
stock_take_date: datetime
|
| 43 |
description: Optional[str]
|
| 44 |
additional_notes: Optional[str]
|
|
@@ -100,7 +100,7 @@ class StockTakeApprovalListRequest(BaseModel):
|
|
| 100 |
"example": {
|
| 101 |
"filters": {
|
| 102 |
"status": "submitted",
|
| 103 |
-
"
|
| 104 |
},
|
| 105 |
"skip": 0,
|
| 106 |
"limit": 50,
|
|
@@ -133,7 +133,7 @@ class StockTakeSummaryRead(BaseModel):
|
|
| 133 |
"""Summary view for stock take approval dashboard"""
|
| 134 |
stock_take_master_id: UUID
|
| 135 |
stock_take_number: str
|
| 136 |
-
|
| 137 |
total_items: int
|
| 138 |
total_variance_value: float
|
| 139 |
status: str
|
|
|
|
| 38 |
stock_take_master_id: UUID
|
| 39 |
stock_take_number: str
|
| 40 |
merchant_id: str
|
| 41 |
+
warehouse_id: str
|
| 42 |
stock_take_date: datetime
|
| 43 |
description: Optional[str]
|
| 44 |
additional_notes: Optional[str]
|
|
|
|
| 100 |
"example": {
|
| 101 |
"filters": {
|
| 102 |
"status": "submitted",
|
| 103 |
+
"warehouse_id": "WH_001"
|
| 104 |
},
|
| 105 |
"skip": 0,
|
| 106 |
"limit": 50,
|
|
|
|
| 133 |
"""Summary view for stock take approval dashboard"""
|
| 134 |
stock_take_master_id: UUID
|
| 135 |
stock_take_number: str
|
| 136 |
+
warehouse_id: str
|
| 137 |
total_items: int
|
| 138 |
total_variance_value: float
|
| 139 |
status: str
|
app/inventory/stock_take/schemas/schema.py
CHANGED
|
@@ -50,7 +50,7 @@ class StockTakeSessionResponse(BaseModel):
|
|
| 50 |
stock_take_master_id: str
|
| 51 |
stock_take_number: str
|
| 52 |
merchant_id: str
|
| 53 |
-
|
| 54 |
stock_take_date: datetime
|
| 55 |
description: Optional[str]
|
| 56 |
additional_notes: Optional[str]
|
|
@@ -71,7 +71,7 @@ class StockTakeMasterResponse(BaseModel):
|
|
| 71 |
stock_take_master_id: str
|
| 72 |
stock_take_number: str
|
| 73 |
merchant_id: str
|
| 74 |
-
|
| 75 |
stock_take_date: datetime
|
| 76 |
description: Optional[str]
|
| 77 |
additional_notes: Optional[str]
|
|
@@ -88,7 +88,7 @@ class StockTakeResponse(BaseModel):
|
|
| 88 |
"""Response schema for stock take"""
|
| 89 |
stock_take_id: str
|
| 90 |
merchant_id: str
|
| 91 |
-
|
| 92 |
sku: str
|
| 93 |
batch_no: Optional[str]
|
| 94 |
system_qty: int
|
|
|
|
| 50 |
stock_take_master_id: str
|
| 51 |
stock_take_number: str
|
| 52 |
merchant_id: str
|
| 53 |
+
warehouse_id: str
|
| 54 |
stock_take_date: datetime
|
| 55 |
description: Optional[str]
|
| 56 |
additional_notes: Optional[str]
|
|
|
|
| 71 |
stock_take_master_id: str
|
| 72 |
stock_take_number: str
|
| 73 |
merchant_id: str
|
| 74 |
+
warehouse_id: str
|
| 75 |
stock_take_date: datetime
|
| 76 |
description: Optional[str]
|
| 77 |
additional_notes: Optional[str]
|
|
|
|
| 88 |
"""Response schema for stock take"""
|
| 89 |
stock_take_id: str
|
| 90 |
merchant_id: str
|
| 91 |
+
warehouse_id: str
|
| 92 |
sku: str
|
| 93 |
batch_no: Optional[str]
|
| 94 |
system_qty: int
|
app/inventory/stock_take/services/approval_service.py
CHANGED
|
@@ -210,7 +210,7 @@ class StockTakeApprovalService:
|
|
| 210 |
stock_query = select(ScmStock).where(
|
| 211 |
and_(
|
| 212 |
ScmStock.merchant_id == master.merchant_id,
|
| 213 |
-
ScmStock.
|
| 214 |
ScmStock.sku == detail.sku
|
| 215 |
)
|
| 216 |
)
|
|
@@ -236,7 +236,7 @@ class StockTakeApprovalService:
|
|
| 236 |
stock_transactions.append(
|
| 237 |
StockTransaction(
|
| 238 |
merchant_id=master.merchant_id,
|
| 239 |
-
|
| 240 |
catalogue_id=str(catalogue_id),
|
| 241 |
sku=detail.sku,
|
| 242 |
batch_no=detail.batch_no,
|
|
@@ -478,8 +478,8 @@ class StockTakeApprovalService:
|
|
| 478 |
elif filters.get("merchant_id"):
|
| 479 |
conditions.append(ScmStockTakeMaster.merchant_id == filters["merchant_id"])
|
| 480 |
|
| 481 |
-
if filters.get("
|
| 482 |
-
conditions.append(ScmStockTakeMaster.
|
| 483 |
|
| 484 |
if filters.get("created_by"):
|
| 485 |
conditions.append(ScmStockTakeMaster.created_by == filters["created_by"])
|
|
|
|
| 210 |
stock_query = select(ScmStock).where(
|
| 211 |
and_(
|
| 212 |
ScmStock.merchant_id == master.merchant_id,
|
| 213 |
+
ScmStock.warehouse_id == master.warehouse_id,
|
| 214 |
ScmStock.sku == detail.sku
|
| 215 |
)
|
| 216 |
)
|
|
|
|
| 236 |
stock_transactions.append(
|
| 237 |
StockTransaction(
|
| 238 |
merchant_id=master.merchant_id,
|
| 239 |
+
warehouse_id=master.warehouse_id,
|
| 240 |
catalogue_id=str(catalogue_id),
|
| 241 |
sku=detail.sku,
|
| 242 |
batch_no=detail.batch_no,
|
|
|
|
| 478 |
elif filters.get("merchant_id"):
|
| 479 |
conditions.append(ScmStockTakeMaster.merchant_id == filters["merchant_id"])
|
| 480 |
|
| 481 |
+
if filters.get("warehouse_id"):
|
| 482 |
+
conditions.append(ScmStockTakeMaster.warehouse_id == filters["warehouse_id"])
|
| 483 |
|
| 484 |
if filters.get("created_by"):
|
| 485 |
conditions.append(ScmStockTakeMaster.created_by == filters["created_by"])
|
app/inventory/stock_take/services/service.py
CHANGED
|
@@ -36,7 +36,7 @@ class StockTakeService:
|
|
| 36 |
@staticmethod
|
| 37 |
async def _validate_stock_record(
|
| 38 |
merchant_id: str,
|
| 39 |
-
|
| 40 |
sku: str,
|
| 41 |
batch_no: Optional[str]
|
| 42 |
) -> bool:
|
|
@@ -46,7 +46,7 @@ class StockTakeService:
|
|
| 46 |
query = select(ScmStock).where(
|
| 47 |
and_(
|
| 48 |
ScmStock.merchant_id == merchant_id,
|
| 49 |
-
ScmStock.
|
| 50 |
ScmStock.sku == sku
|
| 51 |
)
|
| 52 |
)
|
|
@@ -100,7 +100,7 @@ class StockTakeService:
|
|
| 100 |
master = ScmStockTakeMaster(
|
| 101 |
stock_take_number=stock_take_number,
|
| 102 |
merchant_id=effective_merchant_id,
|
| 103 |
-
|
| 104 |
stock_take_date=payload.stock_take_date or datetime.utcnow(),
|
| 105 |
description=f"Stock take session - {len(payload.entries)} items",
|
| 106 |
additional_notes=payload.additional_notes,
|
|
@@ -121,7 +121,7 @@ class StockTakeService:
|
|
| 121 |
stock_query = select(ScmStock).where(
|
| 122 |
and_(
|
| 123 |
ScmStock.merchant_id == effective_merchant_id,
|
| 124 |
-
ScmStock.
|
| 125 |
ScmStock.sku == entry.sku
|
| 126 |
)
|
| 127 |
)
|
|
@@ -168,7 +168,7 @@ class StockTakeService:
|
|
| 168 |
results.append(StockTakeResponse(
|
| 169 |
stock_take_id=str(detail.stock_take_detail_id),
|
| 170 |
merchant_id=master.merchant_id,
|
| 171 |
-
|
| 172 |
sku=detail.sku,
|
| 173 |
batch_no=detail.batch_no,
|
| 174 |
system_qty=int(detail.system_qty),
|
|
@@ -269,7 +269,7 @@ class StockTakeService:
|
|
| 269 |
# Create internal stock adjustment for the variance
|
| 270 |
adjustment_request = CreateStockAdjustmentRequestSingle(
|
| 271 |
merchant_id=master.merchant_id,
|
| 272 |
-
|
| 273 |
sku=detail.sku,
|
| 274 |
batch_no=detail.batch_no,
|
| 275 |
adj_type=AdjustmentType.CYCLE_COUNT,
|
|
@@ -320,7 +320,7 @@ class StockTakeService:
|
|
| 320 |
return StockTakeResponse(
|
| 321 |
stock_take_id=str(detail.stock_take_detail_id),
|
| 322 |
merchant_id=master.merchant_id,
|
| 323 |
-
|
| 324 |
sku=detail.sku,
|
| 325 |
batch_no=detail.batch_no,
|
| 326 |
system_qty=int(detail.system_qty),
|
|
@@ -369,7 +369,7 @@ class StockTakeService:
|
|
| 369 |
return StockTakeResponse(
|
| 370 |
stock_take_id=str(detail.stock_take_detail_id),
|
| 371 |
merchant_id=master.merchant_id,
|
| 372 |
-
|
| 373 |
sku=detail.sku,
|
| 374 |
batch_no=detail.batch_no,
|
| 375 |
system_qty=int(detail.system_qty),
|
|
@@ -415,7 +415,7 @@ class StockTakeService:
|
|
| 415 |
return StockTakeResponse(
|
| 416 |
stock_take_id=str(first_detail.stock_take_detail_id),
|
| 417 |
merchant_id=master.merchant_id,
|
| 418 |
-
|
| 419 |
sku=first_detail.sku,
|
| 420 |
batch_no=first_detail.batch_no,
|
| 421 |
system_qty=int(first_detail.system_qty),
|
|
@@ -510,7 +510,7 @@ class StockTakeService:
|
|
| 510 |
stock_take_master_id=str(master.stock_take_master_id),
|
| 511 |
stock_take_number=master.stock_take_number,
|
| 512 |
merchant_id=master.merchant_id,
|
| 513 |
-
|
| 514 |
stock_take_date=master.stock_take_date,
|
| 515 |
description=master.description,
|
| 516 |
additional_notes=master.additional_notes,
|
|
@@ -556,8 +556,8 @@ class StockTakeService:
|
|
| 556 |
elif filters.get("merchant_id"):
|
| 557 |
conditions.append(ScmStockTakeMaster.merchant_id == filters["merchant_id"])
|
| 558 |
|
| 559 |
-
if filters.get("
|
| 560 |
-
conditions.append(ScmStockTakeMaster.
|
| 561 |
if filters.get("status"):
|
| 562 |
conditions.append(ScmStockTakeMaster.status == filters["status"])
|
| 563 |
if filters.get("stock_take_number"):
|
|
@@ -600,7 +600,7 @@ class StockTakeService:
|
|
| 600 |
stock_take_master_id=str(master.stock_take_master_id),
|
| 601 |
stock_take_number=master.stock_take_number,
|
| 602 |
merchant_id=master.merchant_id,
|
| 603 |
-
|
| 604 |
stock_take_date=master.stock_take_date,
|
| 605 |
description=master.description,
|
| 606 |
additional_notes=master.additional_notes,
|
|
|
|
| 36 |
@staticmethod
|
| 37 |
async def _validate_stock_record(
|
| 38 |
merchant_id: str,
|
| 39 |
+
warehouse_id: str,
|
| 40 |
sku: str,
|
| 41 |
batch_no: Optional[str]
|
| 42 |
) -> bool:
|
|
|
|
| 46 |
query = select(ScmStock).where(
|
| 47 |
and_(
|
| 48 |
ScmStock.merchant_id == merchant_id,
|
| 49 |
+
ScmStock.warehouse_id == warehouse_id,
|
| 50 |
ScmStock.sku == sku
|
| 51 |
)
|
| 52 |
)
|
|
|
|
| 100 |
master = ScmStockTakeMaster(
|
| 101 |
stock_take_number=stock_take_number,
|
| 102 |
merchant_id=effective_merchant_id,
|
| 103 |
+
warehouse_id=payload.warehouse_id,
|
| 104 |
stock_take_date=payload.stock_take_date or datetime.utcnow(),
|
| 105 |
description=f"Stock take session - {len(payload.entries)} items",
|
| 106 |
additional_notes=payload.additional_notes,
|
|
|
|
| 121 |
stock_query = select(ScmStock).where(
|
| 122 |
and_(
|
| 123 |
ScmStock.merchant_id == effective_merchant_id,
|
| 124 |
+
ScmStock.warehouse_id == payload.warehouse_id,
|
| 125 |
ScmStock.sku == entry.sku
|
| 126 |
)
|
| 127 |
)
|
|
|
|
| 168 |
results.append(StockTakeResponse(
|
| 169 |
stock_take_id=str(detail.stock_take_detail_id),
|
| 170 |
merchant_id=master.merchant_id,
|
| 171 |
+
warehouse_id=master.warehouse_id,
|
| 172 |
sku=detail.sku,
|
| 173 |
batch_no=detail.batch_no,
|
| 174 |
system_qty=int(detail.system_qty),
|
|
|
|
| 269 |
# Create internal stock adjustment for the variance
|
| 270 |
adjustment_request = CreateStockAdjustmentRequestSingle(
|
| 271 |
merchant_id=master.merchant_id,
|
| 272 |
+
warehouse_id=master.warehouse_id,
|
| 273 |
sku=detail.sku,
|
| 274 |
batch_no=detail.batch_no,
|
| 275 |
adj_type=AdjustmentType.CYCLE_COUNT,
|
|
|
|
| 320 |
return StockTakeResponse(
|
| 321 |
stock_take_id=str(detail.stock_take_detail_id),
|
| 322 |
merchant_id=master.merchant_id,
|
| 323 |
+
warehouse_id=master.warehouse_id,
|
| 324 |
sku=detail.sku,
|
| 325 |
batch_no=detail.batch_no,
|
| 326 |
system_qty=int(detail.system_qty),
|
|
|
|
| 369 |
return StockTakeResponse(
|
| 370 |
stock_take_id=str(detail.stock_take_detail_id),
|
| 371 |
merchant_id=master.merchant_id,
|
| 372 |
+
warehouse_id=master.warehouse_id,
|
| 373 |
sku=detail.sku,
|
| 374 |
batch_no=detail.batch_no,
|
| 375 |
system_qty=int(detail.system_qty),
|
|
|
|
| 415 |
return StockTakeResponse(
|
| 416 |
stock_take_id=str(first_detail.stock_take_detail_id),
|
| 417 |
merchant_id=master.merchant_id,
|
| 418 |
+
warehouse_id=master.warehouse_id,
|
| 419 |
sku=first_detail.sku,
|
| 420 |
batch_no=first_detail.batch_no,
|
| 421 |
system_qty=int(first_detail.system_qty),
|
|
|
|
| 510 |
stock_take_master_id=str(master.stock_take_master_id),
|
| 511 |
stock_take_number=master.stock_take_number,
|
| 512 |
merchant_id=master.merchant_id,
|
| 513 |
+
warehouse_id=master.warehouse_id,
|
| 514 |
stock_take_date=master.stock_take_date,
|
| 515 |
description=master.description,
|
| 516 |
additional_notes=master.additional_notes,
|
|
|
|
| 556 |
elif filters.get("merchant_id"):
|
| 557 |
conditions.append(ScmStockTakeMaster.merchant_id == filters["merchant_id"])
|
| 558 |
|
| 559 |
+
if filters.get("warehouse_id"):
|
| 560 |
+
conditions.append(ScmStockTakeMaster.warehouse_id == filters["warehouse_id"])
|
| 561 |
if filters.get("status"):
|
| 562 |
conditions.append(ScmStockTakeMaster.status == filters["status"])
|
| 563 |
if filters.get("stock_take_number"):
|
|
|
|
| 600 |
stock_take_master_id=str(master.stock_take_master_id),
|
| 601 |
stock_take_number=master.stock_take_number,
|
| 602 |
merchant_id=master.merchant_id,
|
| 603 |
+
warehouse_id=master.warehouse_id,
|
| 604 |
stock_take_date=master.stock_take_date,
|
| 605 |
description=master.description,
|
| 606 |
additional_notes=master.additional_notes,
|
app/purchases/orders/services/service.py
CHANGED
|
@@ -210,7 +210,7 @@ class OrdersService:
|
|
| 210 |
poi.tax_amt,
|
| 211 |
poi.created_at,
|
| 212 |
s.batch_no,
|
| 213 |
-
s.
|
| 214 |
cr.catalogue_name
|
| 215 |
FROM trans.scm_po_item poi
|
| 216 |
LEFT JOIN trans.catalogue_ref cr ON poi.catalogue_id = cr.catalogue_id
|
|
|
|
| 210 |
poi.tax_amt,
|
| 211 |
poi.created_at,
|
| 212 |
s.batch_no,
|
| 213 |
+
s.warehouse_id as warehouse_id,
|
| 214 |
cr.catalogue_name
|
| 215 |
FROM trans.scm_po_item poi
|
| 216 |
LEFT JOIN trans.catalogue_ref cr ON poi.catalogue_id = cr.catalogue_id
|
app/sql/apply_bulk_stock_movements.sql
CHANGED
|
@@ -23,7 +23,7 @@ BEGIN
|
|
| 23 |
-- Apply individual stock movement using the 9-parameter function
|
| 24 |
PERFORM trans.apply_stock_movement(
|
| 25 |
(v_movement->>'merchant_id')::TEXT,
|
| 26 |
-
(v_movement->>'
|
| 27 |
(v_movement->>'sku')::TEXT,
|
| 28 |
(v_movement->>'batch_no')::TEXT,
|
| 29 |
(v_movement->>'catalogue_id')::TEXT,
|
|
|
|
| 23 |
-- Apply individual stock movement using the 9-parameter function
|
| 24 |
PERFORM trans.apply_stock_movement(
|
| 25 |
(v_movement->>'merchant_id')::TEXT,
|
| 26 |
+
(v_movement->>'warehouse_id')::TEXT,
|
| 27 |
(v_movement->>'sku')::TEXT,
|
| 28 |
(v_movement->>'batch_no')::TEXT,
|
| 29 |
(v_movement->>'catalogue_id')::TEXT,
|
app/sql/apply_stock_movement.sql
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
|
| 5 |
CREATE OR REPLACE FUNCTION trans.apply_stock_movement(
|
| 6 |
p_merchant_id text,
|
| 7 |
-
|
| 8 |
p_sku text,
|
| 9 |
p_batch_no text,
|
| 10 |
p_catalogue_id text,
|
|
@@ -67,7 +67,7 @@ BEGIN
|
|
| 67 |
RAISE NOTICE 'Inserting ledger: %, %, %, %, %, %, %, %, %, %, %',
|
| 68 |
v_ledger_id,
|
| 69 |
p_merchant_id,
|
| 70 |
-
|
| 71 |
p_sku,
|
| 72 |
p_batch_no,
|
| 73 |
p_txn_type,
|
|
@@ -83,7 +83,7 @@ BEGIN
|
|
| 83 |
INSERT INTO trans.scm_stock_ledger (
|
| 84 |
ledger_id,
|
| 85 |
merchant_id,
|
| 86 |
-
|
| 87 |
sku,
|
| 88 |
batch_no,
|
| 89 |
txn_type,
|
|
@@ -95,7 +95,7 @@ BEGIN
|
|
| 95 |
) VALUES (
|
| 96 |
v_ledger_id,
|
| 97 |
p_merchant_id,
|
| 98 |
-
|
| 99 |
p_sku,
|
| 100 |
p_batch_no,
|
| 101 |
p_txn_type,
|
|
@@ -113,7 +113,7 @@ BEGIN
|
|
| 113 |
----------------------------------------------------------------
|
| 114 |
INSERT INTO trans.scm_stock (
|
| 115 |
merchant_id,
|
| 116 |
-
|
| 117 |
sku,
|
| 118 |
batch_no,
|
| 119 |
catalogue_id,
|
|
@@ -128,7 +128,7 @@ BEGIN
|
|
| 128 |
updated_at
|
| 129 |
) VALUES (
|
| 130 |
p_merchant_id,
|
| 131 |
-
|
| 132 |
p_sku,
|
| 133 |
p_batch_no,
|
| 134 |
p_catalogue_id,
|
|
@@ -142,7 +142,7 @@ BEGIN
|
|
| 142 |
NOW(),
|
| 143 |
NOW()
|
| 144 |
)
|
| 145 |
-
ON CONFLICT (merchant_id,
|
| 146 |
DO UPDATE SET
|
| 147 |
qty_on_hand = GREATEST(trans.scm_stock.qty_on_hand + v_qty_change, 0),
|
| 148 |
qty_available = GREATEST((trans.scm_stock.qty_on_hand + v_qty_change) - trans.scm_stock.qty_reserved, 0),
|
|
|
|
| 4 |
|
| 5 |
CREATE OR REPLACE FUNCTION trans.apply_stock_movement(
|
| 6 |
p_merchant_id text,
|
| 7 |
+
p_warehouse_id text,
|
| 8 |
p_sku text,
|
| 9 |
p_batch_no text,
|
| 10 |
p_catalogue_id text,
|
|
|
|
| 67 |
RAISE NOTICE 'Inserting ledger: %, %, %, %, %, %, %, %, %, %, %',
|
| 68 |
v_ledger_id,
|
| 69 |
p_merchant_id,
|
| 70 |
+
p_warehouse_id,
|
| 71 |
p_sku,
|
| 72 |
p_batch_no,
|
| 73 |
p_txn_type,
|
|
|
|
| 83 |
INSERT INTO trans.scm_stock_ledger (
|
| 84 |
ledger_id,
|
| 85 |
merchant_id,
|
| 86 |
+
warehouse_id,
|
| 87 |
sku,
|
| 88 |
batch_no,
|
| 89 |
txn_type,
|
|
|
|
| 95 |
) VALUES (
|
| 96 |
v_ledger_id,
|
| 97 |
p_merchant_id,
|
| 98 |
+
p_warehouse_id,
|
| 99 |
p_sku,
|
| 100 |
p_batch_no,
|
| 101 |
p_txn_type,
|
|
|
|
| 113 |
----------------------------------------------------------------
|
| 114 |
INSERT INTO trans.scm_stock (
|
| 115 |
merchant_id,
|
| 116 |
+
warehouse_id,
|
| 117 |
sku,
|
| 118 |
batch_no,
|
| 119 |
catalogue_id,
|
|
|
|
| 128 |
updated_at
|
| 129 |
) VALUES (
|
| 130 |
p_merchant_id,
|
| 131 |
+
p_warehouse_id,
|
| 132 |
p_sku,
|
| 133 |
p_batch_no,
|
| 134 |
p_catalogue_id,
|
|
|
|
| 142 |
NOW(),
|
| 143 |
NOW()
|
| 144 |
)
|
| 145 |
+
ON CONFLICT (merchant_id, warehouse_id, catalogue_id, batch_no)
|
| 146 |
DO UPDATE SET
|
| 147 |
qty_on_hand = GREATEST(trans.scm_stock.qty_on_hand + v_qty_change, 0),
|
| 148 |
qty_available = GREATEST((trans.scm_stock.qty_on_hand + v_qty_change) - trans.scm_stock.qty_reserved, 0),
|
app/sql/fn_get_po_items_for_order_process.sql
CHANGED
|
@@ -34,7 +34,7 @@ BEGIN
|
|
| 34 |
ON po.catalogue_id = cr.catalogue_id
|
| 35 |
JOIN trans.scm_stock st
|
| 36 |
ON st.catalogue_id = po.catalogue_id
|
| 37 |
-
AND st.
|
| 38 |
WHERE po.po_id = p_po_id
|
| 39 |
AND (po.ord_qty - coalesce(po.dispatched_qty,0)) > 0
|
| 40 |
AND st.qty_available > 0
|
|
|
|
| 34 |
ON po.catalogue_id = cr.catalogue_id
|
| 35 |
JOIN trans.scm_stock st
|
| 36 |
ON st.catalogue_id = po.catalogue_id
|
| 37 |
+
AND st.warehouse_id = p_warehouse_id
|
| 38 |
WHERE po.po_id = p_po_id
|
| 39 |
AND (po.ord_qty - coalesce(po.dispatched_qty,0)) > 0
|
| 40 |
AND st.qty_available > 0
|
app/sql/fn_get_po_items_for_purchase_return.sql
CHANGED
|
@@ -37,7 +37,7 @@ BEGIN
|
|
| 37 |
ON cr.catalogue_id = pi.catalogue_id
|
| 38 |
JOIN trans.scm_stock st
|
| 39 |
ON st.catalogue_id = pi.catalogue_id
|
| 40 |
-
AND st.
|
| 41 |
|
| 42 |
WHERE pi.po_id = p_po_id
|
| 43 |
AND st.qty_available > 0
|
|
|
|
| 37 |
ON cr.catalogue_id = pi.catalogue_id
|
| 38 |
JOIN trans.scm_stock st
|
| 39 |
ON st.catalogue_id = pi.catalogue_id
|
| 40 |
+
AND st.warehouse_id = p_warehouse_id
|
| 41 |
|
| 42 |
WHERE pi.po_id = p_po_id
|
| 43 |
AND st.qty_available > 0
|
app/sql/get_stock_ledger.sql
CHANGED
|
@@ -7,7 +7,7 @@ CREATE OR REPLACE FUNCTION trans.get_stock_ledger(
|
|
| 7 |
p_sku character varying DEFAULT NULL::character varying,
|
| 8 |
p_batch_no character varying DEFAULT NULL::character varying,
|
| 9 |
p_limit integer DEFAULT 100)
|
| 10 |
-
RETURNS TABLE(ledger_id uuid,
|
| 11 |
LANGUAGE 'plpgsql'
|
| 12 |
COST 100
|
| 13 |
VOLATILE PARALLEL UNSAFE
|
|
@@ -16,7 +16,7 @@ CREATE OR REPLACE FUNCTION trans.get_stock_ledger(
|
|
| 16 |
AS $BODY$
|
| 17 |
BEGIN
|
| 18 |
RETURN QUERY
|
| 19 |
-
SELECT l.ledger_id, l.
|
| 20 |
l.txn_type, l.qty, l.uom, l.ref_type, l.ref_id, l.ref_no,
|
| 21 |
l.remarks, l.created_by, l.created_at
|
| 22 |
FROM trans.scm_stock_ledger l
|
|
|
|
| 7 |
p_sku character varying DEFAULT NULL::character varying,
|
| 8 |
p_batch_no character varying DEFAULT NULL::character varying,
|
| 9 |
p_limit integer DEFAULT 100)
|
| 10 |
+
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)
|
| 11 |
LANGUAGE 'plpgsql'
|
| 12 |
COST 100
|
| 13 |
VOLATILE PARALLEL UNSAFE
|
|
|
|
| 16 |
AS $BODY$
|
| 17 |
BEGIN
|
| 18 |
RETURN QUERY
|
| 19 |
+
SELECT l.ledger_id, l.warehouse_id, l.catalogue_id, l.sku, l.batch_no,
|
| 20 |
l.txn_type, l.qty, l.uom, l.ref_type, l.ref_id, l.ref_no,
|
| 21 |
l.remarks, l.created_by, l.created_at
|
| 22 |
FROM trans.scm_stock_ledger l
|
app/sql/get_stock_summary.sql
CHANGED
|
@@ -4,9 +4,9 @@
|
|
| 4 |
|
| 5 |
CREATE OR REPLACE FUNCTION trans.get_stock_summary(
|
| 6 |
p_merchant_id character varying,
|
| 7 |
-
|
| 8 |
p_sku character varying DEFAULT NULL::character varying)
|
| 9 |
-
RETURNS TABLE(stock_id uuid,
|
| 10 |
LANGUAGE 'plpgsql'
|
| 11 |
COST 100
|
| 12 |
VOLATILE PARALLEL UNSAFE
|
|
@@ -15,11 +15,11 @@ CREATE OR REPLACE FUNCTION trans.get_stock_summary(
|
|
| 15 |
AS $BODY$
|
| 16 |
BEGIN
|
| 17 |
RETURN QUERY
|
| 18 |
-
SELECT s.stock_id, s.
|
| 19 |
s.qty_on_hand, s.qty_reserved, s.qty_available, s.uom, s.last_updated_at
|
| 20 |
FROM trans.scm_stock s
|
| 21 |
WHERE s.merchant_id = p_merchant_id
|
| 22 |
-
AND (
|
| 23 |
AND (p_sku IS NULL OR s.sku = p_sku)
|
| 24 |
ORDER BY s.sku, s.batch_no;
|
| 25 |
END;
|
|
|
|
| 4 |
|
| 5 |
CREATE OR REPLACE FUNCTION trans.get_stock_summary(
|
| 6 |
p_merchant_id character varying,
|
| 7 |
+
p_warehouse_id character varying DEFAULT NULL::character varying,
|
| 8 |
p_sku character varying DEFAULT NULL::character varying)
|
| 9 |
+
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, last_updated_at timestamp without time zone)
|
| 10 |
LANGUAGE 'plpgsql'
|
| 11 |
COST 100
|
| 12 |
VOLATILE PARALLEL UNSAFE
|
|
|
|
| 15 |
AS $BODY$
|
| 16 |
BEGIN
|
| 17 |
RETURN QUERY
|
| 18 |
+
SELECT s.stock_id, s.warehouse_id, s.catalogue_id, s.sku, s.batch_no,
|
| 19 |
s.qty_on_hand, s.qty_reserved, s.qty_available, s.uom, s.last_updated_at
|
| 20 |
FROM trans.scm_stock s
|
| 21 |
WHERE s.merchant_id = p_merchant_id
|
| 22 |
+
AND (p_warehouse_id IS NULL OR s.warehouse_id = p_warehouse_id)
|
| 23 |
AND (p_sku IS NULL OR s.sku = p_sku)
|
| 24 |
ORDER BY s.sku, s.batch_no;
|
| 25 |
END;
|
app/sql/release_stock_reservation.sql
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
|
| 5 |
CREATE OR REPLACE FUNCTION trans.release_stock_reservation(
|
| 6 |
p_merchant_id character varying,
|
| 7 |
-
|
| 8 |
p_sku character varying,
|
| 9 |
p_batch_no character varying,
|
| 10 |
p_qty numeric,
|
|
@@ -22,7 +22,7 @@ BEGIN
|
|
| 22 |
qty_available = qty_on_hand - GREATEST(qty_reserved - p_qty, 0),
|
| 23 |
last_updated_at = NOW()
|
| 24 |
WHERE merchant_id = p_merchant_id
|
| 25 |
-
AND
|
| 26 |
AND sku = p_sku
|
| 27 |
AND batch_no = p_batch_no;
|
| 28 |
|
|
|
|
| 4 |
|
| 5 |
CREATE OR REPLACE FUNCTION trans.release_stock_reservation(
|
| 6 |
p_merchant_id character varying,
|
| 7 |
+
p_warehouse_id character varying,
|
| 8 |
p_sku character varying,
|
| 9 |
p_batch_no character varying,
|
| 10 |
p_qty numeric,
|
|
|
|
| 22 |
qty_available = qty_on_hand - GREATEST(qty_reserved - p_qty, 0),
|
| 23 |
last_updated_at = NOW()
|
| 24 |
WHERE merchant_id = p_merchant_id
|
| 25 |
+
AND warehouse_id = p_warehouse_id
|
| 26 |
AND sku = p_sku
|
| 27 |
AND batch_no = p_batch_no;
|
| 28 |
|
app/sql/reserve_stock.sql
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
|
| 5 |
CREATE OR REPLACE FUNCTION trans.reserve_stock(
|
| 6 |
p_merchant_id character varying,
|
| 7 |
-
|
| 8 |
p_sku character varying,
|
| 9 |
p_batch_no character varying,
|
| 10 |
p_qty numeric,
|
|
@@ -23,7 +23,7 @@ BEGIN
|
|
| 23 |
INTO v_available_qty
|
| 24 |
FROM trans.scm_stock
|
| 25 |
WHERE merchant_id = p_merchant_id
|
| 26 |
-
AND
|
| 27 |
AND sku = p_sku
|
| 28 |
AND batch_no = p_batch_no;
|
| 29 |
|
|
@@ -39,7 +39,7 @@ BEGIN
|
|
| 39 |
qty_available = qty_available - p_qty,
|
| 40 |
last_updated_at = NOW()
|
| 41 |
WHERE merchant_id = p_merchant_id
|
| 42 |
-
AND
|
| 43 |
AND sku = p_sku
|
| 44 |
AND batch_no = p_batch_no;
|
| 45 |
|
|
|
|
| 4 |
|
| 5 |
CREATE OR REPLACE FUNCTION trans.reserve_stock(
|
| 6 |
p_merchant_id character varying,
|
| 7 |
+
p_warehouse_id character varying,
|
| 8 |
p_sku character varying,
|
| 9 |
p_batch_no character varying,
|
| 10 |
p_qty numeric,
|
|
|
|
| 23 |
INTO v_available_qty
|
| 24 |
FROM trans.scm_stock
|
| 25 |
WHERE merchant_id = p_merchant_id
|
| 26 |
+
AND warehouse_id = p_warehouse_id
|
| 27 |
AND sku = p_sku
|
| 28 |
AND batch_no = p_batch_no;
|
| 29 |
|
|
|
|
| 39 |
qty_available = qty_available - p_qty,
|
| 40 |
last_updated_at = NOW()
|
| 41 |
WHERE merchant_id = p_merchant_id
|
| 42 |
+
AND warehouse_id = p_warehouse_id
|
| 43 |
AND sku = p_sku
|
| 44 |
AND batch_no = p_batch_no;
|
| 45 |
|
app/trade_sales/services/service.py
CHANGED
|
@@ -492,7 +492,7 @@ class TradeSalesService:
|
|
| 492 |
# Prepare stock transaction (outbound from supplier)
|
| 493 |
stock_transaction = StockTransaction(
|
| 494 |
merchant_id=supplier_id,
|
| 495 |
-
|
| 496 |
catalogue_id=str(order_item.catalogue_id),
|
| 497 |
sku=ship_item.sku,
|
| 498 |
batch_no=ship_item.batch_no,
|
|
|
|
| 492 |
# Prepare stock transaction (outbound from supplier)
|
| 493 |
stock_transaction = StockTransaction(
|
| 494 |
merchant_id=supplier_id,
|
| 495 |
+
warehouse_id=str(request.warehouse_id),
|
| 496 |
catalogue_id=str(order_item.catalogue_id),
|
| 497 |
sku=ship_item.sku,
|
| 498 |
batch_no=ship_item.batch_no,
|
app/transports/controllers/router.py
CHANGED
|
@@ -54,7 +54,7 @@ async def create_transport(
|
|
| 54 |
- tracking_url: Tracking URL template with {awb} placeholder
|
| 55 |
- supports_cod: Cash on Delivery support (default: false)
|
| 56 |
- supports_tracking: Tracking support (default: false)
|
| 57 |
-
-
|
| 58 |
- serviceable_regions: List of serviceable regions
|
| 59 |
- notes: Additional notes
|
| 60 |
|
|
@@ -193,7 +193,7 @@ async def list_transports(
|
|
| 193 |
**Filters:**
|
| 194 |
- type: Filter by transport type (courier, internal, freight)
|
| 195 |
- status: Filter by status (active, inactive, draft)
|
| 196 |
-
-
|
| 197 |
- search: Search in transport name or code
|
| 198 |
|
| 199 |
**Pagination:**
|
|
@@ -216,7 +216,7 @@ async def list_transports(
|
|
| 216 |
merchant_id=current_user.merchant_id, # Use merchant_id from JWT token
|
| 217 |
transport_type=payload.type,
|
| 218 |
status=payload.status,
|
| 219 |
-
|
| 220 |
search=payload.search,
|
| 221 |
skip=payload.skip,
|
| 222 |
limit=payload.limit,
|
|
|
|
| 54 |
- tracking_url: Tracking URL template with {awb} placeholder
|
| 55 |
- supports_cod: Cash on Delivery support (default: false)
|
| 56 |
- supports_tracking: Tracking support (default: false)
|
| 57 |
+
- warehouse_ids: Location IDs (required if location_scope is 'selected')
|
| 58 |
- serviceable_regions: List of serviceable regions
|
| 59 |
- notes: Additional notes
|
| 60 |
|
|
|
|
| 193 |
**Filters:**
|
| 194 |
- type: Filter by transport type (courier, internal, freight)
|
| 195 |
- status: Filter by status (active, inactive, draft)
|
| 196 |
+
- warehouse_id: Filter by location ID (shows transports with scope 'all' or containing this location)
|
| 197 |
- search: Search in transport name or code
|
| 198 |
|
| 199 |
**Pagination:**
|
|
|
|
| 216 |
merchant_id=current_user.merchant_id, # Use merchant_id from JWT token
|
| 217 |
transport_type=payload.type,
|
| 218 |
status=payload.status,
|
| 219 |
+
warehouse_id=payload.warehouse_id,
|
| 220 |
search=payload.search,
|
| 221 |
skip=payload.skip,
|
| 222 |
limit=payload.limit,
|
app/transports/models/model.py
CHANGED
|
@@ -28,7 +28,7 @@ class TransportModel(BaseModel):
|
|
| 28 |
|
| 29 |
# Coverage
|
| 30 |
location_scope: str = Field(..., description="Location scope (all, selected)")
|
| 31 |
-
|
| 32 |
serviceable_regions: List[str] = Field(default_factory=list, description="Serviceable regions")
|
| 33 |
|
| 34 |
# Status and Audit
|
|
@@ -56,7 +56,7 @@ class TransportModel(BaseModel):
|
|
| 56 |
"supports_cod": True,
|
| 57 |
"supports_tracking": True,
|
| 58 |
"location_scope": "all",
|
| 59 |
-
"
|
| 60 |
"serviceable_regions": ["Mumbai", "Delhi"],
|
| 61 |
"status": "active",
|
| 62 |
"notes": "Primary courier partner",
|
|
|
|
| 28 |
|
| 29 |
# Coverage
|
| 30 |
location_scope: str = Field(..., description="Location scope (all, selected)")
|
| 31 |
+
warehouse_ids: List[str] = Field(default_factory=list, description="Location IDs if scope is 'selected'")
|
| 32 |
serviceable_regions: List[str] = Field(default_factory=list, description="Serviceable regions")
|
| 33 |
|
| 34 |
# Status and Audit
|
|
|
|
| 56 |
"supports_cod": True,
|
| 57 |
"supports_tracking": True,
|
| 58 |
"location_scope": "all",
|
| 59 |
+
"warehouse_ids": [],
|
| 60 |
"serviceable_regions": ["Mumbai", "Delhi"],
|
| 61 |
"status": "active",
|
| 62 |
"notes": "Primary courier partner",
|
app/transports/schemas/schema.py
CHANGED
|
@@ -24,7 +24,7 @@ class TransportCreate(BaseModel):
|
|
| 24 |
|
| 25 |
# Coverage
|
| 26 |
location_scope: str = Field(..., description="Location scope (all, selected)")
|
| 27 |
-
|
| 28 |
serviceable_regions: Optional[List[str]] = Field(None, description="Serviceable regions")
|
| 29 |
|
| 30 |
notes: Optional[str] = Field(None, description="Additional notes")
|
|
@@ -108,7 +108,7 @@ class TransportCreate(BaseModel):
|
|
| 108 |
"supports_cod": True,
|
| 109 |
"supports_tracking": True,
|
| 110 |
"location_scope": "all",
|
| 111 |
-
"
|
| 112 |
"serviceable_regions": ["Mumbai", "Delhi"],
|
| 113 |
"notes": "Primary courier partner"
|
| 114 |
}
|
|
@@ -130,7 +130,7 @@ class TransportUpdate(BaseModel):
|
|
| 130 |
supports_tracking: Optional[bool] = Field(None, description="Tracking support")
|
| 131 |
|
| 132 |
location_scope: Optional[str] = Field(None, description="Location scope")
|
| 133 |
-
|
| 134 |
serviceable_regions: Optional[List[str]] = Field(None, description="Serviceable regions")
|
| 135 |
|
| 136 |
notes: Optional[str] = Field(None, description="Additional notes")
|
|
@@ -214,7 +214,7 @@ class TransportResponse(BaseModel):
|
|
| 214 |
supports_tracking: bool = False
|
| 215 |
|
| 216 |
location_scope: str
|
| 217 |
-
|
| 218 |
serviceable_regions: List[str] = Field(default_factory=list)
|
| 219 |
|
| 220 |
status: str
|
|
@@ -229,7 +229,7 @@ class TransportListRequest(BaseModel):
|
|
| 229 |
"""Schema for listing transports with filters"""
|
| 230 |
type: Optional[str] = Field(None, description="Filter by transport type")
|
| 231 |
status: Optional[str] = Field(None, description="Filter by status (active/inactive)")
|
| 232 |
-
|
| 233 |
search: Optional[str] = Field(None, description="Search in name or code")
|
| 234 |
skip: int = Field(0, ge=0, description="Pagination offset")
|
| 235 |
limit: int = Field(100, ge=1, le=500, description="Items per page")
|
|
|
|
| 24 |
|
| 25 |
# Coverage
|
| 26 |
location_scope: str = Field(..., description="Location scope (all, selected)")
|
| 27 |
+
warehouse_ids: Optional[List[str]] = Field(None, description="Location IDs if scope is 'selected'")
|
| 28 |
serviceable_regions: Optional[List[str]] = Field(None, description="Serviceable regions")
|
| 29 |
|
| 30 |
notes: Optional[str] = Field(None, description="Additional notes")
|
|
|
|
| 108 |
"supports_cod": True,
|
| 109 |
"supports_tracking": True,
|
| 110 |
"location_scope": "all",
|
| 111 |
+
"warehouse_ids": None,
|
| 112 |
"serviceable_regions": ["Mumbai", "Delhi"],
|
| 113 |
"notes": "Primary courier partner"
|
| 114 |
}
|
|
|
|
| 130 |
supports_tracking: Optional[bool] = Field(None, description="Tracking support")
|
| 131 |
|
| 132 |
location_scope: Optional[str] = Field(None, description="Location scope")
|
| 133 |
+
warehouse_ids: Optional[List[str]] = Field(None, description="Location IDs")
|
| 134 |
serviceable_regions: Optional[List[str]] = Field(None, description="Serviceable regions")
|
| 135 |
|
| 136 |
notes: Optional[str] = Field(None, description="Additional notes")
|
|
|
|
| 214 |
supports_tracking: bool = False
|
| 215 |
|
| 216 |
location_scope: str
|
| 217 |
+
warehouse_ids: List[str] = Field(default_factory=list)
|
| 218 |
serviceable_regions: List[str] = Field(default_factory=list)
|
| 219 |
|
| 220 |
status: str
|
|
|
|
| 229 |
"""Schema for listing transports with filters"""
|
| 230 |
type: Optional[str] = Field(None, description="Filter by transport type")
|
| 231 |
status: Optional[str] = Field(None, description="Filter by status (active/inactive)")
|
| 232 |
+
warehouse_id: Optional[str] = Field(None, description="Filter by location ID")
|
| 233 |
search: Optional[str] = Field(None, description="Search in name or code")
|
| 234 |
skip: int = Field(0, ge=0, description="Pagination offset")
|
| 235 |
limit: int = Field(100, ge=1, le=500, description="Items per page")
|
app/transports/services/service.py
CHANGED
|
@@ -106,11 +106,11 @@ class TransportService:
|
|
| 106 |
detail=f"Transport code {payload.code} already exists for this merchant"
|
| 107 |
)
|
| 108 |
|
| 109 |
-
# 3) Validate
|
| 110 |
-
if payload.location_scope == "selected" and (not payload.
|
| 111 |
raise HTTPException(
|
| 112 |
status_code=status.HTTP_400_BAD_REQUEST,
|
| 113 |
-
detail="
|
| 114 |
)
|
| 115 |
|
| 116 |
# 4) Create transport model with timestamps
|
|
@@ -128,7 +128,7 @@ class TransportService:
|
|
| 128 |
# Set defaults for optional fields
|
| 129 |
transport_data["supports_cod"] = transport_data.get("supports_cod", False)
|
| 130 |
transport_data["supports_tracking"] = transport_data.get("supports_tracking", False)
|
| 131 |
-
transport_data["
|
| 132 |
transport_data["serviceable_regions"] = transport_data.get("serviceable_regions", [])
|
| 133 |
|
| 134 |
transport_model = TransportModel(**transport_data)
|
|
@@ -203,13 +203,13 @@ class TransportService:
|
|
| 203 |
update_data["updated_by"] = payload.updated_by
|
| 204 |
update_data["updated_at"] = now.isoformat()
|
| 205 |
|
| 206 |
-
# 4) Validate
|
| 207 |
if update_data.get("location_scope") == "selected":
|
| 208 |
-
|
| 209 |
-
if not
|
| 210 |
raise HTTPException(
|
| 211 |
status_code=status.HTTP_400_BAD_REQUEST,
|
| 212 |
-
detail="
|
| 213 |
)
|
| 214 |
|
| 215 |
# 5) Update in database
|
|
@@ -249,7 +249,7 @@ class TransportService:
|
|
| 249 |
merchant_id: Optional[str] = None,
|
| 250 |
transport_type: Optional[str] = None,
|
| 251 |
status: Optional[str] = None,
|
| 252 |
-
|
| 253 |
search: Optional[str] = None,
|
| 254 |
skip: int = 0,
|
| 255 |
limit: int = 100,
|
|
@@ -262,7 +262,7 @@ class TransportService:
|
|
| 262 |
merchant_id: Filter by merchant ID
|
| 263 |
transport_type: Filter by transport type
|
| 264 |
status: Filter by status
|
| 265 |
-
|
| 266 |
search: Search in name or code
|
| 267 |
skip: Pagination offset
|
| 268 |
limit: Page size
|
|
@@ -280,10 +280,10 @@ class TransportService:
|
|
| 280 |
query["type"] = transport_type.lower()
|
| 281 |
if status:
|
| 282 |
query["status"] = status.lower()
|
| 283 |
-
if
|
| 284 |
query["$or"] = [
|
| 285 |
{"location_scope": "all"},
|
| 286 |
-
{"
|
| 287 |
]
|
| 288 |
if search:
|
| 289 |
query["$or"] = [
|
|
@@ -314,7 +314,7 @@ class TransportService:
|
|
| 314 |
"filters": {
|
| 315 |
"type": transport_type,
|
| 316 |
"status": status,
|
| 317 |
-
"
|
| 318 |
"search": search
|
| 319 |
}
|
| 320 |
})
|
|
|
|
| 106 |
detail=f"Transport code {payload.code} already exists for this merchant"
|
| 107 |
)
|
| 108 |
|
| 109 |
+
# 3) Validate warehouse_ids if location_scope is 'selected'
|
| 110 |
+
if payload.location_scope == "selected" and (not payload.warehouse_ids or len(payload.warehouse_ids) == 0):
|
| 111 |
raise HTTPException(
|
| 112 |
status_code=status.HTTP_400_BAD_REQUEST,
|
| 113 |
+
detail="warehouse_ids required when location_scope is 'selected'"
|
| 114 |
)
|
| 115 |
|
| 116 |
# 4) Create transport model with timestamps
|
|
|
|
| 128 |
# Set defaults for optional fields
|
| 129 |
transport_data["supports_cod"] = transport_data.get("supports_cod", False)
|
| 130 |
transport_data["supports_tracking"] = transport_data.get("supports_tracking", False)
|
| 131 |
+
transport_data["warehouse_ids"] = transport_data.get("warehouse_ids", [])
|
| 132 |
transport_data["serviceable_regions"] = transport_data.get("serviceable_regions", [])
|
| 133 |
|
| 134 |
transport_model = TransportModel(**transport_data)
|
|
|
|
| 203 |
update_data["updated_by"] = payload.updated_by
|
| 204 |
update_data["updated_at"] = now.isoformat()
|
| 205 |
|
| 206 |
+
# 4) Validate warehouse_ids if location_scope is being updated to 'selected'
|
| 207 |
if update_data.get("location_scope") == "selected":
|
| 208 |
+
warehouse_ids = update_data.get("warehouse_ids") or existing.get("warehouse_ids", [])
|
| 209 |
+
if not warehouse_ids or len(warehouse_ids) == 0:
|
| 210 |
raise HTTPException(
|
| 211 |
status_code=status.HTTP_400_BAD_REQUEST,
|
| 212 |
+
detail="warehouse_ids required when location_scope is 'selected'"
|
| 213 |
)
|
| 214 |
|
| 215 |
# 5) Update in database
|
|
|
|
| 249 |
merchant_id: Optional[str] = None,
|
| 250 |
transport_type: Optional[str] = None,
|
| 251 |
status: Optional[str] = None,
|
| 252 |
+
warehouse_id: Optional[str] = None,
|
| 253 |
search: Optional[str] = None,
|
| 254 |
skip: int = 0,
|
| 255 |
limit: int = 100,
|
|
|
|
| 262 |
merchant_id: Filter by merchant ID
|
| 263 |
transport_type: Filter by transport type
|
| 264 |
status: Filter by status
|
| 265 |
+
warehouse_id: Filter by location ID
|
| 266 |
search: Search in name or code
|
| 267 |
skip: Pagination offset
|
| 268 |
limit: Page size
|
|
|
|
| 280 |
query["type"] = transport_type.lower()
|
| 281 |
if status:
|
| 282 |
query["status"] = status.lower()
|
| 283 |
+
if warehouse_id:
|
| 284 |
query["$or"] = [
|
| 285 |
{"location_scope": "all"},
|
| 286 |
+
{"warehouse_ids": warehouse_id}
|
| 287 |
]
|
| 288 |
if search:
|
| 289 |
query["$or"] = [
|
|
|
|
| 314 |
"filters": {
|
| 315 |
"type": transport_type,
|
| 316 |
"status": status,
|
| 317 |
+
"warehouse_id": warehouse_id,
|
| 318 |
"search": search
|
| 319 |
}
|
| 320 |
})
|
app/utils/stock_utils.py
CHANGED
|
@@ -36,7 +36,7 @@ async def process_grn_stock_update(
|
|
| 36 |
async def process_stock_adjustment(
|
| 37 |
db: AsyncSession,
|
| 38 |
merchant_id: str,
|
| 39 |
-
|
| 40 |
sku: str,
|
| 41 |
batch_no: str,
|
| 42 |
adjustment_qty: float,
|
|
@@ -53,7 +53,7 @@ async def process_stock_adjustment(
|
|
| 53 |
|
| 54 |
# Process adjustment
|
| 55 |
ledger_id, success = await process_stock_adjustment(
|
| 56 |
-
db, merchant_id,
|
| 57 |
adjustment_qty, "damage", "Damaged goods", user_id, adjustment_id
|
| 58 |
)
|
| 59 |
|
|
@@ -65,7 +65,7 @@ async def process_stock_adjustment(
|
|
| 65 |
stock_service = StockService(db)
|
| 66 |
return await stock_service.process_stock_adjustment(
|
| 67 |
merchant_id=merchant_id,
|
| 68 |
-
|
| 69 |
sku=sku,
|
| 70 |
batch_no=batch_no,
|
| 71 |
adjustment_qty=Decimal(str(adjustment_qty)),
|
|
@@ -79,7 +79,7 @@ async def process_stock_adjustment(
|
|
| 79 |
async def get_current_stock(
|
| 80 |
db: AsyncSession,
|
| 81 |
merchant_id: str,
|
| 82 |
-
|
| 83 |
sku: str = None
|
| 84 |
) -> List[dict]:
|
| 85 |
"""
|
|
@@ -92,13 +92,13 @@ async def get_current_stock(
|
|
| 92 |
stock = await get_current_stock(db, merchant_id)
|
| 93 |
|
| 94 |
# Get stock for specific location
|
| 95 |
-
stock = await get_current_stock(db, merchant_id,
|
| 96 |
|
| 97 |
# Get stock for specific SKU
|
| 98 |
stock = await get_current_stock(db, merchant_id, sku="PROD001")
|
| 99 |
"""
|
| 100 |
stock_service = StockService(db)
|
| 101 |
-
return await stock_service.get_stock_summary(merchant_id,
|
| 102 |
|
| 103 |
|
| 104 |
async def get_stock_history(
|
|
@@ -127,7 +127,7 @@ async def get_stock_history(
|
|
| 127 |
async def reserve_stock(
|
| 128 |
db: AsyncSession,
|
| 129 |
merchant_id: str,
|
| 130 |
-
|
| 131 |
sku: str,
|
| 132 |
batch_no: str,
|
| 133 |
qty: float,
|
|
@@ -141,14 +141,14 @@ async def reserve_stock(
|
|
| 141 |
from app.utils.stock_utils import reserve_stock
|
| 142 |
|
| 143 |
# Reserve stock for sales order
|
| 144 |
-
success = await reserve_stock(db, merchant_id,
|
| 145 |
batch_no, qty, sales_order_id, user_id)
|
| 146 |
"""
|
| 147 |
from decimal import Decimal
|
| 148 |
|
| 149 |
stock_service = StockService(db)
|
| 150 |
return await stock_service.reserve_stock(
|
| 151 |
-
merchant_id,
|
| 152 |
Decimal(str(qty)), ref_id, created_by
|
| 153 |
)
|
| 154 |
|
|
@@ -156,7 +156,7 @@ async def reserve_stock(
|
|
| 156 |
async def release_stock_reservation(
|
| 157 |
db: AsyncSession,
|
| 158 |
merchant_id: str,
|
| 159 |
-
|
| 160 |
sku: str,
|
| 161 |
batch_no: str,
|
| 162 |
qty: float,
|
|
|
|
| 36 |
async def process_stock_adjustment(
|
| 37 |
db: AsyncSession,
|
| 38 |
merchant_id: str,
|
| 39 |
+
warehouse_id: str,
|
| 40 |
sku: str,
|
| 41 |
batch_no: str,
|
| 42 |
adjustment_qty: float,
|
|
|
|
| 53 |
|
| 54 |
# Process adjustment
|
| 55 |
ledger_id, success = await process_stock_adjustment(
|
| 56 |
+
db, merchant_id, warehouse_id, sku, batch_no,
|
| 57 |
adjustment_qty, "damage", "Damaged goods", user_id, adjustment_id
|
| 58 |
)
|
| 59 |
|
|
|
|
| 65 |
stock_service = StockService(db)
|
| 66 |
return await stock_service.process_stock_adjustment(
|
| 67 |
merchant_id=merchant_id,
|
| 68 |
+
warehouse_id=warehouse_id,
|
| 69 |
sku=sku,
|
| 70 |
batch_no=batch_no,
|
| 71 |
adjustment_qty=Decimal(str(adjustment_qty)),
|
|
|
|
| 79 |
async def get_current_stock(
|
| 80 |
db: AsyncSession,
|
| 81 |
merchant_id: str,
|
| 82 |
+
warehouse_id: str = None,
|
| 83 |
sku: str = None
|
| 84 |
) -> List[dict]:
|
| 85 |
"""
|
|
|
|
| 92 |
stock = await get_current_stock(db, merchant_id)
|
| 93 |
|
| 94 |
# Get stock for specific location
|
| 95 |
+
stock = await get_current_stock(db, merchant_id, warehouse_id="WH001")
|
| 96 |
|
| 97 |
# Get stock for specific SKU
|
| 98 |
stock = await get_current_stock(db, merchant_id, sku="PROD001")
|
| 99 |
"""
|
| 100 |
stock_service = StockService(db)
|
| 101 |
+
return await stock_service.get_stock_summary(merchant_id, warehouse_id, sku)
|
| 102 |
|
| 103 |
|
| 104 |
async def get_stock_history(
|
|
|
|
| 127 |
async def reserve_stock(
|
| 128 |
db: AsyncSession,
|
| 129 |
merchant_id: str,
|
| 130 |
+
warehouse_id: str,
|
| 131 |
sku: str,
|
| 132 |
batch_no: str,
|
| 133 |
qty: float,
|
|
|
|
| 141 |
from app.utils.stock_utils import reserve_stock
|
| 142 |
|
| 143 |
# Reserve stock for sales order
|
| 144 |
+
success = await reserve_stock(db, merchant_id, warehouse_id, sku,
|
| 145 |
batch_no, qty, sales_order_id, user_id)
|
| 146 |
"""
|
| 147 |
from decimal import Decimal
|
| 148 |
|
| 149 |
stock_service = StockService(db)
|
| 150 |
return await stock_service.reserve_stock(
|
| 151 |
+
merchant_id, warehouse_id, sku, batch_no,
|
| 152 |
Decimal(str(qty)), ref_id, created_by
|
| 153 |
)
|
| 154 |
|
|
|
|
| 156 |
async def release_stock_reservation(
|
| 157 |
db: AsyncSession,
|
| 158 |
merchant_id: str,
|
| 159 |
+
warehouse_id: str,
|
| 160 |
sku: str,
|
| 161 |
batch_no: str,
|
| 162 |
qty: float,
|
docs/database/sql/stored_procedures/stock_management.sql
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
-- Simplified stock movement function with idempotency
|
| 5 |
CREATE OR REPLACE FUNCTION apply_stock_movement(
|
| 6 |
p_merchant_id TEXT,
|
| 7 |
-
|
| 8 |
p_sku TEXT,
|
| 9 |
p_batch_no TEXT,
|
| 10 |
p_qty NUMERIC,
|
|
@@ -25,22 +25,22 @@ BEGIN
|
|
| 25 |
|
| 26 |
-- Insert ledger entry (immutable audit trail)
|
| 27 |
INSERT INTO scm_stock_ledger (
|
| 28 |
-
ledger_id, merchant_id,
|
| 29 |
qty, txn_type, ref_type, ref_id, ref_no, created_by
|
| 30 |
) VALUES (
|
| 31 |
-
gen_random_uuid(), p_merchant_id,
|
| 32 |
p_qty, p_txn_type, p_ref_type, p_ref_id, p_ref_no, p_user
|
| 33 |
);
|
| 34 |
|
| 35 |
-- Stock snapshot upsert (create or update)
|
| 36 |
INSERT INTO scm_stock (
|
| 37 |
-
stock_id, merchant_id,
|
| 38 |
qty_on_hand, qty_reserved, qty_available
|
| 39 |
) VALUES (
|
| 40 |
-
gen_random_uuid(), p_merchant_id,
|
| 41 |
p_qty, 0, p_qty
|
| 42 |
)
|
| 43 |
-
ON CONFLICT (merchant_id,
|
| 44 |
DO UPDATE SET
|
| 45 |
qty_on_hand = scm_stock.qty_on_hand + p_qty,
|
| 46 |
qty_available = scm_stock.qty_available + p_qty,
|
|
@@ -66,7 +66,7 @@ BEGIN
|
|
| 66 |
-- Apply individual stock movement
|
| 67 |
SELECT apply_stock_movement(
|
| 68 |
(v_movement->>'merchant_id')::VARCHAR(64),
|
| 69 |
-
(v_movement->>'
|
| 70 |
(v_movement->>'catalogue_id')::UUID,
|
| 71 |
(v_movement->>'sku')::VARCHAR(64),
|
| 72 |
(v_movement->>'batch_no')::VARCHAR(50),
|
|
@@ -100,7 +100,7 @@ $$;
|
|
| 100 |
-- Function to reserve stock (for sales orders)
|
| 101 |
CREATE OR REPLACE FUNCTION reserve_stock(
|
| 102 |
p_merchant_id VARCHAR(64),
|
| 103 |
-
|
| 104 |
p_sku VARCHAR(64),
|
| 105 |
p_batch_no VARCHAR(50),
|
| 106 |
p_qty NUMERIC(14,3),
|
|
@@ -117,7 +117,7 @@ BEGIN
|
|
| 117 |
INTO v_available_qty
|
| 118 |
FROM scm_stock
|
| 119 |
WHERE merchant_id = p_merchant_id
|
| 120 |
-
AND
|
| 121 |
AND sku = p_sku
|
| 122 |
AND batch_no = p_batch_no;
|
| 123 |
|
|
@@ -133,7 +133,7 @@ BEGIN
|
|
| 133 |
qty_available = qty_available - p_qty,
|
| 134 |
last_updated_at = NOW()
|
| 135 |
WHERE merchant_id = p_merchant_id
|
| 136 |
-
AND
|
| 137 |
AND sku = p_sku
|
| 138 |
AND batch_no = p_batch_no;
|
| 139 |
|
|
@@ -144,7 +144,7 @@ $$;
|
|
| 144 |
-- Function to release stock reservation
|
| 145 |
CREATE OR REPLACE FUNCTION release_stock_reservation(
|
| 146 |
p_merchant_id VARCHAR(64),
|
| 147 |
-
|
| 148 |
p_sku VARCHAR(64),
|
| 149 |
p_batch_no VARCHAR(50),
|
| 150 |
p_qty NUMERIC(14,3),
|
|
@@ -160,7 +160,7 @@ BEGIN
|
|
| 160 |
qty_available = qty_on_hand - GREATEST(qty_reserved - p_qty, 0),
|
| 161 |
last_updated_at = NOW()
|
| 162 |
WHERE merchant_id = p_merchant_id
|
| 163 |
-
AND
|
| 164 |
AND sku = p_sku
|
| 165 |
AND batch_no = p_batch_no;
|
| 166 |
|
|
@@ -171,11 +171,11 @@ $$;
|
|
| 171 |
-- Function to get current stock summary
|
| 172 |
CREATE OR REPLACE FUNCTION get_stock_summary(
|
| 173 |
p_merchant_id VARCHAR(64),
|
| 174 |
-
|
| 175 |
p_sku VARCHAR(64) DEFAULT NULL
|
| 176 |
) RETURNS TABLE (
|
| 177 |
stock_id UUID,
|
| 178 |
-
|
| 179 |
catalogue_id UUID,
|
| 180 |
sku VARCHAR(64),
|
| 181 |
batch_no VARCHAR(50),
|
|
@@ -189,11 +189,11 @@ LANGUAGE plpgsql
|
|
| 189 |
AS $$
|
| 190 |
BEGIN
|
| 191 |
RETURN QUERY
|
| 192 |
-
SELECT s.stock_id, s.
|
| 193 |
s.qty_on_hand, s.qty_reserved, s.qty_available, s.uom, s.last_updated_at
|
| 194 |
FROM scm_stock s
|
| 195 |
WHERE s.merchant_id = p_merchant_id
|
| 196 |
-
AND (
|
| 197 |
AND (p_sku IS NULL OR s.sku = p_sku)
|
| 198 |
ORDER BY s.sku, s.batch_no;
|
| 199 |
END;
|
|
@@ -207,7 +207,7 @@ CREATE OR REPLACE FUNCTION get_stock_ledger(
|
|
| 207 |
p_limit INTEGER DEFAULT 100
|
| 208 |
) RETURNS TABLE (
|
| 209 |
ledger_id UUID,
|
| 210 |
-
|
| 211 |
catalogue_id UUID,
|
| 212 |
sku VARCHAR(64),
|
| 213 |
batch_no VARCHAR(50),
|
|
@@ -225,7 +225,7 @@ LANGUAGE plpgsql
|
|
| 225 |
AS $$
|
| 226 |
BEGIN
|
| 227 |
RETURN QUERY
|
| 228 |
-
SELECT l.ledger_id, l.
|
| 229 |
l.txn_type, l.qty, l.uom, l.ref_type, l.ref_id, l.ref_no,
|
| 230 |
l.remarks, l.created_by, l.created_at
|
| 231 |
FROM scm_stock_ledger l
|
|
|
|
| 4 |
-- Simplified stock movement function with idempotency
|
| 5 |
CREATE OR REPLACE FUNCTION apply_stock_movement(
|
| 6 |
p_merchant_id TEXT,
|
| 7 |
+
p_warehouse_id TEXT,
|
| 8 |
p_sku TEXT,
|
| 9 |
p_batch_no TEXT,
|
| 10 |
p_qty NUMERIC,
|
|
|
|
| 25 |
|
| 26 |
-- Insert ledger entry (immutable audit trail)
|
| 27 |
INSERT INTO scm_stock_ledger (
|
| 28 |
+
ledger_id, merchant_id, warehouse_id, sku, batch_no,
|
| 29 |
qty, txn_type, ref_type, ref_id, ref_no, created_by
|
| 30 |
) VALUES (
|
| 31 |
+
gen_random_uuid(), p_merchant_id, p_warehouse_id, p_sku, p_batch_no,
|
| 32 |
p_qty, p_txn_type, p_ref_type, p_ref_id, p_ref_no, p_user
|
| 33 |
);
|
| 34 |
|
| 35 |
-- Stock snapshot upsert (create or update)
|
| 36 |
INSERT INTO scm_stock (
|
| 37 |
+
stock_id, merchant_id, warehouse_id, sku, batch_no,
|
| 38 |
qty_on_hand, qty_reserved, qty_available
|
| 39 |
) VALUES (
|
| 40 |
+
gen_random_uuid(), p_merchant_id, p_warehouse_id, p_sku, p_batch_no,
|
| 41 |
p_qty, 0, p_qty
|
| 42 |
)
|
| 43 |
+
ON CONFLICT (merchant_id, warehouse_id, sku, batch_no)
|
| 44 |
DO UPDATE SET
|
| 45 |
qty_on_hand = scm_stock.qty_on_hand + p_qty,
|
| 46 |
qty_available = scm_stock.qty_available + p_qty,
|
|
|
|
| 66 |
-- Apply individual stock movement
|
| 67 |
SELECT apply_stock_movement(
|
| 68 |
(v_movement->>'merchant_id')::VARCHAR(64),
|
| 69 |
+
(v_movement->>'warehouse_id')::VARCHAR(64),
|
| 70 |
(v_movement->>'catalogue_id')::UUID,
|
| 71 |
(v_movement->>'sku')::VARCHAR(64),
|
| 72 |
(v_movement->>'batch_no')::VARCHAR(50),
|
|
|
|
| 100 |
-- Function to reserve stock (for sales orders)
|
| 101 |
CREATE OR REPLACE FUNCTION reserve_stock(
|
| 102 |
p_merchant_id VARCHAR(64),
|
| 103 |
+
p_warehouse_id VARCHAR(64),
|
| 104 |
p_sku VARCHAR(64),
|
| 105 |
p_batch_no VARCHAR(50),
|
| 106 |
p_qty NUMERIC(14,3),
|
|
|
|
| 117 |
INTO v_available_qty
|
| 118 |
FROM scm_stock
|
| 119 |
WHERE merchant_id = p_merchant_id
|
| 120 |
+
AND warehouse_id = p_warehouse_id
|
| 121 |
AND sku = p_sku
|
| 122 |
AND batch_no = p_batch_no;
|
| 123 |
|
|
|
|
| 133 |
qty_available = qty_available - p_qty,
|
| 134 |
last_updated_at = NOW()
|
| 135 |
WHERE merchant_id = p_merchant_id
|
| 136 |
+
AND warehouse_id = p_warehouse_id
|
| 137 |
AND sku = p_sku
|
| 138 |
AND batch_no = p_batch_no;
|
| 139 |
|
|
|
|
| 144 |
-- Function to release stock reservation
|
| 145 |
CREATE OR REPLACE FUNCTION release_stock_reservation(
|
| 146 |
p_merchant_id VARCHAR(64),
|
| 147 |
+
p_warehouse_id VARCHAR(64),
|
| 148 |
p_sku VARCHAR(64),
|
| 149 |
p_batch_no VARCHAR(50),
|
| 150 |
p_qty NUMERIC(14,3),
|
|
|
|
| 160 |
qty_available = qty_on_hand - GREATEST(qty_reserved - p_qty, 0),
|
| 161 |
last_updated_at = NOW()
|
| 162 |
WHERE merchant_id = p_merchant_id
|
| 163 |
+
AND warehouse_id = p_warehouse_id
|
| 164 |
AND sku = p_sku
|
| 165 |
AND batch_no = p_batch_no;
|
| 166 |
|
|
|
|
| 171 |
-- Function to get current stock summary
|
| 172 |
CREATE OR REPLACE FUNCTION get_stock_summary(
|
| 173 |
p_merchant_id VARCHAR(64),
|
| 174 |
+
p_warehouse_id VARCHAR(64) DEFAULT NULL,
|
| 175 |
p_sku VARCHAR(64) DEFAULT NULL
|
| 176 |
) RETURNS TABLE (
|
| 177 |
stock_id UUID,
|
| 178 |
+
warehouse_id VARCHAR(64),
|
| 179 |
catalogue_id UUID,
|
| 180 |
sku VARCHAR(64),
|
| 181 |
batch_no VARCHAR(50),
|
|
|
|
| 189 |
AS $$
|
| 190 |
BEGIN
|
| 191 |
RETURN QUERY
|
| 192 |
+
SELECT s.stock_id, s.warehouse_id, s.catalogue_id, s.sku, s.batch_no,
|
| 193 |
s.qty_on_hand, s.qty_reserved, s.qty_available, s.uom, s.last_updated_at
|
| 194 |
FROM scm_stock s
|
| 195 |
WHERE s.merchant_id = p_merchant_id
|
| 196 |
+
AND (p_warehouse_id IS NULL OR s.warehouse_id = p_warehouse_id)
|
| 197 |
AND (p_sku IS NULL OR s.sku = p_sku)
|
| 198 |
ORDER BY s.sku, s.batch_no;
|
| 199 |
END;
|
|
|
|
| 207 |
p_limit INTEGER DEFAULT 100
|
| 208 |
) RETURNS TABLE (
|
| 209 |
ledger_id UUID,
|
| 210 |
+
warehouse_id VARCHAR(64),
|
| 211 |
catalogue_id UUID,
|
| 212 |
sku VARCHAR(64),
|
| 213 |
batch_no VARCHAR(50),
|
|
|
|
| 225 |
AS $$
|
| 226 |
BEGIN
|
| 227 |
RETURN QUERY
|
| 228 |
+
SELECT l.ledger_id, l.warehouse_id, l.catalogue_id, l.sku, l.batch_no,
|
| 229 |
l.txn_type, l.qty, l.uom, l.ref_type, l.ref_id, l.ref_no,
|
| 230 |
l.remarks, l.created_by, l.created_at
|
| 231 |
FROM scm_stock_ledger l
|
docs/database/sql/stored_procedures/stock_management_updated.sql
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 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 |
-
|
| 8 |
p_sku TEXT,
|
| 9 |
p_batch_no TEXT,
|
| 10 |
p_qty NUMERIC,
|
|
@@ -25,22 +25,22 @@ BEGIN
|
|
| 25 |
|
| 26 |
-- Insert ledger entry (immutable audit trail)
|
| 27 |
INSERT INTO trans.scm_stock_ledger (
|
| 28 |
-
ledger_id, merchant_id,
|
| 29 |
qty, txn_type, ref_type, ref_id, ref_no, created_by, created_at
|
| 30 |
) VALUES (
|
| 31 |
-
gen_random_uuid(), p_merchant_id,
|
| 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,
|
| 38 |
qty_on_hand, qty_reserved, qty_available, created_at, updated_at, last_updated_at
|
| 39 |
) VALUES (
|
| 40 |
-
gen_random_uuid(), p_merchant_id,
|
| 41 |
GREATEST(p_qty, 0), 0, GREATEST(p_qty, 0), NOW(), NOW(), NOW()
|
| 42 |
)
|
| 43 |
-
ON CONFLICT (merchant_id,
|
| 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),
|
|
@@ -69,7 +69,7 @@ BEGIN
|
|
| 69 |
-- Apply individual stock movement
|
| 70 |
PERFORM apply_stock_movement(
|
| 71 |
(v_movement->>'merchant_id')::TEXT,
|
| 72 |
-
(v_movement->>'
|
| 73 |
(v_movement->>'sku')::TEXT,
|
| 74 |
(v_movement->>'batch_no')::TEXT,
|
| 75 |
(v_movement->>'qty')::NUMERIC,
|
|
@@ -119,7 +119,7 @@ $$;
|
|
| 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 |
-
|
| 123 |
p_sku VARCHAR(64),
|
| 124 |
p_batch_no VARCHAR(50),
|
| 125 |
p_qty NUMERIC(14,3),
|
|
@@ -136,7 +136,7 @@ BEGIN
|
|
| 136 |
INTO v_available_qty
|
| 137 |
FROM trans.scm_stock
|
| 138 |
WHERE merchant_id = p_merchant_id
|
| 139 |
-
AND
|
| 140 |
AND sku = p_sku
|
| 141 |
AND batch_no = p_batch_no;
|
| 142 |
|
|
@@ -153,7 +153,7 @@ BEGIN
|
|
| 153 |
updated_at = NOW(),
|
| 154 |
last_updated_at = NOW()
|
| 155 |
WHERE merchant_id = p_merchant_id
|
| 156 |
-
AND
|
| 157 |
AND sku = p_sku
|
| 158 |
AND batch_no = p_batch_no;
|
| 159 |
|
|
@@ -164,7 +164,7 @@ $$;
|
|
| 164 |
-- Function to release stock reservation with proper schema
|
| 165 |
CREATE OR REPLACE FUNCTION release_stock_reservation(
|
| 166 |
p_merchant_id VARCHAR(64),
|
| 167 |
-
|
| 168 |
p_sku VARCHAR(64),
|
| 169 |
p_batch_no VARCHAR(50),
|
| 170 |
p_qty NUMERIC(14,3),
|
|
@@ -181,7 +181,7 @@ BEGIN
|
|
| 181 |
updated_at = NOW(),
|
| 182 |
last_updated_at = NOW()
|
| 183 |
WHERE merchant_id = p_merchant_id
|
| 184 |
-
AND
|
| 185 |
AND sku = p_sku
|
| 186 |
AND batch_no = p_batch_no;
|
| 187 |
|
|
@@ -192,11 +192,11 @@ $$;
|
|
| 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 |
-
|
| 196 |
p_sku VARCHAR(64) DEFAULT NULL
|
| 197 |
) RETURNS TABLE (
|
| 198 |
stock_id UUID,
|
| 199 |
-
|
| 200 |
catalogue_id UUID,
|
| 201 |
sku VARCHAR(64),
|
| 202 |
batch_no VARCHAR(50),
|
|
@@ -210,11 +210,11 @@ LANGUAGE plpgsql
|
|
| 210 |
AS $$
|
| 211 |
BEGIN
|
| 212 |
RETURN QUERY
|
| 213 |
-
SELECT s.stock_id, s.
|
| 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 (
|
| 218 |
AND (p_sku IS NULL OR s.sku = p_sku)
|
| 219 |
ORDER BY s.sku, s.batch_no;
|
| 220 |
END;
|
|
@@ -228,7 +228,7 @@ CREATE OR REPLACE FUNCTION get_stock_ledger(
|
|
| 228 |
p_limit INTEGER DEFAULT 100
|
| 229 |
) RETURNS TABLE (
|
| 230 |
ledger_id UUID,
|
| 231 |
-
|
| 232 |
catalogue_id UUID,
|
| 233 |
sku VARCHAR(64),
|
| 234 |
batch_no VARCHAR(50),
|
|
@@ -246,7 +246,7 @@ LANGUAGE plpgsql
|
|
| 246 |
AS $$
|
| 247 |
BEGIN
|
| 248 |
RETURN QUERY
|
| 249 |
-
SELECT l.ledger_id, l.
|
| 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
|
|
|
|
| 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_warehouse_id TEXT,
|
| 8 |
p_sku TEXT,
|
| 9 |
p_batch_no TEXT,
|
| 10 |
p_qty NUMERIC,
|
|
|
|
| 25 |
|
| 26 |
-- Insert ledger entry (immutable audit trail)
|
| 27 |
INSERT INTO trans.scm_stock_ledger (
|
| 28 |
+
ledger_id, merchant_id, warehouse_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_warehouse_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, warehouse_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_warehouse_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, warehouse_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),
|
|
|
|
| 69 |
-- Apply individual stock movement
|
| 70 |
PERFORM apply_stock_movement(
|
| 71 |
(v_movement->>'merchant_id')::TEXT,
|
| 72 |
+
(v_movement->>'warehouse_id')::TEXT,
|
| 73 |
(v_movement->>'sku')::TEXT,
|
| 74 |
(v_movement->>'batch_no')::TEXT,
|
| 75 |
(v_movement->>'qty')::NUMERIC,
|
|
|
|
| 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_warehouse_id VARCHAR(64),
|
| 123 |
p_sku VARCHAR(64),
|
| 124 |
p_batch_no VARCHAR(50),
|
| 125 |
p_qty NUMERIC(14,3),
|
|
|
|
| 136 |
INTO v_available_qty
|
| 137 |
FROM trans.scm_stock
|
| 138 |
WHERE merchant_id = p_merchant_id
|
| 139 |
+
AND warehouse_id = p_warehouse_id
|
| 140 |
AND sku = p_sku
|
| 141 |
AND batch_no = p_batch_no;
|
| 142 |
|
|
|
|
| 153 |
updated_at = NOW(),
|
| 154 |
last_updated_at = NOW()
|
| 155 |
WHERE merchant_id = p_merchant_id
|
| 156 |
+
AND warehouse_id = p_warehouse_id
|
| 157 |
AND sku = p_sku
|
| 158 |
AND batch_no = p_batch_no;
|
| 159 |
|
|
|
|
| 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_warehouse_id VARCHAR(64),
|
| 168 |
p_sku VARCHAR(64),
|
| 169 |
p_batch_no VARCHAR(50),
|
| 170 |
p_qty NUMERIC(14,3),
|
|
|
|
| 181 |
updated_at = NOW(),
|
| 182 |
last_updated_at = NOW()
|
| 183 |
WHERE merchant_id = p_merchant_id
|
| 184 |
+
AND warehouse_id = p_warehouse_id
|
| 185 |
AND sku = p_sku
|
| 186 |
AND batch_no = p_batch_no;
|
| 187 |
|
|
|
|
| 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_warehouse_id VARCHAR(64) DEFAULT NULL,
|
| 196 |
p_sku VARCHAR(64) DEFAULT NULL
|
| 197 |
) RETURNS TABLE (
|
| 198 |
stock_id UUID,
|
| 199 |
+
warehouse_id VARCHAR(64),
|
| 200 |
catalogue_id UUID,
|
| 201 |
sku VARCHAR(64),
|
| 202 |
batch_no VARCHAR(50),
|
|
|
|
| 210 |
AS $$
|
| 211 |
BEGIN
|
| 212 |
RETURN QUERY
|
| 213 |
+
SELECT s.stock_id, s.warehouse_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_warehouse_id IS NULL OR s.warehouse_id = p_warehouse_id)
|
| 218 |
AND (p_sku IS NULL OR s.sku = p_sku)
|
| 219 |
ORDER BY s.sku, s.batch_no;
|
| 220 |
END;
|
|
|
|
| 228 |
p_limit INTEGER DEFAULT 100
|
| 229 |
) RETURNS TABLE (
|
| 230 |
ledger_id UUID,
|
| 231 |
+
warehouse_id VARCHAR(64),
|
| 232 |
catalogue_id UUID,
|
| 233 |
sku VARCHAR(64),
|
| 234 |
batch_no VARCHAR(50),
|
|
|
|
| 246 |
AS $$
|
| 247 |
BEGIN
|
| 248 |
RETURN QUERY
|
| 249 |
+
SELECT l.ledger_id, l.warehouse_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
|
tests/test_merchant_catalogue_list.py
CHANGED
|
@@ -159,7 +159,7 @@ class MerchantCatalogueListTester:
|
|
| 159 |
# Create test scm_stock records
|
| 160 |
stock_insert_query = """
|
| 161 |
INSERT INTO scm_stock (
|
| 162 |
-
stock_id, merchant_id,
|
| 163 |
qty_on_hand, qty_reserved, qty_available, last_updated, created_at
|
| 164 |
) VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9)
|
| 165 |
ON CONFLICT (stock_id) DO NOTHING
|
|
@@ -168,7 +168,7 @@ class MerchantCatalogueListTester:
|
|
| 168 |
self.test_stock_data = [
|
| 169 |
{
|
| 170 |
"merchant_id": self.test_merchant_id,
|
| 171 |
-
"
|
| 172 |
"sku": "SKU-SHAMPOO-001",
|
| 173 |
"batch_no": "BATCH-001",
|
| 174 |
"qty_on_hand": 100.0,
|
|
@@ -179,7 +179,7 @@ class MerchantCatalogueListTester:
|
|
| 179 |
},
|
| 180 |
{
|
| 181 |
"merchant_id": self.test_merchant_id,
|
| 182 |
-
"
|
| 183 |
"sku": "SKU-SHAMPOO-001",
|
| 184 |
"batch_no": "BATCH-002",
|
| 185 |
"qty_on_hand": 50.0,
|
|
@@ -190,7 +190,7 @@ class MerchantCatalogueListTester:
|
|
| 190 |
},
|
| 191 |
{
|
| 192 |
"merchant_id": self.test_merchant_id,
|
| 193 |
-
"
|
| 194 |
"sku": "SKU-CONDITIONER-001",
|
| 195 |
"batch_no": "BATCH-003",
|
| 196 |
"qty_on_hand": 75.0,
|
|
@@ -206,7 +206,7 @@ class MerchantCatalogueListTester:
|
|
| 206 |
await self.pg_session.execute(
|
| 207 |
stock_insert_query,
|
| 208 |
[
|
| 209 |
-
stock_data["merchant_id"], stock_data["
|
| 210 |
stock_data["sku"], stock_data["batch_no"],
|
| 211 |
stock_data["qty_on_hand"], stock_data["qty_reserved"],
|
| 212 |
stock_data["qty_available"], stock_data["last_updated"],
|
|
|
|
| 159 |
# Create test scm_stock records
|
| 160 |
stock_insert_query = """
|
| 161 |
INSERT INTO scm_stock (
|
| 162 |
+
stock_id, merchant_id, warehouse_id, sku, batch_no,
|
| 163 |
qty_on_hand, qty_reserved, qty_available, last_updated, created_at
|
| 164 |
) VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9)
|
| 165 |
ON CONFLICT (stock_id) DO NOTHING
|
|
|
|
| 168 |
self.test_stock_data = [
|
| 169 |
{
|
| 170 |
"merchant_id": self.test_merchant_id,
|
| 171 |
+
"warehouse_id": "LOC-001",
|
| 172 |
"sku": "SKU-SHAMPOO-001",
|
| 173 |
"batch_no": "BATCH-001",
|
| 174 |
"qty_on_hand": 100.0,
|
|
|
|
| 179 |
},
|
| 180 |
{
|
| 181 |
"merchant_id": self.test_merchant_id,
|
| 182 |
+
"warehouse_id": "LOC-002",
|
| 183 |
"sku": "SKU-SHAMPOO-001",
|
| 184 |
"batch_no": "BATCH-002",
|
| 185 |
"qty_on_hand": 50.0,
|
|
|
|
| 190 |
},
|
| 191 |
{
|
| 192 |
"merchant_id": self.test_merchant_id,
|
| 193 |
+
"warehouse_id": "LOC-001",
|
| 194 |
"sku": "SKU-CONDITIONER-001",
|
| 195 |
"batch_no": "BATCH-003",
|
| 196 |
"qty_on_hand": 75.0,
|
|
|
|
| 206 |
await self.pg_session.execute(
|
| 207 |
stock_insert_query,
|
| 208 |
[
|
| 209 |
+
stock_data["merchant_id"], stock_data["warehouse_id"],
|
| 210 |
stock_data["sku"], stock_data["batch_no"],
|
| 211 |
stock_data["qty_on_hand"], stock_data["qty_reserved"],
|
| 212 |
stock_data["qty_available"], stock_data["last_updated"],
|