Обмен между 1С7.7 и Честным знаком через API. Часть 3.

В этой части пойдет речь о самом интересном – подготовке документа к отправке, подписанию документа и его отправке в Честный знак.

В предыдущих статьях были описаны: получение списка сертификатов из личного хранилища и получение токена сессии.

Функции преобразования в JSON и обратно описаны здесь, кодирование в BASE64 можно найти здесь, функции отправки запроса и получения ответа описаны в предыдущей части.

Этап 3. Подготовка и отправка документа “Вывод из оборота ОСУ”:

  1. Подготовка данных документа в формате JSON.
  2. Кодирование и подпись.
  3. Отправка данных
//****************************
// Отравляет документ на сервер ГИС МТ
// Параметры:
//		текДок 	- отправляемый документ
//		ПараметрыСоединения	- список значений с параметрами соединения {АдресСервера, ТокенПодключения, ОтпечатокСертификата}
// Возвращаемое значение:
//		uuid - уникальный идентификатор документа в ГИС МТ или 0, если при отправке произошла ошибка
Функция ОтправитьДокумент(текДок, ПараметрыСоединения)
    Перем URL, сзJSONЗапрос, КлючСессии, сзЗаголовки, НеОбр429;
    Перем ОтветСервера, ОтветСервераСЗ;
    Перем ТекстСообщения;
    
    URL = ПараметрыСоединения.Получить("АдресСервера") + "/v3/true-api";
    URL = URL + "/lk/documents/create?pg=" + СокрЛП(ПолучитьТоварнуюГруппуГИСМТ(текДок.ОсобенностьУчетаНоменклатуры));
    КлючСессии = ПараметрыСоединения.Получить("ТокенПодключения");
    
    сзJSONЗапрос = ОтправитьДокумент_ПолучитьДанныеДляОтправки(текДок, ПараметрыСоединения);
    Если сзJSONЗапрос = 0 Тогда
        ТекстСообщения = "Не удалось сформировать данные документа для выгруки: " + СокрЛП(текДок);
        Сообщить(ТекстСообщения, "!!");
        Возврат 0;
    КонецЕсли;
    
    ОтветСервера = HTPP_ВыполнитьЗапрос("POST", URL, сзJSONЗапрос, КлючСессии, сзЗаголовки, НеОбр429);
    Если ПустоеЗначение(ОтветСервера) = 0 Тогда
        Если Лев(ОтветСервера, 1) = "{" Тогда
            ОтветСервераСЗ = JSONВСписок(ОтветСервера);
            ТекстСообщения = "Не удалось отправить документ на сервер ГИС МТ! " + СокрЛП(ОтветСервераСЗ.Получить("error_message"));
            Сообщить(ТекстСообщения, "!!");
            Результат = 0;
        Иначе
            Результат = ОтветСервера;
        КонецЕсли;
    Иначе
        Результат = 0;
    КонецЕсли;
    
    Возврат Результат;
    
КонецФункции

//****************************
// Функция возвращает код или наименование товарной группы особенности учета в ГИС МТ
// Параметры:
//		ОсобенностьУчета - значение перечисления "ОсобенностиУчетаНоменклатуры"
//		КодНаименование - вернуть код (0) или наименование (1) товарной группы
// Возвращаемое значение:
//		Число или строка (код или наименование) товарной группы в справочнике ГИС МТ
Функция ПолучитьТоварнуюГруппуГИСМТ(ОсобенностьУчета, КодНаименование = 0)
    
    Результат = 0;
    
    Если ОсобенностьУчета = Перечисление.ОсобенностиУчетаНоменклатуры.МолочнаяПродукция Тогда
        Результат = ?(КодНаименование = 0, 8, "milk");
    ИначеЕсли ОсобенностьУчета = Перечисление.ОсобенностиУчетаНоменклатуры.УпакованнаяВода Тогда
        Результат = ?(КодНаименование = 0, 13, "water");
    КонецЕсли;
    
    Возврат Результат;
    
КонецФункции

