Как подставить и проконтролировать срок годности номенклатуры из приходной накладной в расходную накладную в 1С без партионного учета?

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

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

В данной статье мы подробно разберем две основные проблемы, с которыми сталкиваются разработчики при решении этой задачи:

  1. Как организовать удобный выбор срока годности в документе РасходнаяНакладная, чтобы пользователь видел только те даты, которые реально есть на остатках.
  2. Почему не работает или работает некорректно проверка остатков в процедуре ОбработкаПроведения, и как исправить типичную ошибку с вложенными циклами выборки запроса.

Часть 1. Реализация выбора срока годности на форме Расходной накладной

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

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

Во-первых, на клиенте мы перехватываем стандартное действие платформы, устанавливая параметр СтандартнаяОбработка = Ложь. Это позволит нам вместо стандартного календаря показать пользователю подготовленный список значений.

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

Рассмотрим пример реализации данного алгоритма в модуле управляемой формы документа РасходнаяНакладная:


&НаКлиенте
Процедура СписокНоменклатурыСрокГодностиНачалоВыбора(Элемент, ДанныеВыбора, ВыборДобавлением, СтандартнаяОбработка)
    
    // Отменяем стандартный вызов календаря
    СтандартнаяОбработка = Ложь;
    
    // Получаем текущую строку табличной части
    ТекущаяСтрока = Элементы.СписокНоменклатуры.ТекущиеДанные;
    
    // Проверяем, заполнена ли номенклатура, так как без нее мы не сможем определить остатки
    Если ТекущаяСтрока = Неопределено Или Не ЗначениеЗаполнено(ТекущаяСтрока.Номенклатура) Тогда
        ПоказатьПредупреждение(, "Для выбора срока годности сначала необходимо выбрать номенклатуру!");
        Возврат;
    КонецЕсли;
    
    // Получаем массив уникальных дат со сроками годности, которые есть в наличии
    ДоступныеСроки = ПолучитьДоступныеСрокиГодностиНаСервере(ТекущаяСтрока.Номенклатура, Объект.Дата);
    
    Если ДоступныеСроки.Количество() = 0 Тогда
        ПоказатьПредупреждение(, "На складе нет остатков по данной номенклатуре!");
        Возврат;
    КонецЕсли;
    
    // Заполняем список выбора для интерактивного меню
    СписокВыбора = Новый СписокЗначений;
    СписокВыбора.ЗагрузитьЗначения(ДоступныеСроки);
    
    // Предлагаем пользователю выбрать значение из сформированного списка
    ВыбранныйЭлемент = ВыбратьИзСписка(СписокВыбора, Элемент);
    Если ВыбранныйЭлемент <> Неопределено Тогда
        ТекущаяСтрока.СрокГодности = ВыбранныйЭлемент.Значение;
    КонецЕсли;
    
КонецПроцедуры

Теперь спроектируем серверную функцию, которая выполняет запрос к виртуальной таблице остатков регистра бухгалтерии. Обратите внимание, что мы накладываем жесткий отбор по счету, первому субконто (номенклатуре) и фильтруем записи, оставляя только те, где КоличествоОстаток > 0.


&НаСервереБезКонтекста
Функция ПолучитьДоступныеСрокиГодностиНаСервере(ВыбраннаяНоменклатура, ДатаДокумента)
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ РАЗЛИЧНЫЕ
        |	БухУчетОстатки.Субконто2 КАК СрокГодности
        |ИЗ
        |	РегистрБухгалтерии.БухУчет.Остатки(&Период, Счет = &СчетТовары, &ВидыСубконто, Субконто1 = &Номенклатура) КАК БухУчетОстатки
        |ГДЕ
        |	БухУчетОстатки.КоличествоОстаток > 0
        |
        |УПОРЯДОЧИТЬ ПО
        |	СрокГодности";
        
    Запрос.УстановитьПараметр("Период", ДатаДокумента);
    Запрос.УстановитьПараметр("СчетТовары", ПланыСчетов.Управленческий.Товары);
    Запрос.УстановитьПараметр("Номенклатура", ВыбраннаяНоменклатура);
    
    // Формируем массив видов субконто для правильного позиционирования аналитики в виртуальной таблице
    ВидыСубконто = Новый Массив;
    ВидыСубконто.Добавить(ПланыВидовХарактеристик.ВидыСубконто.Номенклатура);
    ВидыСубконто.Добавить(ПланыВидовХарактеристик.ВидыСубконто.СрокГодности);
    Запрос.УстановитьПараметр("ВидыСубконто", ВидыСубконто);
    
    РезультатЗапроса = Запрос.Выполнить();
    Если РезультатЗапроса.Пустой() Тогда
        Возврат Новый Массив;
    КонецЕсли;
    
    // Выгружаем колонку со сроками годности в массив и возвращаем на клиент
    ТаблицаРезультата = РезультатЗапроса.Выгрузить();
    Возврат ТаблицаРезультата.ВыгрузитьКолонку("СрокГодности");
    
