Как реализовать универсальную функцию корректировки движений регистров накопления в 1С?

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

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

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

Постановка задачи и анализ требований

Нам необходимо реализовать универсальную функцию со следующим интерфейсом:


Функция СкорректироватьЗаписи(ИмяРегистра, ИсходныеДвижения, ЦелевыеДвижения) Экспорт

Параметры функции:

  1. ИмяРегистра (тип Строка) — имя регистра накопления, движения которого мы корректируем.
  2. ИсходныеДвижения (тип ТаблицаЗначений) — фактически существующие в системе движения (например, то, что уже записано в БД).
  3. ЦелевыеДвижения (тип ТаблицаЗначений) — то состояние движений, которое мы хотим получить в итоге (эталон).

Функция должна вернуть новую ТаблицуЗначений, при записи которой в регистр совместно с исходными движениями мы получим целевой результат. Важное условие: исходные движения не должны удаляться. Математически это выражается простой формулой: Коррекция = ЦелевыеДвижения - ИсходныеДвижения.

Выбор оптимального подхода: Запрос или Таблица значений?

При решении этой задачи разработчики обычно делятся на два лагеря:

  1. Сторонники использования СУБД (запросы через временные таблицы).
  2. Сторонники обработки данных в оперативной памяти (методы коллекции ТаблицаЗначений).

Проанализируем обе концепции подробнее.

Подход с использованием запроса. Схема выглядит так: обе таблицы значений передаются в качестве параметров в запрос, индексируются, объединяются через ОБЪЕДИНИТЬ ВСЕ (при этом ресурсы исходной таблицы умножаются на -1), после чего результат группируется по измерениям регистра. Несмотря на то что этот метод кажется "стандартным" для 1С, он имеет огромные накладные расходы. Передача таблиц значений из оперативной памяти сервера приложений на сервер СУБД (сериализация/десериализация), создание временных таблиц в базе данных и обратное получение результата ради простых математических операций — крайне нерациональное использование ресурсов сети и СУБД.

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

Поэтому в качестве целевого мы выбираем метод обработки таблиц в памяти.

Скрытые подводные камни, о которых часто забывают

Если написать простую свертку таблиц без учета особенностей платформы 1С, решение не пройдет проверку в реальной базе данных. Рассмотрим ключевые нюансы:

1. Системное поле "Вид движения" для регистров остатков.
Регистры накопления бывают двух видов: Остатки и Обороты. У регистра остатков есть обязательное системное поле ВидДвижения (принимает значения ВидДвиженияРегистраНакопления.Приход или ВидДвиженияРегистраНакопления.Расход). Это поле не входит в стандартную коллекцию метаданных Измерения, однако без него невозможно корректно свернуть и записать движения. Если мы не добавим это поле в массив группировки при свертке, платформа либо очистит его, либо не сможет корректно сопоставить приходы и расходы.

2. Очистка строк с нулевыми ресурсами.
После свертки (вычитания) многие строки могут обнулиться (например, если исходное количество было 10, и целевое тоже 10, разница составит 0). Такие строки нужно обязательно удалять из результирующей таблицы, чтобы не засорять базу данных пустыми записями. Однако удалять строку можно только тогда, когда все ресурсы в этой строке равны нулю. Если у нас изменилась только стоимость товара, а количество осталось прежним, то ресурс Количество будет равен 0, а ресурс Сумма будет отличным от нуля. Такую строку удалять нельзя!

3. Правильный обход коллекции при удалении строк.
Классическая ошибка начинающих программистов — удаление строк в цикле Для Каждого от начала к концу. При удалении строки индексы последующих элементов смещаются, из-за чего часть строк пропускается, а в конце цикла возникает ошибка выхода за границы массива. Чтобы избежать этого, обход таблицы для удаления пустых строк необходимо выполнять с конца по индексу.

Пошаговый алгоритм реализации универсальной функции

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

  1. Получаем объект метаданных регистра по его строковому имени.
  2. Формируем список измерений регистра. Если регистр имеет тип "Остатки", добавляем в список измерений системное поле ВидДвижения.
  3. Формируем список ресурсов регистра.
  4. Создаем пустую результирующую таблицу значений на основе структуры нашего регистра (для этого выгружаем колонки пустого набора записей).
  5. Копируем целевые движения в результирующую таблицу.
  6. Копируем исходные движения в результирующую таблицу, при этом принудительно умножаем значения всех числовых ресурсов на -1.
  7. Сворачиваем полученную таблицу: полями группировки выступают измерения (и вид движения), полями суммирования — ресурсы.
  8. В обратном цикле обходим результирующую таблицу и удаляем строки, у которых все ресурсы равны нулю.

Проанализируем программный код, реализующий этот алгоритм:


