ERP под ключ
ENRU
Кейс - заказная ERP

Полная ERP с теорией ограничений Голдратта в коде.

Полный бэк-офис для селлера домашнего текстиля на Wildberries: 763 карточки, 4 бренда, около 6 000 заказов в день, производство в Узбекистане с циклом ~60 дней. Система охватывает: досорт на буферах ТОС, ежедневная таблица РНП, 7 типов алертов, дашборд экономики с прогнозом месяца, комбинаторика наборов, ABC-анализ, заказ производителю в один клик и справочник складов. А на складе работает модуль FBS: поставки собираются прямо в ERP, код маркировки проверяется в момент скана, срочные заказы идут первыми, недостающий товар дозаказывается у поставщиков в МойСклад, а владелец видит выработку сборщиков и приёмку по поставщикам.

FastAPIPostgreSQL 16CeleryReact 18 WB APIAPI МойСкладAPI TrueStatsЧестный знакGoogle Sheets
20
модулей бэкенда, у каждого свой README
255
API-эндпоинтов портала
1 286+440
автотестов бэкенда и фронта, прогон каждый день в чистой БД
763
SKU под управлением, 4 бренда
~6 000
заказов в день проходит через систему
4 API
WB + МойСклад + TrueStats + Честный знак, плюс Google Sheets
01 Контекст и боль

Три системы, личный Excel владельца и плечо поставки 60 дней

Быстрорастущий селлер полотенец, халатов и текстильных наборов. Цифры жили в трёх несвязанных сервисах. Главное решение бизнеса, что и когда досортировывать, принималось вручную.

Данные в 3 системах

Кабинет маркетплейса: заказы и реклама. МойСклад: склады и составы наборов. TrueStats: юнит-экономика. Плюс ручные выгрузки файлами. Единой картины не видел никто.

Досорт держался на одном человеке

Какой SKU везти на склад маркетплейса, сколько и когда: владелец решал это ежедневно, в личном Excel, по 763 карточкам в ~27 категориях. Процесс не масштабировался и не переживал отпуск.

Сезонный товар, длинные плечи

Производство занимает около 60 дней, плюс ~10 дней доставка, плюс внутренние плечи. Заказал поздно - пропустил сезон. Заказал рано - заморозил деньги в остатках. Права на ошибку почти нет, а в сезон его нет совсем.

Проблемы замечали глазами

Обнуление остатков, падение заказов, негативные отзывы и расхождения данных всплывали, когда кто-то случайно посмотрел. При 6 000 заказов в день "случайно посмотрел" - это не система контроля.

02 Архитектура

Ночной конвейер на входе, решения на выходе, узкие записи наружу

Каждую ночь Celery-задачи стягивают маркетплейс, МойСклад и TrueStats в PostgreSQL. Расчётные модули превращают сырые строки в буферы, алерты и прогнозы. Люди работают в React-портале или в Google-таблице, которую бэкенд рендерит для менеджеров. Днём, в рабочую смену склада, модуль FBS опрашивает площадку каждую минуту и пишет наружу ровно то, что ему поручено: поставки в кабинет площадки и заказы поставщикам в МойСклад, каждую запись с журналом.

НОЧНОЙ КОНВЕЙЕР ДАННЫХ · ВРЕМЯ МСК Wildberries API заказы · продажи · остатки карточки · реклама · отзывы поставки · тарифы синк 03:30 МойСклад склады · товары · поставщики 349 составов наборов синк 06:30 TrueStats 98 метрик юнит-экономики версии себестоимости синк 08:00, гейт T-1 Ручные файлы аналитика продавца CSV остатков производителя drag-and-drop, маппинг Слой синков конвейер Celery Beat идемпотентные импорты: повторный прогон = 0 изменений гейты свежести: сбой звена = стоп цепочки rate-limit на каждый API watchdog каждые 5 минут журнал каждого прогона PostgreSQL 16 110 таблиц · 61 миграция Redis 7: кэш + брокер бэкапы: 14 дневных + 8 недельных Расчётные модули зоны буфера DBM 08:30 контракт достаточности 7 типов алертов · ABC комбинаторика наборов 08:40 дашборд + прогноз React SPA 31 маршрут · 10 тем виртуализация таблиц Google-витрина рендер для менеджеров запись только RAW правки назад каждые 10 мин Алерты + Excel 7 типов алертов в ленте фирменный экспорт везде Страница Здоровье автотесты каждый день 05:00 светофоры: контейнеры, память, диск, бэкапы итоги тестов и статус хоста, 5 мин ДНЁМ, В СМЕНУ СКЛАДА · СБОРКА FBS · ЗАПИСИ НАРУЖУ УЗКИЕ И С ЖУРНАЛОМ опрос заданий каждую минуту Честный знак: код проверяется при скане WB: 4 операции поставок МойСклад: заказ поставщику
Внешние системы Сервисы ERP Хранилище Самонаблюдение

Одна ночь из жизни системы

8Docker-сервисов за nginx
110таблиц, 61 миграция, одна голова
20backend-модулей, у каждого свой README
~70Kстрок Python + TypeScript без тестов, ещё ~40K строк тестов
10тем интерфейса на CSS-токенах, ноль захардкоженных цветов

Контракты между модулями означают, что ничего не считается дважды: достаточность даёт одна функция, светофор 33/67 живёт в одной настройке, классы ABC отдаёт один сервис. РНП, Google-витрина и алерты потребляют одни и те же цифры.

03 Флагманский модуль

