Как заполнить табличную часть Инвентаризации НЗП и рассчитать суммы НУ на основе БУ

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

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

В этой статье мы подробно разберем практические трудности, с которыми сталкиваются разработчики при написании подобных обработок заполнения табличных частей (ТЧ). Мы последовательно проанализируем четыре критические ошибки из реальной практики, выясним их причины и напишем идеальный, отказоустойчивый код в соответствии со стандартами разработки «1С:Предприятие 8» и Библиотеки стандартных подсистем (БСП).

Ошибка 1. Добавление пустых строк и неправильное использование ЗаполнитьЗначенияСвойств

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

Посмотрим на фрагмент кода, который приводит к такому поведению:


// Ошибочный вариант
Выборка = Запрос.Выполнить().Выгрузить(); // Получаем Таблицу Значений
ДокументДляИзменения = СсылкаНаОбъект.Ссылка.ПолучитьОбъект();
ДокументДляИзменения.Состав.Очистить();

Для каждого ТекущаяСтрока из Выборка Цикл
    НовСтр = ДокументДляИзменения.Состав.Добавить();
    ЗаполнитьЗначенияСвойств(НовСтр, Выборка); // Ошибка здесь!
КонецЦикла;

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

В приведенном коде в качестве источника передается переменная Выборка, которая является целой Таблицей значений (поскольку был вызван метод Выгрузить()), а не текущей строкой обхода. У таблицы значений нет колонок как у индивидуальных свойств объекта (в контексте обращения к ней напрямую), поэтому функция не находит совпадений по именам колонок и оставляет новую строку НовСтр абсолютно пустой.

Для исправления этой ошибки достаточно передавать в функцию текущую строку итератора цикла:


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

Ошибка 2. Поле объекта не обнаружено (НоменклатурнаяГруппа)

При попытке уйти от выгрузки в таблицу значений и использовать классический последовательный выбор строк через Выборка.Следующий() разработчики часто совершают следующую синтаксическую ошибку:


// Ошибочный вариант обращения к ТЧ
Выборка = Запрос.Выполнить().Выбрать();
ДокументДляИзменения = СсылкаНаОбъект.Ссылка.ПолучитьОбъект();
ДокументДляИзменения.Состав.Очистить();

Пока Выборка.Следующий() Цикл
    ДокументДляИзменения.Состав.НоменклатурнаяГруппа = Выборка.НоменклатурнаяГруппа; // Ошибка!
    ДокументДляИзменения.Состав.Сумма = Выборка.Сумма;
    ДокументДляИзменения.Состав.СуммаНУ = Выборка.СуммаНУ;
КонецЦикла;

При выполнении этого кода система выдаст ошибку: «Поле объекта не обнаружено (НоменклатурнаяГруппа)». Почему так происходит?

Объект Состав представляет собой коллекцию строк табличной части (объект типа ДокументТабличнаяЧасть.ИнвентаризацияНЗП.Состав). Сама коллекция не имеет реквизитов вроде НоменклатурнаяГруппа или Сумма. Эти реквизиты есть только у конкретных элементов коллекции — строк табличной части (тип ДокументТабличнаяЧастьСтрока.ИнвентаризацияНЗП.Состав).

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


// Правильный вариант работы с выборкой запроса
Пока Выборка.Следующий() Цикл
    // Создаем строку и получаем ссылку на нее в переменную НовСтр
    НовСтр = ДокументДляИзменения.Состав.Добавить(); 
    НовСтр.НоменклатурнаяГруппа = Выборка.НоменклатурнаяГруппа;
    НовСтр.Сумма = Выборка.Сумма;
    НовСтр.СуммаНУ = Выборка.СуммаНУ;
КонецЦикла;

Ошибка 3. Логическая катастрофа вложенного цикла (перезапись строк)

Теперь разберем более сложную логическую ошибку. Бизнес-требование изменилось: нам больше не нужно очищать и перезаполнять табличную часть заново. Бухгалтер самостоятельно заполнил строки документа (номенклатурные группы и суммы БУ). Наша задача — рассчитать по формулам и заполнить колонку СуммаНУ для уже существующих строк.

Автор темы попытался решить эту задачу с помощью следующей конструкции:


// Ошибочный алгоритм сопоставления данных
Пока Выборка.Следующий() Цикл
    Для каждого НовСтр из ДокументДляИзменения.Состав Цикл
        Приход20 = Выборка.СуммаНачальныйОстатокДт + Выборка.СуммаОборотДт;
        Расход20 = Приход20 - Выборка.СуммаНЗПБУ;
        КоэффициентСписания = Окр(Расход20/Приход20, 2);
        Приход20НУ = Выборка.СуммаНУНачальныйОстатокДт + Выборка.СуммаНУОборотДт;
        Расход20НУ = Приход20НУ * КоэффициентСписания;
        СуммаНЗПНУ = Окр(Приход20НУ - Расход20НУ, 2);
        
        НовСтр.СуммаНУ = СуммаНЗПНУ; // Запись значения во все строки
    КонецЦикла;
КонецЦикла;

Проанализируем ситуацию и поймем, почему этот код работает некорректно. Внутренний цикл Для каждого НовСтр из ДокументДляИзменения.Состав перебирает абсолютно все строки документа на каждом шаге внешней выборки.

