Как построить отчет "Ведомость по партиям товаров" в 1С и избежать путаницы с Регистратором

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

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

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

Разберем главную ошибку: различие между «Регистратором» и «Партией»

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

Рассмотрим пример:

Мы купили 10 штук товара «Смартфон» по Приходной накладной №1. В регистре накопления появляется запись:

Затем мы продаем 3 штуки этого товара по Расходной накладной №5. В регистре накопления формируется движение:

Если в вашей структуре регистра нет измерения Партия, система не сможет определить, какой именно из ранее пришедших товаров списывается при продаже. Вы не сможете реализовать ни метод FIFO (первым пришел — первым ушел), ни метод LIFO, ни учет по конкретным документам поступления.

Шаг 1. Проектирование регистра для партионного учета

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

  1. Откройте конфигуратор и найдите ваш регистр накопления (например, ОстаткиНоменклатуры). Обратите внимание: тип регистра должен быть Остатки.
  2. Перейдите на вкладку Данные.
  3. Добавьте новое Измерение с именем Партия.
  4. Задайте тип данных для измерения Партия. Это должен быть ссылочный тип. Если в вашей системе партией может быть только один вид документа, укажите ДокументСсылка.ПриходнаяНакладная. Если же партию могут образовывать разные документы (Приходная накладная, Оприходование товаров, Поступление из переработки), создайте Определяемый тип (например, ОпределяемыйТип.ПартияТоваров) или укажите составной тип данных.

Теперь наш регистр готов хранить информацию о товарах в разрезе партий. Перейдем к заполнению этих данных при проведении документов.

Шаг 2. Запись партии при приходе товара

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


Процедура ОбработкаПроведения(Отказ, РежимПроведения)
    
    // Очищаем движения регистра перед записью новых
    Движения.ОстаткиНоменклатуры.Записывать = Истина;
    Движения.ОстаткиНоменклатуры.Очистить();
    
    Для Каждого ТекСтрокаТовары Из Товары Цикл
        
        // Создаем новое движение по регистру
        Движение = Движения.ОстаткиНоменклатуры.ДобавитьРасход(); // Для прихода используем ДобавитьПриход()
        Движение.ВидДвижения = ВидДвиженияНакопления.Приход;
        Движение.Период = Дата;
        
        // Заполняем аналитику
        Движение.Номенклатура = ТекСтрокаТовары.Номенклатура;
        Движение.Склад = Склад;
        
        // КРИТИЧЕСКИ ВАЖНО: записываем ссылку на текущий документ как партию
        Движение.Партия = Ссылка; 
        
        // Заполняем ресурсы
        Движение.Количество = ТекСтрокаТовары.Количество;
        Движение.Сумма = ТекСтрокаТовары.Сумма;
        
    КонецЦикла;

КонецПроцедуры

Шаг 3. Алгоритм списания партий при продаже (метод FIFO)

При проведении Расходной накладной нам нужно не просто списать количество, а определить, из каких именно партий (приходных документов) и в каком объеме уходит товар. Реализуем классический алгоритм FIFO (First In, First Out) в модуле проведения документа продажи.

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