Теория ограничений Голдратта, работающая в проде

Заказчик попросил проверить его формулы досорта и предложить улучшения. Его классический расчёт покрытия остался в таблице видимыми колонками. Поверх система ведёт динамическое буферное управление (DBM): у каждого SKU есть целевой буфер, буфер делится на зоны-светофор, и буфер сам корректируется по фактическому потреблению. Это ТОС по учебнику, реализованная кодом и тестами.

B (целевой буфер) = V_план × N дней × 1.1 × M  ·  V_план = V30 × тренд clamp(V3/V30, 0.5..2.0) × Kсезон

×1.1 - это запас "на паранойю" от заказчика на каждый буфер, в духе Голдратта. M - множитель DBM: стартует с 1.00 и меняется шагами по 1/3.

Зелёная · от 67%

Буфер заполнен достаточно. Ничего не делать. Двенадцать зелёных дней подряд предложат уменьшить буфер: капитал не должен спать в остатках.

Жёлтая · 33-67%

Планировать пополнение. SKU попадает в досорт-таблицу с рассчитанным количеством, а не с догадкой.

Красная · ниже 33%

Заказывать сейчас. Красные дни копятся в 12-дневной истории; красная серия запускает рекомендацию увеличить буфер.

Чёрная · нет товара

Остаток ноль при живых продажах. Потерянная выручка каждый день. Чёрная считается красной в сериях и кричит с верха ленты алертов.

Каждый день в 08:30, сразу после ночных синков пересчитать зону каждого SKU, дописать день в историю зон Окно 12 дней (3 × плечо пополнения WB в 4 дня) посчитать красные и зелёные серии; рекомендации только на полной истории Буфер мал 80% и более из 12 дней в красной или чёрной зоне рекомендация: буфер +1/3 Буфер велик 12 зелёных дней подряд пол: никогда ниже 1/3 рекомендация: буфер -1/3 Кулдаун: 12 дней тишины после любого изменения система никогда не гоняется за собственным хвостом Подтверждает человек: кнопка ±1/3 в строке таблицы автоприменение существует, но по умолчанию ВЫКЛЮЧЕНО: по Голдратту изменение буфера утверждает человек новый M Целевой буфер SKU B = V_план × N × 1.1 × M V_план = скорость × тренд × сезон ×1.1 паранойя · M шагами по 1/3 заполненность P = (остаток + в пути) / B 100% 67% 33% 0 зелёная · ок жёлтая · планировать красная · срочно чёрная · нет товара зоны красит единый светофор системы 33/67
Ежедневная задача DBM Ветка увеличения Ветка уменьшения / контроль человека Стабилизатор Математика буфера

Константы, и почему они именно такие

КонстантаЗначениеОбоснование
Окно DBM12 днейТри плеча пополнения склада маркетплейса (4 дня). Короче - реакция на шум, длиннее - реакция с опозданием.
Триггер увеличения≥80% красных из 12Буфер, живущий в красной зоне, буфером не является. Рекомендация: +1/3 от целевого.
Триггер уменьшения12 зелёных подрядБуфер, не выходящий из зелёной зоны, - это замороженные деньги. Рекомендация: -1/3, и считается только непрерывная серия.
ПолM ≥ 1/3Множитель никогда не падает ниже трети. Если шаг пробил бы пол, уменьшение даже не предлагается.
Кулдаун12 днейПосле любой корректировки система молчит целое окно: сначала измерить эффект одного решения, потом предлагать следующее.
Паранойя×1.1Правило заказчика: +10% к каждому буферу. Оформлено именованной константой, закрыто тестами.
Плечи поставки4 / 14 / 74 дняДо склада маркетплейса / со склада производителя / с производства (цикл 60 дней + 10 доставка + внутренние плечи).
ТОП-правило90%Правило заказчика: SKU класса A с покрытием ниже 90% всегда прыгает в верх досорт-таблицы, выше любого SKU класса C, и досортировывается минимум до 90%.

Сезонность поверх буфера

Ручное сильнее автоматики

У каждой категории есть окно сезона и коэффициент. Ручной коэффициент заказчика всегда приоритетнее автоподсказки. Подсказки считаются из прошлогодних заказов: суммы по неделям, сглаживание MA-3, сезон = сегмент выше 50% пика.

Несезонный запас в штуках

Вне сезона скоростная формула игнорируется. Категория держит фиксированный запас в штуках, распределённый по SKU пропорционально их доле в заказах за 90 дней. Совсем нет истории - поровну.

Плановый дефицит на выходе

В закрывающем окне сезона буфер линейно снижается к запланированному дефициту. Сезон заканчивается пустыми полками по плану, а не остатками на год вперёд.

Человек в контуре

Система рекомендует. Решает человек.

Автоприменение корректировок буфера по умолчанию выключено. Ежедневная задача лишь пишет рекомендацию в строку: кнопка ±1/3 с подсказкой, почему. Менеджер нажимает, изменение логируется, стартует кулдаун. Это дисциплина самого Голдратта: буферы адаптируются, люди утверждают.

Досорт-таблица: зоны-светофор буфера, бейджи ТОП, кнопка DBM плюс-минус треть и sticky-блок заказа поставщику
Досорт-таблица: светофор зон у каждого SKU, бейджи ТОП, кнопка DBM ±1/3, спарклайны со всплесками выше μ+2σ и sticky-блок заказа поставщику справа.
2 387строк в модуле ТОС
22REST-эндпоинта
9своих таблиц
17тестов на правила: пол, кулдаун, сезон через Новый год
33/67один светофор для буферов и алертов
04 Ежедневная аналитика

