При разработке отчетов в 1С без использования Системы компоновки данных (СКД) программисты часто сталкиваются с необходимостью вывода итогов по группам строк. Например, когда в одном отчете выводится список товаров по нескольким накладным, и после каждой накладной нужно вывести строку «Итого по документу». Рассмотрим подробно, как реализовать этот функционал программно, избежав типичных ошибок вроде дублирования строк или неверного расчета сложных показателей (таких как ROI).
Это самый эффективный метод с точки зрения производительности, так как он требует всего одного прохода по таблице значений. Основная идея заключается в том, что мы сравниваем текущий документ в цикле с документом из предыдущей итерации.
Разберем по шагам алгоритм реализации:
ПредыдущийДок значением Неопределено и обнуляем счетчики итогов.ПредыдущийДок не равен текущему и не является пустым, значит, группа строк по предыдущему документу закончилась.ПредыдущийДок значение текущего документа.Проанализируем пример кода для этого метода:
ПредыдущийДок = Неопределено;
ИтогПоДокументу = 0;
Для Каждого СтрокаТЗ Из ТаблицаДанных Цикл
// Проверяем, сменился ли документ
Если ПредыдущийДок <> Неопределено И ПредыдущийДок <> СтрокаТЗ.Регистратор Тогда
// Выводим секцию итога для ПРЕДЫДУЩЕГО документа
ОбластьИтог = Макет.ПолучитьОбласть("ИтогПоДокументу");
ОбластьИтог.Параметры.ТекстИтога = "Итого по " + СокрЛП(ПредыдущийДок);
ОбластьИтог.Параметры.Количество = ИтогПоДокументу;
ТабДок.Вывести(ОбластьИтог);
// Сбрасываем счетчик для следующей группы
ИтогПоДокументу = 0;
КонецЕсли;
// Выводим основную строку данных
ОбластьСтрока = Макет.ПолучитьОбласть("Строка");
// ... заполнение параметров строки ...
ТабДок.Вывести(ОбластьСтрока);
// Накапливаем итоги и фиксируем текущий документ
ИтогПоДокументу = ИтогПоДокументу + СтрокаТЗ.Количество;
ПредыдущийДок = СтрокаТЗ.Регистратор;
КонецЦикла;
// Важно: выводим итог по самому последнему документу после завершения цикла
Если ПредыдущийДок <> Неопределено Тогда
ОбластьИтог = Макет.ПолучитьОбласть("ИтогПоДокументу");
// ... заполнение параметров ...
ТабДок.Вывести(ОбластьИтог);
КонецЕсли;
Если логика отчета сложная (например, нужно считать количество уникальных позиций внутри документа или сложные коэффициенты), удобнее использовать вложенные циклы. Выясним причину, почему это иногда приводит к дубликатам: чаще всего разработчики забывают убрать внешний обход всей таблицы или некорректно фильтруют данные.
Рассмотрим правильную структуру вложенных циклов:
НайтиСтроки().Посмотрим на пример реализации:
// Получаем список уникальных документов
ТЗ_Документы = ТЗ_Данные.Скопировать();
ТЗ_Документы.Свернуть("РегистраторДокументПродажи");
Для Каждого СтрокаДок Из ТЗ_Документы Цикл
// Фильтруем детальные записи по текущему документу
ПараметрыОтбора = Новый Структура("РегистраторДокументПродажи", СтрокаДок.РегистраторДокументПродажи);
ДетальныеСтроки = ТЗ_Данные.НайтиСтроки(ПараметрыОтбора);
СуммаПоДоку = 0;
Для Каждого СтрДанные Из ДетальныеСтроки Цикл
// Выводим детальную информацию
ОбластьСтрока = Макет.ПолучитьОбласть("Строка");
ЗаполнитьЗначенияСвойств(ОбластьСтрока.Параметры, СтрДанные);
ТабДок.Вывести(ОбластьСтрока);
СуммаПоДоку = СуммаПоДоку + СтрДанные.Сумма;
КонецЦикла;
// Выводим итог по группе
ОбластьИтог = Макет.ПолучитьОбласть("ИтогПоГруппе");
ОбластьИтог.Параметры.ИтогСумма = СуммаПоДоку;
ТабДок.Вывести(ОбластьИтог);
КонецЦикла;
Проанализируем ситуацию, когда в итогах нужно вывести расчетный показатель, например ROI (Return on Investment). Распространенная ошибка — суммировать значения ROI из каждой строки. Математически это неверно.
Чтобы посчитать ROI по группе документов, мы должны сначала просуммировать все составляющие формулы по этой группе, и только потом производить деление. Например, если ROI = (Выручка - Себестоимость) / Себестоимость * 100, то для итога по документу формула будет выглядеть так:
ИтогROI = (Сумма(Выручка) - Сумма(Себестоимость)) / Сумма(Себестоимость) * 100.
Для реализации этого в коде 1С удобно использовать метод Итог() у таблицы значений. Если мы используем Метод 2 (с вложенными циклами), то расчет будет выглядеть так:
// Создаем временную таблицу для строк текущего документа
ТЗ_ТекущийДок = ТЗ_Данные.Скопировать(ДетальныеСтроки);
СуммаЗаказа = ТЗ_ТекущийДок.Итог("СуммаЗаказа");
СуммаСкидки = ТЗ_ТекущийДок.Итог("СуммаСкидки");
Если СуммаСкидки <> 0 Тогда
ИтогROI = (СуммаЗаказа - СуммаСкидки) / СуммаСкидки * 100;
Иначе
ИтогROI = 0;
КонецЕсли;
Чтобы отчет был удобным для пользователя, мы можем добавить возможность сворачивать и разворачивать итоги. Для этого используются встроенные методы 1С НачатьГруппуСтрок() и ЗакончитьГруппуСтрок().
Разберем, как это добавить в код:
Для Каждого СтрокаДок Из ТЗ_Документы Цикл
// Перед выводом детальных строк начинаем группу
ТабДок.НачатьГруппуСтрок("ГруппаДокумента", Ложь); // Ложь - группа по умолчанию развернута
Для Каждого СтрДанные Из ДетальныеСтроки Цикл
// Вывод строк...
КонецЦикла;
ТабДок.ЗакончитьГруппуСтрок();
// Вывод итога после завершения группы
ТабДок.Вывести(ОбластьИтог);
КонецЦикла;
Важные рекомендации:
ТЗ.Сортировать("Регистратор");.ОбластьСтрока) параметры могут «зависать» от предыдущей итерации. Рекомендуется либо очищать их вручную, либо заново получать область методом Макет.ПолучитьОбласть() внутри цикла.ТЗ.Скопировать() внутри цикла может существенно замедлить работу. В таких случаях лучше использовать ИТОГИ ПО в тексте запроса и обход по группировкам через Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам).Таким образом, для подсчета итогов по идентичным документам в макете лучше всего использовать либо метод сравнения с предыдущим значением (для простых итогов), либо метод предварительной группировки таблицы (для сложных расчетов). Это обеспечит чистоту кода и корректность математических вычислений в отчете.