Для рабочих целей есть потребность учитывать совершённые за наличные расходы. Раньше это делалось вручную - вписывалось наименование покупки и её цена в телеграм-чатик, потом вручную переносилось в google sheets. Потом перенос в google sheets автоматизировался с помощью скрипта python и google-api, но т.к. товаров в чеке могло быть много, поэтому список из 10 (например) позиций сокращался до какой-то общей типа "инструменты" (например) с указанием общей суммы чека, что не особо годилось для возможной дальнейшей аналитики. Как следующий этап развития, возникла идея получать данные о товарах с помощью qr-кода и API ИФНС.

В наличии имеется постоянно подключенный к сети одноплатный компьютер Raspberry PI с установленным FreeBPX (настроено по этой статье) с модемом E1550, перепрошитым на голосовые функции. Соответственно, все настройки выполнялись на этом комплекте.

В моём случае Raspberry и модем уже настроены и работают несколько лет, могут звонить и получать СМС. Дальнейшая задача - получить список товаров из чека.

ИФНС позволяет это сделать двумя путями: 1) использовать API-ключ (и тогда теоретически можно обойтись без модема); 2) проходить авторизацию по СМС.

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

Принцип авторизации следующий: отправляем запрос на сайт налоговой, в котором указываем номер телефона, в ответ на номер телефона приходит смс для авторизации, после чего отправляем запрос с данными чека и кодом из смс, в ответ на который получаем JSON с данными из чека, если чека находится в базе ИФНС, либо информацию с ошибкой, если чека нет.

Принцип работы моей схемы:

  1. Пользователь сканирует qr-код с чека и отправляет текст qr-кода в телеграм-чат;

  2. Телеграм-бот получает текст сообщения и запускает скрипт Python, который проверяет, является ли сообщение QR-кодом и, если да, то делает запрос в налоговую, проходя авторизацию по СМС, получает список товаров и возвращает его попозиционно в телеграм, а также записывает в google sheets;

  3. Если сообщение не является 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 и в телеграм.
"Сырые" данные в sheets
"Сырые" данные в sheets
Данные в sheets после разбивки сообщения на колонки
Данные в sheets после разбивки сообщения на колонки
Данные в Телеграме
Данные в Телеграме

Комментарии (6)


  1. irishmann
    20.09.2023 08:40
    +2

    Почему бы сразу не скидывать в бота фотку QR с чека и считывать его на сервере? Для пользователя это же гораздо удобнее.


    1. v-milenin Автор
      20.09.2023 08:40

      Такой функционал тоже реализован (но в статье не описан), но не могу настроить обработку изображений, чтобы получить эффективность, сопоставимую с распознаванием qr камерой телефона. Примерно в 70% случаев только срабатывает


    1. ovegio
      20.09.2023 08:40

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


      1. v-milenin Автор
        20.09.2023 08:40

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

        Но пока остаётся проблема не 100% распознавания даже хорошего qr ботом, а знаний не хватает для её решения.


  1. tsg
    20.09.2023 08:40

    А почему бы просто переписку с ФНС не парсить? Ну, или на бота сразу аккаунт оформить? Все равно что то надо с QR делать, так почему бы его сразу в приложение ФНС не сосканировать, которое расшифровку тут же пришлет в телегу? При этом если пользователь опознан, то его даже сканировать не надо, он сам туда упадет. А это половина случаев.


  1. aMster1
    20.09.2023 08:40

    А просто чек распознать? Буквы-цифры?