- 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>
771 lines
29 KiB
Markdown
771 lines
29 KiB
Markdown
# 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.
|