Как реализовать накопительную скидку для клиентов в 1С, начиная с пятой стрижки?

Программист 1С v8.3 (Управляемые формы) Управленческий учет Сфера услуг, туризм и социальный сектор
← На главную

Мы сталкиваемся с типовой задачей для многих сфер услуг, когда необходимо поощрять постоянных клиентов. В нашем случае, парикмахерская хочет предоставлять скидку в 3% клиенту, начиная с пятой стрижки. Разберем, как эффективно реализовать такую логику в системе 1С, используя наиболее подходящие объекты и механизмы платформы.

Для решения этой задачи нам потребуется отслеживать количество стрижек, которые уже были оказаны каждому клиенту. Это позволит нам динамически определять статус клиента и применять скидку. Мы рассмотрим несколько подходов, но основной и наиболее рекомендуемый — использование регистра накопления.

Основной подход: Использование регистра накопления

Применение регистра накопления для учета оказанных услуг является наиболее гибким, масштабируемым и "правильным" с точки зрения архитектуры 1С. Он позволяет быстро получать агрегированные данные (например, количество стрижек) без необходимости перебирать большое количество документов, что важно для производительности системы по мере её роста.

Разберем по шагам, как настроить регистр накопления и использовать его для нашей задачи.

Шаг 1: Создание регистра накопления

Прежде всего, нам нужно создать новый регистр накопления в конфигурации. Этот регистр будет хранить информацию о каждой оказанной услуге для каждого клиента.

  1. Создание объекта: В дереве объектов конфигурации создадим новый объект типа "Регистр накопления". Назовем его, например, УчетСтрижекКлиентов.
  2. Вид регистра: Установим "Вид регистра" в значение Обороты, так как нам важно подсчитывать количество событий (стрижек).
  3. Измерения: Измерения определяют, в каком разрезе мы будем накапливать данные. Нам необходимо отслеживать стрижки по каждому клиенту.
    • Добавим измерение Клиент. Тип данных для этого измерения должен быть СправочникСсылка.Клиенты. Это критически важно, так как именно ссылка на элемент справочника "Клиенты" позволит нам идентифицировать конкретного клиента. В сообщении 2 и 4 на форуме поднимался вопрос о типе измерения Клиент — оно должно быть ссылкой на справочник, а не на документ. Документ будет выступать в роли регистратора.
  4. Ресурсы: Ресурсы регистра хранят количественные показатели, которые мы хотим накапливать. В нашем случае нам достаточно ресурса для подсчета стрижек.
    • Добавим ресурс КоличествоСтрижек. Тип данных - Число, точность - 0, неотрицательное. Значение этого ресурса будет увеличиваться на 1 при каждой новой стрижке.
    • Опционально, мы можем добавить ресурс СуммаУслуг (Число, точность 2), чтобы также отслеживать общую сумму, потраченную клиентом, что может быть полезно для будущих программ лояльности. Для текущей задачи это не является обязательным.
  5. Регистраторы: Укажем документ, который будет "двигать" этот регистр, то есть создавать в нем записи при своем проведении.
    • Нам необходим документ, который фиксирует факт оказания услуги. Согласно заданию, это документ Работа. Добавим его в список регистраторов.

Итак, структура нашего регистра УчетСтрижекКлиентов будет выглядеть так:

  1. Имя: УчетСтрижекКлиентов
  2. Вид: Обороты
  3. Измерения:
    • Клиент (Тип: СправочникСсылка.Клиенты)
  4. Ресурсы:
    • КоличествоСтрижек (Тип: Число)
    • СуммаУслуг (Тип: Число, опционально)
  5. Регистраторы:
    • Документ.Работа

Шаг 2: Настройка движений документа "Работа"

Теперь нам нужно настроить документ Работа так, чтобы при его проведении создавались записи в регистре УчетСтрижекКлиентов. Предположим, что документ Работа имеет следующие реквизиты:

Откроем модуль объекта документа Работа и добавим код в обработчик события ОбработкаПроведения.


Процедура ОбработкаПроведения(Отказ, РежимПроведения)
    // Устанавливаем режим записи движений регистра
    Движения.УчетСтрижекКлиентов.Записывать = Истина;

    // Создаем новое движение в регистре накопления
    Движение = Движения.УчетСтрижекКлиентов.Добавить();
    Движение.Период = Дата; // Дата документа
    Движение.Клиент = Клиент; // Ссылка на клиента из документа
    Движение.КоличествоСтрижек = 1; // Увеличиваем счетчик стрижек на 1
    Если Движения.УчетСтрижекКлиентов.ПолучитьВид() = ВидДвиженияНакопления.Приход Тогда
        // Для регистра оборотов, ресурс добавляется напрямую, без прихода/расхода
        // Здесь можно было бы добавить и СуммаУслуг, если она есть
        // Движение.СуммаУслуг = ЦенаСоСкидкой;
    КонецЕсли;

    // ... Здесь могут быть движения по другим регистрам, если они есть ...

КонецПроцедуры

Таким образом, каждый раз, когда мы проводим документ Работа, в регистре УчетСтрижекКлиентов будет появляться новая запись, увеличивающая количество стрижек для конкретного клиента.

Шаг 3: Получение количества предыдущих стрижек (Запрос)

Теперь, когда у нас есть механизм для накопления данных, нам нужно научиться получать количество уже оказанных стрижек для клиента. Это делается с помощью запроса к регистру накопления. Запрос будет выполняться в модуле формы документа Работа.

Создадим функцию, которая будет возвращать количество стрижек для заданного клиента до определенной даты (или исключая текущий документ, если это необходимо).


// Функция для получения количества стрижек клиента
// до текущего момента (или до определенной даты)
Функция ПолучитьКоличествоСтрижекКлиента(Клиент, ДатаДокумента, ТекущийДокумент)

    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |    УчетСтрижекКлиентовОбороты.КоличествоСтрижек КАК КоличествоЗаписей
        |ИЗ
        |    РегистрНакопления.УчетСтрижекКлиентов.Обороты(
        |        НАЧАЛОПЕРИОДА(&ДатаНачала, ДЕНЬ),
        |        &ДатаДокумента,
        |        Авто,
        |        Клиент = &Клиент
        |        {(Регистратор).Ссылка <> &ТекущийДокумент}
        |    ) КАК УчетСтрижекКлиентовОбороты
        |ГДЕ
        |    УчетСтрижекКлиентовОбороты.Клиент = &Клиент";
    
    // Параметры запроса
    Запрос.УстановитьПараметр("Клиент", Клиент);
    Запрос.УстановитьПараметр("ДатаДокумента", ДатаДокумента);
    Запрос.УстановитьПараметр("ДатаНачала", Дата(1,1,1)); // С начала времен, чтобы учесть все стрижки
    Запрос.УстановитьПараметр("ТекущийДокумент", ТекущийДокумент); // Исключаем текущий документ из подсчета

    РезультатЗапроса = Запрос.Выполнить();
    Выборка = РезультатЗапроса.Выбрать();

    Если Выборка.Следующий() Тогда
        Возврат Выборка.КоличествоЗаписей;
    Иначе
        Возврат 0;
    КонецЕсли;

КонецФункции

Мы используем виртуальную таблицу Обороты регистра накопления. Параметры: ДатаНачала и ДатаДокумента позволяют нам ограничить период, а условие Клиент = &Клиент фильтрует по конкретному клиенту. Очень важно использовать {(Регистратор).Ссылка <> &ТекущийДокумент}, если мы хотим получить количество стрижек *до* текущего документа. Если же нам нужно количество *включая* текущий документ (что менее типично для расчета скидки, но может быть полезно для определения нового статуса после проведения), этот параметр можно убрать или изменить логику.

Шаг 4: Размещение логики применения скидки

Логику проверки количества предыдущих стрижек и применения скидки наиболее эффективно размещать в модуле формы документа Работа. Это позволяет динамически рассчитывать и показывать цену со скидкой клиенту ещё до того, как документ будет записан и проведён. Мы хотим, чтобы скидка применялась сразу после выбора клиента или стрижки.

  1. Определение момента расчета: Логично выполнять расчет скидки при изменении поля Клиент или Стрижка на форме документа.
  2. Применение скидки: Скидка в 3% должна быть применена к полю ЦенаБезСкидки, а результат записан в ЦенаСоСкидкой.
  3. Информирование пользователя: Для удобства можно выводить на форму информацию о текущем статусе клиента (например, "Новый клиент", "Постоянный клиент") или о примененной скидке.

