Строим гибкие отчеты по регистру   

Для начала определимся с понятиями (ну все же должно быть по понятиям). Под «гибким отчетом» имеется в виду такой отчет, который в зависимости от действий пользователя выдается в таком виде, в каком это необходимо пользователю. Основные требования к отчету такие:

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

Я проиллюстрирую возможность создания такого отчета на своем примере. Имеется торговая конфигурация. В ней существует регистр «Партии», по которому нам надо построить отчет. Структура регистра такова:

Структура регистра партии.

В форме отчета пользователь может выбирать следующие значения:

  • Склад
  • Товар или группа
  • Контрагент (поставщик) или группа
  • Поставка (приходный документ)
  • Менеджер (в моей конфигурации к каждому товару прикреплен менеджер)
  • Завод (у каждого товара указан завод-изготовитель)
  • Документ (если вдруг захочется посмотреть себестоимость по конкретному документу)
  • Статус (закупленный товар, товар на реализации, товар с отсрочкой платежа)

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

Форма отчета в конфигураторе

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

Таблица в конфигураторе

Как можно видеть, отчет использует 8 абсолютно одинаковых секций, которые различаются только отступом от левого края и расцветкой. Такое расположение обеспечит хорошую читаемость отчета, а числовое обозначение секций обеспечить удобную работу с ними из механизма вывода запроса в таблицу.

Теперь можно рассмотреть модуль формы отчета. Он приведен ниже:

Перем ВремяПостроения, СписокГруппировок, КоличествоГруппировок;
Перем Запрос, ТекстЗапроса, Таб;
Процедура ПриОткрытии()
    Если НазваниеНабораПрав()="Администратор" Тогда
        Форма.ВыбМенеджер.Доступность(1);
    Иначе
        ВыбМенеджер=глПользователь;
        Форма.ВыбМенеджер.Доступность(0);
    КонецЕсли;
    Если СЗ.РазмерСписка()=0 Тогда
        СЗ.ДобавитьЗначение("Группировка Склад без групп;","Склад");
        СЗ.ДобавитьЗначение("Группировка Товар без групп;","Товар");
        СЗ.ДобавитьЗначение("Группировка Контрагент без групп;","Контрагент");
        СЗ.ДобавитьЗначение("Группировка Поставка;","Поставка");
        СЗ.ДобавитьЗначение("Группировка Менеджер без групп;","Менеджер");
        СЗ.ДобавитьЗначение("Группировка Завод;","Завод");
        СЗ.ДобавитьЗначение("Группировка Статус;","Статус");
    КонецЕсли;
КонецПроцедуры

Теперь подробнее посмотрим на приведенный выше участок кода. Для начала - небольшой административый кусок: если пользователь имеет интерфейс «Менеджера», то ему запрещается менять менджера в форме отчета. И, соответственно, в качестве менеджера подставляется текущий пользователь базы. Далее проверяется количество строк для списка значений. Это сделано для того, чтобы обеспечить возможность сохранения порядка сортировки списка группировок «СЗ», а также чтобы обеспечить сохранность всех устанавливаемых пометок («СЗ» - список с пометками). В качестве добавляемого значения используется строка, которая будет использоваться в запросе при пометке данного элемента списка, далее идет строковое представление элемента списка.

Процедура РаскрытьГруппировку(Номер)
    Пока Запрос.Группировка(Номер)=1 Цикл
        ТекЗнач=СписокГруппировок.ПолучитьЗначение(Номер);
        Если ТекЗнач="Товар" Тогда
            ТекАтрибут=""+Запрос.Товар.Код+" "+Запрос.Товар;
            ТекРасшифровка=Запрос.Товар;
        Иначе
            ТекАтрибут=Запрос.ПолучитьАтрибут(ТекЗнач);
            ТекРасшифровка=ТекАтрибут;
        КонецЕсли;
        Таб.ВывестиСекцию(Номер);
        Если Номер<КоличествоГруппировок Тогда
            РаскрытьГруппировку(Номер+1);
        КонецЕсли;
    КонецЦикла;
