При разработке отчетов на системе компоновки данных (СКД) разработчики часто сталкиваются с ситуацией, когда формулы вычисления процентов, рентабельности или наценки в тексте запроса возвращают совершенно некорректные («астрономические») результаты. При этом круглые скобки, расставленные вручную в тексте запроса, автоматически удаляются конструктором запросов платформы.
Разберем причины такого поведения платформы 1С, выясним математические нюансы работы конструктора запросов и рассмотрим правильные архитектурные способы расчета процентных показателей в СКД.
В языке запросов 1С операции умножения (*) и деления (/) имеют одинаковый приоритет и выполняются строго слева направо. Если требуется вычислить процент от величины, представляющей собой произведение двух полей (например, Количество * Цена), математическая формула выглядит так:
Числитель / (Количество * Цена) * 100
Однако при открытии и последующем закрытии графического конструктора запросов встроенный синтаксический анализатор 1С оптимизирует и нормализует синтаксическое дерево. Он считает внешние скобки в выражении ... / (Поле1 * Поле2) * 100 избыточными и удаляет их. В результате в запросе сохраняется выражение:
Числитель / Поле1 * Поле2 * 100
По правилам СУБД такое выражение вычисляется как ((Числитель / Поле1) * Поле2) * 100. Вместо деления на итоговую сумму числитель делится на количество, а затем умножается на цену и на 100, что и приводит к появлению огромных чисел.
Если расчет необходимо выполнить непосредственно в тексте запроса без изменения общей структуры, применим простое математическое правило: деление на произведение двух сомножителей эквивалентно последовательному делению на каждый из них.
Заменим деление на произведение выражением с последовательными знаками деления:
// Было (скобки удалялись конструктором):
(ПродажиОбороты.КоличествоОборот * ПродажиОбороты.ЦенаПродОборот - КассыОбороты.БалансКассыРасход - ПродажиОбороты.КоличествоОборот * ПродажиОбороты.ЦенаЗакупОборот) / (ПродажиОбороты.КоличествоОборот * ПродажиОбороты.ЦенаПродОборот) * 100 КАК ВПрибыль
// Стало (конструктор не ломает логику):
(ПродажиОбороты.КоличествоОборот * ПродажиОбороты.ЦенаПродОборот - КассыОбороты.БалансКассыРасход - ПродажиОбороты.КоличествоОборот * ПродажиОбороты.ЦенаЗакупОборот) / ПродажиОбороты.КоличествоОборот / ПродажиОбороты.ЦенаПродОборот * 100 КАК ВПрибыль
Такую запись конструктор запросов не видоизменяет, а порядок вычисления строго слева направо гарантирует математическую корректность операции.
Расчет относительных величин (процентов, долей, наценок) непосредственно в тексте запроса является частой архитектурной ошибкой при работе с СКД. Дело в том, что проценты нельзя суммировать функцией СУММА() в группировках и общих итогах отчета (сумма процентов по строкам не равна общему проценту по отчету).
Разберем по шагам правильный алгоритм формирования отчета в СКД:
Прибыль = СуммаПродаж - РасходДол - СуммаЗакупки
Выбор
Когда Сумма(СумаПродаж) = 0 Тогда 0
Иначе (Сумма(Прибыль) / Сумма(СумаПродаж)) * 100
Конец
При таком подходе СКД сначала корректно рассчитает сумму прибыли и сумму продаж на каждом уровне группировки отчета (включая общий итог), а затем вычислит правильный процент для каждой строки.
Рассмотрим еще одну распространенную причину завышения итоговых сумм в отчете — некорректное соединение оборотных регистров и таблиц документов в одном запросе.
Когда в запросе связываются РегистрНакопления.Кассы.Обороты, РегистрНакопления.Продажи.Обороты и Документ.РасходнойДокумент через ЛЕВОЕ СОЕДИНЕНИЕ по неполным ключам (например, только по дате или складу), происходит декартово умножение строк. Строка продаж начинает дублироваться столько раз, сколько документов расхода найдено за этот день.
Для исключения этой проблемы применим один из двух подходов:
ОБЪЕДИНИТЬ ВСЕ или связываем по ключевым полям.При выполнении любых арифметических операций в запросах и СКД всегда помним о следующих правилах платформы 1С:
ЛЕВОЕ СОЕДИНЕНИЕ отсутствующие данные возвращают значение NULL. Любая операция с NULL (сложение, вычитание, умножение) возвращает NULL. Всегда оборачиваем числовые поля соединений в функцию ЕСТЬNULL(Поле, 0).
ВЫБОР
КОГДА ПродажиОбороты.КоличествоОборот * ПродажиОбороты.ЦенаПродОборот = 0 ТОГДА 0
ИНАЧЕ Числитель / ПродажиОбороты.КоличествоОборот / ПродажиОбороты.ЦенаПродОборот * 100
КОНЕЦ