КонецФункции

Этот подход гарантирует, что пользователь физически не сможет совершить ошибку ввода даты или выбрать тот срок годности, товара с которым уже нет на складе.

Часть 2. Анализ и исправление ошибок в коде ОбработкиПроведения

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

Ошибка первая: Попытка обхода по группировкам при отсутствии итогов в запросе.

В исходном коде автора результатом запроса управляет следующая строка:

ВыборкаНоменклатура = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);

При этом в самом тексте SQL-запроса полностью отсутствует секция ИТОГИ ... ПО .... По правилам работы платформы 1С, если в запросе нет расчета итогов, то метод Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам) возвращает обычную линейную выборку детальных записей, но позиционирует ее некорректно для вложенных циклов. Когда внутри внешнего цикла мы пишем:

ВыборкаДетальныеЗаписи = ВыборкаНоменклатура.Выбрать();

программа пытается получить подчиненные (детальные) записи для текущей группировки. Но так как текущая запись уже является детальной, подчиненных записей у нее нет. Метод возвращает пустую выборку, и цикл Пока ВыборкаДетальныеЗаписи.Следующий() мгновенно завершается, даже не начавшись.

Ошибка вторая: Некорректное использование переменной Отказ для управления логикой цикла.

В первом цикле автора была написана следующая конструкция:


Если ВыборкаНоменклатура.КоличествоОстатокСклад <= 0 Тогда 
    // ... Вывод сообщения
    Отказ = Истина;
КонецЕсли;

Если Отказ Тогда 
    Продолжить; 
КонецЕсли;

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

Правильное архитектурное решение:

Поскольку в первой временной таблице ВТ_ТЧ уже выполнено группирование по уникальным парам «Номенклатура — Срок годности», вложенные циклы нам вообще не нужны! Результат запроса левого соединения ТЧ с остатками регистра всегда будет плоским и содержать уникальные строки. Достаточно использовать один простой обход.

Посмотрим на исправленный и оптимизированный вариант процедуры проведения с использованием правильных блокировок данных и единого цикла обхода выборки:


Процедура ОбработкаПроведения(Отказ, Режим)
    
    // Очищаем старые движения перед проведением
    Движения.БухУчет.Записывать = Истина;
    Движения.БухУчет.Записать();
    
    // Установка транзакционной блокировки данных для предотвращения "грязного чтения"
    Блокировка = Новый БлокировкаДанных;
    ЭлементБлокировки = Блокировка.Добавить("РегистрБухгалтерии.БухУчет");
    ЭлементБлокировки.УстановитьЗначение("Счет", ПланыСчетов.Управленческий.Товары);
    ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
    
    // В качестве источника данных для блокировки используем табличную часть документа
    ЭлементБлокировки.ИсточникДанных = СписокНоменклатуры;
    ЭлементБлокировки.ИспользоватьИзИсточникаДанных(ПланыВидовХарактеристик.ВидыСубконто.Номенклатура, "Номенклатура");
    ЭлементБлокировки.ИспользоватьИзИсточникаДанных(ПланыВидовХарактеристик.ВидыСубконто.СрокГодности, "СрокГодности");
    Блокировка.Заблокировать();
    
    // Подготовка запроса для расчета остатков
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	РасходнаяСписок.Номенклатура КАК Номенклатура,
        |	РасходнаяСписок.СрокГодности КАК СрокГодности,
        |	СУММА(РасходнаяСписок.Количество) КАК КоличествоДок,
        |	СУММА(РасходнаяСписок.Сумма) КАК СуммаДок
        |ПОМЕСТИТЬ ВТ_ТЧ
        |ИЗ
        |	Документ.РасходнаяНакладная.СписокНоменклатуры КАК РасходнаяСписок
        |ГДЕ
        |	РасходнаяСписок.Ссылка = &Ссылка
        |
        |СГРУППИРОВАТЬ ПО
        |	РасходнаяСписок.Номенклатура,
        |	РасходнаяСписок.СрокГодности
        |
        |ИНДЕКСИРОВАТЬ ПО
        |	Номенклатура,
        |	СрокГодности
        |;
        |
        |////////////////////////////////////////////////////////////////////////////////
        |ВЫБРАТЬ
        |	ВТ_ТЧ.Номенклатура КАК Номенклатура,
        |	ВТ_ТЧ.СрокГодности КАК СрокГодности,
        |	ВТ_ТЧ.КоличествоДок КАК КоличествоДок,
        |	ВТ_ТЧ.СуммаДок КАК СуммаДок,
        |	ЕСТЬNULL(БухУчетОстатки.КоличествоОстаток, 0) КАК КоличествоОстатокСклад,
        |	ВТ_ТЧ.Номенклатура.Представление КАК НоменклатураПред
        |ИЗ
        |	ВТ_ТЧ КАК ВТ_ТЧ
        |		ЛЕВОЕ СОЕДИНЕНИЕ РегистрБухгалтерии.БухУчет.Остатки(
        |				&МоментВремени,
        |				Счет = &Товары,
        |				&Субконто,
        |				(Субконто1, Субконто2) В
        |					(ВЫБРАТЬ
        |						ВТ_ТЧ.Номенклатура,
        |						ВТ_ТЧ.СрокГодности
        |					ИЗ
        |						ВТ_ТЧ КАК ВТ_ТЧ)) КАК БухУчетОстатки
        |		ПО ВТ_ТЧ.Номенклатура = БухУчетОстатки.Субконто1
        |			И ВТ_ТЧ.СрокГодности = БухУчетОстатки.Субконто2";
        
    Запрос.УстановитьПараметр("МоментВремени", МоментВремени);
    Запрос.УстановитьПараметр("Ссылка", Ссылка);
    Запрос.УстановитьПараметр("Товары", ПланыСчетов.Управленческий.Товары);
    
    Субконто = Новый Массив;
    Субконто.Добавить(ПланыВидовХарактеристик.ВидыСубконто.Номенклатура);
    Субконто.Добавить(ПланыВидовХарактеристик.ВидыСубконто.СрокГодности);
    Запрос.УстановитьПараметр("Субконто", Субконто);
    
    РезультатЗапроса = Запрос.Выполнить();
    
    // Прямой линейный обход без указания группировок
    Выборка = РезультатЗапроса.Выбрать();
    
    Пока Выборка.Следующий() Цикл
        
        // Проверяем наличие остатка на складе по конкретному сроку годности
        Если Выборка.КоличествоДок > Выборка.КоличествоОстатокСклад Тогда
            
            Сообщение = Новый СообщениеПользователю;
            Сообщение.Текст = Шаблон(
                "Не хватает остатка по номенклатуре %1 со сроком годности %2. Требуется %3, в наличии %4",
                Выборка.НоменклатураПред,
                Формат(Выборка.СрокГодности, "ДФ=dd.MM.yyyy"),
                Выборка.КоличествоДок,
                Выборка.КоличествоОстатокСклад
            );
            Сообщение.Сообщить();
            
            // Фиксируем отказ в проведении, но продолжаем цикл для выявления остальных ошибок
            Отказ = Истина;
            
        КонецЕсли;
        
        // Если уже зафиксирован отказ, новые движения формировать нет смысла
        Если Отказ Тогда
            Продолжить;
        КонецЕсли;
        
        // 1. Проводка Списание себестоимости: Дт ПрибылиУбытки - Кт Товары
        Движение = Движения.БухУчет.Добавить();
        Движение.СчетДт = ПланыСчетов.Управленческий.ПрибылиУбытки;
        Движение.СчетКт = ПланыСчетов.Управленческий.Товары;
        Движение.Период = Дата;
        Движение.Сумма = Выборка.СуммаДок; // В реальной задаче здесь рассчитывается себестоимость
        Движение.КоличествоКт = Выборка.КоличествоДок;
        
        // Заполнение аналитики Дт и Кт
        Движение.СубконтоДт[ПланыВидовХарактеристик.ВидыСубконто.Номенклатура] = Выборка.Номенклатура;
        Движение.СубконтоКт[ПланыВидовХарактеристик.ВидыСубконто.Номенклатура] = Выборка.Номенклатура;
        Движение.SubkontoКт[ПланыВидовХарактеристик.ВидыСубконто.СрокГодности] = Выборка.СрокГодности;
        
        // 2. Проводка Выручка: Дт Покупатели - Кт ПрибылиУбытки
        ДвижениеВыручка = Движения.БухУчет.Добавить();
        ДвижениеВыручка.СчетДт = ПланыСчетов.Управленческий.Покупатели;
        ДвижениеВыручка.СчетКт = ПланыСчетов.Управленческий.ПрибылиУбытки;
        ДвижениеВыручка.Период = Дата;
        ДвижениеВыручка.Сумма = Выборка.СуммаДок;
        ДвижениеВыручка.СубконтоКт[ПланыВидовХарактеристик.ВидыСубконто.Номенклатура] = Выборка.Номенклатура;
        
    КонецЦикла;
    
КонецПроцедуры

Проанализировав данное решение, мы видим, что код стал лаконичным, производительным и избавился от избыточной вложенности циклов. Мы гарантировали корректный обход всех строк, полное информирование пользователя обо всех ошибках нехватки товаров и обеспечили надежную работу механизма транзакционных блокировок платформы 1С:Предприятие 8.3.

← На главную