Как правильно реализовать движения документа на разные даты с учетом остатков и графиков платежей в 1С

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

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

Архитектурная ошибка: почему нельзя формировать движения в будущее в регистре остатков

Рассмотрим, что происходит, когда документ единовременно создает записи с датами вперед (например, 15 января и 1 февраля из документа от 1 января):

  1. Деградация механизма итогов: Таблицы итогов регистров накопления хранят агрегированные данные на конец каждого месяца. Появление записей с будущими датами искусственно раздвигает границу рассчитанных итогов вперед. При проведении любых документов прошлыми датами системе приходится пересчитывать значительно больший диапазон итогов, что существенно замедляет работу всей базы данных.
  2. Искажение сущности задолженности: Фактическое обязательство контрагента возникает в момент совершения сделки (отгрузки). На дату сделки общий долг уже существует в полном объеме, а даты из графика платежей определяют лишь срок его планового погашения (просрочки), но не дату возникновения самого долга.
  3. Сложности при перепроведении задним числом: Если документ с движениями на будущие даты отменяется или перепроводится, возникают коллизии с оперативными документами других пользователей, которые могли сформировать свои движения в этот промежуточный период.

Архитектурное решение 1: разделение оперативного долга и графика оплат

Разберем классический типовой паттерн, используемый в решениях уровня 1С:ERP и 1С:Комплексная автоматизация. Задача разделяется на два независимых контура:

  1. Регистр накопления остатков «Взаиморасчеты с клиентами»: Фиксирует факт возникновения и погашения реального долга. Все движения документа формируются строго на дату самого документа (оперативное проведение).
  2. Регистр сведений или оборотный регистр накопления «График платежей»: Хранит плановые даты платежей, суммы рассрочки и документы расчетов. Здесь дата платежа выступает обычным измерением или ресурсом, а не полем Период.

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

Архитектурное решение 2: использование отдельного измерения срока в регистре остатков

Если требуется хранить детальный срез задолженности по срокам в одном регистре накопления остатков, проанализируем следующий прием:

  1. В регистр накопления добавляется измерение СрокОплаты (или ДатаПлатежа) с типом Дата.
  2. Поле Период у всех формируемых записей остается единым и равным дате документа-регистратора.
  3. Значение измерения СрокОплаты заполняется датой из соответствующей строки табличной части документа.

При таком подходе виртуальная таблица Остатки позволяет мгновенно получить задолженность, срок оплаты которой уже наступил, передав в параметры виртуальной таблицы условие: СрокОплаты <= &ТекущаяДата.

Алгоритм последовательного списания остатков без циклов в запросе

Выясним, как оптимально реализовать последовательное списание остатков по документам расчета (FIFO), когда в документе присутствует несколько строк с разными условиями, не выполняя запросы в цикле.

Рассмотрим пошаговый алгоритм с использованием блокировок и единой выборки данных:

  1. Установка управляемых блокировок: Перед чтением остатков в транзакции проведения блокируются измерения регистра (например, Организация, Контрагент, Договор). Так как пространство блокировок остатков строится по комбинациям измерений, блокировка гарантирует целостность данных на все периоды.
  2. Получение остатков одним запросом: Выполняется запрос к виртуальной таблице Остатки на момент времени документа с фильтром по ключевым аналитикам.
  3. Обработка распределения в оперативной памяти: Полученная таблица остатков используется как локальный кэш. В цикле по строкам документа остатки списываются из этой таблицы с уменьшением доступного количества или суммы.
  4. Формирование набора записей: Итоговые распределенные строки напрямую записываются в набор движений документа.

Посмотрим на пример реализации модуля проведения документа:


// 1. Установка управляемой блокировки
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.Взаиморасчеты");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.УстановитьЗначение("Контрагент", ЭтотОбъект.Контрагент);
ЭлементБлокировки.УстановитьЗначение("Договор", ЭтотОбъект.Договор);
Блокировка.Заблокировать();

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

Запрос.УстановитьПараметр("МоментВремени", МоментВремени());
Запрос.УстановитьПараметр("Контрагент", ЭтотОбъект.Контрагент);
Запрос.УстановитьПараметр("Договор", ЭтотОбъект.Договор);

ТаблицаОстатков = Запрос.Выполнить().Выгрузить();

// 3. Распределение сумм документа по остаткам (FIFO)
Движения.Взаиморасчеты.Записывать = Истина;

Для Каждого СтрТЧ Из ЭтотОбъект.ГрафикПлатежей Цикл
    СуммаКРаспределению = СтрТЧ.Сумма;
    
    Для Каждого СтрОстатка Из ТаблицаОстатков Цикл
        Если СуммаКРаспределению = 0 Тогда
            Прервать;
        КонецЕсли;
        
        Если СтрОстатка.СуммаОстаток > 0 Тогда
            СуммаСписания = Мин(СуммаКРаспределению, СтрОстатка.СуммаОстаток);
            
            // Формируем движение расхода
            Движение = Движения.Взаиморасчеты.ДобавитьРасход();
            Движение.Период = ЭтотОбъект.Дата;
            Движение.Контрагент = ЭтотОбъект.Контрагент;
            Движение.Договор = ЭтотОбъект.Договор;
            Движение.ДокументРасчета = СтрОстатка.ДокументРасчета;
            Движение.СрокОплаты = СтрТЧ.СрокОплаты;
            Движение.Сумма = СуммаСписания;
            
            // Корректируем локальный кэш
            СтрОстатка.СуммаОстаток = СтрОстатка.СуммаОстаток - СуммаСписания;
            СуммаКРаспределению = СуммаКРаспределению - СуммаСписания;
        КонецЕсли;
    КонецЦикла;
КонецЦикла;

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

← На главную