При разработке отчетов на базе Системы компоновки данных (СКД) в 1С программисты часто сталкиваются с необходимостью динамического расчета итогов по колонкам, которые формируются на основе условий. Типичный пример — вывод выручки по отдельным месяцам (январь, февраль, март) в виде отдельных столбцов и получение их общей суммы в конце. Другая популярная и более сложная задача — организация ручного ввода данных прямо в ячейки сформированного отчета с последующим сохранением этих значений в базу данных.
В рамках этой статьи мы подробно разберем оба сценария. Выясним, почему стандартное сложение полей в запросе или вычисляемых полях часто выдает пустой результат, как правильно настроить агрегацию в ресурсах СКД, а также по шагам реализуем механизм интерактивного изменения данных в отчете с их сохранением в независимый регистр сведений.
Представим ситуацию: в запросе СКД настроено три условных поля для вывода выручки за первый квартал года по отдельным месяцам:
// Поле "ВыручкаЯнварь"
ВЫБОР
КОГДА МЕСЯЦ(Продажи.Документ.Дата) = 1
ТОГДА ЕСТЬNULL(Продажи.СуммаВыручки, 0)
КОНЕЦ
// Поле "ВыручкаФевраль"
ВЫБОР
КОГДА МЕСЯЦ(Продажи.Документ.Дата) = 2
ТОГДА ЕСТЬNULL(Продажи.СуммаВыручки, 0)
КОНЕЦ
// Поле "ВыручкаМарт"
ВЫБОР
КОГДА МЕСЯЦ(Продажи.Документ.Дата) = 3
ТОГДА ЕСТЬNULL(Продажи.СуммаВыручки, 0)
КОНЕЦ
Нам требуется получить четвертую колонку, в которой будет отображаться сумма этих трех месяцев. Посмотрим, почему стандартные подходы могут не работать и как сделать это правильно.
Почему простое сложение полей в запросе или вычисляемых полях не работает?
Попытка написать выражение вида ВыручкаЯнварь + ВыручкаФевраль + ВыручкаМарт на уровне детальных записей запроса или в вычисляемых полях СКД часто приводит к тому, что колонка с итогом оказывается пустой (содержит NULL) или рассчитывается некорректно. Происходит это по двум причинам:
NULL (так как ветка ИНАЧЕ в конструкции ВЫБОР по умолчанию возвращает именно его, если она явно не указана). В базах данных сложение любого числа со значением NULL всегда дает в результате NULL. Даже использование ЕСТЬNULL на верхнем уровне не решает проблему, если само условие ветки КОГДА не сработало.Правильное решение: расчет через ресурсы СКД
Чтобы гарантировать корректный расчет сумм на любом уровне группировки отчета, нам необходимо перенести формулу сложения на вкладку Ресурсы схемы компоновки данных. Выясним, почему этот метод работает безотказно.
При объявлении полей ресурсами, СКД сначала выполняет агрегирование данных (суммирование, среднее и т.д.) по каждой отдельной условной колонке в разрезе текущей группировки, автоматически отсекая и обрабатывая значения NULL. После этого система производит математическое сложение уже полученных агрегированных итогов.
Разберем по шагам, как это настроить:
ВыручкаЯнварь, ВыручкаФевраль, ВыручкаМарт) в список ресурсов. В качестве выражения для каждого из них укажем стандартную функцию агрегации: Сумма(ВыручкаЯнварь), Сумма(ВыручкаФевраль) и так далее.
Сумма(ВыручкаЯнварь) + Сумма(ВыручкаФевраль) + Сумма(ВыручкаМарт)
В этом случае СКД корректно рассчитает математическую сумму для всех уровней группировок и общих итогов отчета, даже если в некоторых месяцах данные полностью отсутствовали.
Прописывать условные конструкции ВЫБОР КОГДА МЕСЯЦ(...) = X в запросе — подход не самый гибкий. Если пользователю потребуется отчет за другой год или за полугодие, вам придется вручную переписывать запрос и ресурсы. Гораздо профессиональнее использовать встроенный инструмент СКД — Таблицу (кросс-таблицу).
Проанализируем этот метод:
ПериодМесяц (дата начала месяца) и универсальный ресурс СуммаВыручки.Номенклатура).ПериодМесяц. Чтобы названия месяцев выглядели красиво, в свойствах этого поля в СКД зададим формат редактирования, например, ДФ='ММММ гггг' (например, "Январь 2024").При таком подходе СКД сама динамически сформирует ровно столько колонок, сколько месяцев попало в выбранный пользователем период. Колонку «Итого» система добавит автоматически в конец таблицы, выполнив сложение всех колонок без написания формул вручную.
Теперь разберем более сложный сценарий: пользователю необходимо, чтобы в отчете была колонка, значения в которой он может заполнять вручную с клавиатуры (например, вносить плановые показатели или комментарии), и чтобы эти данные сохранялись в системе и отображались при следующем открытии и формировании отчета.
Для реализации этой задачи нам потребуется задействовать несколько механизмов платформы: программный вывод отчета, расшифровку СКД и независимый регистр сведений.
Для хранения введенных пользователем данных создадим независимый непериодический Регистр сведений (назовем его ПланыПродаж). Нам важно точно знать, к какому объекту и к какому периоду относится введенное число.
Определим следующую структуру регистра:
Номенклатура (тип СправочникСсылка.Номенклатура).ПериодМесяц (тип Дата, с точностью до месяца).СуммаПлана (тип Число, длина 15, точность 2).Чтобы ранее сохраненные плановые данные выводились в отчете, изменим исходный запрос СКД. Свяжем основную таблицу продаж с таблицей нашего нового регистра сведений с помощью левого соединения:
ВЫБРАТЬ
Продажи.Номенклатура КАК Номенклатура,
Продажи.СуммаВыручки КАК ВыручкаФактическа,
НАЧАЛОПЕРИОДА(Продажи.Документ.Дата, МЕСЯЦ) КАК ПериодМесяц,
ЕСТЬNULL(ПланыПродаж.СуммаПлана, 0) КАК ВыручкаПлан
ИЗ
РегистрНакопления.Продажи КАК Продажи
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ПланыПродаж КАК ПланыПродаж
ПО Продажи.Номенклатура = ПланыПродаж.Номенклатура
И НАЧАЛОПЕРИОДА(Продажи.Документ.Дата, МЕСЯЦ) = ПланыПродаж.ПериодМесяц
Поле ВыручкаПлан добавим в список ресурсов СКД с выражением Сумма(ВыручкаПлан).
По умолчанию сформированный табличный документ в 1С закрыт для редактирования (свойство ТолькоПросмотр установлено в значение Истина), и ячейки не содержат интерактивных элементов. Нам нужно программно перехватить вывод отчета, найти ячейки колонки "План" и разрешить их изменение.
В модуле объекта отчета переопределим процедуру ПриКомпоновкеРезультата:
Процедура ПриКомпоновкеРезультата(ДокументРезультат, ДанныеРасшифровки, СтандартнаяОбработка)
СтандартнаяОбработка = Ложь;
// Настройки компоновки по умолчанию
КомпоновщикМакет = Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакет.Выполнить(СхемаКомпоновкиДанных, КомпоновщикНастроек.ПолучитьНастройки(), ДанныеРасшифровки);
// Запуск процессора компоновки
ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
ПроцессорКомпоновки.Инициализировать(МакетКомпоновки, , ДанныеРасшифровки, Истина);
// Выводим результат в табличный документ
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент;
ПроцессорВывода.УстановитьДокумент(ДокументРезультат);
ПроцессорВывода.Вывести(ПроцессорКомпоновки);
// Теперь разрешаем редактирование документа и настраиваем ячейки
ДокументРезультат.ТолькоПросмотр = Ложь;
// Пробежим по сетке табличного документа, чтобы найти нужную колонку
// и включить редактирование для конкретных ячеек
Для ТекущаяСтрока = 1 По ДокументРезультат.ВысотаТаблицы Цикл
Для ТекущаяКолонка = 1 По ДокументРезультат.ШиринаТаблицы Цикл
Ячейка = ДокументРезультат.Область(ТекущаяСтрока, ТекущаяКолонка);
// Определяем, относится ли ячейка к нашей колонке плана по расшифровке
Если Ячейка.ПараметрРасшифровки <> Неопределено Тогда
ДанныеЯчейки = ДанныеРасшифровки.Элементы[Ячейка.ПараметрРасшифровки];
Если ТипЗнч(ДанныеЯчейки) = Тип("ЭлементРасшифровкиКомпоновкиДанныхПоля") Тогда
Для Каждого ЗначениеПоля Из ДанныеЯчейки.ПолучитьПоля() Цикл
Если ЗначениеПоля.Поле = "ВыручкаПлан" Тогда
// Настраиваем ячейку для ввода чисел
Ячейка.ТолькоПросмотр = Ложь;
Ячейка.СодержитЗначение = Истина;
Ячейка.ТипЗначения = Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2));
КонецЕсли;
КонецЦикла;
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Когда пользователь вводит новое значение в редактируемую ячейку на форме отчета, срабатывает событие табличного документа формы. Нам нужно отследить это изменение, вытащить из расшифровки ключевые поля строки (какой именно это товар и какой месяц) и записать новое значение в регистр сведений.
Рассмотрим процедуру-обработчик события на форме отчета:
&НаКлиенте
Процедура РезультатПриИзмененииСодержимогоОбласти(Элемент, Область)
// Получаем текущее значение из измененной ячейки
НовоеЗначение = Область.Значение;
// Пытаемся получить параметры расшифровки из измененной ячейки
ПараметрРасшифровки = Область.ПараметрРасшифровки;
Если ПараметрРасшифровки = Неопределено Тогда
Возврат;
КонецЕсли;
// Получаем данные строки по идентификатору расшифровки
ДанныеСтроки = ПолучитьДанныеСтрокиРасшифровки(ПараметрРасшифровки);
Если ДанныеСтроки <> Неопределено Тогда
// Записываем данные в регистр сведений на сервере
ЗаписатьПланВРегистр(ДанныеСтроки.Номенклатура, ДанныеСтроки.ПериодМесяц, НовоеЗначение);
КонецЕсли;
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьДанныеСтрокиРасшифровки(Знач ПараметрРасшифровки)
ДанныеРасшифровкиОбъект = ПолучитьИзВременногоХранилища(ДанныеРасшифровкиИдентификатор);
ЭлементРасшифровки = ДанныеРасшифровкиОбъект.Элементы[ПараметрРасшифровки];
Если ТипЗнч(ЭлементРасшифровки) = Тип("ЭлементРасшифровкиКомпоновкиДанныхПоля") Тогда
Результат = Новый Структура("Номенклатура, ПериодМесяц");
// Перебираем все поля родительских группировок, чтобы собрать контекст строки
ТекущийРодитель = ЭлементРасшифровки.ПолучитьРодителей()[0];
Пока ТекущийРодитель <> Неопределено Цикл
Если ТипЗнч(ТекущийРодитель) = Тип("ГруппировкаЭлементовКомпоновкиДанных") Тогда
Для Каждого ПолеРодителя Из ТекущийРодитель.ПолучитьПоля() Цикл
Если ПолеРодителя.Поле = "Номенклатура" Тогда
Результат.Номенклатура = ПолеРодителя.Значение;
ИначеЕсли ПолеРодителя.Поле = "ПериодМесяц" Тогда
Результат.ПериодМесяц = ПолеРодителя.Значение;
КонецЕсли;
КонецЦикла;
КонецЕсли;
ТекущийРодитель = ТекущийРодитель.ПолучитьРодителей()[0];
КонецЦикла;
Возврат Результат;
КонецЕсли;
Возврат Неопределено;
КонецФункции
&НаСервереБезКонтекста
Процедура ЗаписатьПланВРегистр(Номенклатура, ПериодМесяц, ЗначениеПлана)
// Используем менеджер записи для быстрого обновления или добавления данных
МенеджерЗаписи = РегистрыСведений.ПланыПродаж.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Номенклатура = Номенклатура;
МенеджерЗаписи.ПериодМесяц = НачалоДня(ПериодМесяц);
МенеджерЗаписи.СуммаПлана = ЗначениеПлана;
// Если введено 0 или пустое значение — можно удалить запись из регистра
Если ЗначениеПлана = 0 Тогда
МенеджерЗаписи.Удалить();
Иначе
МенеджерЗаписи.Записать(Истина);
КонецЕсли;
КонецПроцедуры
Проанализируем работу данного алгоритма. При изменении ячейки пользователем система считывает идентификатор расшифровки ячейки. Затем на сервере мы поднимаемся вверх по дереву расшифровки родительских группировок текущей строки, собирая значения ключевых полей — конкретной Номенклатуры и ПериодаМесяца. Получив эти данные, мы вызываем процедуру записи в регистр сведений ПланыПродаж. Теперь при следующем открытии и нажатии кнопки "Сформировать", отчет автоматически прочитает измененные и сохраненные данные из регистра.
Рассмотренные подходы позволяют решать сложные и нестандартные аналитические задачи в среде 1С Предприятие, обеспечивая как гибкие расчеты сумм в условных колонках, так и реализацию полноценных интерактивных интерфейсов планирования непосредственно в рамках привычных отчетов СКД.