На сопровождении легко держать в голове десять клиентов и тяжело сорок. До аванса несколько дней, в чате тишина, акты не пришли, выписку обещают вечером. Первое сообщение бухгалтер копирует утром, второе — на следующий день, третье уже пишет человек, которому надоело. Половина клиентов отвечает после третьего напоминания. Вторая пишет «сейчас», и это «сейчас» дотягивает до срока. Ниже схема, которой это снимается: календарь, тексты, тупой бот и график, который живёт либо в файле, либо в 1С. Бот не считает налог и не проводит документы. Он отправляет уже готовый текст в уже известный чат.
Что делает бот
Одна строка графика — одно сообщение. В строке есть идентификатор,chat_id клиента, дата отправки и текст. Бот раз в несколько минут смотрит график и отправляет то, у чего дата отправки — сегодня.
Календарь, паузы и второй пинк собираются данными, не ветками в коде. Так одну и ту же программу можно кормить файлом schedule.json или HTTP-сервисом 1С.
Клиент должен один раз открыть бота и нажать «Старт». Пока этого нет, Telegram не отдаст chat_id, и писать некуда. Номер телефона из карточки контрагента сам по себе чат не открывает. chat_id бухгалтер сохраняет в графике.
Чего в этой схеме нет
Сумму аванса в шаблон не подставляем. Её сверяет человек по банку и по расчёту. В сообщении есть дата и просьба назвать день платежа. Юридически значимые документы остаются в ЭДО. Бот напоминает и приносит ответ в чат бухгалтера. В графике лежат имя чата, дата и текст. Паспорт, картотека ИНН, пароли клиент-банка и переписка с налоговой сюда не входят. Ответ «оплатил» статусом получения не считается. «Получили» бухгалтер ставит после банка. «Получили» по документам — когда файл лежит в папке или документ пришёл по ЭДО.Календарь
Пример: аванс 6 октября 2026, это вторник. Рабочие дни считаем без субботы и воскресенья. Праздники в этом черновике не вычтены: их знает производственный календарь 1С, и ниже дата отправки приходит уже посчитанной. За пять рабочих дней, 29 сентября, уходит первое сообщение по авансу. Если клиент промолчал и бухгалтер не поставил паузу, за один рабочий день, 5 октября, уходит второе. Третьего автоматического сообщения нет. 6 октября бот молчит, пишет или звонит бухгалтер. По документам ритм другой. Первое сообщение — в первый рабочий день после окончания месяца, только по тем позициям, которых нет в папке. Повтор — через три рабочих дня, и только по строкам, которые клиент не закрыл. Если клиент написал «пришлю в четверг», бухгалтер ставит паузу до этой даты. На следующее утро бот спрашивает уже только про этот пункт, отдельной строкой графика. Аванс и первичка уходят разными сообщениями и в разные дни. В одном тексте клиент отвечает на то, что проще, и откладывает платёж.Тексты
Аванс, первое касание: «Добрый день. На следующей неделе у вас авансовый платёж. Напишите, пожалуйста, в какой день планируете его отправить, чтобы мы сверили поступление.» Аванс, второе касание, только если тишина: «Добрый день. Напоминаю про аванс: срок — 6 октября. Ответьте сегодня, пожалуйста, ушёл ли платёж или на какой день он перенесён.» Документы: «Добрый день. Чтобы закрыть месяц, не хватает: акт за сентябрь, счёт от поставщика. Можно файлом в этот чат или через ЭДО, как вам удобнее. Если чего-то ещё нет, напишите дату, когда будет.» Сумму, пени и формулировку про штраф в шаблон не ставим. Сообщение со штрафом клиент пересылает директору, и разговор уходит в спор, хотя платёж и так стоял в плане.Статусы, которые ставит человек
Их четыре: ждём, обещали дату, получили, молчит после второго напоминания. Любой ответ клиента останавливает автоматику по этому авансу. Фраза «а какая сумма?» тоже останавливает: бот не отличает вопрос от обещания. Бухгалтер читает пересланный текст и либо ставит паузу, либо руками добавляет следующее напоминание. Спор («мы уже платили», «акт не подписываем») обрабатывается так же: автоматика молчит, пока человек не снимет паузу.График без 1С
Пока базы нет, хватает файла рядом с ботом. Идентификатор второго шага другой: бот запоминает отправленныеid и второй раз ту же строку не шлёт.
{
"items": [
{
"id": "romashka-2026-10-06-1",
"chat_id": 123456789,
"remind_on": "2026-09-29",
"text": "Добрый день. На следующей неделе у вас авансовый платёж. Напишите, пожалуйста, в какой день планируете его отправить, чтобы мы сверили поступление."
},
{
"id": "romashka-2026-10-06-2",
"chat_id": 123456789,
"remind_on": "2026-10-05",
"text": "Добрый день. Напоминаю про аванс: срок — 6 октября. Ответьте сегодня, пожалуйста, ушёл ли платёж или на какой день он перенесён."
}
]
}Как сюда встаёт 1С
Готового модуля «Bothost для 1С» нет. Связка обычная: в расширении конфигурации лежит регистр сведений, HTTP-сервис отдаёт на сегодня готовые строки, бот их отправляет. Проводки, счета и зарплата боту не видны. Так сделано специально. Аванс в Бухгалтерии предприятия, УНФ и самописной базе лежит в разных местах. Общий запрос «найди все авансы» на чужой конфигурации соврать легче, чем помочь. Надёжный график — отдельный непериодический регистр, который бухгалтер заполняет сам или который заполняет ваше регламентное задание из уже проверенного списка. Регистр, одно измерениеИдентификатор (строка). Остальное — ресурсы:
ChatId— строка, тот самый id чата после «Старт»ДатаАвансаДатаНапоминания— уже рабочий день, с учётом вашего производственного календаряТекстОтправлено— булевоПаузаДо— дата, пустая, если паузы нетПоследнийОтвет— строка
Функция ДатаЗаNРабочихДней(ДатаОтсчета, ЧислоДней)
Дата = НачалоДня(ДатаОтсчета);
Осталось = ЧислоДней;
Пока Осталось > 0 Цикл
Дата = Дата - 86400;
День = ДеньНедели(Дата);
Если День < 6 Тогда
Осталось = Осталось - 1;
КонецЕсли;
КонецЦикла;
Возврат Дата;
КонецФункцииДатаЗаNРабочихДней(ДатаАванса, 5), второе — с пятёркой, заменённой на единицу. Вторую запись регламентное задание создаёт утром того дня, и только если по этому идентификатору аванса ПоследнийОтвет пустой и ПаузаДо пустая. Любой ответ клиента вторую запись не порождает.
Токен сервиса лежит в константе или в безопасном хранилище, не в тексте модуля и не в статье. Пустая строка ниже — заглушка: пока её не замените, сервис отвечает 401, и это правильное поведение.
Функция ТокенНапоминаний()
Возврат "";
КонецФункцииAuthorization: Bearer …. Весь веб-клиент базы в интернет для рассылки не нужен.
Шаблон GET, условный адрес /hs/reminders/advances. Имена регистра в запросе замените на свои. Пустая дата в 1С — ДАТАВРЕМЯ(1, 1, 1).
Функция AdvancesGET(Запрос)
Ожидаем = "Bearer " + ТокенНапоминаний();
Если Запрос.Заголовки.Получить("Authorization") <> Ожидаем Тогда
Возврат Новый HTTPСервисОтвет(401);
КонецЕсли;
Данные = Новый Запрос;
Данные.Текст =
"ВЫБРАТЬ
| График.Идентификатор КАК Идентификатор,
| График.ChatId КАК ChatId,
| График.ДатаНапоминания КАК ДатаНапоминания,
| График.Текст КАК Текст
|ИЗ
| РегистрСведений.ГрафикНапоминаний КАК График
|ГДЕ
| График.ДатаНапоминания = &Сегодня
| И График.Отправлено = ЛОЖЬ
| И (График.ПаузаДо = ДАТАВРЕМЯ(1, 1, 1) ИЛИ График.ПаузаДо < &Сегодня)";
Данные.УстановитьПараметр("Сегодня", НачалоДня(ТекущаяДатаСеанса()));
Массив = Новый Массив;
Выборка = Данные.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Строка = Новый Структура;
Строка.Вставить("id", Выборка.Идентификатор);
Строка.Вставить("chat_id", Выборка.ChatId);
Строка.Вставить("remind_on", Формат(Выборка.ДатаНапоминания, "ДФ=yyyy-MM-dd"));
Строка.Вставить("text", Выборка.Текст);
Массив.Добавить(Строка);
КонецЦикла;
Запись = Новый ЗаписьJSON;
Запись.УстановитьСтроку();
ЗаписатьJSON(Запись, Новый Структура("items", Массив));
Ответ = Новый HTTPСервисОтвет(200);
Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
Ответ.УстановитьТелоИзСтроки(Запись.Закрыть(), КодировкаТекста.UTF8);
Возврат Ответ;
КонецФункции/hs/reminders/ack с телом {"id":"…"}. Флаг Отправлено ставится здесь. Статус «получили» этот вызов не трогает: деньги ещё не сверяли.
Идентификатор должен быть измерением, иначе набор записей по одному полю не отобрать.
Функция AckPOST(Запрос)
Ожидаем = "Bearer " + ТокенНапоминаний();
Если Запрос.Заголовки.Получить("Authorization") <> Ожидаем Тогда
Возврат Новый HTTPСервисОтвет(401);
КонецЕсли;
Чтение = Новый ЧтениеJSON;
Чтение.УстановитьСтроку(Запрос.ПолучитьТелоКакСтроку());
Данные = ПрочитатьJSON(Чтение);
Чтение.Закрыть();
Идентификатор = "";
Если ТипЗнч(Данные) = Тип("Структура") И Данные.Свойство("id") Тогда
Идентификатор = Строка(Данные.id);
ИначеЕсли ТипЗнч(Данные) = Тип("Соответствие") Тогда
Идентификатор = Строка(Данные.Получить("id"));
КонецЕсли;
Если Идентификатор <> "" Тогда
Набор = РегистрыСведений.ГрафикНапоминаний.СоздатьНаборЗаписей();
Набор.Отбор.Идентификатор.Установить(Идентификатор);
Набор.Прочитать();
Если Набор.Количество() > 0 Тогда
Набор[0].Отправлено = Истина;
Набор.Записать();
КонецЕсли;
КонецЕсли;
Возврат Новый HTTPСервисОтвет(204);
КонецФункции/hs/reminders/reply с телом {"chat_id":"…","text":"…"}. Обработчик находит открытые строки с этим ChatId и пишет ПоследнийОтвет. После этого регламентное задание второе напоминание уже не создаёт.
Если 1С в момент ответа недоступна, копия всё равно остаётся в чате бухгалтера. Это основной канал. Повторная доставка в базу — ручная, из этого чата.
Код бота
Файлы:main.py, requirements.txt, при работе без 1С ещё schedule.json. Зависимость одна.
requestsBOT_TOKEN— токен TelegramBOT_TZ— часовой пояс графика, по умолчаниюEurope/MoscowPOLL_SECONDS— пауза между проверками графика, по умолчанию 600ACCOUNTANT_CHAT_ID— куда пересылать ответы клиентовONEC_URL— GET графика; если пусто, читаетсяschedule.jsonONEC_ACK_URL— POST отметки «отправлено»ONEC_REPLY_URL— POST текста ответаONEC_TOKEN— секрет заголовкаAuthorization
sent.json — защита от повтора. Отметка в 1С делается после отправки. Если база не ответила, клиенту второе такое же сообщение не уйдёт: строка уже в sent.json, в логе останется ошибка ack.
import json
import os
import time
from datetime import date, datetime
from pathlib import Path
from zoneinfo import ZoneInfo
import requests
BOT_TOKEN = os.environ["BOT_TOKEN"]
ONEC_URL = os.environ.get("ONEC_URL", "").strip()
ONEC_ACK_URL = os.environ.get("ONEC_ACK_URL", "").strip()
ONEC_REPLY_URL = os.environ.get("ONEC_REPLY_URL", "").strip()
ONEC_TOKEN = os.environ.get("ONEC_TOKEN", "").strip()
ACCOUNTANT_CHAT_ID = os.environ.get("ACCOUNTANT_CHAT_ID", "").strip()
STATE_PATH = Path(os.environ.get("STATE_PATH", "sent.json"))
POLL_SECONDS = int(os.environ.get("POLL_SECONDS", "600"))
TZ = ZoneInfo(os.environ.get("BOT_TZ", "Europe/Moscow"))
OFFSET_PATH = Path(os.environ.get("OFFSET_PATH", "offset.txt"))
def headers():
return {"Authorization": f"Bearer {ONEC_TOKEN}"}
def load_sent():
if not STATE_PATH.exists():
return set()
try:
data = json.loads(STATE_PATH.read_text(encoding="utf-8"))
except json.JSONDecodeError:
return set()
return set(data.get("sent", []))
def save_sent(sent):
STATE_PATH.write_text(
json.dumps({"sent": sorted(sent)}, ensure_ascii=False, indent=2),
encoding="utf-8",
)
def fetch_items():
if ONEC_URL:
response = requests.get(ONEC_URL, headers=headers(), timeout=30)
response.raise_for_status()
payload = response.json()
else:
payload = json.loads(Path("schedule.json").read_text(encoding="utf-8"))
items = payload.get("items", [])
if not isinstance(items, list):
raise ValueError("В ответе нет списка items")
return items
def send_message(chat_id, text):
response = requests.post(
f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
json={"chat_id": chat_id, "text": text},
timeout=20,
)
response.raise_for_status()
body = response.json()
if not body.get("ok"):
raise RuntimeError(body.get("description", "telegram error"))
def ack(item_id):
if not ONEC_ACK_URL or not item_id:
return
response = requests.post(
ONEC_ACK_URL,
headers=headers(),
json={"id": item_id},
timeout=20,
)
response.raise_for_status()
def dispatch_due():
today = datetime.now(TZ).date()
sent = load_sent()
for item in fetch_items():
item_id = str(item.get("id", "")).strip()
raw_date = str(item.get("remind_on", "")).strip()
text = str(item.get("text", "")).strip()
chat_id = item.get("chat_id")
if not item_id or item_id in sent or not text or chat_id in (None, ""):
continue
try:
remind_on = date.fromisoformat(raw_date)
except ValueError:
print(f"bad date {item_id}")
continue
if remind_on != today:
continue
send_message(int(chat_id), text)
sent.add(item_id)
save_sent(sent)
try:
ack(item_id)
except requests.RequestException as exc:
print(f"ack {item_id} failed: {exc}")
print(f"sent {item_id}")
def load_offset():
if not OFFSET_PATH.exists():
return 0
raw = OFFSET_PATH.read_text(encoding="utf-8").strip()
return int(raw) if raw.isdigit() else 0
def save_offset(value):
OFFSET_PATH.write_text(str(value), encoding="utf-8")
def forward_reply(chat_id, text):
if ACCOUNTANT_CHAT_ID:
send_message(int(ACCOUNTANT_CHAT_ID), f"Ответ из чата {chat_id}:\n{text}")
if not ONEC_REPLY_URL:
return
try:
response = requests.post(
ONEC_REPLY_URL,
headers=headers(),
json={"chat_id": str(chat_id), "text": text},
timeout=20,
)
response.raise_for_status()
except requests.RequestException as exc:
print(f"reply sync failed: {exc}")
def poll_replies():
offset = load_offset()
response = requests.get(
f"https://api.telegram.org/bot{BOT_TOKEN}/getUpdates",
params={"timeout": 30, "offset": offset},
timeout=40,
)
response.raise_for_status()
body = response.json()
if not body.get("ok"):
raise RuntimeError(body.get("description", "getUpdates error"))
for update in body.get("result", []):
update_id = int(update["update_id"])
message = update.get("message") or {}
text = str(message.get("text") or "").strip()
chat = message.get("chat") or {}
chat_id = chat.get("id")
if text and chat_id is not None and str(chat_id) != ACCOUNTANT_CHAT_ID:
forward_reply(chat_id, text)
save_offset(update_id + 1)
def main():
next_send = 0.0
while True:
now = time.time()
if now >= next_send:
try:
dispatch_due()
except Exception as exc:
print(f"dispatch failed: {exc}")
next_send = now + POLL_SECONDS
try:
poll_replies()
except Exception as exc:
print(f"poll failed: {exc}")
time.sleep(5)
if __name__ == "__main__":
main()Где бот работает постоянно
Ноутбук бухгалтера вечером закрывается. Рассылка на утро понедельника от этого не должна вставать, поэтому процесс живёт отдельно от рабочей машины. Собрать и запустить такого бота на Bothost можно по короткой инструкции: быстрый старт. Если кода в git нет, тот же каталог уходит архивом: загрузка архива. В переменные окружения кладутсяBOT_TOKEN и, когда появится сервис, адреса 1С. Файл sent.json должен лежать на диске контейнера, а не только в памяти: иначе после перезапуска утренние сообщения уйдут второй раз.
Пока ONEC_URL пустой, бот читает schedule.json из каталога программы. Это нормальный первый месяц: сначала календарь и тексты, потом регистр.
Что проверить в первую неделю
Четыре числа, без них схема не настраивается. Сколько первых сообщений получили ответ в тот же день. Если почти никто не ответил, текст слишком ранний или слишком обтекаемый. Сколько диалогов дошли до второй строки. Если больше половины, первое касание люди откладывают. Раньше двигать дату не стоит: за десять дней сообщение забывают. Сколько обещаний «пришлю в четверг» закрылись на следующее утро. Если почти ни одного, паузу ставят, а файл в папку не кладут. Это уже работа бухгалтера, не бота. Сколько звонков осталось на день аванса. Цель — короткий список. Пустой список в сезон чаще значит, что кто-то потерялся между «ждём» и «обещали дату». Если через две недели второе напоминание всё ещё уходит почти всем, дело в сроках и в тексте. Бот к этому моменту шлёт ровно те строки, которые лежат в графике.
220 просмотров
0 лайков
0 комментариев
Комментарии (0)
Пока нет комментариев. Будьте первым!