Каталог решений - Работа с кассой Atol через веб-сервер ДТО-10

Работа с кассой Atol через веб-сервер ДТО-10

Работа с кассой Atol через веб-сервер ДТО-10

В наличии

Поддержка многопользовательской печати на одном устройстве ККТ. Поддержка изменений в законодательстве (Переход на ФФД 1.05 и НДС 20%).

Категория:

Описание

Предыстория

Так сложилось что работа с кассой в среде 1С строиться через внешнюю компоненту.
Одна из проблем, с которой столкнулись мы – «зависание» com-порта для работы с кассой завершенным сеансом 1С при печати чеков на 1 ККМ с нескольких компьютеров. Также нам не нравилась установка драйвера ДТО на каждом компьютере, с которого требовалось обращение к кассе. 

Ранее использовали службу fdsvc на компьютере, где подключена касса, устанавливатли драйвер на каждом ПК для обеспечения общения с службой. Так же необходимо было зарегистрировать на каждом компьютере библиотеку драйвера Атол FprnM1C.dll, если же dll в новой версии драйвера с тем же именем, то предварительно необходимо почистить временные файлы. Создавался COM объект, в него передавались данные в зависимости от операции, при этом соединение с кассой держалось все время пока выполнялась операция, а так же код выполнялся в синхронном режиме, ожидая выполнения каждой операции.

Что сделали:

Компания Атол выпустила новую версию драйвера, которая поддерживает работу с кассой через HTTP-запросы к веб-серверу Атол. Существует публикация, реализующая механизм работы с этой версией драйвера, но она нас не устраивала закрытостью кода и мы решили реализовать свой механизм.

 

Порядок действий:

1.Установка и настройка сервера от Atol

Скачиваем последний ДТО 10 с сайта Atol

Для работы Web-сервера требуется установленная Java версии 1.8 и выше (х32).

В момент установки отмечаем, что необходимо установить Web-сервер (данное расширение присутствует только в 32-х битном драйвере)

После установки по адресу http://hostname:16732/settings производим настройки web-сервера

Настраиваем параметры подключения

Включаем web-сервер

После перезагрузки необходимо перезапустить службу

2. Взаимодействие с web-сервером

Для обращения к кассе нам необходимо хранить ip-адрес и порт по которому происходит взаимодействие с кассой

В справочнике кассы добавили реквизиты АдресВебСервера и ПортВебСервера

Алгоритм работы

Для добавления задания в очередь на выполнения необходимо отправить его POST-запросом на адрес http://hostname:16732/requests, указав его уникальный идентификатор. В ответ сервер вернет код результата в виде HTTP-статуса.

(Отправляем запрос на регистрацию задания в очереди печати, когда необходимо распечатать чек)

Для того, чтобы узнать результат задания, необходимо отправить GET-запрос на адрес http://hostname:16732/requests/. В ответ вернется JSON, содержащий в себе статусы задания и его результаты.

(Тут сложнее, необходимо запрашивать статусы задач, для этого мы должны организовать хранение отправленных заданий и проверять по ним ответы, мы организовали хранение через регистр сведений, ключом выступил Объект (любая ссылка) – так как необходимо было контролировать уникальность документов оплаты отправленных на печать, и обеспечить повторную отправку печати в случае неуспеха предыдущего задания)

Для отмены задания, которое еще не начало обрабатываться, необходимо отправить DELETE-запрос на адрес http://hostname:16732/requests/. Нельзя отменить задание, которое выполняется в данный момент.

 

Примеры кода

Для выполнения операции была реализована функция выполнения команды, в качестве входных параметров:

Касса на которой необходимо произвести печать

Операция выполняемая в данный момент

Дополнительные параметры для проведения определенной операции

Реализовали функция для постановки в очередь заданий

// Функция - Выполнить операцию

//

// Параметры:

//  Касса - Справочник.КассыККМ - касса на которой необходимо произвести операцию

//  Операция - Строка - Реализованы "ОтчетБезГашения", "ЗакрытиеСмены" и "ФискальныйЧек"

//  ДополнительныеПараметры - Структура - необходимые параметры для выполнения операций 

// 

// Возвращаемое значение:

//  Ответ - Строка - сообщение о результате выполнения операции