РНП на 763 SKU и дашборд экономики с прогнозом месяца

РНП, "Рука на Пульсе", - ежедневная панель по каждому SKU: воронка продаж, экономика рекламы, остатки и цены, около 60 колонок на SKU, с виртуализацией, чтобы 763 строки листались как шёлк. Дашборд отвечает на вопрос владельца: чем закончится месяц, если едем как едем?

Таблица РНП

763 строки × ~60 колонок со sticky-колонкой SKU и настраиваемыми светофорами. Карточный режим для работы по исключениям. Каждый SKU раскрывается в карточку: продажи, реклама, остатки, цены и комментарии в окнах 7 / 14 / 30 дней.

Умный импорт файлов

Выгрузка воронки маркетплейса прилетает drag-and-drop. Колонки матчатся нечётким маппингом; переименованная колонка вызывает диалог подтверждения, а не тихое неверное чтение. Битые строки отклоняются поштучно, файл целиком не падает.

Сверка источников

Выручка TrueStats и маркетплейса сравнивается ежедневно. Расхождение больше 5% поднимает баннер. В проде это поймало реальные 13,31% на первом же боевом прогоне.

Дашборд экономики

Экономика кабинета и категорий из файла, API или их слияния по SKU. Прогноз конца месяца по тренду 15 дней. Расходные метрики инвертируют окраску: растущая реклама красная, а не зелёная. Ответ: 61 мс с кэшем, 101 мс без.

Приём внедрения

Менеджеры остались в своей Google-таблице

Бэкенд рендерит витринные листы прямо в привычную таблицу: главная, сезон, распределение, новые цены, контроль остатков, алерты. Запись только RAW, чтобы штрихкоды не превращались в 2.05E+12. Ручные правки возвращаются дифф-пуллом каждые 10 минут, и рендер никогда не затирает ячейку человека. Кнопка в таблице отправляет новые цены назад через вебхук с проверкой токена.

РНП: виртуализированная таблица SKU со светофорами и карточным режимом
РНП: карточный режим по умолчанию, полная виртуализированная таблица в один клик, светофоры с порогами, которые заказчик настраивает сам.
Дашборд экономики с прогнозом месяца и фильтрами по категориям
Дашборд: экономика кабинета, разбивка по категориям, прогноз конца месяца по тренду 15 дней, фильтры с пересчётом на лету.
~60колонок на SKU в РНП
13,31%реальное расхождение, пойманное сверкой
61 / 101 мсответ дашборда, с кэшем / без
10 минцикл дифф-пулла ручных правок таблицы
05 Контур контроля

Семь типов алертов и ERP, которая тестирует себя каждое утро

Проблемы должны сами находить людей, а не ждать, пока их найдут. Семь детерминированных типов алертов следят за бизнесом; страница здоровья со светофорами следит за самой системой.

АлертТриггерДетали
Крайняя срочка≤100 штук на маркетплейсеВсегда красный, всегда закреплён вверху ленты.
Низкая достаточностьпокрытие ниже порогаТот же контракт достаточности, что и в досорт-таблице. Одна формула, одна правда.
Падение заказовскорость за 3 дня против 10 дней, ≥30% внизЛовит умирающий SKU за дни, а не за недели.
Всплеск заказовскорость за 3 дня резко вверхВсплеск - тоже сигнал досорта: продавать, пока летит.
Негативные отзывыновые отзывы на 1-3 звездыПопап с текстами отзывов, серверная пагинация. На приёмке в базе было 1 447 негативных.
Старт производства<90 дней запасаОтсчитывает 60-дневный цикл производства назад от прогнозного обнуления, вне сезона смягчается.
Расхождение данныхразрыв TrueStats и WB >5%Система не доверяет своим входам раньше, чем предложит доверять своим выводам.

Эпизодный жизненный цикл

Алерт - это эпизод: активен, решён, реактивирован в тот же день, если условие вернулось. Ноль дублей по построению. Отметка "видел" - на каждого пользователя, чужое "видел" проблему не прячет.

Гейт несвежих данных

Если данным маркетплейса больше 24 часов, весь пересчёт алертов пропускается. Никаких алертов на устаревших данных: тишина честнее шума.

388 эпизодов в первый день

Первый боевой прогон поднял 388 активных эпизодов. Это накопленный слой проблем, которых раньше просто не было видно. Лента их отсортировала; крайняя срочка встала первой.

Доверие по построению

Заказчик видит тесты каждый день

Крон каждое утро в 05:00 прогоняет все автотесты, 1 286 backend и 440 frontend, в чистой отдельной базе. Результат ложится в таблицу и показывается на странице Здоровье, рядом со светофорами контейнеров, памяти, диска и свежести бэкапа. ERP ежедневно доказывает владельцу, что всё ещё работает. Не обещание, а зелёный свет.

Лента алертов с чипами типов, светофорами важности и попапом отзывов
Лента алертов: чипы типов, отметка "видел" на пользователя, крайняя срочка закреплена сверху, негативные отзывы открываются в попапе с пагинацией.
Страница здоровья со светофорами системы и результатами ежедневных автотестов
Страница Здоровье: светофоры контейнеров, памяти, диска и бэкапов, плюс утренний прогон автотестов, видимый заказчику.
7типов алертов, один светофор 33/67
388активных эпизодов в первый день
1 286+440автотестов с прогоном каждый день в 05:00
5 минобновление статуса хоста на странице Здоровье
06 Инженерия выручки

