logistic-node.odoo.com · saas~19.4 · разбор

Схемарешения

Что именно не умеет штатная Odoo 19, когда складов становится восемь, и как это оказалось возможно закрыть настройкой, не трогая исходники. Документ объясняет «почему», а не «куда нажать» — за «куда нажать» отвечает прогонный лист.

Узлов в сети
82 хаба, 6 магазинов
Процессов разобрано
94 закрыто настройкой
Строк кода в ядре
0только поля и действия
Осталось на модуль
5честные ограничения
00 / ЗАДАЧА

Что вообще требуется

Есть сеть из восьми складов: два хаба и шесть магазинов. Товар едет с хаба в магазин через транзит. Пока всё идёт по плану, Odoo справляется идеально. Вопрос в том, что происходит, когда план ломается.

Ломается он тремя способами, и все три — обыденные:

Сбой первого рода
У привычного источника пусто

Магазин всегда берёт с центрального склада. Сегодня на центральном не хватает — но на резервном лежит. Кто-то должен заметить и переключить.

Сбой второго рода
У соседа завал

Одному магазину не хватает, соседнему — девать некуда. Везти от хаба дороже и дольше, чем перекинуть между магазинами. Но никто не знает, у кого излишек.

Сбой третьего рода
Узел выпал целиком

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

Это не экзотика и не «хотелки». Это ровно то, ради чего распределённую сеть и строят: чтобы отказ одного плеча не останавливал продажи. И это ровно то место, где штатная Odoo молчит.

01 / ГДЕ ЛОМАЕТСЯ

Граница проходит не там, где ждёшь

Соблазнительно думать, что чего-то не хватает в логистике. Это не так. Физику Odoo закрывает полностью: транзитные локации, двухногие маршруты, приёмки, батчи, сроки — всё собирается штатно и работает. Не хватает не перевозки. Не хватает решения.

FAILOVER ЛАТЕРАЛЬНАЯ ПЕРЕБРОСКА WH-A ЦЕНТРАЛЬНЫЙ WH-B РЕЗЕРВНЫЙ S1 ПОДОЛ S2 ОБОЛОНЬ S3 ТРОЕЩИНА S4 ЛЬВОВ S5 ОДЕССА S6 ДНЕПР основной источник запасной, если у первого не хватает
Топология стенда. Маршруты собраны полной сеткой 8×8 — 56 штук, каждый через свою транзитную локацию. Физически товар может поехать откуда угодно куда угодно. Сплошные стрелки — кто чей источник по умолчанию; пунктир — кто подхватит, если у первого не хватит. Именно этого пунктира в Odoo и нет как понятия.

Три дырки, к которым всё сводится

Дырка 01
Склад не знает, жив ли он

В модели stock.warehouse нет признака состояния. Есть только «активен / в архиве», и архивировать склад с ненулевым остатком Odoo запрещает дословно — проверено на стенде.

То есть выключить узел из сети штатно нечем: единственный выключатель заблокирован ровно в той ситуации, ради которой он нужен.

Дырка 02
Правило знает один источник

Правило пополнения (orderpoint) хранит route_id — ровно одну ссылку. Один маршрут — один поставщик. Списка приоритетов нет, запасного источника нет, условия «если у первого пусто» нет.

Odoo выполнит правило буквально: закажет с указанного склада, даже если там ноль, и будет ждать.

Дырка 03
Нет понятия «излишек»

Система знает остаток каждого склада, но не знает, сколько ему самому надо, а сколько лишнее. Без этого нельзя ответить на вопрос «у кого можно забрать, не навредив».

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

Сколько это стоит вручную

Пока склада три, дырки не видно: кладовщик просто помнит, что и откуда. На восьми узлах и двадцати позициях это уже 160 правил пополнения. Вывод одного хаба из сети означает вручную переставить маршрут у 60 правил шести магазинов, а потом ещё приглушить 20 собственных правил мёртвого склада, чтобы он не заказывал товар сам себе.

