Рассмотрим довольно распространенную логическую ошибку, с которой могут столкнуться разработчики 1С при написании кода проведения документов. Симптомы выглядят очень странно: в документ добавляются и товары, и услуги, но при проведении все бухгалтерские проводки формируются так, как будто в документе находятся только товары, если последняя строка — товар. И наоборот, если последней строкой указать услугу, все проводки формируются как для услуг.
Такое поведение не является сбоем платформы. Это следствие некорректной структуры кода в модуле проведения. Давайте детально разберем причину на конкретном примере и посмотрим, как это исправить.
Проанализируем исходный код, который приводит к такому поведению. Автор пытается сформировать движения по нескольким регистрам накопления и регистру бухгалтерии на основании табличной части документа "Поступление товаров и услуг".
Вот фрагмент кода из модуля проведения документа:
// ... Код запроса к табличной части ...
Пока Выборка.Следующий() Цикл
Если Выборка.НоменклатураТип = Перечисления.ТипыНоменклатуры.Товар Тогда
Движение = Движения.Товары.Добавить();
// ... формирование движений по регистру Товары
Иначе
Движение = Движения.Расходы.Добавить();
// ... формирование движений по регистру Расходы
КонецЕсли;
Движение = Движения.ВзаиморасчетыСКонтрагентами.Добавить();
// ... формирование движений по регистру Взаиморасчеты
КонецЦикла;
// !!! ОШИБКА НАХОДИТСЯ ЗДЕСЬ !!!
Движения.Управленческий.Записывать = Истина;
Если Выборка.НоменклатураТип = Перечисления.ТипыНоменклатуры.Товар Тогда
Для Каждого ТекСтрокаТоварыИУслуги Из ТоварыИУслуги Цикл
Движение = Движения.Управленческий.Добавить();
Движение.СчетДт = ПланыСчетов.Управленческий.Товары;
// ... заполнение проводки для товара
КонецЦикла;
КонецЕсли;
Если Выборка.НоменклатураТип = Перечисления.ТипыНоменклатуры.Услуга Тогда
Для Каждого ТекСтрокаТоварыИУслуги Из ТоварыИУслуги Цикл
Движение = Движения.Управленческий.Добавить();
Движение.СчетДт = ПланыСчетов.Управленческий.Расходы;
// ... заполнение проводки для услуги
КонецЦикла;
КонецЕсли;
Выясним причину по шагам:
Пока Выборка.Следующий() Цикл. Он перебирает сгруппированные данные из табличной части документа. Когда этот цикл завершает свою работу, переменная Выборка не исчезает. Она продолжает существовать и хранит в себе значения полей из последней обработанной записи.
Если Выборка.НоменклатураТип = ... выполняются уже после того, как весь перебор закончен.
Выборка.НоменклатураТип. Так как цикл уже завершен, это поле содержит тип номенклатуры из последней строки сгруппированного запроса. Именно этот тип и определяет, какой из двух блоков Если ... КонецЕсли; будет выполнен.
Если запускается новый цикл Для Каждого ТекСтрокаТоварыИУслуги Из ТоварыИУслуги Цикл. Этот цикл перебирает всю исходную табличную часть документа заново. Но поскольку решение о том, какой счет дебета (СчетДт) использовать, уже принято на предыдущем шаге, для каждой строки документа формируется проводка одного и того же типа.
В результате мы и получаем наблюдаемый эффект: если последняя позиция в документе — товар, все проводки формируются по счету "Товары". Если последняя позиция — услуга, все проводки формируются по счету "Расходы", независимо от реального типа номенклатуры в каждой конкретной строке.
Решение этой проблемы достаточно простое и логичное: необходимо перенести логику формирования проводок по регистру бухгалтерии внутрь основного цикла Пока Выборка.Следующий() Цикл. Таким образом, для каждой строки из выборки будет выполняться своя, индивидуальная проверка типа номенклатуры, и на её основании будет формироваться правильная проводка.
Дополнительно оптимизируем код: движение по взаиморасчетам с контрагентом обычно делается одно, на общую сумму документа, а не по каждой строке. Вынесем его формирование за пределы цикла.
Вот как будет выглядеть исправленный и оптимизированный код:
// ... Код запроса остается без изменений ...
// Включаем запись для всех нужных регистров перед циклом
Движения.Управленческий.Записывать = Истина;
Движения.Товары.Записывать = Истина;
Движения.Расходы.Записывать = Истина;
Движения.ВзаиморасчетыСКонтрагентами.Записывать = Истина;
// Основной цикл по сгруппированным данным
Пока Выборка.Следующий() Цикл
// --- Формируем движения по регистрам накопления ---
Если Выборка.НоменклатураТип = Перечисления.ТипыНоменклатуры.Товар Тогда
ДвижениеТовар = Движения.Товары.ДобавитьПриход();
ДвижениеТовар.Период = Дата;
ДвижениеТовар.Номенклатура = Выборка.Номенклатура;
ДвижениеТовар.Количество = Выборка.Количество;
ДвижениеТовар.Сумма = Выборка.Сумма;
Иначе
ДвижениеРасход = Движения.Расходы.ДобавитьПриход(); // Услуги тоже "приходят" на затраты
ДвижениеРасход.Период = Дата;
ДвижениеРасход.Номенклатура = Выборка.Номенклатура;
ДвижениеРасход.Сумма = Выборка.Сумма;
КонецЕсли;
// --- Формируем движения по регистру бухгалтерии "Управленческий" ---
// Для каждой строки выборки создаем свою проводку
ДвижениеУпр = Движения.Управленческий.Добавить();
ДвижениеУпр.Период = Дата;
// Общая часть проводки (кредит)
ДвижениеУпр.СчетКт = ПланыСчетов.Управленческий.РасчетыСПоставщиками;
ДвижениеУпр.СубконтоКт[ПланыВидовХарактеристик.ВидыСубконто.контрагенты] = Поставщик;
ДвижениеУпр.Сумма = Выборка.Сумма;
// Уникальная часть проводки (дебет) определяется типом номенклатуры
Если Выборка.НоменклатураТип = Перечисления.ТипыНоменклатуры.Товар Тогда
ДвижениеУпр.СчетДт = ПланыСчетов.Управленческий.Товары;
ДвижениеУпр.КоличествоДт = Выборка.Количество;
ДвижениеУпр.СубконтоДт[ПланыВидовХарактеристик.ВидыСубконто.номенклатура] = Выборка.Номенклатура;
Иначе // Это услуга
ДвижениеУпр.СчетДт = ПланыСчетов.Управленческий.Расходы;
КонецЕсли;
КонецЦикла;
// Движение по взаиморасчетам делаем ОДИН РАЗ после цикла на ОБЩУЮ сумму документа
Если ТоварыИУслуги.Количество() > 0 Тогда
ДвижениеВзаиморасчеты = Движения.ВзаиморасчетыСКонтрагентами.ДобавитьРасход();
ДвижениеВзаиморасчеты.Период = Дата;
ДвижениеВзаиморасчеты.Контрагент = Поставщик;
// Используем итоговое значение из табличной части документа
ДвижениеВзаиморасчеты.Сумма = ТоварыИУслуги.Итог("Сумма");
КонецЕсли;
Что изменилось в исправленном коде:
Выборка.НоменклатураТип, Выборка.Сумма и т.д.), теперь находится внутри цикла Пока Выборка.Следующий() Цикл.
ПланыСчетов.Управленческий.Товары или ПланыСчетов.Управленческий.Расходы).
ВзаиморасчетыСКонтрагентами формируется один раз на общую сумму документа с помощью метода ТоварыИУслуги.Итог("Сумма"). Это более правильный и производительный подход, чем создание множества мелких движений в цикле.
Ключевой вывод: всегда размещайте код, который обрабатывает данные текущей итерации цикла, непосредственно внутри этого цикла. Любые действия, вынесенные за его пределы, будут работать с данными, оставшимися от последней итерации, что и приводит к подобным логическим ошибкам.