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