Обычно, когда говорят про автоматизацию закупок, вспоминают ERP, тендерные площадки, ЭДО, 1С, согласования, заявки, договоры и большие корпоративные системы.

Но между “поставщик прислал коммерческое предложение” и “закупку можно согласовывать” часто есть очень приземлённый этап, который до сих пор живёт в Excel.

Закупщик получает несколько КП от поставщиков. Один файл в Excel, второй в PDF, третий в Word. У одного поставщика цена с НДС, у другого без НДС. Где-то рубли, где-то доллары, евро или юани. В одной строке партномер указан полностью, в другой — описание чуть отличается. По части позиций есть целевые цены, по части надо ориентироваться на прошлые закупки.

Дальше всё это вручную сводится в одну таблицу.

Идея нашего приложения родилась именно из этой рутины: сделать локальный desktop-инструмент, который помогает загрузить несколько коммерческих предложений, сопоставить позиции и цены, подсветить спорные места и выгрузить итоговый Excel-отчёт.

В этой статье расскажу не столько про саму закупочную предметную область, сколько про техническую сторону: почему выбрали desktop, как собрали Tauri + React + FastAPI, как упаковали Python backend в sidecar, с какими проблемами столкнулись при сборках под Windows/macOS и что оказалось важным в B2B-приложении, которое должно работать не “в демо на ноутбуке разработчика”, а у пользователя.

Почему не облако

Первый соблазн — сделать обычное веб-приложение: frontend, backend, база, авторизация, загрузка документов, обработка на сервере.

Технически это проще поддерживать: один backend, один деплой, понятная аналитика, обновления без установщиков.

Но для нашей задачи cloud-first подход оказался не самым удачным.

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

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

Поэтому мы выбрали локальную архитектуру:

  • приложение устанавливается на компьютер пользователя;

  • документы обрабатываются локально;

  • данные проекта хранятся локально;

  • интернет не является обязательным условием для базовой работы;

  • результат можно выгрузить в Excel и дальше использовать в привычном процессе согласования.

Да, это усложняет сборку, обновления, диагностику и поддержку. Но для B2B-сценария с документами это оказалось важнее.

Общая архитектура

Высокоуровнево приложение состоит из четырёх частей:

  • desktop-оболочка на Tauri;

  • frontend на React;

  • backend на FastAPI;

  • локальное хранилище: SQLite и файловые каталоги приложения.

Frontend отвечает за интерфейс: проекты, загрузку файлов, настройки НДС/валют, таблицу результатов, предупреждения, экспорт.

Backend отвечает за обработку документов, нормализацию данных, сопоставление позиций, расчёты, формирование Excel-отчёта и лицензионную логику.

Tauri в этой схеме играет роль desktop-оболочки и менеджера жизненного цикла: запустить приложение, поднять локальный backend, проверить его доступность, открыть frontend, корректно завершить процессы при выходе.

В dev-режиме всё просто: backend запускается как обычный Python/FastAPI, frontend — через Vite, Tauri смотрит на dev server.

В production всё интереснее: пользователь не должен устанавливать Python, зависимости, uvicorn и прочее. Поэтому backend упаковывается в отдельный бинарник через PyInstaller и кладётся рядом с desktop-приложением как sidecar.

Почему FastAPI внутри desktop — не такая уж странная идея

На первый взгляд может показаться странным: зачем в desktop-приложении HTTP API?

Но у такого подхода есть плюсы.

Во-первых, frontend и backend остаются слабо связаны. UI общается с backend через понятные API-методы: создать проект, загрузить файл, запустить обработку, получить результаты, скачать export.xlsx.

Во-вторых, backend можно тестировать отдельно от UI. Это особенно удобно для логики, где много предметных правил: НДС, валюты, совпадения позиций, экспорт, лицензирование.

В-третьих, часть логики проще писать на Python. Обработка табличных документов, Excel-экспорт, парсинг, нормализация строк, работа с файлами — это та область, где Python чувствует себя уверенно.

Минус очевиден: в production нужно аккуратно упаковать backend, запустить его как дочерний процесс, дождаться healthcheck, не оставить fallback на системный Python, правильно завершать процесс и не сломать всё на Windows.

Жизненный цикл backend

В dev-режиме можно позволить себе запускать Python backend из окружения разработчика.

В release так делать нельзя. Пользователь не должен зависеть от установленного Python, PATH, виртуального окружения и версий библиотек. Кроме того, для лицензирования и ограничений демо-режима важно понимать, что приложение работает в упакованном сценарии.

