Для рабочих целей есть потребность учитывать совершённые за наличные расходы. Раньше это делалось вручную - вписывалось наименование покупки и её цена в телеграм-чатик, потом вручную переносилось в google sheets. Потом перенос в google sheets автоматизировался с помощью скрипта python и google-api, но т.к. товаров в чеке могло быть много, поэтому список из 10 (например) позиций сокращался до какой-то общей типа "инструменты" (например) с указанием общей суммы чека, что не особо годилось для возможной дальнейшей аналитики. Как следующий этап развития, возникла идея получать данные о товарах с помощью qr-кода и API ИФНС.
В наличии имеется постоянно подключенный к сети одноплатный компьютер Raspberry PI с установленным FreeBPX (настроено по этой статье) с модемом E1550, перепрошитым на голосовые функции. Соответственно, все настройки выполнялись на этом комплекте.
В моём случае Raspberry и модем уже настроены и работают несколько лет, могут звонить и получать СМС. Дальнейшая задача - получить список товаров из чека.
ИФНС позволяет это сделать двумя путями: 1) использовать API-ключ (и тогда теоретически можно обойтись без модема); 2) проходить авторизацию по СМС.
Т.к. у меня есть модем и возможность получать СМС, иду по второму пути.
Принцип авторизации следующий: отправляем запрос на сайт налоговой, в котором указываем номер телефона, в ответ на номер телефона приходит смс для авторизации, после чего отправляем запрос с данными чека и кодом из смс, в ответ на который получаем JSON с данными из чека, если чека находится в базе ИФНС, либо информацию с ошибкой, если чека нет.
Принцип работы моей схемы:
Пользователь сканирует qr-код с чека и отправляет текст qr-кода в телеграм-чат;
Телеграм-бот получает текст сообщения и запускает скрипт Python, который проверяет, является ли сообщение QR-кодом и, если да, то делает запрос в налоговую, проходя авторизацию по СМС, получает список товаров и возвращает его попозиционно в телеграм, а также записывает в google sheets;
Если сообщение не является QR-кодом, то с помощью скрипта осуществляется запись текста сообщения в google sheets.
Для создания скрипта использовалась информация из этой статьи, а также репозиторий на гитхабе.
Исходный скрипт с гитхаба был немного доработан под мои нужды, а именно: добавлена функция проверки срока жизни кода, чтобы не запрашивать каждый раз новый код в случае необходимости проверить несколько чеков подряд. Полученный СМС-код действует 120 секунд.
Как определить жив ли код и остаток его жизни?
Для этого добавил в исходный скрипт функцию проверки жизнеспособности ранее полученного кода check_sms
. Работает это так: делаем post-запрос, включающий номер телефона, в ответ получаем статус ранее отправленного (если такой был) кода:
429 - код есть и действует, также увидим в ответе оставшееся время его действия в секундах (максимум 120);
204 - код устарел, либо его не было.
Учитываем, что asterisk настроен на сохранение входящих смс в файл /var/log/asterisk/sms.txt
. В принципе, можно настроить сохранение смс в базу asteriskcdrdb, а потом читать смс оттуда, но настроить сохранение в файл - дело 1 строчки в конфиге, а сохранение в базу чуть посложнее.
Т.к. я не программист, то много комментировал код, а также добавил кучу print для понимания в процессе доработки, что происходит и какой элемент за что отвечает, поэтому в целом должно быть всё понятно. Доработанный скрипт:
nalog-python.py
import json
import time
import requests
import os
from datetime import datetime
class NalogRuPython:
HOST = 'irkkt-mobile.nalog.ru:8888'
DEVICE_OS = 'iOS'
CLIENT_VERSION = '2.9.0'
DEVICE_ID = '7C82010F-16CC-446B-8F66-FC4080C66521'
ACCEPT = '*/*'
USER_AGENT = 'billchecker/2.9.0 (iPhone; iOS 13.6; Scale/2.00)'
ACCEPT_LANGUAGE = 'ru-RU;q=1, en-US;q=0.9'
CLIENT_SECRET = 'IyvrAbKt9h/8p6a7QPh8gpkXYQ4='
OS = 'Android'
def __init__(self):
self.__session_id = None
self.check_sms()
self.set_session_id()
def check_sms(self): #проверка срока действия кода из смс и запрос нового, если старый код из смс уже не подойдет
# """
# Authorization using phone and SMS code
# """
self.__phone = str('+71234567890') #номер телефона, на который будет приходить смс (asterisk freePBX)
url = f'https://{self.HOST}/v2/auth/phone/request'
payload = {
'phone': self.__phone,
'client_secret': self.CLIENT_SECRET,
'os': self.OS
}
headers = {
'Host': self.HOST,
'Accept': self.ACCEPT,
'Device-OS': self.DEVICE_OS,
'Device-Id': self.DEVICE_ID,
'clientVersion': self.CLIENT_VERSION,
'Accept-Language': self.ACCEPT_LANGUAGE,
'User-Agent': self.USER_AGENT,
}
r = requests.post(url, json=payload, headers=headers) #запрос с параметрами на проверку срока действия кода (варианты - есть и действует какое-то время (не более 120 сек) или нет (шлет новый тогда))
print (datetime.now())
################### СМС код запрошен ######################################################
#статус-код - 204 - код не действует, отправлен новый, 429 - код действует, можно использовать старый
print ("status code: " + str(r.status_code))
#ответные headers - время, до какого действует код из смс, время в сек действия, время текущее
print ('headers: ' + str(r.headers))
# время действия кода оставшееся
time_left = int(r.headers ['Retry-After']) # сколько секунд ещё действует код из СМС
print ("time_left: " + str(time_left))
if r.status_code == 204: # Если кода давно не было (более 120 сек.), то считываем новое СМС
print ("код устарел, получаю новый")
time.sleep(10) # ждем доставку смс
elif r.status_code == 429 and time_left <5: # если код есть, но через 5 сек он уже не будет действовать, то ждем 6 сек и получаем новый
time.sleep(6)
self.check_sms()
elif r.status_code == 429 and time_left >=5: # если код есть и действует более 5 сек, то используем его
#читаем СМС из файла
f_read = open("/var/log/asterisk/sms.txt", "r") #открываем для чтения
last_line = f_read.readlines()[-1] # считываем последнюю строку
print (last_line)
#пример строки: "2023-09-15 21:09:02 - dongle0 - KKT.NALOG: Проверка чеков ФНС России. Код: 2872"
f_read.close() #закрываем файл
sms_lst = last_line.split(' - ')[2] # делим последнюю строку на элементы списка, используя разделитель в виде " - " (пробел минус пробел)
print ("sms last: " + sms_lst)
self.__code = sms_lst.strip().split(': ')[2] # берем последний элемент (третий) из этого списка, удаляем из него перенос строки и пробелы, чтобы получить только цифры
print ("code: -" + self.__code + "-") # минусы поставил, чтобы понять, что перед цифрами и после нет пробелов пример: -0123-
#################### пошло дальше уже с кодом ######################################
def set_session_id(self) -> None:
print ('session_id')
url = f'https://{self.HOST}/v2/auth/phone/verify' #запрос уже с кодом
payload = {
'phone': self.__phone,
'client_secret': self.CLIENT_SECRET,
'code': self.__code,
"os": self.OS
}
headers = {
'Host': self.HOST,
'Accept': self.ACCEPT,
'Device-OS': self.DEVICE_OS,
'Device-Id': self.DEVICE_ID,
'clientVersion': self.CLIENT_VERSION,
'Accept-Language': self.ACCEPT_LANGUAGE,
'User-Agent': self.USER_AGENT,
}
resp = requests.post(url, json=payload, headers=headers)
print ('headers: ')
print (resp.headers)
print ('status: ')
print (resp.status_code)
self.__session_id = resp.json()['sessionId']
self.__refresh_token = resp.json()['refresh_token']
def refresh_token_function(self) -> None:
print ('refresh_token_function')
url = f'https://{self.HOST}/v2/mobile/users/refresh'
payload = {
'refresh_token': self.__refresh_token,
'client_secret': self.CLIENT_SECRET
}
headers = {
'Host': self.HOST,
'Accept': self.ACCEPT,
'Device-OS': self.DEVICE_OS,
'Device-Id': self.DEVICE_ID,
'clientVersion': self.CLIENT_VERSION,
'Accept-Language': self.ACCEPT_LANGUAGE,
'User-Agent': self.USER_AGENT,
}
resp = requests.post(url, json=payload, headers=headers)
self.__session_id = resp.json()['sessionId']
self.__refresh_token = resp.json()['refresh_token']
def _get_ticket_id(self, qr: str) -> str:
# """
# Get ticker id by info from qr code
# :param qr: text from qr code. Example "t=20200727T174700&s=746.00&fn=9285000100206366&i=34929&fp=3951774668&n=1"
# :return: Ticket id. Example "5f3bc6b953d5cb4f4e43a06c"
# """
url = f'https://{self.HOST}/v2/ticket'
payload = {'qr': qr}
headers = {
'Host': self.HOST,
'Accept': self.ACCEPT,
'Device-OS': self.DEVICE_OS,
'Device-Id': self.DEVICE_ID,
'clientVersion': self.CLIENT_VERSION,
'Accept-Language': self.ACCEPT_LANGUAGE,
'sessionId': self.__session_id,
'User-Agent': self.USER_AGENT,
}
resp = requests.post(url, json=payload, headers=headers)
return resp.json()["id"]
def get_ticket(self, qr: str) -> dict:
# """
# Get JSON ticket
# :param qr: text from qr code. Example "t=20200727T174700&s=746.00&fn=9285000100206366&i=34929&fp=3951774668&n=1"
# :return: JSON ticket
# """
ticket_id = self._get_ticket_id(qr)
url = f'https://{self.HOST}/v2/tickets/{ticket_id}'
headers = {
'Host': self.HOST,
'sessionId': self.__session_id,
'Device-OS': self.DEVICE_OS,
'clientVersion': self.CLIENT_VERSION,
'Device-Id': self.DEVICE_ID,
'Accept': self.ACCEPT,
'User-Agent': self.USER_AGENT,
'Accept-Language': self.ACCEPT_LANGUAGE,
'Content-Type': 'application/json'
}
resp = requests.get(url, headers=headers)
print (datetime.now())
return resp.json()
Далее создаём телеграм-бот (процесс не описываю,- в сети достаточно мануалов), а также скрипт для обработки сообщений.
Для записи расходов есть телеграм-чат, куда записываются сообщения в формате "расход;сумма" (например: пирог;100
), либо текст qr-кода с чека (например: t=20200924T1837&s=349.93&fn=9282440300682838&i=46534&fp=1273019065&n=1
).
Принцип действия простой: если сообщение не qr-код, то просто записываем его в google sheets, а если qr-код, то запускаем процесс получения списка товаров, а в google sheets записываем информацию о товарах.
Далее описываю скрипт по частям:
Задаём начальные данные, указываем токен бота
#sheets
import gspread
from oauth2client.service_account import ServiceAccountCredentials
#телеграм бот
import telebot
import time
import datetime
import json
from nalog_python import NalogRuPython # делает запрос в налоговую, проходит авторизацию по смс, берет данные из смс и с ними получает данные чека
#обработчик фото чеков (берет фото чека и сканирует с него qr-код, если смог его найти, либо выдаёт error)
import photo5
# Создаем экземпляр бота
bot = telebot.TeleBot('токен бота')
Т.к. у меня происходит запись результатов в google sheets, то далее указываю ссылки на таблицу и на файл json с ключом для работы с google sheets (получил вот здесь).
Данные для интеграции с google sheets
#ссылка на таблицу, куда записывать
#В таблице колонки: Date, chatID, chat_username, chat_title, username, firstname, lastname, message, (reply src msg), fileName, fileUrl, e
sheet_url = 'https://docs.google.com/spreadsheets/d/sdfsdhbetretr4324dsarf' #рабочий
#что-то для работы с API google
scope = ["https://spreadsheets.google.com/feeds",'https://www.googleapis.com/auth/spreadsheets',"https://www.googleapis.com/auth/drive.file","https://www.googleapis.com/auth/drive"]
#путь к файлу с API (json) гугл console.cloud.google.com
credentials = ServiceAccountCredentials.from_json_keyfile_name('/home/qr/filename.json', scope)
# Авторизация в google
gc = gspread.authorize(credentials)
Далее функция для обработки сообщения от пользователя,— проверяем, является ли оно текстом qr-кода с чека, т.е. начинается ли с t=202
Почему "t=202"
Если отсканировать qr-код c чека, то результатом будет текст типа такого:
t=20200924T1837&s=349.93&fn=9282440300682838&i=46534&fp=1273019065&n=1
Этот текст состоит из нескольких частей, разделённых знаком "&", а именно:
дата и время (ггггммддТччмм): t=20200924T1837
сумма чека: s=349.93
номер фискального накопителя (ФН): fn=9282440300682838
номер фискального документа (ФД): i=46534
значение фискального признака (ФП): fp=1273019065
тип операции (приход/возврат прихода/расход/возврат расхода): n=1
Таким образом, все qr-коды чеков до 31.12.2029 года включительно, если ничего не поменяется, будут начинаться с t=202
Является ли сообщение QR-кодом?
# Получение сообщений от юзера
@bot.message_handler(content_types=["text"])
def handle_text(message):
print (message.text) # текст сообщения
qr_code = message.text # считаем, что нам прислали расшифрованный qr код с чека
time1 = datetime.datetime.now().strftime('%d.%m.%Y') #дата и время в нужном формате для sheets для записи в таблицу
sht2 = gc.open_by_url(sheet_url_my).get_worksheet(0) #открываем гугл-таблицу
if qr_code.startswith("t=202"): #проверяем, так ли это (qr-код начинается с t=202 пример: t=20220110T1730&s=56998.00&fn=9960440301285687&i=93230&fp=3805313241&n=1
print ('Это qr-код')
Если да, то делаем запрос в ИФНС
client = NalogRuPython() # это уже для обращения к сайту ифнс
ticket = client.get_ticket(qr_code) # делаем запрос в ифнс для получения данных из чека
dictData = json.loads(json.dumps(ticket, indent=4, ensure_ascii=False)) # присваиваем переменной данные из чека
Дальше проверяем, есть ли чек в базе ИФНС (бывает не успел загрузиться или ещё что-нибудь) и если да, то парсим ответ и пишем в google sheets и телеграм. Нужно учитывать, что есть ограничение на частоту отправки сообщений в телеграм (опыт показал, что 5 секунд между сообщениями достаточно).
Проверяем чек, получаем и записываем список товаров
if "ticket" in dictData: #проверяем, есть ли в ответе информация о товарах (раздел ticket), если нет, то чек не читается в базе ИФНС
for each in dictData["ticket"]["document"]["receipt"]["items"]: # для каждого товара из чека выводим имя, количество, цену за единицу (делим её на 100, чтобы получить в руб.), сумму позиции (тоже делим)
name = str(each['name']).replace(';',',') # имя товара, меняем точку с запятой в имени на запятую
quantity = str(each['quantity']).replace('.',',') # количество с запятой вместо точки
itemprice = str((float(each['price']))/100).replace('.',',') # цена в копейках
itemsum = str((float(each['sum']))/100).replace('.',',') # сумма в копейках с запятой вместо точки
msg1 = name + ";" + itemsum + ";" + str(quantity) + ";" + itemprice + ";" + dictData["organization"]["name"] # в нужный нам вид переводим
report_line = [str(time1), message.chat.id, message.chat.username, message.chat.title, message.from_user.username, message.from_user.first_name, message.from_user.last_name, msg1, "", "", "", "", qr_code] #отправляем в sheets
sht2.append_row(report_line, table_range='A1') # записываем строку в sheets
bot.send_message(message.chat.id, name + " (колво: " + str(quantity) + ")" + ";" + itemsum + "(Цена за ед.: " + itemprice + ")") # шлем данные о позиции в чат (наименование количество сумма)
time.sleep(5) # ограничение на частоту отправки сообщений в телеграм - не более 20 сообщений в минуту в один чат (или типа того)
print ('goods were written')
Если информации о чеке нет в ИФНС, то пишем об этом в телеграм и записываем текст qr-кода чека в лист ожидания, а другой скрипт (в данной статье не привожу) дважды в день эти записи проверяет.
Если информации о чеке нет в ИФНС, то:
else: # если инфы о товарах нет, то сообщаем об ошибке
msg1 = "ошибка чека (скорее всего чек не загрузился в базу ИФНС), попробую позже и буду проверять сам 2 раза в день в течение месяца. Когда загрузится, запишу инфу в таблицу. Если не загрузится, то тоже запишу, но без списка товаров,- просто сумму (" + str((float(dictData["operation"]["sum"]))/100).replace('.',',') + ")" # в нужный нам вид переводим (инфа об ошибке + сумма чека)
bot.send_message(message.chat.id, msg1) # шлем данные об ошибке в чат (сумма чека, инфа об ошибке)
wrongList = str(time1) + ',' + qr_code + ',' + str(message.chat.id) + ',' + str(message.from_user.first_name) + ',' + str(message.from_user.last_name) #составляем список неправильных qr-кодов и данных о сообщении
with open('/home/qr/wrongList.txt', 'a') as f: #записываем список неправильных чеков в файл
f.write(wrongList + '\n') #каждый чек на новой строке (дата - qr - chatid - user - user_last_name), значения через запятую
print ('ticket error, written to the wronglist')
time.sleep(5) # ограничение на частоту отправки сообщений в телеграм - не более 20 сообщений в минуту в один чат (или типа того)
Также обрабатываем обычные сообщения (в таком случае записываем в google sheets в формате "товар;сумма", а также добавляем данные об отправителе, id чата, имени пользователя)
Если сообщение от пользователя не является qr-кодом, то записываем его в google sheets
else: #если сообщение не qr-код и не ошибка, то записываем его в гугл таблицу
report_line = [str(time1), message.chat.id, message.chat.username, message.chat.title, message.from_user.username, message.from_user.first_name, message.from_user.last_name, message.text, "", "", "", ""] #отправляем в sheets сообщение полностью, если оно не qr-код
sht2.append_row(report_line, table_range='A1') # записываем сообщение в таблицу (из переменной выше)
print ('message is not qrcode and was written to sheets')
Запускаем бота
# Запускаем бота
bot.polling(none_stop=True, interval=0)
В итоге автоматизировался рутинный процесс, а также появилась возможность анализировать стоимость и количество товаров. В моём режиме работы проверяется не более 10 чеков в день, позиций за день не больше 100. В таком режиме работает уже около двух лет, проблем не наблюдалось, кроме как в единичных случаях отсутствуют чеки в базе ИФНС, но это вероятно из-за того, что со стороны продавца чек не ушёл в базу.
В результате получаем запись в google sheets и в телеграм.
Комментарии (6)
tsg
20.09.2023 08:40А почему бы просто переписку с ФНС не парсить? Ну, или на бота сразу аккаунт оформить? Все равно что то надо с QR делать, так почему бы его сразу в приложение ФНС не сосканировать, которое расшифровку тут же пришлет в телегу? При этом если пользователь опознан, то его даже сканировать не надо, он сам туда упадет. А это половина случаев.
irishmann
Почему бы сразу не скидывать в бота фотку QR с чека и считывать его на сервере? Для пользователя это же гораздо удобнее.
v-milenin Автор
Такой функционал тоже реализован (но в статье не описан), но не могу настроить обработку изображений, чтобы получить эффективность, сопоставимую с распознаванием qr камерой телефона. Примерно в 70% случаев только срабатывает
ovegio
В таком случае у пользователя нет быстрой обратной связи, насколько удачно получилась фотография и можно ли на ней неё распознать код.
v-milenin Автор
Примерно 40 секунд с момента отправки фото до обратной связи.
Поэтому в данный момент действуем так:
если qr-код чёткий, без полосок, не мятый чек, то можно скинуть фото;
в остальных случаях считываем текст с qr и шлём текст;
если qr не читается, то скидываем фотографию служебных данных чека, потом по ним вручную в спокойном режиме вбиваем данные
Но пока остаётся проблема не 100% распознавания даже хорошего qr ботом, а знаний не хватает для её решения.