Задача звучала безобидно: загрузить фотографию шильдика на оборудовании — и получить ответ, за кем числится эта единица техники. На балансе организации — 4792 актива в главной выгрузке из учётной системы и шесть инвентарных книг по материально-ответственным лицам в формате.xls, экспортированных из 1С. Разработчик собрал сервис за один вечер, но большую часть времени занял не код, а данные.
Для распознавания текста на шильдиках выбрали Claude Opus 4.6 через OpenRouter — vision-API модели позволяет передать изображение и получить структурированный ответ с инвентарным или серийным номером. Бэкенд написан на FastAPI с SQLAlchemy 2 и SQLite: для задачи масштаба нескольких тысяч активов в одной организации полноценная СУБД и оркестрация контейнеров избыточны. Фронтенд — Jinja2 с ванильным CSS, без React и Vue. Результаты инвентаризации автоматически пушатся в Google Sheets через gspread и Google Service Account.
| Компонент | Технология | Назначение |
|---|---|---|
| Backend | Python 3.12, FastAPI, SQLAlchemy 2 | API и бизнес-логика |
| База данных | SQLite | Хранение активов и результатов сверки |
| ETL | pandas + openpyxl, xlrd 1.2 | Импорт .xlsx и .xls файлов |
| ИИ-распознавание | Claude Opus 4.6 через OpenRouter | Чтение шильдиков по фото |
| Интеграция | gspread + Google Service Account | Выгрузка в Google Sheets |
| Frontend | Jinja2 + ванильный CSS | Административный интерфейс |
| Деплой | Docker Compose, один контейнер | Запуск сервиса |
Первая техническая проблема возникла ещё на этапе чтения файлов. Современный формат.xlsx читается pandas и openpyxl без затруднений, но шесть книг МОЛов — старый бинарный.xls из 1С. Здесь разработчик столкнулся с классическим deadlock: pandas 2.x требует xlrd версии не ниже 2.0, но xlrd 2.0 убрал поддержку формата.xls, оставив только.xlsx. Установить старый xlrd 1.2 при новом pandas не получается из-за проверки версий. Решение — обойти pandas и читать.xls напрямую через xlrd 1.2, вручную пропуская служебные строки-разделители (строки вида «Счёт 0901…», «МОЛ …-ос», «Итого по МОЛу»), которые бухгалтерский экспорт вставляет между записями.
Главная техническая боль — несовместимость xlrd 2.0 и pandas 2.x: xlrd 2.0 убрал поддержку старого.xls, поэтому пришлось читать файлы напрямую через xlrd 1.2.
Вторая и самая нетривиальная проблема — нормализация номеров. В реальных данных один и тот же инвентарный номер встречается в десятках вариантов написания: с двойным дефисом, без дефиса вообще, с точкой в конце, с ведущими нулями или без них. Серийные номера дополнительно склеены через слеш с инвентарными кодами, содержат мусорные плейсхолдеры или хвосты через пробел. Разработчик написал функцию нормализации с четырьмя попытками сопоставления: точный матч по нормализованному инвентарному номеру, по серийному, затем по цифровому fallback для обоих. Цифровой fallback — выделение только цифр из строки — позволил склеить ещё около 50 записей, которые иначе остались бы несопоставленными.
Однако универсальный подход здесь оказался ловушкой. Серийники Z78VBJACB002YJA и Z78VBZJACB002TLX — разные устройства — при извлечении только цифр оба превращаются в «78002» и система считает их одним активом. Для инвентарных номеров формата XXXNNN-NNNNNN цифровой fallback оправдан: он помогает сшить разные форматы записи одного номера. Для серийников он категорически вреден, потому что буквенная часть несёт смысловую нагрузку. Пришлось разделить логику на две отдельные функции — inv_match с digit-fallback и serial_match без него.
Третья проблема — архитектурная. Изначально предполагалось, что книги МОЛов могут содержать записи, которых нет в главной выгрузке, и их нужно добавлять в базу как новые активы. После первого прогона: 4792 актива из выгрузки, 432 строки из книг, 305 сопоставлено, 113 создано новыми. Заказчик объяснил: главная выгрузка — единственный источник истины. Если запись есть в книге МОЛа, но отсутствует в выгрузке — это ошибка в книге, а не новый актив. Логику переписали: книги МОЛов только обогащают существующие записи (проставляют МОЛ, филиал, заполняют book_* поля), а несопоставленные строки уходят в таблицу reconciliation_issue со статусом only_in_book — для отчёта, но не в основную базу.
Отдельно в данных обнаружились задвоения: одна физическая единица техники иногда числится в учёте дважды под разными номерами, о чём бухгалтерия делает пометки в поле примечаний. Для таких случаев написан детектор по ключевым словам в тексте примечания. Итоговый сервис задеплоен в одном Docker-контейнере — без микросервисов, без Postgres, без Kubernetes. Для корпоративной инвентаризации масштаба нескольких тысяч активов в одной организации этого оказалось достаточно.