Перейти к содержимому

Остатки на складах

Что это

Отчёт по текущим остаткам номенклатур на выбранных складах корпорации. Бухгалтер/менеджер видит сводную картину «что и сколько лежит на каждом складе», сравнивает склады через колонки-выборки, ищет позиции по критерию (все / меньше минимума / больше максимума), смотрит остатки на конкретную дату, выгружает в 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)

stock-balance.controller.ts


Сценарий 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) или повторами. Порядок задаёт порядок колонок
dateYYYY-MM-DDНа какую дату строить остатки. Дефолт — текущая
searchQuerystringПоиск по nomenclatureSku и nomenclatureName (регистронезависимо)
stockCriterionenumALL (дефолт) / LESS_THAN_MIN / MORE_THAN_MAX
groupIds[]UUID[]Фильтр по группам номенклатуры. Массив или CSV
includeNegativebooltrue — оставить позиции с отрицательным остатком (по дефолту скрыты)
nomenclatureIdUUIDТочечная выборка по одной позиции

Ответ

{
"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-validatordate не парсится, stockCriterion вне enum, плохой UUIDПодсветить поле
401стандартRefresh → login
403стандартЧужой складТоаст «Склад не принадлежит корпорации»

Сценарий 2. Остатки «больше макс / меньше мин»

Упрощённый сценарий для отдельного экрана лимитов — без meta-итогов.

Запрос

POST /inventory/stock-balances/warehouse-leftover
Content-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
deletedbooltrue — включить позиции с отрицательным остатком (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/export
Content-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-groupsgroupIds[]
ПоискstringsearchQuery
ОтрицательныеcheckboxincludeNegative
Конкретная номенклатура (опционально)GET /menu-management/nomenclatures/searchnomenclatureId

Часовой пояс и формат времени

  • dateYYYY-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.