Как реализовать иерархический контроль остатков денежных средств в 1С

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

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

Анализ проблемы и выбор методики проведения

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

Для надежного контроля остатков рекомендуется использовать «Новую методику проведения». Ее суть заключается в том, что мы сначала записываем движения документа в базу данных, а затем проверяем, не привели ли эти движения к отрицательным остаткам. Это позволяет избежать двойного расчета (сначала «сколько было», потом «вычитание в уме») и обеспечивает точность данных.

Шаг 1. Подготовка данных документа

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

Проанализируем пример подготовки данных в процедуре ОбработкаПроведения:


Процедура ОбработкаПроведения(Отказ, Режим)
    
    // 1. Устанавливаем признак записи движений
    Движения.ДвиженияДС.Записывать = Истина;
    
    // Очищаем старые движения, если документ перепроводится
    Движения.ДвиженияДС.Очистить();
    
    Для Каждого ТекСтрока Из СписаниеДС Цикл
        Движение = Движения.ДвиженияДС.ДобавитьРасход();
        Движение.Период = Дата;
        Движение.Контрагент = ТекСтрока.Контрагент;
        Движение.СчетКонтрагента = ТекСтрока.СчетКонтрагента;
        Движение.Сумма = ТекСтрока.Сумма;
    КонецЦикла;
    
    // 2. Записываем движения в БД до проверки
    Движения.Записать();
    
    // 3. Вызываем функцию контроля
    Если Режим = РежимПроведенияДокумента.Оперативный Тогда
        ПроверитьОстатки(Отказ);
    КонецЕсли;

КонецПроцедуры

Шаг 2. Реализация контроля с учетом иерархии

Теперь разберем самое сложное — запрос для проверки остатков. Главная особенность здесь заключается в использовании оператора В ИЕРАРХИИ. Однако, если мы просто проверим остаток по конкретному счету, мы не увидим общую картину по «родителю». Выясним причину: виртуальная таблица Остатки выдает данные в разрезе измерений. Если деньги лежат на Счете 01.01, а мы списываем со Счета 01, нам нужно получить общую сумму по всей ветке.

Рассмотрим структуру запроса, который эффективно решает эту задачу. Мы будем использовать временную таблицу для передачи списка счетов из документа в виртуальную таблицу остатков.


Процедура ПроверитьОстатки(Отказ)

    Запрос = Новый Запрос;
    Запрос.МенеджерВременных Таблиц = Новый МенеджерВременныхТаблиц;
    
    // Помещаем данные текущего документа во временную таблицу
    Запрос.Текст = 
    "ВЫБРАТЬ
    |    ТЧ.СчетКонтрагента КАК СчетКонтрагента,
    |    ТЧ.Контрагент КАК Контрагент
    |ПОМЕСТИТЬ ВТ_СчетаДокумента
    |ИЗ
    |    &ТЧ КАК ТЧ
    |ГРУППИРОВАТЬ ПО
    |    ТЧ.СчетКонтрагента,
    |    ТЧ.Контрагент";
    
    Запрос.УстановитьПараметр("ТЧ", СписаниеДС.Выгрузить());
    Запрос.Выполнить();
    
    // Основной запрос проверки отрицательных остатков
    Запрос.Текст = 
    "ВЫБРАТЬ
    |    Остатки.СчетКонтрагента КАК Счет,
    |    Остатки.Контрагент КАК Контрагент,
    |    Остатки.СуммаОстаток КАК СуммаОстаток
    |ИЗ
    |    РегистрНакопления.ДвиженияДС.Остатки(
    |        &МоментКонтроля,
    |        (Контрагент, СчетКонтрагента) В
    |            (ВЫБРАТЬ
    |                ВТ.Контрагент,
    |                ВТ.СчетКонтрагента
    |            ИЗ
    |                ВТ_СчетаДокумента КАК ВТ)
    |    ) КАК Остатки
    |ГДЕ
    |    Остатки.СуммаОстаток < 0";
    
    // Используем Границу, чтобы включить движения текущего документа
    ГраницаКонтроля = Новый Граница(МоментВремени(), ВидГраницы.Включая);
    Запрос.УстановитьПараметр("МоментКонтроля", ГраницаКонтроля);
    
    Результат = Запрос.Выполнить();
    
    Если Не Результат.Пустой() Тогда
        Отказ = Истина;
        Выборка = Результат.Выбрать();
        Пока Выборка.Следующий() Цикл
            Сообщение = Новый СообщениеПользователю;
            Сообщение.Текст = СтрШаблон("Недостаточно средств! Контрагент: %1, Счет: %2. Отрицательный остаток: %3", 
                Выборка.Контрагент, Выборка.Счет, -Выборка.СуммаОстаток);
            Сообщение.Сообщить();
        КонецЦикла;
    КонецЕсли;

КонецПроцедуры

Шаг 3. Использование управляемых блокировок

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

Перед выполнением запроса на остатки необходимо заблокировать данные в регистре. Добавим следующий код в начало процедуры контроля:


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

Важные рекомендации по иерархии

Если ваша задача требует, чтобы остаток проверялся не по конкретному элементу, а суммарно по всей ветке иерархии (например, списываем с 01, но учитываем деньги на 01.01 и 01.02), то логика запроса должна измениться:

  1. Агрегация данных: В параметрах виртуальной таблицы используйте условие СчетКонтрагента В ИЕРАРХИИ (&МассивСчетов).
  2. Группировка: В самом запросе необходимо будет группировать остатки по родителю верхнего уровня или по тем счетам, которые указаны в документе, используя соединения.
  3. Тип иерархии: Убедитесь, что в справочнике СчетаКонтрагентов установлена «Иерархия элементов». Если используется «Иерархия групп и элементов», работа с Родителем будет отличаться.

Резюме: Чтобы контроль остатков работал корректно, всегда записывайте движения перед проверкой, используйте Границу(Включая) для получения актуальных данных, не забывайте про управляемые блокировки и правильно настраивайте отборы В ИЕРАРХИИ в виртуальных таблицах запроса.

← На главную