В торговой практике часто возникает задача определить, сколько дней конкретный товар фактически находился на складе за определенный период (например, за полтора года). Этот показатель критически важен для расчета чистой скорости продаж (среднедневных продаж) и анализа упущенной прибыли (дефицита). Если мы будем делить общий объем продаж на календарные 540 дней (за 1.5 года), не учитывая, что товара физически не было на складе половину этого времени, мы получим искаженные данные, которые приведут к ошибкам при закупках.
В этой статье мы подробно разберем, как реализовать подсчет дней нахождения номенклатуры на складе с учетом требований автора: учитывать только рабочие дни и субботы (исключая воскресенья и государственные праздники), а также брать в расчет только «полные дни» (когда остаток был и на начало, и на конец дня).
---
Прежде чем перейти к написанию кода в 1С, давайте проанализируем методологию. Автор темы предлагает считать день «полным», если остаток есть на начало и на конец дня. Однако в реальной практике такой жесткий подход может исказить статистику. Представьте ситуацию: ходовой товар пришел на склад утром и в этот же день был полностью распродан. По логике «полного дня» этот день запишется как день отсутствия товара, хотя по факту товар был и успешно продавался.
Поэтому в профессиональных алгоритмах (например, в товарном планировании ERP или УТ) применяют более гибкое правило: день считается днем наличия, если выполнено хотя бы одно из условий:
Тем не менее, ниже мы рассмотрим классическую реализацию, которая строго следует условию автора, но вы легко сможете адаптировать ее под нужды вашего бизнеса.
---
Чтобы отсечь воскресенья и праздничные дни, нам обязательно понадобится календарная сетка. В типовых конфигурациях 1С на базе БСП (Библиотеки Стандартных Подсистем) для этого используется регистр сведений ДанныеПроизводственногоКалендаря. Этот регистр хранит информацию о каждом дне года и его типе (рабочий, праздничный, предпраздничный, суббота, воскресенье).
Сформулируем условия для отбора дней согласно ТЗ автора:
ДЕНЬНЕДЕЛИ(Календарь.Дата) <> 7.ВидДня, исключив значение Праздник.Рассмотрим, как получить такую сетку дат в запросе 1С:
ВЫБРАТЬ
Календарь.Дата КАК Дата
ПОМЕСТИТЬ ВТ_РабочиеДни
ИЗ
РегистрСведений.ДанныеПроизводственногоКалендаря КАК Календарь
ГДЕ
Календарь.Дата МЕЖДУ &НачалоПериода И &КонецПериода
И ДЕНЬНЕДЕЛИ(Календарь.Дата) <> 7
И Календарь.ВидДня <> ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Праздник)
;
---
Это классический способ решения задачи, который упоминался в обсуждении на форуме. Суть метода заключается в том, что мы строим декартово произведение (полную матрицу) всех товаров и всех рабочих дней за выбранный период, а затем для каждого дня рассчитываем остаток товара нарастающим итогом.
Давайте разберем этот алгоритм по шагам в одном запросе:
// Шаг 1: Формируем сетку рабочих дней и суббот
ВЫБРАТЬ
Календарь.Дата КАК Дата
ПОМЕСТИТЬ ВТ_Календарь
ИЗ
РегистрСведений.ДанныеПроизводственногоКалендаря КАК Календарь
ГДЕ
Календарь.Дата МЕЖДУ &НачалоПериода И &КонецПериода
И ДЕНЬНЕДЕЛИ(Календарь.Дата) <> 7
И Календарь.ВидДня <> ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Праздник)
;
// Шаг 2: Получаем остатки и обороты по товарам с детализацией до дня
ВЫБРАТЬ
Товары.Номенклатура КАК Номенклатура,
Товары.Период КАК Период,
Товары.ВНаличииКонечныйОстаток КАК Остаток,
Товары.ВНаличииНачальныйОстаток КАК НачальныйОстаток
ПОМЕСТИТЬ ВТ_ДвиженияТоваров
ИЗ
РегистрНакопления.ТоварыНаСкладах.ОстаткиИОбороты(&НачалоПериода, &КонецПериода, День, , ) КАК Товары
;
// Шаг 3: Строим полную таблицу "Товар - Рабочий день"
ВЫБРАТЬ
Номенклатура.Ссылка КАК Номенклатура,
Календарь.Дата КАК Дата
ПОМЕСТИТЬ ВТ_МатрицаТоваровИДат
ИЗ
Справочник.Номенклатура КАК Номенклатура
СОЕДИНЕНИЕ ВТ_Календарь КАК Календарь ПО ИСТИНА
ГДЕ
НЕ Номенклатура.ЭтоГруппа
;
// Шаг 4: Для каждой даты матрицы находим ближайший предыдущий день, когда было движение по регистру
ВЫБРАТЬ
Матрица.Номенклатура КАК Номенклатура,
Матрица.Дата КАК Дата,
МАКСИМУМ(Движения.Период) КАК ДатаПоследнегоДвижения
ПОМЕСТИТЬ ВТ_СвязьВременныхТочек
ИЗ
ВТ_МатрицаТоваровИДат КАК Матрица
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ДвиженияТоваров КАК Движения
ПО Матрица.Номенклатура = Движения.Номенклатура
И Движения.Период <= Матрица.Дата
ГРУППИРОВАТЬ ПО
Матрица.Номенклатура,
Матрица.Дата
;
// Шаг 5: Соединяем данные и определяем остаток на каждый рабочий день
ВЫБРАТЬ
Связь.Номенклатура КАК Номенклатура,
Связь.Дата КАК Дата,
ЕСТЬNULL(Движения.Остаток, 0) КАК ОстатокКонечный,
ЕСТЬNULL(Движения.НачальныйОстаток, 0) КАК ОстатокНачальный
ПОМЕСТИТЬ ВТ_ОстаткиНаКаждыйДень
ИЗ
ВТ_СвязьВременныхТочек КАК Связь
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ДвиженияТоваров КАК Движения
ПО Связь.Номенклатура = Движения.Номенклатура
И Связь.ДатаПоследнегоДвижения = Движения.Период
;
// Шаг 6: Подсчитываем только те дни, где остаток был и на начало, и на конец дня
ВЫБРАТЬ
Остатки.Номенклатура КАК Номенклатура,
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Остатки.Дата) КАК КоличествоДнейНаСкладе
ИЗ
ВТ_ОстаткиНаКаждыйДень КАК Остатки
ГДЕ
Остатки.ОстатокНачальный > 0
И Остатки.ОстатокКонечный > 0
ГРУППИРОВАТЬ ПО
Остатки.Номенклатура
;
Преимущества: Логика запроса линейна и ее легко отлаживать. Данный метод гарантирует точный результат, так как мы физически анализируем каждую дату периода для каждой позиции номенклатуры.
Недостатки: Низкая производительность. Представьте, что в вашей базе данных 10 000 товаров, а период анализа — 1.5 года (около 450 рабочих дней). Временная таблица ВТ_МатрицаТоваровИДат будет содержать 4 500 000 строк. Выполнение операций группировки и поиска максимума по такой таблице в оперативной памяти СУБД приведет к колоссальным нагрузкам и зависанию системы. Метод подходит только для небольшого количества товаров или коротких периодов.
---
Чтобы избежать перегрузки сервера СУБД многомиллионными таблицами, высококвалифицированные разработчики используют обратный метод расчета. Вместо того чтобы проверять каждый день присутствия товара, мы найдем периоды отсутствия товара (интервалы дефицита, когда остаток был равен нулю).
Проанализируем этот алгоритм:
Посмотрим на пример построения такого запроса:
// Шаг 1: Получаем общее количество рабочих дней за период
ВЫБРАТЬ
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Календарь.Дата) КАК ВсегоРабочихДней
ПОМЕСТИТЬ ВТ_ОбщийФондВремени
ИЗ
РегистрСведений.ДанныеПроизводственногоКалендаря КАК Календарь
ГДЕ
Календарь.Дата МЕЖДУ &НачалоПериода И &КонецПериода
И ДЕНЬНЕДЕЛИ(Календарь.Дата) <> 7
И Календарь.ВидДня <> ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Праздник)
;
// Шаг 2: Получаем интервалы, когда товара физически не было на складе (остаток = 0)
// Для этого выбираем моменты движения, где конечный остаток стал равен 0
ВЫБРАТЬ
Движения.Номенклатура КАК Номенклатура,
Движения.Период КАК ДатаНачалаОтсутствия,
МИНИМУМ(ЕСТЬNULL(ДвиженияСледом.Период, &КонецПериода)) КАК ДатаКонцаОтсутствия
ПОМЕСТИТЬ ВТ_ИнтервалыОтсутствия
ИЗ
ВТ_ДвиженияТоваров КАК Движения
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ДвиженияТоваров КАК ДвиженияСледом
ПО Движения.Номенклатура = ДвиженияСледом.Номенклатура
И ДвиженияСледом.Период > Движения.Период
И ДвиженияСледом.НачальныйОстаток > 0 // Товар снова появился
ГДЕ
Движения.Остаток = 0
ГРУППИРОВАТЬ ПО
Движения.Номенклатура,
Движения.Период
;
// Шаг 3: Считаем рабочие дни внутри интервалов отсутствия товара
ВЫБРАТЬ
Интервалы.Номенклатура КАК Номенклатура,
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Календарь.Дата) КАК ДнейОтсутствия
ПОМЕСТИТЬ ВТ_ДниОтсутствияПоТоварам
ИЗ
ВТ_ИнтервалыОтсутствия КАК Интервалы
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ДанныеПроизводственногоКалендаря КАК Календарь
ПО Календарь.Дата МЕЖДУ Интервалы.ДатаНачалаОтсутствия И Интервалы.ДатаКонцаОтсутствия
И ДЕНЬНЕДЕЛИ(Календарь.Дата) <> 7
И Календарь.ВидДня <> ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Праздник)
ГРУППИРОВАТЬ ПО
Интервалы.Номенклатура
;
// Шаг 4: Вычитаем дни отсутствия из общего фонда рабочих дней
ВЫБРАТЬ
Номенклатура.Ссылка КАК Номенклатура,
ЕСТЬNULL(Фонд.ВсегоРабочихДней, 0) - ЕСТЬNULL(Отсутствие.ДнейОтсутствия, 0) КАК КоличествоДнейНаСкладе
ИЗ
Справочник.Номенклатура КАК Номенклатура
КРОСС СОЕДИНЕНИЕ ВТ_ОбщийФондВремени КАК Фонд
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ДниОтсутствияПоТоварам КАК Отсутствие
ПО Номенклатура.Ссылка = Отсутствие.Номенклатура
ГДЕ
НЕ Номенклатура.ЭтоГруппа
;
Преимущества: Огромная скорость работы. Запрос оперирует только реальными транзакциями изменений остатков, а не виртуальными строками на каждый день. Если товар стабильно лежал на складе, СУБД обработает всего пару строк для этой номенклатуры, вместо сотен строк в прямом методе.
Недостатки: Запрос сложнее для понимания и требует аккуратной отладки краевых условий (например, когда товар отсутствовал еще до начала анализируемого периода или остался отсутствующим после его окончания).
---
В процессе обсуждения на форуме участники затронули тему партионного учета (сообщения 4, 7, 8, 11). Важно разграничивать две принципиально разные бизнес-задачи:
ТоварыНаСкладах.Поскольку автор темы решает задачу анализа присутствия за 1.5 года, партионный учет привлекать не стоит — это лишь усложнит и замедлит работу алгоритма.
---
Для реализации задачи подсчета дней присутствия номенклатуры на складе рекомендуем придерживаться следующего плана действий: