January 27, 2026
Item Master vs Stock Receipts: Where Does Package Info Belong?
A user asked a simple question: "I have 50 KG of cooking oil to add. Should I name it '50 KG Cooking Oil'?"
The answer is no - just call it "Cooking Oil." But this leads to a deeper question: If both 50 KG bags and 100 KG bags are just "Cooking Oil", what differentiates them?
Two Concepts, Two Roles
Item Master (InventoryItem)
The Item Master defines WHAT something is:
| Field | Purpose |
|---|---|
| Name | "Cooking Oil" |
| SKU | "CKO-001" |
| Unit of Measure | Litre (L) |
| Category | Cooking Oils |
| Supplier | Default supplier |
| Reorder Level | 20 L |
The item master is the identity of the product. It doesn't change based on how you buy it.
Stock Receipt (GRN / Quick Add)
The Stock Receipt records HOW you received it:
| Field | Purpose |
|---|---|
| Packages Received | 2 |
| Package Size | 50 L per jerrycan |
| Package Cost | 6,500 KES per jerrycan |
| Total Quantity | 100 L |
| Unit Cost | 130 KES/L |
| Batch Number | BATCH-2026-01 |
| Expiry Date | 2027-01-15 |
The receipt is a transaction - it varies each time you buy.
The Problem With Package Info on Item Master
Current Design
InventoryItem: Cooking Oil
├── package_size: 50 ← Fixed!
├── package_unit: JERRYCAN
├── package_cost: 6,500
└── cost_price: 130/L
What Happens When Reality Changes?
| Week | What You Buy | Problem |
|---|---|---|
| Week 1 | 2 × 50L jerrycan @ 6,500 | Works fine |
| Week 2 | 1 × 100L drum @ 12,000 | Must edit master! |
| Week 3 | 4 × 20L jerrycan @ 2,800 | Edit again! |
You're forced to constantly update the item master, losing historical data about what you actually received.
The Correct Architecture
Package Info Belongs on the Receipt
InventoryItem: Cooking Oil ← Identity only
├── name: Cooking Oil
├── SKU: CKO-001
├── UOM: Litre
└── cost_price: 128.50/L ← Weighted average
Receipt #1 (Jan 15): ← Package info here
├── packages: 2
├── package_size: 50 L
├── package_unit: JERRYCAN
├── package_cost: 6,500
├── total_qty: 100 L
└── unit_cost: 130/L
Receipt #2 (Jan 22): ← Different package, same item
├── packages: 1
├── package_size: 100 L
├── package_unit: DRUM
├── package_cost: 12,000
├── total_qty: 100 L
└── unit_cost: 120/L
Stock Balance: 200 L @ avg 125/L
How This Works for Different Users
Enterprise Mode: Full GRN Process
Large retailers use the complete Goods Received Note workflow:
Purchase Order
↓
Goods Received Note (GRN)
├── Supplier: Bidco Kenya
├── PO Reference: PO-2026-0042
├── Items:
│ ├── Cooking Oil
│ │ ├── packages_received: 2
│ │ ├── package_size: 50 L
│ │ ├── package_unit: JERRYCAN
│ │ ├── package_cost: 6,500
│ │ ├── quantity_received: 100 L
│ │ ├── quantity_accepted: 100 L
│ │ ├── batch_number: BATCH-2026-01
│ │ └── expiry_date: 2027-01-15
│ └── Sugar
│ ├── packages_received: 5
│ ├── package_size: 50 KG
│ └── ...
└── Complete GRN → Stock Updated
Benefits:
- Full audit trail
- Quality checks (accepted vs rejected)
- Batch/expiry tracking
- Supplier accountability
- Discrepancy notes
Duka Mode: Quick Add Stock
Small shops use the simplified quick adjustment:
Quick Add Stock
├── Item: Cooking Oil
├── Adjustment Type: ADD
├── Quantity: 100 L
├── Unit Cost: 130/L
├── Reason: Opening Stock
└── Submit → Stock Updated Immediately
Benefits:
- No purchase order needed
- Single API call
- Auto-approved
- Instant stock update
- Perfect for opening stock
The Duka Challenge: Where's Package Info?
Current Quick Add Schema
QuickStockAdjustmentSchema:
- inventory_item_id (required)
- adjustment_type: ADD/SUBTRACT/SET
- quantity (in base units)
- unit_cost (optional)
- reason
- notes
Missing: No package fields!
A duka owner buying 2 jerrycans of 50L each must:
- Calculate: 2 × 50 = 100 L
- Calculate: 6,500 ÷ 50 = 130/L
- Enter: quantity=100, unit_cost=130
They can't just say: "I bought 2 jerrycans at 6,500 each."
Enhanced Quick Add (Proposed)
QuickStockAdjustmentSchema:
- inventory_item_id (required)
- adjustment_type: ADD/SUBTRACT/SET
# Option A: Direct quantity entry (current)
- quantity (in base units)
- unit_cost
# Option B: Package entry (new)
- packages_received
- package_size
- package_unit
- package_cost
# System calculates: quantity = packages × size
# System calculates: unit_cost = package_cost ÷ size
- reason
- notes
UI can show:
┌─ Add Stock: Cooking Oil ────────────────────┐
│ │
│ ○ Enter by quantity │
│ Quantity: [____] L Cost: [____]/L │
│ │
│ ● Enter by package │
│ Packages: [2] Size: [50] L │
│ Cost per package: [6,500] KES │
│ │
│ → Adding 100 L at 130 KES/L │
│ │
│ [Cancel] [Add Stock] │
└──────────────────────────────────────────────┘
Weighted Average Cost
When you receive stock at different costs, the system calculates weighted average:
Before: 100 L @ 130/L = 13,000 value
Receipt: 100 L @ 120/L = 12,000 value
After: 200 L, Total value = 25,000
New avg cost = 25,000 ÷ 200 = 125/L
This works regardless of whether package info is captured - but capturing packages gives you:
- Historical accuracy
- Supplier analysis
- Reorder intelligence ("We usually buy 50L jerrycans")
Implementation Recommendations
For Enterprise Mode (GRN)
Add to GoodsReceivedItem model:
# Package info captured at receipt time
packages_received = Column(Numeric(10, 2))
package_size = Column(Numeric(15, 3))
package_unit = Column(String(50))
package_cost = Column(Numeric(15, 2))
For Duka Mode (Quick Add)
Add optional package fields to QuickStockAdjustmentSchema:
# Optional: Enter by package instead of quantity
packages = fields.Decimal(allow_none=True)
package_size = fields.Decimal(allow_none=True)
package_unit = fields.String(allow_none=True)
package_cost = fields.Decimal(allow_none=True)
Service calculates:
if data.get('packages') and data.get('package_size'):
quantity = data['packages'] * data['package_size']
unit_cost = data['package_cost'] / data['package_size']
else:
quantity = data['quantity']
unit_cost = data.get('unit_cost')
Keep Default Package on Item Master
For convenience, keep default package info on InventoryItem:
- Pre-fills the receipt form
- Suggests "You usually buy this in 50L jerrycans"
- Can be overridden per receipt
Summary
| Concept | What It Stores | Changes When |
|---|---|---|
| Item Master | Identity (name, SKU, UOM) | Rarely (new product, rename) |
| Stock Receipt | Transaction (packages, cost, batch) | Every purchase |
| Package Info | How you bought it | Per receipt |
The Item Master defines WHAT something is. The Receipt defines HOW you got it.
Sugar is sugar whether it comes in 50 KG bags or 100 KG bags. The packaging is a property of the transaction, not the product.
Quick Reference
"Should I name it '50 KG Sugar'?"
No. Name it "Sugar."
- 50 KG is the package size → goes on receipt
- Sugar is the product → goes on item master
"What if I buy different sizes?"
Each receipt records its own package info:
- Receipt 1: 2 × 50 KG bags
- Receipt 2: 1 × 100 KG bag
- Same item master, different receipts
"What about dukas using quick add?"
Two options:
- Current: Enter calculated quantity and unit cost
- Enhanced: Enter packages and let system calculate
Both work - enhanced is more intuitive for shop owners who think in "bags" not "kilograms."
This architecture serves both the duka owner who just needs to add stock quickly, and the enterprise retailer who needs full procurement audit trails. Same data model, different interfaces.