Как в 1С получить отчет по остаткам товаров и продажам по месяцам, выявив слабооборачиваемые позиции?

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

В реальной торговой практике менеджеры и аналитики регулярно сталкиваются с необходимостью выявления так называемых «неликвидов» или слабооборачиваемых товаров. Это позиции, которые мертвым грузом лежат на складах, замораживая оборотные средства компании. Бизнес-логика задачи проста: нужно найти товары, объем продаж по которым за определенный период ничтожно мал по сравнению с накопленными остатками.

В рамках этой статьи мы подробно разберем, как спроектировать такой отчет в конфигурации «Управление торговлей 10.3» (на базе обычных форм платформы 1С:Предприятие), проанализируем критические логические ошибки, которые часто допускают начинающие разработчики, и построим надежный и быстрый запрос с использованием Системы компоновки данных (СКД).

Постановка задачи и бизнес-смысл

Сформулируем исходные требования заказчика. Необходимо вывести в отчет список номенклатуры с показателями:

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

Разбор критических логических ошибок в исходном решении

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


ВЫБРАТЬ
    ТоварыНаСкладахОстаткиИОбороты.Номенклатура КАК Номенклатура,
    ТоварыНаСкладахОстаткиИОбороты.КоличествоНачальныйОстаток КАК НачОст1,
    ТоварыНаСкладахОстаткиИОбороты.КоличествоКонечныйОстаток КАК КонОст1
ПОМЕСТИТЬ ТаблПромеж
ИЗ
    РегистрНакопления.Продажи.Обороты(&НачПериода, &КонПериода, , ) КАК ПродажиОбороты
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.ОстаткиИОбороты(&НачПериода, &КонПериода, , , ) КАК ТоварыНаСкладахОстаткиИОбороты
    ПО ПродажиОбороты.Номенклатура = ТоварыНаСкладахОстаткиИОбороты.Номенклатура

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

Проблема 1: Использование ЛЕВОГО СОЕДИНЕНИЯ со стороны Продаж

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

Но подумаем логически: кто является главным неликвидом для компании? Это тот товар, который лежит на складе, но по которому продаж не было вообще (то есть продажи равны нулю). В предложенной схеме такие товары будут полностью отрезаны левым соединением! Они просто не попадут в результирующую выборку. Чтобы исправить эту ситуацию, соединение необходимо строить в обратную сторону — от остатков к продажам, либо использовать полное соединение таблиц.

Проблема 2: Математическая некорректность условия «разности остатков»

Заказчик формулирует условие отбора так: «продажи не превысили 10% между разностью остатков на начало и на конец». Математически это выглядит как формула:

Продажи <= 0.1 * (НачальныйОстаток - КонечныйОстаток)

Давайте рассмотрим пограничные случаи, которые полностью ломают эту логику:

  1. Отрицательная разность при поступлении товара. Допустим, на начало месяца у нас было 5 единиц товара. Позже на склад поступило еще 100 единиц, а в конце периода на остатках зафиксировано 80 единиц. При этом продали мы всего 25 единиц. Разность остатков будет отрицательной: 5 - 80 = -75. 10% от этого значения составят -7.5. Очевидно, что реальные продажи (25) не могут быть меньше отрицательного числа (-7.5). Товар отсечется из отчета, хотя он продавался крайне неэффективно по отношению к общему объему запаса.
  2. Остаток не изменился, но движения были. Представим ситуацию: на начало периода было 10 штук товара. В течение месяца мы продали 5 штук и ровно столько же (5 штук) нам доставил поставщик. На конец периода у нас снова 10 штук. Разность остатков равна нулю (10 - 10 = 0). В результате формула отбора потребует, чтобы продажи не превышали 0. Товар автоматически пометится как неликвидный, хотя по нему шла стабильная торговля.

Как правильно: В реальном бизнесе оборачиваемость или долю продаж считают по отношению либо к начальному остатку за анализируемый период, либо к среднему остатку. Формула среднего остатка рассчитывается как среднее арифметическое между начальным и конечным остатками: (НачальныйОстаток + КонечныйОстаток) / 2.

Проблема 3: Вывод пустых периодов и разворот по месяцам

Если мы выбираем периодичность «Месяц» в виртуальной таблице остатков и оборотов, система возвращает нам записи по каждому месяцу отдельно. Из-за этого крайне сложно красиво сопоставить остатки и продажи в одной строке программным кодом. Если делать это через циклы (как предлагалось в Сообщении 6), отчет будет работать очень медленно на больших базах данных.

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

Проблема 4: Специфика параметров и границ периодов

Обратите внимание на параметры &НачПериода и &КонПериода. Если пользователь указывает в качестве даты окончания периода, например, 31.12.2023, то без явного указания времени 1С преобразует этот параметр в начало дня: 31.12.2023 00:00:00.

В итоге все движения и продажи, совершенные в течение последнего дня периода, будут полностью проигнорированы. Чтобы этого избежать, в коде необходимо передавать параметр конца периода с использованием типа Граница или использовать функции СКД для приведения даты к концу дня.

Правильное решение с использованием Временных Таблиц

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

Рассмотрим текст правильного запроса:


// Шаг 1: Получаем остатки товаров за весь период
ВЫБРАТЬ
    ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
    ТоварыНаСкладахОстатки.КоличествоНачальныйОстаток КАК НачОст,
    ТоварыНаСкладахОстатки.КоличествоКонечныйОстаток КАК КонОст,
    0 КАК КоличествоПродано
ПОМЕСТИТЬ ВТ_Данные
ИЗ
    РегистрНакопления.ТоварыНаСкладах.ОстаткиИОбороты(&НачПериода, &КонПериода, , , ) КАК ТоварыНаСкладахОстатки

ОБЪЕДИНИТЬ ВСЕ

// Шаг 2: Получаем продажи товаров за этот же период
ВЫБРАТЬ
    ПродажиОбороты.Номенклатура,
    0,
    0,
    ПродажиОбороты.КоличествоОборот
ИЗ
    РегистрНакопления.Продажи.Обороты(&НачПериода, &КонПериода, , ) КАК ПродажиОбороты
;

////////////////////////////////////////////////////////////////////////////////
// Шаг 3: Группируем полученные данные по номенклатуре и рассчитываем показатели
ВЫБРАТЬ
    ВТ_Данные.Номенклатура КАК Номенклатура,
    СУММА(ВТ_Данные.НачОст) КАК НачальныйОстаток,
    СУММА(ВТ_Данные.КонОст) КАК КонечныйОстаток,
    СУММА(ВТ_Данные.КоличествоПродано) КАК ВсегоПродано,
    // Вычисляем процент продаж от начального остатка во избежание деления на ноль:
    ВЫБОР
        КОГДА СУММА(ВТ_Данные.НачОст) > 0
            ТОГДА (СУММА(ВТ_Данные.КоличествоПродано) / СУММА(ВТ_Данные.НачОст)) * 100
        ИНАЧЕ 0
    КОНЕЦ КАК ПроцентПродаж
ИЗ
    ВТ_Данные КАК ВТ_Данные
ГРУППИРОВАТЬ ПО
    ВТ_Данные.Номенклатура
ИМЕЮЩИЕ
    // Накладываем фильтр оборачиваемости (например, продажи не превысили 10%)
    ВЫБОР
        КОГДА СУММА(ВТ_Данные.НачОст) > 0
            ТОГДА (СУММА(ВТ_Данные.КоличествоПродано) / СУММА(ВТ_Данные.НачОст)) * 100
        ИНАЧЕ 0
    КОНЕЦ <= &ДопустимыйПроцентПродаж
    И СУММА(ВТ_Данные.НачОст) > 0 // Анализируем только те товары, которые реально были на складе

Давайте проанализируем преимущества этого запроса:

  1. Благодаря конструкции ОБЪЕДИНИТЬ ВСЕ мы полностью защищены от потери товаров, по которым остатки были, а продаж не было вообще. Они гарантированно попадут в результирующую выборку с нулевым показателем КоличествоПродано.
  2. Использование конструкции ВЫБОР КОГДА защищает систему от фатальной ошибки деления на ноль, если по какой-то причине начальный остаток оказался пустым.
  3. Фильтрация происходит в секции ИМЕЮЩИЕ на уровне базы данных, что отсекает передачу лишних данных на клиентский компьютер и повышает скорость работы отчета.

Настройка СКД для автоматического распределения данных по месяцам

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

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

Обработка границы периода

Для того чтобы отчет выводил правильные остатки на самую последнюю секунду выбранного пользователем периода, обязательно обрабатывайте параметр даты окончания в коде модуля объекта отчета перед его формированием:


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

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

← На главную