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