Как программно сторнировать несколько документов в одной «Операции»

Программист 1С v8.3 (Управляемые формы) 1C:Бухгалтерия Бухгалтерский учет
← На главную

При работе в конфигурации «1С:Бухгалтерия» часто возникает задача автоматизировать сторнирование не одного, а целого пакета документов. Стандартная процедура 1С рассчитана на обработку одного документа-основания, и при попытке вызвать её в цикле программисты сталкиваются с тем, что движения каждого последующего документа затирают движения предыдущего. В этой статье мы подробно разберем, почему так происходит, и как правильно реализовать накопление сторнирующих записей в одном документе ОперацияБух.

Почему типовой механизм не подходит для массового сторно

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

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

Алгоритм реализации массового сторнирования

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

  1. Подготовить массив или таблицу документов, которые подлежат сторнированию.
  2. Создать пустую таблицу значений со структурой, идентичной структуре регистра бухгалтерии (или использовать существующий набор записей текущей операции).
  3. В цикле по документам-основаниям считывать их движения во временные наборы записей.
  4. Выгружать данные из временных наборов в общую таблицу значений.
  5. Выполнить инверсию знаков для всех ресурсов (умножение на -1).
  6. Загрузить итоговую таблицу в набор записей текущего документа и записать его.

Рассмотрим подробнее реализацию этого алгоритма на языке 1С.

Пример кода для объединения движений в таблицу значений

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


// Предположим, СписокДокументов - это массив ссылок на документы для сторно
// Объект - это текущий документ "ОперацияБух"

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

После того как мы собрали все проводки в одну таблицу ТЗ_Движений, нам нужно изменить знаки сумм. Важно помнить, что в регистре бухгалтерии могут быть не только Сумма, но и СуммаВалютная, СуммаНУ, СуммаПР, СуммаВР, а также Количество. Проанализируем, как это сделать программно.

Инверсия ресурсов регистра бухгалтерии

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


МетаданныеРегистра = Метаданные.РегистрыБухгалтерии.Хозрасчетный;

Для Каждого Строка Из ТЗ_Движений Цикл
    // Сбрасываем регистратор, так как теперь регистратором будет наша "Операция"
    Строка.Регистратор = Объект.Ссылка;
    Строка.Период = Объект.Дата;
    
    Для Каждого Ресурс Из МетаданныеРегистра.Ресурсы Цикл
        // Проверяем, является ли ресурс числовым
        Если Строка[Ресурс.Имя] <> 0 Тогда
            Строка[Ресурс.Имя] = -Строка[Ресурс.Имя];
        КонецЕсли;
    КонецЦикла;
КонецЦикла;

// Теперь загружаем результат в движения нашей операции
НаборИтоговый = РегистрыБухгалтерии.Хозрасчетный.СоздатьНаборЗаписей();
НаборИтоговый.Отбор.Регистратор.Установить(Объект.Ссылка);
НаборИтоговый.Загрузить(ТЗ_Движений);
НаборИтоговый.Записать();

Особенности работы с методом Добавить()

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

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


НаборИтоговый = РегистрыБухгалтерии.Хозрасчетный.СоздатьНаборЗаписей();
НаборИтоговый.Отбор.Регистратор.Установить(Объект.Ссылка);

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

НаборИтоговый.Записать();

Работа с субконто в доработанных конфигурациях

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

Однако, стоит обратить внимание на Активность записей. Если исходный документ был сторнирован ранее или помечен на удаление, его движения могут быть неактивны. Рекомендуется проверять свойство Активность перед тем, как добавлять запись в сторно, чтобы не создавать «сторно от сторна», которое может запутать учет.

Рекомендации по производительности

Рассмотрим ситуацию, когда количество сторнируемых документов исчисляется сотнями. В этом случае:

  1. Не обновляйте форму в цикле. Если вы выполняете сторнирование из формы документа «Операция», делайте это на сервере и только после завершения всех расчетов обновляйте данные на клиенте.
  2. Используйте привилегированный режим. Чтение движений чужих документов может потребовать дополнительных прав доступа. Если код выполняется в рамках регламентной задачи или специальной обработки, установите УстановитьПривилегированныйРежим(Истина).
  3. Контроль объема данных. Помните, что документ «Операция» имеет ограничение на визуальное отображение строк. Если проводок станет больше нескольких тысяч, форма документа может начать работать очень медленно. В таких случаях лучше разбивать сторно на несколько разных операций.

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

← На главную