Как организовать работу двух кассиров на общих фискальных регистраторах и разделить чеки по организациям в 1С?

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

В розничной торговле нередко возникает ситуация, когда торговая точка оформлена на два юридических лица (например, два индивидуальных предпринимателя для разных групп товаров), а в пиковые периоды требуется посадить двух кассиров на одну кассовую зону. Покупка дополнительных фискальных регистраторов (ФР/ККТ) не всегда оправдана экономически. Рассмотрим, как программно организовать работу двух независимых сеансов 1С с двумя общими фискальными регистраторами и обеспечить разделение позиций чека по организациям.

Проблема конкурентного доступа к ККТ

Фискальный регистратор, подключенный по интерфейсу COM, USB (в режиме эмуляции COM) или через локальную сеть (TCP/IP), как правило, монопольно захватывается драйвером устройства. Если первый сеанс 1С держит порт постоянным включением (DeviceEnabled = 1), второй сеанс при попытке обращения получает ошибку недоступности порта или тайм-аута соединения.

Разберем основные архитектурные подходы для решения этой проблемы:

  1. Асинхронная очередь печати через базу данных — клиентские сеансы только записывают задания в регистр сведений или отдельную таблицу, а выделенный управляющий сеанс последовательно фискализирует их на ККТ.
  2. Использование внешнего кассового сервера — специализированное ПО или встроенный Web-сервер драйвера ККТ принимает команды по HTTP/JSON и самостоятельно организует очередь печати.
  3. Динамическое монопольное подключение (захват устройства «на лету») — сеанс подключается к кассе исключительно в момент печати фискального чека или открытия денежного ящика и мгновенно освобождает порт (DeviceEnabled = 0) после завершения операции.

Проанализируем детально третий подход — динамический захват ККТ с обработкой ожидания, так как он не требует дополнительной инфраструктуры и реализуется непосредственно в коде конфигурации 1С.

Реализация циклического захвата порта ККТ

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

Посмотрим на пример кода функции занятия устройства с использованием метода DeviceEnabled:


Если глФР.DeviceEnabled = 0 Тогда
    ~ещеРаз:
    глФР.DeviceEnabled = 1;
    Если глФР.DeviceEnabled = 0 Тогда
        Если Вопрос("ФР занят другим сеансом, ждем...", РежимДиалогаВопрос.ОКОтмена, 1) = КодВозвратаДиалога.Отмена Тогда
            Возврат;
        КонецЕсли;
        Перейти ~ещеРаз;
    КонецЕсли;
КонецЕсли;

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

Важный момент: частое подключение и отключение через DeviceEnabled является штатным режимом работы драйверов торгового оборудования и не приводит к аппаратному износу фискального регистратора.

Управление денежным ящиком при работе с несколькими ККТ

Когда в кассовой зоне установлено два фискальных регистратора (для ИП №1 и ИП №2), физический денежный ящик обычно подключен через интерфейсный кабель (RJ-12) только к одному из них. Это порождает коллизию: если чек пробивается на устройстве, к которому ящик не подключен, ящик сам по себе не откроется.

Рассмотрим алгоритм переключения логических устройств для корректного открытия денежного ящика:


ПараметрыКассовогоРегистратора = КэшПараметров.КассовыеРегистраторы.Получить(К.Организация);

Если КэшПараметров.РазделятьЧеки Тогда
    // Если денежный ящик привязан к конкретному номеру устройства
    Если глФР.CurrentDeviceNumber = КэшПараметров.ДенежныйЯщикПодключенКУстройству И Не КэшПараметров.НеОткрыватьЯщик Тогда
        ЗанятьФР(глФР);
        ОткрытьЯщик(глФР, НужноОткрытьЯщик);
    КонецЕсли;
    НужноОткрытьЯщик = Ложь;

    // Переключение на устройство, привязанное к организации чека
    Если глФР.CurrentDeviceNumber <> ПараметрыКассовогоРегистратора.НомерУстройства Тогда
        глФР.DeviceEnabled = 0;
        глФР.CurrentDeviceNumber = ПараметрыКассовогоРегистратора.НомерУстройства;
        ЗанятьФР(глФР);
        ОткрытьЯщик(глФР, НужноОткрытьЯщик);
    КонецЕсли;
