Как в 1С вычесть одну табличную часть из другой с учетом количества?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При решении прикладных задач в 1С разработчики часто сталкиваются с необходимостью вычислить разность двух наборов данных с учетом количественных показателей (например, сопоставление плана и факта, контроль отгрузки по заказу, сверка двух документов). Рассмотрим подробнее основные подходы к вычислению разности множеств: через объединение выборок в запросе, через полное соединение таблиц, а также средствами работы с объектом ТаблицаЗначений в оперативной памяти.

Вариант 1. Запрос с объединением и инверсией знака (рекомендуемый способ)

Разберем по шагам наиболее надежный и производительный алгоритм на языке запросов. Идея заключается в том, чтобы выбрать данные первого документа с положительным знаком количества, а данные второго документа — с отрицательным знаком (умножив на -1). После объединения обеих выборок через ОБЪЕДИНИТЬ ВСЕ выполняется группировка по номенклатуре и суммирование количества.

Посмотрим на пример текста запроса:


ВЫБРАТЬ
    Док1.Номенклатура КАК Номенклатура,
    Док1.Количество КАК Количество
ПОМЕСТИТЬ ВТ_ОбъединенныеДанные
ИЗ
    Документ.РасходнаяНакладная.Запасы КАК Док1
ГДЕ
    Док1.Ссылка = &Ссылка1

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    Док2.Номенклатура,
    -Док2.Количество
ИЗ
    Документ.РасходнаяНакладная.Запасы КАК Док2
ГДЕ
    Док2.Ссылка = &Ссылка2
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВТ.Номенклатура КАК Номенклатура,
    СУММА(ВТ.Количество) КАК РазницаКоличества
ИЗ
    ВТ_ОбъединенныеДанные КАК ВТ
СГРУППИРОВАТЬ ПО
    ВТ.Номенклатура
ИМЕЮЩИЕ
    СУММА(ВТ.Количество) > 0

Проанализируем важные нюансы этого подхода:

  1. Корректность при дублях строк: Если в документе одна и та же номенклатура повторяется в нескольких строках, объединение с последующим суммированием автоматически свернет такие позиции без риска декартова перемножения строк.
  2. Фильтрация нулевых и отрицательных значений: Фильтрация выполняется в секции ИМЕЮЩИЕ, так как она применяется к результатам агрегатных функций (СУММА) после группировки, в отличие от секции ГДЕ.

Вариант 2. Запрос с полным соединением таблиц

Выясним причину, почему альтернативный способ через соединение таблиц требует особого внимания. Если позиция присутствует во втором документе, но отсутствует в первом, обычное левое соединение ее проигнорирует. Чтобы учесть все строки из обоих документов, используется ПОЛНОЕ СОЕДИНЕНИЕ, а для выбора номенклатуры применяется функция ЕСТЬNULL.

Посмотрим на реализацию запроса с полным соединением:


ВЫБРАТЬ
    Док1.Номенклатура КАК Номенклатура,
    СУММА(Док1.Количество) КАК Количество
ПОМЕСТИТЬ ВТ_Документ1
ИЗ
    Документ.РасходнаяНакладная.Запасы КАК Док1
ГДЕ
    Док1.Ссылка = &Ссылка1
СГРУППИРОВАТЬ ПО
    Док1.Номенклатура
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    Док2.Номенклатура КАК Номенклатура,
    СУММА(Док2.Количество) КАК Количество
ПОМЕСТИТЬ ВТ_Документ2
ИЗ
    Документ.РасходнаяНакладная.Запасы КАК Док2
ГДЕ
    Док2.Ссылка = &Ссылка2
СГРУППИРОВАТЬ ПО
    Док2.Номенклатура
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ЕСТЬNULL(ВТ1.Номенклатура, ВТ2.Номенклатура) КАК Номенклатура,
    ЕСТЬNULL(ВТ1.Количество, 0) - ЕСТЬNULL(ВТ2.Количество, 0) КАК РазницаКоличества
ИЗ
    ВТ_Документ1 КАК ВТ1
        ПОЛНОЕ СОЕДИНЕНИЕ ВТ_Документ2 КАК ВТ2
        ПО ВТ1.Номенклатура = ВТ2.Номенклатура
ГДЕ
    ЕСТЬNULL(ВТ1.Количество, 0) - ЕСТЬNULL(ВТ2.Количество, 0) > 0

Важное предупреждение: Ни в коем случае нельзя накладывать отборы по ссылкам документов в секции ГДЕ при прямом полном соединении табличных частей (например, ГДЕ Таб1.Ссылка = &Ссылка1 И Таб2.Ссылка = &Ссылка2). При отсутствии строки в одной из таблиц соответствующее поле ссылки станет NULL, и условие в ГДЕ вернет Ложь, превратив полное соединение во внутреннее. Именно поэтому предварительная выгрузка во временные таблицы с группировкой является обязательной.

Вариант 3. Обработка данных в памяти с помощью метода Свернуть()

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

Разберем по шагам следующий алгоритм:

  1. Скопируем строки второй таблицы в первую, предварительно умножив их количество на -1.
  2. Вызовем метод Свернуть() по ключевым колонкам.
  3. Удалим строки, разность в которых равна нулю (или отрицательна, в зависимости от бизнес-логики).

Посмотрим на пример кода 1С:


// Получаем копию первой таблицы
ТЗРезультат = ТЗДокумент1.Скопировать();

// Добавляем строки второго документа с противоположным знаком
Для Каждого Стр Из ТЗДокумент2 Цикл
    НоваяСтр = ТЗРезультат.Добавить();
    НоваяСтр.Номенклатура = Стр.Номенклатура;
    НоваяСтр.Количество = -Стр.Количество;
КонецЦикла;

// Сворачиваем строки по номенклатуре и суммируем количество
ТЗРезультат.Свернуть("Номенклатура", "Количество");

// Удаляем нулевые и отрицательные остатки с конца таблицы
Индекс = ТЗРезультат.Количество() - 1;
Пока Индекс >= 0 Цикл
    Если ТЗРезультат[Индекс].Количество <= 0 Тогда
        ТЗРезультат.Удалить(Индекс);
    КонецЕсли;
    Индекс = Индекс - 1;
КонецЦикла;

Метод Свернуть() реализован на уровне платформы на C++, поэтому на небольших и средних объемах данных в оперативной памяти он отрабатывает в разы быстрее последовательного поиска строк кодом на встроенном языке.

← На главную