Робот сверки остатков ходил по номенклатуре, сравнивал количество в учётной системе (ERP, Enterprise Resource Planning, система управления ресурсами предприятия) с количеством в складской (WMS, Warehouse Management System, система управления складом) и найденную разницу сам переносил документом на технический склад проверки. В комментарии к документу стояла строка "при нулевых остатках в WMS". Восемь месяцев её читали как показание прибора: склад отдал ноль, товар пропал, идите искать.
Подняли снимок конкретного запуска из журнала регистрации. Учётная система в момент запуска: 438. Складская: 436. Разница 2, перенесено 2. Нуля там не было никогда, и по этой позиции его не было ни разу.
Ниже разбор того, что робот меряет вместо остатков, с обеими формулами, замерами и одной гипотезой, которую пришлось снять. Фактура с одного проекта у крупной розничной сети, цифры покомпонентно проверены на сходимость.
Чья это боль
Такой робот стоит у многих, кто живёт на двух системах. Логика у всех примерно одна: регламентным заданием пробегаем по остаткам, находим расхождение, оформляем его документом, чтобы учёт не расходился с физикой. Дальше начинается быт: кладовщик получает задачу "на складе проверки две штуки", идёт в зону, а там всё на месте и никакой задачи в складской системе нет. Он приходит с вопросом "где потеря". Ему отвечают строкой из комментария: "в WMS был ноль". Он идёт искать ноль, которого не было.
Цена такой петли измеряется не этими двумя штуками. Она в том, что склад перестаёт верить документам, а разработчик получает регулярный поток обращений без единого воспроизводимого случая. Восемь месяцев разбора шли по ложному следу, хотя реальные числа лежали рядом: событие "Синхронизация остатков" в журнале регистрации пишет оба значения на момент запуска.
Вывод раздела: при жалобе на робота первым делом поднимаем его собственный протокол в журнале. Документ он породил уже потом, и как источник он вторичен.
Почему очевидное решение не работает
Очевидное решение звучит так: раз системы расходятся, чиним обмен. Ищем, где теряется движение, добавляем ретраи, сверяем очередь.
На этом проекте чинить было нечего. Обычный цикл "отбор - отгрузка - расходный ордер" оказался атомарным. Замерили лаг между физической отгрузкой в складской системе и появлением расходного ордера в учётной на пяти случаях подряд: 0, 0, 9, 13 и 1 секунда. Верхняя граница 13 секунд. Робот просыпается по расписанию регламентного задания, и его окно на порядки больше: попасть внутрь тринадцатисекундного разрыва можно, но регулярную разницу такой канал не даст.
То есть обмен работал. Расхождение бралось из другого места.
Вывод раздела: прежде чем чинить обмен, померьте его лаг на нескольких случаях руками. Если лаг измеряется секундами, а сверка запускается по расписанию с интервалом в десятки минут, обмен невиновен, и дальше надо смотреть на саму сверку.
Первое: комментарий был константой
Строка "при нулевых остатках в WMS" стоит в коде процедуры, которая формирует перемещение. Она пишется на каждый документ независимо от того, какие числа робот увидел. Когда-то она описывала одну ветку логики, ветка изменилась, строка осталась.
Дальше сработала обычная человеческая механика: текст в документе выглядит как факт, потому что его написала программа. Программа же не врёт. Врёт наше предположение, что она формулирует по данным.
Проверить это стоит десять минут: найти строку поиском по конфигурации и посмотреть, собирается она из переменных или лежит константой. Если константа - все выводы, которые на ней стояли, обнуляются.
Вывод раздела: любой человекочитаемый текст, порождённый роботом, до первой проверки по коду считается декорацией. Показанием он становится только тогда, когда вы видели, из каких переменных он собран.
Второе: обе цифры робота - синтетика
Это главное. Робот сравнивал два числа, из которых ни одно не равно остатку своего регистра в моменте.
Складская сторона считалась так:
ОстатокWMS = россыпь(зона_1) + россыпь(зона_2) + содержимое_коробов
- позиции из списка исключений
Список исключений выкидывал позицию целиком, если по ней:
- была отгрузка в окне 60 минут (для закрытых заказов) или 12 часов (для открытых);
- была приёмка младше 12 часов;
- была корректировка младше 60 минут.
Учётная сторона считалась так:
ОстатокERP = остаток регистра на конец дня
- непроведённые расходные ордера младше 3 часов
Почему формулы такие. Каждое исключение когда-то ставили под конкретную ложную тревогу: приходила жалоба "робот увёл товар, который в этот момент отбирали", и в код добавлялось окно. Логика понятная, вот только каждое окно делает величину чуть менее похожей на остаток и чуть более похожей на "остаток за вычетом того, что нас недавно ругали".
Два следствия, которые видны прямо из формул.
Первое: величины разные по природе. Слева физический срез с вычетами по активности, справа учётный остаток на конец дня с вычетом трёхчасового хвоста. Их разность не имеет физического смысла и меняется от того, в какую минуту робот проснулся.
Второе: окна разной длины (60 минут против 12 часов в одной формуле) делают невозможным разбор инцидента по времени документа. Позиция не паркуется в момент причины. Она паркуется в первый тихий момент после волны активности, когда все окна исключений успели закрыться. Между событием и документом может лежать полсуток и любое количество чужих движений. Разбирать такой документ по времени бесполезно, и склад, который честно смотрит "что происходило в 14:20", не найдёт ничего.
Вывод раздела: прежде чем сравнивать два числа, напишите обе формулы рядом на бумаге. Если в них разные окна и разные вычеты, вы сравниваете не остатки, а два разных прибора.
Третье: порог стоял в нуле
При таких формулах разница между ними колеблется сама по себе. На быстрой позиции измеренный дрейф составил 2 единицы при остатке 438, это 0,46%. По второй позиции того же снимка разница была 4 (86 против 82) и разошлась двумя перемещениями, 1 и 3.
Порог срабатывания у робота был нулевой. Любая разница больше нуля превращалась в документ.
Отсюда тезис, который я готов защищать: автоматическая сверка двух учётных систем в первую очередь меряет собственную методику измерения, и её порог обязан быть больше шума этой методики. Если порог ноль, а формулы разные, вы построили генератор задач для склада. Он будет работать вечно, потому что источник у него бесконечный: сама разница определений.
Масштаб генерации на разобранном складе проверки за 60 дней:
| Показатель | Значение |
|---|---|
| Строк движений | 9 372 |
| Документов-регистраторов | 2 061 |
| Артикулов | 2 102 |
| Приход | 15 879 |
| Расход | 15 490 |
| Нетто | +389 |
| Оборот (приход + расход) | 31 369 |
Из этого оборота 19 660 единиц, то есть 62,7%, создаёт напрямую сам робот сверки. И 2 081 артикул из 2 102 имеют и приход, и расход по этому складу: товар уезжает на проверку и возвращается обратно. Второй робот, пополнение канала, тянет позиции назад, и на горячих днях счёт взаимных перекидок идёт на десятки.
Нетто +389 единиц за два месяца при обороте 31 369 - это отношение примерно 1 к 80. Восемьдесят движений ради одного результата.
Вывод раздела: посчитайте по своему техническому складу долю движений, которую создаёт сам робот. Если она в районе половины оборота, склад работает на сверку, а не сверка на склад.
Гипотеза, которую пришлось снять
Первая версия разбора звучала красиво: "фазовый рассинхрон конвейера отгрузки". То есть отбор, отгрузка и оформление расходного ордера разъезжаются во времени, и робот ловит систему в середине цикла.
Замер лага её убил. 0, 0, 9, 13, 1 секунда - конвейер атомарен, разъезжаться там нечему. Версию сняли.
Осталось объяснить дрейф по дням, а он был. Нетто по дням за пять дней подряд:
| День | Учётная система | Складская система |
|---|---|---|
| 1 | -31 | -31 |
| 2 | +50 | +59 |
| 3 | -13 | -25 |
| 4 | +60 | +60 |
| 5 | -10 | -10 |
| Сумма | +56 | +53 |
Первый, четвёртый и пятый день совпадают точно. Расходятся второй и третий, и суммарно за пять дней набегает 3 единицы.
Канал нашёлся один: продажи через маркетплейсы. Там продажа проводится в учётной системе на сутки раньше, чем товар физически уезжает со склада. В сутках расхождения по дням и живут, а совпадение по дням без такой активности это подтверждает.
Дальше самое неприятное для авторов исходного разбора. Робот вычитает непроведённые расходные ордера младше 3 часов, то есть компенсирует ровно тот канал, который и без него атомарен (0-13 секунд), и никак не компенсирует канал с реальным суточным сдвигом. Заплатка стоит мимо источника. Работает она при этом честно: три часа действительно гасят часть колебаний, только по совпадению.
Вывод раздела: если поправка стоит в коде давно и никто не помнит, под что её ставили, померьте канал, который она чинит. Есть шанс, что чинить там нечего.
Про арифметику, которую надо считать руками
Накопленные за пять дней +3 и припаркованные роботом +2 связаны базовым сдвигом в одну единицу. В рабочей записи разбора эта связка была записана как "+3 минус (-1) = +2", и она не сходится: три минус минус один это четыре. Числа по краям подтверждены независимо, оба верны, неверна была только запись перехода между ними: базовый сдвиг вычитается со знаком плюс, 3 - 1 = 2.
Я оставляю это в статье намеренно. Разбор жил с этой строкой некоторое время, её читали и кивали, потому что цифры по краям правильные и общая логика правдоподобна. Поймала её арифметика, а не логика: сел и посчитал руками. Любое место в разборе, где стоит "сходится" без выписанного действия, надо считать непроверенным.
Честная граница: мгновенный срез задним числом здесь не воспроизводится
Тут начинается часть, которую обычно не пишут.
Фактический срез регистра по всем складам на момент запуска робота даёт 442 (165 + 3 + 10 + 110 + 154). Робот в ту же секунду записал 438. Разрыв 4 единицы. Он больше, чем найденная роботом разница, ради которой всё затевалось.
Документами этот разрыв не объясняется. Все 64 регистратора за период после среза не тронуты, вечерние ордера созданы позже. Формулировка "расхождение методики, в пределах нормы" здесь была бы рационализацией, поэтому скажу прямо: регистр в моменте отличался от сегодняшней ретроспективы способом, который по документам не восстанавливается.
Причины, по которым в такой базе это в принципе нормально:
- за один вечер прошло 670 допроведений документов прошлыми датами;
- правка наборов записей и изменение количеств в табличных частях при перепроведении не журналируются вовсе;
- регистр остатков складской системы журналируется не полностью: зона отбора опустошается молча, наблюдали исчезновение ячейки на 24 штуки без единого события в журнале;
- replay журнала против живого состояния дал 240 против 214, расхождение 26 единиц;
- номера документов переиспользуются примерно раз в год, поиск по номеру без фильтра по дате находит четыре разных документа.
Отсюда практическое следствие для всех, кто расследует такие инциденты. Вопрос "сколько было на самом деле в 14:07" в базе с активным допроведением задним числом корректного ответа не имеет. Ответ имеет только вопрос "сколько показал прибор в 14:07", и потому снимок робота в журнале ценнее любой сегодняшней ретроспективы.
Вывод раздела: проверьте, пишет ли ваш робот сверки свои входные значения в журнал регистрации. Если нет, добавьте это раньше, чем всё остальное: без снимка расследовать нечего.
Спящий дефект: процедура мутирует список, по которому же и считает
Отдельная находка, которая на разобранном кейсе не выстрелила. Процедура создания перемещений добавляет технический склад проверки в тот же массив складов, по которому считается учётная сторона. При повторном вызове на одном и том же объекте учётная величина считалась бы уже вместе со складом проверки, то есть с собственным результатом предыдущего прогона.
Не выстрелило только потому, что объект пересоздаётся на каждый запуск. Это не защита, это везение конкретной реализации: любой рефакторинг с кэшированием объекта включает дефект.
Проверяется механически. Ищем в процедуре расчёта все места, где в коллекцию параметров что-то добавляется, и смотрим, приходит эта коллекция снаружи или собирается локально. Приходит снаружи и модифицируется - дефект, независимо от того, срабатывает он сегодня.
Вывод раздела: список складов, по которому считается сверка, должен собираться заново внутри расчёта. Общий на весь цикл массив рано или поздно получит в себя результат самого расчёта.
Что с этим делать
Порядок такой.
Убрать из документа константу. Комментарий обязан собираться из тех же переменных, которые робот сравнивал: два числа и разница. Это единственный пункт, который стоит полчаса и снимает большую часть непонимания со складом.
Записывать снимок. Оба входных значения на момент запуска в журнал регистрации, с идентификатором позиции. Без этого расследование через неделю невозможно.
Поднять порог выше шума методики. Как его выбрать: взять историю сверок за месяц, построить распределение модуля разницы по позициям и посмотреть, где начинается хвост. На разобранном складе дрейф на быстрых позициях был около 0,5%, а типовая срабатывавшая разница составляла 1-2 единицы при остатках в сотни.
Или, вместо порога, требовать повтора. Позиция паркуется, если разница держится два прогона подряд. Шум методики между прогонами меняется, реальная недостача остаётся.
Здесь же граница честности. Порог мы не подобрали на исторических данных. Рекомендация "от трёх единиц или два прогона подряд" выглядит разумной, но сколько реальных расхождений она пропустит, на этих данных не посчитано. Пока это гипотеза, требующая прогона по истории. Копировать её в свою конфигурацию как готовую цифру рано. Кто такой прогон сделает, тот и закроет вопрос по-настоящему.
Отдельно: категория "перемещения с другим комментарием" - 942 документа с нетто +1 584 единицы - осталась неразобранной по шаблонам. Это почти треть оборота технического склада, и что там внутри, честно говоря, неизвестно.
Открытый вопрос
Прямой ответ на исходный вопрос склада "где потеря": потери не было ни одной. Был прибор, который меряет сам себя, и строка-константа, которая восемь месяцев отправляла людей искать несуществующий ноль.
Мне интересно, насколько это общий случай. С каким порогом стоит сверка у вас, и мерил ли кто-нибудь распределение модуля разницы за месяц до того, как поставить этот порог? Если у вас порог ноль и склад при этом не жалуется, это тоже ответ, и мне он был бы полезен: значит формулы у вас сопоставимы, и интересно, как вы этого добились.
Снимок конкретного запуска, с которого начался весь разбор, лежит в журнале регистрации, и достать его руками из боевой базы за восемь месяцев тяжело. У нас это делает Журнал регистрации, свёрнутый для нейросети: события сворачиваются до строк с параметрами, и видно оба числа на момент прогона, а не пересказ в комментарии документа.
Другие наши инструменты диагностики 1С:
- Журнал регистрации для нейросети - поднять историю запусков регламентного задания и посмотреть, какие числа стояли в момент каждого прогона.
- Разбор технологического журнала 1С - когда надо понять, что делал сервер в минуту прогона, а журнал регистрации про это уже молчит.
- Анализ кода внешних обработок - прочитать сам регламентник чужими глазами: где считается разница и откуда берётся строка комментария, которую потом читают как показание прибора.
Вступайте в нашу телеграмм-группу Инфостарт