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

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

В практике разработки на платформе 1С:Предприятие 8.3 часто возникает классическая задача: из одного головного (родительского) документа генерируется цепочка подчиненных документов по строкам табличной части. Например, из документа ИндивидуальныйЗаказ создаются отдельные документы КонкретноеИзделиеВЗаказе для каждой строки спецификации. После того как в подчиненных документах рассчитываются или вводятся вручную итоговые суммы, их необходимо вернуть в исходную табличную часть родительского документа.

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

---

Вариант 1. Ручное обновление сумм по кнопке (Простой способ)

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

Выполним предварительную настройку метаданных в конфигураторе:

  1. Откроем документ ИндивидуальныйЗаказ и перейдем на вкладку Данные.
  2. В табличной части Заказ добавим новый реквизит СсылкаНаКонкретноеИзделие с типом ссылки на документ ДокументСсылка.КонкретноеИзделиеВЗаказе.
  3. В этой же табличной части убедимся в наличии числового реквизита Сумма.

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


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

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

Рассмотрим код обработчика этой команды:


&НаКлиенте
Процедура ОбновитьСуммыИзделий(Команда)
    ОбновитьСуммыНаСервере();
    ПоказатьОповещениеПользователя("Успешно", , "Суммы по изделиям обновлены!");
КонецПроцедуры

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

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

Недостатки: Требует ручного нажатия кнопки. Если в подчиненном документе изменится сумма, пользователь не узнает об этом, пока сам не обновит данные в родительском документе.

---

Вариант 2. Интерактивное обновление через механизм Оповещений (Клиент-клиент)

Что делать, если у пользователя на экране одновременно открыты и родительский заказ, и подчиненное изделие? Если подчиненный документ запишет новую сумму напрямую в базу данных "в спину" открытому родительскому документу, то при попытке пользователя сохранить родительский документ система выдаст ошибку: "Редактируемые данные были изменены в другой сессии или другим пользователем".

Чтобы избежать этой коллизии, мы применим паттерн клиентского взаимодействия с использованием глобального метода Оповестить().

Проанализируем шаги реализации:

  1. В подчиненном документе КонкретноеИзделиеВЗаказе должен быть реквизит шапки ДокументОснование с типом ДокументСсылка.ИндивидуальныйЗаказ.
  2. В модуле формы подчиненного документа переопределим обработчик события ПослеЗаписи на клиенте:

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

Теперь настроим прием этого сигнала в форме родительского документа ИндивидуальныйЗаказ. Для этого в свойствах формы определим обработчик события ОбработкаОповещения:


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

Преимущества: Идеально для совместной работы на одном компьютере (или в веб-клиенте), мгновенное визуальное обновление данных на глазах у пользователя, полное отсутствие блокировок СУБД.

Недостатки: Метод работает только тогда, когда форма родительского документа физически открыта у пользователя на экране в момент записи подчиненного документа.

---

Вариант 3. Автоматическое обновление на сервере через Подписки на события

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

Разберем по шагам создание подписки:

  1. В ветке конфигурации перейдем в Общие -> Подписки на события и добавим новую подписку.
  2. Назовем её ОбновитьСуммуВЗаказеПриЗаписиИзделия.
  3. В поле Источник выберем объект подчиненного документа: ДокументОбъект.КонкретноеИзделиеВЗаказе.
  4. В поле Событие выберем ПриЗаписи.
  5. В поле Обработчик укажем имя процедуры в общем модуле. Обратите внимание, что общий модуль должен быть серверным (флаг Сервер) и желательно неконтекстным.

Напишем код обработчика в общем модуле:


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

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

Недостатки: В многопользовательской базе при высокой интенсивности записи документов этот метод может приводить к взаимным транзакционным блокировкам (Deadlocks).

---

Вариант 4. Асинхронное обновление через Регистр Сведений (Архитектурный стандарт для высоких нагрузок)

Если в системе работают сотни пользователей одновременно, то прямое изменение родительского документа из подчиненного (как в Варианте 3) приведет к катастрофическому падению производительности из-за блокировок СУБД. В крупных ERP-системах для решения этой задачи применяется асинхронный паттерн "Очередь обновлений".

Проанализируем архитектуру этого решения:

  1. Создадим независимый, непериодический регистр сведений ОчередьОбновленияЗаказов.
  2. Добавим одно измерение: ИндивидуальныйЗаказ (тип ДокументСсылка.ИндивидуальныйЗаказ).
  3. Добавим один ресурс: ТребуетОбновления (тип Булево).

Теперь в подписке на событие подчиненного документа (или прямо в его модуле объекта в процедуре ПриЗаписи) вместо изменения родительского документа мы будем делать сверхбыструю запись в этот регистр. Запись в независимый регистр сведений по одной строке происходит мгновенно и не накладывает блокировок на тяжелые документы.


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

Далее мы настраиваем Регламентное задание в конфигураторе, которое будет запускаться в фоновом режиме на сервере с периодичностью, например, каждые 5 минут. Это регламентное задание будет вызывать процедуру общего модуля для обработки очереди.

Посмотрим на пример кода регламентного задания:


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

Преимущества: Максимальная производительность и стабильность базы данных. Блокировки сведены к минимуму. Если родительский документ занят пользователем, система не упадет с ошибкой у того, кто записывает изделие, а просто отложит обновление на несколько минут.

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

---

Резюме: какой метод выбрать?

Мы проанализировали четыре кардинально разных способа синхронизации сумм между подчиненными и родительскими документами в системе 1С:

← На главную