Функция ВыполнитьОперацию(Касса,Операция,ДополнительныеПараметры) Экспорт



HTTPЗапрос = Новый HTTPЗапрос();

HTTPЗапрос.АдресРесурса = "/requests";

HTTPЗапрос.Заголовки.Вставить("Content-Type", "application/json");



Если Операция = "ОтчетБезГашения" Тогда

СтруктураJSON = СформироватьСтруктуруДляОтчетаБезГашения(Касса,Операция,ДополнительныеПараметры);

ИначеЕсли Операция = "ЗакрытиеСмены" Тогда

СтруктураJSON = СформироватьСтруктуруДляЗакрытияСмены(Касса,Операция,ДополнительныеПараметры);

ИначеЕсли Операция = "ФискальныйЧек" Тогда

СтруктураJSON = СформироватьСтруктуруДляФискальногоЧека(Касса,Операция,ДополнительныеПараметры);

Иначе

Возврат "Ошибка выполнения операции! " + "Операция " + Операция + " не реализована!";

КонецЕсли;



Если СтруктураJSON = Неопределено Тогда

Возврат "Не удалось зарегистрировать в очередь!, ошибка формирования менеджера задания (JSON)";

КонецЕсли;



ЗаписьJSON = новый ЗаписьJSON;

ЗаписьJSON.УстановитьСтроку();

ЗаписатьJSON(ЗаписьJSON,СтруктураJSON);

СтрокаЗапросаJS = ЗаписьJSON.Закрыть();

HTTPЗапрос.УстановитьТелоИзСтроки(СтрокаЗапросаJS,КодировкаТекста.UTF8);



Попытка

Соединение = Новый HTTPСоединение(Касса.АдресВебСервера,Касса.ПортВебСервера);

ОтветHTTP = Соединение.ОтправитьДляОбработки(HTTPЗапрос);

Исключение

Возврат "Ошибка регистрации в очереди! Проверьте работоспособность сервера и параметры кассы!";

КонецПопытки;



//обработаем ответ



Тело = ОтветHTTP.ПолучитьТелоКакСтроку();

Если Не ОтветHTTP.КодСостояния = 201 Тогда

Возврат "Ошибка регистрации в очереди!";

КонецЕсли;



//добавим в регистр очереди

Если Операция = "ФискальныйЧек" Тогда

новМенеджер = РегистрыСведений.ОчередьРаботыСКкт.СоздатьМенеджерЗаписи();

новМенеджер.Период = ТекущаяДата();

новМенеджер.Объект = ДополнительныеПараметры.ДокументОплаты;

новМенеджер.Операция = Операция;

новМенеджер.уникИД = СтруктураJSON.uuid;

новМенеджер.Касса = ДополнительныеПараметры.ДокументОплаты.Касса;

новМенеджер.Записать(Ложь);

КонецЕсли;



Возврат "Данные добавлены в очередь!";

КонецФункции

Для каждой операции необходимо формировать свое тело запроса, реализовали под каждую операцию свою функцию которая формирует структуру для отправки на задание

Функция СформироватьСтруктуруДляОтчетаБезГашения(Касса,Операция,ДополнительныеПараметры)



уникИД = Формат(ТекущаяДата(),"ДФ=ддММггггЧЧммсс");

СтруктураJSON = Новый Структура();

СтруктураJSON.Вставить("uuid",уникИД);

МассивПараметров = Новый Массив();

СтруктураОперация  = Новый Структура("type","reportX");

МассивПараметров.Добавить(СтруктураОперация);

СтруктураОператор = Новый Структура();

СтруктураОператор.Вставить("name",Строка(ПараметрыСеанса.Пользователь));

МассивПараметров.Добавить(СтруктураОператор);

СтруктураJSON.Вставить("request",МассивПараметров);



Возврат СтруктураJSON;

КонецФункции



Функция СформироватьСтруктуруДляЗакрытияСмены(Касса,Операция,ДополнительныеПараметры)



уникИД = Формат(ТекущаяДата(),"ДФ=ддММггггЧЧммсс");

ЗаписьJSON = новый ЗаписьJSON;

ЗаписьJSON.УстановитьСтроку();

СтруктураJSON = Новый Структура();

СтруктураJSON.Вставить("uuid",уникИД);

МассивПараметров = Новый Массив();