Замер на стенде. Ручной failover одного узла — примерно 70–115 действий оператора в шести разных меню, и это при условии, что он ни разу не ошибётся и ничего не забудет. Обратная операция — столько же, плюс нужно откуда-то знать, какой маршрут у каждого правила был до переключения. Этого нигде не записано.

Необратимость здесь хуже трудоёмкости. Переключить можно; вернуть точно как было — уже нет.

02 / ИДЕЯ

Одна точка, а не три подсистемы

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

Кто решает, откуда приедет товар? Тот, кто записал маршрут в правило

Каскад источников, латеральная переброска и failover узла — это не три разные задачи. Это три разных повода сделать одно и то же действие: пересчитать, какой маршрут должен стоять в orderpoint.route_id, и записать его туда. Дальше штатный планировщик Odoo сам создаст перемещение, транзит и приёмку — он это умеет безупречно.

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

Компонент 01
Паспорт узла

Три поля на складе: состояние, резервный узел, очередь источников. Это те знания о сети, которых в модели не было.

Компонент 02
Память правила

Три поля на правиле: маршрут до failover, маршрут до латерала, метка приглушения. Без памяти любое переключение необратимо.

Компонент 03
Пять действий

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

Почему это вообще получилось без кода. В saas~19.4 не установлены ни Studio, ни модуль автоматизаций. Зато ir.actions.server с питоновским кодом работает штатно — в базе их уже 211 от самой Odoo, — и пользовательские поля x_* создаются через интерфейс. Этого набора хватило ровно впритык.

03 / ЖИЗНЕННЫЙ ЦИКЛ

Что такое узел и что с ним бывает

Без этого раздела дальнейшее читается как фокус. Кнопка «вывести узел» кажется всемогущей — а она намеренно делает мало. Понять, где проходит граница её полномочий, важнее, чем помнить порядок шагов.

Склад — это не строка в справочнике

Когда в Odoo создают склад, рождается не запись, а целое хозяйство. На стенде у WH-A это:

Что порождаетсяСколько на WH-AПочему это важно
Локации WHA (вид)
WHA/Stock (внутренняя)
На них ссылается каждый остаток и каждая строка каждого перемещения за всю историю склада.
Типы операций WHA/IN/ · WHA/OUT/
WHA/INT/ · WHA/POS/
Четыре штуки, у каждого своя нумерация документов. Все приходы, отгрузки, внутренние перемещения и продажи в кассе привязаны к ним.
Маршруты снабжения 7 штук
по числу узлов в Resupply From
Каждая галочка «пополнять отсюда» создаёт маршрут из двух правил: «склад → транзит» и «транзит → получатель». На всей сети это 56 маршрутов.

Отсюда следует то, что сбивает с толку всех, кто приходит из систем попроще: склад в Odoo нельзя удалить. За ним тянется хвост из локаций, типов операций, последовательностей, маршрутов и всех документов, которые на них ссылаются. Единственное, что с ним можно сделать штатно, — спрятать.

Штатный цикл: два состояния и одна дверь

Odoo знает про склад ровно две вещи: active = true и active = false. Активен или в архиве. Ни «временно недоступен», ни «выводится», ни «резерв» — этих понятий в модели нет.

И даже эта единственная дверь заперта. Архивировать склад с ненулевым остатком Odoo отказывается дословно — проверено на стенде. Логика разработчиков понятна: спрятать склад, на котором лежит товар, значит завести неучтённые запасы. Логика верная — но означает она вот что: штатного способа выключить работающий узел не существует вообще. Выключатель заблокирован ровно в той ситуации, ради которой он нужен.

Цикл, который нужен на самом деле

АВАРИЯ · СРАЗУ, ОДНОЙ КНОПКОЙ ДЕЙСТВИЕ 934 · СТРОГО ПО ЗАПОМНЕННЫМ МАРШРУТАМ ДОСТУПЕН ДОНОР · РЕЗЕРВ · ИСТОЧНИК ОГРАНИЧЕН ДОЕЗЖАЕТ НАЧАТОЕ ВЫВЕДЕН НАГРУЗКА НА РЕЗЕРВЕ АРХИВ ВНЕ РЕШЕНИЯ ПЛАНОВО вручную ДЕЙСТВИЕ 933 РУЧНАЯ процедура
Четыре состояния вместо двух. Три первых — это поле x_node_state, четвёртое — штатный архив Odoo, до которого решение сознательно не доводит. Верхняя дуга — аварийный сценарий: из «доступен» сразу в «выведен», одной кнопкой. Нижняя — возврат, и он единственный, кто знает, куда именно возвращать.

