Ревизия стенда · Odoo 19 saas~19.4 · logistic-node.odoo.com

Распределённая логистика: что стенд теперь умеет мерить

Четыре дефекта конфигурации найдены и устранены, восемь недель истории спроса записаны задним числом, остатки вернулись на сценарные значения. Поверх этого четыре процесса из девяти закрыты прямо в базе — пятью полями и пятью серверными действиями, без единой строки внешнего модуля. Стенд перешёл из состояния «данные выглядят правдоподобно» в состояние «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 дн.
Стало
112 отгрузочных правил → delay 0
56 приёмных: 6×1 дн, 44×2 дн, 6×3 дн
2

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Центральный склад5 075 ед · 20 SKU
WH-BРезервный склад2 490 ед · 20 SKU
ТранзитInter-warehouse transit56 маршрутов × 2 ноги
S1Подол · дефицит ESP-01722 ед
S2Оболонь · дефицит FST-01712 ед
S3Троещина · ноль UNV-01719 ед
S4Львов · донор ESP-01746 ед
S5Одесса · база725 ед
S6Днепр · донор FST-01770 ед

жирная рамка — сценарный перекос · пунктир — транзитная локация

Открытые перемещения на момент замера — предаварийное состояние
УзелТипГотовоВ ожиданииВсего
WH-Aприёмки + отгрузки13013
S1поступления112
S2поступления101
S3поступления + отгрузка156
S5поступления011
Итого16723

Плюс 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
W120.07.20264261
W227.07.20264665
W303.08.20263653
W410.08.20264161
W517.08.20263760
W624.08.20263655
W731.08.20263555
W807.09.20263755
Σ8 недель310≈ 39 ед/нед465≈ 58 ед/нед
Сверка: записанный расход против итогового остатка
МагазинДокументовСтрокПроданоОстаток фактОстаток цельСходится
S18114465722722да
S28114310712712да
S38114310719719да
S48114310746746да
S58114310725725да
S68114465770770да
Итого486842 1704 3944 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 на подтверждённой закупке проходят, но отгрузка и приёмка остаются на старом складе.вручную
9Failover узлаСтал кнопкой в меню склада: одно действие вместо кабинетных ≈ 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-й остаются батчи.архитектура
11qty_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: результат приходит всплывающим окном прямо на экран, а в журнал пишется вторым экземпляром для разбора.исправлено
15Studio и автоматизацииweb_studio и base_automation на базе не установлены. Но ir.actions.server работает — в базе уже 211 действий с кодом, и ручные поля x_* создаются штатно.норма
API 19-й версии — отличия, на которые напоролся

read_group убран, вместо него formatted_read_group с groupby и aggregates. У орденпоинта больше нет qty_multiple, у stock.pickinggroup_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ОбъектНазначениеПроверка
19577stock.warehouse.x_node_stateСостояние узла: доступен / ограничен / выведен. То самое понятие, которого в модели не было.8 узлов
19578stock.warehouse.x_failover_toРезервный узел. WH-A ↔ WH-B замкнуты друг на друга.задано
19579stock.warehouse.x_source_priorityУпорядоченный каскад источников. Киевские магазины — WHA,WHB; региональные — WHB,WHA.6 точек
19580…orderpoint.x_route_before_failoverПамять маршрута до переключения — без неё возврат узла в сеть необратим.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

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

  1. Зафиксировать исходное. 23 открытых перемещения, 5 заказов клиентов, 2 закупки, остатки по всем восьми узлам.
  2. Вывести WH-A вручную. Попытка архивировать с остатком 5 075 ед — ожидается отказ. Зафиксировать, чем оператор заменяет отключение, не имея поля состояния.
  3. Переключить источники вручную. Шесть магазинов переводятся на WH-B массовой сменой маршрута — ожидается 7 действий на точку. Эталон для сравнения: действие 933 делает то же за одно и логирует «60 из 60».
  4. Разобрать транзит. 13 документов WH-A: что отменяется, что доезжает, что зависает. propagate_cancel: false оставлен намеренно — отмена не каскадирует.
  5. Переадресовать документы. 5 заказов и 2 закупки на WH-A: смена склада проходит, но отгрузки остаются на старом — считать доработку руками.
  6. Прогнать планировщик. Проверить, что WH-B покрывает спрос: 2 490 ед против недельного расхода сети ≈ 271 ед — запас примерно на 9 недель.
  7. Свести замер. Число действий и меню против кабинетной оценки 70–115 в 6 меню — и против одного действия на собранной кнопке. Это и есть цена процесса 9 в понятных единицах.