В практике разработки на платформе 1С:Предприятие очень часто возникает задача анализа взаиморасчетов. Одна из классических задач, которая регулярно ставит в тупик начинающих разработчиков — поиск договоров с контрагентами, баланс (остаток взаиморасчетов) по которым равен нулю. Казалось бы, задача тривиальная, но при попытке решить ее «в лоб» через обращение к регистру накопления разработчики сталкиваются с тем, что система не возвращает нужные данные.
В рамках этой статьи мы подробно разберем, почему возникает эта сложность, как устроены регистры накопления в 1С, и шаг за шагом спроектируем оптимальный, быстродействующий запрос, решающий эту задачу с учетом всех нюансов реального учета.
Прежде чем переходить к написанию кода, давайте разберем фундаментальный механизм работы платформы 1С. Физические таблицы остатков регистров накопления в СУБД (будь то MS SQL Server, PostgreSQL или файловая база) спроектированы по принципу исключения избыточности.
Когда приходные и расходные движения по определенному измерению (в нашем случае — по конкретному договору) полностью закрывают друг друга, итоговый остаток становится равен нулю. В этот момент СУБД удаляет запись об этом остатке из таблицы итогов. Платформа 1С физически не хранит записи с нулевыми остатками.
Поэтому простой запрос к виртуальной таблице остатков регистра с условием СуммаОстаток = 0 всегда будет возвращать пустой результат. База данных просто не найдет строк, соответствующих такому условию.
Чтобы обойти это ограничение, нам необходимо использовать комбинацию справочника договоров и таблицы остатков регистра накопления. Мы должны пойти «от обратного»:
NULL, что в нашей бизнес-логике означает отсутствие долга, или «ноль»).Связующим звеном здесь выступает операция ЛЕВОГО СОЕДИНЕНИЯ. При левом соединении в результат запроса обязательно попадут абсолютно все записи из левой таблицы (справочник договоров), даже если для них нет соответствующих записей в правой таблице (виртуальная таблица остатков регистра). Для таких несовпадений поля правой таблицы заполняются значением NULL.
Давайте посмотрим на базовый пример реализации такого запроса:
ВЫБРАТЬ
Договоры.Ссылка КАК Договор,
ВзаиморасчетыОстатки.СуммаОстаток КАК СуммаОстаток
ИЗ
Справочник.ДоговорыКонтрагентов КАК Договоры
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ВзаиморасчетыСКонтрагентами.Остатки КАК ВзаиморасчетыОстатки
ПО Договоры.Ссылка = ВзаиморасчетыОстатки.Договор
ГДЕ
Договоры.Владелец = &Контрагент
И ВзаиморасчетыОстатки.СуммаОстаток ЕСТЬ NULL
В данном примере условие ВзаиморасчетыОстатки.СуммаОстаток ЕСТЬ NULL является ключевым. Оно отсекает все договоры, по которым в регистре зафиксирован какой-либо остаток (будь то дебиторская или кредиторская задолженность), оставляя только те договоры, по которым остатка нет вообще.
Представленный выше запрос будет отлично работать на маленькой тестовой базе. Однако на реальной рабочей базе, где справочник договоров содержит десятки тысяч элементов, а регистр накопления — миллионы записей, такой запрос может вызвать серьезные проблемы с производительностью (вплоть до зависания системы или превышения времени ожидания СУБД).
Почему это происходит? Дело в том, что если мы пишем условия фильтрации в секции ГДЕ, СУБД вынуждена сначала рассчитать остатки по ВСЕМ договорам ВСЕХ контрагентов в информационной базе, выполнить полное левое соединение двух огромных таблиц, и только на самом последнем этапе отсечь лишние записи по конкретному контрагенту.
Чтобы избежать этой критической ошибки, мы обязаны передавать параметры отбора непосредственно в параметры виртуальной таблицы остатков. В этом случае платформа сформирует запрос к СУБД таким образом, что остатки будут рассчитываться только по конкретному контрагенту (или по конкретному договору), что ускорит выполнение операции в сотни раз.
Проанализируем оптимизированный вариант запроса:
ВЫБРАТЬ
Договоры.Ссылка КАК Договор
ИЗ
Справочник.ДоговорыКонтрагентов КАК Договоры
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ВзаиморасчетыСКонтрагентами.Остатки(
&Период,
Договор.Владелец = &Контрагент
) КАК ВзаиморасчетыОстатки
ПО Договоры.Ссылка = ВзаиморасчетыОстатки.Договор
ГДЕ
Договоры.Владелец = &Контрагент
И ВзаиморасчетыОстатки.СуммаОстаток ЕСТЬ NULL
Обратите внимание: мы передали условие отбора по контрагенту Договор.Владелец = &Контрагент внутрь параметров виртуальной таблицы остатков. Также мы продублировали отбор Договоры.Владелец = &Контрагент в секции ГДЕ для левой таблицы справочника, чтобы выбрать только договоры нужного нам партнера.
При использовании базового алгоритма левого соединения в результирующую выборку попадут две принципиально разные категории договоров:
Для целей реального бизнеса (например, при проведении инвентаризации или для автоматического закрытия неиспользуемых договоров) нам часто требуются только те договоры, по которым велась реальная хозяйственная деятельность, но сейчас задолженность отсутствует.
Для решения этой задачи нам потребуется дополнительно проанализировать обороты по договорам. Для этого мы подключим к запросу виртуальную таблицу Обороты регистра накопления. Если по договору были обороты (сумма оборотов больше нуля), а конечный остаток при этом равен NULL — значит, это «закрытый» в ноль договор.
Рассмотрим структуру такого запроса:
ВЫБРАТЬ
Договоры.Ссылка КАК Договор
ИЗ
Справочник.ДоговорыКонтрагентов КАК Договоры
// Присоединяем обороты, чтобы понять, велась ли деятельность по договору
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрНакопления.ВзаиморасчетыСКонтрагентами.Обороты(
,
,
Авто,
Договор.Владелец = &Контрагент
) КАК ВзаиморасчетыОбороты
ПО Договоры.Ссылка = ВзаиморасчетыОбороты.Договор
// Присоединяем остатки, чтобы отсечь те договоры, по которым есть долг
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ВзаиморасчетыСКонтрагентами.Остатки(
&Период,
Договор.Владелец = &Контрагент
) КАК ВзаиморасчетыОстатки
ПО Договоры.Ссылка = ВзаиморасчетыОстатки.Договор
ГДЕ
Договоры.Владелец = &Контрагент
И ВзаиморасчетыОстатки.СуммаОстаток ЕСТЬ NULL
Использование ВНУТРЕННЕГО СОЕДИНЕНИЯ с таблицей оборотов автоматически отсекает все новые, «пустые» договоры, по которым никогда не было движений, оставляя в выборке только реально отработавшие, но закрытые на текущий момент соглашения.
Еще одна ловушка, в которую часто попадают разработчики — это игнорирование аналитики взаиморасчетов. В современных конфигурациях 1С (таких как ERP, Комплексная автоматизация или Управление торговлей) учет взаиморасчетов ведется не просто по договорам, а в разрезе дополнительных аналитик: «Заказ», «Накладная» или «Документ расчетов».
Представим ситуацию: по договору у нас есть два расчетных документа. По первому документу (например, авансовый платеж покупателя) висит наш долг в размере 50 000 рублей. По второму документу (отгрузка товара) покупатель должен нам те же 50 000 рублей.
Если мы снимем остатки по договору в целом (в «свернутом» виде), баланс будет равен нулю. Но фактически взаиморасчеты не закрыты! Нам необходимо произвести операцию зачета аванса. Если мы ошибочно посчитаем такой договор закрытым, это приведет к серьезным ошибкам в бухгалтерском и налоговом учете (например, неверно рассчитается НДС с авансов).
Чтобы найти договоры с истинным, «чистым» нулевым балансом, мы должны проверять отсутствие остатков по каждой аналитике внутри договора. Договор считается закрытым только тогда, когда остатки равны нулю по каждому расчетному документу в отдельности.
Посмотрим на пример запроса, который учитывает детальную аналитику расчетов:
ВЫБРАТЬ
Договоры.Ссылка КАК Договор
ИЗ
Справочник.ДоговорыКонтрагентов КАК Договоры
ГДЕ
Договоры.Владелец = &Контрагент
И НЕ Договоры.Ссылка В (
// Вложенным запросом выбираем договоры, по которым есть ХОТЯ БЫ ОДИН ненулевой остаток
// по любому расчетному документу (развернутое сальдо)
ВЫБРАТЬ РАЗЛИЧНЫЕ
ОстаткиДетальные.Договор
ИЗ
РегистрНакопления.ВзаиморасчетыСКонтрагентами.Остатки(
&Период,
Договор.Владелец = &Контрагент
) КАК ОстаткиДетальные
ГДЕ
ОстаткиДетальные.СуммаОстаток <> 0
)
В данном запросе мы использовали конструкцию НЕ Договоры.Ссылка В (...) со вложенным запросом. Вложенный запрос находит все договоры, по которым есть хоть какая-то задолженность на детальном уровне аналитики. Внешний запрос исключает эти договоры из общего списка договоров контрагента, гарантируя, что мы получим только договоры со стопроцентно закрытыми взаиморасчетами.
При выборке данных для реальных бизнес-задач (например, для вывода списка договоров в форму выбора нового документа) разработчики часто забывают о служебных реквизитах справочника. Обязательно учитывайте следующие параметры:
Договоры.ПометкаУдаления = ЛОЖЬ).Если наш запрос выполняется в рамках сложного отчета или является частью тяжелой транзакции проведения документа, использование левых соединений «на лету» с виртуальными таблицами может вызвать неоптимальный план запроса в СУБД. Профессиональным стандартом разработки на платформе 1С является разделение запроса на пакеты с использованием Временных таблиц.
Давайте разберем, как реализовать этот алгоритм максимально эффективно:
// ПАКЕТ 1: Выбираем все действующие договоры контрагента во временную таблицу
ВЫБРАТЬ
Договоры.Ссылка КАК Договор
ПОМЕСТИТЬ ВТ_ДоговорыКонтрагента
ИЗ
Справочник.ДоговорыКонтрагентов КАК Договоры
ГДЕ
Договоры.Владелец = &Контрагент
И Договоры.ПометкаУдаления = ЛОЖЬ
;
// ПАКЕТ 2: Выбираем договоры, по которым есть действующие остатки
ВЫБРАТЬ РАЗЛИЧНЫЕ
ВзаиморасчетыОстатки.Договор КАК Договор
ПОМЕСТИТЬ ВТ_ДоговорыСОстатками
ИЗ
РегистрНакопления.ВзаиморасчетыСКонтрагентами.Остатки(
&Период,
Договор.Владелец = &Контрагент
) КАК ВзаиморасчетыОстатки
;
// ПАКЕТ 3: Соединяем две временные таблицы и получаем итоговый результат
ВЫБРАТЬ
ВТ_Договоры.Договор КАК Договор
ИЗ
ВТ_ДоговорыКонтрагента КАК ВТ_Договоры
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ДоговорыСОстатками КАК ВТ_Остатки
ПО ВТ_Договоры.Договор = ВТ_Остатки.Договор
ГДЕ
ВТ_Остатки.Договор ЕСТЬ NULL
Такой подход гарантирует:
Мы подробно разобрали задачу поиска договоров с нулевым балансом взаиморасчетов. Как мы выяснили, ключевое правило при работе с регистрами накопления — помнить, что они не хранят нулевые итоги физически. Использование левого соединения справочника с виртуальной таблицей остатков при обязательной фильтрации в параметрах этой таблицы — залог создания быстрого, стабильного и правильного алгоритма в 1С.