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