Комбинаторика наборов: 1 813 путей продажи за 3,0 секунды

Полотенца продаются поштучно и в наборах. Одно и то же сырьё может уйти десятком способов, у каждого своя цена за штуку, маржа и потолок по остаткам. Раньше эту комбинаторику держал в голове заказчик. Теперь её считает система и объясняет каждое предложение в колонке "Почему".

Этап 1: все существующие пути

Для каждого сырья-полотенца: все варианты продажи, поштучно или внутри любого из 349 составов из техкарт МойСклад. По варианту: цена за штуку, маржа, потолок по текущим остаткам, лимитирующий компонент и жадный план распределения. 294 сырья × 349 составов = 1 813 вариантов за 3,0 секунды, сверено вручную SQL-ом на боевых данных.

Этап 2: новые комбинации

Мерчандайзинговое правило заказчика, переведённое в код: розовый сам не продаётся, а в связке с серым улетает. Движок разбирает модельную основу артикула, ставит в пару топ-цвет и неликвид той же основы, скрещивает основы и оценивает экономику каждого кандидата по аналогам.

Колонка "Почему"

У каждого кандидата есть человеческое обоснование: какой цвет тянет, какой остаток разморозит, какая ожидаемая экономика на штуку. Оптимизация сознательно жадная, без LP-солвера: заказчик может проследить каждый шаг, а доверие важнее последнего процента оптимальности.

Комбинаторика наборов: варианты по сырью и кандидаты новых комбинаций с колонкой Почему
Комбинаторика наборов: варианты по сырью на одной вкладке, сгенерированные кандидаты с обоснованием "Почему" на другой, плюс ручной конструктор набора.
1 813вариантов за один прогон
3,0 сполный пересчёт
294 × 349сырьё × составы наборов
100%кандидатов объясняют сами себя
07 Фокус

ABC-анализ: 134 SKU держат 80,2% прибыли

Плавающая ABC-классификация сразу по трём критериям: прибыль, выручка и заказы, с настраиваемыми порогами 80/95 под защитой CHECK-ограничения в базе.

Первый боевой прогон оцифровал интуицию: класс A - это 134 SKU, 17,6% каталога, держащие 80,2% прибыли. Классы сверяются с собственным ABC аналитического сервиса, а убыточные SKU и SKU без данных показываются отдельными корзинами с разложением причин, а не тихо выпадают.

Классы считаются один раз и отдаются по контракту: РНП и досорт-таблица подтягивают букву без пересчётов. ТОП-правило досорта питается именно этими классами.

Зачем это досорту

SKU класса A с покрытием ниже 90% буфера прыгает в верх заказа поставщику, выше любого SKU класса C с худшим процентом. Бизнес защищает те 134 SKU, которые платят за всё остальное. Этот приоритет - оттестированное правило, а не привычка в чьей-то голове.

134SKU в классе A
17,6%каталога...
80,2%...держат такую долю прибыли
3критерия: прибыль, выручка, заказы
08 Замыкание цикла

Заказ производителю в один клик. Честно: галки плюс одна кнопка.

Буфер говорит, чего не хватает. Модуль заказа говорит, где это взять и к какому сроку, и собирает тот самый Excel, который ждёт производитель в Узбекистане.

  1. Двухпроходное количество. Q(L) = max(0, ceil(V_план × (N + L) × 1.1 - общий остаток)). Проход первый смотрит склад производителя: хватает - режим "со склада", плечо 14 дней. Иначе режим "производство", плечо 74 дня (60 производство + 10 доставка + внутренние плечи), с пометкой, если часть количества есть уже сейчас.
  2. Срочность по третям. Покрытие буфера ниже 1/3: заказывать сейчас. От 1/3 до 2/3: планировать. Выше 2/3: потерпит. Режим производства оценивается на ступень жёстче: длинное плечо оставляет меньше права на ошибку.
  3. Галки, которые выживают. Менеджер отмечает строки в sticky-блоке заказа; галки и ручные количества живут в базе, а не во вкладке браузера. Формирование заказа их не сбрасывает; сбрасывает только явная кнопка.
  4. Одна кнопка, один файл. "Сформировать заказ" собирает Excel в формате, согласованном с поставщиком: артикул производителя из справочника, цвет, режим, количество, срок, затраты на закупку, итоги. Артикул без маппинга получает подсвеченную ячейку "нет в справочнике" вместо тихой пустоты. Файл скачивается и ложится в историю заказов с полным снапшотом позиций.
  5. Грязный вход, чистые данные. Остатки производителя приходят полуструктурированным CSV из системы маркировки. Толерантный regex-парсер отклоняет битые строки поштучно; несматченные артикулы становятся задачами "создать правило" в интерфейсе, и справочник ведёт заказчик, а не разработчик.
14 / 74дней плечо: со склада / с производства
90%пол ТОП-правила для SKU класса A
1клик от отмеченных строк до файла производителю
100%заказов в истории со снапшотами
09 Сборка FBS

Проверка кода маркировки переехала в момент скана

Склад собирал заказы руками: кабинет площадки, лист подбора, Excel, сканер в Excel, ручная замена скрытых разделителей и загрузка шаблона обратно. Главная потеря времени наступала уже после загрузки: площадка проверяет каждый код в национальной системе прослеживаемости и часть отбраковывает, а люди идут искать эти товары по коробкам. Модуль переносит проверку в момент скана: сборщик узнаёт о плохом коде, пока товар у него в руках. Следом в ERP переехали и сами поставки: как они собираются из очереди, рассказано в следующем разделе.

