В оптовой торговле часто возникает задача предоставления накопительных скидок за объем закупленного товара в определенном периоде. В типовом решении 1С:Управление торговлей ред. 11.4 (а также в 1С:ERP 2 и 1С:Комплексная автоматизация 2) этот механизм ориентирован на фактически совершенные продажи. Данные анализируются по регистру накопления ВыручкаИСебестоимостьПродаж.
Однако на практике оптовые клиенты не покупают все одной транзакцией. Они могут резервировать товар частями в течение месяца через разные документы ЗаказКлиента (например, из-за работы с разными договорами, контрагентами или организациями в рамках одного партнера). Возникает сложная задача: как учесть текущие резервы во всех еще не отгруженных заказах клиента за период, рассчитать общую скидку и динамически применить ее ко всем этим заказам?
Если решать эту задачу «в лоб» через автопересчет и программное проведение связанных заказов при изменении текущего, система неизбежно столкнется с транзакционными блокировками (deadlocks) и хаосом в печатных формах счетов, которые уже были отправлены клиентам. Рассмотрим подробнее, как обойти эти ограничения и какие архитектурные и методические варианты решения существуют.
---
Проанализируем работу типового механизма скидок. Накопительные скидки рассчитываются в момент оформления заказа на основе исторических данных о продажах. Резервы, устанавливаемые в заказах клиентов, делают движения по складским регистрам (например, ОбеспечениеЗаказов), но никак не влияют на регистры выручки до момента проведения документа РеализацияТоваровУслуг.
Если мы попытаемся программно пересчитывать и перезаписывать «старые» заказы в режиме реального времени при проведении нового заказа, мы получим две критические проблемы:
Ниже мы разберем по шагам альтернативные варианты реализации этой задачи — от простых методических изменений процесса до создания кастомных механизмов в коде 1С.
---
Это самый простой и методологически чистый подход, который не требует серьезного изменения программного кода конфигурации.
Суть подхода: Мы разделяем понятия «Счет на оплату» и «Итоговая цена отгрузки». В документах ЗаказКлиента (которые менеджеры используют как счета) скидка не рассчитывается вовсе либо рассчитывается по минимальному базовому порогу. Счета клиенту выставляются по стандартным оптовым ценам.
В договоре или соглашении с клиентом фиксируется юридическое условие: «Окончательная цена товара и размер скидки определяются в момент фактической отгрузки (оформления накладной) исходя из суммарного объема закупаемой партии за период».
При создании документа РеализацияТоваровУслуг система рассчитывает автоматическую скидку, анализируя как уже совершенные отгрузки, так и объем товаров в текущей накладной. Предыдущие заказы при этом никак не затрагиваются и не перепроводятся, что полностью исключает блокировки БД.
---
Если бизнес-процессы вашей компании требуют, чтобы клиент платил строго по согласованным счетам, но рассчитывать скидку «на лету» по резервам слишком рискованно, оптимальным решением станет схема ретро-скидок.
Суть подхода: Весь месяц (или иной отчетный период) клиент приобретает товары по базовым ценам без учета накопительной скидки. В конце периода запускается регламентная процедура (специальный отчет или обработка), которая анализирует суммарный объем закупок партнера.
На основании полученных данных рассчитывается общая сумма скидки, которую клиент «заработал» за этот период. Эту сумму можно компенсировать клиенту несколькими способами:
КорректировкаРеализации с уменьшением стоимости по прошлым поставкам.ВзаимозачетЗадолженности, уменьшив текущий долг покупателя на сумму заработанной скидки.---
Если бизнес-требования категоричны: менеджер должен видеть актуальную накопительную скидку с учетом текущих резервов непосредственно в момент оформления каждого нового заказа, нам потребуется программная доработка конфигурации через механизм расширений.
Рассмотрим, как правильно реализовать этот механизм без ущерба для производительности системы.
Во-первых, создадим новый кастомный оборотный РегистрНакопления — например, _ЗапланированныеПродажиПоПартнерам. Данный регистр будет содержать следующие измерения и ресурсы:
Партнер, Номенклатура, ЗаказКлиента.Количество.Во-вторых, настроим проведение документа ЗаказКлиента по нашему новому регистру. При проведении заказа в статусе «В работе» (когда товары находятся в резерве) система должна записывать объем планируемых продаж со знаком «плюс». При отмене заказа, снятии резерва или фактической отгрузке (когда создается РеализацияТоваровУслуг) — делать сторнирующие движения (минус).
Пример кода проведения документа ЗаказКлиента в модуле объекта:
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
// Типовой код проведения документа...
// Запись движений в кастомный регистр планируемых продаж
Движения._ЗапланированныеПродажиПоПартнерам.Записывать = Истина;
Для Каждого СтрокаТЧ Из Товары Цикл
// Проверяем, что позиция находится в резерве
Если СтрокаТЧ.ВариантОбеспечения = Перечисления.ВариантыОбеспечения.РезервироватьСклад
Или СтрокаТЧ.ВариантОбеспечения = Перечисления.ВариантыОбеспечения.РезервироватьПоМереПоступления Тогда
Движение = Движения._ЗапланированныеПродажиПоПартнерам.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Приход;
Движение.Период = Дата;
Движение.Партнер = Партнер;
Движение.Номенклатура = СтрокаТЧ.Номенклатура;
Движение.ЗаказКлиента = Ссылка;
Движение.Количество = СтрокаТЧ.Количество;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
В-третьих, необходимо переопределить алгоритм расчета скидок. В УТ 11 расчет автоматических скидок выполняется в общем модуле СкидкиНаценкиСервер (или аналогичном в вашей версии конфигурации). Нам нужно кастомизировать запрос, проверяющий условия предоставления скидки «За накопленный объем».
В этот запрос мы добавим левое соединение к остаткам и оборотам нашего нового виртуального регистра _ЗапланированныеПродажиПоПартнерам. Таким образом, при расчете скидки в документе система будет суммировать фактически совершенные продажи из регистра выручки и планируемые объемы (резервы) из нашего кастомного регистра по всем активным заказам данного партнера.
---
Если при изменении резервов в одном заказе нам все же необходимо физически обновить проценты скидок и суммы в других открытых заказах того же партнера, нельзя делать это в той же транзакции проведения. Это гарантированно вызовет дедлоки базы данных при одновременной работе пользователей.
Для решения этой проблемы применим асинхронный метод с использованием очереди отложенного пересчета.
Выясним по шагам, как настроить этот процесс:
РегистрСведений под названием ОчередьПересчетаСкидокЗаказов с измерениями Партнер и Период.ЗаказКлиента, в этот регистр сведений записывается информация о партнере и периоде, требующих пересчета. Сами связанные заказы в этот момент не трогаются. Транзакция проведения текущего заказа завершается мгновенно и без блокировок.РегламентноеЗадание, которое будет запускаться в фоновом режиме с высокой частотой (например, каждые 2-5 минут). Это задание будет считывать записи из очереди пересчета, находить все связанные незакрытые заказы партнера за указанный период, блокировать их программно по очереди, пересчитывать скидки и перепроводить документы.Посмотрим на пример кода обработчика регламентного задания, выполняющего фоновый пересчет:
Процедура ВыполнитьПересчетОчередиЗаказов() Экспорт
// Получаем список партнеров, по которым изменились резервы
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| Очередь.Партнер КАК Партнер,
| Очередь.Период КАК Период
|ИЗ
| РегистрСведений.ОчередьПересчетаСкидокЗаказов КАК Очередь";
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
// Находим все открытые заказы партнера за период
ЗапросЗаказы = Новый Запрос;
ЗапросЗаказы.Текст =
"ВЫБРАТЬ
| Заказ.Ссылка КАК Ссылка
|ИЗ
| Документ.ЗаказКлиента КАК Заказ
|ГДЕ
| Заказ.Партнер = &Партнер
| И Заказ.Дата МЕЖДУ &НачалоПериода И &КонецПериода
| И Заказ.Проведен = ИСТИНА
| И Заказ.Статус <> ЗНАЧЕНИЕ(Перечисление.СтатусыЗаказовКлиентов.Закрыт)";
ЗапросЗаказы.УстановитьПараметр("Партнер", Выборка.Partner);
ЗапросЗаказы.УстановитьПараметр("НачалоПериода", НачалоМесяца(Выборка.Период));
ЗапросЗаказы.УстановитьПараметр("КонецПериода", КонецМесяца(Выборка.Период));
ВыборкаЗаказы = ЗапросЗаказы.Выполнить().Выбрать();
Пока ВыборкаЗаказы.Следующий() Цикл
Попытка
// Пытаемся заблокировать и пересчитать скидки в документе
ОбъектДокумента = ВыборкаЗаказы.Ссылка.ПолучитьОбъект();
// Вызываем типовой метод пересчета скидок
ОбработкаТабличныхЧастей.РассчитатьСкидкиНаценки(ОбъектДокумента);
// Записываем и проводим документ
ОбъектДокумента.Записать(РежимЗаписиДокумента.Проведение);
Исключение
// Если документ заблокирован пользователем, мы пропустим его
// и попробуем обработать в следующем сеансе работы регламентного задания
ЗаписьЖурналаРегистрации("ПересчетСкидок", УровеньЖурналаРегистрации.Предупреждение, , ,
"Не удалось пересчитать скидки в документе " + ВыборкаЗаказы.Ссылка + ": " + ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
// После успешной обработки удаляем запись из очереди
НаборЗаписей = РегистрыСведений.ОчередьПересчетаСкидокЗаказов.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Партнер.Установить(Выборка.Партнер);
НаборЗаписей.Отбор.Период.Установить(Выборка.Период);
НаборЗаписей.Записать();
КонецЦикла;
КонецПроцедуры
Использование очереди отложенного пересчета позволяет сохранить целостность данных в информационной базе, автоматизировать рутинную работу менеджеров и полностью защитить систему от критических зависаний и транзакционных взаимных блокировок.
---
Подводя итоги анализа проблемы, можно дать следующие рекомендации по выбору архитектурного решения: