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