Автоматическое принятие приглашений к обмену документами в ЭДО Диадок по API

от автора

Предпосылки

ФНС в 2020 году утвердила концепцию перехода документооборота с контрагентами в электронный вид. В июле 2021 года обмен первичными документами и счетами-фактурами по закупке и продаже некоторых товаров уже стал безальтернативно электронным.

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

Операторы ЭДО предоставляют модули для интеграции с 1С, SAP и прочими популярными ERP. Коробочные инструменты не позволят добиться максимума автоматизации без затрат на их доработку. Операторы предлагают хорошую альтернативу — возможность интеграции по API, за сравнительно небольшие деньги.

Пример использования API

Для возможности отправить и принять документы контрагенты должны обменяться приглашениями. Приглашение можно отправить любому юр.лицу по ключу ИНН-КПП, поэтому они могут поступать ежедневно от любых юридических лиц. Приглашение, которое не содержит доп.соглашения или договора об ЭДО между лицами, не порождает никаких правовых обязательств у сторон, поэтому его можно смело принимать. Но желательно ограничить список теми, с кем есть нерасторгнутый договор и активно ведутся сделки, и поставщик не в списке ненадежных.

Описание ниже не претендует на оценку качества кода, соблюдения правил. Код просто работает. Назначение — демонстрация возможностей лицам, принимающим решения (бизнесу, руководству), тестирование. То есть для стадии «минимально жизнеспособный продукт» (MVP).

Инструменты: Python, PyCharm Community Edition, SQL Server Express Edition, SSMS, ключ разработчика API (купить/получить у оператора ЭДО), описание методов и структур данных API оператора ЭДО. Все бесплатное, кроме ключа API.

Общий алгоритм:

  1. Python: заходит в кабинет вашей организации (или нескольких в цикле), забирает все приглашения, помещает в таблицу на сервере

  2. SQL server: связывает таблицу приглашений с таблицей оборотов (можно добавить доп.фильтры — договоры, список ненадежных) по ключу ИНН, строит список ИНН с ненулевыми оборотами за последний период

  3. Python: забирает таблицу ИНН, заходит в кабинет ЭДО, принимает приглашения, заносит в таблицу на сервере записи о принятых приглашениях. Опционально (не описано в этой статье) — отправляет списки принятых приглашений ответственным сотрудникам.

  4. Планировщик задач Windows: запускает программу, выполняющую три пункта выше, регулярно

Создаем базу данных на сервере с таблицами:

CREATE TABLE [dbo].[tbl_organizations]( [OrgGUID] [char](36) NULL, [OrgID] [char](36) NULL, [Inn] [char](12) NULL, [Kpp] [char](9) NULL, [Full_name] [nvarchar](max) NULL, [Short_name] [nvarchar](max) NULL, [BoxID] [varchar](100) NULL, [BoxGUID] [char](36) NULL, [Box_title] [nvarchar](max) NULL, [Invoice_format_ver] [char](20) NULL, [Ogrn] [char](15) NULL, [FNS_participant] [char](50) NULL ) ON [PRIMARY] GO  CREATE TABLE [dbo].[tbl_departments]( [DepartmentID] [char](36) NULL, [Parent_dept_ID] [char](36) NULL, [Dept_name] [nvarchar](max) NULL, [Dept_abbr] [nvarchar](50) NULL, [OrgID] [char](36) NULL ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] GO  CREATE TABLE [dbo].[tbl_invitations]( [Short_name] [nvarchar](255) NULL, [OrgID_our] [char](36) NULL, [Inn] [char](12) NULL, [OrgID] [char](36) NULL ) ON [PRIMARY] GO  CREATE TABLE [dbo].[tbl_invitations_accepted]( [OrgId] [char](36) NULL, [Record_date] [date] NULL, [Inn] [char](12) NULL, [OrgId_our] [char](36) NULL ) ON [PRIMARY] GO

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

Предполагается, что на сервере уже есть таблица или представление (view) с оборотами поставщиков по ключу ИНН или ИНН-КПП, или активными договорами. Если нет, её можно периодически заливать на сервер полуавтоматом из любой системы — вывод в эксель из 1С или SAP и загрузка утилитой bcp, импортом в SSMS или программой.

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