Предположим, у нас в документе 3 строки с разными номенклатурными группами: «А», «Б» и «В».

  1. На первом шаге внешней выборки (для группы «А») программа рассчитывает СуммаНЗПНУ и записывает ее во все три строки документа.
  2. На втором шаге (для группы «Б») программа снова рассчитывает сумму и опять записывает ее во все строки документа, затирая предыдущие данные для группы «А».
  3. На последнем шаге (для группы «В») во всех строках документа устанавливается значение, рассчитанное исключительно для группы «В».

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


// Правильное сопоставление данных через метод Найти()
Пока Выборка.Следующий() Цикл
    
    // Ищем в ТЧ документа строку, где НоменклатурнаяГруппа совпадает с выборкой запроса
    СтрокаДокумента = ДокументДляИзменения.Состав.Найти(Выборка.НоменклатурнаяГруппа, "НоменклатурнаяГруппа");
    
    // Если строка найдена, выполняем расчет только для нее
    Если СтрокаДокумента <> Неопределено Тогда
        
        Приход20 = Выборка.СуммаНачальныйОстатокДт + Выборка.СуммаОборотДт;
        
        // Обязательно добавляем проверку на деление на 0!
        Если Приход20 <> 0 Тогда 
            Расход20 = Приход20 - СтрокаДокумента.Сумма; // Сумму БУ берем из строки документа
            КоэффициентСписания = Окр(Расход20 / Приход20, 2);
            
            Приход20НУ = Выборка.СуммаНУНачальныйОстатокДт + Выборка.СуммаНУОборотДт;
            Расход20НУ = Приход20НУ * КоэффициентСписания;
            
            СтрокаДокумента.СуммаНУ = Окр(Приход20НУ - Расход20НУ, 2);
        Иначе
            СтрокаДокумента.СуммаНУ = 0;
        КонецЕсли;
        
    КонецЕсли;
КонецЦикла;

Ошибка 4. Критическая архитектурная ошибка (принудительная запись в БД)

Обратите внимание, как в коде автора темы реализовано сохранение изменений: сначала получается объект из базы данных через метод ПолучитьОбъект(), затем он модифицируется и принудительно записывается методом ДокументДляИзменения.Записать(РежимЗаписиДокумента.Проведение). На клиенте форма при этом принудительно перечитывается.

Почему это грубый антипаттерн в разработке 1С:

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

Правильное решение: интеграция обработки по стандартам БСП

Современный стандарт разработки на управляемых формах с использованием Библиотеки стандартных подсистем (БСП) предлагает специальный тип команды дополнительных обработок — ЗаполнениеФормы. Этот тип команды передает в процедуру обработки контекст открытой формы (ДанныеФормыСтруктура). Мы можем изменить данные документа прямо на экране пользователя без записи в базу данных!

Рассмотрим по шагам, как написать правильную внешнюю обработку заполнения.

Шаг 1. Модуль объекта внешней обработки (Регистрация)

В модуле объекта внешней обработки разместим обязательную функцию интеграции с БСП, указав тип команды ЗаполнениеФормы:


Функция СведенияОВнешнейОбработке() Экспорт
    
    ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1");
    ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.ВидОбработкиЗаполнениеОбъекта();
    ПараметрыРегистрации.Наименование = "Расчет сумм НУ в Инвентаризации НЗП";
    ПараметрыРегистрации.Версия = "1.1";
    ПараметрыРегистрации.Назначение.Добавить("Документ.ИнвентаризацияНЗП");
    
    Команда = ПараметрыРегистрации.Команды.Добавить();
    Команда.Представление = "Рассчитать суммы НУ (без перезаписи)";
    Команда.Идентификатор = "РассчитатьСуммыНУ";
    // Использование типа команды "ЗаполнениеФормы" позволяет работать с контекстом формы клиента
    Команда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыЗаполнениеФормы();
    
    Возврат ПараметрыРегистрации;
    
КонецФункции

// Основной метод, вызываемый БСП при нажатии кнопки заполнения
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполнения) Экспорт
    
    Если ИдентификаторКоманды = "РассчитатьСуммыНУ" Тогда
        ВыполнитьЗаполнениеСумм(ПараметрыВыполнения);
    КонецЕсли;
    
КонецПроцедуры

Шаг 2. Реализация алгоритма заполнения данных формы

Ниже приведем полный код процедуры заполнения, который работает непосредственно с формой документа без обращения к СУБД для записи. Обратите внимание, что мы получаем остатки и обороты из регистра сведений, выгружаем их во временную таблицу в памяти сервера, индексируем по полю связи для быстрого поиска и обходим непосредственно строки формы документа.


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

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

Подведем итоги проделанной работы

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

  1. Отсутствуют пустые строчки: Мы правильно передаем строку-итератор в метод ЗаполнитьЗначенияСвойств.
  2. Нет ошибок обращения к реквизитам: Обращение происходит строго к конкретной строке табличной части, а не ко всей коллекции в целом.
  3. Нет затирания данных: Вместо двойного цикла мы применили эффективный метод индексированного поиска ТаблицаРегистра.Найти().
  4. Безопасность транзакций: Благодаря использованию механизма ЗаполнениеФормы БСП, данные меняются на экране пользователя мгновенно, без принудительной фоновой записи в базу данных. Бухгалтер может визуально проверить правильность расчетов и сам нажать стандартную кнопку сохранения документа.
← На главную