В "1С:Бухгалтерии 8" одного из клиентов обнаружил невозможность сформировать стандартный отчет "Карточка счета" по некоторым счетам (чуть позже похожее поведение нашел еще в "Карточке субконто" и в "Отчете по проводкам"). Из-за данной периодически возникающей ошибки бухгалтера для работы стали использовать другую версию этого отчета из более ранних редакций конфигурации (когда еще до появления СКД применяли объект "Построитель отчета"). А вообще, если не считать эту тянувшуюся несколько лет ошибку, то в целом в работе системы их больше ничего не беспокоило.
В конфигурации была включена возможность внесения изменений и моими предшественниками были выполнены некоторые доработки под процессы компании. Но, к моему удивлению, конкретно бухгалтерская отчетность не модифицировалась. Что это - "особенные" настройки? Или результат "кривого" ручного обновления? Неужели ошибка типового функционала, которую за много лет никто не обнаружил?
Поиск причины ошибки
Первое допущение: мои предшественники намудрили с RLS из-за чего возникла ошибка в типовом функционале. Но под полными правами проблема то же воспроизвелась - при попытке сформировать отчет получаю странную картинку с частично сформированной таблицей результата в "поломанном" оформлением и сообщение:
Отчет не сформирован!
Неверные параметры "+"
Вторая гипотеза: вывод отчета формируется программно, а на выводе конкретной строки почему-то вывод сбоит (как вариант, возможно при ручном обновлении были обновили функции из общих модулей, а про их вызов из модулей отчетов забыли). Поиском по верхней части текста ошибки обнаружил место фактического формирования табличного документа штатными механизмами платформы. Ошибка генерировалась уже через несколько секунд после начала выполнения следующей строки кода:
ПроцессорВывода.Вывести(ПроцессорКомпоновки, Истина);
Именно во время выполнения обвернутого в попытку встроенного метода платформы и генерировалось исключение про неверный параметр плюса, который далее перехватывался в блоке обработки исключения и показывался пользователю. Следовательно теперь в поисках причины заинтересовавшей меня ошибки нужно идти в описание схемы компоновки.
Третье подозрение: возможно в схеме есть вызовы общих модулей либо для получения значения параметров отчета, либо для расчета вычисляемых полей, либо для формирования представления. И снова нет - ни единого вызова внешних функций, в которых могла возникнуть ошибка. Следовательно дальнейшие поиски ограничены исключительно схемой.
Вариант с ошибкой в запросе был маловероятным. Но я на всякий случай скопировал запрос в консоль запросов, повторил значения параметров и получил все данные периода. Включая те строки, с которыми система компоновки почему-то не смогла справится. Их анализ чуть позже подсказал причину проблемы и ее решение. Итак, поскольку с источником данных для запроса все было в порядке, то ошибку следует искать при пост-обработке в процессе вывода в макет.
Четвертой догадкой стала: проблема с формулами вычисляемых полей. В данном разделе были описано одиннадцать расчетных полей, в трех из которых содержались операции по конкатенации строк с помощью оператора "+". Это были формирование представления документа, а так же описания аналитик по Дебету и Кредиту. Выглядят они во всех типовых (и отраслевых) конфигурациях следующим образом:
Представление(Регистратор) + &ПС + Содержание
Выбор Когда ((НалоговоеНазначениеДт Есть NULL) ИЛИ ( НалоговоеНазначениеДт=Значение(Справочник.НалоговыеНазначенияАктивовИЗатрат.ПустаяСсылка))) Тогда ""
Иначе Выбор Когда Не ЗначениеЗаполнено(Представление(НалоговоеНазначениеДт)) Тогда "<...>"+ &ПС Иначе Представление(НалоговоеНазначениеДт)+ &ПС Конец
Конец
+
Выбор Когда СубконтоДт1 Есть NULL Тогда ""
Иначе Выбор Когда Не ЗначениеЗаполнено(Представление(СубконтоДт1)) Тогда "<...>"+ &ПС Иначе Представление(СубконтоДт1)+ &ПС Конец
Конец
+
Выбор Когда СубконтоДт2 Есть NULL Тогда ""
Иначе Выбор Когда Не ЗначениеЗаполнено(Представление(СубконтоДт2)) Тогда "<...>" Иначе Представление(СубконтоДт2)+ &ПС Конец
Конец
+
Выбор Когда СубконтоДт3 Есть NULL Тогда ""
Иначе Выбор Когда Не ЗначениеЗаполнено(Представление(СубконтоДт3)) Тогда "<...>" Иначе Представление(СубконтоДт3) Конец
Конец
С представлением документа все нормально - все три операнда в данном уравнении всегда являются строками (согласно текста запроса и явно инициированного переносом строки параметра &ПС) и потому не могут быть причиной ошибки. Проверил суждение практикой и заменил формулу на пустую строку - отчет ожидаемо не сформировался до конца.
Строки с описанием аналитик тоже внешне выглядят "правильными". Конечно, смущает что при наличии проверок на заполненность и обладая знанием, что первое или второе субконто могут являются Null-ами (признак отсутствия аналитики на счете), все равно тратится время на анализ последующих субконто. Еще бросается в глаза, что для второго субконто (как для Дебета, так и для Кредита) перенос строки добавляют лишь после имеющих значение представлений, а после строки "<...>" про перенос забыли. (Кстати, из любопытства проверил последнюю ERP - там точно так же забыт этот несчастный перенос строки. Видимо типовые бухгалтерские отчеты как написали лет 10 назад, так больше ни разу и не трогали.) Но после заполнения полей аналитик пустыми строками, отчет наконец-то успешно сформировался. Следовательно не все параметры имеют тип строки. Единственным подозреваемым остается результат функции Представление().
Исправление ошибки
Ошибку тут же исправил заключив результаты вызова функций Представление() в функции Строка().
Заодно не удержался от прочих исправлений - вернул потерянные переносы строк, а выводы второго и третьего субконто перенес в ветки ИНАЧЕ их предшественников.
Что бы я изменил в стандартных бухгалтерских отчетах?
Я сделал быстрое исправление, которое "починило отчет" и обрадовало бухгалтеров, которые теперь снова смогут пользоваться стандартной СКД-отчетностью. Но будь я разработчиком типовых, то исправление носило бы более глобальный характер.
Мне сразу не понравилась бездумная конкатенация в описании вычисляемых полей. Если учитывать каждое генерируемое представление, то получаем от 9 до 14 операций конкатенации на КАЖДУЮ выводимую строку отчета. Как я уже упоминал в другой своей статье, операции по сложению строк не являются дешевыми. По факту каждый раз в памяти создается новая строка, в которую копируются две предшественницы. Чем больше участников в операции "сложения", тем больше будет создано и затем уничтожено промежуточных строк. (Теперь вы понимаете почему квартальные и годовые отчеты нужно так долго ждать?)
Тут оптимальным решением я вижу для обслуживания отчетов создание общего модуля со включенной опцией повторно использования возвращаемых значений. В таком общем модуле требуется реализовать все требуемые функции с быстрой конкатенации требуемых представлений (на мою устаревшую статью не смотрите - сейчас для быстрого сложения строк существует функция СтрСоединить()). А в схеме компоновки можно просто вызывать эти новые функции. Думаю, что получился бы довольно приличный прирост скорости на больших объемах информации.
Так что же привело к ошибке?
Остался вопрос: что же было такого необычного в той строке отчета, которая "ломала" СКД?
Анализ показал, что это была корреспонденция анализируемого счета со счетом затрат будущего периода, куда было добавлено второе субконто "Период" с типом Дата. Полагаю, что данное субконто добавили мои предшественники-разработчики по просьбе бухгалтеров, которым для контроля было неудобно использовать строковые представления дат, с которыми в отличии от настоящих дат нельзя ни отборы применить, ни условным оформление воспользоваться.
Этот же факт объясняет почему экземпляр данного отчета, который успешно работает в сотнях тысяч инсталяций типовых и отраслевых решений по всему СНГ, у моих клиентов внезапно сломался. Дело в том, что разработчики типовых в плане видов характеристик ВидыСубконтоХозрасчетные стандартно разрешили только ссылочные типы и ни одного из примитивных! Если кому-то нужны даты, (видимо думали они) то пусть заводят их строковые представления в справочнике Субконто. Хотя несколько лет применения такого субконто показало, что вообще никаких негативных последствий для системы это не несет.
Особенности функции Представление()
Почему же функция Представление() не вернула строку? Без чтения документации тут некуда.
Согласно данным справочной информации эта функция существует самостоятельно в контексте (а не как метод объекта) лишь в двух местах - в языке запросов и как встроенная функция системы компоновки данных.
Давайте освежим знания по языку запросов и посмотрим статью во встроенной документации платформы:
Функция ПРЕДСТАВЛЕНИЕ
Данная функция предназначена для получения строкового представления значения произвольного типа.
Параметр функции – выражение любого типа.
Возвращаемое значение – представление значения, тип СТРОКА.
Результат работы функции не может быть использован внутри других функций, за исключением функции ПРЕДСТАВЛЕНИЕ.
Далее обратимся на ИТС, где поиском я нашел лишь одну релевантную статью на эту тему: Особенности работы с полем Представление и функцией Представление() языка запросов из раздела "1С:Предприятия 8. Разработка и администрирование":
Функция Представление()
Функция представление предназначена для получения текстового представления любого значения, которое может быть получено при помощи языка запросов. Функция Представление() работает как для ссылочных, так и для примитивных типов. Для ссылочных типов результат функции полностью аналогичен получению поля "Представление" от ссылки, переданной в качестве параметра функции. Для примитивных типов, результатом работы функции является строка, в которую было преобразовано значение, переданное в качестве параметра. Особенностью данной функции является то, что ее результат не может быть использован в выражении.
Таким образом в двух источниках видим практически одно и тоже, но разными словами: результат всегда строка и результат нельзя использовать в выражениях.
Теперь обратимся к реализации в функциях СКД и посмотрим статью "Функции языка выражений системы компоновки данных" из встроенной справки платформы (на ИТС аналогично):
Представление (Resentation)
Данная функция возвращает строковое представление переданного значения не примитивного типа. Для значений примитивного типа возвращает само значение.
Название у двух функция одно, а поведение такое разное... В варианте для СКД есть и разрешение использования в выражениях и тип возвращаемого значения уже может быть произвольным.
А нет ли ошибок еще и в платформе?
Но перепроверяя данные из документации я обнаружил, что функция Представление() имеет наглость возвращать значения разных типов даже при идентичном входном параметре. Причем такое неоднозначное поведение было зафиксировано как в версиях 8.2, так и в современных 8.3 - т.е. оно сопровождало СКД с самого начала.
Предлагаю немного попрактиковаться и создать внешний отчет, в котором будет 2 источника описанных запросами. В обоих запросах выберем предопределенные дату и число. Единственное различие будет в том, что во втором варианте мы кроме значений дополнительно запросим еще представления этих полей:
Далее на закладке вычисляемых полей создадим набор полей с типами значений как наших значений, так и их представлений, которые получим уже встроенной функцией схемы компоновки данных:
Теперь создадим вариант настройки с табличками отдельно по первому источнику (в котором только данные) и по второму источнику (где мы дополнительно запрашивали представления):
А теперь посмотрим результат:
Получившийся отчет для платформы 8.3 я поместил во вложение. Если (внезапно) захотите проверить на платформе 8.2, то вам необходимо переделать вычисляемые поля, так как в те времена в СКД еще не было встроенной функции ТипЗначения(). Можно или вызвать свою собственную функцию из общего модуля или не заморачиваться и вывести в отчет сам результат Представление() - результаты для двух запросов будут снова разных типов.
Согласитесь - ведь получился забавный сайд-эффект. (Просто обхохочешься). Я в своем отчете для полноты эксперимента даже проверяю типы изначальных полей, по которым рассчитывается Представление(), что бы убедится в отсутствии подмены и в использовании абсолютно идентичных входных данных.
Так почему же результаты различаются? Точно не понятно, или вызов представления в запросе заполняет кеш системы компоновки некоторыми значениями, которые подхватываются одноименной функцией схемы (все таки у них одинаковая сигнатура), или это особенности анализа текста запроса (типа как с периодичностью регистров накопления, где если выбрать Авто и группировку скажем по дням, то начальное и конечное сальдо в группировках будет корректным, но если нигде ничего не меняя исправить в тексте запроса периодичность на День, то отчет сразу поломается и роли полей-остатков станут игнорироваться, они превратятся в обычные поля с обычным суммированием). В документации про такую особенность использования (и тем более про ее логику) нет ни слова. Абсолютно не понятно хотят ли разработчики оставить эту "фичу" навсегда или передумают и уберут в одном из свежих релизов. Но нам, "простым пацанам", помнить о таких особенностях точно нужно, что бы не забывать покрывать код дополнительными проверками типов - на всякий случай.