ESPNWhere is Jaylen Waddle? Why the Broncos' big offseason pickup hasn't found his form in DenverBollywood HungamaRajpal Yadav deposits Rs 2 crores in cheque bounce case; Supreme Court refers dispute to mediationPunchNBA probes mismanagement claims of ₦100m Imo govt gift to Owerri branchRTP DesportoPalmeiras a um passo de nova final da LibertadoresThe Jerusalem PostBasij female unit commander assassinated in latest attack targeting Iranian armed forcesDaily MaverickIndustry Influence: Experts call for ‘big food’ ban at future SA Nutrition CongressesInquirerDTI: Prices of water, basic goods stable amid looming ‘Super El Niño’ZDF heuteSo haben sich die Mieten in ihrer Region entwickeltObservador DesportoAs notícias das 19hThe South African’13th cheque please’: SASSA grant recipients share their Christmas wish listGlobo EsporteCom Falcão, Brasil e Polônia se enfrentam pelo Legends Game Brasil em SC20 MinutenTödlicher Achterbahn-Unfall – sparte Six Flags bei Wartung?
The Daily Newsstand · Free, Always
Thursday, October 15, 2026

Как Google Sheets стал backend для закупок и логистики

Translate

Я руководитель подразделения: занимаюсь закупками, логистикой и персоналом. Несколько лет назад мне нужно было организовать учёт документов и рейсов для трёх магазинов, склада и трёх автомобилей. Отдельного бюджета на разработку не было. Начал с Google Sheets и Glide, а затем добавил Apps Script и Telegram.

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

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

Сначала нужно было изменить момент ввода данных

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

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

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

Так появилась единая таблица с мобильным интерфейсом. Позже к ней добавились уведомления о предстоящей машине, утренние PDF со списком документов и месячные отчёты.

Что такое Glide и какую задачу он решал

Glide — платформа для создания приложений из готовых компонентов с подключением источников данных. В моём проекте таким источником стали Google Sheets: поверх строк и столбцов я собрал формы, списки и карточки для сотрудников.

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

В приложении были два основных экрана: список рейсов с датой и автомобилем и список документов, сгруппированных по рейсам. Это давало сотрудникам доступ к нужным записям без работы со структурой таблицы.

Обезличенные примеры интерфейса: экран рейсов и экран документов.

В этой архитектуре Glide выполнял роль пользовательского интерфейса. Часть настроек и вычислений могла оставаться внутри платформы, а обработка данных и интеграции постепенно переходили в Apps Script.

Я описываю конфигурацию своего проекта, начатого в 2022 году. Повторять её сегодня нужно с проверкой действующих условий Glide: возможности подключения Google Sheets и ограничения тарифов менялись. Этот опыт не означает, что такую же связку сейчас можно собрать на любом бесплатном плане.

Из каких частей состоит работающая система

Словом backend здесь я называю весь слой хранения и обработки данных. Сам по себе Google Sheets не выполняет эту роль целиком: бизнес-логика находится также в Apps Script, а состояние диалогов бота — в Script Properties.

Архитектура действующей системы

Архитектура действующей системы

Схема действующей системы. Telegram используется и для запросов к боту, и для доставки результатов. Генерация PDF — отдельный сценарий отчётности.

Компонент

Роль в проекте

Glide

Ввод данных и просмотр рейсов, документов и расходов

Google Sheets

Рабочие записи, справочники и часть служебного состояния

Apps Script

Обработка запросов бота, расчёты, отправка сообщений и копирование данных в архив

Script Properties

Состояние диалога и параметры запроса конкретного пользователя

Telegram

Запрос отчёта и получение уведомлений, сводок и PDF

Архивная Google Sheets

Исторические данные, отделённые от рабочей таблицы

Собственного постоянно работающего сервера у этой версии нет. Выполнение сценариев обеспечивает инфраструктура Apps Script: бот принимает HTTP-запросы через doPost, периодические задачи запускаются по расписанию.

