При работе в терминальном режиме (RDP) на сервере часто возникает классическая задача: необходимо обеспечить совместный доступ нескольких удаленных пользователей к одной физической кассе ККМ (например, АТОЛ 55Ф), подключенной по сети (IP). На практике при использовании типового механизма БПО (Библиотеки подключаемого оборудования) в 1С:Управление торговлей 11 разработчики сталкиваются со следующей проблемой. Касса успешно проходит тест подключения у всех пользователей, но при попытке пробить чек система требует открыть кассовую смену, несмотря на то что смена уже открыта на другом рабочем месте. Попытка открыть смену повторно ни к чему не приводит, а порт кассы намертво блокируется монопольным подключением первого обратившегося сеанса.
В этой статье мы подробно разберем, почему возникает эта проблема, проанализируем внутреннее устройство БПО 1С и реализуем надежное решение с использованием встроенного Web-сервера АТОЛ (ДТО 10), а также разберем тонкости работы с его базой данных SQLite из 1С.
---
Для начала разберем архитектурную логику работы торгового оборудования в современных конфигурациях 1С на управляемых формах:
КассоваяСмена жестко привязан не только к справочнику КассыККМ, но и к справочнику РабочиеМеста. Рабочее место в терминальной сессии формируется динамически на основе сетевого имени компьютера клиента и имени пользователя операционной системы. Для 1С каждый удаленный пользователь — это абсолютно уникальное рабочее место, требующее монопольного владения кассой и собственной уникальной кассовой смены в информационной базе.Чтобы обойти эти ограничения без масштабного переписывания БПО (которое сотрется при первом же обновлении конфигурации), наиболее эффективным решением является перевод работы с ККТ на независимый Web-сервер АТОЛ. В этом случае касса общается с миром по протоколу HTTP через JSON-запросы, а сам веб-сервер берет на себя управление очередью документов, исключая конфликты блокировки портов.
---
Web-сервер АТОЛ уже встроен в стандартный пакет ДТО 10 (Драйвер торгового оборудования). Его настройка выглядит следующим образом:
16732.http://localhost:16732 (или используя IP-адрес сервера).---
Теперь рассмотрим, как отправлять команды на веб-сервер из кода 1С. Общение происходит асинхронно: сначала мы отправляем задание в очередь и получаем его уникальный идентификатор (GUID), а затем опрашиваем сервер о готовности результата.
Проанализируем ситуацию, когда нам необходимо проверить текущий статус смены на кассе ККТ. Напишем функцию, которая отправляет JSON-запрос getDeviceStatus:
Функция ПолучитьСтатусКассы(АдресСервера, Порт) Экспорт
HTTPСосоединение = Новый HTTPСосоединение(АдресСервера, Порт, , , , , , Ложь);
HTTPЗапрос = Новый HTTPЗапрос("/api/v2/requests?deviceID=KKT");
HTTPЗапрос.Заголовки.Вставить("Content-type", "application/json");
// Формируем тело запроса в формате JSON
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.ПроверятьСтруктуру = Ложь;
ЗаписьJSON.УстановитьСтроку();
СтруктураЗапроса = Новый Структура;
ГУИД = Новый УникальныйИдентификатор;
СтруктураЗапроса.Вставить("uuid", Строка(ГУИД));
МассивЗаданий = Новый Массив;
Задание = Новый Структура;
Задание.Вставить("type", "getDeviceStatus");
МассивЗаданий.Добавить(Задание);
СтруктураЗапроса.Вставить("request", МассивЗаданий);
ЗаписатьJSON(ЗаписьJSON, СтруктураЗапроса);
СтрокаJSON = ЗаписьJSON.Закрыть();
HTTPЗапрос.УстановитьТелоИзСтроки(СтрокаJSON, КодировкаТекста.UTF8, ИспользованиеByteOrderMark.НеИспользовать);
HTTPОтвет = HTTPСосоединение.ОтправитьДляОбработки(HTTPЗапрос);
Если HTTPОтвет.КодСостояния = 201 Тогда // Задание успешно принято в очередь
Пока Истина Цикл
ОбработкаПрерыванияПользователя();
// Запрашиваем результат выполнения по ГУИД задания
HTTPЗапросРезультата = Новый HTTPЗапрос("requests/" + Строка(ГУИД));
ОтветРезультата = HTTPСосоединение.Получить(HTTPЗапросРезультата);
Если ОтветРезультата.КодСостояния = 200 Тогда
СтрокаОтвета = ОтветРезультата.ПолучитьТелоКакСтроку();
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(СтрокаОтвета);
СтруктураОтвета = ПрочитатьJSON(ЧтениеJSON);
СтатусОперации = СтруктураОтвета.results[0].status;
Если СтатусОперации = "ready" Тогда
СостояниеКассы = СтруктураОтвета.results[0].result.deviceStatus;
Возврат СостояниеКассы.shift; // Возвращает "opened", "closed" или "expired"
ИначеЕсли СтатусОперации = "error" Тогда
ВызватьИсключение "Ошибка ККТ: " + СтруктураОтвета.results[0].errorDescription;
КонецЕсли;
КонецЕсли;
КонецЦикла;
Иначе
ВызватьИсключение "Ошибка HTTP-соединения: " + HTTPОтвет.ПолучитьТелоКакСтроку();
КонецЕсли;
КонецФункции
Рассмотрим шаг формирования и отправки JSON для нефискального документа (X-отчета). Здесь нам также потребуется передать информацию об операторе (кассире):
Процедура НапечататьXОтчет(АдресСервера, Порт, ИмяКассира, ИННКассира) Экспорт
HTTPСосоединение = Новый HTTPСосоединение(АдресСервера, Порт, , , , , , Ложь);
HTTPЗапрос = Новый HTTPЗапрос("requests");
HTTPЗапрос.Заголовки.Вставить("Content-type", "application/json");
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
СтруктураЗапроса = Новый Структура;
ГУИД = Новый УникальныйИдентификатор;
СтруктураЗапроса.Вставить("uuid", Строка(ГУИД));
МассивЗаданий = Новый Массив;
Задание = Новый Структура;
Задание.Вставить("type", "reportX");
// Передаем кассира согласно ФФД
СтруктураОператора = Новый Структура;
СтруктураОператора.Вставить("name", ИмяКассира);
СтруктураОператора.Вставить("vatin", ИННКассира);
Задание.Вставить("operator", СтруктураОператора);
МассивЗаданий.Добавить(Задание);
СтруктураЗапроса.Вставить("request", МассивЗаданий);
ЗаписатьJSON(ЗаписьJSON, СтруктураЗапроса);
СтрокаJSON = ЗаписьJSON.Закрыть();
HTTPЗапрос.УстановитьТелоИзСтроки(СтрокаJSON, КодировкаТекста.UTF8, ИспользованиеByteOrderMark.НеИспользовать);
HTTPОтвет = HTTPСосоединение.ОтправитьДляОбработки(HTTPЗапрос);
Если HTTPОтвет.КодСостояния = 201 Тогда
Сообщить("Задание на печать X-отчета успешно добавлено в очередь ККТ.");
Иначе
Сообщить("Не удалось отправить запрос. Код ошибки: " + HTTPОтвет.КодСостояния);
КонецЕсли;
КонецПроцедуры
---
При реальной эксплуатации веб-сервера АТОЛ неизбежно возникает критическая ситуация: если в очередь отправлен чек с ошибочным реквизитом (например, неверная ставка НДС или некорректный код маркировки), задание зависает в статусе inProgress. Попытка повторно отправить исправленный документ с тем же самым uuid приводит к ошибке дублирования идентификатора.
Штатного веб-метода для принудительного удаления конкретного "зависшего" задания из очереди веб-сервера АТОЛ нет. Решается эта проблема прямым подключением к внутренней базе данных веб-сервера web.s3db, которая работает на СУБД SQLite.
При попытке подключиться к SQLite через стандартный драйвер ODBC в коде 1С вы можете столкнуться с системной ошибкой: "Источник данных не найден и не указан драйвер, используемый по умолчанию".
Причина кроется в разрядности сеанса 1С. Даже если у вас установлена 64-битная операционная система Windows, клиентское приложение 1С:Предприятие (тонкий или толстый клиент) чаще всего запускается как 32-разрядное (x86) приложение. Из 32-разрядного процесса 1С невозможно вызвать 64-битный драйвер ODBC.
Решение: Обязательно скачайте и установите на сервере/ПК именно 32-разрядную версию драйвера SQLite ODBC (например, sqliteodbc.exe для Win32). Только после этого COM-объект 1С сможет установить соединение.
Напишем процедуру для очистки очереди зависших заданий в SQLite:
Процедура ОчиститьОчередьЗаданийАтол(ИмяФайлаБазы) Экспорт
// ИмяФайлаБазы обычно указывает на: C:\ProgramData\ATOL\drivers10\gweb\web.s3db
Попытка
База = Новый COMОбъект("ADODB.Connection");
СтрокаСоединения = "DRIVER=SQLite3 ODBC Driver;Database=" + ИмяФайлаБазы + ";";
База.Open(СтрокаСоединения);
Исключение
ЗаписатьЛогСобытий("Не удалось подключиться к SQLite: " + ОписаниеОшибки());
Возврат;
КонецПопытки;
Попытка
// Удаляем зависшие запросы и результаты
База.Execute("DELETE FROM requests");
База.Execute("DELETE FROM results");
Сообщить("Очередь веб-сервера АТОЛ успешно очищена.");
Исключение
Сообщить("Ошибка при выполнении SQL-запроса: " + ОписаниеОшибки());
КонецПопытки;
База.Close();
КонецПроцедуры
---
Чтобы не переписывать логику проведения документов и формирования кассовых смен в 1С:УТ 11, мы рекомендуем применить следующую архитектуру обхода:
МенеджерОборудованияКлиент. Пусть для всех терминальных сессий функция возвращает имя сервера, к которому подключена касса. В результате для 1С все кассиры будут работать на одном рабочем месте ККМ, и смена будет общей для всех.Такой гибридный подход позволит вам сохранить базу в полностью обновляемом состоянии и обеспечит стабильную, быструю и полностью независимую от блокировок портов печать чеков сотен удаленных пользователей на одном кассовом аппарате.