Иначе
    Если КэшПараметров.ПодключатьФРТолькоПриПечатиЧека Тогда
        ЗанятьФР(глФР);
    КонецЕсли;
    ОткрытьЯщик(глФР, НужноОткрытьЯщик);
КонецЕсли;

После завершения печати чека обязательно освобождаем порт, чтобы устройство стало доступным для соседнего рабочего места:


Если НужноОткрытьЯщик И Не КэшПараметров.НеОткрыватьЯщик Тогда
    Если глФР.CurrentDeviceNumber <> КэшПараметров.ДенежныйЯщикПодключенКУстройству Тогда
        Если КэшПараметров.ПодключатьФРТолькоПриПечатиЧека Тогда
            глФР.DeviceEnabled = 0; // Освобождаем текущий порт
        КонецЕсли;
        глФР.CurrentDeviceNumber = КэшПараметров.ДенежныйЯщикПодключенКУстройству;
    КонецЕсли;
    ЗанятьФР(глФР);
    ОткрытьЯщик(глФР, НужноОткрытьЯщик);
КонецЕсли;

// Финальное освобождение оборудования
Если КэшПараметров.ПодключатьФРТолькоПриПечатиЧека Тогда
    глФР.DeviceEnabled = 0;
КонецЕсли;

Автоматическое разделение чека по двум организациям

Когда покупатель приносит на кассу товары, относящиеся к разным ИП, документ продажи должен автоматически разделиться на два отдельных фискальных документа с пропорциональным распределением видов оплат (наличные, безналичные).

Разберем по шагам функцию разделения документа Чек:


