При внедрении и настройке бонусных программ лояльности в программных продуктах 1С (таких как «1С:Управление торговлей», «1С:Розница» или «1С:Комплексная автоматизация») технические специалисты часто сталкиваются со специфическим поведением системы в Рабочем месте кассира (РМК). Две наиболее распространенные проблемы — это некорректный пересчет общей суммы чека в сторону увеличения при частичной оплате бонусами и начисление новых бонусных баллов на ту часть чека, которая уже была оплачена накопленными баллами.
В данной статье мы подробно разберем причины возникновения этих проблем, проанализируем внутренние механизмы работы конфигураций 1С и рассмотрим практические варианты их решения как программными методами, так и с помощью штатных настроек системы.
Рассмотрим ситуацию: кассир оформляет чек на сумму 90 рублей. Клиент решает оплатить часть покупки накопленными бонусами (например, списать 30 баллов). Кассир выбирает смешанную оплату, вводит 30 баллов для списания, но после этого общая сумма чека внезапно увеличивается до 120 рублей, а система по-прежнему требует доплатить наличными или картой те же 90 рублей. Получается, что баллы списались, но общая стоимость покупки выросла на сумму этих баллов.
Выясним причину такого поведения. В типовых конфигурациях 1С оплата бонусными баллами позиционируется не как скидка, а как встречное предоставление (форма оплаты). Соответственно, при оплате бонусами базовая цена и сумма по строке товара в табличной части не должны уменьшаться в плане коммерческой стоимости, но уменьшается сумма, подлежащая доплате «живыми» деньгами. Информация о списанных баллах фиксируется в специальном реквизите табличной части (например, СуммаСписанияБаллов или аналогичном).
В локализованных версиях (в частности, в локализации для Республики Беларусь от СООО «1С-Минск» для «1С:Управление торговлей 11.4») разработчики добавили собственный алгоритм в общий модуль для пересчета цен с учетом всех скидок. Данный алгоритм расположен в процедуре:
ОбработкаТабличныхЧастейСервер.ОбработатьСтрокуТЧ(ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения)
При вызове пересчета цен в этой процедуре программа рассчитывает сумму строки без учета ранее списанных баллов. Логика расчета выглядит следующим образом: берется цена товара, умножается на количество и вычитается сумма автоматических скидок. Реквизит СуммаСписанияБаллов при этом полностью игнорируется:
Сумма = (Цена * Количество) - АвтоматическиеСкидки
Когда кассир инициирует оплату баллами, система временно записывает списанные баллы в строку чека. Однако при финальном пересчете табличной части (который вызывается перед пробитием чека или при расчете скидок) срабатывает вышеуказанный код локализатора. Он перезаписывает сумму строки, возвращая ее к значению без учета оплаты бонусами. Из-за этого сумма списанных баллов фактически приплюсовывается обратно к итоговой сумме чека.
Для решения этой проблемы мы можем пойти двумя путями: быстрым (отключение ошибочного кода локализации) и корректным (доработка формулы расчета в расширении конфигурации).
Вариант А: Отключение кода пересчета (быстрое решение)
Если бизнес-логика вашего предприятия не требует специфического пересчета цен, внедренного локализатором в процедуру ОбработатьСтрокуТЧ, этот код можно просто закомментировать в основной конфигурации (предварительно сняв ее с поддержки) или переопределить через расширение с помощью метода &Вместо.
Вариант Б: Корректировка формулы расчета через расширение (рекомендуемое решение)
Для сохранения корректной работы всех механизмов системы правильнее будет изменить формулу расчета суммы строки, добавив в нее учет списанных баллов. Проанализируем пример доработки кода в процедуре ОбработатьСтрокуТЧ общего модуля:
&Вместо("ОбработатьСтрокуТЧ")
Процедура ИНТ_ОбработатьСтрокуТЧ(ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения)
// Вызываем оригинальный расчет локализатора (или пишем свою логику)
ПродолжитьВызов(ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
// Добавляем проверку наличия реквизита списания баллов и корректируем сумму строки
МетаданныеТЧ = ТекущаяСтрока.ЭтотОбъект().Метаданные();
Если МетаданныеТЧ.Реквизиты.Найти("СуммаСписанияБаллов") <> Неопределено Тогда
Если ТекущаяСтрока.СуммаСписанияБаллов > 0 Тогда
// Корректируем сумму строки, вычитая из нее оплату бонусами
ТекущаяСтрока.Сумма = ТекущаяСтрока.Цена * ТекущаяСтрока.Количество
- ТекущаяСтрока.СуммаАвтоматическойСкидки
- ТекущаяСтрока.СуммаСписанияБаллов;
КонецЕсли;
КонецЕсли;
КонецПроцедуры
При таком подходе принудительный пересчет цен от локализатора больше не будет завышать итоговую сумму чека, так как списанные баллы будут корректно уменьшать сумму строки.
Вторая классическая проблема программ лояльности — это избыточное начисление новых бонусов. Представим ситуацию: клиент приобретает товар стоимостью 100 рублей. По условиям его бонусной карты ему должно вернуться 10% в виде новых бонусов. При оплате клиент списывает 30 ранее накопленных баллов и доплачивает 70 рублей наличными. Логика бизнеса требует, чтобы новые бонусы начислялись только на сумму, оплаченную реальными деньгами (10% от 70 рублей = 7 бонусов). Однако стандартная система начисляет бонусы на полную стоимость товара (10% от 100 рублей = 10 бонусов), позволяя клиенту бесконечно генерировать «бонусы на бонусы».
Выясним, почему стандартные механизмы 1С ведут себя именно так. Как уже упоминалось, бонусные баллы в системе — это вид оплаты, а не скидка. С точки зрения программы, товар был продан за полные 100 рублей, поэтому и начисление бонусов происходит со всей суммы продажи.
Рассмотрим, как можно решить эту задачу штатными средствами и с помощью программирования.
В настройках бонусной программы лояльности присутствует параметр «При оплате позиции бонусными баллами» со значением «Бонусные баллы не начисляются» (в некоторых релизах это может быть флаг «Начислять баллы при оплате бонусами»).
Однако у этой настройки есть существенное ограничение: она работает по принципу «всё или ничего». Если этот параметр активирован, то при частичной оплате позиции бонусами начисление новых бонусов на эту позицию блокируется полностью. То есть, оплатив хотя бы 1 рубль баллами за товар стоимостью 10 000 рублей, клиент не получит вообще никаких бонусов на оставшиеся 9 999 рублей, оплаченных наличными.
Кроме того, кассиры часто жалуются, что даже при включении этой настройки бонусы все равно начисляются на полную сумму. Это связано с порядком действий в РМК:
Чтобы исправить этот рассинхрон без доработки кода, необходимо скорректировать права пользователей. Перейдем в раздел Администрирование -> Пользователи и права -> Персональные настройки -> Дополнительные права пользователей. Найдем и снимем флаг «Автоматический расчет скидок при редактировании документа продажи».
В таком случае скидки и начисления бонусов не будут рассчитываться динамически при каждом изменении строки. Система произведет расчет один раз — непосредственно перед пробитием чека, когда все виды оплат (включая списание бонусов) уже зафиксированы кассиром. Это позволит штатной настройке ограничений сработать корректно.
Если бизнесу критически важно начислять новые баллы только на ту часть стоимости товара, которая была оплачена денежными средствами (наличными или картой), нам потребуется доработка конфигурации.
Для реализации пропорционального расчета программисты используют два основных механизма:
Метод А: Использование механизма внешних обработок в скидках
В настройках автоматических скидок (которые отвечают за начисление бонусов) в качестве условия предоставления можно указать «Внешняя обработка». В коде этой обработки описывается алгоритм, который обращается к таблице оплат чека ККМ, вычисляет долю оплаты наличными/безналичными средствами для каждой строки и рассчитывает процент бонусов строго от этой доли.
Метод Б: Корректировка начисления через расширение перед проведением чека
Мы можем перехватить процедуру расчета начисления бонусов непосредственно перед проведением документа чека ККМ и принудительно уменьшить базу расчета на величину списанных баллов. Проанализируем пример такого алгоритма:
&После("РассчитатьБонусныеБаллыПриОплате")
Процедура ИНТ_РассчитатьБонусныеБаллыПриОплате(ДокументОбъект) Экспорт
// Проходим по табличной части документа
Для Каждого СтрокаТовара Из ДокументОбъект.Товары Цикл
// Проверяем, была ли по строке оплата баллами
Если ОбщегоНазначенияКлиентСервер.ЕстьРеквизитТабЧасти(СтрокаТовара, "СуммаСписанияБаллов")
И СтрокаТовара.СуммаСписанияБаллов > 0 Тогда
// Вычисляем фактическую сумму оплаты деньгами
СуммаОплатыДеньгами = СтрокаТовара.Сумма - СтрокаТовара.СуммаСписанияБаллов;
// Корректируем начисленные баллы пропорционально оплате деньгами
Если СтрокаТовара.Сумма > 0 Тогда
КоэффициентОплаты = СуммаОплатыДеньгами / СтрокаТовара.Сумма;
СтрокаТовара.СуммаНачисленныхБаллов = ОкруглитьБонусы(СтрокаТовара.СуммаНачисленныхБаллов * КоэффициентОплаты);
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Этот код позволяет гибко и справедливо рассчитывать новые бонусы: если клиент оплатил 70% стоимости товара деньгами, он получит ровно 70% от потенциально возможного объема новых бонусных баллов, что полностью решает проблему «бонусов на бонусы» и исключает финансовые потери предприятия.