Разница между «ограничен» и «выведен» — не в градусе тяжести, а в том, что происходит с уже назначенными правилами:

Состояние · ограничен
Плановый слив

Узел перестаёт выбираться: каскад его пропустит, латераль не назначит донором, failover не возьмёт резервом. Но ничего не переключается — те, кто уже на нём сидит, так и сидят.

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

Состояние · выведен
Аварийный вывод

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

Это реакция на аварию, и потому она одной кнопкой. Ставится действием 933, руками поле лучше не трогать — переключение не произойдёт, а состояние соврёт.

Что кнопка не делает — и правильно

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

ОбъектЧто с ним при выводе узла
Правила пополнения потребителей
60 штук на стенде
переключены  На резервный узел, прежний маршрут записан в память.
Собственные правила узла
20 штук
приглушены  Переведены в ручной режим и помечены. Мёртвый склад не заказывает себе товар.
Физический остаток на складене тронут  Как лежало 52 штуки UNV-01, так и лежит. Вывоз — отдельная работа людей, а не кнопки.
Резервы под созданные заявкине сняты  Из тех же 52 штук 25 остаются зарезервированными. Свободно по-прежнему 27.
Открытые документы
13 незакрытых на WH-A сейчас
не тронуты  Доедут по-старому. Отменять или переадресовывать их — решение человека.
Локации, типы операций, нумерацияживы  Склад продолжает существовать во всех смыслах, кроме одного: он больше не выбирается источником.

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

И сроки поедут. На стенде плечо WH-A → S1 состоит из двух ног: «склад → транзит» с задержкой 0 и «транзит → магазин» с задержкой 1 день. У резервного узла плечо своё. Помните ловушку: lead_days правила показывает только последнюю ногу, а реальная длина цепочки — сумма обеих.

Если узел закрывается навсегда

Это другая задача, и решение её сознательно не делает. Аварийный вывод обратим по замыслу; окончательное закрытие — необратимо, и автоматизировать необратимое одной кнопкой не стоит. Порядок такой:

01
Перевести узел в «Ограничен»Вручную, полем на форме. С этого момента новых назначений на него не будет, а начатое доедет.
02
Довести до нуля открытые документыЗакрыть или отменить все незакрытые приходы и отгрузки. Пока они висят, склад держит резервы.
03
Физически вывезти остатокВнутренними перемещениями на другой узел, до нуля и по свободному, и по зарезервированному. Это работа людей на местах, не системы.
04
Снять галочки «Resupply From»У всех, кто с него кормился, — иначе маршруты останутся и будут предлагаться в выпадающих списках ещё годы.
05
Только теперь архивироватьOdoo пропуститОстаток нулевой — штатная блокировка снята. Историю это не стирает: документы и остатки прошлых периодов остаются на месте, склад просто исчезает из выбора.

Честно: шаги 02–04 в решении не автоматизированы никак. Это ручная процедура, и её стоит записать в регламент отдельно — она выполняется раз в несколько лет, и к моменту, когда понадобится, её никто не вспомнит.

04 / МОДЕЛЬ ДАННЫХ

Шесть полей, на которых всё держится

Ни одно из них не считается автоматически — все заполняет человек или действие. Это осознанно: сеть должна быть описана явно, а не выведена из догадок.

На складе — кто ты в сети

ПолеПодпись на формеЧто означает и почему без него нельзя
x_node_state Состояние узла Доступен / Ограничен / Выведен. Замена архивированию, которое заблокировано при остатке. Среднее значение не декорация: «ограниченный» узел не выбирается ни донором, ни резервом, но уже назначенные на него правила с места не срываются — это режим планового вывода, когда склад доезжает начатое.
x_failover_to Резервный узел Кто подхватит нагрузку. Заполнен только у хабов, замкнуто: WH-A ↔ WH-B. У магазинов пуст, и это правильно — магазин никто не подменяет, у него меняется поставщик.
x_source_priority Приоритет источников Очередь доноров через запятую, например WHA,WHB. Обычный текст, а не связь — сознательное упрощение: порядок важнее ссылочной целостности, а редактировать строку кладовщик может без обучения. У киевских магазинов WHA,WHB, у региональных WHB,WHA.

На правиле — что с тобой сделали

ПолеГде видноЗачем
x_route_before_failover Колонка в списке
скрыта по умолчанию
Маршрут, который стоял до вывода узла. Возврат идёт строго по этой записи, а не по догадке «наверное, был центральный». Без неё round-trip невозможен в принципе.
x_route_before_lateral Колонка в списке То же для латеральной переброски — отдельным полем. Латераль может висеть одновременно с failover, и одно не должно затирать память другого.
x_muted_by_failover Колонка в списке Метка «правило переведено в ручной режим потому, что его узел выведен». Без неё возврат не отличил бы приглушённые правила от тех, что оператор сам поставил на ручной — и вернул бы в авто лишнее.

Почему память — два поля, а не одно. Это тот случай, когда экономия на поле стоит корректности. Представьте: магазин S1 переброшен на донора S6 (латераль), и в этот момент выводится хаб WH-A. Если память одна, второе переключение затрёт первое, и после возврата узла правило останется на S6 навсегда — тихо, без единой ошибки в журнале.

05 / АЛГОРИТМЫ

Что делает каждое действие

Ниже — не пересказ, а фактическая логика того, что сейчас стоит на стенде. Если что-то в прогоне повело себя иначе, чем здесь написано, расхождение стоит считать дефектом документа, а не вашей ошибкой.

Каскад источников · действие 935

Запускается на выделенных правилах пополнения из шестерёнки. Для каждого правила — по шагам, до первого совпадения:

01
Есть ли дефицитнет → пропускБерётся qty_to_order. Ноль или меньше — правилу ничего не нужно, трогать нечего.
02
Межузловое ли правилонет → пропускПроверяется route_id.supplier_wh_id. Если у маршрута нет склада-поставщика — это закупка у внешнего вендора (Buy), и подменять её межскладским маршрутом нельзя.
03
Читается очередь источниковСтрока x_source_priority склада-получателя разбирается по запятой. Дальше — цикл по кодам в заданном порядке.
04
Узел существует и в строюнет → следующийНет склада с таким кодом — записывается «нет такого узла». Состояние не «доступен» — записывается «не в строю» с причиной.
05
Свободного остатка хватаетнет → следующийЧитается free_qty в контексте этого склада. Свободный, а не физический: зарезервированное под другие заявки не считается. Меньше потребности — узел отсеивается с числами в отчёте.
06
Маршрут между узлами естьнет → следующийИщется маршрут с нужной парой «поставщик → получатель». На полной сетке он есть всегда, но проверка нужна для неполных топологий.
07
Маршрут записывается, цикл обрываетсяготовоВ правило пишется найденный маршрут, в отчёт — строка вида S1 / UNV-01: WH-A → WHB (нужно 29, свободно 30). Первый подошедший побеждает — дальше очередь не просматривается.

Ключевая проверка — шаг 02. Её не было в первой версии, и ночной прогон это немедленно наказал: действие увидело у хабов заполненную очередь источников (она стоит там ради failover) и бодро подменило их закупочный маршрут Buy на межскладской. Хабы перестали закупать у поставщиков и начали заказывать товар друг у друга по кругу.

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

Латеральная переброска · действия 936 / 937

Ищет донора не среди хабов, а среди равных — соседних магазинов. Вся суть в одной формуле:

# сколько у соседа реально лишнего
излишек = free_qty(склад-кандидат) − product_max_qty(его собственное правило)

То есть «лишнее» — это не весь остаток, а только то, что выше его собственного максимума. Магазин, который сам стоит на грани, донором не станет, даже если у него на полке много.

