Копия документа из кода 1С: Скопировать(), ручной перенос реквизитов и движения

· 9 мин

Содержание

Чтобы скопировать документ программно в 1С, получите объект документа и вызовите у него Скопировать(): НовыйДок = Ссылка.ПолучитьОбъект().Скопировать(). Затем задайте дату, установите новый номер и запишите копию через Записать(). Реквизиты и табличные части перенесутся сами, движения — нет.

Это работает на сервере в любой конфигурации на платформе 8.3, потому что метод платформенный. Ниже код для трёх случаев: полная копия, копия с выборочным переносом полей и копия с движениями. Отдельно — как открыть копию в форме и как не уронить базу при копировании тысяч документов.

Какие есть способы получить копию документа

Способов три, и они не взаимозаменяемы. Выбор зависит от того, нужна ли точная копия или документ «по мотивам» исходного.

СпособКогда подходитЧто учесть
ДокументОбъект.Скопировать()Нужна копия того же вида документа со всеми даннымиСрабатывает обработчик ПриКопировании в модуле объекта
СоздатьДокумент() + ЗаполнитьЗначенияСвойств()Часть полей переносить не нужно или вид документа другойТабличные части переносите отдельно
Параметр формы ЗначениеКопированияПользователь должен увидеть копию и сам решить, записывать ли еёКопия живёт в форме до записи

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

Отдельного метода ЗаписатьНовый() у объекта документа нет. Новый объект пишется тем же Записать(), а проверить, что он ещё не записан, можно через ЭтоНовый().

Все три способа требуют серверного контекста. На клиенте нет ДокументОбъект, есть только данные формы. Поэтому код копирования кладите в общий модуль с флагом «Сервер», в модуль менеджера или в серверную процедуру формы.

Копирование документа методом Скопировать(): пошаговый алгоритм

Метод вызывается у объекта, а не у ссылки. Вызов Ссылка.Скопировать() упадёт с ошибкой «Метод объекта не обнаружен ».

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

Готовая функция для общего модуля:

// Возвращает ссылку на записанную копию документа.
// Проводить - Булево: провести копию сразу после записи.
Функция СкопироватьДокумент(ДокументСсылка, Проводить = Ложь) Экспорт

    Источник = ДокументСсылка.ПолучитьОбъект();
    Копия = Источник.Скопировать();

    Копия.Дата = ТекущаяДатаСеанса();
    Копия.Номер = "";
    Копия.УстановитьНовыйНомер();
    Копия.Комментарий = СтрШаблон("Копия документа %1 от %2",
        Источник.Номер, Формат(Источник.Дата, "ДФ=dd.MM.yyyy"));

    Если Проводить Тогда
        Копия.Записать(РежимЗаписиДокумента.Проведение);
    Иначе
        Копия.Записать(РежимЗаписиДокумента.Запись);
    КонецЕсли;

    Возврат Копия.Ссылка;

КонецФункции

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

Обработчик ПриКопировании в модуле объекта

Скопировать() вызывает в модуле объекта копии событие ПриКопировании(ОбъектКопирования). То же событие срабатывает при интерактивном копировании кнопкой в списке. Это правильное место для правил «что не копировать»:

Процедура ПриКопировании(ОбъектКопирования)

    ДокументОснование = Неопределено;
    Ответственный = Пользователи.ТекущийПользователь();

    Для Каждого Строка Из Товары Цикл
        Строка.КоличествоОтгружено = 0;
    КонецЦикла;

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

Код условный: Пользователи.ТекущийПользователь() — функция БСП, реквизиты табличной части у вас свои. Логика простая: всё, что пишется в ПриКопировании, применится и к программной, и к ручной копии. Не дублируйте эти правила в коде вызова.

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

Перенос реквизитов и табличных частей вручную

Ручной перенос нужен в двух случаях. Первый — копия в документ другого вида, например из «Заказа клиента» в «Счёт на оплату». Второй — когда ПриКопировании делает лишнее, а трогать его нельзя.

Шапку переносят функцией ЗаполнитьЗначенияСвойств. Четвёртый параметр — список свойств, которые нужно исключить:

