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

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

В процессе разработки собственных конфигураций или доработки типовых решений на платформе «1С:Предприятие» программисты часто сталкиваются с задачей контроля взаиморасчетов. Одна из классических задач в этой области — получение просроченной задолженности в разрезе покупателей и документов расчетов. Однако при попытке написать первый запрос разработчики нередко совершают типичную архитектурную ошибку: они пытаются получить остатки долга, обращаясь напрямую к физической таблице движений регистра накопления.

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

Выясняем причину: почему исходный запрос работает некорректно?

Для начала давайте проанализируем запрос, который был представлен в первом сообщении темы:


ВЫБРАТЬ
    ВзаиморасчетыСПокупателями.Контрагент КАК Контрагент,
    ВзаиморасчетыСПокупателями.Счет КАК Счет,
    ВзаиморасчетыСПокупателями.Сумма КАК Сумма,
    ВзаиморасчетыСПокупателями.ДатаПлатежа КАК ДатаПлатежа
ИЗ
    РегистрНакопления.ВзаиморасчетыСПокупателями КАК ВзаиморасчетыСПокупателями
ГДЕ
    ВзаиморасчетыСПокупателями.ДатаПлатежа <> &ПустаяДата
    И ВзаиморасчетыСПокупателями.ДатаПлатежа < &Дата

На первый взгляд запрос выглядит логичным, но он содержит фундаментальную ошибку проектирования. Мы обращаемся к таблице РегистрНакопления.ВзаиморасчетыСПокупателями. Это физическая таблица движений. Она хранит абсолютно все записи, которые когда-либо были сделаны документами в системе: и отгрузки (приходные движения, увеличивающие долг), и оплаты (расходные движения, уменьшающие долг).

Рассмотрим, что происходит в базе данных на простом примере:

  1. Была произведена отгрузка по документу «Счет на оплату №1» на сумму 10 000 рублей. В регистр записалось движение «Приход» с суммой 10 000 руб. и заполненным реквизитом ДатаПлатежа (например, 15.10.2023).
  2. Через три дня покупатель оплатил этот счет в полном объеме. В регистр записалось движение «Расход» на сумму 10 000 руб. В документе оплаты реквизит ДатаПлатежа, как правило, не заполняется, так как факт оплаты уже свершился.

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

Для получения актуального долга необходимо использовать механизмы агрегации СУБД — то есть обращаться к виртуальной таблице остатков .Остатки(), которая автоматически «схлопывает» приходы и расходы в единый итоговый показатель по каждому измерению.

Но здесь мы сталкиваемся со второй проблемой: в виртуальной таблице остатков нет поля ДатаПлатежа. Почему так происходит? Дело в том, что ДатаПлатежа в исходном регистре создана как реквизит. Реквизиты регистров накопления предназначены исключительно для хранения справочной информации в конкретной строке движения. Они не участвуют в формировании итогов и не сохраняются в таблицах остатков. В итогах участвуют только измерения (в нашем случае это Контрагент и Счет).


Вариант решения №1. Перенос даты платежа в документ «Счет» (Рекомендуемый)

Если в вашей конфигурации измерение Счет — это ссылка на документ (например, ДокументСсылка.СчетНаОплату или ДокументСсылка.ЗаказПокупателя), то хранить дату платежа непосредственно в регистре накопления избыточно. Логичнее и правильнее с точки зрения архитектуры хранить этот реквизит в самом документе.

Поскольку документ Счет является измерением регистра, мы можем легко «дотянуться» до его реквизитов через точку прямо из виртуальной таблицы остатков. Давайте посмотрим, как будет выглядеть такой запрос:


ВЫБРАТЬ
    ВзаиморасчетыОстатки.Контрагент КАК Контрагент,
    ВзаиморасчетыОстатки.Счет КАК Счет,
    ВзаиморасчетыОстатки.СуммаОстаток КАК СуммаОстаток,
    ВзаиморасчетыОстатки.Счет.ДатаПлатежа КАК ДатаПлатежа
ИЗ
    РегистрНакопления.ВзаиморасчетыСПокупателями.Остатки(&МоментВремени, ) КАК ВзаиморасчетыОстатки
ГДЕ
    ВзаиморасчетыОстатки.СуммаОстаток > 0
    И ВзаиморасчетыОстатки.Счет.ДатаПлатежа <> &ПустаяДата
    И ВзаиморасчетыОстатки.Счет.ДатаПлатежа < &ДатаКонтроля

