При интеграции кассового оборудования и внедрении систем лояльности (скидок, списания бонусов) разработчики часто сталкиваются с ошибкой «Сумма всех типов оплаты меньше итога чека» или расхождением итогов на 1–2 копейки. Выясним причину такого поведения оборудования и разберем по шагам практические способы устранения проблемы в коде 1С.
Проанализируем математику работы фискальных регистраторов. В прошивках кассовых аппаратов (семейства «Штрих-М», «Элвес», «АТОЛ» и других) расчет стоимости товарной строки выполняется аппаратно по строгому правилу:
СтоимостьПозиции = Округлить(Цена × Количество, 2)
Кассовое устройство не позволяет передать произвольную итоговую сумму строки в обход цены и количества. Если в учетной системе после применения скидки сумма распределилась с образованием периодической дроби (например, итоговая сумма позиции 104 рубля за 3 штуки товара), цена за единицу составляет 34.6666... рубля.
При передаче в свойство Объект.Price драйвер или учетная система выполняет округление цены до копеек — получается 34.67. При аппаратном перемножении в кассе получается:
34.67 × 3 = 104.01 руб.
В результате чек на ККТ формируется на 104.01 руб., в то время как 1С передает на закрытие чека тип оплаты на исходную сумму 104.00 руб. Драйвер справедливо блокирует операцию из-за нехватки оплаты.
Рассмотрим самое надежное и соответствующее требованиям фискального законодательства решение — разделение штучной товарной позиции на две строки в момент передачи данных на печать. Основная часть товара продается по базовой округленной цене, а на последнюю 1 штуку относится копеечная разница.
Разберем математику расчета:
Цена1 = Окр(СуммаПозиции / Количество, 2).Количество1 = Количество - 1.Сумма1 = Цена1 * Количество1.Цена2 = СуммаПозиции - Сумма1.Посмотрим на пример реализации алгоритма печати чека в цикле обхода строк документа:
Пока СписокТоваров.ПолучитьСтроку() = 1 Цикл
СтрокаДляПечати = СокрЛП(СписокТоваров.ТовНаим);
ДлинаСтроки = 32;
ВсегоКол = СписокТоваров.Количество;
ВсегоСум = СписокТоваров.Сумма;
Если ВсегоКол > 1 Тогда
// Рассчитываем базовую цену с округлением
ЦенаБазовая = Окр(ВсегоСум / ВсегоКол, 2);
// Если при базовой цене сумма не сходится, разбиваем позицию
Если (ЦенаБазовая * ВсегоКол) <> ВсегоСум Тогда
Кол1 = ВсегоКол - 1;
Сум1 = ЦенаБазовая * Кол1;
ЦенаОстаток = ВсегоСум - Сум1;
// Печать первой части позиции
Объект.Password = Пароль;
Объект.StringForPrinting = Лев(СтрокаДляПечати, ДлинаСтроки);
Объект.Price = ЦенаБазовая;
Объект.Quantity = Кол1;
Объект.Department = НомерСекции;
Объект.Sale();
// Печать второй части (1 шт. с корректировкой копеек)
Объект.Password = Пароль;
Объект.StringForPrinting = Лев(СтрокаДляПечати, ДлинаСтроки);
Объект.Price = ЦенаОстаток;
Объект.Quantity = 1;
Объект.Department = НомерСекции;
Объект.Sale();
Продолжить;
КонецЕсли;
КонецЕсли;
// Стандартная печать строки (если количество = 1 или сумма делится без остатка)
Объект.Password = Пароль;
Объект.StringForPrinting = Лев(СтрокаДляПечати, ДлинаСтроки);
Если ВсегоКол = 0 Тогда
Объект.Price = СписокТоваров.Цена;
Иначе
Объект.Price = Окр(ВсегоСум / ВсегоКол, 2);
КонецЕсли;
Объект.Quantity = ВсегоКол;
Объект.Department = НомерСекции;
Объект.Sale();
КонецЦикла;
В результате покупатель получает чек, где сумма по двум строкам идеально совпадает с суммой документа, а касса закрывается без ошибок.
Если разница между ценой основной партии и последней штуки кажется визуально заметной, рассмотрим альтернативный вариант равномерного распределения копеек остатка:
ЦенаБазовая = Цел((СуммаПозиции / Количество) * 100) / 100.ОстатокКопеек = Окр((СуммаПозиции - (ЦенаБазовая * Количество)) * 100, 0).Количество - ОстатокКопеек по цене ЦенаБазовая, вторая с количеством ОстатокКопеек по цене ЦенаБазовая + 0.01.Такой подход делает разницу в цене между строками чека ровно в 1 копейку.
Проанализируем работу с драйвером ККТ без предварительного расчета конечной цены в 1С. Если модель фискального регистратора и протокол поддерживают скидки на позиции, мы можем передавать исходную учетную цену без скидки и значение скидки в специальное свойство драйвера (например, процент или абсолютную сумму скидки на позицию):
Объект.Price = СписокТоваров.ЦенаБезСкидки;
Объект.Quantity = СписокТоваров.Количество;
Объект.Discount = СуммаСкидкиНаПозицию;
Объект.Sale();
В этом случае округление и распределение скидок выполняет сам фискальный регистратор по заложенным в его микропрограмму правилам. Однако следует учитывать, что итоговая сумма чека на кассе может отличаться от документа 1С на 1–2 копейки, поэтому при закрытии чека сумму оплаты необходимо запрашивать из регистратора или выполнять корректировку копеек в учетной программе.
Обратим внимание: разбиение на строки методом N - 1 применимо исключительно к штучному товару с целым количеством. Если позиция весовая (например, 1.435 кг), делить количество на единицы нельзя. Для весовых товаров суммовые скидки распределяют с округлением цены до минимального шага веса либо относят всю сумму скидки программы лояльности на штучные позиции чека.