fix inventory

This commit is contained in:
root
2026-06-11 11:06:32 -04:00
parent 9483750161
commit c91fa2ce4d
53 changed files with 2936 additions and 12666 deletions
@@ -0,0 +1,162 @@
# Inventory System Improvement — Implementation Summary
## Overview
This implementation applies the first three sprints of the inventory improvement plan:
1. **Stabilize Inventory Data** — Make movement ledger the source of truth
2. **Movement and Audit Cleanup** — Append-only ledger with void/correction support
3. **Procurement Integration** — Link purchase orders to inventory movements
4. **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`**: Removed `quantity` from validatable fields. Stock changes must go through movements (adjust, audit, etc.).
- **`app/Services/Inventory/InventoryItemService.php`**: `filterItemData()` now only includes `quantity` during creation (`$lockedType === null`), not during updates.
- **`app/Http/Requests/Inventory/InventoryItemStoreRequest.php`**: Strengthened validation — `type` uses `in:classroom,book,office,kitchen`, `condition` uses `in:good,needs_repair,need_replace,cannot_find`, `category_id` checks `exists: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 voided
- `voided_by` (bigint) — user who voided it
- `void_reason` (varchar 255) — reason for voiding
- `corrects_movement_id` (bigint) — links a correction to the original movement
- `source_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` — requires `reason`
- `POST movements/{id}/correct` — requires `qty_change` and `reason`
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_id` set, creates an inventory movement via `recordMovement()`
- Continues to update legacy `supplies.qty_on_hand` for 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_cost`
- `reorder_point`, `reorder_quantity`, `lead_time_days`
- `barcode`, `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
1. **Existing endpoints remain unchanged.** `GET/POST/PATCH/DELETE inventory/items`, `inventory/movements`, and all summary/distribution endpoints continue to work with the same payloads.
2. **Old `create/update/delete` movement methods** work as before, but are marked `@deprecated`. They still enforce new guards (e.g., cannot edit voided movements, cannot delete movements with corrections).
3. **Purchase order receiving** continues to update `supplies.qty_on_hand` for legacy support. The new inventory movement is added alongside, not instead.
4. **Voided movements** are excluded from quantity calculations by default. Existing reports that call `recalcQuantity()` automatically benefit.
---
## Console Commands
```bash
# 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: "..." }
```