Контроль залежавшихся остатков и своевременное выявление неликвидных товаров — одна из ключевых задач складского и финансового учета в любой торговой компании. На практике программистам 1С часто поступает задача: «Вывести список товаров, которые поступили на склад в определенный период (например, в прошлом году), но с тех пор не продавались и продолжают лежать мертвым грузом».
В этой статье мы подробно разберем, как правильно составить такой запрос, проанализируем критические ошибки, которые часто допускают разработчики при решении этой задачи, и рассмотрим оптимальный шаблон запроса с использованием временных таблиц, обеспечивающий максимальную производительность базы данных.
Анализ обсуждения показывает, что участники столкнулись с рядом классических трудностей. Разберем основные методические и технические ошибки, чтобы не допускать их в своей практике.
1. Использование неподходящего регистра накопления
В первом сообщении автор пытается построить запрос по регистру СвободныеОстатки. Однако далее (в сообщении 13) он уточняет, что ему необходимы данные не только по количеству, но и по себестоимости (цене прихода). Ресурсы регистра СвободныеОстатки содержат исключительно количественные данные. В нем физически нет информации о стоимости товаров. Для получения стоимостной оценки остатков необходимо использовать другие регистры, например ПартииТоваровНаСкладах (в старых конфигурациях) или СебестоимостьТоваров (в современных ERP-системах).
2. Логическое противоречие при указании периодов
Участники форума пытаются ограничить виртуальную таблицу остатков и оборотов единым периодом &НачалоПериода и &КонецПериода. Если установить этот период равным первому кварталу прошлого года, запрос выберет товары, которые не продавались внутри этого же первого квартала. Но этот товар мог быть успешно распродан позже! Чтобы корректно выявить неликвид, нам необходимы два разных периода: период закупки (когда товар пришел на склад) и период анализа продаж (который должен длиться с момента закупки по сегодняшний день).
3. Попытка исключить продажи только по одному типу документа
Рекомендация использовать фильтр по регистратору РеализацияТоваровУслуг ошибочна. Списание и продажа товаров со склада могут осуществляться множеством других документов: ОтчетОРозничныхПродажах, СписаниеТоваров, ПередачаТоваровМеждуОрганизациями (в схемах Интеркампани) и т.д. Если исключить только один документ реализации, в отчет попадут товары, которые на самом деле были проданы через розницу или списаны по другим причинам.
4. Использование тяжелых конструкций с оператором НЕ В
В сообщениях 16 и 18 предлагается использовать подзапрос в параметрах виртуальной таблицы с оператором НЕ В. С точки зрения производительности СУБД (особенно на крупных базах данных MS SQL Server или PostgreSQL), использование НЕ В с подзапросом, содержащим периодичность Регистратор, — это критическая ошибка. Такой запрос приведет к полному сканированию таблиц (Table Scan), вызовет избыточную нагрузку на сервер и может привести к зависанию системы.
Для быстрого и корректного решения этой задачи мы применим проверенную методику пакетирования запросов с использованием временных таблиц. Наш алгоритм будет состоять из трех последовательных шагов:
NULL). Это позволит нам мгновенно отсечь все позиции, по которым было хоть какое-то движение.Рассмотрим подробнее практическую реализацию этого алгоритма на примере регистра накопления ПартииТоваровНаСкладах.
// ШАГ 1: Получаем остатки и приходы за целевой период закупки
ВЫБРАТЬ
ПартииОстаткиИОбороты.Номенклатура КАК Номенклатура,
ПартииОстаткиИОбороты.КоличествоКонечныйОстаток КАК ОстатокКоличество,
ПартииОстаткиИОбороты.СтоимостьКонечныйОстаток КАК ОстатокСебестоимость
ПОМЕСТИТЬ ВТ_ОстаткиИПриход
ИЗ
РегистрНакопления.ПартииТоваровНаСкладах.ОстаткиИОбороты(&НачалоПериодаЗакупки, &КонецПериодаЗакупки, , , ) КАК ПартииОстаткиИОбороты
ГДЕ
ПартииОстаткиИОбороты.КоличествоПриход > 0
И ПартииОстаткиИОбороты.КоличествоКонечныйОстаток > 0
;
////////////////////////////////////////////////////////////////////////////////
// ШАГ 2: Собираем список всех проданных товаров за весь период анализа
ВЫБРАТЬ РАЗЛИЧНЫЕ
ПродажиОбороты.Номенклатура КАК Номенклатура
ПОМЕСТИТЬ ВТ_ПроданныеТовары
ИЗ
РегистрНакопления.Продажи.Обороты(&НачалоПериодаЗакупки, &ТекущаяДата, , ) КАК ПродажиОбороты
;
////////////////////////////////////////////////////////////////////////////////
// ШАГ 3: Левым соединением находим товары без продаж и рассчитываем цену
ВЫБРАТЬ
ВТ_ОстаткиИПриход.Номенклатура КАК Номенклатура,
ВТ_ОстаткиИПриход.ОстатокКоличество КАК ОстатокКоличество,
ВТ_ОстаткиИПриход.ОстатокСебестоимость КАК ОстатокСебестоимость,
ВЫБОР
КОГДА ВТ_ОстаткиИПриход.ОстатокКоличество > 0
ТОГДА ВТ_ОстаткиИПриход.ОстатокСебестоимость / ВТ_ОстаткиИПриход.ОстатокКоличество
ИНАЧЕ 0
КОНЕЦ КАК ЦенаЗакупки
ИЗ
ВТ_ОстаткиИПриход КАК ВТ_ОстаткиИПриход
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ПроданныеТовары КАК ВТ_ПроданныеТовары
ПО ВТ_ОстаткиИПриход.Номенклатура = ВТ_ПроданныеТовары.Номенклатура
ГДЕ
ВТ_ПроданныеТовары.Номенклатура ЕСТЬ NULL
Разберем, почему данный подход эффективен:
Продажи. Это гарантирует, что мы учтем абсолютно все продажи, независимо от того, какими документами они были оформлены (опт, розница, отчеты комиссионеров).ЛЕВОЕ СОЕДИНЕНИЕ с проверкой на ЕСТЬ NULL позволяет СУБД построить оптимальный план выполнения запроса, эффективно задействуя индексы таблиц.Если вам необходимо быстро написать простой запрос для небольшой базы данных (например, в рамках разовой проверки или тестирования), можно воспользоваться подходом, предложенным в финальном сообщении форума. Проанализируем этот вариант кода, адаптированный под регистр накопления ПартииТоваровНаСкладах:
ВЫБРАТЬ РАЗЛИЧНЫЕ
ПартииОбороты.Номенклатура КАК Номенклатура
ИЗ
РегистрНакопления.ПартииТоваровНаСкладах.ОстаткиИОбороты(
&НачалоПериодаЗакупки,
&КонецПериодаЗакупки,
,
,
НЕ Номенклатура В (
ВЫБРАТЬ РАЗЛИЧНЫЕ
Продажи.Номенклатура
ИЗ
РегистрНакопления.Продажи.Обороты(&НачалоПериодаЗакупки, &КонецПериодаЗакупки) КАК Продажи
)
) КАК ПартииОбороты
Обратим внимание на особенности этого решения:
Продажи, а не фильтровать по регистраторам складского учета (как предлагалось изначально).В реальном бизнесе жесткий критерий «вообще не было движений» используется редко, так как он не дает полной картины затоваренности. Профессиональные аналитики и категорийные менеджеры применяют более гибкие показатели:
1. Расчет показателя «Дней без движения»
Вместо жесткой привязки к дате закупки, для каждого товара на складе определяется дата его последней продажи. Количество дней, прошедших с даты последней продажи до текущего дня, и есть показатель залежалости. Это позволяет ранжировать товары по степени их «зависания».
2. Оценка оборачиваемости запасов в днях
Классический показатель, который рассчитывается как отношение среднего остатка за период к объему продаж за этот же период, умноженное на количество дней. Если оборачиваемость позиции превышает норматив (например, 90 или 120 дней), товар признается неликвидным, даже если редкие продажи по нему все-таки происходят.
3. Использование типовых механизмов 1С
Прежде чем приступать к написанию собственного сложного запроса, проверьте возможности вашей конфигурации. В современных решениях на платформе 1С:Предприятие 8 (таких как 1С:Управление торговлей ред. 11, 1С:ERP или 1С:Комплексная автоматизация) уже присутствуют мощные встроенные инструменты: