7.0 KiB
Inventory System Improvement — Implementation Summary
Overview
This implementation applies the first three sprints of the inventory improvement plan:
- Stabilize Inventory Data — Make movement ledger the source of truth
- Movement and Audit Cleanup — Append-only ledger with void/correction support
- Procurement Integration — Link purchase orders to inventory movements
- Low-Stock and Reorder Workflow — New endpoints and features
Changes Made
Phase 1: Stabilize Inventory Data
1. Quantity removed from item updates
app/Http/Requests/Inventory/InventoryItemUpdateRequest.php: Removedquantityfrom validatable fields. Stock changes must go through movements (adjust, audit, etc.).app/Services/Inventory/InventoryItemService.php:filterItemData()now only includesquantityduring creation ($lockedType === null), not during updates.app/Http/Requests/Inventory/InventoryItemStoreRequest.php: Strengthened validation —typeusesin:classroom,book,office,kitchen,conditionusesin:good,needs_repair,need_replace,cannot_find,category_idchecksexists:inventory_categories,id.
2. Append-only movement ledger with void/correction
New migration: 2026_06_11_074503_add_inventory_movement_audit_fields.php
Adds to inventory_movements:
voided_at(timestamp) — when the movement was voidedvoided_by(bigint) — user who voided itvoid_reason(varchar 255) — reason for voidingcorrects_movement_id(bigint) — links a correction to the original movementsource_type/source_id(varchar/bigint) — polymorphic source reference
app/Models/InventoryMovement.php: Added fillable fields, casts, scopes (notVoided, voided), and relationships (voidedBy, correctsMovement, corrections).
app/Services/Inventory/InventoryMovementService.php:
| Method | Status | Notes |
|---|---|---|
create() |
⚠️ Deprecated | Still works, but marked @deprecated. Use recordMovement() instead. |
update() |
⚠️ Deprecated | Still works. Prevents editing voided movements. |
delete() |
⚠️ Deprecated | Prevents deleting movements that have corrections. |
bulkDelete() |
⚠️ Deprecated | Same prevention for corrections. |
recordMovement() |
✅ Primary method | Append-only, creates initial movement automatically. |
voidMovement() |
✅ NEW | Marks original as voided + creates reverse correction. |
correctMovement() |
✅ NEW | Adds an adjusting entry linked to the original. |
recalcQuantity() |
✅ Updated | Excludes voided movements from sum calculations. |
reconcile() |
✅ NEW | Compares stored quantity vs movement sum, reports discrepancies. |
3. Reconciliation command
app/Console/Commands/InventoryReconcile.php: php artisan inventory:reconcile with --school-year and --fix options.
Phase 2: Movement and Audit Cleanup
Void and correction API endpoints
app/Http/Controllers/Api/Inventory/InventoryMovementController.php:
POST movements/{id}/void— requiresreasonPOST movements/{id}/correct— requiresqty_changeandreason
Both require authentication and return appropriate success/error responses.
Phase 3: Procurement Integration
1. inventory_item_id on purchase_order_items
New migration: 2026_06_11_074642_add_inventory_item_id_to_purchase_order_items.php
Adds inventory_item_id (unsigned int, nullable) to purchase_order_items with an index. This allows linking PO items to inventory items.
app/Models/PurchaseOrderItem.php: Added inventory_item_id to $fillable.
2. PurchaseOrderReceiveService links to inventory
app/Services/PurchaseOrders/PurchaseOrderReceiveService.php:
- Injects
InventoryMovementService - On receiving PO items with
inventory_item_idset, creates an inventory movement viarecordMovement() - Continues to update legacy
supplies.qty_on_handfor backward compatibility
Phase 4: Low-Stock and Reorder Workflow
New database fields
New migration: 2026_06_11_074621_add_inventory_item_reorder_fields.php
Adds to inventory_items:
preferred_supplier_id,last_purchase_price,average_unit_costreorder_point,reorder_quantity,lead_time_daysbarcode,qr_code,asset_tag
app/Models/InventoryItem.php: Added fillable fields, casts, relationships (preferredSupplier), and scopes (lowStock, outOfStock).
app/Http/Resources/Inventory/InventoryItemResource.php: Exposes all new fields.
New API endpoints
app/Http/Controllers/Api/Inventory/InventoryController.php:
| Method | Endpoint | Description |
|---|---|---|
lowStock() |
GET inventory/low-stock |
Lists items where quantity <= reorder_point |
reorderSuggestions() |
GET inventory/reorder-suggestions |
Calculates suggested order quantities |
reorderRequest() |
POST inventory/items/{id}/reorder-request |
Single item reorder info |
stockStatus() |
GET inventory/stock-status |
All items with status: ok/low_stock/out_of_stock |
dashboard() |
GET inventory/dashboard |
Summary counts by type, low stock, out of stock, etc. |
All routes are registered under api/v1/inventory/ in routes/api.php.
Database Migrations
| Migration | Description |
|---|---|
2026_06_11_074503_add_inventory_movement_audit_fields |
Adds void/correction fields to movements |
2026_06_11_074621_add_inventory_item_reorder_fields |
Adds reorder/supplier/barcode fields to items |
2026_06_11_074642_add_inventory_item_id_to_purchase_order_items |
Links PO items to inventory items |
Key Backward Compatibility Notes
-
Existing endpoints remain unchanged.
GET/POST/PATCH/DELETE inventory/items,inventory/movements, and all summary/distribution endpoints continue to work with the same payloads. -
Old
create/update/deletemovement methods work as before, but are marked@deprecated. They still enforce new guards (e.g., cannot edit voided movements, cannot delete movements with corrections). -
Purchase order receiving continues to update
supplies.qty_on_handfor legacy support. The new inventory movement is added alongside, not instead. -
Voided movements are excluded from quantity calculations by default. Existing reports that call
recalcQuantity()automatically benefit.
Console Commands
# Check for discrepancies between stored quantity and movement sum
php artisan inventory:reconcile
# With --fix to auto-correct discrepancies
php artisan inventory:reconcile --fix
# For a specific school year
php artisan inventory:reconcile --school-year=2025-2026
API Reference
New Endpoints
GET /api/v1/inventory/low-stock
GET /api/v1/inventory/reorder-suggestions
POST /api/v1/inventory/items/{id}/reorder-request
GET /api/v1/inventory/stock-status?type=book
GET /api/v1/inventory/dashboard?school_year=2025-2026
POST /api/v1/inventory/movements/{id}/void { reason: "..." }
POST /api/v1/inventory/movements/{id}/correct { qty_change: -5, reason: "..." }