Экран поставки: задания со статусами и панель сканера с вердиктом по коду маркировки
Страница поставки: слева задания со статусами и подсветкой найденного, справа сканер с крупным фото изделия, стадиями скана и вердиктом по коду. Поле всегда в фокусе: у сборщика в руках сканер и товар, а не мышь.
ВердиктЧто видит сборщик и что делает дальше
Код принятзелёный, задание закрыто, стикер уходит на печать
Принят без проверкижёлтый: реестр не ответил, код принят по настройке, ночью перепроверяется
Код чужойвладелец кода другая компания: отложить, взять другой экземпляр
Уже проданкод выбыл из оборота: отложить, взять другой экземпляр
Не в оборотекод не введён в оборот: отправить в карантин
Заблокированкарантин
Нет в базекарантин
Дублькрасный, с номером задания-владельца: оттуда можно перепечатать стикер
Экземпляр лишнийвсе задания этого размера в поставке уже собраны: сканер называет товар, размер и время последней сборки и просит отложить экземпляр
Другой товарсканер называет товар и размер и говорит, где ждут его задания: в этой поставке, в другой партии или в другой поставке
Код не распознанкод прочитан не целиком: пикнуть ещё раз. Срезанный хвост кода не выдаётся за чужой код

Порядок проверок важен. У чужого проданного кода причин отказа несколько, а сборщику нужно назвать одну: ту, с которой он что-то может сделать.

Одна точка входа вместо режимов

Сервер сам классифицирует ввод: штрихкод, код маркировки или стикер, и двигает состояние задания. Переключать режимы человеку с товаром в руках нечем.

Повтор возвращает сохранённый ответ

У каждого нажатия свой идентификатор, и повторный запрос отдаёт тот же ответ, а не пересчитанный. Иначе сборщик увидел бы "этот код уже отсканирован" на собственный же скан.

Импорт не обнуляет работу смены

Повторный импорт обновляет поля площадки и не трогает наши: статус, партию, сканы и коды. Ночной прогон не стирает то, что сделали люди.

Карантин вместо тихой пропажи

Задание с плохим кодом уходит в карантин с причиной, а не исчезает из списка. Начальник склада видит, сколько заданий встало и почему.

Сотрудников заводит начальник склада

Право администратора модуля даёт заводить и выключать сборщиков без администратора портала. Только выключение, не удаление: на человеке висят сканы и сводки.

Сводка по смене

Смена это день по московскому времени. По каждому сотруднику видно: взял с полки, код принят, готово, проблем, отклонённых сканов, первый и последний скан. Число отклонённых сканов открывается списком: время, товар, что пикнули и что ответил сканер.

Сканер без Enter

Сканер выдаёт символы за миллисекунды, поэтому 300 мс тишины после похожего на законченный кода уже означают конец ввода. Пик ловится на всей странице: клик по вкладке или кнопке его больше не съедает.

Любая раскладка клавиатуры

Сканер нажимает клавиши по английской раскладке, и при русской код приходит кириллицей и чужими знаками. Карта исправления применяется одним проходом и только если в строке есть кириллица: точка и запятая в серийнике законны.

Карточка главнее выученной связки

Код маркировки находит задание сначала по штрихкоду задания, потом по штрихкоду размера в карточке площадки и только потом по связкам, выученным на складе. Признак, нужен ли товару код, тоже берётся у карточки. Лишний запрос кода стоит одного пика, а пропущенный отправил бы товар без кода.

Молчаливая деградация здесь недопустима. Если токен доступа к реестру умер, модуль поднимает системный алерт и продолжает работать в режиме "принят без проверки", честно помечая такие коды. Именно тихий отказ и стоил бы той самой отбраковки на площадке, ради которой всё затевалось.

Список поставок FBS с поиском по всей истории и полосой готовности
Список поставок: одно поле ищет по номеру поставки, артикулу, штрихкоду, названию, заданию и стикеру во всей истории, совпадения показаны строками под поставкой.
Сводка по смене: кто сколько собрал и сколько проблем
Сводка по смене с итоговой строкой: она отвечает на вопрос начальника склада, кто сегодня сколько собрал, не поднимая никого от стола. Отклонённые сканы открываются списком.
10 Поставки FBS

Поставка собирается в ERP, а не галками в кабинете

Поставку создавали в кабинете площадки: галки по списку новых заданий сверху вниз. В одну поставку попадала сотня разных артикулов, и сборщик обходил с ней весь склад. Заказы юрлиц в кабинете никак не помечены: отметишь сто заданий, а поставку создать нельзя, и ищи, какое мешает. Теперь очередь живёт в ERP, а поставка складывается из того, что удобно собирать одним походом к полке.

