Как настроить печать чеков на одной кассе АТОЛ для нескольких удаленных пользователей в 1С:УТ 11?

Программист 1С v8.3 (Управляемые формы) 1С:Управление торговлей Управленческий учет Торговля и дистрибуция
← На главную

При работе в терминальном режиме (RDP) на сервере часто возникает классическая задача: необходимо обеспечить совместный доступ нескольких удаленных пользователей к одной физической кассе ККМ (например, АТОЛ 55Ф), подключенной по сети (IP). На практике при использовании типового механизма БПО (Библиотеки подключаемого оборудования) в 1С:Управление торговлей 11 разработчики сталкиваются со следующей проблемой. Касса успешно проходит тест подключения у всех пользователей, но при попытке пробить чек система требует открыть кассовую смену, несмотря на то что смена уже открыта на другом рабочем месте. Попытка открыть смену повторно ни к чему не приводит, а порт кассы намертво блокируется монопольным подключением первого обратившегося сеанса.

В этой статье мы подробно разберем, почему возникает эта проблема, проанализируем внутреннее устройство БПО 1С и реализуем надежное решение с использованием встроенного Web-сервера АТОЛ (ДТО 10), а также разберем тонкости работы с его базой данных SQLite из 1С.

---

Выясняем причину: почему типовой механизм БПО блокирует работу?

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

  1. Привязка к рабочему месту. В типовых конфигурациях документ КассоваяСмена жестко привязан не только к справочнику КассыККМ, но и к справочнику РабочиеМеста. Рабочее место в терминальной сессии формируется динамически на основе сетевого имени компьютера клиента и имени пользователя операционной системы. Для 1С каждый удаленный пользователь — это абсолютно уникальное рабочее место, требующее монопольного владения кассой и собственной уникальной кассовой смены в информационной базе.
  2. Монопольный захват порта. При стандартном подключении ККТ через драйвер 1С захватывает порт кассы. Даже если в настройках драйвера снят флаг "Устройство подключено", в многопользовательской терминальной среде регулярно возникают конфликты блокировки сетевого порта, из-за которых драйвер выдает ошибку связи, а сеансы зависают в бесконечном ожидании освобождения ККТ.

Чтобы обойти эти ограничения без масштабного переписывания БПО (которое сотрется при первом же обновлении конфигурации), наиболее эффективным решением является перевод работы с ККТ на независимый Web-сервер АТОЛ. В этом случае касса общается с миром по протоколу HTTP через JSON-запросы, а сам веб-сервер берет на себя управление очередью документов, исключая конфликты блокировки портов.

---

Шаг 1. Настройка Web-сервера АТОЛ

Web-сервер АТОЛ уже встроен в стандартный пакет ДТО 10 (Драйвер торгового оборудования). Его настройка выглядит следующим образом:

  1. Запускаем утилиту интеграции и службы Web-сервера АТОЛ на компьютере, к которому подключена касса (или прямо на сервере, если ККТ сетевая).
  2. Прописываем корректный IP-адрес кассы в настройках веб-сервера. По умолчанию служба начинает слушать порт 16732.
  3. Проверяем доступность веб-интерфейса, перейдя в браузере по адресу http://localhost:16732 (или используя IP-адрес сервера).

---

Шаг 2. Реализация HTTP-запросов к Web-серверу из 1С

Теперь рассмотрим, как отправлять команды на веб-сервер из кода 1С. Общение происходит асинхронно: сначала мы отправляем задание в очередь и получаем его уникальный идентификатор (GUID), а затем опрашиваем сервер о готовности результата.

Пример 1. Получение состояния ККТ (готовность к работе)

Проанализируем ситуацию, когда нам необходимо проверить текущий статус смены на кассе ККТ. Напишем функцию, которая отправляет 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Ответ.ПолучитьТелоКакСтроку();
    КонецЕсли;
    
КонецФункции

Пример 2. Печать X-отчета (без гашения)

Рассмотрим шаг формирования и отправки 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Ответ.КодСостояния);
    КонецЕсли;
    
КонецПроцедуры

---

Шаг 3. Решение проблемы с зависшими заданиями через SQLite

При реальной эксплуатации веб-сервера АТОЛ неизбежно возникает критическая ситуация: если в очередь отправлен чек с ошибочным реквизитом (например, неверная ставка НДС или некорректный код маркировки), задание зависает в статусе inProgress. Попытка повторно отправить исправленный документ с тем же самым uuid приводит к ошибке дублирования идентификатора.

Штатного веб-метода для принудительного удаления конкретного "зависшего" задания из очереди веб-сервера АТОЛ нет. Решается эта проблема прямым подключением к внутренней базе данных веб-сервера web.s3db, которая работает на СУБД SQLite.

Тонкость разрядности драйверов (x86 vs x64)

При попытке подключиться к 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. Создайте в системе 1С одно общее фиктивное рабочее место для всех удаленных кассиров.
  2. Напишите небольшое расширение конфигурации, переопределяющее функцию получения имени компьютера в общем модуле МенеджерОборудованияКлиент. Пусть для всех терминальных сессий функция возвращает имя сервера, к которому подключена касса. В результате для 1С все кассиры будут работать на одном рабочем месте ККМ, и смена будет общей для всех.
  3. Для эмуляции работы кассы добавьте в справочник торгового оборудования "Эмулятор ККТ". Это позволит конфигурации штатно проводить документы без ошибок со стороны БПО.
  4. Само пробитие чека перехватите в расширении перед вызовом стандартных процедур фискализации. Перенаправьте данные чека, преобразованные в JSON, на Web-сервер АТОЛ, используя разобранные нами выше методы.

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

← На главную