Правда про склад, которую система не говорила сама: реверс-инжиниринг схемы данных ERP→BI без документации


Построить BI-отчётность по данным ERP без документации по схеме данных — можно, если знать где смотреть и как проверять гипотезы. В этом кейсе — диагностика данных Zoho Inventory→Analytics для производственной B2B-компании: как восстановить структуру таблиц методом прямых SQL-запросов, разобраться в двух параллельных слоях учёта складских остатков и построить дашборд, который отражает физическую реальность, а не бухгалтерский статус оплаты.


Контекст: B2B производство, несколько складов, нет документации по данным

Клиент — производственная B2B-компания с продажами в несколько стран и несколькими складами. Проект ведёт команда, частью которой я являюсь; вся рабочая коммуникация с клиентом — на мне, проджект-менеджер закрывает организационную сторону: согласование встреч, фиксация результатов, синхрон по задачам. Этот блок работы — аналитика и диагностика данных: нужно было построить отчётность в BI на основе данных из ERP, для которой не существовало документации по схеме.

Исходная ситуация: две проблемы с одной первопричиной

Фактически два взаимосвязанных запроса, которые оба вытекали из одной первопричины.

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

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

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

Дополнительная сложность: никакой схемы данных, никакой документации по тому, как ERP публикует данные в BI. Задача — разобраться в структуре самостоятельно и построить отчёты, которые отвечают на правильный вопрос.

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

Как шло расследование схемы данных ERP→BI

Не угадывать структуру — спрашивать напрямую. Вместо того чтобы листать дерево таблиц в интерфейсе BI, начала с SELECT * FROM "<таблица>" LIMIT 5 прямо в конструкторе запросов. Это дало реальную схему полей быстрее и точнее, чем UI, который, как выяснилось, сам иногда обрезает список колонок.

Первый же «нет поля» оказался не ограничением платформы. Таблица строк заказов содержала подозрительно мало полей: ни складов, ни количества — ничего для JOIN. Проверила настройки источника синхронизации и обнаружила: нужные поля просто не были отмечены при первоначальной настройке. Не пошла по пути «системе это недоступно» — сначала проверила конфиг. За сессию это случилось дважды.

Три гипотезы про триггер списания остатка — прежде чем нашла настоящую. Мне нужно было понять, в какой момент система фиксирует расход товара со склада. Первая гипотеза — «по факту оплаты». Нашла один подтверждающий пример, остановилась — рано. Вторая гипотеза — «по выставлению документа». Снова один подтверждающий пример, снова остановилась — снова рано. Только когда нашла случай, где документ выставлен, а остаток не списался, стало ясно, что вторая гипотеза тоже неверна. Настоящий триггер — статус самого документа: «черновик» против «проведён». У черновика нет бухгалтерских проводок — значит и списания нет, независимо от оплаты. Нашла это, только намеренно искав противоречащий пример, а не новый подтверждающий.

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

Ключевые технические решения

1. Не патчить производное поле «остаток» — пересобрать с нуля из первичных событий. Попытки скорректировать готовое поле «текущий остаток» заходили в тупик: оно пыталось быть универсальным ответом сразу на два вопроса (себестоимость и физический склад) и не подходило идеально ни для одного. Решение — спуститься на уровень ниже и собрать нужную метрику из трёх отдельных надёжных лент: приход товара, перемещения между складами, реальная физическая отгрузка. Каждая из трёх лент на уровне физического склада оказалась корректной — путаницу вносило именно производное поле.

2. Перемещения между складами — отдельная лента, не учтённая в основных таблицах. Трансферы между складами не отражаются ни в таблице прихода, ни в таблице расхода — только в отдельной ленте. При первой попытке джойнить эту ленту без разбивки по складу результат «схлопнулся» до нуля: каждый трансфер хранится как две строки с противоположными знаками, и без фильтра по складу они взаимно обнуляются. Ошибки не было — просто неверный нулевой результат. Поймала на контрольном примере с заранее известным ненулевым ответом.

3. Честная коммуникация по открытому вопросу. В процессе выяснилось, что настройка момента указания партии товара была изменена по операционной причине и не отражена в проектной документации. Влияет ли эта настройка на расчёт себестоимости — осталось открытым вопросом. Вместо угадывания или «не знаю» — сформулировала два варианта с явными плюсами и минусами каждого, честно указала на неопределённость и отправила точный вопрос в поддержку провайдера. Клиент получил чёткую картину того, что известно точно, что предположительно и что проверяется.

Результат: дашборд продаж и отчёт по складским остаткам

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

Отчёт по фактическим остаткам на складах. В разрезе объёма (кг) и суммы, в той форме, которую отдел продаж прислал как пожелание — не общий шаблон, а именно тот формат, с которым им удобно работать.

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

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

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

Принципы диагностики данных в действии

— Не угадывать структуру — добывать напрямую. SELECT *, сырой ответ API, прямой запрос к источнику — надёжнее любой документации и UI-деревьев, которые оба могут обрезать и врать.

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

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

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

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

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

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

— Конфиг системы дрейфует по операционным причинам; документация, зафиксированная один раз, устаревает молча.

— После решения основной задачи явно спрашивать про усложнения, не добавлять по инерции.


Стек

Zoho Inventory · Zoho Books · Zoho Analytics (Query Table, SQL)


Опубликовано с разрешения компании.