import pprint import requests import json import pyodbc import datetime import sys  #заводим файл лога для перенаправления вывода в файл f_name = r'C:\Temp\log_' + datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + '.txt' stdoutOrigin = sys.stdout sys.stdout = open(f_name, "w")  try:     conn = pyodbc.connect(r'Driver={SQL Server};                           SERVER=……;Database=diadoc_test;                           Trusted_Connection=yes')     cursor = conn.cursor() except:     ctypes.windll.user32.MessageBoxW(0,                                       "Нет прав на работу с БД"                                      , "Ошибка доступа", 1)     print(datetime.datetime.now()           .strftime('%Y-%m-%d_%H-%M-%S') + ' - не получилось соединиться с БД')     sys.stdout.close()     sys.stdout = stdoutOrigin     exit()  log_pass = {'login': '……', 'password': '…'} pass_json = json.dumps(log_pass) # ниже вместо точек указываем полученный от оператора ключ auth_header_key = "DiadocAuth ddauth_api_client_id=..."  # авторизуемся для получения токена доступа r = requests.request(method='POST'                      , url='https://diadoc-api.kontur.ru/V3/Authenticate',                      headers={"Authorization": auth_header,                               "Content-Length": "1252",                               "Connection": "Keep-Alive",                               "Content-Type": "application/json"},                      params={"type": "password"},                      data=pass_json) # добавляем полученный токен к заголовку авторизации auth_header = auth_header_key + ",ddauth_token=" + r.text print(datetime.datetime.now().__str__() + ' - авторизовался'  # обновляем реестр организаций и подразделений в базе данных r1 = requests.request(method='GET'                       , url='https://diadoc-api.kontur.ru/GetMyOrganizations'                       ,headers={"Authorization": auth_header,                               "Content-Length": "1252",                               "Connection": "Keep-Alive",                               "Content-Type": "application/json",                               "Accept": "application/json"},                      params={"type": "password"},                      data=pass_json) r1 = r1.json() print(datetime.datetime.now().__str__() + ' - получил список организаций')  conn.cursor().execute('truncate table diadoc_test.dbo.tbl_organizations')       .commit() print(datetime.datetime.now().__str__() + ' - очистил таблицу организаций')  conn.cursor().execute('truncate table diadoc_test.dbo.tbl_departments').commit() print(datetime.datetime.now().__str__() + ' - очистил таблицу подразделений')  for i in range(len(r1['Organizations'])):     if not r1['Organizations'][i]['IsTest']:         q_str = 'INSERT INTO diadoc_test.dbo.tbl_organizations VALUES(' + \             "'" + r1['Organizations'][i]['OrgIdGuid'] + \             "', '" + r1['Organizations'][i]['OrgId'] + "', '" + \             r1['Organizations'][i]['Inn'] + "', '" + \             r1['Organizations'][i]['Kpp'] + "', '" + \             r1['Organizations'][i]['FullName'] + "', '" + \             r1['Organizations'][i]['ShortName'] + "', '" + \             r1['Organizations'][i]['Boxes'][0]['BoxId'] + "', '" + \             r1['Organizations'][i]['Boxes'][0]['BoxIdGuid'] + "', '" + \             r1['Organizations'][i]['Boxes'][0]['Title'] + "', '" + \             r1['Organizations'][i]['Boxes'][0]['InvoiceFormatVersion'] + \             "', '" + r1['Organizations'][i]['Ogrn'] + "', '" + \             r1['Organizations'][i]['FnsParticipantId'] + "')"         conn.cursor().execute(q_str).commit()         print(datetime.datetime.now().__str__() + ' - ' + q_str)     for j in range(len(r1['Organizations'][i]['Departments'])):         if not r1['Organizations'][i]['Departments'][j]['IsDisabled']:             buff = r1['Organizations'][i]['Departments'][j]             q_str = 'INSERT INTO diadoc_test.dbo.tbl_departments VALUES(' + \                 "'" + buff['DepartmentId'] + "', '" + \                 buff['ParentDepartmentId'] + "', '" + \                 buff['Name'] + "', '" + buff['Abbreviation'] + "', '" + \                 r1['Organizations'][i]['OrgId'] + "')"             conn.cursor().execute(q_str).commit()             print(datetime.datetime.now().__str__() + ' - ' + q_str)  # обновляем реестр приглашений conn.cursor().execute('truncate table diadoc_test.dbo.tbl_invitations').commit() print(datetime.datetime.now().__str__() + ' - очистил таблицу приглашений')  orgs = conn.cursor().execute('select distinct OrgId from dbo.tbl_organizations').fetchall() orgs_list = list(orgs[x][0] for x in range(len(orgs))) print(datetime.datetime.now().__str__()        + ' - построил список ЮЛ, записей - ' + str(len(orgs_list)))  for org in orgs_list:     after_index = -1     counter = 0     eol = False     while not eol:         print(org)         r1 = requests.request(method='GET'                         , url='https://diadoc-api.kontur.ru/V2/GetCounteragents',                         headers={"Authorization": auth_header,                                  "Content-Length": "1252",                                  "Connection": "Keep-Alive",                                  "Content-Type": "application/json",                                  "Accept": "application/json"},                         params={"myOrgId": org,                                 "afterIndexKey": after_index,                                 "counteragentStatus": "InvitesMe"},                         data=pass_json)         if r1.text[:8] != 'Доступ з':             r1 = r1.json()             for i in range(len(r1['Counteragents'])):                 buff = r1['Counteragents'][i]['Organization']                 q_str = 'INSERT INTO diadoc_test.dbo.tbl_invitations ' + \                         'VALUES(' + "'" + buff['ShortName'] + "', '" + \                         org + "', '" + buff['Inn'] + "', '" + \                         buff['OrgId'] + "')"                 conn.cursor().execute(q_str).commit()                 print(datetime.datetime.now().__str__() + ' - ' + q_str)             if len(r1['Counteragents']) < 99:                 eol = True             else:                 # максимум 100 записей, запоминаем индекс, передаем в сл. запрос                 after_index =                      r1['Counteragents'][len(r1['Counteragents'])-1]['IndexKey']         else:             print(datetime.datetime.now().__str__() + ' - ' +                   org + ' - ' + r1.text)             eol = True  # забираем с сервера список ИНН, OrgID, очищенный от неизвестных лиц, # и принимаем приглашения  a = conn.cursor().execute(   'select distinct OrId_our, OrgID, ИНН from dbo.view_Приглашения_от_моих_КА'    ).fetchall() ka_list = list([a[x][0], a[x][1], a[x][2]] for x in range(len(a)))  print(datetime.datetime.now().__str__() +        ' - построил список поставщиков по ЮЛ, записей - ' + str(len(ka_list)))  for i in range(len(ka_list)):     r1 = requests.request(method='POST'                           , url='https://diadoc-api.kontur.ru/V2/AcquireCounteragent'                           , headers={"Authorization": auth_header,                                    "Content-Length": "1252",                                    "Connection": "Keep-Alive",                                    "Content-Type": "application/json",                                    "Accept": "application/json"},                           params={"myOrgId": ka_list[i][0]},                           data=json.dumps({"OrgId": ka_list[i][1]}))     #если приглашение требует подписи - оно не примется, просто заносим в лог     if r1.text[0:6] == 'Cannot':         print(r1.text)     else:     #если приглашение "простое" - оно принято, вносим в таблицу на сервере         conn.cursor().execute('INSERT INTO dbo.tbl_invitations_accepted' +                               'VALUES (' + "'" + ka_list[i][1] + "', '" +                               datetime.datetime.now().strftime('%Y-%m-%d') +                               "', '" + ka_list[i][2] + "', '" +                                ka_list[i][1] + "')").commit()         print(datetime.datetime.now().__str__() +                ' - принято приглашение от ' + ka_list[i][2]) print(datetime.datetime.now().__str__() + ' - задача завершена')  #закрываем файл лога sys.stdout.close() sys.stdout = stdoutOrigin 

С помощью команды pyinstaller код собирается в исполняемый файл .exe, создаем задачу в планировщике задач Windows, в «Действиях» выбираем .exe-файл, на вкладке «Триггеры» добавляем периоды срабатывания по расписанию.

Результат:

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


ссылка на оригинал статьи https://habr.com/ru/articles/588957/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *