При работе с данными в 1С, особенно с табличными частями документов или отчетов, часто возникает задача по обработке повторяющихся элементов. Типичная ситуация – нам необходимо удалить дубликаты записей, но при этом сохранить и агрегировать связанные с ними числовые значения, например, суммировать их. Исходный подход, который нередко пытаются применить начинающие разработчики, заключается в последовательном добавлении различных по смыслу данных (например, поставщика и суммы) в плоский массив и попытке удалить их в цикле, что приводит к ошибкам типа "индекс вне диапазона" и некорректной логике обработки. Давайте разберем, как правильно и эффективно решить эту задачу.
Основная проблема в начальном подходе заключалась в следующем:
Массив.Добавить(Строка.Поставщик); Массив.Добавить(Строка.Сумма);. Это создавало плоский массив, где соседние элементы не были однозначно связаны, например, первый элемент — поставщик, второй — сумма, третий — снова поставщик и так далее. При удалении одного элемента нарушалась вся эта последовательность, и невозможно было корректно найти "сумму, относящуюся к поставщику". Для связывания нескольких значений (поставщик и его сумма) необходимо использовать структуры данных, способные хранить пары "ключ-значение" или наборы реквизитов, такие как Структура, Соответствие или ТаблицаЗначений.В 1С существует несколько мощных и правильных инструментов для решения задачи группировки и суммирования элементов табличной части. Рассмотрим наиболее эффективные методы.
Этот метод является наиболее производительным и рекомендуемым для решения задач группировки и агрегации данных, подобных свертыванию по поставщику и суммированию. Он идеально подходит для работы с табличными данными.
Объект ТаблицаЗначений в 1С — это универсальный контейнер для табличных данных, который похож на таблицу базы данных или электронную таблицу. У него есть колонки и строки. Метод Свернуть() позволяет сгруппировать строки таблицы по значениям одной или нескольких колонок, а затем выполнить агрегирующие операции (суммирование, подсчет количества, поиск минимума или максимума) над значениями других колонок.
Давайте рассмотрим, как мы можем использовать метод Свернуть() для нашей задачи:
ТаблицуЗначений: Сначала нам необходимо получить данные из исходной табличной части (например, Объект.Товары) и выгрузить их в объект ТаблицаЗначений. Это можно сделать с помощью метода Выгрузить().ТаблицыЗначений: Затем вызываем метод Свернуть(). Мы указываем колонки, по которым будет происходить группировка (в нашем случае это "Поставщик"), и колонки, значения которых необходимо суммировать (в нашем случае это "Сумма").ТаблицаЗначений, содержащая уникальных поставщиков и их общие суммы. Эту таблицу мы можем использовать для очистки и повторного заполнения исходной табличной части или для заполнения новой табличной части в другой форме/объекте.Рассмотрим пример кода, который демонстрирует использование ТаблицыЗначений для свертывания данных. Этот код обычно выполняется на сервере, особенно если он работает с объектом базы данных, таким как документ.
Предположим, у нас есть документ, который содержит табличную часть с колонками "Поставщик" и "Сумма".
// Предположим, что этот код выполняется на сервере
// (например, в процедуре, вызываемой с клиента по кнопке)
Процедура ОбработатьТоварыНаСервере()
// 1. Получаем объект, если это документ
// Если код находится в модуле объекта документа, то 'ЭтотОбъект' уже доступен.
// Если код вызывается из формы и передается ссылка на объект, то:
// ДокументОбъект = Объект.ПолучитьОбъект();
// или если форма открывается с объектом, как в сообщении 1:
// Докум - это объект формы обработки, а не сам документ.
// Пусть у нас есть ссылка на обрабатываемый документ:
// ДокументСсылка = Параметры.СсылкаНаДокумент; // Пример передачи ссылки
// ДокументОбъект = ДокументСсылка.ПолучитьОбъект();
// Для примера используем Объект.Товары, предполагая, что это табличная часть текущего объекта
// Если это форма обработки, и мы работаем с ее табличной частью:
// ТаблицаДанныхОбработки = ЭтотОбъект.Товары;
// Если мы работаем с табличной частью документа, который открыл эту форму:
// Например, если 'Докум' это форма обработки, а 'Докум.Объект' это ее реквизит,
// в который уже помещен документ для обработки.
// В данном примере будем работать с табличной частью 'Объект.Товары'
// изначального документа, к которому относится кнопка.
// 2. Выгружаем табличную часть в ТаблицуЗначений
ТаблицаТоваров = Объект.Товары.Выгрузить(); // Выгружаем данные из табличной части
// 3. Создаем копию ТаблицыЗначений для свертывания
// Это не всегда обязательно, но может быть полезно, если вы хотите сохранить оригинальные данные
// и работать с модифицированной версией. В данном случае, можно свернуть и оригинал.
КопияТаблицыТоваров = ТаблицаТоваров.Скопировать();
// 4. Сворачиваем таблицу по Поставщику, суммируя Сумму
// Первый параметр - строки для группировки (ключи свертки)
// Второй параметр - строки для агрегации (ресурсы свертки)
КопияТаблицыТоваров.Свернуть("Поставщик", "Сумма");
// Если в исходной табличной части были другие колонки,
// но они не были указаны ни как ключи, ни как ресурсы свертки,
// они будут удалены из результирующей таблицы.
// Если нам нужны эти колонки, и они должны быть одинаковыми для каждой группы,
// их можно добавить в список ключей свертки.
// 5. Очищаем исходную табличную часть
Объект.Товары.Очистить();
// 6. Загружаем свернутые данные обратно в табличную часть
// Или в новую табличную часть обработки, как в исходной задаче
Для Каждого СтрокаСвернутая Из КопияТаблицыТоваров Цикл
НоваяСтрока = Объект.Товары.Добавить();
НоваяСтрока.Поставщик = СтрокаСвернутая.Поставщик;
НоваяСтрока.Сумма = СтрокаСвернутая.Сумма; // Предполагаем, что колонка называется "Сумма"
// Если в обработке колонка называется "СуммаОтПоставщика", то:
// НоваяСтрока.СуммаОтПоставщика = СтрокаСвернутая.Сумма;
КонецЦикла;
// 7. Записываем изменения в объект (если это документ)
// Объект.Записать();
// Если же нужно передать результат в форму обработки, как в исходной задаче
// то этот код должен быть частью процедуры обработки на сервере
// которая возвращает ТаблицуЗначений на клиент.
// Пример:
// Возврат КопияТаблицыТоваров;
КонецПроцедуры
// Пример клиентского кода, который вызывает серверную процедуру и обрабатывает результат
// Процедура Кнопка_Свернуть_НаКлиенте()
// // Вызываем серверную процедуру
// СвернутаяТаблица = ОбработатьТоварыНаСервере();
//
// // Теперь у нас есть СвернутаяТаблица на клиенте
// // Мы можем использовать ее для заполнения табличной части формы обработки
// // Сначала очищаем табличную часть формы обработки
// // Элементы.ТаблицаФормы.Очистить(); // Пример для ТЧ на форме
// // Затем загружаем новые данные
// // Для Каждого Строка Из СвернутаяТаблица Цикл
// // НоваяСтрокаФормы = Элементы.ТаблицаФормы.Добавить();
// // НоваяСтрокаФормы.Поставщик = Строка.Поставщик;
// // НоваяСтрокаФормы.СуммаОтПоставщика = Строка.Сумма;
// // КонецЦикла;
//
// Сообщить("Данные успешно свернуты и обновлены.");
// КонецПроцедуры
Метод Выгрузить() и Свернуть() объекта ТаблицаЗначений доступны как на клиенте, так и на сервере. Однако, если данные берутся из объекта базы данных (например, Объект.Товары документа), то операцию выгрузки и свертывания рекомендуется выполнять на сервере. Это минимизирует сетевой трафик (мы передаем только готовый результат) и обеспечивает корректный доступ к данным базы данных. Передача ТаблицаЗначений между клиентом и сервером осуществляется платформой 1С автоматически и без проблем.
Если вам нужно свернуть данные на сервере, а затем передать их в форму обработки на клиенте, вы можете это сделать, оформив серверный код как функцию, возвращающую ТаблицуЗначений. Например:
// Модуль формы обработки (Сервер)
Функция СвернутьДанныеДокументаНаСервере(СсылкаНаДокумент)
ДокументОбъект = СсылкаНаДокумент.ПолучитьОбъект();
ТаблицаТоваров = ДокументОбъект.Товары.Выгрузить();
ТаблицаТоваров.Свернуть("Поставщик", "Сумма");
Возврат ТаблицаТоваров;
КонецФункции
// Модуль формы обработки (Клиент)
Процедура Кнопка_ЗагрузитьСвернутыеДанные(Команда)
СсылкаДокумента = Объект.СсылкаНаДокумент; // Предполагаем, что ссылка передана в реквизит формы
Если Не ЗначениеЗаполнено(СсылкаДокумента) Тогда
Сообщить("Не указан документ для обработки.");
Возврат;
КонецЕсли;
// Вызываем серверную функцию для свертывания данных
СвернутыеДанные = СвернутьДанныеДокументаНаСервере(СсылкаДокумента);
// Очищаем текущую табличную часть формы обработки
Объект.Товары.Очистить(); // Предполагаем, что "Товары" - это табличная часть формы обработки
// Заполняем табличную часть формы обработки свернутыми данными
Для Каждого СтрокаСвернутая Из СвернутыеДанные Цикл
НоваяСтрокаТЧФормы = Объект.Товары.Добавить();
НоваяСтрокаТЧФормы.Поставщик = СтрокаСвернутая.Поставщик;
НоваяСтрокаТЧФормы.СуммаОтПоставщика = СтрокаСвернутая.Сумма; // Используем имя колонки из формы
КонецЦикла;
КонецПроцедуры
Этот подход позволяет избежать ошибки "интерактивные операции недоступны", которая возникает при попытке получить форму или выполнить другие клиентские действия на сервере.
Метод с использованием Соответствия (аналог словаря или хеш-таблицы в других языках программирования) также является очень эффективным и гибким. Он особенно удобен, когда необходимо агрегировать данные на клиенте или когда требуется более сложная логика агрегации, чем предоставляет метод Свернуть().
Объект Соответствие хранит пары "ключ-значение". В нашем случае мы можем использовать поставщика как ключ, а его суммарную сумму — как значение. При проходе по исходным данным мы проверяем, есть ли уже такой поставщик в Соответствии. Если есть, мы добавляем текущую сумму к уже имеющейся. Если нет, мы добавляем нового поставщика с его начальной суммой.
Соответствия: Инициализируем новый объект Соответствие.Соответствия: Проходим циклом по исходной табличной части. Для каждой строки получаем поставщика и сумму. Проверяем, существует ли поставщик как ключ в Соответствии. Если нет, добавляем его с текущей суммой. Если да, прибавляем текущую сумму к уже накопленной.Соответствие будет содержать уникальных поставщиков в качестве ключей и их общие суммы в качестве значений. Затем мы можем пройти циклом по элементам Соответствия и использовать эти пары "ключ-значение" для заполнения новой табличной части.Предположим, у нас есть доступ к табличной части Объект.Товары (например, из документа или формы обработки).
// Этот код может выполняться как на клиенте, так и на сервере.
// Для примера, пусть он будет в модуле формы обработки, выполняющийся на клиенте
// (или в вызываемой с клиента серверной процедуре, если Объект.Товары это ТЧ документа)
Процедура СвернутьТоварыЧерезСоответствие()
// 1. Создаем новое Соответствие
СоответствиеПоставщиковИСумм = Новый Соответствие;
// 2. Проходим по исходной табличной части и заполняем Соответствие
Для Каждого СтрокаТЧ Из Объект.Товары Цикл
// Получаем текущую сумму для данного поставщика из Соответствия
// Если поставщика еще нет, то ТекЗначение будет Неопределено
ТекЗначение = СоответствиеПоставщиковИСумм[СтрокаТЧ.Поставщик];
// Обновляем (или устанавливаем) значение в Соответствии
// Если ТекЗначение=Неопределено, значит поставщик встретился впервые, начинаем с 0
СоответствиеПоставщиковИСумм[СтрокаТЧ.Поставщик] = ?(ТекЗначение = Неопределено, 0, ТекЗначение) + СтрокаТЧ.Сумма;
КонецЦикла;
// 3. Очищаем исходную табличную часть формы (или другую, куда нужно вывести результат)
Объект.Товары.Очистить(); // Предполагаем, что это ТЧ формы обработки
// 4. Проходим по элементам Соответствия и заполняем новую табличную часть
Для Каждого КлючЗначение Из СоответствиеПоставщиковИСумм Цикл
НоваяСтрока = Объект.Товары.Добавить();
НоваяСтрока.Поставщик = КлючЗначение.Ключ; // Ключ - это Поставщик
НоваяСтрока.СуммаОтПоставщика = КлючЗначение.Значение; // Значение - это суммарная Сумма
КонецЦикла;
КонецПроцедуры
Этот метод также очень гибок, например, если нам нужно было бы не просто суммировать, а, скажем, хранить список уникальных номенклатур для каждого поставщика, мы могли бы хранить в значении Соответствия не число, а другой массив или список значений.
Как мы уже выяснили, добавление Строка.Поставщик и Строка.Сумма как отдельных, несвязанных элементов в один плоский массив Массив.Добавить(Строка.Поставщик); Массив.Добавить(Строка.Сумма); является некорректным для данной задачи. В 1С, когда требуется связать несколько значений, мы должны использовать объекты, способные хранить их вместе:
Структура: Для хранения фиксированного набора реквизитов, например: Новый Структура("Поставщик, Сумма", Строка.Поставщик, Строка.Сумма).Соответствие: Для пар "ключ-значение", где ключ уникален.ТаблицаЗначений: Для табличных данных со строками и колонками.Если бы мы хотели работать с массивом структур, то это выглядело бы так:
МассивСтруктур = Новый Массив;
Для Каждого Строка Из Объект.Товары Цикл
// Добавляем в массив объект Структура, который содержит Поставщика и Сумму
МассивСтруктур.Добавить(Новый Структура("Поставщик, Сумма", Строка.Поставщик, Строка.Сумма));
КонецЦикла;
Даже с таким массивом структур, ручное удаление дубликатов с суммированием вложенными циклами оставалось бы сложной и неэффективной задачей по сравнению с ТаблицейЗначений или Соответствием.
Ошибка "индекс вне диапазона" в исходном коде:
Пока ИндексТекущегоЭлемента < ВсегоЭлементов Цикл
ИндексСледующегоЭлемента = ИндексТекущегоЭлемента + 1;
Пока ИндексСледующегоЭлемента < ВсегоЭлементов Цикл
Если Массив[ИндексСледующегоЭлемента] = Массив[ИндексТекущегоЭлемента] Тогда
Массив.Удалить(ИндексСледующегоЭлемента); // Удаление элемента
ВсегоЭлементов = ВсегоЭлементов - 1; // Изменение размера массива
// ... Здесь могут быть проблемы, так как ИндексСледующегоЭлемента теперь указывает не на тот элемент
// Или вовсе за пределы нового, уменьшенного массива.
// И попытка Массив.Удалить(ИндексСледующегоЭлемента + 1); - почти гарантированная ошибка.
Иначе
ИндексСледующегоЭлемента = ИндексСледующегоЭлемента + 1;
КонецЕсли;
КонецЦикла;
ИндексТекущегоЭлемента = ИндексТекущегоЭлемента + 1;
КонецЦикла;
Эта проблема возникает, когда размер коллекции (в данном случае Массив) изменяется во время итерации по ней с использованием числовых индексов. При удалении элемента индексы оставшихся элементов сдвигаются. Если вы продолжаете увеличивать индекс или используете старое значение ВсегоЭлементов, вы можете либо пропустить элемент, который "переместился" на место удаленного, либо попытаться обратиться к индексу, который уже находится за пределами нового, уменьшенного размера массива. Это и приводит к ошибке "индекс вне диапазона".
Рекомендация: Если вам действительно необходимо удалять элементы из массива в цикле, всегда итерируйте в обратном порядке (от последнего элемента к первому). Это гарантирует, что удаление элемента не повлияет на индексы тех элементов, которые вы еще не обработали.
// Пример удаления дубликатов в обратном порядке (только для демонстрации, не для данной задачи с суммированием)
МассивУникальных = Новый Массив;
// Заполняем МассивУникальных какими-то данными
Для Индекс = МассивУникальных.Количество() - 1 По 0 С Шагом -1 Цикл
Элемент = МассивУникальных[Индекс];
// Если нужно удалить элемент по какому-то условию
// Например, удалить все "Тестовый"
Если Элемент = "Тестовый" Тогда
МассивУникальных.Удалить(Индекс);
КонецЕсли;
КонецЦикла;
Однако, для задачи группировки и суммирования, как показано выше, использование ТаблицыЗначений.Свернуть() или Соответствия всегда будет более правильным, производительным и менее подверженным ошибкам подходом.
Мы с вами рассмотрели несколько эффективных методов для решения задачи удаления дубликатов с суммированием связанных значений в 1С. Каждый метод имеет свои преимущества:
ТаблицаЗначений.Свернуть() является наиболее мощным и производительным решением для табличных данных, особенно когда речь идет о больших объемах информации и необходимости гибкой агрегации.Соответствие предлагает отличную альтернативу, особенно когда данные невелики или требуется более гибкая логика обработки на стороне клиента без промежуточного создания ТаблицыЗначений.Выбирайте метод, который лучше всего соответствует вашим конкретным требованиям и контексту выполнения (клиент или сервер), всегда отдавая предпочтение встроенным, оптимизированным механизмам платформы 1С. Использование правильных структур данных и подходов позволяет писать более чистый, надежный и производительный код.