Очередь FBS: дерево категорий, карточек и размеров с галками и панелью выбора
Очередь FBS: дерево категория, карточка, размер по одному складу. Отмечать можно чипами категорий и тегов, галками карточек и отдельными размерами. Панель внизу считает выбранное и собирает из него поставки.
  1. Очередь по одному складу. Площадка не смешивает склады в одной поставке, поэтому очередь заперта на выбранный склад FBS. Переключатель физлиц и юрлиц делит её на две: поставка всегда одного типа покупателя.
  2. Предложенная сборка. Строка таблицы это будущая поставка. Артикул с числом заданий от порога идёт своей поставкой: один артикул, один поход к полке. Мелкие артикулы категории объединяются, остальное уходит в прочее. У каждой строки написано правило, по которому она сложилась.
  3. Подтверждение человеком. Рекомендации только считают. Поставка появляется у площадки после галок и окна подтверждения: как делить, как назвать, отдать ли сразу на сборку одной партией. После создания видно, какие задания не попали и почему.
  4. Передача в доставку. Сначала площадка отвечает, приняла ли она коды маркировки из загруженного шаблона. Передать в доставку можно, только если эта проверка свежее 10 минут и без блокеров. После передачи ERP перечитывает поставку у площадки, пока не увидит, что та закрыта.
Первое исключение из правила только читать

Четыре операции записи, у каждой строка в журнале

До этого модуля ERP ничего не писала во внешние системы. Исключение сделано узким: создать поставку, добавить задания, задать параметры отгрузки и передать в доставку. Каждая запись ложится в журнал с запросом и ответом, его видно на вкладке поставки. Если площадка потеряет поставку, есть чем ответить в поддержку. Коды маркировки по-прежнему уходят шаблоном, который загружает человек: это честное ограничение.

Отказник ищется делением пополам

Задания добавляются пачками по 100. Если площадка отвергла пачку, она делится пополам, пока не найдутся конкретные задания-отказники. Они остаются в очереди с причиной, остальные доезжают. Одно плохое задание не роняет поставку.

След остаётся раньше записи

Строка поставки сохраняется сразу после её создания у площадки, до добавления заданий. Если дальше что-то упадёт, ERP знает о пустой поставке и удаляет её. От двойного клика защищает замок, а задания в очереди блокируются, чтобы параллельный опрос их не переставил.

Одна поставка, один склад, один габарит

Смешанную выборку система режет сама: крупногабаритное отдельно, юрлица отдельно, и в строке плана об этом написано. Человеку не нужно помнить правила площадки, чтобы не получить отказ.

Расформировали в кабинете

Если поставку расформировали на стороне площадки, ERP это замечает при опросе. Задания возвращаются в очередь, собранное остаётся собранным, а пустая поставка из ERP удаляется.

Поставка из ERP это одна партия

Сканер работает по партии. Задания, которые площадка перенесла в другую поставку, на каждом опросе сами отвязываются от старой партии, а баннер предлагает объединить партии, если их всё же стало несколько.

Докинуть в открытую поставку

Новые задания того же артикула можно добавить в уже открытую поставку из панели выбора или со страницы поставки. Они встают в ту же партию, и сборщик продолжает с того места, где был.

Горящие: сборка по срочности

Иногда собирать нужно не по типу товара, а по возрасту заказа. В кабинете видно самый старый заказ, а в очереди раньше не было ни даты, ни возраста. Теперь у очереди две вкладки: вся очередь и горящие. На горящих стоит лесенка ждут дольше 20, 24, 36, 48 и 72 часов со счётчиками, а колонка Ждёт показывает возраст самого старого задания: от порога янтарный, от 48 часов красный.

Вкладка Горящие: лесенка возраста заданий и дерево срочных артикулов
Вкладка Горящие: лесенка по возрасту заказа, порог правится прямо на вкладке, дерево отсортировано от самых старых заданий.
Окно создания поставок: способ деления, имена поставок, число заданий
Окно подтверждения: одна поставка, по поставке на карточку или на категорию, редактируемые имена и отдача на сборку одной партией.

Фильтр возраста на сервере

Дерево приходит уже суженным, поэтому выбор, план и создание поставок ничего не знают про срочность и остаются теми же. Отметил категорию на горящих, и в поставку уходят только горящие задания этой категории, а свежие остаются в очереди.

Лесенка вместо одной кнопки

При пороге 20 часов в выходной горит половина очереди. Самые старые менеджер берёт ступенью от 48 часов, а не правкой настройки. Вкладка живёт в адресе страницы, на неё можно дать ссылку.

Призраки не горят

Задания, по которым площадка уже отдала конечный статус, но которые у неё висят новыми, из расчёта возраста исключены. Иначе они навсегда остались бы самыми старыми и прятали настоящие срочные.

Страница поставки: проверка кодов маркировки у площадки и передача в доставку
Страница поставки: проверка кодов маркировки у площадки, блокеры красным списком и передача в доставку. Рядом вкладка Журнал WB с каждой записью, запросом и ответом.
4записи у площадки, все в журнале
100заданий в пачке, деление пополам
10 мингодна проверка кодов
20-72 члесенка горящих заданий
11 Контроль склада

Сборщики, поставщики и приёмка в цифрах

Когда поставки и сканы живут в ERP, у владельца появляются ответы на вопросы, которые раньше решались на глаз: чем был занят сборщик, какие товары дают отказы сканера, чего не хватает на складе и сколько товара поставщика площадка на самом деле приняла.

Статистика сборщиков

До четырёх сборщиков работают параллельно. Смен и норм в системе нет, поэтому всё считается по сканам. Присутствие длится от первого до последнего скана дня. Активное время складывается из пауз между соседними сканами короче порога: 5, 10 или 15 минут на выбор, длиннее это простой, обед или другая работа. Скорость это штуки за активный час, загрузка это активное время к присутствию. Эффективность одним числом сравнивает человека со средним по складу за тот же период, а скорость и загрузка рядом объясняют, за счёт чего оно такое. Динамику по людям видит только владелец.

Статистика сборщиков: выработка, загрузка и эффективность с динамикой по дням
Сотрудники: штуки, активное время, присутствие, скорость, загрузка и эффективность относительно среднего по складу. Клик по человеку открывает его динамику по дням.
Загрузка склада за день: собранные штуки по пятиминуткам и отказы сканера
Загрузка за день: столбик это штуки за пять минут по каждому человеку, провал между столбиками значит, что в это время никто не сканировал. Отказы идут отдельным рядом.

Отчёты не расходятся

Цифры статистики за день обязаны совпадать со сводкой по смене, и это закреплено тестом. Агрегатных таблиц нет, всё считается на лету, а миграция добавила только индексы, построенные без блокировки горячих таблиц посреди смены.

Товар отказа по коду

У двух третей отказов задание не найдено: сканер отказал раньше. Поэтому товар восстанавливается по самому коду той же цепочкой, что у сканера. Так видно, какие товары поставщика дают отказы, и с этим можно идти к поставщику.

Шум отсекается

Учётная запись без единой собранной штуки и меньше чем с десятью сканами в статистику не попадает: это проверка сканера или тестовый вход. Маркированный товар собирается дольше, и экран прямо пишет, что сравнение людей на разном товаре условно.

Дозаказ поставщикам в МойСклад

Раньше сборщик глазами находил, что товара нет, и к обеду писал об этом в имени поставки. Список уходил поставщику, тот собирал его ещё несколько часов и привозил на следующий день: терялись сутки, а просрочка сборки бьёт по рейтингу. Теперь кнопка в очереди раскладывает несобранные задания по поставщикам из карточек МойСклад и создаёт по заказу поставщику на каждого. Наборы раскладываются на компоненты по техкарте, упаковка не заказывается. Это другая функция, чем заказ производителю из раздела 08: там буфер ТОС, здесь конкретные задания FBS.

Окно заказов поставщикам: заказ на каждого поставщика, раскрытые товары и предупреждения
Заказы поставщикам: отдельный заказ на каждого поставщика, строка раскрывается в товары, наборы уже разложены. Сверху предупреждение о товарах без поставщика: они не попадут ни в один заказ, пока карточку не поправят.

Каждое задание заказывается один раз

Заказали 100, пришло ещё 7, и следующий заказ будет на 7, а не на 107. Это держит частичный уникальный индекс в базе. Если заказ удалили или отменили в МойСклад, его задания снова идут к заказу: статусы сверяются перед каждым предпросмотром.

Запрос в МойСклад уходит ровно один раз

Строка журнала с внешним кодом сохраняется до запроса. При обрыве или таймауте документ сначала ищется по этому коду, и только если его нет, задания освобождаются. Иначе повтор создал бы второй заказ поставщику.

Документ непроведённый

Заказ создаётся непроведённым, остаток склада пока не вычитается: лишнее люди вычёркивают в МойСклад. Это осознанная первая итерация. После создания документ перечитывается, и сверяются число строк и штук.

Один токен на всю ERP

Одним токеном МойСклад читает ночной синк и создаются заказы, менять его может только суперпользователь. Карточки читаются в момент нажатия: поставщика и штрихкод менеджер правит днём, и кнопка сразу видит правку.

Учёт приёмки по поставщикам

Часть поставщиков работает как фулфилмент: сами везут товар на склад площадки, а платят им по принятому. Передали 600, площадка приняла 599, оплатят 599. Экран учёта показывает, сколько товара каждого поставщика сдано в поставках FBS и сколько из этого принято, по дате приёмки или по дате сдачи, с разбивкой по дням и товарам и отдельной вкладкой того, что сдано и до сих пор не принято.

Учёт поставщиков: сдано, принято площадкой и ждёт приёмки по каждому поставщику
Учёт поставщиков: карточки итогов сдано, принято, ждёт и отменено, ниже разбивка по дням с раскрытием в товары и своей выгрузкой в Excel.

Принятое не откатывается

Поставщик фиксируется в момент сдачи, а отметка принято липкая. Заказ, отменённый покупателем уже после приёмки поставки на складе площадки, тоже считается принятым: поставщик сдал, склад получил.

Неточная дата честно помечена

Дата приёмки берётся по скану поставки на складе площадки. Если скана нет, ставится момент, когда ERP увидела приёмку, с пометкой неточно, и позже уточняется.

Поиск по всей истории без индекса

Одно поле на экране поставок ищет по номеру, артикулу, штрихкоду, названию, заданию и стикеру во всех поставках, включая закрытые. Около 155 тысяч заданий проходятся за 150-270 мс по замеру на боевой базе, поэтому индекс и миграция с блокировкой горячей таблицы посреди смены пока не нужны.

5 / 10 / 15мин: порог паузы в статистике
1вид документа в МойСклад
30 минцикл обновления учёта приёмки
200+76автотестов модуля FBS
12 Склады

Признак исключения это дата, а не галочка

Справочника складов в системе не было: имя склада жило голой строкой внутри дневных снимков остатков, и выключить склад из расчётов было нечем. Заказчик держал заметную часть товара на складах, которых уже нет, а досорт, товарооборот и РНП считали этот товар живым остатком.

Справочник складов с ролями, кластерами и датой исключения из расчётов
Справочник складов: роль, регион, кластер, остаток и признак участия в расчётах. Наверху доля остатка, лежащая на выключенных складах: до появления справочника этот товар считался живым.

Дата, а не булево

