Как получить и объединить информацию из двух регистров в 1С 7.7

Программист 1С v7.7 Управленческий учет IT и автоматизация бизнеса
← На главную

Разработчики, приходящие в 1С 7.7 из сред разработки 1С 8.х, часто сталкиваются с серьезным ограничением: во встроенном языке запросов «семерки» отсутствуют привычные операторы соединения таблиц, такие как LEFT JOIN, RIGHT JOIN или UNION. Из-за этого стандартная задача — например, получить остатки товаров из одного регистра и связать их со стоимостью или ценами из другого регистра (партионного учета) — превращается в настоящую техническую проблему.

В данной статье мы подробно разберем, почему возникают ошибки при попытке решить эту задачу «в лоб», проанализируем некорректный код и рассмотрим три надежных, проверенных временем способа объединения данных из разных регистров в 1С 7.7.

Разбор критических ошибок при работе с запросами и Таблицами Значений

Для начала проанализируем типичный ошибочный код, который часто пишут начинающие разработчики (подобный пример был представлен в сообщении 21 обсуждения). Понимание этих ошибок убережет вас от неверных архитектурных решений.


// ПРИМЕР НЕКОРРЕКТНОГО КОДА!
ТекстЗапроса = "
|Период с ДатаНач по ДатаКон;
|Товар = Регистр.ОстаткиТоваров.Товар;
|ОстатокТовара = Регистр.ОстаткиТоваров.ОстатокТовара;
|Стоимость = Регистр.ПартииТоваров.Стоимость;
|Функция ОстатокТовараКонОст = КонОст(ОстатокТовара);
|Группировка Товар;
|";

ТЗ = СоздатьОбъект("ТаблицаЗначений");
Запрос.Выгрузить(ТЗ, 1, 0);

ТЗ.НоваяКолонка("Товар");
ТЗ.НоваяКолонка("Стоимость");

ТЗ.ВыбратьСтроки();
Пока ТЗ.ПолучитьСтроку() = 1 Цикл
    ТЗ.НоваяСтрока();
    ТЗ.Товар = Запрос.Товар;
    ТЗ.Стоимость = Запрос.Стоимость;
КонецЦикла;

ТЗ.Свернуть("Товар", "Стоимость");
Сумма = ТЗ.Итог("Стоимость");

Этот код содержит сразу несколько грубейших логических и синтаксических ошибок, из-за которых результат сложения всегда будет равен нулю либо программа уйдет в бесконечный цикл:

  1. Некорректная связь в самом запросе. Попытка просто объявить переменные из разных регистров (ОстаткиТоваров и ПартииТоваров) внутри одного запроса без указания связи приведет к тому, что 1С 7.7 не сможет сопоставить эти данные. На уровне СУБД произойдет декартово произведение, либо (что чаще для 1С 7.7) данные по второму регистру просто не будут выбраны.
  2. Дублирование колонок после выгрузки. Метод Запрос.Выгрузить(ТЗ, 1, 0) автоматически создает в Таблице Значений (ТЗ) структуру колонок, соответствующую группировкам и функциям запроса. Команды ТЗ.НоваяКолонка(), вызванные сразу после этого, пытаются создать новые колонки с уже существующими именами. Платформа либо выдаст ошибку, либо создаст дублирующие колонки вида «Товар1», что нарушит дальнейшую логику работы.
  3. Разрушение цикла обхода ТЗ. Метод ТЗ.НоваяСтрока() внутри цикла Пока ТЗ.ПолучитьСтроку() = 1 Цикл добавляет новую строку в конец таблицы при каждой итерации. Это классическая ошибка, которая приводит к лавинообразному увеличению размера таблицы прямо во время обхода и вызывает бесконечный цикл.
  4. Обращение к позиционированию запроса вне его цикла. Попытка присвоить значение через конструкцию ТЗ.Товар = Запрос.Товар внутри обхода Таблицы Значений не имеет смысла. Объект Запрос позиционируется на конкретную запись только внутри своего собственного цикла Пока Запрос.Группировка() = 1 Цикл. Вне этого цикла свойства запроса возвращают пустые значения.