Функция КопияВручную(ДокументСсылка) Экспорт

    Источник = ДокументСсылка.ПолучитьОбъект();
    Копия = Документы.ЗаказПоставщику.СоздатьДокумент();

    ЗаполнитьЗначенияСвойств(Копия, Источник, ,
        "Номер, Дата, Проведен, ПометкаУдаления, ДокументОснование");

    Копия.Дата = ТекущаяДатаСеанса();
    Копия.УстановитьНовыйНомер();

    // Табличные части: целиком, через таблицу значений
    Копия.Товары.Загрузить(Источник.Товары.Выгрузить());
    Копия.Услуги.Загрузить(Источник.Услуги.Выгрузить());

    Копия.Записать(РежимЗаписиДокумента.Запись);
    Возврат Копия.Ссылка;

КонецФункции

Если вид документа другой, колонки табличных частей не совпадут. Тогда переносите построчно:

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

ЗаполнитьЗначенияСвойств переносит только совпадающие по имени поля. Поле с другим именем в приёмнике останется пустым, ошибки не будет. Это частая причина «потерянной» суммы или ставки НДС: проверьте имена колонок в обоих документах.

Что делать со связанными табличными частями

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

Связь рвётся, если строки переносить построчно с пересчётом ключа. В таком случае пересчитывайте ключ сразу в обеих табличных частях, по одному соответствию «старый ключ → новый ключ».

Движения документа при копировании

Ни Скопировать(), ни ЗаполнитьЗначенияСвойств движения не переносят. Движения принадлежат регистратору, а у копии регистратор другой.

Есть два пути.

  • ✅ Провести копию. Записать(РежимЗаписиДокумента.Проведение) вызовет ОбработкаПроведения, и движения сформируются по правилам документа. Это правильный путь почти всегда.
  • 🔧 Перенести движения как есть. Нужно для документов с ручными движениями, например «Операция» в бухгалтерии, или если алгоритм проведения изменился, а нужна точная копия старых записей.

Код для второго случая. Сначала документ записывается, чтобы у него появилась ссылка, затем копируется каждый набор записей:

Процедура СкопироватьДвижения(Источник, Копия)

    Для Каждого НаборИсточника Из Источник.Движения Цикл

        ИмяРегистра = НаборИсточника.Метаданные().Имя;
        НаборИсточника.Прочитать();
        Если НаборИсточника.Количество() = 0 Тогда
            Продолжить;
        КонецЕсли;

        НаборКопии = Копия.Движения[ИмяРегистра];
        НаборКопии.Отбор.Регистратор.Установить(Копия.Ссылка);

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

        НаборКопии.Записать();

    КонецЦикла;

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

Вызывайте процедуру после Копия.Записать(РежимЗаписиДокумента.Запись) и внутри одной транзакции с записью документа.

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

Отдельная опасность — копия, проведённая в рабочей базе. Она задваивает остатки и обороты так же, как реальный документ. Если копия создавалась для теста, после проверки отмените её проведение и пометьте на удаление.

Как перенести движения в копию?

Копия документа в управляемой форме

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

&НаКлиенте
Процедура СкопироватьВФорме(Команда)

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

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

Форма откроется с новым документом. Платформа сама вызовет Скопировать() у исходного объекта, а значит отработает и ПриКопировании. Номер и ссылки в форме не будет, пока пользователь не запишет документ.

В ПриСозданииНаСервере формы признак копирования проверяется так:

Если ЗначениеЗаполнено(Параметры.ЗначениеКопирования) Тогда
    // форма открыта как копия: дозаполняем поля
КонецЕсли;

Если копию нужно записать без открытия формы, например кнопкой в списке, вызывайте сервер. Клиентская процедура передаёт только ссылки:

&НаКлиенте
Процедура СоздатьКопии(Команда)
    Ссылки = Элементы.Список.ВыделенныеСтроки;
    СоздатьКопииНаСервере(Ссылки);
    Элементы.Список.Обновить();
КонецПроцедуры

