# 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.