Теперь рассмотрим, как решить задачу объединения данных правильно.

Способ А. Объединение измерений в одном запросе (через запятую)

Штатный конструктор запросов 1С 7.7 позволяет связать два регистра в рамках одного запроса, если у них есть общие измерения (например, Товар и Склад). Для этого переменные запроса необходимо описать через запятую, указав пути к соответствующим измерениям обоих регистров.

Рассмотрим пример правильного построения такого запроса:


Запрос = СоздатьОбъект("Запрос");
ТекстЗапроса = "
|Период с ДатаНач по ДатаКон;
|Товар = Регистр.ОстаткиТоваров.Товар, Регистр.ПартииТоваров.Товар;
|Склад = Регистр.ОстаткиТоваров.Склад, Регистр.ПартииТоваров.Склад;
|Остаток = Регистр.ОстаткиТоваров.ОстатокТовара;
|Стоимость = Регистр.ПартииТоваров.Стоимость;
|Функция КонОстТовара = КонОст(Остаток);
|Функция СуммаТовара = Расход(Стоимость); 
|Группировка Склад;
|Группировка Товар Все; // Ключевое слово 'Все' критически важно!
|";

Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
    Предупреждение("Ошибка выполнения запроса!");
    Возврат;
КонецЕсли;

Важный нюанс: Обратите внимание на использование ключевого слова Все в группировке Группировка Товар Все;. Если его не указать, то запрос сработает по принципу INNER JOIN (в выборку попадут только те товары, которые одновременно присутствуют и в регистре остатков, и в регистре партий). Добавление слова Все заставляет систему работать по аналогии с FULL OUTER JOIN, выводя полную номенклатуру, даже если по какому-то регистру данные отсутствуют.

Способ Б. Слияние двух Таблиц Значений через метод «Свернуть» (Рекомендуемый способ)

Если структура регистров слишком сильно различается или использование одного сложного запроса приводит к падению производительности, самым надежным решением будет использование двух простых независимых запросов. Полученные результаты выгружаются в две отдельные ТЗ, которые затем программно объединяются и сворачиваются.

Этот способ работает очень быстро, так как встроенный метод Свернуть() в 1С 7.7 реализован на уровне платформы на языке C++ и оптимизирован по скорости.

Разберем этот алгоритм по шагам:

Шаг 1. Получаем остатки товаров


ЗапросОст = СоздатьОбъект("Запрос");
ТекстОст = "
|Период с ДатаНач по ДатаКон;
|Товар = Регистр.ОстаткиТоваров.Товар;
|Склад = Регистр.ОстаткиТоваров.Склад;
|Остаток = Регистр.ОстаткиТоваров.ОстатокТовара;
|Функция КонОстТовара = КонОст(Остаток);
|Группировка Склад;
|Группировка Товар Без Групп;
|";

ЗапросОст.Выполнить(ТекстОст);
ТЗ_Остатки = СоздатьОбъект("ТаблицаЗначений");
// Выгружаем результаты запроса в ТЗ_Остатки
ЗапросОст.Выгрузить(ТЗ_Остатки, 1, 0); 
// Переименуем колонку функции для удобства дальнейшего слияния
ТЗ_Остатки.УстановитьПараметрыКолонки("КонОстТовара", , , , "Количество");
// Добавляем пустую колонку под будущую стоимость
ТЗ_Остатки.НоваяКолонка("Стоимость", "Число", 15, 2);

Шаг 2. Получаем стоимостные показатели партий


ЗапросПарт = СоздатьОбъект("Запрос");
ТекстПарт = "
|Период с ДатаНач по ДатаКон;
|Товар = Регистр.ПартииТоваров.Товар;
|Склад = Регистр.ПартииТоваров.Склад;
|Стоимость = Регистр.ПартииТоваров.Стоимость;
|Функция СтоимостьТовара = КонОст(Стоимость);
|Группировка Склад;
|Группировка Товар Без Групп;
|";

