Как исправить неправильное отображение остатков товаров и оптимизировать запросы в 1С

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

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

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

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

  1. ПриходПродукции (учет товаров на основном складе).
  2. ПередачаПродукции (учет товаров в торговом зале).

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


// ОШИБОЧНЫЙ КОД
Движения.ПередачаПродукции.Записывать = Истина;
Для Каждого ТекСтрока Из СписокПродукции Цикл
    Движение = Движения.ПередачаПродукции.Добавить();
    Движение.ВидДвижения = ВидДвиженияНакопления.Приход;
    // Заполнение реквизитов...
КонецЦикла;

Движения.ПриходПродукции.Записывать = Истина;
Для Каждого ТекСтрока Из СписокПродукции Цикл
    // Ошибка здесь: вместо списания со склада ПриходПродукции 
    // разработчик снова добавляет движение в регистр ПередачаПродукции
    Движение = Движения.ПередачаПродукции.Добавить(); 
    Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
    // Заполнение реквизитов...
КонецЦикла;

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

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


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

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

Оптимизация запроса получения остатков товаров

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


// НЕОПТИМАЛЬНЫЙ КОД
Запрос.Текст = "ВЫБРАТЬ
               | ПередачаПродукцииОстаткиИОбороты.КоличествоКонечныйОстаток КАК Количество
               |ИЗ
               | РегистрНакопления.ПередачаПродукции.ОстаткиИОбороты КАК ПередачаПродукцииОстаткиИОбороты
               |ГДЕ
               | ПередачаПродукцииОстаткиИОбороты.Продукция = &Продукт";

В этой функции допущено сразу несколько серьезных ошибок проектирования:

  1. Использование таблицы ОстаткиИОбороты вместо таблицы Остатки: виртуальная таблица ОстаткиИОбороты является наиболее тяжелой для СУБД, так как рассчитывает не только конечные итоги, но и все движения за период. Для получения текущего остатка всегда нужно использовать таблицу Остатки.
  2. Фильтрация через предложение ГДЕ: наложение условий после выборки из виртуальной таблицы заставляет систему считывать остатки по всей номенклатуре базы данных, а затем отсекать лишнее. Это приводит к колоссальному падению производительности. Параметры отбора необходимо передавать непосредственно в саму виртуальную таблицу.
  3. Ошибка в именах параметров: в тексте запроса указано &Продукция (или &Продукт), а при установке параметров передается другое имя. Имена должны совпадать на 100%.

Посмотрим на пример правильного, оптимизированного запроса для получения остатка товара:


// ОПТИМИЗИРОВАННЫЙ ЗАПРОС
Запрос.Текст = 
    "ВЫБРАТЬ
    |   ЕСТЬNULL(ПередачаПродукцииОстатки.КоличествоОстаток, 0) КАК Количество
    |ИЗ
    |   РегистрНакопления.ПередачаПродукции.Остатки(&Период, Продукция = &Продукция) КАК ПередачаПродукцииОстатки";

Исключение лишних серверных вызовов и метода ПолучитьОбъект()

Проанализируем клиентскую процедуру ПрайсЛистПродукцияПриИзменении, которая срабатывает при выборе товара в табличной части документа:


// ОШИБОЧНЫЙ КЛИЕНТСКИЙ КОД
СтрокаТабличнойЧасти.Фирма = СтрокаТабличнойЧасти.Продукция.ПолучитьОбъект.Фирма;
СтрокаТабличнойЧасти.ТипПродукции = СтрокаТабличнойЧасти.Продукция.ПолучитьОбъект.ТипПродукции;
СтрокаТабличнойЧасти.КоличествоВзале = ПолучитьОстаток(СтрокаТабличнойЧасти.Продукция);

Разберем по шагам, почему этот код крайне неэффективен:

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

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


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

Теперь перепишем клиентский обработчик события изменения номенклатуры в табличной части формы. Он станет максимально лаконичным и будет работать мгновенно даже при медленном интернет-соединении:


&НаКлиенте
Процедура ПрайсЛистПродукцияПриИзменении(Элемент)
    
    СтрокаТабличнойЧасти = Элементы.ПрайсЛист.TeкущиеДанные;
    
    Если СтрокаТабличнойЧасти <> Неопределено И ЗначениеЗаполнено(СтрокаТабличнойЧасти.Продукция) Тогда
        
        // Делаем ОДИН серверный вызов вместо серии скрытых обращений
        СтруктураДанных = ПолучитьДанныеНоменклатуры(СтрокаТабличнойЧасти.Продукция, Объект.Дата);
        
        // Быстро заполняем реквизиты строки табличной части
        ЗаполнитьЗначенияСвойств(СтрокаТабличнойЧасти, СтруктураДанных);
        
    КонецЕсли;
    
КонецПроцедуры

Контроль отрицательных остатков при проведении

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

Для этого используется следующая схема («новая» методика проведения в 1С):

  1. Устанавливаем свойство Движения.ПередачаПродукции.Записывать = Истина.
  2. Записываем движения «Расход» по документу без предварительной проверки.
  3. Блокируем таблицы регистра для предотвращения конкурентного изменения данных другими пользователями.
  4. Выполняем запрос к виртуальной таблице остатков этого же регистра с отбором по нашему документу-регистратору.
  5. Если после проведения документа остаток по какой-либо позиции стал меньше нуля (КоличествоОстаток < 0), выводим сообщение об ошибке пользователю, устанавливаем параметр Отказ = Истина и отменяем транзакцию проведения.

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

← На главную