Проблема «двойного чека» знакома каждому программисту 1С, работающему с торговым оборудованием. Ситуация классическая: программа отправляет команду на фискализацию, касса успешно печатает чек и записывает данные в фискальный накопитель (ФН), но в этот момент происходит разрыв связи, сбой питания или зависание драйвера. В результате 1С не получает подтверждение (номер чека и ФПД), транзакция считается неудачной, и пользователь нажимает кнопку «Пробить» еще раз. Мы получаем два идентичных чека в ОФД и расхождение в эквайринге.
В этой статье мы подробно разберем, как программно реализовать защиту от таких ситуаций, используя возможности драйвера Атол ДТО 10 и логику контроля на стороне 1С.
Многие разработчики ошибочно полагают, что в драйвер можно передать уникальный идентификатор (GUID) чека, и ККТ сама заблокирует дубль. К сожалению, на уровне протокола фискального накопителя такой проверки не существует. Тег 1084 (дополнительный реквизит пользователя) позволяет записать GUID в чек, но ККТ не анализирует его на уникальность перед печатью. Нам необходимо реализовать механизм проверки «до» и «после» вызова метода закрытия чека.
Рассмотрим алгоритм, основанный на фиксации состояния счетчиков ККТ перед началом операции. Если прошлая попытка пробития прервалась, мы сможем понять это по изменившемуся номеру последнего чека в кассе.
Для реализации этого метода нам потребуются методы запроса данных queryData(). Разберем по шагам логику, которую необходимо встроить в модуль обработки торгового оборудования:
fptr.closeReceipt() (закрытие чека) мы запрашиваем в ККТ текущий номер последнего документа или общее количество чеков.Пример кода для получения текущего номера чека из ККТ:
// Запрос текущего статуса ККТ
Объект.setParam(Объект.LIBFPTR_PARAM_DATA_TYPE, Объект.LIBFPTR_DT_STATUS);
Объект.queryData();
// Получаем номер последнего пробитого чека
НомерПоследнегоЧека = Объект.getParamInt(Объект.LIBFPTR_PARAM_RECEIPT_NUMBER);
// Номер смены также может быть полезен для контроля
НомерСмены = Объект.getParamInt(Объект.LIBFPTR_PARAM_SHIFT_NUMBER);
Более надежный способ — это прямой опрос фискального накопителя. Мы можем запросить данные о самом последнем документе, который был записан в ФН. Если сумма и время этого документа совпадают с нашим «проблемным» чеком, значит, дублирование запрещено.
Для этого в Атол ДТО 10 используется метод fnQueryData с типом LIBFPTR_FNDT_LAST_RECEIPT. Рассмотрим пример получения данных последнего фискального документа:
// Настраиваем запрос к ФН на получение последнего чека
fptr.setParam(fptr.LIBFPTR_PARAM_FN_DATA_TYPE, fptr.LIBFPTR_FNDT_LAST_RECEIPT);
fptr.fnQueryData();
// Извлекаем фискальный признак (ФПД) - это лучший уникальный ID чека
ФискальныйПризнак = fptr.getParamString(fptr.LIBFPTR_PARAM_FISCAL_SIGN);
// Извлекаем номер ФД (Фискального документа)
НомерФД = fptr.getParamInt(fptr.LIBFPTR_PARAM_DOCUMENT_NUMBER);
// Получаем сумму последнего чека для дополнительной сверки
// Внимание: ID 1020 соответствует тегу суммы чека
СуммаЧека = fptr.getParamDouble(1020);
Важный нюанс: Фискальный признак (ФП) — это уникальная цифровая последовательность, генерируемая ФН. Если вы сохраните ФП в документе 1С, вы всегда сможете доказать факт пробития чека. Проанализируем ситуацию: если после сбоя связи 1С делает этот запрос и видит ФП, который еще не зафиксирован в базе, программа должна обновить статус документа и прекратить попытки повторной печати.
Проанализируем пример комплексной процедуры контроля, которую можно интегрировать в конфигурацию. Мы будем использовать регистр сведений для хранения «намерения» пробития.
&НаКлиенте
Процедура ПередПробитиемЧека(Драйвер, ДокументСсылка)
// 1. Получаем номер чека из ККТ до начала печати
Драйвер.setParam(Драйвер.LIBFPTR_PARAM_DATA_TYPE, Драйвер.LIBFPTR_DT_STATUS);
Драйвер.queryData();
ТекущийНомерККТ = Драйвер.getParamInt(Драйвер.LIBFPTR_PARAM_RECEIPT_NUMBER);
// 2. Проверяем, нет ли зависших транзакций в 1С
СтруктураКонтроля = ПолучитьДанныеПоследнейПопытки(Пользователь);
Если СтруктураКонтроля.Документ <> Неопределено Тогда
Если ТекущийНомерККТ > СтруктураКонтроля.Значение И НЕ ДокументПробитВ1С(СтруктураКонтроля.Документ) Тогда
// Ситуация: номер в кассе вырос, а в 1С флаг не стоит. Чек ПРОБИТ.
ЗафиксироватьУспехПробития(СтруктураКонтроля.Документ, ТекущийНомерККТ);
ВызватьИсключение "Предыдущий чек был успешно пробит! Данные обновлены. Повторная печать отменена.";
КонецЕсли;
КонецЕсли;
// 3. Записываем текущее состояние перед пробитием
СохранитьНамерениеПробития(Пользователь, ТекущийНомерККТ, ДокументСсылка);
КонецПроцедуры
В этой схеме мы используем метод «защиты от дурака». Даже если кассир решит, что чек не вышел, программный код при повторном нажатии увидит, что счетчик ККТ увеличился, и принудительно завершит процесс в 1С без физической печати второго чека.
Если ваша архитектура позволяет использовать Atol Web-server, задача упрощается. При работе через JSON API у заданий есть параметр uuid. Рассмотрим преимущество этого подхода: если вы отправляете JSON-задание с одним и тем же uuid, Web-сервер Атола сам контролирует идемпотентность. Он проверит свою внутреннюю базу данных и, если задание с таким ID уже выполнялось, он просто вернет результат старого чека вместо генерации нового.
Однако для локальных решений на базе fptr10.dll описанный выше метод с запросом LIBFPTR_FNDT_LAST_RECEIPT остается самым надежным и быстрым.
Когда вы пробиваете чек с типом оплаты «Безналичные», 1С обычно сначала инициирует списание средств на банковском терминале, а затем дает команду ККТ. Если чек на ККТ задублируется, бухгалтер в конце смены увидит, что сумма по Z-отчету (ОФД) выше, чем сумма по отчету из банка. Это потребует ручного поиска операций и оформления чеков коррекции (возврата), что крайне трудозатратно для крупных торговых точек.
Используя предложенные методы контроля фискальных признаков, мы обеспечиваем целостность распределенной транзакции между 1С, банковским терминалом и фискальным накопителем.