&НаСервере
Функция СкорректироватьЗаписи(ИмяРегистра, ИсходныеДвижения, ЦелевыеДвижения) Экспорт

    // Шаг 1. Получаем метаданные регистра и проверяем их наличие
    МетаданныеРегистра = Метаданные.РегистрыНакопления.Найти(ИмяРегистра);
    Если МетаданныеРегистра = Неопределено Тогда
        ВызватьИсключение "Регистр накопления '" + ИмяРегистра + "' не найден в метаданных конфигурации!";
    КонецЕсли;

    // Шаг 2. Собираем массив измерений регистра
    МассивИзмерений = Новый Массив;
    Для Каждого Измерение Из МетаданныеРегистра.Измерения Цикл
        МассивИзмерений.Добавить(Измерение.Имя);
    КонецЦикла;

    // Особое внимание регистру остатков — добавляем системное поле
    Если МетаданныеРегистра.ВидРегистра = ОбъектыМетаданных.ВидРегистраНакопления.Остатки Тогда
        МассивИзмерений.Добавить("ВидДвижения");
    КонецЕсли;

    // Шаг 3. Собираем массив ресурсов регистра
    МассивРесурсов = Новый Массив;
    Для Каждого Ресурс Из МетаданныеРегистра.Ресурсы Цикл
        МассивРесурсов.Добавить(Ресурс.Имя);
    КонецЦикла;

    // Преобразуем массивы в строки для использования в методе Свернуть()
    СтрокаИзмерений = СтрСоединить(МассивИзмерений, ", ");
    СтрокаРесурсов  = СтрСоединить(МассивРесурсов, ", ");

    // Шаг 4. Создаем эталонную таблицу на основе структуры регистра
    // Выгрузка колонок из набора записей гарантирует точное соответствие типов данных
    НаборЗаписей = РегистрыНакопления[ИмяРегистра].СоздатьНаборЗаписей();
    ТаблицаРезультат = НаборЗаписей.ВыгрузитьКолонки();

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

    // Шаг 6. Загружаем исходные движения с инверсией числовых ресурсов
    Для Каждого СтрокаИсходная Из ИсходныеДвижения Цикл
        НоваяСтрока = ТаблицаРезультат.Добавить();
        ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаИсходная);
        
        Для Каждого ИмяРесурса Из МассивРесурсов Цикл
            // Проверяем тип, чтобы избежать ошибок при случайных нечисловых значениях
            Если ТипЗнч(СтрокаИсходная[ИмяРесурса]) = Тип("Число") Тогда
                НоваяСтрока[ИмяРесурса] = -СтрокаИсходная[ИмяРесурса];
            КонецЕсли;
        КонецЦикла;
    КонецЦикла;

    // Шаг 7. Выполняем группировку и суммирование в памяти
    ТаблицаРезультат.Свернуть(СтрокаИзмерений, СтрокаРесурсов);

    // Шаг 8. Очищаем строки, где корректировка не требуется (все ресурсы равны 0)
    // Используем обратный цикл для безопасного удаления элементов из коллекции
    Индекс = ТаблицаРезультат.Количество() - 1;
    Пока Индекс >= 0 Цикл
        ТекущаяСтрока = ТаблицаРезультат[Индекс];
        НулеваяСтрока = Истина;
        
        Для Каждого ИмяРесурса Из МассивРесурсов Цикл
            Если ТекущаяСтрока[ИмяРесурса] <> 0 Тогда
                НулеваяСтрока = Ложь;
                Прервать; // Если хотя бы один ресурс не равен 0, строку оставляем
            КонецЕсли;
        КонецЦикла;
        
        Если НулеваяСтрока Тогда
            ТаблицаРезультат.Удалить(Индекс);
        КонецЕсли;
        
        Индекс = Индекс - 1;
    КонецЦикла;

    Возврат ТаблицаРезультат;

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

Пример работы алгоритма

Проанализируем работу функции на примере, предложенном автором темы. У нас есть регистр остатков "ОстаткиНоменклатуры" с измерениями Номенклатура (тип СправочникСсылка) и ресурсами Количество (число), Сумма (число).

Исходные движения (Приходная №2):

Целевые движения (Приходная №1):

После объединения таблиц и инвертирования ресурсов исходных движений наша таблица перед сверткой примет следующий вид:

  1. Kohinor: Количество = 10, Сумма = 70 (Целевое движение)
  2. Паркер: Количество = 10, Сумма = 1000 (Целевое движение)
  3. Kohinor: Количество = -60, Сумма = -390 (Инвертированное исходное движение)
  4. Паркер: Количество = -40, Сумма = -3800 (Инвертированное исходное движение)

После вызова метода Свернуть() платформа сгруппирует строки по номенклатуре и сложит ресурсы. На выходе мы получим точный математический результат корректировки:

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

Создание интерфейса для тестирования решения

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

На форму обработки добавим следующие реквизиты:

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


&НаКлиенте
Процедура ИмяРегистраПриИзменении(Элемент)
    ПерестроитьТаблицыНаСервере();
КонецПроцедуры

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

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

Заключение

Мы детально разобрали алгоритм создания универсального механизма корректировки движений. Выбранный нами подход с обработкой таблиц значений в оперативной памяти сервера приложений является наиболее эффективным, так как минимизирует нагрузку на СУБД.

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

← На главную