При разработке механизмов заполнения табличных частей документов в 1С программисты часто сталкиваются со странным поведением платформы: после применения стандартного метода Свернуть() числовые показатели (например, сумма зарплаты, выручки или скидки) могут увеличиться ровно в два, три или более раз. На первый взгляд код выглядит абсолютно корректным, а сама функция свертки работает строго по заложенным в нее алгоритмам. Однако итоговый результат оказывается неверным.
В этой статье мы подробно разберем, почему возникает этот эффект «мультипликации» данных, проанализируем взаимосвязь между выборками запроса и поведением табличных частей, а также рассмотрим два эффективных способа решения проблемы с примерами готового кода.
Рассмотрим типовой сценарий, с которым столкнулся автор темы. В документе ОтчетОРозничныхПродажах заполняется табличная часть Сотрудники на основании данных из чеков ККМ. Для этого используется цикл обхода выборки из запроса:
СотрудникТЧ = ОтчетОРозничныхПродажах.Сотрудники.Добавить();
СотрудникТЧ.Сотрудник = Выборка.СсылкаЧекККМ.ВыборСотрудник;
СотрудникТЧ.Продажа = Выборка.СсылкаЧекККМ;
СотрудникТЧ.СуммаЗП = Выборка.ЗП;
ТЗ = ОтчетОРозничныхПродажах.Сотрудники.Выгрузить();
ТЗ.Свернуть("Сотрудник,Продажа", "СуммаЗП");
ОтчетОРозничныхПродажах.Сотрудники.Загрузить(ТЗ);
После выполнения данного фрагмента кода по тем чекам, где было продано несколько позиций номенклатуры, сумма в колонке СуммаЗП удваивается (или умножается на количество строк товаров в чеке).
Выясним причину такого поведения. Метод Свернуть() группирует строки таблицы значений по указанным колонкам группировки (в нашем случае это Сотрудник и Продажа) и суммирует значения в колонках ресурсов (СуммаЗП). Если до момента вызова метода в табличной части уже присутствовали дублирующиеся строки с одинаковыми сотрудниками и чеками, они будут объединены, а их суммы сложатся.
Но откуда берутся дублирующиеся строки в табличной части перед сворачиванием? Источником проблемы всегда является исходный запрос, результат которого обходится в цикле.
Как справедливо заметил автор в сообщении №8, проблема возникает только по тем документам ЧекККМ, в которых продано больше одной номенклатуры. Это классическое следствие избыточного связывания таблиц в SQL-запросах 1С.
Если в тексте запроса происходит обращение к табличной части документа (например, к таблице Документ.ЧекККМ.Товары) или выполняется ЛЕВОЕ СОЕДИНЕНИЕ с ней, то количество строк в результате запроса для конкретного документа будет равно количеству строк в его табличной части «Товары».
Представим, что в Чеке №1 продано два товара: "Смартфон" и "Чехол". В самом документе значение реквизита шапки ЗП (Зарплата сотрудника за чек) составляет 100 рублей. Если запрос выбирает данные из табличной части товаров, то для Чека №1 он вернет две строки:
При обходе выборки в цикле программа послушно добавит в табличную часть Сотрудники две строки для Иванова по Чеку №1, в каждой из которых будет указана сумма 100 рублей. Суммарно в таблице окажется 200 рублей.
Затем вызывается метод ТЗ.Свернуть("Сотрудник,Продажа", "СуммаЗП"). Платформа видит две строки, где значения колонок Сотрудник ("Иванов") и Продажа (Чек №1) полностью совпадают. Она объединяет их в одну строку, а ресурс суммирует: 100 + 100 = 200. Сумма удвоилась. Если бы в чеке было 5 товаров, сумма бы выросла в 5 раз.
Для решения этой проблемы мы можем пойти двумя путями: исправить сам источник данных (запрос) или добавить проверку на уникальность при заполнении табличной части в коде 1С.
Самый правильный и наиболее производительный способ — не допускать появления дубликатов на уровне СУБД. Нам необходимо скорректировать запрос так, чтобы он возвращал ровно одну строку по каждому документу, независимо от количества номенклатурных позиций внутри него.
Если в запросе нет жесткой необходимости анализировать конкретные номенклатурные позиции (например, фильтровать по определенной группе товаров), мы должны выбирать данные напрямую из шапки документа, избегая обращений к его табличным частям:
ВЫБРАТЬ
ЧекККМ.Ссылка КАК СсылкаЧекККМ,
ЧекККМ.ВыборСотрудник КАК ВыборСотрудник,
ЧекККМ.ЗП КАК ЗП
ИЗ
Документ.ЧекККМ КАК ЧекККМ
ГДЕ
ЧекККМ.Проведен = ИСТИНА
И ЧекККМ.Дата МЕЖДУ &НачалоПериода И &КонецПериода
Если же обращение к табличной части Товары все-таки необходимо (например, нужно отобрать только те чеки, где проданы услуги, или рассчитать процент по конкретным товарным группам), обязательно используйте ключевое слово РАЗЛИЧНЫЕ или конструкцию СГРУППИРОВАТЬ ПО:
ВЫБРАТЬ РАЗЛИЧНЫЕ
ЧекТовары.Ссылка КАК СсылкаЧекККМ,
ЧекТовары.Ссылка.ВыборСотрудник КАК ВыборСотрудник,
ЧекТовары.Ссылка.ЗП КАК ЗП
ИЗ
Документ.ЧекККМ.Товары КАК ЧекТовары
ГДЕ
ЧекТовары.Ссылка.Проведен = ИСТИНА
Применение ключевого слова РАЗЛИЧНЫЕ (DISTINCT в SQL) гарантирует, что если в результате соединения образуются полностью идентичные строки по выбранным полям (Ссылка, Сотрудник, ЗП), то в итоговую выборку попадет только одна из них. Соответственно, при обходе выборки дубликаты добавлены не будут, и свертка отработает корректно.
Бывают ситуации, когда исходный запрос является частью сложного закрытого механизма конфигурации (например, частью общего модуля или типовой схемы компоновки данных), изменить который напрямую невозможно или нежелательно из-за требований к обновлению системы. В таком случае мы можем скорректировать алгоритм заполнения непосредственно в цикле обхода выборки.
Перед добавлением новой строки проверим с помощью метода НайтиСтроки(), не добавляли ли мы уже этот чек для данного сотрудника ранее. Если строка уже существует, мы просто проигнорируем повторное добавление.
Рассмотрим пример реализации такого алгоритма:
// Создаем структуру для быстрого поиска существующих записей
ПараметрыОтбора = Новый Структура;
Пока Выборка.Следующий() Цикл
// Подготавливаем параметры для поиска дубликатов
ПараметрыОтбора.Вставить("Сотрудник", Выборка.СсылкаЧекККМ.ВыборСотрудник);
ПараметрыОтбора.Вставить("Продажа", Выборка.СсылкаЧекККМ);
// Выполняем поиск в уже заполненной части таблицы
НайденныеСтроки = ОтчетОРозничныхПродажах.Сотрудники.НайтиСтроки(ПараметрыОтбора);
// Если таких строк еще нет, то добавляем новую запись
Если НайденныеСтроки.Количество() = 0 Тогда
СотрудникТЧ = ОтчетОРозничныхПродажах.Сотрудники.Добавить();
СотрудникТЧ.Сотрудник = Выборка.СсылкаЧекККМ.ВыборСотрудник;
СотрудникТЧ.Продажа = Выборка.СсылкаЧекККМ;
СотрудникТЧ.СуммаЗП = Выборка.ЗП;
КонецЕсли;
КонецЦикла;
// Теперь выгрузка и свертка отработают корректно без удваивания сумм
ТЗ = ОтчетОРозничныхПродажах.Сотрудники.Выгрузить();
ТЗ.Свернуть("Сотрудник,Продажа", "СуммаЗП");
ОтчетОРозничныхПродажах.Сотрудники.Загрузить(ТЗ);
Важное примечание по производительности: Использование метода НайтиСтроки() внутри большого цикла по тысячам записей может снизить скорость работы алгоритма. Если выборка содержит огромные объемы данных, эффективнее использовать объект Соответствие для кэширования уже обработанных уникальных ключей или использовать первый способ с оптимизацией самого запроса.
Удваивание сумм при свертке таблиц в 1С — это всегда симптом дублирования исходных строк в выборке. Помните следующие правила разработки, чтобы не допускать подобных ошибок:
РАЗЛИЧНЫЕ.Свернуть() анализируйте, какие именно данные попадают в таблицу на этапе ее заполнения.