СтруктураОперация  = Новый Структура("type","closeShift");

МассивПараметров.Добавить(СтруктураОперация);

СтруктураОператор = Новый Структура();

СтруктураОператор.Вставить("name",Строка(ПараметрыСеанса.Пользователь));

МассивПараметров.Добавить(СтруктураОператор);

СтруктураJSON.Вставить("request",МассивПараметров);



Возврат СтруктураJSON;



КонецФункции



Функция СформироватьСтруктуруДляФискальногоЧека(Касса,Операция,ДополнительныеПараметры)



ДокументОплаты = ДополнительныеПараметры.ДокументОплаты;

Если Не ПустаяСтрока(ДокументОплаты.НомерЧека) Тогда



СообщениеПользователю = Новый СообщениеПользователю();

СообщениеПользователю.Текст = "По документу уже пробит чек!";

СообщениеПользователю.КлючДанных = ДокументОплаты;

СообщениеПользователю.Сообщить();

Возврат Неопределено;



КонецЕсли;



СтруктураОтвета = ПолучитьСостояниеЗадания("",ДокументОплаты.Касса,ДокументОплаты);

Если Не СтруктураОтвета.ВозможнаОтправка Тогда



СообщениеПользователю = Новый СообщениеПользователю();

СообщениеПользователю.Текст = СтруктураОтвета.ОписаниеОшибки;

СообщениеПользователю.КлючДанных = ДокументОплаты;

СообщениеПользователю.Сообщить();

Возврат Неопределено;



КонецЕсли;



уникИД = Формат(ДокументОплаты.Номер,"ЧГ=") + "-" + Формат(ТекущаяДата(),"ДФ=ддММггЧЧммсс");

ЗаписьJSON = новый ЗаписьJSON;

ЗаписьJSON.УстановитьСтроку();

СтруктураJSON = Новый Структура();

СтруктураJSON.Вставить("uuid",уникИД);



МассивПараметров = Новый Массив();

СтруктураЧека = Новый Структура();

//Тип задания

//sell - чек прихода buy - чек расхода sellReturn - чек возврата прихода buyReturn - чек возврата расхода

ВидОперации = ДокументОплаты.ВидОперации;

Если ВидОперации = ПредопределенноеЗначение("Перечисление.ВидыОперации.ОплатаКлиентом") ИЛИ 

ВидОперации = ПредопределенноеЗначение("Перечисление.ВидыОперации.ПополнениеЛицевогоСчета") ИЛИ 

ВидОперации = ПредопределенноеЗначение("Перечисление.ВидыОперации.ВнесениеДенежныхСредств") Тогда



type = "sell";



ИначеЕсли ВидОперации = ПредопределенноеЗначение("Перечисление.ВидыОперации.ВозвратКлиенту") Тогда



type = "sellReturn";



Иначе



Возврат Неопределено;



КонецЕсли;

СтруктураЧека.Вставить("type",type);

//Электронный чек

СтруктураЧека.Вставить("electronically",Ложь);

//useVAT18 использовать при регистрации чека ставку налога 18%

СтруктураЧека.Вставить("useVAT18",Ложь);

//taxationType Система налогообложения

Если Касса.СистемаНалогооблажения = ПредопределенноеЗначение("Справочник.СистемыНалогообложения.Общая") Тогда

taxationType = "osn";

ИначеЕсли Касса.СистемаНалогооблажения = ПредопределенноеЗначение("Справочник.СистемыНалогообложения.УпрощеннаяДоход") Тогда

taxationType = "usnIncome";

ИначеЕсли Касса.СистемаНалогооблажения = ПредопределенноеЗначение("Справочник.СистемыНалогообложения.УпрощеннаяДоходМинусРасход") Тогда

taxationType = "usnIncomeOutcome";

ИначеЕсли Касса.СистемаНалогооблажения = ПредопределенноеЗначение("Справочник.СистемыНалогообложения.ЕНВД") Тогда

taxationType = "envd";

ИначеЕсли Касса.СистемаНалогооблажения = ПредопределенноеЗначение("Справочник.СистемыНалогообложения.ЕСН") Тогда

taxationType = "esn";

ИначеЕсли Касса.СистемаНалогооблажения = ПредопределенноеЗначение("Справочник.СистемыНалогообложения.ПСН") Тогда

