В практике разработчика 1С часто возникает задача, когда из единого массива данных (например, таблицы значений, загруженной из внешнего источника или полученной из ТЧ другого документа) требуется сформировать несколько документов, сгруппировав строки по определенному признаку. Типичный пример — создание нескольких документов «Отчет о розничных продажах» (ОРП), где данные должны быть разделены строго по кассовым сменам.
В этой статье мы подробно разберем, как решить эту задачу, проанализируем решения, предложенные на форуме, выявим их скрытые архитектурные недостатки и предоставим оптимизированные, безопасные шаблоны кода для промышленной разработки.
Имеется исходная таблица значений (в коде — Объект.Продажи), содержащая перечень проданных товаров. Строки таблицы отличаются друг от друга реквизитом Смена. Необходимо создать столько документов «Отчет о розничных продажах», сколько уникальных смен представлено в таблице значений, и перенести в каждый документ только те строки, которые относятся к соответствующей смене.
Для решения этой задачи существует два основных архитектурных подхода:
Рассмотрим детально оба варианта, исправив критические ошибки производительности и проектирования, которые часто допускают начинающие разработчики.
---
Этот способ основан на работе исключительно с объектом ТаблицаЗначений в оперативной памяти. Сначала мы получаем список уникальных смен, а затем в цикле выбираем строки для каждой смены.
Давайте посмотрим на базовый вариант реализации, который часто выбирают разработчики, и разберем его узкие места:
// НАИВНЫЙ (НЕОПТИМИЗИРОВАННЫЙ) ВАРИАНТ
ОбъектПродажи = Объект.Продажи.Выгрузить();
ИндексыПоСменам = ОбъектПродажи.Скопировать();
ИндексыПоСменам.Свернуть("Смена", "Сумма");
Для Каждого СтрСмена Из ИндексыПоСменам Цикл
НовыйОРП = Документы.ОтчетОРозничныхПродажах.СоздатьДокумент();
СтруктураПоиска = Новый Структура("Смена", СтрСмена.Смена);
НайденныеСтроки = ОбъектПродажи.НайтиСтроки(СтруктураПоиска);
Для Каждого СтрТЗПродажи Из НайденныеСтроки Цикл
СтрОРП = НовыйОРП.Товары.Добавить();
ЗаполнитьЗначенияСвойств(СтрОРП, СтрТЗПродажи);
КонецЦикла;
НовыйОРП.Записать();
КонецЦикла;
1. Линейный поиск в цикле (низкая производительность). Метод НайтиСтроки() без предварительного индексирования выполняет последовательный перебор (сканирование) всей таблицы значений на каждой итерации цикла. Если в таблице тысячи строк, а смен десятки, это вызовет лавинообразное падение скорости работы. Для исправления ситуации необходимо явно проиндексировать колонку поиска перед запуском цикла:
<code>ОбъектПродажи.Индексы.Добавить("Смена");</code>
Это переводит поиск из класса сложности $O(N)$ в быстрый хеш-поиск $O(1)$.
2. Игнорирование стандартной бизнес-логики. При вызове СоздатьДокумент() платформа создает «пустой» объект. Если мы просто заполняем реквизиты вручную, мы обходим встроенные обработчики конфигурации (заполнение по умолчанию валют, складов, счетов учета, налоговых ставок). Правильным решением будет вызов метода Заполнить() сразу после создания объекта.
3. Отсутствие контроля транзакционности. Если при записи третьего документа из пяти произойдет ошибка (например, заблокированы регистры или не хватило прав), первые два документа останутся в базе данных, а остальные созданы не будут. Это приведет к рассинхронизации данных. Пакетное создание документов обязательно должно выполняться в единой транзакции.
НачатьТранзакцию();
Попытка
// Шаг 1. Подготавливаем таблицы данных
ТаблицаПродаж = Объект.Продажи.Выгрузить();
// Обязательно индексируем колонку, по которой будем вести поиск в цикле!
ТаблицаПродаж.Индексы.Добавить("Смена");
// Получаем уникальный список смен
ТаблицаСмен = ТаблицаПродаж.Скопировать();
ТаблицаСмен.Свернуть("Смена");
// Шаг 2. Обходим уникальные смены и создаем документы
Для Каждого СтрокаСмены Из ТаблицаСмен Цикл
// Пропускаем пустые смены, если они есть
Если Не ЗначениеЗаполнено(СтрокаСмены.Смена) Тогда
Продолжить;
КонецЕсли;
НовыйОРП = Документы.ОтчетОРозничныхПродажах.СоздатьДокумент();
// Инициализируем стандартное заполнение реквизитов конфигурации
НовыйОРП.Заполнить(Неопределено);
// Заполняем шапку документа
НовыйОРП.Дата = ТекущаяДата();
НовыйОРП.Организация = Объект.Организация;
НовыйОРП.Склад = Объект.Склад;
НовыйОРП.КассаККМ = Объект.КассаККМ;
// Ищем строки, относящиеся к текущей смене
ПараметрыОтбора = Новый Структура("Смена", СтрокаСмены.Смена);
СтрокиТекущейСмены = ТаблицаПродаж.НайтиСтроки(ПараметрыОтбора);
// Переносим строки в документ с использованием ЗаполнитьЗначенияСвойств
Для Каждого СтрокаИсточника Из СтрокиТекущейСмены Цикл
// Обрабатываем возвраты и продажи в разные табличные части
Если СтрокаИсточника.Возврат = 2 Тогда
НоваяСтрока = НовыйОРП.ВозвращенныеТовары.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаИсточника);
// Заполняем специфичные реквизиты
НоваяСтрока.Склад = Объект.Склад;
Иначе
НоваяСтрока = НовыйОРП.Товары.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаИсточника);
НоваяСтрока.Склад = Объект.Склад;
КонецЕсли;
КонецЦикла;
// Свернем данные на уровне документа, чтобы не плодить дублирующие строки
НовыйОРП.Товары.Свернуть("Номенклатура, Цена, Склад", "Количество, Сумма");
НовыйОРП.ВозвращенныеТовары.Свернуть("Номенклатура, Цена, Склад", "Количество, Сумма");
// Записываем документ
НовыйОРП.Записать(РежимЗаписиДокумента.Запись);
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации("ПрограммноеСозданиеОРП",
УровеньЖурналаРегистрации.Ошибка,,,
"Ошибка при формировании документов по сменам: " + ОписаниеОшибки());
ВызватьИсключение "Не удалось сформировать документы по сменам. Операция отменена. Подробнее в журнале регистрации.";
КонецПопытки;
---
Методологически более верным в платформе 1С является использование механизма запросов. Мы можем передать исходную таблицу значений в запрос в качестве параметра, поместить ее во временную таблицу и выполнить выборку с обходом результатов по группировкам (используя ключевое слово ИТОГИ ПО).
При передаче таблицы значений в качестве параметра запроса (Запрос.УстановитьПараметр("ТЧ", ИсходнаяТаблица)) платформа накладывает жесткое ограничение: все колонки таблицы должны быть строго типизированы. Если таблица была создана динамически в коде без явного указания типов при вызове метода Колонки.Добавить(), запрос завершится аварийно с системной ошибкой: «Служебная ошибка: Тип не может быть выбран в запросе».
Перед передачей ТЗ в запрос убедитесь, что колонки инициализированы следующим образом:
ОписаниеТипаНоменклатура = Новый ОписаниеТипов("СправочникСсылка.Номенклатура");
ОписаниеТипаЧисло = Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2));
ТаблицаЗначений = Новый ТаблицаЗначений;
ТаблицаЗначений.Колонки.Добавить("Номенклатура", ОписаниеТипаНоменклатура);
ТаблицаЗначений.Колонки.Добавить("Количество", ОписаниеТипаЧисло);
ТаблицаЗначений.Колонки.Добавить("Сумма", ОписаниеТипаЧисло);
ТаблицаЗначений.Колонки.Добавить("Смена", Новый ОписаниеТипов("Число")); // или СправочникСсылка.КассовыеСмены
// Предполагается, что ИсходнаяТаблицаЗначений строго типизирована
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТЧ.Номенклатура КАК Номенклатура,
| ТЧ.Количество КАК Количество,
| ТЧ.Сумма КАК Сумма,
| ТЧ.Смена КАК Смена,
| ТЧ.Возврат КАК Возврат
|ПОМЕСТИТЬ ВТ_ИсходныеДанные
|ИЗ
| &ТЧ КАК ТЧ
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_ИсходныеДанные.Номенклатура КАК Номенклатура,
| ВТ_ИсходныеДанные.Количество КАК Количество,
| ВТ_ИсходныеДанные.Сумма КАК Сумма,
| ВТ_ИсходныеДанные.Смена КАК Смена,
| ВТ_ИсходныеДанные.Возврат КАК Возврат
|ИЗ
| ВТ_ИсходныеДанные КАК ВТ_ИсходныеДанные
|ИТОГИ ПО
| Смена";
Запрос.УстановитьПараметр("ТЧ", ИсходнаяТаблицаЗначений);
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
НачатьТранзакцию();
Попытка
// Выбираем первый уровень обхода - Группировки (Смена)
ВыборкаПоСменам = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаПоСменам.Следующий() Цикл
НовыйОРП = Документы.ОтчетОРозничныхПродажах.СоздатьДокумент();
НовыйОРП.Заполнить(Неопределено);
НовыйОРП.Дата = ТекущаяДата();
НовыйОРП.Организация = Объект.Организация;
НовыйОРП.Склад = Объект.Склад;
НовыйОРП.КассаККМ = Объект.КассаККМ;
// Получаем детальные записи только по текущей смене
ВыборкаДетальныеСтроки = ВыборкаПоСменам.Выбрать();
Пока ВыборкаДетальныеСтроки.Следующий() Цикл
Если ВыборкаДетальныеСтроки.Возврат = 2 Тогда
НоваяСтрока = НовыйОРП.ВозвращенныеТовары.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, ВыборкаДетальныеСтроки);
НоваяСтрока.Склад = Объект.Склад;
Иначе
НоваяСтрока = НовыйОРП.Товары.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, ВыборкаДетальныеСтроки);
НоваяСтрока.Склад = Объект.Склад;
EndIf;
КонецЦикла;
// Сворачиваем строки для оптимизации записи
НовыйОРП.Товары.Свернуть("Номенклатура, Цена, Склад", "Количество, Сумма");
НовыйОРП.ВозвращенныеТовары.Sвернуть("Номенклатура, Цена, Склад", "Количество, Сумма");
НовыйОРП.Записать(РежимЗаписиДокумента.Запись);
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации("ПрограммноеСозданиеОРП_Запрос",
УровеньЖурналаРегистрации.Ошибка,,,
"Ошибка создания документов: " + ОписаниеОшибки());
ВызватьИсключение "Ошибка при обработке транзакции создания документов.";
КонецПопытки;
КонецЕсли;
---
Оба представленных алгоритма гарантированно решают проблему создания документов по сменам, но имеют свои сферы применения: