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

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

Задача расчета средней длительности нулевых остатков является распространенной и важной для анализа складских запасов и эффективности управления товарными потоками. Мы сталкиваемся с ситуацией, когда нам необходимо не просто посчитать общее количество дней без остатка, а определить, сколько в среднем дней подряд товар отсутствовал на складе. Например, если товара не было с 1 по 2 число (2 дня), а затем с 5 по 9 число (5 дней), средняя длительность будет не (2+5)/2 = 3.5 дня. Это требует особого подхода к выявлению непрерывных интервалов.

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

Суть проблемы: поиск непрерывных интервалов

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

Рассмотрим два основных подхода к решению этой задачи: аналитический запрос с выявлением границ интервалов и итеративная обработка данных на стороне кода 1С.

Подход 1: Аналитический запрос с выявлением границ интервалов

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

  1. Шаг 1: Получение всех дней с признаком нулевого остатка.

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

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

    В этом примере #ТаблицаИДЕНТИФИКАТОРОВ - это либо заранее созданная временная таблица с числами, либо можно использовать любой крупный справочник, чтобы набрать достаточно строк для генерации дат.

  2. Шаг 2: Определение начала интервалов нулевых остатков.

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

    
    ВЫБРАТЬ
        ТекДень.Дата КАК ДатаНачалаИнтервала
    ПОМЕСТИТЬ ВТ_НачалаИнтервалов
    ИЗ
        ВТ_ДневныеОстаткиСПризнаком КАК ТекДень
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ДневныеОстаткиСПризнаком КАК ПредДень
            ПО ТекДень.Дата = ДОБАВИТЬКДАТЕ(ПредДень.Дата, ДЕНЬ, 1)
    ГДЕ
        ТекДень.НулевойОстаток = ИСТИНА
        И (ПредДень.Дата ЕСТЬ NULL ИЛИ ПредДень.НулевойОстаток = ЛОЖЬ)
    ;
    
  3. Шаг 3: Определение конца интервалов нулевых остатков.

    Аналогично, день является концом интервала, если остаток в этот день нулевой, а в следующий день либо будет остаток, либо это последний день в нашем периоде.

    
    ВЫБРАТЬ
        ТекДень.Дата КАК ДатаКонцаИнтервала
    ПОМЕСТИТЬ ВТ_КонцыИнтервалов
    ИЗ
        ВТ_ДневныеОстаткиСПризнаком КАК ТекДень
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ДневныеОстаткиСПризнаком КАК СледДень
            ПО ТекДень.Дата = ДОБАВИТЬКДАТЕ(СледДень.Дата, ДЕНЬ, -1)
    ГДЕ
        ТекДень.НулевойОстаток = ИСТИНА
        И (СледДень.Дата ЕСТЬ NULL ИЛИ СледДень.НулевойОстаток = ЛОЖЬ)
    ;
    
  4. Шаг 4: Сопоставление начал и концов интервалов.

    Теперь нам нужно объединить найденные начала и концы. Для каждого начала интервала мы ищем ближайший следующий конец интервала.

    
    ВЫБРАТЬ
        Начала.ДатаНачалаИнтервала,
        МИНИМУМ(Концы.ДатаКонцаИнтервала) КАК ДатаКонцаИнтервала
    ПОМЕСТИТЬ ВТ_ИнтервалыНулевыхОстатков
    ИЗ
        ВТ_НачалаИнтервалов КАК Начала
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_КонцыИнтервалов КАК Концы
            ПО Концы.ДатаКонцаИнтервала >= Начала.ДатаНачалаИнтервала
    СГРУППИРОВАТЬ ПО
        Начала.ДатаНачалаИнтервала
    ;
    
  5. Шаг 5: Расчет длительности каждого интервала и их среднего значения.

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

    
    ВЫБРАТЬ
        РАЗНОСТЬДАТ(Интервалы.ДатаНачалаИнтервала, Интервалы.ДатаКонцаИнтервала, ДЕНЬ) + 1 КАК ДлительностьИнтервала
    ПОМЕСТИТЬ ВТ_ДлительностиИнтервалов
    ИЗ
        ВТ_ИнтервалыНулевыхОстатков КАК Интервалы
    ;
    
    ВЫБРАТЬ
        СУММА(Длительности.ДлительностьИнтервала) / КОЛИЧЕСТВО(Длительности.ДлительностьИнтервала) КАК СредняяДлительностьНулевыхОстатков
    ИЗ
        ВТ_ДлительностиИнтервалов КАК Длительности
    ;
    

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

Подход 2: Итеративная обработка данных на стороне кода 1С

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

  1. Шаг 1: Получение исходных данных.

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

    
    // Пример получения данных из регистра накопления
    Запрос = Новый Запрос;
    Запрос.Текст =
    "ВЫБРАТЬ
    |    ОстаткиТоваровОстаткиИОбороты.ПериодДня КАК Дата,
    |    ОстаткиТоваровОстаткиИОбороты.КоличествоКонечныйОстаток КАК Остаток
    |ИЗ
    |    РегистрНакопления.ОстаткиТоваров.ОстаткиИОбороты(&НачалоПериода, &КонецПериода, День, , Товар = &Товар) КАК ОстаткиТоваровОстаткиИОбороты
    |
    |УПОРЯДОЧИТЬ ПО
    |    Дата";
    
    Запрос.УстановитьПараметр("НачалоПериода", НачалоДня(ДатаНачала));
    Запрос.УстановитьПараметр("КонецПериода", КонецДня(ДатаОкончания));
    Запрос.УстановитьПараметр("Товар", ВыбранныйТовар);
    
    ДневныеОстатки = Запрос.Выполнить().Выгрузить();
    
  2. Шаг 2: Перебор данных и выявление интервалов.

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

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

    После того как все интервалы найдены и их длительности собраны в массив, мы можем легко рассчитать среднее значение.

    
    Если МассивДлительностей.Количество() > 0 Тогда
        СуммаДлительностей = 0;
        Для Каждого Длительность Из МассивДлительностей Цикл
            СуммаДлительностей = СуммаДлительностей + Длительность;
        КонецЦикла;
        СредняяДлительность = СуммаДлительностей / МассивДлительностей.Количество();
        Сообщить("Средняя длительность нулевых остатков: " + СредняяДлительность + " дней.");
    Иначе
        Сообщить("Нулевых остатков за период не обнаружено.");
    КонецЕсли;
    

Интеграция в СКД

Оба описанных подхода можно интегрировать в отчет на СКД, но с разной степенью сложности:

  1. Для запросового подхода:

    Мы можем создать несколько наборов данных в СКД, каждый из которых будет использовать временные таблицы (ПОМЕСТИТЬ). Первый набор данных получает ежедневные остатки с признаком нулевого остатка. Последующие наборы данных будут определять начала и концы интервалов, а затем объединять их, как показано в запросах. Финальный набор данных будет рассчитывать среднюю длительность. Этот метод позволяет полностью реализовать логику внутри СКД, но требует глубокого понимания построения сложных запросов и работы с временными таблицами.

  2. Для итеративного подхода (в коде 1С):

    Мы можем использовать СКД для получения "сырых" данных — таблицы ежедневных остатков. Затем, в модуле объекта отчета (например, в обработчике события ПриКомпоновкеРезультата или ПриЗагрузкеДанных), мы выполняем описанный выше программный код. Результат (среднюю длительность) можно вывести либо в дополнительное поле отчета, либо в отдельное сообщение пользователю. Этот подход является более гибким, поскольку позволяет использовать всю мощь языка 1С, но требует написания кода вне интерфейса СКД. Также можно использовать вычисляемое поле в СКД, которое будет вызывать общую функцию из модуля, реализующую итеративный расчет, передавая ей уже сформированную таблицу с остатками.

Рекомендации

Выбор подхода зависит от нашей квалификации и сложности конкретной задачи:

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

← На главную