taxationType = "patent";

Иначе

taxationType = "";

КонецЕсли;

Если Не ПустаяСтрока(taxationType) Тогда

СтруктураЧека.Вставить("taxationType",taxationType);

КонецЕсли;

//данные о кассире

СтруктураОператор = Новый Структура();

СтруктураОператор.Вставить("name", Строка(ДокументОплаты.Автор));

СтруктураЧека.Вставить("operator",СтруктураОператор);



//данные о товарах массив items

МассивПозиций = Новый Массив();



Если ВидОперации = Перечисления.ВидыОперации.ПополнениеЛицевогоСчета ИЛИ

ВидОперации = ПредопределенноеЗначение("Перечисление.ВидыОперации.ВнесениеДенежныхСредств") Тогда



СтруктураПозиции = Новый Структура();

СтруктураПозиции.Вставить("type","position");

СтруктураПозиции.Вставить("name","Оказание медицинских услуг по договору");

СтруктураПозиции.Вставить("price",ДокументОплаты.Сумма);

СтруктураПозиции.Вставить("quantity",1);

СтруктураПозиции.Вставить("amount",ДокументОплаты.Сумма);

СтруктураПозиции.Вставить("paymentMethod","fullPrepayment");

СтруктураПозиции.Вставить("paymentObject","service");

СтруктураНДС = Новый Структура();

СтруктураНДС.Вставить("type","none");

СтруктураПозиции.Вставить("tax",СтруктураНДС);

МассивПозиций.Добавить(СтруктураПозиции);



Иначе



СписокУслуг = ДокументОплаты.СписокУслуг.Выгрузить();

СуммаОплтыБонусами = 0;

МассивОплатБонусами = ДокументОплаты.ВидыОплат.НайтиСтроки(Новый Структура("ВидОплаты",ПредопределенноеЗначение("Перечисление.ВидОплаты.Бонусами")));

Если МассивОплатБонусами.Количество() = 1 Тогда

СуммаОплтыБонусами = МассивОплатБонусами[0].Сумма;

КонецЕсли;



Для Каждого СтрПозиций Из СписокУслуг Цикл



Если СуммаОплтыБонусами = 0 Тогда

Прервать;

КонецЕсли;



Если СтрПозиций.Сумма - 1 <= СуммаОплтыБонусами Тогда

СуммаОплтыБонусами = СуммаОплтыБонусами - (СтрПозиций.Сумма - 1);

СтрПозиций.Сумма = 1;

СтрПозиций.Цена = СтрПозиций.Сумма/СтрПозиций.Количество;

Иначе

СтрПозиций.Сумма = СтрПозиций.Сумма - СуммаОплтыБонусами;

СтрПозиций.Цена = СтрПозиций.Сумма/СтрПозиций.Количество;

СуммаОплтыБонусами = 0;

КонецЕсли;



КонецЦикла;



Для Каждого СтрОплаты Из СписокУслуг Цикл



СтруктураПозиции = Новый Структура();

СтруктураПозиции.Вставить("type","position");

СтруктураПозиции.Вставить("name",СтрОплаты.Услуга.Наименование);

СтруктураПозиции.Вставить("price",СтрОплаты.Цена);

СтруктураПозиции.Вставить("quantity",СтрОплаты.Количество);

СтруктураПозиции.Вставить("amount",СтрОплаты.Сумма);

Если СтруктураЧека.type = "sell" Тогда 

СтруктураПозиции.Вставить("infoDiscountAmount",СтрОплаты.СуммаБезСкидки - СтрОплаты.Сумма);

КонецЕсли;

//paymentMethod - Признак способа рaсчета

//fullPrepayment - предоплата 100% 

//prepayment - предоплата 

//advance - аванс 

//fullPayment - полный расчет 

//partialPayment - частичный расчет и кредит 

//credit - передача в кредит 

//creditPayment - оплата кредита

СтруктураПозиции.Вставить("paymentMethod","fullPrepayment");



//paymentObject

//commodity - товар 

//excise - подакцизный товар 

//job - работа 

//service - услуга 

//gamblingBet - ставка азартной игры 

//gamblingPrize - выигрыш азартной игры 

//lottery - лотерейный билет 

