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