01
Хабы исключаютсявсегдаЛюбой склад, чей код есть в x_source_priority получателя, из кандидатов выбрасывается. Иначе латераль выродилась бы в обычное пополнение с хаба.
02
Не в строю — мимопропускКандидат со состоянием «ограничен» или «выведен» донором не назначается.
03
Считается излишек каждогоПо формуле выше. Все кандидаты с числами попадают в отчёт — включая отсеянных, чтобы было видно, почему выбран именно этот.
04
Побеждает наибольший излишекодин донорПрежний маршрут уходит в x_route_before_lateral, правило переводится на донора. Действие 937 разворачивает это обратно.

Failover узла · действия 933 / 934

Самое крупное по эффекту: одна кнопка на форме склада вместо семидесяти с лишним ручных правок.

WH-A WH-B S1 S6 60 ПРАВИЛ СМОТРЯТ НА WH-A ДО
WH-A WH-B S1 S6 ВЫВЕДЕН ИЗ СЕТИ 20 ПРАВИЛ ПРИГЛУШЕНО 60 ИЗ 60 ПЕРЕКЛЮЧЕНЫ · МАРШРУТЫ ЗАПОМНЕНЫ ПОСЛЕ
01
Два отказа до начала работыстрогоНет резервного узла — действие отказывается. Резервный сам не «доступен» — тоже отказывается. Полумер нет: переводить нагрузку на второй лежащий склад хуже, чем не переводить вовсе.
02
Собираются все, кто кормился от узлаИщутся правила, у которых route_id.supplier_wh_id — выводимый склад. На стенде это 60 правил шести магазинов.
03
Каждому — маршрут на резерв, прежний в памятьобратимоЕсли маршрута «резерв → этот магазин» не существует, правило не трогается, а склад попадает в список «нет маршрута для…» в отчёте. Тихо пропустить нельзя.
04
Собственные правила узла глушатсяВсе правила самого выводимого склада переводятся с «авто» на «вручную» и помечаются x_muted_by_failover. Смысл житейский: мёртвый склад не должен заказывать себе товар.
05
Состояние узла → «выведен»Теперь его не выберет ни каскад, ни латераль: обе проверки смотрят на это поле.
06
Возврат — зеркально и только по памятидействие 934Действие 934 берёт правила, у которых заполнен x_route_before_failover и запомненный маршрут вёл именно на этот узел. Возвращает маршрут, чистит память, снимает приглушение, ставит состояние «доступен». Ни одной догадки.

Проверено на стенде: 60 из 60 правил переключены и 60 из 60 возвращены, распределение маршрутов после round-trip совпало с исходным до единицы — по 20 на маршрутах 33/40/47, 39 на Buy, всего 160. Обе колонки памяти после возврата пусты у всех 160 строк: ни одного осиротевшего значения.

Ночной прогон · крон 70

Каскад сам по себе — ручная кнопка. Чтобы он работал без оператора, на него посажен ir.cron: ежедневно в 02:00 собирает правила с ненулевым дефицитом и прогоняет по ним каскад до того, как отработает штатный планировщик. Если дефицитов нет — пишет строку и ничего не трогает.

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

06 / ОГРАНИЧЕНИЯ

Где у этого решения потолок

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

ОграничениеЧто это значит на практикеЧем закрывается
Один донор на правило Действие выбирает единственный узел. Если нужно 30, а у одного соседа лишних 20 и у другого 15 — оно не сложит 20 + 10, а возьмёт того, у кого больше. Хуже того: выбранный донор может покрыть потребность не полностью — условие «излишек больше нуля» выполнено, а «излишка хватает» не проверяется. модуль
Каскад не резервирует Самое коварное. Действие смотрит свободный остаток, но ничего не занимает. Если прогнать каскад по десяти правилам подряд, а у WH-A свободно ровно на одно, — все десять честно увидят «хватает» и все десять встанут на WH-A. Дефицит вскроется только когда планировщик создаст перемещения. модуль
Раз в сутки, а не по событию Пересчёт источника происходит ночью по крону или когда оператор нажал кнопку. Продали последнюю штуку в 10:15 — источник переедет в 02:00 следующих суток. Реакции на событие нет. модуль
Расстояние не учитывается Очередь источников — статичная строка, которую задал человек. Ни километров, ни стоимости доставки, ни фактического времени плеча в выборе не участвует. Донор с излишком 45 за 500 км победит донора с излишком 40 за углом. модуль
Документы не переезжают Переключается правило, то есть будущие заявки. Уже созданные заказы и перемещения остаются на прежнем узле. Смена склада на подтверждённом заказе проходит, но отгрузка остаётся на старом складе — проверено. модуль
История не хранится Результат приходит всплывающим окном и живёт до первого клика. Постоянный след остаётся только в техническом журнале, который не привязан ни к складу, ни к правилу и не виден рядовому пользователю. Ответить «кто и когда вывел узел» через полгода нечем. модуль
Заявки дробятся на документы Одна потребность правила распадается на несколько документов. Причина структурная: модели procurement.group в saas~19.4 нет вообще, поле group_id убрано и с перемещения, и с документа. Прежним способом слить нечем — на эту роль остаются батчи, но это склейка на исполнении, а не единая заявка. батчи

Про количества — отдельно. Дробление документов легко спутать с переордером, но это разные вещи. Проверено: четыре документа по одному правилу созданы в одну секунду, одним источником, с одним сроком; арифметика сходится точно. Дефект в форме, не в числах.

07 / ГРАНИЦА

Что закрыто, что осталось

Девять процессов, по которым шёл разбор. Вердикт у каждого — не мнение, а результат прогона на стенде.

ПроцессСостояниеВердикт
1Сеть узлов и маршрутыПолная сетка 8×8 через транзитные локации собирается штатно. Бонусом Odoo сама сливает отгрузки на разные магазины в один документ.штатно
2Состояние узлаПонятия в модели не было, архивирование с остатком заблокировано. Закрыто полями на форме склада — отключение узла больше не требует архивирования.настройка
3Пополнение по мин/макс160 правил работают. Требуется ручной перевод в авто и осмысленный горизонт.вручную
4Переключение источника у точкиМассовая смена маршрута в списке правил работает — 7 действий на точку.вручную
5Каскад источниковПравило по-прежнему знает один источник; каскад надстроен действием и ночным кроном. При нужде 29 пропустил WH-A с 27 и выбрал WH-B с 30.настройка
6Исполнение переброскиТранзит, приёмка, батчи — штатно, оценки трудоёмкости реалистичны.вручную
7Выбор латерального донораМаршрут между магазинами настраивается штатно; выбор донора закрыт действием. Нашёл S6 с излишком 45, отсеяв пять кандидатов с нулём.настройка
8Переадресация документовСмена склада на подтверждённом заказе проходит, но отгрузка и приёмка остаются на старом узле. Правится только кодом в stock.модуль
9Failover узлаСтал кнопкой. 60 правил шести магазинов переключены на резерв и возвращены в исходное состояние; 20 собственных правил узла приглушены.настройка

Четыре процесса из девяти перешли из «нужен модуль» в «хватило настройки»

Это главный вывод разбора. Не «Odoo всё умеет» и не «без разработки никак», а вполне конкретная граница: решения о маршруте выражаются полями и действиями, а работа с уже созданными документами, событийность и история — нет.
08 / КАК ПРОВЕРИТЬ

Не верить на слово

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

Документ 01
Прогонный лист

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

Открыть прогонный лист →

Документ 02
Отчёт по стенду

Ревизия базы, топология, история спроса, матрица покрытия, перечень созданных объектов и список того, что осталось за границей настройки.

Открыть отчёт →

Один нюанс перед прогоном. Сценарии подобраны под точные числа на стенде: например, каскад имеет смысл проверять при потребности ровно 29, потому что у WH-A свободно 27, а у WH-B — 30. Если остатки на базе уже кто-то двигал, это окно схлопнется, и сценарий покажет не то. В прогонном листе есть раздел с исходными числами — сверьтесь с ним до начала.