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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Предпосылки

ФНС в 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/post/588957/


Интересные статьи

Интересные статьи

В этой статье я расскажу, как мы организовали последовательное автоматическое увеличение номера версии приложения при выполнении коммита в ветку main с помощью Azure DevOps Pipeline. Мы делаем этого д...
Возможность интеграции с «1С» — это ключевое преимущество «1С-Битрикс» для всех, кто профессионально занимается продажами в интернете, особенно для масштабных интернет-магазинов.
В Челябинске проходят митапы системных администраторов Sysadminka, и на последнем из них я делал доклад о нашем решении для работы приложений на 1С-Битрикс в Kubernetes. Битрикс, Kubernetes, Сep...
Согласно многочисленным исследованиям поведения пользователей на сайте, порядка 25% посетителей покидают ресурс, если страница грузится более 4 секунд.