Процедура ОбработкаПроведения(Отказ, РежимПроведения)
    
    Движения.ОстаткиНоменклатуры.Записывать = Истина;
    Движения.ОстаткиНоменклатуры.Очистить();
    
    // 1. Управляемая блокировка данных для предотвращения "коллизий"
    Блокировка = Новый БлокировкаДанных;
    ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ОстаткиНоменклатуры");
    ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
    ЭлементБлокировки.ИсточникДанных = Товары;
    ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Номенклатура", "Номенклатура");
    Блокировка.Заблокировать();
    
    // 2. Подготовим таблицу товаров для анализа (сгруппируем по номенклатуре)
    ТаблицаТоваров = Товары.Выгрузить();
    ТаблицаТоваров.Свернуть("Номенклатура", "Количество");
    
    // 3. Получим остатки по партиям из виртуальной таблицы регистра
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	Остатки.Номенклатура КАК Номенклатура,
        |	Остатки.Партия КАК Партия,
        |	Остатки.КоличествоОстаток КАК КоличествоОстаток
        |ИЗ
        |	РегистрНакопления.ОстаткиНоменклатуры.Остатки(&МоментВремени, Номенклатура В (&СписокНоменклатуры)) КАК Остатки
        |
        |УПОРЯДОЧИТЬ ПО
        |	Остатки.Партия.МоментВремени ВОЗР"; // Сортировка по дате партии (FIFO)
        
    СписокНоменклатуры = ТаблицаТоваров.ВыгрузитьКолонку("Номенклатура");
    Запрос.УстановитьПараметр("МоментВремени", Новый Граница(МоментВремени(), ВидГраницы.Исключая));
    Запрос.УстановитьПараметр("СписокНоменклатуры", СписокНоменклатуры);
    
    РезультатЗапроса = Запрос.Выполнить();
    Если РезультатЗапроса.Пустой() Тогда
        ВызватьИсключение "На складе нет остатков для списания!";
    КонецЕсли;
    
    ВыборкаОстатков = РезультатЗапроса.Выбрать();
    
    // 4. Обход товаров и списание по партиям
    Для Каждого СтрокаТовара Из ТаблицаТоваров Цикл
        
        НужноСписать = СтрокаТовара.Количество;
        
        // Сбрасываем фильтр выборки на начало
        ВыборкаОстатков.Сбросить();
        
        Пока НужноСписать > 0 И ВыборкаОстатков.НайтиСледующий(Новый Структура("Номенклатура", СтрокаТовара.Номенклатура)) Цикл
            
            ДоступноНаПартии = ВыборкаОстатков.КоличествоОстаток;
            
            Если ДоступноНаПартии <= 0 Тогда
                Продолжить;
            КонецЕсли;
            
            // Сколько мы можем забрать из текущей партии?
            СписываемСПартии = Мин(НужноСписать, ДоступноНаПартии);
            
            // Создаем движение расхода
            Движение = Движения.ОстаткиНоменклатуры.ДобавитьРасход();
            Движение.Период = Дата;
            Движение.Номенклатура = СтрокаТовара.Номенклатура;
            Движение.Склад = Склад;
            Движение.Партия = ВыборкаОстатков.Партия; // Указываем конкретную партию списания
            Движение.Количество = СписываемСПартии;
            
            // Уменьшаем счетчики
            НужноСписать = НужноСписать - СписываемСПартии;
            ВыборкаОстатков.КоличествоОстаток = ВыборкаОстатков.КоличествоОстаток - СписываемСПартии; // Корректируем остаток в выборке для повторных проходов
            
        КонецЦикла;
        
        // Если после обхода всех партий товар остался не списан — фиксируем нехватку
        Если НужноСписать > 0 Тогда
            ОбщегоНазначенияКлиентСервер.СообщитьПользователю(
                НСтр("ru = 'Недостаточно товара " + СтрокаТовара.Номенклатура + " в количестве " + НужноСписать + " шт.'")
            );
            Отказ = Истина;
        КонецЕсли;
        
    КонецЦикла;
    
КонецПроцедуры

Обратите внимание на конструкцию УПОРЯДОЧИТЬ ПО Остатки.Партия.МоментВремени ВОЗР. Именно она гарантирует, что система выберет самую старую приходную накладную для первоочередного списания. Если вам потребуется метод LIFO, достаточно изменить направление сортировки на убывающее (УБЫВ).

Шаг 4. Разработка правильного запроса для отчета

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

Для решения нашей задачи идеально подходит одна-единственная виртуальная таблица: РегистрНакопления.ОстаткиНоменклатуры.ОстаткиИОбороты.

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

Проанализируем шаблон правильного запроса для отчета:


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

Почему этот запрос работает правильно?

Шаг 5. Построение отчета в СКД (Системе компоновки данных)

Чтобы отчет выглядел презентабельно и имел удобную структуру группировок (Номенклатура -> Партия -> Регистратор), мы настроим его с помощью схемы компоновки данных.

  1. Создайте новый отчет и откройте Схему компоновки данных.
  2. Добавьте набор данных - запрос, скопировав текст запроса, который мы спроектировали на предыдущем шаге.
  3. Перейдите на вкладку Настройки.
  4. Создайте структуру группировок. Добавьте группировку Номенклатура, внутрь нее добавьте подчиненную группировку Партия, а внутрь нее — подчиненную группировку Регистратор.
  5. На вкладке Выбранные поля укажите ресурсы: НачОст, Приход, Расход, КонОст.

Важное замечание по расчету итогов:

Поскольку мы используем периодичность по регистратору, простое суммирование полей остатков на уровне группировок «Партия» и «Номенклатура» выдаст неверные (завышенные) результаты. Система компоновки данных должна рассчитывать остатки по временной шкале.

Для этого перейдите на вкладку Роли ваших полей в наборе данных запроса:

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

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

← На главную