При автоматизации розничной торговли, курьерской доставки или пассажирских перевозок разработчики часто сталкиваются с задачей предварительной генерации чеков. Типичный сценарий: в течение дня оформляется множество заказов или продается большое количество билетов, но физически пробивать чеки на кассе (ККТ) планируется только в конце смены или по факту оплаты. При этом на самом билете или бланке заказа крайне желательно напечатать QR-код для проверки чека.
В этой статье мы подробно разберем, почему технически невозможно получить оригинальный фискальный QR-код до момента пробития чека, как эту проблему решает действующее законодательство РФ (54-ФЗ) для сферы транспорта и услуг, и как правильно спроектировать архитектуру решения в среде 1С:Предприятие 8.
Для начала проанализируем, как устроен QR-код кассового чека. Согласно требованиям ФНС, QR-код на чеке должен содержать строго определенный набор реквизитов, закодированных в текстовую строку:
t).s).n).fn).i).fp или ФПД).Главным техническим тупиком является именно фискальный признак документа (ФПД). ФПД — это уникальная цифровая подпись чека, которая генерируется аппаратным криптографическим процессором, находящимся внутри защищенного устройства — фискального накопителя (ФН). ФН высчитывает ФПД в режиме реального времени на основе текущего состояния внутренних счетчиков ККТ (которые инкрементируются с каждым пробитым чеком), точного времени и контрольной суммы данных самого чека.
Следовательно, ни одна внешняя программа (включая 1С) не может предугадать, какой ФПД выдаст фискальный накопитель для конкретного чека, пока этот чек не будет физически записан в память ФН. Попытка подделать ФПД или сгенерировать его программно без участия сертифицированного ФН приведет к тому, что такой чек не пройдет проверку в ФНС.
Многие разработчики и бизнес-аналитики опасаются, что неотправка чека в ОФД в момент приема денег является грубым нарушением закона 54-ФЗ. Однако для пассажирского транспорта и некоторых видов выездной торговли законодательство делает официальное исключение.
Согласно пункту 5.9 статьи 1.2 Федерального закона № 54-ФЗ, при продаже проездных документов (билетов) и талонов для проезда в общественном транспорте в салоне транспортного средства, перевозчик имеет право не выдавать классический кассовый чек на бумаге в момент расчета. Вместо этого разрешено использовать схему отложенной фискализации:
Таким образом, схема «собираем продажи за смену в 1С — фискализируем пачкой вечером через облачную ККТ» полностью легальна и активно применяется крупнейшими транспортными операторами.
Чтобы предоставить пассажиру возможность проверить чек, но при этом обойти аппаратное ограничение ФН, мы разделим понятия фискального QR-кода и сервисного QR-кода:
https://mytrans.ru/check?id=7c8d9e0a-1a2b-3c4d-5e6f-7a8b9c0d1e2f.Рассмотрим по шагам, как работает этот сценарий:
Рассмотрим практическую реализацию этой схемы в конфигурациях на платформе 1С:Предприятие 8.3.
При оформлении продажи билета нам необходимо программно сгенерировать UUID и сформировать строку для сервисного QR-кода. В платформе 1С начиная с версии 8.3.10 доступен встроенный генератор двухмерных штрихкодов без использования сторонних библиотек и компонент.
Посмотрим на пример кода формирования билета и вывода сервисного QR-кода в табличный документ:
&НаСервере
Функция СформироватьБилетНаПечать(ДанныеПродажи)
// Инициализируем табличный документ билета
ТабДок = Новый ТабличныйДокумент;
Макет = ПолучитьМакет("МакетБилета");
ОбластьБилет = Макет.ПолучитьОбласть("Билет");
// Генерируем уникальный идентификатор для отложенной фискализации
НовыйGUID = Новый УникальныйИдентификатор();
СтрокаGUID = Строка(НовыйGUID);
// Заполняем параметры макета
ОбластьБилет.Параметры.НомерБилета = ДанныеПродажи.Номер;
ОбластьБилет.Параметры.Цена = ДанныеПродажи.Цена;
ОбластьБилет.Параметры.Дата = ТекущаяДатаСеанса();
// Формируем сервисную ссылку для проверки
СервиснаяСсылка = "https://check.mytransport.ru/verify?ticket_id=" + СтрокаGUID;
// Добавляем QR-код на табличный документ средствами платформы
РисунокQR = ОбластьБилет.Рисунки.Добавить(ТипРисункаТабличногоДокумента.QRКод);
РисунокQR.Верх = 10; // Позиционирование в мм
РисунокQR.Лево = 50;
РисунокQR.Ширина = 40;
РисунокQR.Высота = 40;
РисунокQR.Текст = СервиснаяСсылка;
ТабДок.Вывести(ОбластьБилет);
// Сохраняем информацию о билете в базу данных для последующей фискализации
ЗаписатьДанныеПроданногоБилета(ДанныеПродажи.Номер, ДанныеПродажи.Цена, НовыйGUID);
Возврат ТабДок;
КонецФункции
Создадим документ или регистр сведений ПроданныеБилеты, в котором будем хранить записи со статусом фискализации. Поля структуры данных:
ИдентификаторБилета (Строка, индексированное поле для быстрого поиска по UUID).Сумма (Число).СтатусФискализации (Перечисление: НеФискализирован, ОтправленВККТ, Фискализирован, Ошибка).РеквизитыЧека (Строка или подчиненные реквизиты: НомерФД, ФПД, ДатаЧека).Для того чтобы сайт проверки мог оперативно получать информацию о статусе билета, мы можем развернуть простейший веб-сервер или использовать механизм HTTP-сервисов в 1С.
Разберем пример реализации метода HTTP-сервиса 1С, который принимает ticket_id и возвращает JSON с текущим состоянием билета:
Функция ПолучитьСтатусБилета(Запрос)
ИдентификаторБилета = Запрос.ПараметрыЗапроса.Получить("ticket_id");
Ответ = Новый HTTPСервисОтвет(200);
Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
// Ищем билет в информационной базе
ЗапросБД = Новый Запрос;
ЗапросБД.Текст =
"ВЫБРАТЬ
| ПроданныеБилеты.СтатусФискализации КАК Статус,
| ПроданныеБилеты.ФПД КАК ФПД,
| ПроданныеБилеты.НомерФД КАК НомерФД,
| ПроданныеБилеты.ДатаЧека КАК ДатаЧека
|ИЗ
| РегистрСведений.ПроданныеБилеты КАК ПроданныеБилеты
|ГДЕ
| ПроданныеБилеты.ИдентификаторБилета = &ИдентификаторБилета";
ЗапросБД.УстановитьПараметр("ИдентификаторБилета", ИдентификаторБилета);
Выборка = ЗапросБД.Выполнить().Выбрать();
СтруктураОтвета = Новый Структура;
Если Выборка.Следующий() Тогда
Если Выборка.Статус = Перечисления.СтатусыФискализации.Фискализирован Тогда
СтруктураОтвета.Вставить("status", "fiscalized");
СтруктураОтвета.Вставить("fpd", Выборка.ФПД);
СтруктураОтвета.Вставить("fd", Выборка.НомерФД);
СтруктураОтвета.Вставить("date", Формат(Выборка.ДатаЧека, "ДФ=yyyy-MM-ddTHH:mm:ss"));
// Ссылка на ОФД для быстрого перехода
СтруктураОтвета.Вставить("ofd_url", "https://lk.platformaofd.ru/web/noauth/cheque?fp=" + Выборка.ФПД);
Иначе
СтруктураОтвета.Вставить("status", "processing");
СтруктураОтвета.Вставить("message", "Билет зарегистрирован. Чек формируется облачной кассой до конца дня.");
КонецЕсли;
Иначе
Ответ.КодСостояния = 404;
СтруктураОтвета.Вставить("status", "error");
СтруктураОтвета.Вставить("message", "Билет с указанным идентификатором не найден.");
КонецЕсли;
// Сериализация структуры в JSON
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, СтруктураОтвета);
Ответ.УстановитьТелоИзСтроки(ЗаписьJSON.Закрыть());
Возврат Ответ;
КонецФункции
Для минимизации издержек и отказа от покупки физических касс в каждый автобус целесообразно использовать облачную ККТ (например, АТОЛ Онлайн, Кит Онлайн, Ferma и др.). Интеграция 1С с облачным сервисом осуществляется по API.
Рассмотрим процедуру регламентного задания в 1С, которое раз в день выбирает все нефискализированные продажи за смену и пачкой отправляет их на фискализацию:
&НаСервере
Процедура ВыполнитьПакетнуюФискализацию()
// Получаем список нефискализированных билетов
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ПроданныеБилеты.ИдентификаторБилета КАК Идентификатор,
| ПроданныеБилеты.Сумма КАК Сумма
|ИЗ
| РегистрСведений.ПроданныеБилеты КАК ПроданныеБилеты
|ГДЕ
| ПроданныеБилеты.СтатусФискализации = ЗНАЧЕНИЕ(Перечисление.СтатусыФискализации.НеФискализирован)";
РезультатЗапроса = Запрос.Выполнить();
Если РезультатЗапроса.Пустой() Тогда
Возврат; // Нечего фискализировать
КонецЕсли;
Выборка = РезультатЗапроса.Выбрать();
// Настройки интеграции с Облачной ККТ (условный пример для API кассы)
АдресСервиса = "partner.atol.ru";
РесурсAPI = "/v4/sell";
ТокенАвторизации = ПолучитьТокенДоступаККассе();
Пока Выборка.Следующий() Цикл
// Формируем JSON-пакет чека
СтруктураЧека = Новый Структура;
СтруктураЧека.Вставить("external_id", Выборка.Идентификатор);
// Описываем покупку
ПозицияЧека = Новый Структура;
ПозицияЧека.Вставить("name", "Проездной билет пассажирский");
ПозицияЧека.Вставить("price", Выборка.Сумма);
ПозицияЧека.Вставить("quantity", 1);
ПозицияЧека.Вставить("sum", Выборка.Сумма);
ПозицияЧека.Вставить("tax", "none"); // Без НДС
МассивПозиций = Новый Массив;
МассивПозиций.Добавить(ПозицияЧека);
Receipt = Новый Структура;
Receipt.Вставить("items", МассивПозиций);
Receipt.Вставить("total", Выборка.Сумма);
СтруктураЧека.Вставить("receipt", Receipt);
// Сериализуем и отправляем POST-запрос
СтрокаJSON = СформироватьСтрокуJSON(СтруктураЧека);
HTTPСоединение = Новый HTTPСоединение(АдресСервиса, , , , , , Новый ЗащищенноеСоединениеOpenSSL());
HTTPЗапрос = Новый HTTPЗапрос(РесурсAPI);
HTTPЗапрос.Заголовки.Вставить("Token", ТокенАвторизации);
HTTPЗапрос.Заголовки.Вставить("Content-Type", "application/json");
HTTPЗапрос.УстановитьТелоИзСтроки(СтрокаJSON);
Попытка
HTTPОтвет = HTTPСоединение.ОтправитьДляОбработки(HTTPЗапрос);
Если HTTPОтвет.КодСостояния = 200 ИЛИ HTTPОтвет.КодСостояния = 201 Тогда
// Помечаем в базе, что билет отправлен в ККТ
ОбновитьСтатусБилета(Выборка.Идентификатор, Перечисления.СтатусыФискализации.ОтправленВККТ);
Иначе
ЗаписатьЛогОшибки(Выборка.Идентификатор, HTTPОтвет.ПолучитьТелоКакСтроку());
КонецЕсли;
Исключение
ЗаписатьЛогОшибки(Выборка.Идентификатор, ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
КонецПроцедуры
Примечание: После отправки пакета касса обрабатывает его асинхронно. Нам потребуется еще одно регламентное задание, которое будет периодически запрашивать у облачного сервиса результаты фискализации по external_id (в качестве которого мы передали UUID билета), забирать значения ФД, ФПД и переводить статус билета в «Фискализирован».
Если транспортное предприятие стремится полностью исключить бумажные билеты и ручную работу контролеров/кондукторов, идеальным решением является внедрение оплаты через Систему быстрых платежей (СБП) с автоматической фискализацией:
Современные конфигурации 1С (такие как 1С:Розница 3.0, 1С:Управление нашей фирмой (УНФ)) имеют штатные модули интеграции со СБП крупных банков, что позволяет настроить этот процесс с минимальными затратами на программирование.
Полноценный фискальный QR-код невозможно получить до физического пробития чека из-за особенностей криптографической защиты фискального накопителя. Тем не менее, для транспорта и выездной торговли закон 54-ФЗ разрешает отложенную фискализацию в течение суток. Лучшим техническим решением этой задачи является использование сервисных QR-кодов на билетах, ведущих на промежуточный веб-портал, интеграция 1С с облачной ККТ по API и пакетное проведение чеков в конце смены.