feat(odin): Phase 2a read-only agentic assistant + tool-trace UI
- agentic chat loop (max 6 round-trips, budget re-checked between iterations) over 10 read-only, permission-delegated tools in src/services/ai-tools.ts - persist assistant tool trace in ai_chat_messages.content_json (+ migration) - consulted-tools chips in OdinThread; header now shows month spend only (budget cap hidden from the UI, server-side 402 guard unchanged) - seed: ai.use permission; Settings exposes the ai permission module - docs: REVIEW consolidation; deployment-guide.md superseded by docs/release.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
770
docs/superpowers/specs/2026-05-29-warehouse-module-design.md
Normal file
770
docs/superpowers/specs/2026-05-29-warehouse-module-design.md
Normal file
@@ -0,0 +1,770 @@
|
||||
# Warehouse (Sklad) Module Design
|
||||
|
||||
**Date:** 2026-05-29
|
||||
**Status:** Approved
|
||||
**Module:** Warehouse/Inventory management for boha-app-ts
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Full warehouse management module under the Administration section. Tracks material receiving, issuing, reservations, inventory, and project assignment. Uses true FIFO with batch-level tracking, document-driven architecture (header + lines), and integrates with existing projects, number sequences, and NAS file storage.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### Movements
|
||||
|
||||
- **Receipt (Prijem):** Material enters warehouse. Multi-item document with delivery note attachment.
|
||||
- **Issue (Vydej):** Material leaves warehouse, mandatory project assignment. Multi-item document.
|
||||
- **Reservation:** Virtual hold on stock before issue. Reduces available qty but not physical stock. Issue consumes reservation.
|
||||
- **Inventory (Inventura):** Corrective movements based on physical count vs system count.
|
||||
- **Storno:** Cancel a confirmed receipt or issue. Creates opposite corrective movements, preserves audit trail.
|
||||
|
||||
### Catalog
|
||||
|
||||
- Each material has: catalog number, name, description, category, unit, min quantity.
|
||||
- Categories are user-defined (CRUD).
|
||||
- Units are fixed list: ks, m, kg, bal, sada, m2, m3, l.
|
||||
- Items can be soft-deactivated (is_active=false).
|
||||
|
||||
### Pricing
|
||||
|
||||
- Purchase price per unit, recorded bez DPH.
|
||||
- True FIFO: each receipt line creates a batch. Issues consume oldest batches first.
|
||||
- Batch-level tracking with remaining quantity.
|
||||
|
||||
### Delivery Notes
|
||||
|
||||
- File attachment (PDF/image) uploaded to NAS.
|
||||
- Delivery note number and date stored as metadata on receipt header.
|
||||
|
||||
### Suppliers
|
||||
|
||||
- Separate `sklad_suppliers` table (ICO, DIC, contact info).
|
||||
- Not shared with customers table.
|
||||
|
||||
### Locations
|
||||
|
||||
- Warehouse has named locations/shelves (code + name, e.g. "A1 - Regal A, police 1").
|
||||
- Junction table tracks qty per item per location.
|
||||
|
||||
### Projects
|
||||
|
||||
- Every issue is mandatory FK to `projects` table.
|
||||
- No free/issues without project assignment.
|
||||
|
||||
### Min Stock Alerts
|
||||
|
||||
- Each item has optional `min_quantity`.
|
||||
- Dashboard highlights items below minimum.
|
||||
- No email notification (UI alert only).
|
||||
|
||||
### Immutability
|
||||
|
||||
- All movements are immutable once confirmed.
|
||||
- Corrections via storno (cancel receipt/issue) which creates opposite movements.
|
||||
- Draft movements can be edited freely.
|
||||
|
||||
### Numbering
|
||||
|
||||
- Auto-numbering via `number_sequences` on confirm.
|
||||
- Types: `warehouse_receipt` (e.g. PRI-2026-001), `warehouse_issue` (e.g. VYD-2026-001).
|
||||
|
||||
### Issue Documents
|
||||
|
||||
- No PDF generation for issues. Record only in system.
|
||||
|
||||
### Reports
|
||||
|
||||
- Stock status: all items with qty, min_qty, value, below-min flag.
|
||||
- Project consumption: material consumed per project (filter by project, date range).
|
||||
- Movement log: all receipts + issues with filters.
|
||||
- Below minimum: items under min_quantity.
|
||||
|
||||
---
|
||||
|
||||
## Architecture: Document-Driven
|
||||
|
||||
Each movement type is a document (header + lines), matching the existing invoice/offer pattern.
|
||||
|
||||
### Document Flow
|
||||
|
||||
```
|
||||
DRAFT → CONFIRMED (affects stock)
|
||||
DRAFT → CANCELLED (no stock effect)
|
||||
CONFIRMED → CANCELLED (storno: reverses stock changes)
|
||||
```
|
||||
|
||||
Only CONFIRMED documents affect `sklad_batches`, `sklad_item_locations`, and reservations.
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Enums
|
||||
|
||||
```prisma
|
||||
enum sklad_receipt_status {
|
||||
DRAFT
|
||||
CONFIRMED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
enum sklad_issue_status {
|
||||
DRAFT
|
||||
CONFIRMED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
enum sklad_reservation_status {
|
||||
ACTIVE
|
||||
FULFILLED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
enum sklad_inventory_status {
|
||||
DRAFT
|
||||
CONFIRMED
|
||||
}
|
||||
```
|
||||
|
||||
### Master Data
|
||||
|
||||
```prisma
|
||||
model sklad_categories {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @db.VarChar(100)
|
||||
description String? @db.Text
|
||||
sort_order Int @default(0)
|
||||
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||
modified_at DateTime? @db.DateTime(0)
|
||||
|
||||
items sklad_items[]
|
||||
|
||||
@@map("sklad_categories")
|
||||
}
|
||||
|
||||
model sklad_suppliers {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @db.VarChar(255)
|
||||
ico String? @db.VarChar(20)
|
||||
dic String? @db.VarChar(20)
|
||||
contact_person String? @db.VarChar(255)
|
||||
email String? @db.VarChar(255)
|
||||
phone String? @db.VarChar(50)
|
||||
address String? @db.Text
|
||||
notes String? @db.Text
|
||||
is_active Boolean @default(true)
|
||||
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||
modified_at DateTime? @db.DateTime(0)
|
||||
|
||||
receipts sklad_receipts[]
|
||||
|
||||
@@map("sklad_suppliers")
|
||||
}
|
||||
|
||||
model sklad_locations {
|
||||
id Int @id @default(autoincrement())
|
||||
code String @unique @db.VarChar(20)
|
||||
name String @db.VarChar(100)
|
||||
description String? @db.Text
|
||||
is_active Boolean @default(true)
|
||||
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||
modified_at DateTime? @db.DateTime(0)
|
||||
|
||||
items sklad_item_locations[]
|
||||
|
||||
@@map("sklad_locations")
|
||||
}
|
||||
|
||||
model sklad_items {
|
||||
id Int @id @default(autoincrement())
|
||||
item_number String? @unique @db.VarChar(50)
|
||||
name String @db.VarChar(255)
|
||||
description String? @db.Text
|
||||
category_id Int?
|
||||
unit String @db.VarChar(20)
|
||||
min_quantity Decimal? @db.Decimal(12, 3)
|
||||
is_active Boolean @default(true)
|
||||
notes String? @db.Text
|
||||
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||
modified_at DateTime? @db.DateTime(0)
|
||||
|
||||
category sklad_categories? @relation(fields: [category_id], references: [id])
|
||||
batches sklad_batches[]
|
||||
item_locations sklad_item_locations[]
|
||||
|
||||
@@index([category_id], map: "sklad_items_category_id")
|
||||
@@map("sklad_items")
|
||||
}
|
||||
```
|
||||
|
||||
### FIFO Batches & Location Tracking
|
||||
|
||||
```prisma
|
||||
model sklad_batches {
|
||||
id Int @id @default(autoincrement())
|
||||
item_id Int
|
||||
receipt_line_id Int
|
||||
quantity Decimal @db.Decimal(12, 3)
|
||||
original_qty Decimal @db.Decimal(12, 3)
|
||||
unit_price Decimal @db.Decimal(12, 2)
|
||||
received_at DateTime? @db.DateTime(0)
|
||||
is_consumed Boolean @default(false)
|
||||
|
||||
item sklad_items @relation(fields: [item_id], references: [id])
|
||||
receipt_line sklad_receipt_lines @relation(fields: [receipt_line_id], references: [id])
|
||||
|
||||
@@index([item_id, is_consumed, received_at], map: "sklad_batches_fifo")
|
||||
@@map("sklad_batches")
|
||||
}
|
||||
|
||||
model sklad_item_locations {
|
||||
id Int @id @default(autoincrement())
|
||||
item_id Int
|
||||
location_id Int
|
||||
quantity Decimal @default(0) @db.Decimal(12, 3)
|
||||
|
||||
item sklad_items @relation(fields: [item_id], references: [id])
|
||||
location sklad_locations @relation(fields: [location_id], references: [id])
|
||||
|
||||
@@unique([item_id, location_id], map: "sklad_item_locations_unique")
|
||||
@@map("sklad_item_locations")
|
||||
}
|
||||
```
|
||||
|
||||
### Receipts
|
||||
|
||||
```prisma
|
||||
model sklad_receipts {
|
||||
id Int @id @default(autoincrement())
|
||||
receipt_number String? @db.VarChar(50)
|
||||
supplier_id Int?
|
||||
delivery_note_number String? @db.VarChar(100)
|
||||
delivery_note_date DateTime? @db.DateTime(0)
|
||||
received_by Int?
|
||||
notes String? @db.Text
|
||||
status sklad_receipt_status @default(DRAFT)
|
||||
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||
modified_at DateTime? @db.DateTime(0)
|
||||
|
||||
supplier sklad_suppliers? @relation(fields: [supplier_id], references: [id])
|
||||
received_by_user users? @relation(fields: [received_by], references: [id])
|
||||
lines sklad_receipt_lines[]
|
||||
attachments sklad_receipt_attachments[]
|
||||
|
||||
@@map("sklad_receipts")
|
||||
}
|
||||
|
||||
model sklad_receipt_lines {
|
||||
id Int @id @default(autoincrement())
|
||||
receipt_id Int
|
||||
item_id Int
|
||||
quantity Decimal @db.Decimal(12, 3)
|
||||
unit_price Decimal @db.Decimal(12, 2)
|
||||
location_id Int?
|
||||
notes String? @db.VarChar(255)
|
||||
|
||||
receipt sklad_receipts @relation(fields: [receipt_id], references: [id])
|
||||
item sklad_items @relation(fields: [item_id], references: [id])
|
||||
location sklad_locations? @relation(fields: [location_id], references: [id])
|
||||
batch sklad_batches?
|
||||
|
||||
@@index([receipt_id], map: "sklad_receipt_lines_receipt_id")
|
||||
@@map("sklad_receipt_lines")
|
||||
}
|
||||
|
||||
model sklad_receipt_attachments {
|
||||
id Int @id @default(autoincrement())
|
||||
receipt_id Int
|
||||
file_name String @db.VarChar(255)
|
||||
file_mime String @db.VarChar(100)
|
||||
file_size Int
|
||||
file_path String @db.VarChar(500)
|
||||
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||
|
||||
receipt sklad_receipts @relation(fields: [receipt_id], references: [id])
|
||||
|
||||
@@map("sklad_receipt_attachments")
|
||||
}
|
||||
```
|
||||
|
||||
### Issues
|
||||
|
||||
```prisma
|
||||
model sklad_issues {
|
||||
id Int @id @default(autoincrement())
|
||||
issue_number String? @db.VarChar(50)
|
||||
project_id Int
|
||||
issued_by Int?
|
||||
notes String? @db.Text
|
||||
status sklad_issue_status @default(DRAFT)
|
||||
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||
modified_at DateTime? @db.DateTime(0)
|
||||
|
||||
project projects @relation(fields: [project_id], references: [id])
|
||||
issued_by_user users? @relation(fields: [issued_by], references: [id])
|
||||
lines sklad_issue_lines[]
|
||||
|
||||
@@map("sklad_issues")
|
||||
}
|
||||
|
||||
model sklad_issue_lines {
|
||||
id Int @id @default(autoincrement())
|
||||
issue_id Int
|
||||
item_id Int
|
||||
batch_id Int
|
||||
quantity Decimal @db.Decimal(12, 3)
|
||||
location_id Int?
|
||||
reservation_id Int?
|
||||
notes String? @db.VarChar(255)
|
||||
|
||||
issue sklad_issues @relation(fields: [issue_id], references: [id])
|
||||
item sklad_items @relation(fields: [item_id], references: [id])
|
||||
batch sklad_batches @relation(fields: [batch_id], references: [id])
|
||||
location sklad_locations? @relation(fields: [location_id], references: [id])
|
||||
reservation sklad_reservations? @relation(fields: [reservation_id], references: [id])
|
||||
|
||||
@@index([issue_id], map: "sklad_issue_lines_issue_id")
|
||||
@@map("sklad_issue_lines")
|
||||
}
|
||||
```
|
||||
|
||||
### Reservations
|
||||
|
||||
```prisma
|
||||
model sklad_reservations {
|
||||
id Int @id @default(autoincrement())
|
||||
item_id Int
|
||||
project_id Int
|
||||
quantity Decimal @db.Decimal(12, 3)
|
||||
remaining_qty Decimal @db.Decimal(12, 3)
|
||||
reserved_by Int?
|
||||
notes String? @db.Text
|
||||
status sklad_reservation_status @default(ACTIVE)
|
||||
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||
modified_at DateTime? @db.DateTime(0)
|
||||
|
||||
item sklad_items @relation(fields: [item_id], references: [id])
|
||||
project projects @relation(fields: [project_id], references: [id])
|
||||
reserved_by_user users? @relation(fields: [reserved_by], references: [id])
|
||||
issue_lines sklad_issue_lines[]
|
||||
|
||||
@@index([item_id, status], map: "sklad_reservations_item_status")
|
||||
@@map("sklad_reservations")
|
||||
}
|
||||
```
|
||||
|
||||
### Inventory
|
||||
|
||||
```prisma
|
||||
model sklad_inventory_sessions {
|
||||
id Int @id @default(autoincrement())
|
||||
session_number String? @db.VarChar(50)
|
||||
notes String? @db.Text
|
||||
status sklad_inventory_status @default(DRAFT)
|
||||
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||
modified_at DateTime? @db.DateTime(0)
|
||||
|
||||
lines sklad_inventory_lines[]
|
||||
|
||||
@@map("sklad_inventory_sessions")
|
||||
}
|
||||
|
||||
model sklad_inventory_lines {
|
||||
id Int @id @default(autoincrement())
|
||||
session_id Int
|
||||
item_id Int
|
||||
location_id Int?
|
||||
system_qty Decimal @db.Decimal(12, 3)
|
||||
actual_qty Decimal @db.Decimal(12, 3)
|
||||
difference Decimal @db.Decimal(12, 3)
|
||||
notes String? @db.VarChar(255)
|
||||
|
||||
session sklad_inventory_sessions @relation(fields: [session_id], references: [id])
|
||||
item sklad_items @relation(fields: [item_id], references: [id])
|
||||
location sklad_locations? @relation(fields: [location_id], references: [id])
|
||||
|
||||
@@index([session_id], map: "sklad_inventory_lines_session_id")
|
||||
@@map("sklad_inventory_lines")
|
||||
}
|
||||
```
|
||||
|
||||
**Total: 13 models + 5 enums**
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
All under prefix `/api/admin/warehouse`.
|
||||
|
||||
### Items (warehouse.manage)
|
||||
|
||||
| Method | Path | Description |
|
||||
| ------ | ------------ | ------------------------------------------------------------- |
|
||||
| GET | `/items` | List items (paginated, filter by category, search, below-min) |
|
||||
| GET | `/items/:id` | Item detail with batches, locations, reservations |
|
||||
| POST | `/items` | Create item |
|
||||
| PUT | `/items/:id` | Update item |
|
||||
| DELETE | `/items/:id` | Soft-delete (is_active=false) |
|
||||
|
||||
### Categories (warehouse.manage)
|
||||
|
||||
| Method | Path | Description |
|
||||
| ------ | ----------------- | ------------------------- |
|
||||
| GET | `/categories` | List all |
|
||||
| POST | `/categories` | Create |
|
||||
| PUT | `/categories/:id` | Update |
|
||||
| DELETE | `/categories/:id` | Delete (only if no items) |
|
||||
|
||||
### Suppliers (warehouse.manage)
|
||||
|
||||
| Method | Path | Description |
|
||||
| ------ | ---------------- | ----------------------------- |
|
||||
| GET | `/suppliers` | List (paginated, search) |
|
||||
| GET | `/suppliers/:id` | Detail |
|
||||
| POST | `/suppliers` | Create |
|
||||
| PUT | `/suppliers/:id` | Update |
|
||||
| DELETE | `/suppliers/:id` | Soft-delete (is_active=false) |
|
||||
|
||||
### Locations (warehouse.manage)
|
||||
|
||||
| Method | Path | Description |
|
||||
| ------ | ---------------- | ------------------------------------- |
|
||||
| GET | `/locations` | List all |
|
||||
| POST | `/locations` | Create |
|
||||
| PUT | `/locations/:id` | Update |
|
||||
| DELETE | `/locations/:id` | Delete (only if no items at location) |
|
||||
|
||||
### Receipts (warehouse.operate)
|
||||
|
||||
| Method | Path | Description |
|
||||
| ------ | ----------------------------------------- | ------------------------------------------------------------- |
|
||||
| GET | `/receipts` | List (paginated, filter by status/supplier/date) |
|
||||
| GET | `/receipts/:id` | Detail with lines + attachments |
|
||||
| POST | `/receipts` | Create receipt (DRAFT, with lines) |
|
||||
| PUT | `/receipts/:id` | Update receipt + lines (DRAFT only) |
|
||||
| POST | `/receipts/:id/confirm` | Confirm: creates batches, updates locations, generates number |
|
||||
| POST | `/receipts/:id/cancel` | Cancel: storno batches if confirmed |
|
||||
| POST | `/receipts/:id/attachments` | Upload delivery note (multipart) |
|
||||
| DELETE | `/receipts/:id/attachments/:attachmentId` | Delete attachment |
|
||||
|
||||
### Issues (warehouse.operate)
|
||||
|
||||
| Method | Path | Description |
|
||||
| ------ | --------------------- | --------------------------------------------------------------------------------------- |
|
||||
| GET | `/issues` | List (paginated, filter by status/project/date) |
|
||||
| GET | `/issues/:id` | Detail with lines |
|
||||
| POST | `/issues` | Create issue (DRAFT, with lines) |
|
||||
| PUT | `/issues/:id` | Update issue + lines (DRAFT only) |
|
||||
| POST | `/issues/:id/confirm` | Confirm: decrements batches, updates locations, fulfills reservations, generates number |
|
||||
| POST | `/issues/:id/cancel` | Cancel: restores batch quantities if confirmed |
|
||||
|
||||
### Reservations (warehouse.operate)
|
||||
|
||||
| Method | Path | Description |
|
||||
| ------ | -------------------------- | ------------------------------------ |
|
||||
| GET | `/reservations` | List (filter by item/project/status) |
|
||||
| POST | `/reservations` | Create reservation |
|
||||
| POST | `/reservations/:id/cancel` | Cancel reservation |
|
||||
|
||||
### Inventory (warehouse.inventory)
|
||||
|
||||
| Method | Path | Description |
|
||||
| ------ | --------------------------------- | --------------------------------------- |
|
||||
| GET | `/inventory-sessions` | List sessions |
|
||||
| GET | `/inventory-sessions/:id` | Detail with lines |
|
||||
| POST | `/inventory-sessions` | Create session (system_qty auto-filled) |
|
||||
| PUT | `/inventory-sessions/:id` | Update lines (DRAFT only) |
|
||||
| POST | `/inventory-sessions/:id/confirm` | Confirm: creates corrective movements |
|
||||
|
||||
### Reports (warehouse.view)
|
||||
|
||||
| Method | Path | Description |
|
||||
| ------ | ------------------------------ | ---------------------------------------------------- |
|
||||
| GET | `/reports/stock-status` | All items with qty, min_qty, value, below-min flag |
|
||||
| GET | `/reports/project-consumption` | Material per project (filter by project, date range) |
|
||||
| GET | `/reports/movement-log` | All movements with filters |
|
||||
| GET | `/reports/below-minimum` | Items below min_quantity |
|
||||
|
||||
### Utility (warehouse.view)
|
||||
|
||||
| Method | Path | Description |
|
||||
| ------ | -------------------------- | -------------------------------------- |
|
||||
| GET | `/items/:id/available-qty` | Available qty (total stock - reserved) |
|
||||
| GET | `/items/:id/batches` | FIFO batch queue for item |
|
||||
|
||||
---
|
||||
|
||||
## Business Logic
|
||||
|
||||
### Receipt Confirm (single Prisma transaction)
|
||||
|
||||
1. Validate status is DRAFT
|
||||
2. Generate `receipt_number` via `number_sequences` (type: `warehouse_receipt`)
|
||||
3. For each receipt line:
|
||||
- Create `sklad_batches` row (quantity = original_qty = line qty, unit_price from line, received_at = now)
|
||||
- Upsert `sklad_item_locations` (add qty to specified location)
|
||||
4. Update receipt status to CONFIRMED
|
||||
5. Log audit
|
||||
|
||||
### Receipt Cancel
|
||||
|
||||
**If DRAFT:** Set status to CANCELLED. No stock changes.
|
||||
|
||||
**If CONFIRMED (storno):**
|
||||
|
||||
1. Validate no issue lines have consumed batches from this receipt
|
||||
2. For each receipt line / batch:
|
||||
- If batch is fully consumed (is_consumed=true), reject cancellation
|
||||
- Decrement `sklad_item_locations` by batch remaining quantity
|
||||
- Delete the batch row
|
||||
3. Set receipt status to CANCELLED
|
||||
4. Log audit
|
||||
|
||||
### Issue Confirm (single Prisma transaction)
|
||||
|
||||
1. Validate status is DRAFT
|
||||
2. For each issue line, validate: batch has enough remaining quantity
|
||||
3. Generate `issue_number` via `number_sequences` (type: `warehouse_issue`)
|
||||
4. For each issue line:
|
||||
- Decrement `sklad_batches.quantity`, set `is_consumed=true` if 0
|
||||
- Decrement `sklad_item_locations.quantity`
|
||||
- If `reservation_id` set, decrement `sklad_reservations.remaining_qty`
|
||||
5. Check reservations: if remaining_qty = 0, set status to FULFILLED
|
||||
6. Update issue status to CONFIRMED
|
||||
7. Log audit
|
||||
|
||||
### Issue Cancel
|
||||
|
||||
**If DRAFT:** Set status to CANCELLED. No stock changes.
|
||||
|
||||
**If CONFIRMED (storno):**
|
||||
|
||||
1. For each issue line:
|
||||
- Increment `sklad_batches.quantity`, set `is_consumed=false` if was true
|
||||
- Increment `sklad_item_locations.quantity`
|
||||
- If `reservation_id` set, increment `sklad_reservations.remaining_qty`, set status back to ACTIVE if was FULFILLED
|
||||
2. Set issue status to CANCELLED
|
||||
3. Log audit
|
||||
|
||||
### Auto-FIFO on Issue Creation
|
||||
|
||||
When creating an issue line, if user doesn't pick a specific batch:
|
||||
|
||||
- Service selects oldest unconsumed batches for the item (via `sklad_batches_fifo` index)
|
||||
- May span multiple batches if quantity exceeds one batch's remaining qty
|
||||
- Frontend shows available batches for manual override
|
||||
|
||||
### Reservation Creation
|
||||
|
||||
1. Validate item exists and is_active
|
||||
2. Calculate available_qty = total stock qty - sum of active reservation quantities for this item
|
||||
3. Validate available_qty >= requested quantity
|
||||
4. Create reservation with quantity = remaining_qty
|
||||
5. Does NOT modify `sklad_batches` or `sklad_item_locations`
|
||||
|
||||
### Inventory Confirm (single Prisma transaction)
|
||||
|
||||
1. Validate status is DRAFT
|
||||
2. For each inventory line where difference != 0:
|
||||
- If difference > 0 (more stock than system): create a corrective receipt (type: inventory, auto-confirmed)
|
||||
- If difference < 0 (less stock than system): create a corrective issue (type: inventory, auto-confirmed)
|
||||
3. Generate `session_number` via `number_sequences` (type: `warehouse_inventory`)
|
||||
4. Set status to CONFIRMED
|
||||
5. Log audit
|
||||
|
||||
---
|
||||
|
||||
## Frontend
|
||||
|
||||
### Sidebar
|
||||
|
||||
Entry in `Sidebar.tsx` with permission `warehouse.view`:
|
||||
|
||||
- Icon: package/box icon
|
||||
- Label: "Sklad"
|
||||
- Path: `/warehouse`
|
||||
|
||||
### Pages (lazy-loaded)
|
||||
|
||||
| Route | Component | Permission |
|
||||
| ------------------------------ | ------------------------ | ------------------- |
|
||||
| `/warehouse` | Warehouse | warehouse.view |
|
||||
| `/warehouse/items` | WarehouseItems | warehouse.view |
|
||||
| `/warehouse/items/:id` | WarehouseItemDetail | warehouse.view |
|
||||
| `/warehouse/receipts` | WarehouseReceipts | warehouse.view |
|
||||
| `/warehouse/receipts/new` | WarehouseReceiptForm | warehouse.operate |
|
||||
| `/warehouse/receipts/:id` | WarehouseReceiptDetail | warehouse.view |
|
||||
| `/warehouse/receipts/:id/edit` | WarehouseReceiptForm | warehouse.operate |
|
||||
| `/warehouse/issues` | WarehouseIssues | warehouse.view |
|
||||
| `/warehouse/issues/new` | WarehouseIssueForm | warehouse.operate |
|
||||
| `/warehouse/issues/:id` | WarehouseIssueDetail | warehouse.view |
|
||||
| `/warehouse/issues/:id/edit` | WarehouseIssueForm | warehouse.operate |
|
||||
| `/warehouse/reservations` | WarehouseReservations | warehouse.view |
|
||||
| `/warehouse/inventory` | WarehouseInventory | warehouse.inventory |
|
||||
| `/warehouse/inventory/new` | WarehouseInventoryForm | warehouse.inventory |
|
||||
| `/warehouse/inventory/:id` | WarehouseInventoryDetail | warehouse.inventory |
|
||||
| `/warehouse/reports` | WarehouseReports | warehouse.view |
|
||||
| `/warehouse/suppliers` | WarehouseSuppliers | warehouse.manage |
|
||||
| `/warehouse/locations` | WarehouseLocations | warehouse.manage |
|
||||
| `/warehouse/categories` | WarehouseCategories | warehouse.manage |
|
||||
|
||||
### Warehouse Dashboard
|
||||
|
||||
- Summary cards: total items, total stock value, below-minimum count, active reservations
|
||||
- Below-minimum alert section (highlighted)
|
||||
- Recent movements (last 10)
|
||||
|
||||
### WarehouseReports
|
||||
|
||||
- Tab-based: Stock Status | Project Consumption | Movement Log | Below Minimum
|
||||
- Each tab has filters (date range, project, category, supplier)
|
||||
- Paginated data tables with sorting
|
||||
|
||||
### Key Shared Components
|
||||
|
||||
| Component | Purpose |
|
||||
| ---------------------- | ----------------------------------------------------------------------------- |
|
||||
| ItemPicker | Searchable item selector (name + catalog number), used in receipt/issue forms |
|
||||
| BatchPicker | FIFO batch selector for issue lines, shows available qty per batch |
|
||||
| LocationSelect | Location dropdown |
|
||||
| SupplierSelect | Supplier dropdown |
|
||||
| WarehouseMovementTable | Reusable multi-line editor for receipt/issue lines |
|
||||
|
||||
### Query Keys
|
||||
|
||||
```
|
||||
["warehouse"] -> invalidates all
|
||||
["warehouse", "items", filters] -> item list
|
||||
["warehouse", "items", id] -> item detail
|
||||
["warehouse", "receipts", filters] -> receipt list
|
||||
["warehouse", "receipts", id] -> receipt detail
|
||||
["warehouse", "issues", filters] -> issue list
|
||||
["warehouse", "issues", id] -> issue detail
|
||||
["warehouse", "reservations", filters] -> reservation list
|
||||
["warehouse", "inventory", filters] -> inventory sessions
|
||||
["warehouse", "inventory", id] -> inventory detail
|
||||
["warehouse", "suppliers", filters] -> supplier list
|
||||
["warehouse", "locations"] -> location list
|
||||
["warehouse", "categories"] -> category list
|
||||
["warehouse", "reports", type, filters] -> report data
|
||||
```
|
||||
|
||||
Mutation invalidation: broad `["warehouse"]` for any create/update/delete.
|
||||
|
||||
---
|
||||
|
||||
## Permissions
|
||||
|
||||
| Permission | Display Name | Module | Description |
|
||||
| --------------------- | -------------- | ------ | ------------------------------------------------------ |
|
||||
| `warehouse.view` | Zobrazit sklad | sklad | View stock status, items, reports, movement history |
|
||||
| `warehouse.operate` | Prijem a vydej | sklad | Create/confirm receipts, issues, reservations |
|
||||
| `warehouse.manage` | Sprava skladu | sklad | Manage items catalog, suppliers, locations, categories |
|
||||
| `warehouse.inventory` | Inventura | sklad | Create/confirm inventory sessions |
|
||||
|
||||
---
|
||||
|
||||
## Number Sequences
|
||||
|
||||
Three new types registered in `number_sequences`:
|
||||
|
||||
- `warehouse_receipt` - generated on receipt confirm
|
||||
- `warehouse_issue` - generated on issue confirm
|
||||
- `warehouse_inventory` - generated on inventory session confirm
|
||||
|
||||
Pattern configurable via `company_settings`, same as invoices.
|
||||
|
||||
---
|
||||
|
||||
## File Storage
|
||||
|
||||
Receipt attachments use existing `NasFinancialsManager`:
|
||||
|
||||
- Stored under `warehouse/YYYY/MM/` on NAS
|
||||
- DB metadata in `sklad_receipt_attachments` (file_name, file_mime, file_size, file_path)
|
||||
- Upload via `@fastify/multipart`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
### New Files
|
||||
|
||||
```
|
||||
src/
|
||||
routes/admin/warehouse.ts
|
||||
services/warehouse.service.ts
|
||||
schemas/warehouse.schema.ts
|
||||
admin/
|
||||
pages/
|
||||
Warehouse.tsx
|
||||
WarehouseItems.tsx
|
||||
WarehouseItemDetail.tsx
|
||||
WarehouseReceipts.tsx
|
||||
WarehouseReceiptDetail.tsx
|
||||
WarehouseReceiptForm.tsx
|
||||
WarehouseIssues.tsx
|
||||
WarehouseIssueDetail.tsx
|
||||
WarehouseIssueForm.tsx
|
||||
WarehouseReservations.tsx
|
||||
WarehouseInventory.tsx
|
||||
WarehouseInventoryDetail.tsx
|
||||
WarehouseInventoryForm.tsx
|
||||
WarehouseReports.tsx
|
||||
WarehouseSuppliers.tsx
|
||||
WarehouseLocations.tsx
|
||||
WarehouseCategories.tsx
|
||||
lib/queries/warehouse.ts
|
||||
components/warehouse/
|
||||
ItemPicker.tsx
|
||||
BatchPicker.tsx
|
||||
LocationSelect.tsx
|
||||
SupplierSelect.tsx
|
||||
WarehouseMovementTable.tsx
|
||||
```
|
||||
|
||||
### Modified Files
|
||||
|
||||
| File | Change |
|
||||
| ---------------------------------- | ---------------------------------- |
|
||||
| `src/server.ts` | Import + register warehouse routes |
|
||||
| `src/types/index.ts` | Add 8 EntityType values |
|
||||
| `src/admin/AdminApp.tsx` | Lazy imports + routes |
|
||||
| `src/admin/components/Sidebar.tsx` | Add warehouse menu entry |
|
||||
| `prisma/schema.prisma` | Add 13 models + 5 enums |
|
||||
| `prisma/seed.ts` | Add 4 permissions |
|
||||
|
||||
---
|
||||
|
||||
## Audit Entity Types
|
||||
|
||||
New values added to `EntityType` union in `src/types/index.ts`:
|
||||
|
||||
- `warehouse_item`
|
||||
- `warehouse_receipt`
|
||||
- `warehouse_issue`
|
||||
- `warehouse_reservation`
|
||||
- `warehouse_inventory`
|
||||
- `warehouse_supplier`
|
||||
- `warehouse_category`
|
||||
- `warehouse_location`
|
||||
|
||||
---
|
||||
|
||||
## Constraints & Edge Cases
|
||||
|
||||
1. **Batch consumption on cancel:** If any batch from a receipt has been partially or fully consumed by an issue, the receipt cannot be cancelled. User must first cancel the consuming issues.
|
||||
|
||||
2. **Reservation availability:** Available qty = sum of active batch quantities - sum of active reservation quantities. Reservation creation validates available_qty >= requested.
|
||||
|
||||
3. **Location qty consistency:** `sklad_item_locations` is a denormalized cache for quick lookups. It must always stay in sync with `sklad_batches`. The confirm/cancel transactions maintain this.
|
||||
|
||||
4. **FIFO spanning batches:** When auto-FIFO selects batches for an issue line and the quantity spans multiple batches, the service creates **multiple issue lines** (one per batch consumed), all under the same issue document. The user's original requested quantity is preserved across the split lines. This works because each `sklad_issue_lines` row has exactly one `batch_id`.
|
||||
|
||||
5. **Inventory corrective movements:** On confirm, each line with a difference creates an auto-confirmed receipt or issue. The `notes` field of these corrective documents references the inventory session ID and line, so they can be traced back. These corrective documents are fully audited like manual receipts/issues.
|
||||
|
||||
6. **Unit consistency:** Once an item is created with a unit, it cannot be changed if any movements exist. This prevents unit mismatches in stock calculations.
|
||||
Reference in New Issue
Block a user