Как реализовать условный счетчик и расчет непрерывных периодов в отчетах СКД?

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

При разработке аналитических отчетов в системе компоновки данных (СКД) часто возникает задача подсчета непрерывных интервалов. Классический пример — определить, сколько месяцев подряд товар находился на остатках склада. Алгоритмически логика выглядит просто: при наличии остатка увеличиваем переменную на единицу, а при нулевом остатке — сбрасываем счетчик в ноль.

Однако при попытке перенести такой алгоритм в СКД разработчики сталкиваются со сложностями. Выясним причину этого ограничения и подробно разберем проверенные способы решения задачи.

Почему стандартное вычисляемое поле не решает задачу?

Система компоновки данных выполняет реляционные операции над множествами. Язык выражений СКД не поддерживает концепцию изменяемых переменных и циклов. Основные ограничения:

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

Для реализации такого механизма мы можем применить один из четырех практических подходов.

Способ 1. Вызов внешней экспортной функции в вычисляемом поле

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

Разберем по шагам реализацию этого метода:

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

Функция РассчитатьНепрерывныйПериод(МассивОстатков) Экспорт
    Результат = 0;
    Если ТипЗнч(МассивОстатков) <> Тип("Массив") Тогда
        Возврат 0;
    КонецЕсли;
    
    Для Индекс = 1 По МассивОстатков.Количество() Цикл
        ТекущийОстаток = МассивОстатков[МассивОстатков.Количество() - Индекс];
        Если ТекущийОстаток > 0 Тогда
            Результат = Результат + 1;
        Иначе
            Прервать;
        КонецЕсли;
    КонецЦикла;
    
    Возврат Результат;
КонецФункции
  1. В схеме компоновки данных на вкладке Вычисляемые поля добавим новое поле, например, МесяцевНаСкладе.
  2. В выражении поля вызовем функцию платформы ВычислитьВыражениеСГруппировкойМассив, передав результат в наш модуль:

МойМодуль.РассчитатьНепрерывныйПериод(ВычислитьВыражениеСГруппировкойМассив("Остаток", "ПериодМесяц"))

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

Способ 2. Определение последнего разрыва в запросе

Проанализируем классический SQL-подход. Его преимущество состоит в том, что расчет выполняется целиком на стороне СУБД без привлечения внешнего кода.

Логика заключается в поиске даты последнего «разрыва» (месяца, когда остаток был равен нулю или отсутствовал):

  1. Формируем временную таблицу помесячных остатков по товарам.
  2. Соединяем таблицу саму с собой для каждого товара по условию, что дата левой таблицы больше даты правой таблицы, где остаток равен нулю.
  3. Для каждой текущей строки находим максимальную дату нулевого остатка (МАКСИМУМ(ТаблицаРазрывов.ПериодМесяц)).
  4. Вычисляем количество месяцев между датой последнего разрыва и текущей датой с помощью функции РАЗНОСТЬДАТ:

ВЫБРАТЬ
    Т.Номенклатура КАК Номенклатура,
    Т.ПериодМесяц КАК ПериодМесяц,
    Т.Остаток КАК Остаток,
    ВЫБОР
        КОГДА Т.Остаток = 0 ТОГДА 0
        КОГДА ЕСТЬNULL(МАКСИМУМ(Разрывы.ПериодМесяц), ДАТАВРЕМЯ(1, 1, 1)) = ДАТАВРЕМЯ(1, 1, 1)
            ТОГДА РАЗНОСТЬДАТ(МИНИМУМ(ВсеПериоды.ПериодМесяц), Т.ПериодМесяц, МЕСЯЦ) + 1
        ИНАЧЕ РАЗНОСТЬДАТ(МАКСИМУМ(Разрывы.ПериодМесяц), Т.ПериодМесяц, МЕСЯЦ)
    КОНЕЦ КАК МесяцевПодряд
ИЗ
    ВТ_ОстаткиПоМесяцам КАК Т
    ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ОстаткиПоМесяцам КАК Разрывы
        ПО Т.Номенклатура = Разрывы.Номенклатура
        И Т.ПериодМесяц > Разрывы.ПериодМесяц
        И Разрывы.Остаток = 0
    ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ОстаткиПоМесяцам КАК ВсеПериоды
        ПО Т.Номенклатура = ВсеПериоды.Номенклатура
СГРУППИРОВАТЬ ПО
    Т.Номенклатура,
    Т.ПериодМесяц,
    Т.Остаток

Способ 3. Применение оконных функций в запросе (версии 8.3.20 и новее)

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

Посмотрим на пример формирования групп непрерывности:

  1. Вычисляем сплошной номер строки для всех записей товара: НОМЕРСТРОКИ() OVER (PARTITION BY Номенклатура ORDER BY ПериодМесяц).
  2. Для строк с положительным остатком вычисляем отдельную нумерацию: НОМЕРСТРОКИ() OVER (PARTITION BY Номенклатура, (Остаток > 0) ORDER BY ПериодМесяц).
  3. Разность между этими двумя номерами строк остается постоянной внутри каждого непрерывного интервала наличия остатка, формируя уникальный идентификатор непрерывной группы.
  4. Счетчик месяцев внутри каждой группы рассчитывается итоговой оконной функцией НОМЕРСТРОКИ() OVER (PARTITION BY Номенклатура, ИдентификаторГруппы ORDER BY ПериодМесяц).

Способ 4. Программный вывод отчета при компоновке

Если структура отчета сложная или требуется производить расчеты непосредственно при генерации результирующего табличного документа, используем модуль объекта отчета и событие ПриКомпоновкеРезультата.

Разберем пример обхода элементов компоновки с ручным ведением счетчика:


Процедура ПриКомпоновкеРезультата(ДокументРезультат, ДанныеРасшифровки, СтандартнаяОбработка)
    СтандартнаяОбработка = Ложь;
    
    Настройки = КомпоновщикНастроек.ПолучитьНастройки();
    КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
    МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, Настройки, ДанныеРасшифровки);
    
    ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
    ПроцессорКомпоновки.Инициализировать(МакетКомпоновки, , ДанныеРасшифровки, Истина);
    
    ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент;
    ПроцессорВывода.УстановитьДокумент(ДокументРезультат);
    ПроцессорВывода.НачатьВывод();
    
    ТекущийСчетчик = 0;
    
    Пока Истина Цикл
        ЭлементРезультата = ПроцессорКомпоновки.Следующий();
        Если ЭлементРезультата = Неопределено Тогда
            Прервать;
        КонецЕсли;
        
        // Пример проверки значения и модификации параметров вывода
        Если ЭлементРезультата.ТипЭлемента = ТипЭлементаРезультатаКомпоновкиДанных.Записи Тогда
            Остаток = ЭлементРезультата.ЗначенияПараметров.Найти("Остаток");
            Если Остаток <> Неопределено И Остаток.Значение > 0 Тогда
                ТекущийСчетчик = ТекущийСчетчик + 1;
            Иначе
                ТекущийСчетчик = 0;
            КонецЕсли;
        КонецЕсли;
        
        ПроцессорВывода.ВывестиЭлемент(ЭлементРезультата);
    КонецЦикла;
    
    ПроцессорВывода.ЗакончитьВывод();
КонецПроцедуры

Сводная рекомендация

Для типовых отчетов на СКД оптимальным выбором является Способ 1 (вызов функции через ВычислитьВыражениеСГруппировкойМассив) благодаря простоте внедрения, либо Способ 3 (оконные функции в запросе), если отчет оперирует большими объемами данных на платформе 8.3.20 и выше.

← На главную