ЗапросПарт.Выполнить(ТекстПарт);
ТЗ_Партии = СоздатьОбъект("ТаблицаЗначений");
ЗапросПарт.Выгрузить(ТЗ_Партии, 1, 0);
ТЗ_Партии.УстановитьПараметрыКолонки("СтоимостьТовара", , , , "Стоимость");
// Добавляем пустую колонку под количество для идентичности структуры таблиц
ТЗ_Партии.НоваяКолонка("Количество", "Число", 15, 3);

Шаг 3. Объединение таблиц значений

Теперь перенесем все строки из второй таблицы в конец первой таблицы. Нам не нужно искать соответствия на данном этапе — мы просто копируем строки.


ТЗ_Партии.ВыбратьСтроки();
Пока ТЗ_Партии.ПолучитьСтроку() = 1 Цикл
    ТЗ_Остатки.НоваяСтрока();
    ТЗ_Остатки.Склад = ТЗ_Партии.Склад;
    ТЗ_Остатки.Товар = ТЗ_Партии.Товар;
    ТЗ_Остатки.Стоимость = ТЗ_Партии.Стоимость;
    // Колонка 'Количество' для этих строк останется равной 0
КонецЦикла;

Шаг 4. Свертка объединенной таблицы

На данном этапе в нашей таблице ТЗ_Остатки присутствуют дублирующиеся строки для пар «Склад - Товар» (одна строка содержит количество, а вторая — стоимость). Применим метод группировки:


// Сворачиваем строки по Складу и Товару, суммируя Количество и Стоимость
ТЗ_Остатки.Свернуть("Склад, Товар", "Количество, Стоимость");

В результате мы получаем идеально объединенную таблицу значений, где для каждого товара на каждом складе выведены и количество, и общая стоимость. Теперь эту таблицу можно легко обойти циклом и вывести в печатную форму отчета.

Способ В. Использование прямого SQL-запроса через класс ODBCRecordset (1С++)

Если база данных 1С 7.7 работает под управлением MS SQL Server, то использование штатных запросов на больших объемах данных может приводить к серьезным задержкам. Профессиональные разработчики для решения таких задач используют внешнюю компоненту 1С++, которая позволяет выполнять прямые SQL-запросы к таблицам СУБД.

В этом случае задача соединения двух регистров решается на уровне SQL-сервера с помощью классического соединения FULL OUTER JOIN. Это работает в десятки и сотни раз быстрее штатных механизмов платформы.

Проанализируем пример кода с прямым SQL-запросом:


БазаДанных = СоздатьОбъект("ODBCRecordset");
ТекстЗапроса = "
|SELECT 
|    isnull(RegOst.Tovar, RegPart.Tovar) as [Товар $Справочник.Номенклатура],
|    isnull(RegOst.Sklad, RegPart.Sklad) as [Склад $Справочник.Склады],
|    isnull(RegOst.Ostatok, 0) as Количество,
|    isnull(RegPart.Stoimost, 0) as Стоимость
|FROM 
|    (SELECT 
|         SP$Tovar as Tovar, 
|         SP$Sklad as Sklad, 
|         SP$OstatokTovara as Ostatok 
|     FROM $РегистрОстатки.ОстаткиТоваров) RegOst
|FULL OUTER JOIN 
|    (SELECT 
|         SP$Tovar as Tovar, 
|         SP$Sklad as Sklad, 
|         SP$Стоимость as Stoimost 
|     FROM $РегистрОстатки.ПартииТоваров) RegPart 
|ON RegOst.Tovar = RegPart.Tovar AND RegOst.Sklad = RegPart.Sklad
|";

ТЗ_Результат = БазаДанных.ВыполнитьИнструкцию(ТекстЗапроса);

Использование этого метода требует наличия установленной компоненты 1cpp.dll в каталоге программы и базовых знаний синтаксиса T-SQL, однако это незаменимый инструмент для оптимизации высоконагруженных систем на платформе 1С 7.7.

Резюме

Мы подробно разобрали, как решить проблему объединения данных из двух регистров в 1С 7.7. Для простых и средних отчетов оптимальным выбором будет Способ Б (использование двух запросов и последующее слияние таблиц через метод Свернуть()). Он гарантирует стабильность работы, отсутствие логических ошибок со связями в запросе и простоту отладки кода.

← На главную