Остатки на складах
Что это
Отчёт по текущим остаткам номенклатур на выбранных складах корпорации. Бухгалтер/менеджер видит сводную картину «что и сколько лежит на каждом складе», сравнивает склады через колонки-выборки, ищет позиции по критерию (все / меньше минимума / больше максимума), смотрит остатки на конкретную дату, выгружает в Excel.
Отдельный POST-сценарий warehouse-leftover — для упрощённого экрана «остатки больше макс / меньше мин» (без meta-итогов).
Связанная веха: LOCALIOFFICE-966 — Inventory: аудит бизнес-логики.
Картина пользователя
┌─────────────────────────────────────────────────────────────────────┐│ Остатки на складах 📥 Download││ 🔍 Поиск Склады [▾]✅ обязательный Дата [13.11.25] Группы [▾] ││ Критерий [Все ▾] ⬜ Отрицательные │├─────────────────────────────────────────────────────────────────────┤│ Артикул Название Род.группа Бух.кат Ед. Размер Кронли ... ││ M-001 Мука пшен. Бакалея Сырьё кг 1 20.500 ││ ─ Группа: Бакалея ─────────────────────────── (клиентская группи- ──││ ... ровка) │├─────────────────────────────────────────────────────────────────────┤│ Грандтотал: 12450.00₽ │ Кронли: 8200.00₽ │ Кронли Мюли: 4250.00₽ │└─────────────────────────────────────────────────────────────────────┘Базовое
- Auth.
Authorization: Bearer <access_token>. corporationId — из токена./auth/*для refresh / login / switch-corporation. - Время.
dateпринимается какYYYY-MM-DD. Если не задан — текущая дата корпорации. - Деньги и количества. Возвращаются строками. Парсить в
Decimal. - Сортировка. Бэк отдаёт строки в стабильном порядке (по умолчанию — по
nomenclatureName). Клиентская сортировка — поверх. warehouseIds[]обязателен. Без выбранных складов выборка не строится (400). Порядок выбранных складов задаёт порядок колонок в таблице и порядокmeta.totalsByWarehouse[]в подвале.
Эндпоинты раздела
Префикс — /inventory/stock-balances.
| Метод | Маршрут | Когда вызывается из UI |
|---|---|---|
GET | /inventory/stock-balances | Загрузка таблицы остатков (основной сценарий) |
POST | /inventory/stock-balances/warehouse-leftover | Упрощённая выборка «больше макс / меньше мин» (без meta) |
POST | /inventory/stock-balances/export | Кнопка «Download» (xlsx) |
Сценарий 1. Загрузить таблицу остатков
Запрос
GET /inventory/stock-balances? warehouseIds=uuid1,uuid2& date=2026-04-13& searchQuery=Мука& stockCriterion=ALL& groupIds=uuid3& includeNegative=false& nomenclatureId=| Параметр | Тип | Обяз. | Описание |
|---|---|---|---|
warehouseIds[] | UUID[] | ✅ | Массив UUID складов через запятую (?warehouseIds=uuid1,uuid2) или повторами. Порядок задаёт порядок колонок |
date | YYYY-MM-DD | — | На какую дату строить остатки. Дефолт — текущая |
searchQuery | string | — | Поиск по nomenclatureSku и nomenclatureName (регистронезависимо) |
stockCriterion | enum | — | ALL (дефолт) / LESS_THAN_MIN / MORE_THAN_MAX |
groupIds[] | UUID[] | — | Фильтр по группам номенклатуры. Массив или CSV |
includeNegative | bool | — | true — оставить позиции с отрицательным остатком (по дефолту скрыты) |
nomenclatureId | UUID | — | Точечная выборка по одной позиции |
Ответ
{ "rows": [ { "nomenclatureId": "uuid", "nomenclatureName": "Мука пшеничная", "nomenclatureInternalCode": null, "nomenclatureSku": "M-001", "nomenclatureType": "GOODS", "groupId": "uuid", "groupName": "Бакалея", "parentGroupId": null, "parentGroupName": null, "accountingCategoryName": "Сырьё", "measurementUnitShortName": "кг", "size": "1", "warehouseId": "<u1>", "warehouseName": "Кронли", "totalBalance": "20.500", "costPrice": "60.00", "averagePurchasePrice": "60.00", "totalCost": "1230.00", "minStock": null, "maxStock": null } ], "meta": { "grandTotalAmount": "12450.00", "totalsByWarehouse": [ { "warehouseId": "<u1>", "totalAmount": "8200.00" }, { "warehouseId": "<u2>", "totalAmount": "4250.00" } ] }}Что фронту делать с полями
Бэк возвращает плоский список строк: одна строка = одна номенклатура × один склад. Если по позиции выбрано N складов, в ответе будет до N строк по этой номенклатуре (с разными warehouseId). Фронт сводит в матрицу nomenclatureId × warehouseId.
| Поле | Куда оно идёт в UI |
|---|---|
nomenclatureSku | Колонка «Артикул» (nullable) |
nomenclatureInternalCode | Колонка «Внутренний код» (nullable) |
nomenclatureName (nomenclatureId) | Колонка «Название товара» |
nomenclatureType | Колонка «Тип» (иконка) |
groupName (groupId) | Заголовок группы строк (клиентская группировка) |
parentGroupName (parentGroupId) | Колонка «Родительская группа» (Wave 1). null для корневой группы |
accountingCategoryName | Колонка «Бухгалтерская категория» (nullable) |
measurementUnitShortName | Колонка «Ед. измерения» |
size | Колонка «Размер». Коэффициент базовой ед. (1, если базовая не настроена) |
warehouseName (warehouseId) | Имя колонки-склада в матрице |
totalBalance | Кол-во на складе (значение в ячейке матрицы) |
totalCost | Стоимость на складе (= totalBalance × costPrice) |
costPrice | Себестоимость за ед. (скользящая средневзвешенная) |
averagePurchasePrice | Средняя цена закупки. Может отличаться от costPrice после списаний/перевалок |
minStock, maxStock | Нижний/верхний предел (из StockLimit, nullable). Используются для индикаторов «ниже минимума» / «выше максимума» |
Что в подвале
meta.grandTotalAmount — общий итог totalCost по всем строкам выборки. meta.totalsByWarehouse[] — массив {warehouseId, totalAmount} строго в порядке Query.warehouseIds. Склады без позиций возвращаются с totalAmount: "0" — это гарантирует, что фронт рендерит подвал колонок одним циклом по meta.totalsByWarehouse, не теряя пустые склады.
Ошибки
| HTTP | Тело | Когда | Что делать в UI |
|---|---|---|---|
400 | {message:"Необходимо указать хотя бы один склад"} | Пустой warehouseIds[] | Подсветить селект складов |
400 | стандарт class-validator | date не парсится, stockCriterion вне enum, плохой UUID | Подсветить поле |
401 | стандарт | Refresh → login | |
403 | стандарт | Чужой склад | Тоаст «Склад не принадлежит корпорации» |
Сценарий 2. Остатки «больше макс / меньше мин»
Упрощённый сценарий для отдельного экрана лимитов — без meta-итогов.
Запрос
POST /inventory/stock-balances/warehouse-leftoverContent-Type: application/json
{ "warehouseIds": ["uuid1", "uuid2"], "leftoverFilter": "moreMax", "deleted": false}DTO: warehouse-leftover.dto.ts.
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
warehouseIds[] | UUID[] | ✅ | Склады выборки |
leftoverFilter | "moreMax"|"lessMin" | ✅ | Маппится в stockCriterion=MORE_THAN_MAX или LESS_THAN_MIN |
deleted | bool | — | true — включить позиции с отрицательным остатком (includeNegative) |
Ответ
Плоский массив StockBalanceResponseDto[] без meta:
[ { "nomenclatureId": "uuid", "nomenclatureName": "Мука пшеничная", "warehouseId": "<u1>", "warehouseName": "Кронли", "totalBalance": "120.000", "costPrice": "60.00", "totalCost": "7200.00", "minStock": null, "maxStock": "100", // ... остальные поля как в Сценарии 1 }]Сценарий 3. Скачать Excel
POST /inventory/stock-balances/exportContent-Type: application/json
{ "warehouseIds": ["uuid1", "uuid2"], "date": "2026-04-13", "stockCriterion": "ALL", "columns": [ "nomenclatureSku", "nomenclatureName", "parentGroupName", "accountingCategoryName", "measurementUnitShortName", "totalBalance", "totalCost", "warehouse:<u1>", "warehouse:<u2>" ]}DTO — StockBalanceExportDto. Принимает тот же набор фильтров, что GET, плюс массив выбранных колонок (warehouse:<id> — динамические колонки для матричного представления). Ответ — .xlsx (attachment stock-balances.xlsx).
Цвета и индикаторы
Бэк не отдаёт явного «статуса» по строке. Цвета и иконки рассчитывает фронт:
| Состояние | Условие | Индикатор |
|---|---|---|
| Меньше минимума | totalBalance < minStock (если minStock != null) | 🟠 |
| Больше максимума | totalBalance > maxStock (если maxStock != null) | 🟣 |
| Отрицательный | totalBalance < 0 (видно при includeNegative=true) | 🔴 |
| Норма | всё остальное | — |
stockCriterion = LESS_THAN_MIN / MORE_THAN_MAX уже сделает фильтрацию на бэке; индикаторы нужны, чтобы пользователь видел причину строки в выборке ALL.
Селекторы
| Селектор | Эндпоинт | Параметры |
|---|---|---|
| Склады (мультивыбор, обязательный) | GET /organization/subdivisions | Порядок выбора — порядок колонок |
| Дата остатков | datepicker (клиентский) | date |
| Критерий остатков | enum (хардкод) | stockCriterion |
| Группы | GET /menu-management/nomenclature-groups | groupIds[] |
| Поиск | string | searchQuery |
| Отрицательные | checkbox | includeNegative |
| Конкретная номенклатура (опционально) | GET /menu-management/nomenclatures/search | nomenclatureId |
Часовой пояс и формат времени
date—YYYY-MM-DD, без TZ. Бэк интерпретирует как «конец дня в таймзоне корпорации».- Учётный день для расчёта остатков начинается в
Corporation.accountingDayStartTime(по дефолту 06:00). Если оператор смотрит остатки утром текущего дня — реально это остатки на конец вчерашнего учётного дня. Эта тонкость скрыта от фронта; для UI достаточно отдать дату как есть.
Открытые вопросы
- Persistence настроек складов и колонок —
localStorageили server-side user preferences. - Сортировка — бэк отдаёт стабильный порядок (по
nomenclatureName). Если потребуется server-side сортировка по другим полям, бэк добавит whitelist параметраsortBy. - Группировка «Свёрнутая группа» — сейчас бэк отдаёт плоский список, фронт группирует по
groupIdи считает подытоги. Дерево групп с агрегатами по узлам пока не предусмотрено; добавится отдельной задачей при необходимости.
Известные расхождения и план доработок
| Что | Задача |
|---|---|
| Все 8 волн правок по аудиту inventory закрыты | LOCALIOFFICE-966 |
Что работает уже сейчас и менять не нужно
- ✅
parentGroupId/parentGroupNameв строке — колонка «Родительская группа» (Wave 1). - ✅ Page-level meta (
grandTotalAmount,totalsByWarehouse[]) — для подвала. Порядок строго совпадает с порядкомwarehouseIdsзапроса (Wave 2). Склады без позиций возвращаются с нулём. - ✅ Серверные фильтры
stockCriterion,groupIds[],includeNegative,nomenclatureId,searchQuery— клиентская пост-фильтрация не нужна. - ✅
costPrice(скользящая средневзвешенная) иaveragePurchasePrice— отдельные поля. - ✅
minStock/maxStockберутся изStockLimitи приходят сразу в строке остатков. - ✅ Отдельный POST-сценарий
warehouse-leftoverдля упрощённой выборки по лимитам. - ✅ Экспорт в Excel.