Поэтому в production используется только PyInstaller sidecar.

Упрощённо логика такая:

  1. Tauri стартует.

  2. Проверяет, нет ли уже живого backend на нужном локальном порту.

  3. Если backend не найден — запускает sidecar.

  4. Дожидается /health.

  5. Проверяет, что ответил именно нужный backend приложения.

  6. После этого frontend начинает работать с API.

  7. При закрытии приложения дочерний процесс завершается.

Отдельно пришлось следить за тем, чтобы в release-сборке не оставался fallback на системный Python. В dev это удобно, в production — источник странных багов и обходных путей. Для этого мы вынесли debug-only функции под условную компиляцию и добавили тест, который проверяет политику запуска backend.

Почему важен healthcheck

Когда desktop-приложение запускает локальный backend, нельзя просто “поднять процесс и надеяться”.

Пользователь может быстро кликнуть по кнопке, backend может стартовать чуть дольше, порт может быть занят, процесс может упасть, а в худшем случае на том же порту может жить что-то другое.

Поэтому healthcheck стал обязательной частью старта.

Минимального “порт открыт” недостаточно. Нужно проверить не только доступность, но и идентичность сервиса: приложение должно понимать, что на локальном порту отвечает именно его backend, а не случайный процесс.

Для пользователя это превращается в простую вещь: если backend не готов, UI должен показать понятное сообщение, а не молча падать или зависать.

Работа с документами: самая грязная часть

Самая неприятная часть в подобных приложениях — не UI и не упаковка. Самая неприятная часть — реальные файлы.

В идеальном мире поставщики присылают одинаковый шаблон Excel, где есть колонки:

  • партномер;

  • описание;

  • производитель;

  • количество;

  • цена;

  • валюта;

  • НДС;

  • срок поставки;

  • комментарий.

В реальном мире каждый поставщик делает как привык.

Один пишет “Part Number”, другой “Артикул”, третий “Код”, четвёртый вообще ставит описание в первую колонку. Где-то есть объединённые ячейки, где-то скрытые строки, где-то повторный заголовок внутри таблицы, где-то внизу итоги и примечания.

Поэтому задача быстро перестаёт быть “прочитать Excel”. Она становится задачей распознавания структуры таблицы и аккуратной нормализации.

Мы пошли по прагматичному пути:

  • определяем заголовки;

  • пытаемся сопоставить колонки с ожидаемыми сущностями;

  • нормализуем партномера;

  • парсим числа, валюты, НДС;

  • собираем предупреждения;

  • не делаем вид, что всё распознано идеально.

Последний пункт важнее, чем кажется. В B2B-документах опасно уверенно ошибаться. Лучше показать пользователю “эта строка требует проверки”, чем молча выбрать поставщика на основании сомнительного совпадения.

Сопоставление позиций

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

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

Мы используем нормализацию и fuzzy matching, но с ограничениями.

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

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

НДС: маленькая настройка, большой источник ошибок

НДС оказался одной из самых важных функций.

Вручную сравнивать цены поставщиков опасно, если один прислал цену с НДС, второй без НДС, а третий указал условие текстом внизу КП.

Если просто выбрать минимальную цену “как есть”, можно получить неверный результат. Поставщик без НДС будет выглядеть дешевле, хотя после приведения цены к единому виду картина изменится.

Поэтому мы добавили несколько режимов:

  • оставить цены согласно КП;

  • привести всё к ценам с НДС;

  • привести всё к ценам без НДС.

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

Валюты

Для производственных закупок и электронных компонентов валюты — обычная история.

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

Мы сделали ввод курсов для RUB, USD, EUR, CNY и пересчёт в валюту отчёта.

Здесь тоже важна прозрачность: пользователь должен видеть не только итоговую цену, но и исходную цену поставщика, валюту, применённый режим НДС и комментарий по пересчёту. Иначе отчёт становится “магическим”, а в закупках это плохо.

Таргеты и отклонения

Отдельный сценарий — сравнение с целевыми ценами.

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

Поэтому мы добавили загрузку таблицы с таргетами и вывод отклонений.

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

Excel-отчёт как главный артефакт

Можно сколько угодно делать красивый интерфейс, но в корпоративной реальности итог часто всё равно должен быть в Excel.

