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

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

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

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

---

Почему не работал исходный код автора? Разбор транзакции

В сообщении №1 автор пытается обновить реквизит табличной части документа непосредственно в процедуре ОбработкаПроведения модуля объекта:


Для Каждого Строка Из Перечень Цикл
    // ... Выполнение запроса к остаткам по одной позиции ...
    Если Результат.Следующий Тогда
        Строка.Остаток = Результат.КоличествоОстаток;
    КонецЕсли;
КонецЦикла;

Давайте выясним причину, почему этот код не сохраняет данные в базе данных после нажатия кнопки "Провести и закрыть". Дело в строгом порядке транзакции записи объекта в платформе "1С:Предприятие 8":

  1. При нажатии кнопки проведения сначала вызывается событие ПередЗаписью на сервере.
  2. Затем данные документа (шапка и табличные части) физически записываются в таблицы базы данных (событие ПриЗаписи).
  3. Только после успешной записи документа в базу данных стартует процедура ОбработкаПроведения.

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

---

Почему хранить остатки в реквизитах объекта — плохая практика?

Попытка сделать "Остаток" полноценным реквизитом табличной части (который физически сохраняется в таблице документа в базе данных) нарушает базовые принципы проектирования баз данных и логику работы ERP-систем по следующим причинам:

---

Правильный подход: динамический расчет остатков на форме

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

Рассмотрим по шагам, как правильно реализовать этот механизм без ущерба для производительности системы.

Шаг 1. Добавление колонки на форму

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

Шаг 2. Создание серверного метода расчета остатков (Пакетный запрос)

Крайне важно избегать запросов в цикле! Если в табличной части документа будет 100 строк, и для каждой строки мы сделаем отдельный запрос к регистру остатков (как было в коде автора), система начнет сильно зависать при открытии формы. Мы должны получить все остатки одним запросом.

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


&НаСервере
Процедура ЗаполнитьОстаткиНаФорме()

    // 1. Проверяем заполнение ключевых реквизитов
    Если Не ЗначениеЗаполнено(Объект.Склад) Тогда
        Для Каждого СтрокаТЧ Из Объект.Товары Цикл
            СтрокаТЧ.ОстатокНаСкладе = 0;
        КонецЦикла;
        Возврат;
    КонецЕсли;

    // 2. Собираем массив уникальных товаров из табличной части для фильтрации запроса
    МассивТоваров = Объект.Товары.ВыгрузитьКолонку("Товар");
    
    // Удаляем дубликаты из массива для оптимизации работы СУБД
    ОбщегоНазначенияКлиентСервер.СвернутьМассив(МассивТоваров);
    
    Если МассивТоваров.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;

    // 3. Формируем один пакетный запрос к регистру накопления
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	ОстаткиТоваровОстатки.Товар КАК Товар,
        |	ОстаткиТоваровОстатки.КоличествоОстаток КАК КоличествоОстаток
        |ИЗ
        |	РегистрНакопления.ОстаткиТоваров.Остатки(&Период, Товар В (&МассивТоваров) И Склад = &Склад) КАК ОстаткиТоваровОстатки";

    // Нам важно правильно определить период (Момент времени)
    // Если документ уже проведен или записан, нам нужно получить остатки ДО его проведения (исключая движения самого документа)
    Если Объект.Ссылка.Пустая() Тогда
        // Новый документ — берем текущие актуальные остатки
        ГраницаОстатков = Неопределено;
    Иначе
        // Существующий документ — берем остатки на момент времени документа, исключая его собственные движения
        ГраницаОстатков = Новый Граница(Объект.Ссылка.МоментВремени(), ВидГраницы.Исключая);
    EndIf;

    Запрос.УстановитьПараметр("МассивТоваров", МассивТоваров);
    Запрос.УстановитьПараметр("Склад", Объект.Склад);
    Запрос.УстановитьПараметр("Период", ГраницаОстатков);

    ТаблицаОстатков = Запрос.Выполнить().Выгрузить();
    ТаблицаОстатков.Индексировать("Товар"); // Ускоряет поиск методом Найти()

    // 4. Заполняем колонку остатков на форме
    Для Каждого СтрокаТЧ Из Объект.Товары Цикл
        НайденнаяСтрока = ТаблицаОстатков.Найти(СтрокаТЧ.Товар, "Товар");
        Если НайденнаяСтрока <> Неопределено Тогда
            СтрокаТЧ.ОстатокНаСкладе = НайденнаяСтрока.КоличествоОстаток;
        Иначе
            СтрокаТЧ.ОстатокНаСкладе = 0;
        КонецЕсли;
    КонецЦикла;

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

---

Интерактивное обновление остатков при действиях пользователя

Для того чтобы пользователь всегда видел актуальную информацию в процессе редактирования документа, нам необходимо вызывать процедуру ЗаполнитьОстаткиНаФорме() в ключевые моменты жизненного цикла формы.

Разберем, к каким событиям формы нужно привязать обновление данных:

  1. При создании формы на сервере: остатки должны рассчитаться сразу при открытии документа.
    
    &НаСервере
    Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
        ЗаполнитьОстаткиНаФорме();
    КонецПроцедуры
    
  2. При изменении Склада в шапке документа: если пользователь меняет склад отгрузки, остатки по всей табличной части должны мгновенно пересчитаться под новый склад.
    
    &НаКлиенте
    Процедура СкладПриИзменении(Элемент)
        ЗаполнитьОстаткиНаФорме();
    КонецПроцедуры
    
  3. При изменении Номенклатуры в строке табличной части: когда пользователь добавляет новую строку или меняет товар в существующей строке, нам нужно актуализировать остаток именно по этой позиции. Чтобы не пересчитывать всю таблицу целиком, можно написать оптимизированную процедуру получения остатка для одной строки, либо вызвать общую процедуру (для небольших табличных частей это вполне допустимо).
    
    &НаКлиенте
    Процедура ТоварыТоварПриИзменении(Элемент)
        // Вызываем общий пересчет остатков на форме
        ЗаполнитьОстаткиНаФорме();
    КонецПроцедуры
    

---

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

В современных конфигурациях уровня 1С:ERP, Управление торговлей (УТ 11) или УНФ разработчики пошли еще дальше для оптимизации производительности:

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

← На главную