КонецПроцедуры

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

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

Этот довольно длинный кусок кода используется для формирования переменных и функций запроса. Перебираются все реквизиты формы, проверяются на предмет заполненности и проверяется также - нет является ли элемент помеченным в списке группировок. Если реквизит выбран, или помечен, мы должны добавить его в качестве переменной запроса. Если он выбран - ниже мы установим по нему условие, а если имеется значок группировки - то по нему будет сделана группировка. Иначе - нам нет никакой необходимости включать данный реквизит в список переменных запроса. Этим мы сэкономим время и место в таблице запроса. Также, в зависимости от выбранной формы отчета (краткая либо полная) в запрос добавляется список функций, ну, и, соответственно, чуть выше добавляются переменные для этих функций.

    //теперь идут все группировки.
    Если ТипЗначенияСтр(СписокГруппировок)<>"СписокЗначений" Тогда
        СписокГруппировок=СоздатьОбъект("СписокЗначений");
    Иначе
        СписокГруппировок.УдалитьВсе();
    КонецЕсли;
    СтрокаЗаголовка="";
    Для к=1 по 7 Цикл
        Если СЗ.Пометка(к)=1 Тогда
            ТекСтр="";
            ТекстЗапроса=ТекстЗапроса+РазделительСтрок+СЗ.ПолучитьЗначение(к,ТекСтр);
            СписокГруппировок.ДобавитьЗначение(ТекСтр);
            СтрокаЗаголовка=СтрокаЗаголовка+?(СтрДлина(СтрокаЗаголовка)=0,ТекСтр," / "+ТекСтр);
        КонецЕсли;
    КонецЦикла;
    Если Подробно=1 Тогда
        ТекстЗапроса=ТекстЗапроса+РазделительСтрок+"Группировка Документ;";
        СписокГруппировок.ДобавитьЗначение("Документ");
        СтрокаЗаголовка=СтрокаЗаголовка+?(СтрДлина(СтрокаЗаголовка)=0,"Документ"," / Документ");
    КонецЕсли;

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

    Если ВыбСтатус.Выбран()=1 Тогда
        Если ВыбСтатус=Перечисление.ВидыПоставки.Закупка Тогда
            //закупка
            ТекстЗапроса=ТекстЗапроса+"
            |Условие(Статус = глЗакупка);"
;
        ИначеЕсли ВыбСтатус=Перечисление.ВидыПоставки.Отсрочка Тогда
            //отсрочка
            ТекстЗапроса=ТекстЗапроса+"
            |Условие(Статус = глОтсрочка);
";
        Иначе
            //реализация
            ТекстЗапроса=ТекстЗапроса+"
            |Условие(Статус = глРеализация);"
;
        КонецЕсли;
    КонецЕсли;
    Если ВыбСклад.Выбран()=1 Тогда
        ТекстЗапроса=ТекстЗапроса+"
        |Условие(Склад = ВыбСклад);"
;
    КонецЕсли;
    Если ВыбТовар.Выбран()=1 Тогда
        Если ВыбТовар.ЭтоГруппа()=1 Тогда
            ТекстЗапроса=ТекстЗапроса+"
            |Условие(Товар в ВыбТовар);"
;
        Иначе
            ТекстЗапроса=ТекстЗапроса+"
            |Условие(Товар = ВыбТовар);"
;
        КонецЕсли;
    КонецЕсли;
    Если ВыбКонтрагент.Выбран()=1 Тогда
        Если ВыбКонтрагент.ЭтоГруппа()=1 Тогда
            ТекстЗапроса=ТекстЗапроса+"
            |Условие(Контрагент в ВыбКонтрагент);"
;
        Иначе
            ТекстЗапроса=ТекстЗапроса+"
            |Условие(Контрагент = ВыбКонтрагент);"
;
        КонецЕсли;
    КонецЕсли;
    Если ВыбПоставка.Выбран()=1 Тогда
        ТекстЗапроса=ТекстЗапроса+"
        |Условие(Поставка = ВыбПоставка);"
