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