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

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

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

Проблема: Почему данные в журнале обновляются только со второго раза?

Разберем типичную ситуацию. Разработчик создает процедуру в модуле объекта ПередЗаписью и пытается собрать данные из табличной части с помощью запроса. В запросе устанавливается параметр Ссылка, равный ссылке текущего документа. Проанализируем, что происходит в этот момент внутри транзакции 1С:

  1. Пользователь нажимает кнопку «Записать».
  2. Стартует транзакции записи.
  3. Вызывается событие ПередЗаписью в модуле объекта.
  4. В этот момент в базе данных еще лежат старые данные документа, так как физическая запись в таблицы БД произойдет позже.
  5. Запрос, обращаясь к &Ссылка, читает данные из таблиц БД, то есть получает состояние документа до текущих изменений.
  6. Реквизит шапки заполняется старыми данными.
  7. Происходит физическая запись объекта в БД.

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

Решение 1: Обработка коллекции в оперативной памяти

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

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


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

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

Решение 2: Использование запроса с передачей Таблицы Значений

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

Разберем по шагам, как это реализовать:

  1. Выгружаем табличную часть объекта в обычную таблицу значений.
  2. Создаем запрос и передаем эту таблицу в качестве параметра.
  3. В тексте запроса описываем структуру таблицы, чтобы 1С могла работать с ней как с временной таблицей.

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

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

Почему нельзя использовать процедуру ПриЗаписи в модуле формы?

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

Если вы в этот момент меняете реквизит объекта через код формы, платформа выставляет флаг Модифицированность для формы. С точки зрения программы, данные в базе уже сохранены, а в форме появились «новые» изменения, которые еще не записаны. Поэтому при закрытии документа 1С заботливо спрашивает пользователя: «Сохранить изменения?», создавая бесконечный цикл вопросов.

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

Оптимизация для современных версий платформы (8.3.10+)

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


// Пример быстрой сборки строки цен
МассивЦен = СоставПоставки.ВыгрузитьКолонку("Цена");

// Удалим дубли и нули через соответствие или временную ТЗ
Таб = Новый ТаблицаЗначений;
Таб.Колонки.Добавить("Цена");
Таб.ЗагрузитьКолонку(МассивЦен, "Цена");
Таб.Свернуть("Цена");

// Преобразуем обратно в массив для СтрСоединить
АктуальныеЦены = Таб.ВыгрузитьКолонку("Цена");

// Удаляем пустые/нулевые значения, если нужно
Индекс = АктуальныеЦены.Количество() - 1;
Пока Индекс >= 0 Цикл
    Если АктуальныеЦены[Индекс] <= 0 Тогда
        АктуальныеЦены.Удалить(Индекс);
    КонецЕсли;
    Индекс = Индекс - 1;
КонецЦикла;

// Изящный финал
ЭтотОбъект.Цены = СтрСоединить(АктуальныеЦены, "; ");

Метод СтрСоединить работает на уровне платформы гораздо эффективнее, чем сложение строк через +, особенно если строк в документе сотни или тысячи. Это позволяет избежать фрагментации памяти при работе с большими строками.

Резюме

Подведем итоги нашего анализа. Чтобы реквизит «Цены» в журнале всегда был актуальным и записывался без лишних вопросов от системы, следуйте этим рекомендациям:

  1. Всегда используйте процедуру ПередЗаписью именно в модуле объекта, а не в модуле формы.
  2. Никогда не делайте запросы к табличным частям «самих себя» по ссылке (ГДЕ Ссылка = &Ссылка) внутри транзакции записи. Обращайтесь к коллекциям строк напрямую: ЭтотОбъект.ИмяТабличнойЧасти.
  3. Для простых расчетов используйте циклы Для Каждого по строкам табличной части.
  4. Если используете 1С 8.3.10 и выше, применяйте функцию СтрСоединить для формирования итоговых строк.
  5. Помните, что реквизит шапки, дублирующий данные табличной части, — это избыточность. Если база данных очень большая, рассмотрите альтернативу в виде динамических списков с произвольным запросом, которые могут собирать такую строку «на лету» только при просмотре журнала, не увеличивая объем хранимых данных.
← На главную