{
    "version": "https:\/\/jsonfeed.org\/version\/1",
    "title": "Блог об аналитике, визуализации данных, data science и BI, заметки с тегом: nalog.ru",
    "home_page_url": "http:\/\/test.leftjoin.ru\/tags\/nalog-ru\/",
    "feed_url": "http:\/\/test.leftjoin.ru\/tags\/nalog-ru\/json\/",
    "icon": "http:\/\/test.leftjoin.ru\/user\/userpic@2x.jpg",
    "author": {
        "name": "Николай Валиотти",
        "url": "http:\/\/test.leftjoin.ru\/",
        "avatar": "http:\/\/test.leftjoin.ru\/user\/userpic@2x.jpg"
    },
    "items": [
        {
            "id": "56",
            "url": "http:\/\/test.leftjoin.ru\/all\/nalog-ru-client\/",
            "title": "Пишем клиент для нового API nalog.ru",
            "content_html": "<p><b>UPD 29-09-2021<\/b>: Мы обновили клиент. Теперь проходить аутентификацию можно по номеру телефона и подтверждению по SMS. <a href=\"https:\/\/github.com\/valiotti\/get-receipts\">Репозиторий на GitHub<\/a><\/p>\n<p>Ранее в блоге мы рассказывали, как благодаря <a href=\"http:\/\/test.leftjoin.ru\/all\/sobiraem-dannye-s-chekov-gipermarketov-na-python\/\">открытому API можно собирать данные от ФНС по нашим чекам из магазинов<\/a>, обращаясь к приложению налоговой. С прошлой недели способ стал нерабочим: ФНС обновили методы приложения. Сегодня напишем свой клиент для nalog.ru, который проходит авторизацию и отправляет чеки на проверку.<\/p>\n<h2>Авторизация<\/h2>\n<p>Прежде чем начать пользоваться приложением, наш профиль необходимо авторизовать. В отличии от предыдущей версии, текущая требует прохождение капчи для авторизации по мобильному телефону — такой способ нам не подходит. Проще всего будет входить в профиль по ИНН и паролю от <a href=\"https:\/\/lkfl2.nalog.ru\/lkfl\/login\">личного кабинета<\/a> налогоплательщика. Для хранения этих данных создадим файл <span class=\"inline-code\">credentials.env<\/span>. Переменную CLIENT_SECRET зададим согласно коду: она отвечает за авторизацию приложения. А ИНН и пароль подставляем свои.<\/p>\n<pre class=\"e2-text-code\"><code>INN = \r\nPASSWORD = \r\nCLIENT_SECRET=IyvrAbKt9h\/8p6a7QPh8gpkXYQ4=<\/code><\/pre><p>Теперь создадим файл <span class=\"inline-code\">nalog_python.py<\/span>, в котором будет описан клиент. Библиотека <span class=\"inline-code\">dotenv<\/span> используется для загрузки нашего логина и пароля из .env файла.<\/p>\n<pre class=\"e2-text-code\"><code>import os\r\nimport json\r\nimport requests\r\nfrom dotenv import load_dotenv<\/code><\/pre><p>Опишем класс <span class=\"inline-code\">NalogRuPython<\/span>, и начнём с глобальных переменных класса. Здесь перечисляем headers, необходимые для отправки запроса.<\/p>\n<pre class=\"e2-text-code\"><code>class NalogRuPython:\r\n    HOST = 'irkkt-mobile.nalog.ru:8888'\r\n    DEVICE_OS = 'iOS'\r\n    CLIENT_VERSION = '2.9.0'\r\n    DEVICE_ID = '7C82010F-16CC-446B-8F66-FC4080C66521'\r\n    ACCEPT = '*\/*'\r\n    USER_AGENT = 'billchecker\/2.9.0 (iPhone; iOS 13.6; Scale\/2.00)'\r\n    ACCEPT_LANGUAGE = 'ru-RU;q=1, en-US;q=0.9'<\/code><\/pre><p>В конструкторе класса прочитаем данные из нашего окружения методом <span class=\"inline-code\">load_dotenv<\/span> и вызовем метод <span class=\"inline-code\">set_session_id<\/span> для получения <span class=\"inline-code\">__session_id<\/span>, который сейчас опишем. Идентификатор сессии необходим для отправки прочих запросов к серверу налоговой, поэтому его получаем первым делом.<\/p>\n<pre class=\"e2-text-code\"><code>def __init__(self):\r\n        load_dotenv()\r\n        self.__session_id = None\r\n        self.set_session_id()<\/code><\/pre><p>Метод set_session_id проводит авторизацию пользователя по ИНН и паролю от личного кабинета налогоплательщика и ничего не возвращает, только задаёт значение переменной <span class=\"inline-code\">__session_id<\/span>. Отправляем по указанному в глобальных переменных <span class=\"inline-code\">HOST<\/span> запрос с нашими данными от аккаунта и получаем идентификатор сессии в ответе.<\/p>\n<pre class=\"e2-text-code\"><code>def set_session_id(self) -&gt; None:\r\n        if os.getenv('CLIENT_SECRET') is None:\r\n            raise ValueError('OS environments not content &quot;CLIENT_SECRET&quot;')\r\n        if os.getenv('INN') is None:\r\n            raise ValueError('OS environments not content &quot;INN&quot;')\r\n        if os.getenv('PASSWORD') is None:\r\n            raise ValueError('OS environments not content &quot;PASSWORD&quot;')\r\n\r\n        url = f'https:\/\/{self.HOST}\/v2\/mobile\/users\/lkfl\/auth'\r\n        payload = {\r\n            'inn': os.getenv('INN'),\r\n            'client_secret': os.getenv('CLIENT_SECRET'),\r\n            'password': os.getenv('PASSWORD')\r\n        }\r\n        headers = {\r\n            'Host': self.HOST,\r\n            'Accept': self.ACCEPT,\r\n            'Device-OS': self.DEVICE_OS,\r\n            'Device-Id': self.DEVICE_ID,\r\n            'clientVersion': self.CLIENT_VERSION,\r\n            'Accept-Language': self.ACCEPT_LANGUAGE,\r\n            'User-Agent': self.USER_AGENT,\r\n        }\r\n\r\n        resp = requests.post(url, json=payload, headers=headers)\r\n        self.__session_id = resp.json()['sessionId']<\/code><\/pre><h2>Получение идентификатора чека<\/h2>\n<p>Следующий шаг — получение <span class=\"inline-code\">ticket_id<\/span>. Прежде чем отправить сам чек, необходимо получить его идентификатор для проверки. Опишем метод <span class=\"inline-code\">_get_ticket_id<\/span>, который принимает строку с расшифрованным QR-кодом чека, отправляет соответствующий запрос на сервер и возвращает идентификатор для этой строки. В headers помимо указания глобальных переменных появился также <span class=\"inline-code\">__session_id<\/span>, который требует метод <span class=\"inline-code\">\/v2\/ticket<\/span>.<\/p>\n<pre class=\"e2-text-code\"><code>def _get_ticket_id(self, qr: str) -&gt; str:\r\n        url = f'https:\/\/{self.HOST}\/v2\/ticket'\r\n        payload = {'qr': qr}\r\n        headers = {\r\n            'Host': self.HOST,\r\n            'Accept': self.ACCEPT,\r\n            'Device-OS': self.DEVICE_OS,\r\n            'Device-Id': self.DEVICE_ID,\r\n            'clientVersion': self.CLIENT_VERSION,\r\n            'Accept-Language': self.ACCEPT_LANGUAGE,\r\n            'sessionId': self.__session_id,\r\n            'User-Agent': self.USER_AGENT,\r\n        }\r\n        resp = requests.post(url, json=payload, headers=headers)\r\n        return resp.json()[&quot;id&quot;]<\/code><\/pre><h2>Расшифровка чека<\/h2>\n<p>Последний шаг — проверка чека. Формируем по <span class=\"inline-code\">ticket_id<\/span> запрос к серверу и получаем подробную расшифровку чека в ответе. На этом клиент полностью описан и готов к работе.<\/p>\n<pre class=\"e2-text-code\"><code>def get_ticket(self, qr: str) -&gt; dict:\r\n        ticket_id = self._get_ticket_id(qr)\r\n        url = f'https:\/\/{self.HOST}\/v2\/tickets\/{ticket_id}'\r\n        headers = {\r\n            'Host': self.HOST,\r\n            'sessionId': self.__session_id,\r\n            'Device-OS': self.DEVICE_OS,\r\n            'clientVersion': self.CLIENT_VERSION,\r\n            'Device-Id': self.DEVICE_ID,\r\n            'Accept': self.ACCEPT,\r\n            'User-Agent': self.USER_AGENT,\r\n            'Accept-Language': self.ACCEPT_LANGUAGE,\r\n        }\r\n        resp = requests.get(url, headers=headers)\r\n        return resp.json()<\/code><\/pre><p>Наконец, для удобства опишем пример работы клиента. Создадим экземпляр класса <span class=\"inline-code\">NalogRuPython<\/span>, зададим для примера строку QR-кода и получим расшифрованный <span class=\"inline-code\">ticket<\/span>, который затем напечатаем на экране.<\/p>\n<pre class=\"e2-text-code\"><code>if __name__ == '__main__':\r\n    client = NalogRuPython()\r\n    qr_code = &quot;t=20200727T1117&amp;s=4850.00&amp;fn=9287440300634471&amp;i=13571&amp;fp=3730902192&amp;n=1&quot;\r\n    ticket = client.get_ticket(qr_code)\r\n    print(json.dumps(ticket, indent=4))<\/code><\/pre><p>Клиент можно использовать и в своих скриптах: для этого нужно импортировать класс, создать экземпляр и, как и в примере, вызвать метод <span class=\"inline-code\">get_ticket<\/span>.<\/p>\n<pre class=\"e2-text-code\"><code>from nalog_python import NalogRuPython\r\n\r\nclient = NalogRuPython()\r\nqr_code = &quot;t=20200727T1117&amp;s=4850.00&amp;fn=9287440300634471&amp;i=13571&amp;fp=3730902192&amp;n=1&quot;\r\nticket = client.get_ticket(qr_code)<\/code><\/pre><p><a href=\"https:\/\/github.com\/valiotti\/get-receipts\">Полный код проекта на GitHub<\/a><\/p>\n",
            "date_published": "2020-08-19T12:59:28+03:00",
            "date_modified": "2021-09-29T11:25:35+03:00",
            "_date_published_rfc2822": "Wed, 19 Aug 2020 12:59:28 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "56",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css"
                ],
                "og_images": []
            }
        }
    ],
    "_e2_version": 3365,
    "_e2_ua_string": "E2 (v3365; Aegea)"
}