//lotteryPrize - выигрыш лотереи 

//intellectualActivity - предоставление результатов интерелектуальной деятельности 

//payment - платеж 

//agentCommission - агентское вознаграждение 

//proprietaryLaw - имущественное право 

//nonOperatingIncome - внереализационный доход 

//insuranceСontributions - страховые взносы 

//merchantTax - торговый сбор 

//resortFee - курортный сбор 

//composite - составной предмет расчета 

//another - иной предмет расчета

СтруктураПозиции.Вставить("paymentObject","service");



//tax

СтруктураНДС = Новый Структура();

СтруктураНДС.Вставить("type","none");

СтруктураПозиции.Вставить("tax",СтруктураНДС);



МассивПозиций.Добавить(СтруктураПозиции);



КонецЦикла;



КонецЕсли;

СтруктураЧека.Вставить("items", МассивПозиций);



//формирование стуктуры оплат

МассивОплат = Новый Массив();

Для Каждого СтрОплаты Из ДокументОплаты.ВидыОплат Цикл

СтруктураОплаты = Новый Структура();

Если СтрОплаты.ВидОплаты = ПредопределенноеЗначение("Перечисление.ВидОплаты.БезНаличными") Тогда

СтруктураОплаты.Вставить("type", "electronically");

ИначеЕсли СтрОплаты.ВидОплаты = ПредопределенноеЗначение("Перечисление.ВидОплаты.Наличными") Тогда

СтруктураОплаты.Вставить("type", "cash");

Иначе

Продолжить;

КонецЕсли;

СтруктураОплаты.Вставить("sum", СтрОплаты.Сумма);

МассивОплат.Добавить(СтруктураОплаты);

КонецЦикла;

СтруктураЧека.Вставить("payments",МассивОплат);



СтруктураJSON.Вставить("request",СтруктураЧека);



Возврат СтруктураJSON;

КонецФункции

 

Опрос заданий организовали через фоновое задание раз в 60 секунд

Процедура ОбработкаОчередиККТ() Экспорт 

Запрос = Новый Запрос();

Запрос.Текст = "ВЫБРАТЬ

| ОчередьРаботыСККТ.Период КАК Период,

| ОчередьРаботыСККТ.Объект КАК Объект,

| ОчередьРаботыСККТ.Операция КАК Операция,

| ОчередьРаботыСККТ.уникИД КАК уникИД,

| ОчередьРаботыСККТ.Касса КАК Касса,

| ОчередьРаботыСККТ.Статус КАК Статус,

| ОчередьРаботыСККТ.Результат КАК Результат

|ИЗ

| РегистрСведений.ОчередьРаботыСККТ КАК ОчередьРаботыСККТ

|ГДЕ

