Склад (Phase 1)
Источник требований
Складской учёт Phase 1: склады per-ТТ, складские остатки, партии с закупочными ценами, акты приёмки (приход ингредиентов), акты списания (порча, недостача). Средневзвешенная цена из партий используется для расчёта себестоимости в техкартах.
Сервис: Warehouse Service (:3008).
Фронт: не входит в Phase 1 (только бэкенд). Фронтенд складского учёта — отдельная BR.
Склады (warehouses)
Один склад на торговую точку. Создаётся автоматически при создании ТТ (через событие store.created из Store Service).
Поля
| Поле | Обязательность | Описание |
|---|---|---|
| Франшиза | Обязательно | franchise_id (из JWT) |
| Торговая точка | Обязательно | store_id (кросс-сервисная ссылка → Store Service) |
| Название | Обязательно | Совпадает с названием ТТ |
| Статус | Обязательно | active / inactive |
Правила
- Нельзя создать склад вручную — только автоматически при создании ТТ
- Нельзя удалить склад — привязан к жизненному циклу ТТ
- Один склад на одну ТТ (UNIQUE franchise_id + store_id)
- Название склада = название торговой точки
Складские остатки (stock balances)
Текущее количество каждого ингредиента на каждом складе. Обновляется автоматически при проводке документов (приёмка +, списание -).
Поля
| Поле | Обязательность | Описание |
|---|---|---|
| Склад | Обязательно | warehouse_id (FK) |
| Ингредиент | Обязательно | ingredient_id (FK → ingredients) |
| Текущее количество | Обязательно | Обновляется при проводке. Не может быть отрицательным |
| Средняя цена | Необязательно | Средневзвешенная закупочная цена (пересчитывается при приёмке/списании) |
| Единица измерения | Обязательно | Должна совпадать с единицей ингредиента |
| Дата обновления | Обязательно | Время последнего изменения |
Правила
- Уникальная пара (warehouse_id, ingredient_id)
- Остатки не могут быть отрицательными — валидация при списании
- Создаются/обновляются автоматически при проводке документов
Складские партии (stock batches)
Каждая приёмка создаёт партию с закупочной ценой. Партии используются для расчёта средневзвешенной цены и FIFO-списания.
Поля
| Поле | Обязательность | Описание |
|---|---|---|
| Склад | Обязательно | warehouse_id (FK) |
| Ингредиент | Обязательно | ingredient_id (FK → ingredients) |
| Закупочная цена | Обязательно | Цена за единицу из акта приёмки |
| Количество | Обязательно | Текущий остаток партии (уменьшается при списании) |
| Единица измерения | Обязательно | Совпадает с единицей ингредиента |
| Дата поступления | Обязательно | Когда партия поступила на склад |
| Срок годности | Необязательно | Если указан в акте приёмки |
| Статус | Обязательно | active / exhausted (исчерпана) |
Правила
- При списании по FIFO — сначала расходуются самые старые партии (по received_date)
- Когда количество партии достигает 0, статус = exhausted
- Средневзвешенная цена =
SUM(purchase_price * quantity) / SUM(quantity)по всем партиям с qty > 0
Акты приёмки (receipt acts)
Документ поступления ингредиентов на склад. Без поставщика (Phase 1 без справочника поставщиков).
Жизненный цикл
stateDiagram-v2 [*] --> draft : Создание draft --> posted : Проводка posted --> [*] style draft fill:#1a1a2e,stroke:#e94560,color:#fff style posted fill:#2d6a4f,stroke:#40916c,color:#fff
- draft — можно редактировать (добавлять/удалять строки, менять количества, даты, комментарий)
- posted — зафиксирован, создаёт партии, обновляет остатки. Нельзя редактировать
Поля документа
| Поле | Обязательность | Описание |
|---|---|---|
| Склад | Обязательно | warehouse_id (FK) |
| Номер документа | Обязательно | Автоинкремент per franchise |
| Дата приёмки | Обязательно | Когда приняли товар |
| Комментарий | Необязательно | Произвольный текст |
| Статус | Обязательно | draft / posted |
| Общая сумма | Вычисляемое | Сумма всех строк |
| Создал | Обязательно | created_by (user_id из JWT) |
Строки документа
| Поле | Обязательность | Описание |
|---|---|---|
| Ингредиент | Обязательно | ingredient_id (FK → ingredients) |
| Количество | Обязательно | Сколько поступило |
| Единица измерения | Обязательно | Должна совпадать с единицей ингредиента |
| Цена за единицу | Обязательно | Закупочная цена |
| Сумма строки | Вычисляемое | quantity * unit_price |
| Срок годности | Необязательно | Дата окончания срока |
При проводке (post)
- Для каждой строки создать
stock_batch(партия с закупочной ценой) - Обновить
stock_balance(+quantity) - Пересчитать
average_costингредиента на этом складе - Заблокировать редактирование документа (status = posted)
Валидации
- Нельзя провести пустой документ (без строк) —
EMPTY_DOCUMENT - Нельзя провести уже проведённый документ —
DOCUMENT_ALREADY_POSTED - Нельзя редактировать проведённый документ —
DOCUMENT_ALREADY_POSTED
Акты списания (write-off acts)
Документ списания ингредиентов со склада (порча, истечение срока, недостача).
Жизненный цикл
Аналогичен актам приёмки: draft → posted.
Поля документа
| Поле | Обязательность | Описание |
|---|---|---|
| Склад | Обязательно | warehouse_id (FK) |
| Номер документа | Обязательно | Автоинкремент per franchise |
| Дата списания | Обязательно | Когда списали |
| Причина | Обязательно | Текст: порча, истечение срока, недостача и т.д. |
| Статус | Обязательно | draft / posted |
| Общая стоимость | Вычисляемое | Себестоимость списанных ингредиентов |
| Создал | Обязательно | created_by (user_id из JWT) |
Строки документа
| Поле | Обязательность | Описание |
|---|---|---|
| Ингредиент | Обязательно | ingredient_id (FK → ingredients) |
| Количество | Обязательно | Сколько списать |
| Себестоимость единицы | Вычисляемое | Из средневзвешенной цены |
| Стоимость строки | Вычисляемое | quantity * unit_cost |
При проводке (post)
- Проверить достаточность остатков на складе
- Списать по FIFO (с самых старых партий)
- Обновить
stock_balance(-quantity) - Зафиксировать стоимость списания
Валидации
- Нельзя списать больше чем есть на складе —
INSUFFICIENT_STOCK - Нельзя провести пустой документ —
EMPTY_DOCUMENT - Нельзя провести уже проведённый —
DOCUMENT_ALREADY_POSTED
Себестоимость (средневзвешенная цена)
Per-ingredient per-warehouse. Рассчитывается из складских партий.
average_cost = SUM(batch.purchase_price * batch.quantity) / SUM(batch.quantity)
где batch.quantity > 0 (только непустые партии).
Пересчитывается при каждой приёмке и списании. Используется в техкартах для расчёта себестоимости блюд (CostCalculationService). (BR 1.14)
До BR 1.14
Средняя цена хранилась как статичное значение. Теперь рассчитывается динамически из реальных складских партий.
Ролевая матрица
Франшиза (владелец бренда)
- Просмотр остатков всех складов
- Создание/проводка приёмок на любом складе
- Создание/проводка списаний на любом складе
- Просмотр средней цены
Франчайзи (партнёр)
- Просмотр остатков своих складов (по store_ids из JWT)
- Создание/проводка приёмок на своих складах
- Создание/проводка списаний на своих складах
- Просмотр средней цены своих складов
Менеджер ТТ
- Просмотр остатков своего склада
- Создание/проводка приёмок на своём складе
- Создание/проводка списаний на своём складе
- Просмотр средней цены своего склада
Кассир
- Нет доступа к складскому учёту
Бизнес-правила
- Склад = ТТ — один склад на торговую точку, автоматическое создание
- Draft → Posted — документ проводится один раз, после этого неизменяем
- FIFO — при списании сначала расходуются самые старые партии
- Неотрицательные остатки — нельзя списать больше чем есть на складе
- Средневзвешенная — цена считается по партиям с положительным количеством
- Единица измерения — при приёмке/списании должна совпадать с единицей ингредиента (или конвертация через unit_conversions)
Что НЕ входит (Phase 2+)
- Поставщики (справочник контрагентов)
- Акты перемещения между складами
- Инвентаризация (сверка факт vs учёт)
- Акты производства (списание по техкарте при изготовлении)
- Автоматическое списание при продаже (интеграция с Order Service)
- Авто-стоп по остаткам (интеграция со стоп-листами)
- Фронт-интерфейс складского учёта
- Партионный учёт по срокам годности (алерты)
- Возвраты поставщику
Связи с другими модулями
- Техкарты — себестоимость рассчитывается из средневзвешенной цены складских партий
- Store Service — при создании ТТ автоматически создаётся склад
- Стоп-листы — Phase 2: авто-стоп по остаткам