Как программно создать записи в регистре накопления на основании другого регистра

Программист 1С v8.3 (Обычные формы) Управленческий учет Торговля и дистрибуция
← На главную

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

Анализ типичных ошибок при работе с набором записей

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


Для каждого Стр Из ТабТЗ Цикл
    мНабор.Отбор.Регистратор.Установить(Стр.Регистратор);
    ЗапРег = мНабор.Добавить();
    // Заполнение полей...
    мНабор.Записать(Истина); // ОШИБКА: Запись происходит внутри цикла по строкам
КонецЦикла;

В чем здесь проблема? Метод Записать(Истина) при каждом вызове полностью замещает движения по данному регистратору. Если в одном документе (регистраторе) было 10 строк с разными товарами, то на каждой итерации цикла набор будет очищаться и записываться заново только с одной текущей строкой. В итоге в регистре останется только последняя обработанная строка. Если же логика записи построена иначе, может возникнуть дублирование. Выясним причину: набор записей всегда работает в контексте конкретного регистратора, и записывать его нужно один раз для всех строк одного документа.

Решение №1: Правильный обход выборки с группировкой по регистратору

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


Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ
|   Партии.Регистратор КАК Регистратор,
|   Партии.Период КАК Период,
|   Партии.Номенклатура КАК Номенклатура,
|   Партии.Количество КАК Количество
|ИЗ
|   РегистрНакопления.ПартииТоваровНаСкладах КАК Партии
|ГДЕ
|   Партии.Период МЕЖДУ &ДатаНач И &ДатаКон
|УПОРЯДОЧИТЬ ПО
|   Регистратор"; // Важно для работы метода СледующийПоЗначениюПоля

РезультатЗапроса = Запрос.Выполнить();
ВыборкаРегистратор = РезультатЗапроса.Выбрать();

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

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

Решение №2: Метод «фиктивного проведения» в транзакции

Иногда логика формирования движений в новом регистре настолько сложна (например, требуется расчет себестоимости, учет партий и остатков), что дублировать её в отдельной обработке нецелесообразно. В этом случае мы можем применить метод «фиктивного проведения». Разберем этот прием по шагам:

  1. Открываем транзакцию.
  2. Вызываем штатный метод документа Записать(РежимЗаписиДокумента.Проведение). При этом срабатывают все встроенные алгоритмы конфигурации, и движения формируются в оперативной памяти и временных таблицах БД.
  3. Читаем сформированные движения из нужных регистров документа.
  4. Копируем или модифицируем эти данные для нашего нового регистра.
  5. Отменяем транзакцию. Таким образом, состояние базы данных (статус документа, движения в типовых регистрах) остается прежним, а у нас на руках оказывается готовая таблица движений, которую мы можем записать в свой регистр.

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


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

Работа с моментом времени и расчетом остатков

Один из частых вопросов: как установить МоментВремени для записи регистра? Важно понимать, что МоментВремени — это не поле в таблице БД, а расчетный объект платформы 1С, состоящий из даты и ссылки на документ. Вы не можете записать его напрямую.

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

Оптимизация и безопасность при массовой записи

Проанализируем несколько советов, которые помогут сделать работу обработки стабильной:

1. Использование Таблицы Значений. Метод НаборЗаписей.Загрузить(ТаблицаЗначений) работает значительно быстрее, чем добавление строк в цикле через Добавить(). Старайтесь сначала подготовить данные в ТЗ, а потом одним махом загружать их в набор.

2. Управление блокировками. При обработке данных за 2 года вы наверняка столкнетесь с блокировками. Разделите работу на порции (например, по месяцам) и фиксируйте транзакции чаще. Это позволит другим пользователям работать с системой параллельно с вашей обработкой.

3. Риск очистки движений. Помните, что если вы привязали записи к стандартному документу (например, РеализацияТоваровУслуг), то при следующем ручном перепроведении этого документа пользователем 1С автоматически очистит все наборы записей по нему, включая ваши программно созданные движения в новом регистре. Чтобы этого избежать, нужно либо добавить ваш регистр в список Движения документа в конфигураторе и прописать логику в модуле объекта, либо использовать технический документ-регистратор для хранения ваших записей.

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

← На главную