Разберем преимущества этого подхода:


Вариант решения №2. Изменение метаданных: делаем «Дату платежа» измерением

Бывают ситуации, когда по одному и тому же документу (например, по крупному договору или счету) оплата должна происходить частями в разные даты (например, 30% предоплаты до 10 числа, и 70% постоплаты до 25 числа). В этом случае дата платежа не привязана жестко к самому документу, а значит, решение из Варианта №1 нам не подходит.

Для реализации такого сценария нам необходимо изменить метаданные регистра накопления ВзаиморасчетыСПокупателями и перенести поле ДатаПлатежа из категории «Реквизиты» в категорию «Измерения».

Как только поле становится измерением, платформа начинает рассчитывать остатки взаиморасчетов в разрезе каждой конкретной даты платежа. Теперь мы можем использовать следующий запрос:


ВЫБРАТЬ
    ВзаиморасчетыОстатки.Контрагент КАК Контрагент,
    ВзаиморасчетыОстатки.Счет КАК Счет,
    ВзаиморасчетыОстатки.ДатаПлатежа КАК ДатаПлатежа,
    ВзаиморасчетыОстатки.СуммаОстаток КАК СуммаОстаток
ИЗ
    РегистрНакопления.ВзаиморасчетыСПокупателями.Остатки(
        &МоментВремени, 
        ДатаПлатежа <> &ПустаяДата И ДатаПлатежа < &ДатаКонтроля
    ) КАК ВзаиморасчетыОстатки
ГДЕ
    ВзаиморасчетыОстатки.СуммаОстаток > 0

Обратите внимание, что условия по дате платежа мы перенесли непосредственно в параметры виртуальной таблицы остатков. Это позволяет СУБД выполнить фильтрацию данных на самом раннем этапе построения выборки, что существенно экономит ресурсы сервера.

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


Вариант решения №3. Без изменения метаданных (Соединение таблиц в запросе)

Что делать, если конфигурация находится на поддержке, менять ее структуру строго запрещено, а поле ДатаПлатежа так и осталось обычным реквизитом регистра накопления? В таком случае нам придется решить задачу программным путем в одном запросе с помощью временных таблиц и механизмов соединения.

Идея решения заключается в следующем:

  1. Сначала мы обратимся к физической таблице движений регистра, чтобы найти минимальную (первую) дату платежа по каждому счету, где эта дата была заполнена. Этот результат мы сохраним во временную таблицу.
  2. Затем мы выберем актуальные остатки долга из виртуальной таблицы остатков.
  3. На последнем этапе мы соединим эти две таблицы по полю Счет и отфильтруем только те строки, где дата платежа уже просрочена.

Проанализируем готовый текст такого запроса:


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

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВзаиморасчетыОстатки.Контрагент КАК Контрагент,
    ВзаиморасчетыОстатки.Счет КАК Счет,
    ВзаиморасчетыОстатки.СуммаОстаток КАК СуммаОстаток,
    ЕСТЬNULL(ВТ_ДатыПлатежа.ДатаПлатежа, &ПустаяДата) КАК ДатаПлатежа
ИЗ
    РегистрНакопления.ВзаиморасчетыСПокупателями.Остатки(&МоментВремени, ) КАК ВзаиморасчетыОстатки
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ДатыПлатежа КАК ВТ_ДатыПлатежа
        ПО ВзаиморасчетыОстатки.Счет = ВТ_ДатыПлатежа.Счет
ГДЕ
    ВзаиморасчетыОстатки.СуммаОстаток > 0
    И ВТ_ДатыПлатежа.ДатаПлатежа < &ДатаКонтроля

Этот способ позволяет решить задачу без внесения изменений в конфигурацию. Однако помните, что сканирование физической таблицы движений (первый пакет запроса) — это ресурсоемкая операция. На базах данных большого объема (с сотнями тысяч движений) такой запрос может работать медленно. Рекомендуется ограничить выборку в первом пакете по периоду или наложить дополнительные фильтры по контрагентам, если это возможно.


Как устроен контроль просрочки в типовых конфигурациях 1С?

В современных типовых решениях от фирмы «1С» (например, в 1С:ERP Управление предприятием или 1С:Управление торговлей 11) механизмы контроля задолженности построены на более сложных и производительных принципах:

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

← На главную