inventory-management
Platform Notes
- Optional helper plugins may help in some environments, but they must not be treated as required for this skill.
Inventory Management Skill
Use When
- Coordinate infrastructure for inventory, stock movement, BOMs, valuation, and multi-location controls while referencing the existing ERP implementation, inventory docs, and the small-business bookkeeping playbook.
- The task needs reusable judgment, domain constraints, or a proven workflow rather than ad hoc advice.
Do Not Use When
- The task is unrelated to
inventory-managementor would be better handled by a more specific companion skill. - The request only needs a trivial answer and none of this skill's constraints or references materially help.
Required Inputs
- Gather relevant project context, constraints, and the concrete problem to solve; load
referencesonly as needed. - Confirm the desired deliverable: design, code, review, migration plan, audit, or documentation.
Workflow
- Read this
SKILL.mdfirst, then load only the referenced deep-dive files that are necessary for the task. - Apply the ordered guidance, checklists, and decision rules in this skill instead of cherry-picking isolated snippets.
- Produce the deliverable with assumptions, risks, and follow-up work made explicit when they matter.
Quality Standards
- Keep outputs execution-oriented, concise, and aligned with the repository's baseline engineering standards.
- Preserve compatibility with existing project conventions unless the skill explicitly requires a stronger standard.
- Prefer deterministic, reviewable steps over vague advice or tool-specific magic.
Anti-Patterns
- Treating examples as copy-paste truth without checking fit, constraints, or failure modes.
- Loading every reference file by default instead of using progressive disclosure.
Outputs
- A concrete result that fits the task: implementation guidance, review findings, architecture decisions, templates, or generated artifacts.
- Clear assumptions, tradeoffs, or unresolved gaps when the task cannot be completed from available context alone.
- References used, companion skills, or follow-up actions when they materially improve execution.
Evidence Produced
| Category | Artifact | Format | Example |
|---|---|---|---|
| Data safety | Inventory data model | Markdown doc per skill-composition-standards/references/entity-model-template.md covering stock, movement, BOM, and valuation entities |
docs/inventory/data-model.md |
| Correctness | Inventory workflow test plan | Markdown doc covering receive, issue, transfer, valuation, and multi-location scenarios | docs/inventory/workflow-tests.md |
References
- Use the
references/directory for deep detail after reading the core workflow below.
Overview
Pair the existing stock-tracking implementation (stock ledgers, unit conversion, movements, purchase and sales flows, asset-level constraints) with general bookkeeping principles such as multi-location control, valuation methods, auditing, and SKU types. Use this skill whenever a change touches stock items, transfers, inventory valuation, assembly or BOM flows, stock adjustments, or reporting.
Inventory Data Model
Ledger-first design: every quantity change appends a row to stock_movements; stock_levels is a materialised view of the ledger for fast reads. Never mutate historical rows.
CREATE TABLE products (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
sku_root VARCHAR(64) NOT NULL,
name VARCHAR(255) NOT NULL,
category_id BIGINT UNSIGNED NULL,
unit_of_measure VARCHAR(16) NOT NULL DEFAULT 'each',
is_stock_item TINYINT(1) NOT NULL DEFAULT 1,
is_perishable TINYINT(1) NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_products_tenant_sku (tenant_id, sku_root),
KEY idx_products_category (tenant_id, category_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE skus (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
product_id BIGINT UNSIGNED NOT NULL,
sku_code VARCHAR(64) NOT NULL,
variant VARCHAR(128) NULL,
barcode VARCHAR(32) NULL,
unit_cost DECIMAL(14,4) NOT NULL DEFAULT 0,
UNIQUE KEY uq_skus_code (sku_code),
KEY idx_skus_barcode (barcode),
CONSTRAINT fk_skus_product FOREIGN KEY (product_id) REFERENCES products(id)
) ENGINE=InnoDB;
CREATE TABLE locations (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
code VARCHAR(64) NOT NULL,
name VARCHAR(255) NOT NULL,
parent_id BIGINT UNSIGNED NULL,
kind ENUM('company','region','warehouse','zone','aisle','bay','shelf','bin') NOT NULL,
UNIQUE KEY uq_loc_code (tenant_id, code),
KEY idx_loc_parent (parent_id)
) ENGINE=InnoDB;
CREATE TABLE stock_levels (
sku_id BIGINT UNSIGNED NOT NULL,
location_id BIGINT UNSIGNED NOT NULL,
on_hand_qty DECIMAL(14,4) NOT NULL DEFAULT 0,
reserved_qty DECIMAL(14,4) NOT NULL DEFAULT 0,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (sku_id, location_id),
CONSTRAINT fk_sl_sku FOREIGN KEY (sku_id) REFERENCES skus(id),
CONSTRAINT fk_sl_loc FOREIGN KEY (location_id) REFERENCES locations(id)
) ENGINE=InnoDB;
CREATE TABLE stock_movements (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
sku_id BIGINT UNSIGNED NOT NULL,
location_id BIGINT UNSIGNED NOT NULL,
movement_type ENUM('receive','issue','transfer_out','transfer_in','adjust','stocktake','return') NOT NULL,
qty_delta DECIMAL(14,4) NOT NULL,
unit_cost DECIMAL(14,4) NOT NULL DEFAULT 0,
batch_no VARCHAR(64) NULL,
expiry_date DATE NULL,
reference_type VARCHAR(32) NULL,
reference_id BIGINT UNSIGNED NULL,
reason_code VARCHAR(32) NULL,
user_id BIGINT UNSIGNED NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_mv_sku_loc (sku_id, location_id, created_at),
KEY idx_mv_ref (reference_type, reference_id),
KEY idx_mv_batch (batch_no),
CONSTRAINT fk_mv_sku FOREIGN KEY (sku_id) REFERENCES skus(id),
CONSTRAINT fk_mv_loc FOREIGN KEY (location_id) REFERENCES locations(id)
) ENGINE=InnoDB;
CREATE TABLE suppliers (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
name VARCHAR(255) NOT NULL,
contact_email VARCHAR(255) NULL,
contact_phone VARCHAR(32) NULL,
payment_terms VARCHAR(64) NULL,
lead_time_days INT UNSIGNED NOT NULL DEFAULT 7,
moq DECIMAL(14,4) NOT NULL DEFAULT 1,
currency CHAR(3) NOT NULL DEFAULT 'UGX',
UNIQUE KEY uq_supplier (tenant_id, name)
) ENGINE=InnoDB;
CREATE TABLE purchase_orders (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
supplier_id BIGINT UNSIGNED NOT NULL,
location_id BIGINT UNSIGNED NOT NULL,
po_number VARCHAR(32) NOT NULL,
status ENUM('draft','sent','confirmed','partial','received','closed','cancelled') NOT NULL DEFAULT 'draft',
currency CHAR(3) NOT NULL,
ordered_at DATETIME NULL,
expected_at DATE NULL,
UNIQUE KEY uq_po_number (tenant_id, po_number),
CONSTRAINT fk_po_supplier FOREIGN KEY (supplier_id) REFERENCES suppliers(id),
CONSTRAINT fk_po_location FOREIGN KEY (location_id) REFERENCES locations(id)
) ENGINE=InnoDB;
CREATE TABLE purchase_order_lines (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
po_id BIGINT UNSIGNED NOT NULL,
sku_id BIGINT UNSIGNED NOT NULL,
ordered_qty DECIMAL(14,4) NOT NULL,
received_qty DECIMAL(14,4) NOT NULL DEFAULT 0,
unit_cost DECIMAL(14,4) NOT NULL,
KEY idx_pol_po (po_id),
CONSTRAINT fk_pol_po FOREIGN KEY (po_id) REFERENCES purchase_orders(id) ON DELETE CASCADE,
CONSTRAINT fk_pol_sku FOREIGN KEY (sku_id) REFERENCES skus(id)
) ENGINE=InnoDB;
Stock Level Tracking
- On-hand qty lives in
stock_levels.on_hand_qtyand is updated by a trigger or service on everystock_movementsinsert. - Reserved qty reflects allocations for unfulfilled sales orders, picking tasks, or in-transit transfers.
- Available qty is always derived, never stored:
available_qty = on_hand_qty - reserved_qty.
Reorder point formula:
reorder_point = (avg_daily_demand * lead_time_days) + safety_stock
Safety stock formula (normal-demand approximation):
safety_stock = z * stddev_daily_demand * sqrt(lead_time_days)
Service level z table: z=1.28 for 90%, z=1.65 for 95%, z=2.33 for 99%. Recompute weekly from the last 90 days of stock_movements where movement_type = 'issue'.
SELECT sku_id,
AVG(daily_qty) AS avg_daily_demand,
STDDEV_SAMP(daily_qty) AS stddev_daily_demand
FROM (
SELECT sku_id, DATE(created_at) d, SUM(-qty_delta) daily_qty
FROM stock_movements
WHERE movement_type = 'issue' AND created_at >= NOW() - INTERVAL 90 DAY
GROUP BY sku_id, DATE(created_at)
) t
GROUP BY sku_id;
Reorder Triggers
- A daily cron job (02:00 local) scans
stock_levelsjoined againstskus.reorder_pointand finds rows where(on_hand_qty - reserved_qty) <= reorder_point. - Candidates are grouped by
preferred_supplier_id, one draft PO per supplier per location. - PO is created with
status = 'draft'and emailed to procurement with an approval link. No goods move until a human confirmsstatus = 'sent'. - Manual override: any user with
inventory.reorder.createpermission can open a draft PO directly from the product screen, bypassing the cron. - Do not auto-send to supplier. Auto-send violates approval controls and risks duplicate POs after ERP restarts.
Barcode Scanning
EAN-13 and Code128 are mandatory for African retail (EAN for FMCG, Code128 for internal labels and cartons). Support three capture methods:
- Camera via browser: use the native
BarcodeDetectorAPI on Chromium; fall back to@zxing/browseron Safari and older browsers. - Camera via native: ML Kit
BarcodeScanningon Android,VisionframeworkVNDetectBarcodesRequeston iOS. - USB HID scanner: device presents as a keyboard and types the code followed by
Enter. Capture the stream by timing keydown events.
// USB HID scanner listener (web)
let buffer = '';
let lastKeyAt = 0;
document.addEventListener('keydown', (e) => {
const now = performance.now();
if (now - lastKeyAt > 50) buffer = '';
lastKeyAt = now;
if (e.key === 'Enter' && buffer.length >= 6) {
handleScan(buffer);
buffer = '';
return;
}
if (e.key.length === 1) buffer += e.key;
});
- Debounce duplicate scans within 500 ms.
- Never trust the barcode alone for price; resolve
barcode -> sku_id -> unit_coston the server.
Goods Receive
Three supported patterns; every pattern writes stock_movements rows with movement_type = 'receive' and positive qty_delta:
- PO-matched receive: receiver selects an open PO, scans each line, and confirms line-by-line. System updates
purchase_order_lines.received_qtyand transitions PO topartialorreceived. - Blind receive: receiver sees only the SKU list (no ordered qty). This prevents "count to match the PO" fraud. System compares counts to PO after submission and flags variance.
- Quantity variance: if
received_qty != ordered_qty, prompt for a reason code (short_shipped,damaged_in_transit,over_shipped,wrong_item). Variance over 5% or$100requires manager approval.
Stock Transfer
Inter-location transfer runs a two-phase state machine to prevent phantom stock:
- States:
draft -> in_transit -> completed | cancelled. - Draft: receiver and sender both see the request. No stock moves.
- In-transit: sending location fires a
transfer_outmovement (negative delta at source). Atransfer_inrow is NOT created yet. The stock belongs to a syntheticin_transitlocation owned by the company, not the destination. - Completed: receiving location confirms arrival. System writes a
transfer_inmovement (positive delta at destination) and empties thein_transitbucket for that transfer ID. - Cancelled before in-transit: no movement rows exist; delete the draft.
- Cancelled after in-transit (e.g., lost in transit): write a
transfer_inback to the source location withreason_code = 'transfer_reversal'.
Stock Adjustment
- Write-off: negative
qty_deltawithmovement_type = 'adjust'andreason_codefromdamaged,expired,miscounted,theft,donation,sample. - Write-up: positive
qty_deltawithreason_code = 'found'or'correction'. - Threshold rule: any adjustment whose absolute value of
qty_delta * unit_costexceeds a tenant-configurable threshold (default UGX 370,000 equivalent to roughly $100) requires manager approval before the movement is inserted. Store pending adjustments instock_adjustmentswithapproval_state. - Every adjustment posts a matching journal entry to the accounting ledger: write-off debits an expense account (
Inventory Shrinkage) and creditsInventory Asset.
Batch Operations
CSV import supports receive, adjust, and transfer in bulk with a dry-run preview that shows resulting stock levels before commit.
CSV columns:
| Column | Required | Type | Notes |
|---|---|---|---|
| operation | yes | enum | receive, adjust, transfer |
| sku_code | yes | string | must exist |
| location_code | yes | string | must exist |
| dest_location_code | conditional | string | required when operation=transfer |
| qty | yes | decimal | non-negative unless operation=adjust |
| unit_cost | conditional | decimal | required on receive |
| batch_no | optional | string | required on receive if product is_perishable |
| expiry_date | optional | date | ISO 8601 (YYYY-MM-DD) |
| reason_code | conditional | string | required when operation=adjust |
Example:
operation,sku_code,location_code,dest_location_code,qty,unit_cost,batch_no,expiry_date,reason_code
receive,SKU-1001,UG-KLA-WH1,,120,4500,B2026-04,2026-10-31,
adjust,SKU-1002,UG-KLA-WH1,,-5,,,,damaged
transfer,SKU-1003,UG-KLA-WH1,UG-MBR-WH2,20,,,,
Validation: SKU must exist, location must exist, numeric parse must succeed, reason_code in allowed set, dry-run renders projected stock_levels. Commit runs inside a single transaction per upload.
Stock-Take (Physical Count)
- Cycle count: weekly sweep of a rotating SKU subset (ABC class A weekly, B monthly, C quarterly). Counters scan each bin and enter the physical qty; variance is committed immediately.
- Full stock-take: freeze sales (
POSenters read-only mode), count every SKU in every bin, reconcile, then reopen sales. Usually run at financial year-end. - Variance report:
variance = counted_qty - system_on_hand. Negative variance is shrinkage; positive variance usually indicates earlier miscount. - Automatic adjustment: the reconciliation step writes one
stock_movementsrow per SKU withmovement_type = 'stocktake'andqty_delta = variance. Variance over the adjustment threshold still requires manager approval.
FIFO / LIFO / Weighted Average
Costing method is selected per product, not per tenant. Document the choice in the product master.
- FIFO (default for perishables and most retail): oldest stock consumed first; COGS reflects older costs; ending inventory reflects current costs.
- LIFO: newest stock consumed first; rare outside USA tax contexts and banned under IFRS.
- Weighted average: recomputed after every receive as
total_cost / total_qty; smooths price spikes, common in manufacturing.
Layers table (FIFO/LIFO implementation):
CREATE TABLE stock_layers (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
sku_id BIGINT UNSIGNED NOT NULL,
location_id BIGINT UNSIGNED NOT NULL,
received_at DATETIME NOT NULL,
qty_remaining DECIMAL(14,4) NOT NULL,
unit_cost DECIMAL(14,4) NOT NULL,
KEY idx_layer_consume (sku_id, location_id, received_at)
) ENGINE=InnoDB;
On sale under FIFO, deduct from the oldest layer first; on sale under LIFO, deduct from the newest. On weighted average, ignore layers and use products.moving_avg_cost, recomputed as (old_qty * old_avg + received_qty * received_cost) / (old_qty + received_qty).
Expiry Date Tracking
stock_movements.batch_noandstock_movements.expiry_dateare mandatory for any product withis_perishable = 1.- FEFO (First Expired First Out) picking order overrides FIFO for perishables: pick the batch with the earliest
expiry_datewhoseqty_remaining > 0. - Daily job flags items where
expiry_date <= CURRENT_DATE + INTERVAL 30 DAYand emails the store manager. A second alert fires at 7 days. - Mandatory for pharmacy (NDA compliance), food service, cosmetics, and any regulated SKU class.
- Expired stock is written off with
reason_code = 'expired'and must not be sold, even at discount.
Supplier Management
- Catalog columns: name, contact email and phone, payment terms (
net30,net45,cod),lead_time_days, MOQ, preferred currency. - PO workflow:
draft -> sent -> confirmed -> partial -> received -> closed;cancelledis a terminal state reachable from any pre-receivedstate. - Supplier performance KPIs (recomputed monthly):
- On-time delivery rate =
deliveries_on_or_before_expected_at / total_deliveries. - Fill rate =
sum(received_qty) / sum(ordered_qty)per line. - Defect rate =
returned_qty / received_qtyover rolling 90 days.
- On-time delivery rate =
- Suppliers with on-time rate under 80% or fill rate under 90% are flagged for review; procurement decides whether to renegotiate or drop.
Multi-Location Inventory
- Location hierarchy:
company -> region -> warehouse -> zone -> aisle -> bay -> shelf -> bin. Use thekindenum inlocations. - Location codes encode hierarchy for zone-picking efficiency, e.g.,
UG-KLA-WH1-A-03-2-B:UGcountry,KLAcity/region,WH1warehouse,Azone,03aisle,2bay,Bbin.
- Stock levels are stored at the lowest location level the tenant tracks; rollups to warehouse or region are computed on read.
- In-transit stock belongs to a synthetic location per transfer (
TRANSFER-<id>), not to source or destination, until the transfer is completed. - Cross-location visibility is governed by role: a clerk at
WH1should not seeWH2stock unless given theinventory.read_all_locationspermission.
Reporting
Four core reports every deployment must ship:
- Stock valuation:
SUM(on_hand_qty * unit_cost)grouped by location and category. Uses the product's costing method (FIFO layers, LIFO layers, or moving average). - Slow-moving items: SKUs with no
issuemovement in the last 90 days. Output: SKU, location, on-hand qty, tied-up cost, last movement date. Action: clearance, markdown, or supplier return. - Stock turnover rate:
COGS / average_inventory_valueover the reporting window. Target bands by industry: grocery 10-20, pharmacy 6-10, electronics 4-6, fashion 3-5. Under-target means dead stock; over-target can mean stockouts. - Stock ageing: bucket each SKU by days since last receive into
0-30,31-60,61-90,90+. Ageing stock over 90 days ties up working capital and usually loses value.
Example ageing query:
SELECT sku_id,
CASE
WHEN DATEDIFF(NOW(), MAX(received_at)) <= 30 THEN '0-30'
WHEN DATEDIFF(NOW(), MAX(received_at)) <= 60 THEN '31-60'
WHEN DATEDIFF(NOW(), MAX(received_at)) <= 90 THEN '61-90'
ELSE '90+'
END AS age_bucket,
SUM(qty_remaining) AS qty
FROM stock_layers
GROUP BY sku_id, age_bucket;
Mobile Interface Patterns
- Scan-first design: the barcode scanner is the primary input. Do not force keyboard entry of SKU codes; offer it only as fallback.
- Large input targets: minimum 48 x 48 dp for every tap target (Material guideline and iOS HIG floor). Receive screens should push this to 56 dp.
- Offline-capable: warehouse cold rooms, basements, and upcountry branches frequently lose WiFi. Queue movements locally (IndexedDB for web, Room for Android, SwiftData for iOS) and sync with conflict resolution on reconnect.
- Field-friendly contrast: default to high-contrast dark-on-light themes. Warehouse lighting is inconsistent; avoid grey-on-grey.
- Glove-usable buttons: no tiny tap targets; no gesture-only actions; every primary action must have a visible button at least 48 dp tall. Capacitive touch fails through thick gloves, so pair scan inputs with physical trigger handhelds where possible.
Companion Skills
saas-erp-system-design— multi-module ERP architecture where inventory fitspos-sales-ui-design— retail sales that consume inventorypos-restaurant-ui-standard— restaurant inventory depletes on order firemysql-data-modeling/postgresql-fundamentals— database patterns for ledger-style movementspwa-offline-first— offline-capable warehouse and field workflowsmobile-saas-planning— mobile app that drives inventory operations
Sources
- Warehouse Management — Gwynne Richards (Kogan Page)
- Operations Management — Jay Heizer (Pearson)
- FEFO / FIFO methodology —
en.wikipedia.org/wiki/FIFO_and_LIFO_accounting - Uganda Revenue Authority EFRIS inventory requirements —
efris.ura.go.ug
More from peterbamuhigire/skills-web-dev
google-play-store-review
Google Play Store compliance and review readiness for Android apps. Use
77multi-tenant-saas-architecture
Use when designing or reviewing a multi-tenant SaaS platform — tenant
68jetpack-compose-ui
Jetpack Compose UI standards for beautiful, sleek, minimalistic Android
49gis-mapping
Use for web apps that need Leaflet-first GIS mapping, location selection,
49saas-accounting-system
Implement a complete double-entry accounting system inside any SaaS app.
47manual-guide
Generate end-user manuals and reference guides for ERP modules. Use when
40