&НаСервереБезКонтекста
Процедура СоздатьКопииНаСервере(Ссылки)
    Для Каждого Ссылка Из Ссылки Цикл
        КопированиеДокументов.СкопироватьДокумент(Ссылка);
    КонецЦикла;
КонецПроцедуры

Здесь КопированиеДокументов — имя вашего общего модуля с функцией из первого раздела. Серверный вызов без контекста не тянет на сервер данные формы и работает быстрее.

В динамическом списке с группировками ВыделенныеСтроки может вернуть не только ссылки. Перед копированием проверьте тип: ТипЗнч(Ссылка) = Тип("ДокументСсылка.РеализацияТоваровУслуг").

Ошибки при копировании документов кодом

Диагностика: выберите симптом

Ещё одна неочевидная ошибка — копирование с ОбменДанными.Загрузка = Истина. В этом режиме не срабатывают ПередЗаписью, ПриЗаписи и проверка заполнения. Копия запишется быстро, но без служебных реквизитов, которые типовой код заполняет в этих обработчиках. Проведение в режиме загрузки тоже не выполняется.

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

Если копирование вызывается из регламентного задания, учтите права. Задание работает от имени пользователя, указанного в его настройках. Без прав на добавление документа запись упадёт с ошибкой нарушения прав доступа.

Массовое копирование: как не перегрузить базу

Копирование сотен документов в цикле упирается в три вещи: число транзакций, проведение и чтение объектов.

  • 📦 Транзакции пачками. Одна транзакция на каждый документ — медленно, одна на десять тысяч — долгие блокировки. Рабочая середина: 50–200 документов в одной транзакции.
  • ⏳ Проведение отдельно. Сначала запишите копии без проведения, затем проведите их вторым проходом в хронологическом порядке. Так проще найти документ, на котором упало проведение.
  • 🔍 Меньше ПолучитьОбъект(). Если реквизиты нужны для фильтрации, получите их одним запросом, а объект открывайте только у тех документов, которые действительно копируете.
  • 🧵 Фоновое задание. Долгую операцию запускайте через ФоновыеЗадания.Выполнить(), чтобы пользователь не ждал с зависшим окном.

Каркас пачечной записи:

РазмерПачки = 100;
Счетчик = 0;
НачатьТранзакцию();
Попытка
    Для Каждого Ссылка Из СписокСсылок Цикл
        Копия = Ссылка.ПолучитьОбъект().Скопировать();
        Копия.Номер = "";
        Копия.УстановитьНовыйНомер();
        Копия.Записать(РежимЗаписиДокумента.Запись);
        Счетчик = Счетчик + 1;
        Если Счетчик % РазмерПачки = 0 Тогда
            ЗафиксироватьТранзакцию();
            НачатьТранзакцию();
        КонецЕсли;
    КонецЦикла;
    ЗафиксироватьТранзакцию();
Исключение
    ОтменитьТранзакцию();
    ВызватьИсключение;
КонецПопытки;

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

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

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

Частые вопросы

Копируются ли присоединённые файлы при Скопировать()?
Нет. Файлы хранятся в отдельном справочнике, связанном со ссылкой документа. Их нужно копировать отдельно, создавая новые элементы справочника с владельцем-копией.
Можно ли скопировать документ в другую базу кодом?
Скопировать() работает только внутри одной базы. Для переноса между базами используют обмен данными, выгрузку в XML или универсальную обработку переноса.
Сохраняется ли дата исходного документа?
Да, если её не перезаписать. Задайте Дата явно, иначе копия встанет в ту же позицию в хронологии, что и оригинал.
Как в ПередЗаписью понять, что пишется программная копия?
Перед записью положите метку в дополнительные свойства: Копия.ДополнительныеСвойства.Вставить("ЭтоКопия", Истина). В обработчиках модуля объекта проверьте её через ДополнительныеСвойства.Свойство(). Метка живёт только в памяти и в базу не пишется.
Как скопировать документ через расширение, не меняя типовой модуль?
Создайте в расширении общий модуль с функцией копирования и команду в форме списка. ПриКопировании при необходимости дополните через аннотацию &После в модуле объекта расширения.