Расписание здесь важно отличать от события редактирования. Изменение данных внешним приложением или другим скриптом нельзя автоматически считать запуском onEdit: для внешних записей нужен подходящий механизм обработки, например периодическая проверка. Эта особенность описана в документации триггеров Apps Script.

Telegram оказался ещё одним интерфейсом к данным

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

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

В действующем коде появились состояния:

var STATES = {
  AWAITING_NAME: 'AWAITING_NAME',
  AWAITING_START_DATE: 'AWAITING_START_DATE',
  AWAITING_END_DATE: 'AWAITING_END_DATE',
  AWAITING_USER_SELECTION: 'AWAITING_USER_SELECTION'
};

Текущее состояние сохраняется между отдельными запросами к скрипту:

function setUserState(chatId, state) {
  PropertiesService.getScriptProperties()
    .setProperty(chatId + '_STATE', state);
}

function getUserState(chatId) {
  return PropertiesService.getScriptProperties()
    .getProperty(chatId + '_STATE');
}

После следующего сообщения бот читает состояние и выбирает обработчик. Так внутри небольшого Apps Script-проекта появилась простая машина состояний.

При /start бот записывает chatId, запрашивает полное имя и сохраняет его в отдельной таблице пользователей. Параметры незавершённого запроса отчёта также хранятся в Script Properties под ключом конкретного чата.

Приведённые здесь фрагменты взяты из действующих скриптов; для статьи приведено в порядок форматирование и исключены внутренние идентификаторы и настройки. Это части реализации, а не готовый к развёртыванию комплект кода.

Роли появились раньше полноценной авторизации

Бот проверяет роль пользователя по записи в таблице:

function isPrivilegedUser(chatId) {
  var position = getChatIdData(chatId, 3);
  return ['руководитель', 'разработчик'].includes(position);
}

В сценарии отчёта руководитель или разработчик получает выбор сотрудника. Для обычного пользователя отчёт строится по имени, связанному с его chatId.

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

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

Отчёт стал вычислением над общей историей

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

Один из реальных фрагментов — выбор пяти крупнейших категорий:

const categorySummary = getCategorySummary(filteredData);
const topCategories = Object.entries(categorySummary)
  .sort((a, b) => b[1].total - a[1].total)
  .slice(0, 5);

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

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

Получателей отчёта скрипт выбирает из таблицы пользователей по ролям «руководитель» и «разработчик», затем отправляет каждому сообщение через Telegram Bot API.

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

В таблице появились данные о работе самой системы

Со временем рабочие листы перестали быть единственным содержимым файла. Сейчас в base.krsk 12 листов, включая скрытый App: Metadata.

Панель листов рабочей таблицы

Панель листов рабочей таблицы

Фрагмент реальной панели вкладок. На снимке видна часть листов; скрытый App: Metadata в панели не отображается.

Кроме документов, рейсов, сотрудников, поставщиков и расходов, в файле есть ReportState и App: Logins. У них разные назначения: состояние обработки, записи о входах и метаданные приложения не следует смешивать в один общий «лог».

В ReportState используются поля:

Поле

Что фиксирует

TripKey

Ключ рейса

LastUpdate

Дату последнего обновления

LastEntryDate

Дату последней записи

ProcessedDate

Дату обработки

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

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

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

Дата для человека и дата для расчёта оказались разными задачами

В таблице установлены локаль ru_RU и часовой пояс Asia/Krasnoyarsk. В коде бота эти правила заданы явно:

var DATE_FORMAT = 'dd.MM.yyyy';
var DATETIME_FORMAT = 'dd.MM.yyyy HH:mm:ss';
var TIMEZONE = 'Asia/Krasnoyarsk';

function formatDate(date) {
  return Utilities.formatDate(date, TIMEZONE, DATE_FORMAT);
}

Так дата выводится в привычном для сотрудников виде. Это именно форматирование результата, а не изменение момента времени внутри Date.