;
    КонецЕсли;
    Если ВыбМенеджер.Выбран()=1 Тогда
        ТекстЗапроса=ТекстЗапроса+"
        |Условие(Менеджер = ВыбМенеджер);"
;
    КонецЕсли;
    Если ВыбЗавод.Выбран()=1 Тогда
        ТекстЗапроса=ТекстЗапроса+"
        |Условие(Завод = ВыбЗавод);"
;
    КонецЕсли;
    Если ВыбДокумент.Выбран()=1 Тогда
        ТекстЗапроса=ТекстЗапроса+"
        |Условие(ТекДок = ВыбДокумент);"
;
    КонецЕсли;

Здесь были сформированы условия для запроса. Добавляются только непустые условия. Для справочников с группами осуществляется проверка выбранного значения. Если выбрана группа то в текст запроса добавляется строка с условием вхождение во множество (в), иначе (в случае, если выбран элемент справочника) используется условие равенства (=). Условие равенства обрабатывается быстрее. В результате всех наших манипуляций с тестом запроса, он будет иметь минимально необходимый вид. То есть, без излишних переменных, функций и условий.

    //Сообщить(ТекстЗапроса);
    // Если ошибка в запросе, то выход из процедуры
    Если фТранзакция=1 Тогда
        НачатьТранзакцию();
        Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
            ОтменитьТранзакцию();
            Возврат;
        КонецЕсли;
        ЗафиксироватьТранзакцию();
    Иначе
        Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
            Возврат;
        КонецЕсли;
    КонецЕсли;

В отчет включена возможность работы с механизмом транзакций. В разделенном режиме при работе по сети использование транзакции может значительно ускорить (в 10-ки раз) скорость формирования отчета. НО! К транзакциям в отчете нужно относится достаточно осторожно, так как отчет (как некритичная, или, как менее критичная операция с базой) может застопорить критичные задачи. В данном случае при использовании транзакций при попытке обращения к регистру партий другой пользователь будет вынужден ожидать (и может не дождаться) завершения выполнения запроса. Либо, в некоторых случаях, обоим пользователям, по истечения интервала ожидания будет выдано сообщение «Ошибка при транзакции, выполняемой другим пользователем».

    // Подготовка к заполнению выходных форм данными запроса
    Таб = СоздатьОбъект("Таблица");
    Если ФлагОтчета=1 Тогда
        Таб.ИсходнаяТаблица("Сформировать");
    Иначе
        Таб.ИсходнаяТаблица("Краткая");
    КонецЕсли;
    Таб.ВывестиСекцию("Заголовок");
    Состояние("Заполнение выходной таблицы...");
    Таб.Опции(0, 0, Таб.ВысотаТаблицы(), 0);
    КоличествоГруппировок=СписокГруппировок.РазмерСписка();
    Если КоличествоГруппировок>0 Тогда
        РаскрытьГруппировку(1);
    КонецЕсли;
    // Заполнение полей "Итого"
    Таб.ВывестиСекцию("Итого");
    // Вывод заполненной формы
    Таб.ТолькоПросмотр(1);
    Таб.Показать("Сформировать", "");
    Запрос="";
    ТекстЗапроса="";
    Таб="";
    ВремяПостроения=глЗакончитьЗамер();
    //Это для замера времени построения отчета (прочитать подробнее можно здесь)
КонецПроцедуры

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

Отчет использует похожую на типовую конфигурацию структуру регистров, так что адаптировать его под стандартную (или, наоборот, под нестандартную) конфигурацию не составит труда.

 

 

Перепечатка, воспроизведение в любой форме, распространение, в том числе в переводе, любых материалов с сайта www.softpoint.ru возможны только с письменного разрешения компании "СофтПоинт". Это правило действует для всех без исключения случаев, кроме тех, когда в материале прямо указано разрешение на копирование (основание: Закон Российской Федерации "Об авторском праве и смежных правах").