//****************************
// Получает данные для передачи на сервер ГИС МТ
// Параметры:
//		текДок 	- отправляемый документ, ссылка на справочник "ЖурналОбменаГИСМТ"
//		ПараметрыСоединения	- список значений с параметрами соединения
// Возвращаемое значение:
//		Список значений с данными для отправки или 0
Функция ОтправитьДокумент_ПолучитьДанныеДляОтправки(текДок, ПараметрыСоединения)
    Перем ТекстСообщения;
    Перем ДанныеДокументаСЗ, ДанныеДокументаJSON, ДанныеДокументаJSON_Base64, Сигнатура, Результат;
    Перем тмпТекст, ИмяФайлаДокумента, ИмяФайлаДокументаПодписанного;
    
    Результат = 1;
    
    Если текДок.ТипДокументаГИСМТ = Перечисление.ТипыДокументовГИСМТ.LK_GTIN_RECEIPT Тогда
        ДанныеДокументаСЗ = ОтправитьДокумент_ПолучитьДанныеДляОтправки_ВыводИзОборотаОСУ(текДок);
    Иначе
        ТекстСообщения = "Непредвиденный тип документа ГИС МТ!";
        Сообщить(ТекстСообщения, "!!");
        Результат = 0;
    КонецЕсли;
    
    Если Результат = 1 Тогда
        ИмяФайлаДокумента 		= КаталогВременныхФайлов() + "senddoc.txt";
        ИмяФайлаДокументаПодписанного 	= КаталогВременныхФайлов() + "senddoc_s.txt";
        
        ФС.УдалитьФайл(ИмяФайлаДокумента);
        ФС.УдалитьФайл(ИмяФайлаДокументаПодписанного);
        
        ДанныеДокументаJSON = ЗначениеВJSON(ДанныеДокументаСЗ);
        ДанныеДокументаJSON_Base64 = КодироватьBase64(ДанныеДокументаJSON);
        Попытка
            тмпТекст = СоздатьОбъект("Текст");
            тмпТекст.ДобавитьСтроку(ДанныеДокументаJSON_Base64);
            тмпТекст.Записать(ИмяФайлаДокумента);
        Исключение
            Результат = 0;
        КонецПопытки;
    КонецЕсли;
    
    Если Результат = 1 Тогда
        Результат = Подписать_ПодписатьТелоДокумента(ИмяФайлаДокумента, ИмяФайлаДокументаПодписанного, ПараметрыСоединения);
    КонецЕсли;
    
    Если Результат = 0 Тогда
        ТекстСообщения = "Не удалось сформировать и подписать данные документа для отправки!";
        Сообщить(ТекстСообщения, "!!");
    КонецЕсли;
    
    Если Результат = 1 Тогда
        Результат = СоздатьОбъект("СписокЗначений");
        Результат.Установить("document_format", "MANUAL");
        Результат.Установить("type", СокрЛП(текДок.ТипДокументаГИСМТ.Идентификатор()));
        Результат.Установить("product_document", РаботаСФайлами_ПолучитьТекстФайлаКакСтроку(ИмяФайлаДокумента));
        Результат.Установить("signature", РаботаСФайлами_ПолучитьТекстФайлаКакСтроку(ИмяФайлаДокументаПодписанного));
    КонецЕсли;
    
    Возврат Результат;
    
КонецФункции

//****************************
// Получает данные табличной части документа типа "Вывод из оборота ОСУ"
// Параметры:
//		текДок	- отправляемый документ
// Возвращаемое значение:
//		Таблица значений или 0 (в случае ошибки)
Функция ОтправитьДокумент_ПолучитьДанныеДляОтправки_ТЧОСУ(текДок)
    
    ДанныеТЧ = СоздатьОбъект("ТаблицаЗначений");
    ДанныеТЧ.НоваяКолонка("gtin");
    ДанныеТЧ.НоваяКолонка("gtin_quantity");

    ДанныеТЧ.НоваяСтрока();
    ДанныеТЧ.gtin = "01234567891234";
    ДанныеТЧ.gtin_quantity = 10;

    Возврат ДанныеТЧ;
    
КонецФункции

//****************************
// Получает данные документа типа "Вывод из оборота ОСУ"
// Параметры:
//		текДок 	- отправляемый документ
// Возвращаемое значение:
//		Список значений с данными для отправки или 0
Функция ОтправитьДокумент_ПолучитьДанныеДляОтправки_ВыводИзОборотаОСУ(текДок)
    Перем ДанныеТЧ;
    
    ДанныеТЧ = ОтправитьДокумент_ПолучитьДанныеДляОтправки_ТЧОСУ(текДок);
    Если ДанныеТЧ = 0 Тогда
        Возврат 0;
    КонецЕсли;
    
    Результат = СоздатьОбъект("СписокЗначений");
    
    Результат.Установить("inn", "1234567890");
    //Результат.Установить("buyer_inn", "");
    Результат.Установить("action", "OWN_USE"); // собственные нужды
    //Результат.Установить("withdrawal_type_other", "");
    Результат.Установить("action_date", текДок.ДатаДок);
    Результат.Установить("document_type", "OTHER");
    Результат.Установить("document_number", текДок.НомерДок);
    Результат.Установить("document_date", текДок.ДатаДок);
    Результат.Установить("primary_document_custom_name", "Sale document");
    //Результат.Установить("fias_id", "");
    Результат.Установить("products", ДанныеТЧ);
    
    Возврат Результат;
    
КонецФункции

Функция РаботаСФайлами_ПолучитьТекстФайлаКакСтроку(ИмяФайла, БезБОМ = 0)
    Перем ВсегоСтрок, Счетчик;
    
    Результат = "";
    
    Попытка
        тмпТекст = СоздатьОбъект("Текст");
        тмпТекст.Открыть(ИмяФайла);
        ВсегоСтрок = тмпТекст.КоличествоСтрок();
        Для Счетчик = 1 по ВсегоСтрок Цикл
            текСтрока = тмпТекст.ПолучитьСтроку(Счетчик);
            Если (Счетчик = 1) И (БезБОМ = 1) Тогда
                текСтрока = Сред(текСтрока, 4);
            КонецЕсли;
            Результат = Результат + текСтрока;
        КонецЦикла;
    Исключение
        //;
    КонецПопытки;
    
    Возврат Результат;
    
КонецФункции

Здесь немного прервусь, чтобы добавить комментарий к коду. Некорректная связка команд не позволяет отправлять в Честный знак наименование первичного документа, написанного кириллицей, т.к. Честный знак принимает кодировку UTF-8, а 1С7.7 работает только с windows-1251.

ДанныеДокументаJSON = ЗначениеВJSON(ДанныеДокументаСЗ);
ДанныеДокументаJSON_Base64 = КодироватьBase64(ДанныеДокументаJSON);

В идеале, здесь надо получить данные документа JSON, записать их в файл, после чего с помощью javascript сменить кодировку на utf-8 и только после этого также, с помощью javascript закодировать в BASE64. И только после этого в 1С получившийся файл можно открыть и подписать. Но это на будущее. А пока передадим наименование документа латиницей. Честный знак это не запрещает.

Ну а теперь самое интересное. Подписание.

//****************************
// Вызывает jscript для подписи текста
// параметры подписи: "Данные сохраняются в виде чистой двоичной последовательности"
//		93 - Тип подписи CAdES-X Long Type 1
//		true - отделенная подпись
//		1 - Данные сохраняются в виде чистой двоичной последовательности
// Параметры:
//		ТекстДляПодписи 	- строка, текст для подписи
//		ИмяФайлаРезультат	- строка, имя файла, в который записать подписанные данные
//		oSigner				- объект "CAdESCOM.CPSigner"
//		oSignedData			- объект "CAdESCOM.CadesSignedData"
// Возвращаемое значение:
//		 0 (в случае ошибки) или 1
Функция Подписать_ПодписатьТелоДокументаСкрипт(ТекстДляПодписи, ИмяФайлаРезультат, oSigner, oSignedData)

    Попытка
        JS = СоздатьОбъект("MSScriptControl.ScriptControl");
        JS.Language = "jscript";
        JS.Timeout = -1;
    Исключение
        ТекстСообщения = "Не удалось создать объект MSScriptControl.ScriptControl! " + ОписаниеОшибки();
        Сообщить(ТекстСообщения, "!!");
        Возврат 0;
    КонецПопытки;
    
    ТекстСкрипта="
    |function SignText(TextForSign,OutFileName,oSigner,oSignedData)
    |{
    | oSignedData.Content = TextForSign;
    | OutSignedData = oSignedData.SignCades(oSigner, 93, true, 0);
    |
    | OutStream=new ActiveXObject(""ADODB.Stream"");
    | OutStream.CharSet=""utf-8"";
    | OutStream.Type=2; // text data
    | OutStream.Mode=3; // read/write
    | OutStream.Open();
    | OutStream.WriteText(OutSignedData);
    | OutStream.SaveToFile(OutFileName,2);
    | OutStream.Close();
    |
    | return(1);
    |}
    |";
        
    Попытка
        JS.AddCode(ТекстСкрипта);
        Результат = JS.Modules("Global").CodeObject.SignText(ТекстДляПодписи, ИмяФайлаРезультат, oSigner, oSignedData);
    Исключение
        ТекстСообщения = "Произошла ошибка при подписи файла! " + ОписаниеОшибки();
        Сообщить(ТекстСообщения, "!!");
        Возврат 0;
    КонецПопытки;
    
    Возврат Результат;
    
КонецФункции

Функция Подпись_ПолучитьСертификатПоОтпечаткуСкрипт(ОтпечатокСертификата)

    Попытка
        JS = СоздатьОбъект("MSScriptControl.ScriptControl");
        JS.Language="jscript";
        JS.Timeout=-1;
    Исключение
        ТекстСообщения = "Не удалось создать объект MSScriptControl.ScriptControl! " + ОписаниеОшибки();
        Сообщить(ТекстСообщения, "!!");
        Возврат 0;
    КонецПопытки;
        
    ТекстСкрипта="
    |function FindSert(Cert)
    |{
    | 
    |oStore = new ActiveXObject(""CAdESCOM.Store"");
    |oStore.Open(2, ""My"", 0);
    |Certificates = oStore.Certificates.Find(0, Cert);
    |
    | return(Certificates.Item(1));
    |}
    |";
        
    Попытка
        JS.AddCode(ТекстСкрипта);
        Рез = JS.Modules("Global").CodeObject.FindSert(ОтпечатокСертификата);
    Исключение
        ТекстСообщения = "Произошла ошибка при подписи файла! " + ОписаниеОшибки();
        Сообщить(ТекстСообщения, "!!");
        Возврат 0;
    КонецПопытки;
    
    Возврат Рез;
    
КонецФункции

//****************************
// Подписывает тело документа для отправки в ГИС МТ
// Параметры:
//		ИмяФайлаВходящего 	- строка, имя файла, в котором записан JSON передаваемого документа перекодированный в BASE64
//		ИмяФайлаРезультат 	- строка, имя файла, в который будет сохранен подписанный документ
//		ПараметрыСоединения	- список знаений с параметрами соединения
// Возвращаемое значение:
//		 0 или 1
Функция Подписать_ПодписатьТелоДокумента(ИмяФайлаВходящего, ИмяФайлаРезультат, ПараметрыСоединения)
    Перем Сертификат, oSigner, oSignedData;
    Перем ТекстДляПодписи;
    
    Сертификат = Подпись_ПолучитьСертификатПоОтпечаткуСкрипт(ПараметрыСоединения.Получить("ОтпечатокСертификата"));
    
    ТекстДляПодписи = РаботаСФайлами_ПолучитьТекстФайлаКакСтроку(ИмяФайлаВходящего);
    
    oSigner = СоздатьОбъект("CAdESCOM.CPSigner");
    oSigner.Certificate = Сертификат;
    oSigner.TSAAddress = "http://tax4.tensor.ru/tsp-tensor_gost2012/tsp.srf";
    
    oSignedData = СоздатьОбъект("CAdESCOM.CadesSignedData");
    oSignedData.ContentEncoding = 1; //CADESCOM_BASE64_TO_BINARY = 0x01	Данные будут перекодированы из Base64 в бинарный массив.
    
    Результат = Подписать_ПодписатьТелоДокументаСкрипт(ТекстДляПодписи, ИмяФайлаРезультат, oSigner, oSignedData);
    
    Возврат Результат;
    
КонецФункции

В качестве комментария, почему сделаны такие переходы между кодом 1С и javascript, скажу так – не понял, толи это глюки установки, толи глюки платформы, но было две загвоздки:

  1. При получении сертификата с через объект “CAdESCOM.Store” средствами 1С, вылетала ошибка “метод Open не найден”, а в скрипте работает корректно
  2. При установке свойства oSignedData.ContentEncoding = 1 в скрипте javascript, скрипт прерывался по ошибке установки неверного значения, а объект 1С нормально принимает значение.

В итоге, после такого разделения все взлетело и работает.

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

Если вы заметили ошибку, неточность или у вас появились вопросы, пишите в комментариях, я обязательно вам отвечу и приму все ваши замечания.

Author: admin

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