Распределённая логистика: что стенд теперь умеет мерить
Четыре дефекта конфигурации найдены и устранены, восемь недель истории спроса записаны задним числом, остатки вернулись на сценарные значения. Поверх этого четыре процесса из девяти закрыты прямо в базе — пятью полями и пятью серверными действиями, без единой строки внешнего модуля. Стенд перешёл из состояния «данные выглядят правдоподобно» в состояние «failover нажимается кнопкой и логируется».
База
logistic-node
Сборка
saas~19.4
Узлов
8
Маршрутов
56
Правил пополнения
160
История спроса
8 недель
Закрыто настройкой
4 из 9
01 / РЕВИЗИЯ
Что было настроено неверно
Проверка шла по конфигурации, а не по интерфейсу: правила тяги, триггеры орденпоинтов, горизонт компании, именование типов операций. Четыре находки, все исправлены через коннектор.
1
Удвоенное время доставки на всех 56 маршрутах исправлено
Задержка стояла на обеих ногах маршрута — и «склад → транзит», и «транзит → назначение». Орденпоинт при этом показывал lead_days только по второй ноге, а физически груз шёл вдвое дольше. Доказано замером: маршрут S5 ← S3 объявлял 2 дня, но между S3/OUT/00001 (11.09) и S5/IN/00001 (13.09) прошло 4 дня.
Было
delay на обеих ногах заявлено 2 дн. → фактически 4 дн.
121 правило пополнения из 160 не срабатывало исправлено
Все 120 магазинных правил и одно на WH-A стояли в ручном триггере. Ежедневный планировщик их не видел, то есть сеть физически не пополнялась — а значит любой замер failover считал бы действия в мёртвой системе.
Было
39 auto · 121 manual
Стало
160 auto
3
Горизонт пополнения — год исправлено
res.company.horizon_days = 365 заставлял отчёт пополнения смотреть на годовую потребность вперёд. Прогноз считался до сентября 2027-го, и любая оценка «сколько заказать» была оторвана от недельного цикла магазина.
Было
365 дн. → горизонт 14.09.2027
Стало
7 дн. → горизонт 21–22.09.2026
4
Рассинхрон именования типов операций исправлено
WH-A остался на английских названиях, когда остальные семь узлов были переименованы, а его POS-тип использовал чужой префикс WH/POS/. В списках операций это ломало сортировку и мешало считать действия по узлам.
Было
WH-A: Delivery Orders, Receipts префикс WH/POS/
Стало
единая схема по всем узлам префикс WHA/POS/
Отменённая правка
Собирался добавить маршрут Buy всем 20 товарам — у них пустой route_ids. Проверка stock.route id 4 показала product_selectable: false: в 19-й версии Buy выбирается на складе, а не на товаре. Пустой список здесь — норма, а не дефект. Правка отменена до внесения.
Оставлено намеренно
propagate_cancel: false на транзитных правилах — чтобы отмена цепочки требовала ручных действий и замер failover оставался честным. Нулевой остаток FST-03 на WH-B, орденпоинт 125 (латераль S5 ← S3) и 103 (каскад WH-A ← WH-B) — это сценарные заготовки, а не ошибки.
02 / ТОПОЛОГИЯ
Состояние сети на момент замера
Все восемь узлов одношаговые (one_step / ship_only) — по прямому указанию, чтобы лишние шаги приёмки не раздували счётчик действий. Полная сетка Resupply From: каждый узел может питать каждый.
Открытые перемещения на момент замера — предаварийное состояние
Узел
Тип
Готово
В ожидании
Всего
WH-A
приёмки + отгрузки
13
0
13
S1
поступления
1
1
2
S2
поступления
1
0
1
S3
поступления + отгрузка
1
5
6
S5
поступления
0
1
1
Итого
—
16
7
23
Плюс 5 подтверждённых заказов клиентов и 2 проведённых закупки на WH-A. Это и есть тот «живой» фон, поверх которого будет считаться отключение центрального узла.
03 / ИСТОРИЯ
Восемь недель спроса, которых не было
Главный пробел стенда: в базе не было ни одной завершённой продажи. Без потребления нечем считать дневную скорость, нечем обосновать мин/макс и невозможно проверить подсказку количеств по историческому спросу. Записано 48 отгрузок «магазин → покупатель» за 20.07 — 07.09.2026.
Механика
Остатки сперва подняты инвентаризацией ровно на восьминедельный расход, и только потом проведены отгрузки — чтобы склад приземлился обратно на сценарные значения без второго корректирующего прохода. Провести задним числом напрямую нельзя:button_validate всегда ставит текущий момент, поэтому после валидации дата переписывается в трёх моделях — stock.picking.date_done, stock.move.date и stock.move.line.date. Пропустить любую из них — и отчёты по историческому спросу отнесут продажу не к той неделе.
Недельный расход по профилям, единиц
Неделя
Дата
Базовый профиль S2 · S3 · S4 · S5
—
Профиль FST×2 S1 · S6
—
W1
20.07.2026
42
61
W2
27.07.2026
46
65
W3
03.08.2026
36
53
W4
10.08.2026
41
61
W5
17.08.2026
37
60
W6
24.08.2026
36
55
W7
31.08.2026
35
55
W8
07.09.2026
37
55
Σ
8 недель
310
≈ 39 ед/нед
465
≈ 58 ед/нед
Сверка: записанный расход против итогового остатка
Магазин
Документов
Строк
Продано
Остаток факт
Остаток цель
Сходится
S1
8
114
465
722
722
да
S2
8
114
310
712
712
да
S3
8
114
310
719
719
да
S4
8
114
310
746
746
да
S5
8
114
310
725
725
да
S6
8
114
465
770
770
да
Итого
48
684
2 170
4 394
4 394
да
Сценарные перекосы уцелели: S1/ESP-01 = 1, S2/FST-01 = 2, S3/UNV-01 = 0, S4/ESP-01 = 25, S6/FST-01 = 60. Отрицательных остатков нет ни в одной точке.
04 / ПОКРЫТИЕ
Девять процессов против штатного Odoo
Граница проходит не по «физике», а по «решениям». Перемещение груза Odoo закрывает полностью. Выбор, откуда и когда брать, когда привычный источник недоступен, — не закрывает ничем. Четыре вердикта ниже сменились с «доработки» на «конфиг»: это решения, которые удалось выразить полями и серверными действиями прямо на стенде — см. раздел 05.
№
Процесс
Что показал стенд
Вердикт
1
Сеть узлов и маршруты поставки
Полная сетка 8×8 = 56 маршрутов через транзитную локацию собирается штатно. Odoo сам сливает отгрузки на разные магазины в один документ.
штатно
2
Состояние узла доступен / ограничен / выведен
В модели понятия нет, архивирование с остатком заблокировано. Закрыто настройкой: поля x_node_state и x_failover_to выведены на форму склада отдельным блоком. Отключение узла больше не требует архивирования. Среднее состояние «ограничен» тоже не декорация: такой узел не выбирается ни донором, ни резервом, но уже назначенные на него правила не срываются с места.
конфиг
3
Плановое пополнение по мин/макс
160 орденпоинтов работают после перевода в auto. Горизонт и lead time теперь дают осмысленные даты.
вручную
4
Переключение источника у точки
Массовая смена маршрута в списке правил работает — замерено 7 действий на точку.
вручную
5
Каскад источников первый непустой донор
Само правило по-прежнему знает один источник. Каскад надстроен действием: обходит x_source_priority, сверяет свободный остаток и переводит правило на первый покрывающий узел. Проверено — при нужде 29 пропустило WH-A (27) и выбрало WH-B (30). Ночной ir.cron прогоняет каскад по всем дефицитным правилам перед планировщиком.
конфиг
6
Исполнение переброски
Транзит, приёмка, батчи — штатно. Действия считаются, оценки реалистичны.
вручную
7
Правило выбора латерального донора
Маршрут между магазинами настраивается штатно (S5 ← S3). Выбор донора закрыт действием: считает излишек сверх собственного максимума магазина и берёт наибольший. Проверено — S1/FST-01 нашёл S6 с излишком 45, отсеяв пять кандидатов с нулём.
конфиг
8
Переадресация документов
Смена склада на подтверждённом заказе и Deliver To на подтверждённой закупке проходят, но отгрузка и приёмка остаются на старом складе.
вручную
9
Failover узла
Стал кнопкой в меню склада: одно действие вместо кабинетных ≈ 70–115 в 6 меню. Проверено на WH-A — 60 правил шести магазинов переключено на WH-B и возвращено ровно в исходное состояние. Заодно глушатся 20 собственных правил выведенного узла: мёртвый склад больше не заказывает себе товар.
конфиг
Проверки на стенде
Утверждения о 19-й версии, которые проверены запросом к базе, а не памятью о прошлых версиях.
№
Проверка
Результат
Итог
01
Архивирование склада с остатком
Заблокировано дословно — как и предполагалось кабинетно.
подтв.
02
Массовая смена маршрута в списке
Работает. Прогнано на S1 и возвращено обратно, чтобы полный замер шёл с чистого листа.
подтв.
03
Смена склада на подтверждённом SO
Проходит, но документ отгрузки остаётся на старом складе.
частично
04
Слияние отгрузок через транзит
Odoo объединяет отгрузки на разные магазины в один документ — незапланированный плюс к failover.
плюс
05
«Suggest quantities» из документации 19.0
В сборке saas~19.4 функция не найдена. Требует проверки глазами в каталоге товаров на закупке.
нет
06
Маршрут Buy на товаре
product_selectable: false — в 19-й выбирается на складе. Пустой route_ids у товаров корректен.
норма
07
Источник lead_days орденпоинта
Берётся с ноги «транзит → назначение». Физическая длина цепочки — сумма delay обеих ног.
нюанс
08
Дубли MTO-правил при полной сетке
Каждый маршрут добавляет своё правило «Stock → Транзит» в общий маршрут MTO: 56 идентичных правил вместо 8.
шум
09
Пять цепочек на одном орденпоинте пересмотрено — версия «повторных прогонов» не подтвердилась
Считалось, что каждый прогон планировщика плодит новую цепочку. Не подтвердилось: все четыре документа созданы в одну секунду, одним правилом, с одним origin OP/00123 и одним сроком. Переордера тоже нет — 0 на руках + 31 входящих − 24 исходящих даёт прогноз ровно 7, как и показывает Odoo. Дефект не в количестве, а в дроблении документов.
не дефект
10
Куда делись группы снабжения
Причина дробления структурная: модели procurement.group в saas~19.4 нет вообще, поле group_id убрано и с stock.move, и с stock.picking. Сливать процюремент одного орденпоинта в один документ больше нечем — на эту роль в 19-й остаются батчи.
архитектура
11
qty_to_order = 0 при остатке ниже минимума
Не дефект: qty_forecast уже покрыт открытыми входящими. Судить по qty_to_order_computed и незакрытым перемещениям.
норма
12
Остаток по отдельному узлу
Читается контекстом warehouse_id. Старый ключ warehouse из прошлых версий молча игнорируется: отдаёт остаток по всей компании — 1 920 вместо 800. Ошибки не будет, будет неверное число.
ловушка
13
Ограничения safe_eval в серверных действиях
Присваивание атрибута запрещено: wh.x_node_state = 'down' падает на forbidden opcode STORE_ATTR. Писать только через .write({...}).
нюанс
14
Куда пишет log() из действия
В ir.logging, а не в чаттер записи. Беда в том, что ir.logging живёт за режимом разработчика — оператор туда не попадёт. Поэтому действия теперь возвращают display_notification: результат приходит всплывающим окном прямо на экран, а в журнал пишется вторым экземпляром для разбора.
исправлено
15
Studio и автоматизации
web_studio и base_automation на базе не установлены. Но ir.actions.server работает — в базе уже 211 действий с кодом, и ручные поля x_* создаются штатно.
норма
API 19-й версии — отличия, на которые напоролся
read_group убран, вместо него formatted_read_group с groupby и aggregates. У орденпоинта больше нет qty_multiple, у stock.picking — group_id. Поля qty_to_order и show_supply_warning нехранимые: по ним нельзя ни агрегировать, ни фильтровать. В stock.picking.create каждая строка move_ids обязана нести свои location_id и location_dest_id — от заголовка они не наследуются.
05 / СОБРАНО
Что закрыто режимом разработчика
Studio и base_automation на базе не установлены, но этого и не потребовалось: шести ручных полей, пяти серверных действий, двух наследованных представлений и одного планировщика хватило, чтобы процессы 2, 5, 7 и 9 заработали без единой строки во внешнем модуле. Всё создано данными — переносится на прод выгрузкой записей.
ID
Объект
Назначение
Проверка
19577
stock.warehouse.x_node_state
Состояние узла: доступен / ограничен / выведен. То самое понятие, которого в модели не было.
8 узлов
19578
stock.warehouse.x_failover_to
Резервный узел. WH-A ↔ WH-B замкнуты друг на друга.
Память маршрута до переключения — без неё возврат узла в сеть необратим.
round-trip
19585
…orderpoint.x_route_before_lateral
То же для латерала, отдельным полем: латераль может висеть одновременно с failover и не должна его затирать.
round-trip
19587
…orderpoint.x_muted_by_failover
Метка «правило приглушено потому, что его узел выведен». Без неё возврат не отличил бы приглушённые правила от тех, что оператор перевёл в ручной режим сам.
20 из 20
2770
Форма склада: блок состояния наследование stock.view_warehouse
Состояние, резервный узел и каскад выведены на форму после контрагента. До этого поля существовали, но оператор их не видел — а значит, их как бы и не было.
видно в UI
2771
Список правил: две колонки
Маршрут до failover и метка приглушения добавлены как optional="hide" — включаются галочкой, когда нужно разобрать, что натворило переключение.
видно в UI
933
Вывести узел из сети действие на складе
Ставит состояние «выведен», переводит все правила, что кормились от узла, на резервный, запоминает прежние маршруты.
60 из 60
934
Вернуть узел в сеть
Обратная операция строго по запомненным маршрутам, а не по догадке.
60 из 60
935
Выбрать источник по каскаду действие на правиле
Первый узел из приоритета, у которого свободного остатка хватает на qty_to_order. Выведенные узлы пропускает.
WHB, не WHA
936
Латеральная переброска: найти донора
Донор — магазин с наибольшим излишком сверх собственного максимума. Хабы из каскада исключаются, чтобы латераль не выродилась в обычное пополнение.
S6, +45
937
Отменить латерал
Возврат правила на маршрут до переброски.
round-trip
70
Каскад: ночной прогон ir.cron, ежедневно 02:00
Собирает правила с ненулевым дефицитом и прогоняет по ним каскад перед планировщиком. Если дефицитов нет — пишет об этом строку и ничего не трогает. Прогнан вручную дважды; на время замеров снят с расписания, чтобы не двигал стенд между прогонами.
прогнан
Что показал прогон failover на WH-A
Одно действие вместо ≈ 70–115 ручных: 60 правил шести магазинов переключились с WH-A на WH-B, и каждое запомнило свой прежний маршрут. Возврат восстановил конфигурацию в точности — 20/20/20 по S1–S3, ни одного осиротевшего поля памяти. Защита сработала отдельно: попытка вывести WH-B, когда WH-A уже выведен, отклонена с текстом «резервный узел сам выведен из сети» — сеть нельзя обезглавить двумя кликами. Каждый прогон оставил строку в ir.logging.
Чему научил первый прогон крона
Ночной каскад с ходу переписал двенадцать правил на самих хабах: у WH-A и WH-B тоже был заполнен каскад источников — он нужен им для failover, — и действие честно подменило им маршрут Buy на межскладское пополнение. Хабы перестали закупать у поставщика и начали перекладывать друг у друга. Лечится одной строкой: правило трогаем, только если его текущий маршрут межузловой; закупочное не наш случай. Двенадцать правил разобраны поимённо — одиннадцать возвращены на Buy, двенадцатое сидело на межскладском маршруте изначально. Автоматика, которая умеет перенацеливать, обязана уметь и молчать.
06 / ОСТАТОК
Где настройка упирается в потолок
Собранное — это слой перенацеливания поверх штатных правил, а не многоисточниковый движок. Он решает задачу в момент запуска и на этом честно заканчивается. Что остаётся модулю:
Ограничение 01
Раз в сутки, а не по событию
Крон закрыл худшую часть: каскад считается сам, ночью, перед планировщиком. Но это по-прежнему опрос по расписанию. Провал остатка в 02:05 будет замечен через сутки, а правильное место для пересчёта — момент подбора правила, и туда данными не дотянуться.
Ограничение 02
Один донор на правило
Если нужны 100, а у двух узлов по 60 — действие не возьмёт 60 + 40, оно сообщит, что донора нет. Дробление потребности между источниками штатной моделью не выражается.
Ограничение 03
Расстояние не учитывается
Донор выбирается по величине излишка. Логистического веса — плечо, стоимость, срок — в модели нет, и добавить его полем без пересчёта в подборе бессмысленно.
Ограничение 04
Документы не переезжают
Процесс 8 остаётся ручным: смена склада на подтверждённом заказе проходит, но отгрузка остаётся на старом узле. Это правится только кодом в stock.
Ограничение 05
История не хранится
Оператор видит итог во всплывающем окне, но окно живёт до первого клика. Постоянный след — только в ir.logging, а он за режимом разработчика и не привязан ни к складу, ни к правилу. На вопрос «кто и когда вывёл узел» настройка не отвечает.
Ограничение 06
Сливать документы больше нечем
Одна потребность распадается на пять цепочек, и собрать их обратно нельзя: procurement.group в 19-й версии нет, group_id убран и с перемещения, и с документа. Штатная замена — батчи, но это склейка на исполнении, а не единая заявка. Пока это косметика; станет проблемой, когда трудоёмкость failover будем измерять в документах.
07 / ПРОТОКОЛ
Замер failover WH-A
Стенд к прогону готов: сеть живая, пополнения идут, история есть, перекосы на месте. Замеряется число действий оператора и время по шагам — теперь уже против собранной кнопки, а не в пустоту: конфигурационная ветка прогнана и заняла одно действие.
Зафиксировать исходное. 23 открытых перемещения, 5 заказов клиентов, 2 закупки, остатки по всем восьми узлам.
Вывести WH-A вручную. Попытка архивировать с остатком 5 075 ед — ожидается отказ. Зафиксировать, чем оператор заменяет отключение, не имея поля состояния.
Переключить источники вручную. Шесть магазинов переводятся на WH-B массовой сменой маршрута — ожидается 7 действий на точку. Эталон для сравнения: действие 933 делает то же за одно и логирует «60 из 60».
Разобрать транзит. 13 документов WH-A: что отменяется, что доезжает, что зависает. propagate_cancel: false оставлен намеренно — отмена не каскадирует.
Переадресовать документы. 5 заказов и 2 закупки на WH-A: смена склада проходит, но отгрузки остаются на старом — считать доработку руками.
Прогнать планировщик. Проверить, что WH-B покрывает спрос: 2 490 ед против недельного расхода сети ≈ 271 ед — запас примерно на 9 недель.
Свести замер. Число действий и меню против кабинетной оценки 70–115 в 6 меню — и против одного действия на собранной кнопке. Это и есть цена процесса 9 в понятных единицах.