Заказчик просил указывать, с какого числа склад отключён, и пересчитывать историю начиная с неё. Снимки остатков старше этой даты остаются как были: прошлое не переписывается.

Журнал отвечает на вопрос "почему"

Два поля в строке склада отвечают только на вопрос "кто выключил его сейчас". Вопрос заказчика был другой, и на него отвечает история: кто, когда, с какой даты и по какой причине.

Строка пишется по факту изменения

Повторное сохранение с теми же данными журнал не пополняет. Иначе за неделю он зарастает шумом и перестаёт отвечать на исходный вопрос.

Защищённые строки

Псевдосклады "в пути" и точки приёмки исключать нельзя: они не хранят товар, а обозначают его движение. Попытка выключить такую строку отвергается сервером, а не только гардом на фронте.

13 Как это построено

Девятнадцать письменных ТЗ, одна пара рук

Сначала бизнес-анализ. Каждый этап начинался с самодостаточного письменного ТЗ: контракты данных, формулы, критерии приёмки. Реализация шла шаг за шагом по железным правилам, записанным после предыдущего ERP-проекта. Каждое изменение небольшим проверяемым шагом.

  1. Согласовано клиентское ТЗ: источники данных, таблица пяти складов, логика буфера, алерты, РНП, комбинаторика наборов. Контракты данных и критерии приёмки зафиксированы до первой строки кода.
  2. Реализованы все функциональные этапы ТЗ-00...ТЗ-10: усиленная инфраструктура, портал с ролями, три API-синка, РНП, Google-витрина, контроль товарооборота с DBM, алерты, дашборд, наборы, экспорт, ABC.
  3. Редизайн интерфейса: тёмный сворачиваемый сайдбар, карточный режим, 10 тем на CSS-токенах. Контраст всех тем проверяется тестом, а не глазами.
  4. Склад стал отдельным продуктом внутри ERP: сборка FBS со сканером, поставки из очереди, горящие задания, статистика сборщиков, дозаказ поставщикам и учёт приёмки. Каждый шаг начинался с разбора процесса на складе и письменного ТЗ.
  5. Приёмка - это документ. Отчёт приёмки на 630 строк, матрица покрытия требований на 187 строк и честный раздел с 18 недочётами и отступлениями. Каждая цифра этого кейса прослеживается до отчёта или репозитория.
Честные ограничения

Чего система не делает вид, что знает

Автоматический сезонный коэффициент пока равен 1: API маркетплейса хранит только ~6 скользящих месяцев истории, поэтому сезонность год к году физически недоступна. Система честно пишет "мало истории" и копит собственную. Ключ сервисного аккаунта Google на приёмке ещё не был передан, поэтому модуль мягко пропускает работу, а не падает. Парсинг конкурентов написан, но ждёт прокси. Все три пункта записаны в отчёте приёмки, а не обнаружены потом.

Стек

PythonFastAPISQLAlchemy 2 asyncAlembic Celery + BeatPostgreSQL 16Redis 7 Docker Composenginx React 18TypeScriptViteTailwind Radix UIZustandTanStack Queryrecharts

Интеграции

Wildberries APIAPI МойСклад API TrueStatsAPI Честного знакаGoogle Sheets API

Масштаб

110 таблиц БД19 письменных ТЗ ~70K строк кода1 286+440 автотестов
14 Итог

Было и стало

ОбластьБылоСтало
Решения по досорту Личный Excel владельца, ежедневно, по ощущениям. Держалось на одном человеке. Буферы ТОС с зонами-светофором по каждому SKU, самокоррекция шагами ±1/3, каждое изменение подтверждает человек.
Данные Три сервиса плюс ручные выгрузки. Единой картины нет. Один PostgreSQL, ночной конвейер с гейтами свежести и идемпотентными импортами.
Обнаружение проблем Глазами, когда кто-то случайно посмотрел. 7 типов алертов; 388 ранее невидимых эпизодов подняты в первый же день, крайняя срочка всегда сверху.
Экономика месяца Понятна после окончания месяца. Дашборд с прогнозом конца месяца по тренду 15 дней, за 61 мс.
Планирование наборов Комбинаторика в голове владельца. 1 813 рассчитанных вариантов за 3,0 с, у каждого нового кандидата письменное "Почему".
Заказ производителю Собирался вручную под каждый заказ. Галки плюс одна кнопка: точный Excel-формат производителя, с историей и снапшотами.
Поставки FBS Галки в кабинете площадки сверху вниз, в одной поставке сотня разных артикулов. Очередь по складу и предложенная сборка: артикул своей поставкой, горящие заказы первыми, передача в доставку из ERP с журналом.
Нехватка товара Сборщик к обеду писал, чего нет, в имени поставки, список уходил поставщику. Заказ поставщику в МойСклад из очереди FBS, наборы разложены по техкарте, каждое задание заказывается один раз.
Контроль склада Кто чем был занят и какие товары дают отказы, видно не было. Статистика сборщиков по сканам и учёт приёмки по поставщикам: сдано, принято площадкой, ждёт.
Доверие "Должно работать." 1 286+440 автотестов каждое утро в чистой базе, результат виден заказчику на странице Здоровье.

Кирилл Седов

Бизнес-аналитик и строитель систем. Начинаю с бизнес-процесса и контракта данных, потом автоматизирую. Эта ERP спроектирована, описана и выведена в прод и ведёт досорт заказчика на буферах Голдратта, а не на интуиции.

Связаться в LinkedIn