Функция РазделитьЧекПоОрганизациям() Экспорт
    Если Не КэшПараметров.РазделятьЧеки Тогда
        Возврат Истина;
    КонецЕсли;
    
    Записать();
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |    ЧекТовары.НомерСтроки КАК НомерСтроки,
        |    ЧекТовары.Товар.Вид.РеализацияС КАК РеализацияС
        |ИЗ
        |    Документ.Чек.Товары КАК ЧекТовары
        |ГДЕ
        |    ЧекТовары.Ссылка = &Ссылка";
        
    Запрос.УстановитьПараметр("Ссылка", ЭтотОбъект.Ссылка);
    СтрокиПоОрганизациям = Запрос.Выполнить().Выгрузить();
    
    Для Каждого Стр Из СтрокиПоОрганизациям Цикл
        Если Стр.РеализацияС = Null Или Стр.РеализацияС = Неопределено Или Стр.РеализацияС.Пустая() Тогда
            Стр.РеализацияС = УмолчанияПользователя.РозничнаяОрганизация;
        КонецЕсли;
        Если Стр.РеализацияС <> Организация Тогда
            ОрганизацияВторогоЧека = Стр.РеализацияС;
        КонецЕсли;
    КонецЦикла;
    
    ИспОрганизации = Новый Массив;
    ЧастоИспользуемые.ЗаполнитьМассивУникальнымиЗначениями(ИспОрганизации, СтрокиПоОрганизациям.ВыгрузитьКолонку("РеализацияС"));
    
    Если ИспОрганизации.Количество() = 1 Тогда
        Если Организация <> ИспОрганизации[0] Тогда
            Организация = ИспОрганизации[0];
            глПриСменеОрганизации(ЭтотОбъект,,, Ложь);
        КонецЕсли;
        Возврат Истина;
        
    ИначеЕсли ИспОрганизации.Количество() = 2 Тогда
        // В корзине товары двух ИП: создаем второй чек и переносим позиции
        Чек2 = Документы.Чек.СоздатьДокумент();
        СтрокиПереносим = Новый Массив;
        
        Для Каждого Стр Из СтрокиПоОрганизациям Цикл
            Если Стр.РеализацияС <> Организация Тогда
                СтрокаДока = Товары[Стр.НомерСтроки - 1];
                СтрокиПереносим.Добавить(СтрокаДока);
                ЗаполнитьЗначенияСвойств(Чек2.Товары.Добавить(), СтрокаДока);
            КонецЕсли;
        КонецЦикла;
        
        ЗаполнитьЗначенияСвойств(Чек2, ЭтотОбъект);
        Чек2.Организация = ОрганизацияВторогоЧека;
        глПриСменеОрганизации(Чек2,,, Ложь);
        Чек2.КассовыйРегистратор = КэшПараметров.КассовыеРегистраторы[ОрганизацияВторогоЧека].КассовыйРегистратор;
        
        НачатьТранзакцию();
        
        // Распределение оплат между двумя чеками
        РасчетЧека2 = Чек2.Товары.Итог("Всего");
        РасчетЧека1 = ЭтотОбъект.Товары.Итог("Всего") - РасчетЧека2;
        
        Для Каждого СтрОплаты Из ЭтотОбъект.Оплата Цикл
            Было = СтрОплаты.Получено;
            СтрОплаты.Получено = Мин(РасчетЧека1, Было);
            Было = Было - СтрОплаты.Получено;
            
            Если РасчетЧека1 > 0 Тогда
                РасчетЧека1 = РасчетЧека1 - СтрОплаты.Получено;
            КонецЕсли;
            
            Если РасчетЧека2 > 0 И Было > 0 Тогда
                НовОплата = Чек2.Оплата.Добавить();
                ЗаполнитьЗначенияСвойств(НовОплата, СтрОплаты);
                НовОплата.Получено = Мин(РасчетЧека2, Было);
                РасчетЧека2 = РасчетЧека2 - НовОплата.Получено;
            КонецЕсли;
        КонецЦикла;
        
        Чек2.Записать(РежимЗаписиДокумента.Проведение, РежимПроведенияДокумента.Неоперативный);
        
        Отказ = Ложь;
        СерверТО.ПечатьЧекаФР(Чек2, "ПробитьЧек", "Чек", Отказ);
        
        Если Не Отказ Тогда
            Чек2.ЧекПробитККМ = 1;
            Чек2.Записать(РежимЗаписиДокумента.Запись);
        КонецЕсли;
        
        // Удаляем перенесенные товары из первого чека
        Для Каждого Стр Из СтрокиПереносим Цикл
            Товары.Удалить(Стр);
        КонецЦикла;
        
        Записать();
        ЗафиксироватьТранзакцию();
    КонецЕсли;
    
    Возврат Истина;
КонецФункции

Требования законодательства и технические нюансы

При внедрении совместного использования фискальных регистраторов необходимо учитывать требования 54-ФЗ и специфику сетевых протоколов:

  1. Реквизиты кассира в чеке: согласно формату фискальных документов (ФФД), в каждый чек обязательно должны передаваться теги 1021 («Кассир» — ФИО) и 1203 («ИНН кассира»). Поскольку за одним аппаратом работают разные сотрудники, эти реквизиты должны заполняться динамически из текущего сеанса пользователя 1С перед открытием чека.
  2. Ограничение TCP-сокетов ККТ: при подключении кассы через Ethernet (LAN/Wi-Fi) микроконтроллер ККТ обычно поддерживает только одно активное TCP-соединение. Если сеанс не закрывает сокет, второй сеанс мгновенно получает сетевой сброс. Использование DeviceEnabled = 0 принудительно разрывает сетевую сессию, освобождая сокет для других клиентов.
  3. Раздельный учет наличных: при общем физическом денежном ящике невозможно аппаратно разделить купюры двух ИП. Программно необходимо вести раздельный учет инкассаций и сменных остатков в разрезе кассиров и организаций, сверяя итоговую сумму с фактическим содержимым ящика.

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

← На главную