Excel-отчёт нужен, чтобы:

  • отправить руководителю;

  • приложить к согласованию;

  • сохранить в папку закупки;

  • передать в бухгалтерию или снабжение;

  • вручную дописать комментарии;

  • вернуться к решению через месяц.

Поэтому экспорт для нас стал не второстепенной функцией, а одним из главных артефактов продукта.

Мы старались сделать отчёт понятным закупщику:

  • валюта отчёта;

  • партномер;

  • описание;

  • производитель;

  • комментарии и замены;

  • срок поставки;

  • лучший поставщик;

  • лучшая цена;

  • цены по каждому поставщику;

  • отклонения от таргета;

  • комментарии по НДС;

  • подсветка лучших и худших значений;

  • отдельные места, требующие ручной проверки.

Хороший Excel-экспорт — это не просто dataframe.to_excel() Это структура, порядок колонок, подписи, ширины, стили, подсветки и объяснимость.    

Демо-режим и лицензирование

Для B2B desktop-приложения нужен понятный демо-сценарий.

С одной стороны, пользователь должен иметь возможность скачать приложение, установить и проверить на тестовом проекте.

С другой — демо не должно превращаться в полноценную бесплатную коммерческую версию.

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

Сборка под macOS

С macOS всплыли свои нюансы.

Во-первых, имена. Для пользователя приложение называется по-русски, но productName в Tauri мы оставили ASCII. Это снижает риск проблем с путями, DMG и системными особенностями.

Во-вторых, архитектура. Нужно понимать, что именно собирается: Intel, arm64 или universal. В нашем случае отдельно фиксировали сценарий для x86_64 и проверяли DMG.

В-третьих, проверка образа. Недостаточно собрать .dmg; его нужно верифицировать, смонтировать, проверить состав, убедиться, что приложение запускается.

Отдельная тема — подпись, notarization, quarantine и всё, что знакомо каждому, кто пытался раздавать macOS-приложение вне App Store.

Сборка под Windows

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

Мы готовим MSI и классический .exe-установщик. Сборка вынесена в GitHub Actions: Windows runner, Node, Python, Rust, сборка backend sidecar и Tauri bundle.

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

Минимальный чеклист перед релизом:

  • версии совпадают в манифестах и UI;

  • frontend проходит typecheck;

  • backend проходит pytest;

  • sidecar собран;

  • Tauri bundle собран;

  • установщик проверен на чистой машине;

  • контрольные суммы обновлены;

  • сайт отдаёт установщики как файлы, а не ломает скачивание SPA-роутингом.

Тесты, которые действительно пригодились

Самыми полезными оказались не “тесты ради покрытия”, а регрессии на бизнес-логику:

  • режимы НДС;

  • выбор лучшего поставщика после нормализации;

  • Excel-экспорт;

  • лицензирование;

  • запрет нежелательных production-fallback сценариев;

  • обработка некорректных файлов.

Когда приложение работает с документами и деньгами, баг в UI неприятен, но баг в расчёте цены намного хуже.

Что бы мы сделали раньше

Если бы начинали заново, я бы раньше зафиксировал несколько вещей.

Во-первых, сразу разделил бы “демо-датасет для продаж” и “грязные файлы для тестов”. Красивое демо и жёсткие тестовые кейсы — это разные наборы данных.

Во-вторых, раньше бы сделал строгую политику release backend. Любой fallback на системный Python в production лучше убрать как можно раньше.

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

В-четвёртых, не стоит недооценивать Excel-отчёт. Для разработчика это может казаться “просто экспортом”. Для пользователя это главный результат работы.

Что получилось

В итоге получился локальный desktop-инструмент для закупочного сценария:

  • загрузить КП от нескольких поставщиков;

  • обработать документы локально;

  • сопоставить позиции;

  • привести цены к единой логике по НДС;

  • пересчитать валюты;

  • сравнить с таргетами;

  • показать спорные места;

  • выгрузить Excel-отчёт.

Технически для нас это был интересный опыт на стыке desktop, web frontend, Python backend, упаковки, Excel, бизнес-логики и корпоративных ограничений.

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

Иногда это означает не облако, а локальное приложение.

Не “умный чёрный ящик”, а осторожный помощник с ручной проверкой.

Не красивая аналитическая панель, а понятный Excel-отчёт, который можно отправить руководителю.

И не “давайте заменим весь процесс закупок”, а “давайте уберём самый ручной и ошибкоопасный кусок между получением КП и согласованием”.