Рассмотрим пример кода для модуля формы документа Работа.

Начнем с функции, которая будет выполнять весь расчет скидки для текущего документа. Эту функцию можно вызывать из различных обработчиков событий.


// В модуле формы документа "Работа"

// Процедура для расчета стоимости стрижки с учетом скидки
Процедура РассчитатьСкидкуИСтоимость()

    Если Объект.Клиент.Пустая() ИЛИ Объект.Стрижка.Пустая() Тогда
        // Если клиент или стрижка не выбраны, скидка не применяется
        Объект.ЦенаБезСкидки = 0;
        Объект.ПроцентСкидки = 0;
        Объект.ЦенаСоСкидкой = 0;
        Объект.СтатусКлиентаНаМоментОплаты = "Клиент не выбран";
        Возврат;
    КонецЕсли;

    // Получаем базовую стоимость стрижки из справочника
    // Предполагаем, что у Стрижки есть реквизит "Стоимость"
    Объект.ЦенаБезСкидки = Объект.Стрижка.Стоимость;

    // Определяем количество предыдущих стрижек
    // Важно: передаем ссылку на текущий объект, чтобы исключить его из подсчета
    // для корректного расчета статуса *до* текущей стрижки
    КоличествоПредыдущихСтрижек = ПолучитьКоличествоСтрижекКлиента(Объект.Клиент, Объект.Дата, Объект.Ссылка); 
    
    // Применяем логику скидки
    Если КоличествоПредыдущихСтрижек >= 4 Тогда // Начиная с 5-ой стрижки, то есть после 4-х предыдущих
        Объект.ПроцентСкидки = 3;
        Объект.СтатусКлиентаНаМоментОплаты = "Постоянный клиент (скидка 3%)";
    Иначе
        Объект.ПроцентСкидки = 0;
        Объект.СтатусКлиентаНаМоментОплаты = "Новый клиент";
    КонецЕсли;

    // Рассчитываем итоговую стоимость
    Объект.ЦенаСоСкидкой = Объект.ЦенаБезСкидки * (1 - Объект.ПроцентСкидки / 100);

КонецПроцедуры

Теперь нам нужно вызвать эту процедуру при изменении ключевых полей:


// Обработчик события ПриИзменении для поля Клиент
Процедура КлиентПриИзменении(Элемент)
    РассчитатьСкидкуИСтоимость();
КонецПроцедуры

// Обработчик события ПриИзменении для поля Стрижка
Процедура СтрижкаПриИзменении(Элемент)
    РассчитатьСкидкуИСтоимость();
КонецПроцедуры

// Обработчик события ПриСозданииНаСервере (для первичного расчета при открытии формы)
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
    Если НЕ Объект.Ссылка.Пустая() Тогда // Если документ уже существует
        РассчитатьСкидкуИСтоимость();
    КонецЕсли;
КонецПроцедуры

// Обработчик события ПриОткрытии (если ПриСозданииНаСервере не срабатывает для существующих)
Процедура ПриОткрытии(Отказ)
    РассчитатьСкидкуИСтоимость();
КонецПроцедуры

Таким образом, при выборе клиента или стрижки, система автоматически проверит, сколько раз клиент уже стригся, и применит соответствующую скидку, обновив поля ПроцентСкидки и ЦенаСоСкидкой. Также будет показан статус клиента. Заметьте, что ПолучитьКоличествоСтрижекКлиента вызывается с параметром Объект.Ссылка для &ТекущийДокумент, чтобы исключить *текущий, ещё не проведенный* документ из подсчета, поскольку скидка должна применяться *после* 4-х стрижек, то есть на пятую.

Шаг 5: Применение скидки

В приведенном выше коде мы уже реализовали применение скидки:

Объект.ЦенаСоСкидкой = Объект.ЦенаБезСкидки * (1 - Объект.ПроцентСкидки / 100);

Эта формула корректно уменьшает базовую стоимость на заданный процент. Важно, чтобы реквизиты ЦенаБезСкидки, ПроцентСкидки и ЦенаСоСкидкой были созданы в документе Работа.

Автоматический статус "Постоянный клиент"

