Как реализовать печать чеков на две ККТ от разных организаций в 1С:Бухгалтерии предприятия 3.0?

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

В практике розничной торговли часто возникает ситуация, когда за одним кассовым узлом обслуживаются продажи сразу от двух юридических лиц или ИП. При этом кассир формирует единый список покупок для покупателя, но фискализация позиций должна производиться строго на кассовый аппарат соответствующей организации. Разберем, как реализовать подобную логику в типовой конфигурации «1С:Бухгалтерия предприятия 3.0» на управляемых формах.

Архитектурные особенности и постановка задачи

В специализированных решениях (таких как 1С:Розница или 1С:УНФ) механизм разделения чеков по кассам ККМ реализован на уровне типового функционала. Однако в 1С:Бухгалтерии предприятия 3.0 учет продаж ориентирован на то, что документ РозничнаяПродажа (Чек) оформляется строго по одной выбранной организации в шапке документа, а кассовый аппарат жестко привязан к текущему рабочему месту и юридическому лицу.

Для решения задачи выполним следующие доработки:

  1. Добавим реквизит принадлежности к организации/ККТ в табличную часть товаров документа РозничнаяПродажа (например, ККТОрганизации или ИдентификаторККТ).
  2. Доработаем форму подбора номенклатуры, чтобы кассир сразу видел принадлежность товара к организации и актуальные остатки по складам.
  3. Переопределим процедуру печати чека: при проведении операции создадим временные документы под каждую организацию, выполним фискализацию на подключенные устройства и очистим служебные данные.

Доработка обработки подбора номенклатуры

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

В модуле формы подбора модифицируем команду подтверждения выбора:


Процедура ОК(Команда)
    
    Отказ = НЕ ПроверитьЗаполнение();
    Если НЕ Отказ Тогда
        // Добавляем параметр ККТОрганизации в структуру возврата
        ПараметрЗакрытия = Новый Структура("Номенклатура, Количество, Цена, Валюта, ККТОрганизации");
        ЗаполнитьЗначенияСвойств(ПараметрЗакрытия, ЭтотОбъект);
        Закрыть(ПараметрЗакрытия);
    КонецЕсли;
    
КонецПроцедуры

В процедуре ПриСозданииНаСервере получаем остатки номенклатуры по регистру бухгалтерии и программно выводим их в таблицу на форме:


&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
    
    СписокСвойств = "Номенклатура, Количество, ЕдиницаИзмерения, Цена, Валюта, ЕстьКоличество, ЕстьЦена";
    ЗаполнитьЗначенияСвойств(ЭтотОбъект, Параметры, СписокСвойств);
    
    Если НЕ ЕстьКоличество И НЕ ЕстьЦена Тогда
        Отказ = Истина;
    КонецЕсли;
    
    СвойстваНоменклатуры = ОбщегоНазначения.ЗначенияРеквизитовОбъекта(Номенклатура, "Наименование, ЕдиницаИзмерения");
    
    Заголовок = СвойстваНоменклатуры.Наименование;
    Если НЕ ЗначениеЗаполнено(ЕдиницаИзмерения) Тогда
        ЕдиницаИзмерения = СвойстваНоменклатуры.ЕдиницаИзмерения;
    КонецЕсли;
    
    Элементы.Количество.Видимость       = ЕстьКоличество;
    Элементы.ЕдиницаИзмерения.Видимость = ЕстьКоличество;
    Элементы.Цена.Видимость             = ЕстьЦена;
    Элементы.ВалютаЦены.Видимость       = ЕстьЦена;
    Элементы.Сумма.Видимость            = ЕстьЦена;
    Элементы.ВалютаСуммы.Видимость      = ЕстьЦена;
    
    Сумма = Цена * Количество;
    
    // Получение остатков товара на счете 41.01
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |    ХозрасчетныйОстатки.Субконто3 КАК Склад,
        |    ХозрасчетныйОстатки.Субконто1 КАК Товар,
        |    ХозрасчетныйОстатки.КоличествоОстатокДт КАК Остаток
        |ИЗ
        |    РегистрБухгалтерии.Хозрасчетный.Остатки(&КонецПериода, Счет В ИЕРАРХИИ (&ВыбСчет), , Субконто1 = &Номенклатура) КАК ХозрасчетныйОстатки
        |СГРУППИРОВАТЬ ПО
        |    ХозрасчетныйОстатки.Субконто3,
        |    ХозрасчетныйОстатки.Субконто1,
        |    ХозрасчетныйОстатки.КоличествоОстатокДт
        |ИТОГИ ПО
        |    Склад";
    
    Запрос.УстановитьПараметр("ВыбСчет", ПланыСчетов.Хозрасчетный.НайтиПоКоду("41.01"));
    Запрос.УстановитьПараметр("КонецПериода", КонецМесяца(ТекущаяДата()));      
    Запрос.УстановитьПараметр("Номенклатура", Номенклатура.Ссылка);
    ТабВКоде = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.Прямой);
    
    ВывестиТаблицуЗначенияВКодеВТаблицуНаФорме(ТабВКоде, "ТабРеквизит", "ТабНаФорме");
    
КонецПроцедуры

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


&НаСервере
Процедура ВывестиТаблицуЗначенияВКодеВТаблицуНаФорме(ТабВКоде, ТабРеквизит, ТабНаФорме)
    
    // 1. Добавляем реквизиты формы
    НовыеРеквизиты = Новый Массив;
    Для Каждого Колонка Из ТабВКоде.Колонки Цикл
        НовыеРеквизиты.Добавить(
            Новый РеквизитФормы(Колонка.Имя, Колонка.ТипЗначения, ТабРеквизит)
        );
    КонецЦикла;
    ИзменитьРеквизиты(НовыеРеквизиты);
    
    // 2. Добавляем элементы управления на форму
    Для Каждого Колонка Из ТабВКоде.Колонки Цикл
        НовыйЭлемент = Элементы.Добавить(ТабРеквизит + "_" + Колонка.Имя, Тип("ПолеФормы"), Элементы[ТабНаФорме]);
        НовыйЭлемент.Вид = ВидПоляФормы.ПолеВвода;
        НовыйЭлемент.ПутьКДанным = ТабРеквизит + "." + Колонка.Имя;
    КонецЦикла;
    
    // 3. Заполняем данные
    ЗначениеВРеквизитФормы(ТабВКоде, ТабРеквизит);
    
КонецПроцедуры

Модификация модуля формы документа «Розничная продажа (чек)»

Перейдем к основной логике разделения чека. В форме документа ФормаДокументаОбщая перехватываем процедуру НапечататьЧек. Алгоритм разделяет строки единого чека по организациям, пропорционально распределяет оплаты, создает отдельные документы и последовательно инициирует печать на каждую ККТ через БПО (Библиотеку подключаемого оборудования).


&НаКлиенте
Процедура НапечататьЧек(ПараметрыОплаты)

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

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

Серверные методы формирования и разделения чеков

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


&НаСервере
Процедура ПолучитьТаблицуТоваровДляПечатиНаНесколькоОрганизаций(ВыбраннаяОрганизация, ВыбранныйСклад, НомерПрохода) 

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

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

&НаСервере
Функция ПолучитьПараметрыПробиваемогоЧека(СсылкаНаДокумент)
    
    ПолучаемДокументПробития = СсылкаНаДокумент.ПолучитьОбъект();
    ПараметрыДляПробитияЧека = Новый Структура;
    ПараметрыДляПробитияЧека.Вставить("УИНДокумента", СсылкаНаДокумент.УникальныйИдентификатор());
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |    ПодключаемоеОборудование.Ссылка КАК Ссылка
        |ИЗ
        |    Справочник.ПодключаемоеОборудование КАК ПодключаемоеОборудование
        |ГДЕ
        |    ПодключаемоеОборудование.Организация.Ссылка = &Организация";
    Запрос.УстановитьПараметр("Организация", ПолучаемДокументПробития.Организация.Ссылка);
    Результат = Запрос.Выполнить().Выбрать();
    
    УИНОборудования = Неопределено;
    Пока Результат.Следующий() Цикл
        УИНОборудования = Результат.Ссылка;    
    КонецЦикла;
    
    ПараметрыДляПробитияЧека.Вставить("УИНУстройства", УИНОборудования);
    ПараметрыДляПробитияЧека.Вставить("ОбъектПробития", СсылкаНаДокумент);
    Возврат ПараметрыДляПробитияЧека;
    
КонецФункции

Важные практические рекомендации и альтернативы

При внедрении подобной доработки необходимо учитывать ряд технических ограничений:

  1. Эквайринговые операции: При оплате банковской картой нельзя провести одну транзакцию по терминалу для двух разных организаций (если у них разные договоры эквайринга и расчетные счета). Кассиру потребуется дважды прикладывать карту покупателя на разные суммы либо использовать банковский терминал с поддержкой мультиэквайринга.
  2. Безопасность обновлений: Чтобы сохранить типовую поддержку конфигурации 1С:Бухгалтерия предприятия 3.0, весь дополнительный программный код рекомендуется переносить в Расширение конфигурации (.cfe) с помощью аннотаций &Вместо, &После или &Перед.
  3. Альтернатива без доработки конфигурации (Агентская схема): Если организации могут заключить между собой агентский договор или договор комиссии, дорабатывать разделение чеков не требуется. По 54-ФЗ достаточно использовать одну ККТ и печатать единый чек, в котором для чужих позиций заполняются теги агента и ИНН принципала (поставщика). Этот механизм штатно поддерживается типовой БП 3.0.
← На главную