| ОчередьРаботыСККТ.Статус = """"";



Рез = Запрос.Выполнить().Выбрать();



Пока Рез.Следующий() Цикл

Если Не ЗначениеЗаполнено(Рез.Объект) Тогда

МенеджерЗаписи = РегистрыСведений.ОчередьРаботыСКкт.СоздатьМенеджерЗаписи();

МенеджерЗаписи.Объект = Рез.Объект;

МенеджерЗаписи.Период = Рез.Период;

МенеджерЗаписи.Прочитать();

МенеджерЗаписи.Удалить();

Продолжить;

КонецЕсли;



СтруктураОтвета = ПолучитьСостояниеЗадания(Рез.уникИД,Рез.Объект.Касса,Рез.Объект);

Если Не СтруктураОтвета.Результат Тогда

Продолжить;

КонецЕсли;



СтатусОперации = СтруктураОтвета.results[0].status;

ОписаниеОшибки = СтруктураОтвета.results[0].errorDescription;

Если СтатусОперации = "error" Тогда

//запишем ошибку

МенеджерЗаписи = РегистрыСведений.ОчередьРаботыСКкт.СоздатьМенеджерЗаписи();

МенеджерЗаписи.Объект = Рез.Объект;

МенеджерЗаписи.Период = Рез.Период;

МенеджерЗаписи.Прочитать();

МенеджерЗаписи.Статус = СтатусОперации;

МенеджерЗаписи.Результат = ОписаниеОшибки; 

МенеджерЗаписи.Записать();

Продолжить;

ИначеЕсли СтатусОперации = "ready" Тогда



//проверим номер чека и присвоем оплате

НомерЧека = СтруктураОтвета.results[0].result.fiscalParams.fiscalDocumentNumber;

ДокОплаты = Рез.Объект.ПолучитьОбъект();

ДокОплаты.НомерЧека = НомерЧека;

Попытка

ДокОплаты.Записать(РежимЗаписиДокумента.Запись);

Исключение

Продолжить;

КонецПопытки;

//если задание выполненно успешно то данные о задании удаляем из очереди

МенеджерЗаписи = РегистрыСведений.ОчередьРаботыСКкт.СоздатьМенеджерЗаписи();

МенеджерЗаписи.Объект = Рез.Объект;

МенеджерЗаписи.Период = Рез.Период;

МенеджерЗаписи.Прочитать();

МенеджерЗаписи.Удалить();

КонецЕсли;

КонецЦикла;



КонецПроцедуры

Функция ПолучитьСостояниеЗадания(УникИД, Касса, Объект) Экспорт



Если Не ЗначениеЗаполнено(УникИД) Тогда

//найдем последний УникИД по объекту

Запрос = Новый Запрос();

Запрос.Текст = "ВЫБРАТЬ

| ОчередьРаботыСККТСрезПоследних.Объект КАК Объект,

| ОчередьРаботыСККТСрезПоследних.Операция КАК Операция,

| ОчередьРаботыСККТСрезПоследних.уникИД КАК уникИД,

| ОчередьРаботыСККТСрезПоследних.Касса КАК Касса

|ИЗ

| РегистрСведений.ОчередьРаботыСККТ.СрезПоследних КАК ОчередьРаботыСККТСрезПоследних

|ГДЕ

| ОчередьРаботыСККТСрезПоследних.Объект = &Объект";

Запрос.УстановитьПараметр("Объект",Объект);

Рез = Запрос.Выполнить().Выбрать();

Если Рез.Следующий() Тогда

УникИД = Рез.уникИД;

Иначе

СтруктураОтвета = Новый Структура();

СтруктураОтвета.Вставить("Результат",Ложь);

СтруктураОтвета.Вставить("ВозможнаОтправка",Истина);

СтруктураОтвета.Вставить("ОписаниеОшибки","Объект не отправлялся в очередь для печати!");

Возврат СтруктураОтвета;

КонецЕсли;



КонецЕсли;



Запрос = Новый Запрос();



HTTPСоединение = Новый HTTPСоединение(Касса.АдресВебСервера,Касса.ПортВебСервера,,,,,);  

HTTPЗапрос = Новый HTTPЗапрос("/requests/" + УникИД);

Попытка

Ответ = HTTPСоединение.Получить(HTTPЗапрос);

ОписаниеОшибки = Ответ.ПолучитьТелоКакСтроку();

Исключение

Возврат Неопределено;

КонецПопытки;



Попытка

ЧтениеJSON = Новый ЧтениеJSON();

ЧтениеJSON.УстановитьСтроку(ОписаниеОшибки);

СтруктураОтвета = ПрочитатьJSON(ЧтениеJSON);

ЧтениеJSON.Закрыть();

Исключение

СтруктураОтвета = Новый Структура();

СтруктураОтвета.Вставить("Результат",Ложь);

СтруктураОтвета.Вставить("ВозможнаОтправка",Ложь);

СтруктураОтвета.Вставить("ОписаниеОшибки","Не удалось прочитать статус");

Возврат СтруктураОтвета;

КонецПопытки;

СтатусОперации = СтруктураОтвета.results[0].status;

СтруктураОтвета.Вставить("Результат",Истина);

СтруктураОтвета.Вставить("ВозможнаОтправка",?(СтатусОперации = "error",Истина,Ложь));

СтруктураОтвета.Вставить("ОписаниеОшибки",?(СтатусОперации = "ready","Чек успешно расепчатан ранее!",СтруктураОтвета.results[0].errorDescription));

Возврат СтруктураОтвета;



КонецФункции



Документация по взаимодействию с web-сервером atol описана на сайте http://integration.atol.ru/#web-server

Спасибо за внимание, будем рады если статья будет полезна Вам, готовы ответить на вопросы в комментариях.

has been added to your cart:
Оформление заказа