С вводом получилось сложнее. В текущем сценарии Telegram бот просит ММ.ДД.ГГГГ, а данные с временем разбирает как ДД.ММ.ГГГГ ЧЧ:мм:сс. В парсере есть две разные ветки.

Такое наследие особенно неприятно на неоднозначных значениях: 04.05.2026 можно прочитать как 4 мая или 5 апреля. Ни локаль таблицы, ни формат вывода не исправляют уже неверно разобранный ввод.

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

Ещё один независимый параметр — часовой пояс проекта Apps Script. Он может отличаться от часового пояса таблицы. В документации Google это прямо отмечено как частый источник ошибок. Явное форматирование помогает вывести дату, но при фильтрации по дню или месяцу всё равно нужно согласовать правила вычисления границ периода.

Например, бот включает конечный день отчёта через границу следующего дня:

var start = new Date(
  startDate.getFullYear(), startDate.getMonth(), startDate.getDate()
);
var end = new Date(
  endDate.getFullYear(), endDate.getMonth(), endDate.getDate() + 1
);

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

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

Архивирование стало отдельным сценарием

Рабочие данные и история со временем разделились. Отдельный Apps Script выбирает строки из logistic_krsk за предыдущий месяц и дописывает их в архивную Google Sheets.

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

При разборе этой реализации обнаружились две вещи, которые стоит учитывать при повторении подхода.

Первая — граница месяца. В исходном коде используется:

var lastDayOfLastMonth = new Date(
  now.getFullYear(), now.getMonth(), 0
);

Это начало последнего дня месяца, а не его конец. При сравнении rowDate <= lastDayOfLastMonth запись со временем позже полуночи последнего дня может не попасть в выборку. Для такого фильтра удобнее начало следующего месяца как исключающая верхняя граница.

Вторая — повторный запуск. Скрипт дописывает выбранные строки в конец архивного листа без проверки их идентификаторов. Если запустить его ещё раз за тот же период, в архиве могут появиться дубли.

Эти свойства видны в коде; я не утверждаю, что из-за них уже произошла потеря данных или дублирование в рабочей системе. Но именно такие детали превращают задачу «скопировать строки» в задачу об устойчивой обработке: выбрать правильный период, проверить повторность и только потом считать операцию завершённой.

Почему следующим шагом стала PostgreSQL

Текущая система решает рабочие задачи. Однако теперь я хочу получать документы поставщиков ещё до формирования рейса: из почты и мессенджера, в PDF, XLS/XLSX и иногда на фотографии.

Получение вложений из Mail.ru по IMAP в Google Drive уже проверено на контрольных файлах. Это ещё не непрерывно работающий производственный контур: автоматический запуск, распознавание, подтверждения и запись результата требуют дальнейшей работы.

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

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

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

При этом PostgreSQL не заменит интерфейс, обработчик почты, распознавание и Telegram. Она даст более подходящую основу хранения; приложение по-прежнему должно управлять доступом, повторными попытками, доставкой уведомлений и журналом действий.

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

Что я бы повторил и что изменил бы сразу

Для небольшого внутреннего процесса я снова рассмотрел бы таблицу как основу MVP. Она помогла быстро проверить главное: удобно ли людям вносить документы, какие поля действительно нужны и какие отчёты полезны руководителю.

Но с первого этапа я бы зафиксировал несколько правил:

  • У каждой сущности есть устойчивый идентификатор; имя сотрудника и строковое описание рейса не заменяют его.

  • Формат ввода дат, часовой пояс и границы отчётного периода определены одинаково для всех сценариев.

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

  • Состояние обработки и журнал событий хранятся раздельно и связаны с конкретным объектом.

  • Доступ проверяется перед выдачей данных; самостоятельный ввод имени не считается подтверждением личности.

Смысл этой истории для меня в том, что модель системы выросла из эксплуатации. Сначала появился общий ввод документов. Затем — доставка информации сотрудникам, запросы к данным, состояние диалогов, отчёты и обслуживание истории.

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

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

Ссылки по техническим деталям

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.