В сообщении 4 на форуме автор предлагает использовать реквизит ПризнакПостоянногоКлиента в справочнике "Клиенты". Как правильно подсказали в сообщении 2, не рекомендуется хранить такой признак непосредственно в справочнике. Почему?

  1. Расчетный характер: Статус "постоянный" является расчетным. Он зависит от количества стрижек.
  2. Неактуальность данных: Если одна из предыдущих стрижек будет отменена или помечена на удаление, клиент может перестать быть "постоянным" по нашим правилам. Если статус будет записан в справочник, он не обновится автоматически, что приведет к некорректным скидкам.
  3. Избыточность: Мы всегда можем получить этот статус в любой момент с помощью запроса.

Вместо хранения мы динамически рассчитываем статус и выводим его на форму (мы использовали реквизит СтатусКлиентаНаМоментОплаты для этого в документе, который является информационным и не хранится в справочнике Клиенты). Это обеспечивает актуальность данных и простоту поддержки.

Альтернативный подход: Прямой запрос к документам

Как упоминалось в сообщениях 2 и 3, для новичков может быть проще напрямую считать количество документов Работа для клиента. Этот метод проще в реализации на первый взгляд, но имеет серьезные недостатки:

  1. Производительность: При большом количестве документов и клиентов, прямой запрос к таблицам документов может быть значительно медленнее, чем запрос к регистру накопления, который оптимизирован для агрегации данных.
  2. Сложность фильтрации: Регистр накопления уже содержит только проведенные документы. При прямом запросе к документам нужно дополнительно фильтровать по проведенным документам, а также учитывать возможные пометки удаления.

Пример прямого запроса к документам:


// Функция для получения количества стрижек клиента прямым запросом к документам
Функция ПолучитьКоличествоСтрижекКлиентаПрямымЗапросом(Клиент, ДатаДокумента, ТекущийДокумент)

    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |    КОЛИЧЕСТВО(ДокументРабота.Ссылка) КАК КоличествоЗаписей
        |ИЗ
        |    Документ.Работа КАК ДокументРабота
        |ГДЕ
        |    ДокументРабота.Клиент = &Клиент
        |    И ДокументРабота.Проведен = ИСТИНА
        |    И ДокументРабота.Дата < &ДатаДокумента
        |    И ДокументРабота.Ссылка <> &ТекущийДокумент";
    
    Запрос.УстановитьПараметр("Клиент", Клиент);
    Запрос.УстановитьПараметр("ДатаДокумента", ДатаДокумента);
    Запрос.УстановитьПараметр("ТекущийДокумент", ТекущийДокумент);

    РезультатЗапроса = Запрос.Выполнить();
    Выборка = РезультатЗапроса.Выбрать();

    Если Выборка.Следующий() Тогда
        Возврат Выборка.КоличествоЗаписей;
    Иначе
        Возврат 0;
    КонецЕсли;

КонецФункции

Как видим, запрос выглядит схоже, но внутренние механизмы 1С для работы с регистрами накопления делают первый подход предпочтительным для решения подобных задач.

Рекомендации и дополнительные моменты

  1. Информирование пользователя: Мы уже включили вывод статуса клиента на форму. Это повышает удобство работы кассира или администратора, позволяя им сразу видеть, какую скидку получает клиент.
  2. Пересчет скидок при изменении: Убедитесь, что функция РассчитатьСкидкуИСтоимость() вызывается при любых изменениях, которые могут повлиять на скидку (например, изменение даты документа, если правила скидок зависят от периода).
  3. Оптимизация: Для очень больших баз данных и очень частых запросов к регистру можно рассмотреть кэширование результатов запросов для одного клиента на время работы с формой документа, но для большинства парикмахерских это не потребуется.
  4. Гибкость скидок: Если в будущем потребуется более сложная система скидок (например, разные проценты для разных видов стрижек или по сумме накоплений), регистр накопления с ресурсом СуммаУслуг станет отличной основой для таких расширений.

Итак, мы рассмотрели подробное решение задачи по реализации накопительной скидки в 1С. Применив регистр накопления и запросы, мы создали надежный и производительный механизм для определения статуса клиента и автоматического применения скидок, что способствует повышению лояльности клиентов в вашей парикмахерской.

← На главную