Как мы сделали desktop-приложение для сравнения коммерческих предложений: Tauri, FastAPI, Python sidecar и Excel-отчёты
Обычно, когда говорят про автоматизацию закупок, вспоминают 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.
Упрощённо логика такая:
Tauri стартует.
Проверяет, нет ли уже живого backend на нужном локальном порту.
Если backend не найден — запускает sidecar.
Дожидается
/health.Проверяет, что ответил именно нужный backend приложения.
После этого frontend начинает работать с API.
При закрытии приложения дочерний процесс завершается.
Отдельно пришлось следить за тем, чтобы в 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-отчёт, который можно отправить руководителю.
И не “давайте заменим весь процесс закупок”, а “давайте уберём самый ручной и ошибкоопасный кусок между получением КП и согласованием”.