При разработке собственных конфигураций на платформе «1С:Предприятие 8.3» одной из самых распространенных задач является организация складского учета. Начинающие разработчики часто сталкиваются с ситуацией, когда после проведения документов реализации или перемещения текущие остатки товаров на форме не обновляются или рассчитываются некорректно. В этой статье мы подробно разберем, почему возникает эта проблема, исправим логику проведения документов и оптимизируем программный код для работы в клиент-серверном режиме.
Проанализируем классическую схему движения товаров на предприятии. Обычно учет делится на два контура: хранение на оптовом складе и нахождение товаров в торговом зале (розница). Для корректного списания и перемещения остатков нам понадобятся два регистра накопления остатков:
ПриходПродукции (учет товаров на основном складе).ПередачаПродукции (учет товаров в торговом зале).Рассмотрим ошибку, которую совершил автор темы в процедуре проведения документа перемещения. В коде документа «Передача продукции в торговый зал» движения записывались следующим образом:
// ОШИБОЧНЫЙ КОД
Движения.ПередачаПродукции.Записывать = Истина;
Для Каждого ТекСтрока Из СписокПродукции Цикл
Движение = Движения.ПередачаПродукции.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Приход;
// Заполнение реквизитов...
КонецЦикла;
Движения.ПриходПродукции.Записывать = Истина;
Для Каждого ТекСтрока Из СписокПродукции Цикл
// Ошибка здесь: вместо списания со склада ПриходПродукции
// разработчик снова добавляет движение в регистр ПередачаПродукции
Движение = Движения.ПередачаПродукции.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
// Заполнение реквизитов...
КонецЦикла;
Выясним причину ошибки: при таком проведении в одном и том же регистре ПередачаПродукции по одной и той же номенклатуре одновременно делаются две записи — «Приход» и «Расход» на одинаковое количество. В результате регистр закрывается «в ноль», на основном складе товар по-прежнему числится, а в торговом зале остаток равен нулю.
Чтобы исправить это поведение, перепишем процедуру проведения документа ПередачаПродукцииВЗал. Нам нужно списать товар с основного склада (Расход по регистру ПриходПродукции) и оприходовать его в торговый зал (Приход по регистру ПередачаПродукции):
// ИСПРАВЛЕННЫЙ КОД ПРОВЕДЕНИЯ
Процедура ОбработкаПроведения(Отказ, Режим)
// Включаем запись для обоих регистров
Движения.ПриходПродукции.Записывать = Истина;
Движения.ПередачаПродукции.Записывать = Истина;
Для Каждого ТекСтрокаСписокПродукции Из СписокПродукции Цикл
// 1. Списание со склада (Расход)
ДвижениеРасход = Движения.ПриходПродукции.Добавить();
ДвижениеРасход.ВидДвижения = ВидДвиженияНакопления.Расход;
ДвижениеРасход.Период = Дата;
ДвижениеРасход.Продукция = ТекСтрокаСписокПродукции.Номенклатура;
ДвижениеРасход.Количество = ТекСтрокаСписокПродукции.Количество;
// Заполняем остальные измерения и ресурсы...
// 2. Поступление в торговый зал (Приход)
ДвижениеПриход = Движения.ПередачаПродукции.Добавить();
ДвижениеПриход.ВидДвижения = ВидДвиженияНакопления.Приход;
ДвижениеПриход.Период = Дата;
ДвижениеПриход.Продукция = ТекСтрокаСписокПродукции.Номенклатура;
ДвижениеПриход.Количество = ТекСтрокаСписокПродукции.Количество;
// Заполняем остальные измерения и ресурсы...
КонецЦикла;
КонецПроцедуры
После реализации этой логики при проведении документа «Реализация продукции» мы будем делать движение «Расход» только по регистру ПередачаПродукции. В этом случае остатки на складах начнут списываться корректно.
Теперь обратим внимание на код функции получения остатка товара, который использовался изначально:
// НЕОПТИМАЛЬНЫЙ КОД
Запрос.Текст = "ВЫБРАТЬ
| ПередачаПродукцииОстаткиИОбороты.КоличествоКонечныйОстаток КАК Количество
|ИЗ
| РегистрНакопления.ПередачаПродукции.ОстаткиИОбороты КАК ПередачаПродукцииОстаткиИОбороты
|ГДЕ
| ПередачаПродукцииОстаткиИОбороты.Продукция = &Продукт";
В этой функции допущено сразу несколько серьезных ошибок проектирования:
ОстаткиИОбороты является наиболее тяжелой для СУБД, так как рассчитывает не только конечные итоги, но и все движения за период. Для получения текущего остатка всегда нужно использовать таблицу Остатки.&Продукция (или &Продукт), а при установке параметров передается другое имя. Имена должны совпадать на 100%.Посмотрим на пример правильного, оптимизированного запроса для получения остатка товара:
// ОПТИМИЗИРОВАННЫЙ ЗАПРОС
Запрос.Текст =
"ВЫБРАТЬ
| ЕСТЬNULL(ПередачаПродукцииОстатки.КоличествоОстаток, 0) КАК Количество
|ИЗ
| РегистрНакопления.ПередачаПродукции.Остатки(&Период, Продукция = &Продукция) КАК ПередачаПродукцииОстатки";
Проанализируем клиентскую процедуру ПрайсЛистПродукцияПриИзменении, которая срабатывает при выборе товара в табличной части документа:
// ОШИБОЧНЫЙ КЛИЕНТСКИЙ КОД
СтрокаТабличнойЧасти.Фирма = СтрокаТабличнойЧасти.Продукция.ПолучитьОбъект.Фирма;
СтрокаТабличнойЧасти.ТипПродукции = СтрокаТабличнойЧасти.Продукция.ПолучитьОбъект.ТипПродукции;
СтрокаТабличнойЧасти.КоличествоВзале = ПолучитьОстаток(СтрокаТабличнойЧасти.Продукция);
Разберем по шагам, почему этот код крайне неэффективен:
ПолучитьОбъект() на сервере для простого чтения реквизитов — грубая ошибка. Этот метод полностью считывает объект из базы данных в оперативную память сервера приложений вместе со всеми табличными частями. Для чтения реквизитов достаточно использовать обращение напрямую к ссылке.Продукция.ТипПродукции заставляет платформу выполнять неявный (скрытый) серверный вызов для получения каждого реквизита. В итоге для заполнения одной строки система делает 3-4 отдельных сетевых запроса к серверу базы данных, что вызывает зависание интерфейса у пользователя.Решение проблемы: объединим получение всех необходимых свойств товара (реквизитов справочника) и его актуального остатка в рамках одного единого запроса на сервере.
Реализуем оптимальную функцию на сервере, которая будет возвращать структуру со всеми данными за один вызов:
&НаСервере
Функция ПолучитьДанныеНоменклатуры(Знач Продукция, Знач ДатаДокумента)
Результат = Новый Структура("Фирма, ТипПродукции, ЕдиницаИзмерения, КоличествоВзале",
Неопределено, Неопределено, Неопределено, 0);
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СправочникПродукция.Фирма КАК Фирма,
| СправочникПродукция.ТипПродукции КАК ТипПродукции,
| СправочникПродукция.ЕдИзмерения КАК ЕдиницаИзмерения,
| ЕСТЬNULL(ПередачаПродукцииОстатки.КоличествоОстаток, 0) КАК КоличествоВзале
|ИЗ
| Справочник.Продукция КАК СправочникПродукция
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ПередачаПродукции.Остатки(&Период, Продукция = &Продукция) КАК ПередачаПродукцииОстатки
| ПО ПередачаПродукцииОстатки.Продукция = СправочникПродукция.Ссылка
|ГДЕ
| СправочникПродукция.Ссылка = &Продукция";
Запрос.УстановитьПараметр("Продукция", Продукция);
// Передаем МоментВремени или Дату документа, чтобы видеть остатки на актуальный момент
Если ЗначениеЗаполнено(ДатаДокумента) Тогда
Запрос.УстановитьПараметр("Период", Новый Граница(ДатаДокумента, ВидГраницы.Включая));
Иначе
Запрос.УстановитьПараметр("Period", Неопределено);
КонецЕсли;
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
ЗаполнитьЗначенияСвойств(Результат, Выборка);
КонецЕсли;
Возврат Результат;
КонецФункции
Теперь перепишем клиентский обработчик события изменения номенклатуры в табличной части формы. Он станет максимально лаконичным и будет работать мгновенно даже при медленном интернет-соединении:
&НаКлиенте
Процедура ПрайсЛистПродукцияПриИзменении(Элемент)
СтрокаТабличнойЧасти = Элементы.ПрайсЛист.TeкущиеДанные;
Если СтрокаТабличнойЧасти <> Неопределено И ЗначениеЗаполнено(СтрокаТабличнойЧасти.Продукция) Тогда
// Делаем ОДИН серверный вызов вместо серии скрытых обращений
СтруктураДанных = ПолучитьДанныеНоменклатуры(СтрокаТабличнойЧасти.Продукция, Объект.Дата);
// Быстро заполняем реквизиты строки табличной части
ЗаполнитьЗначенияСвойств(СтрокаТабличнойЧасти, СтруктураДанных);
КонецЕсли;
КонецПроцедуры
Просто показать остаток на форме недостаточно для надежного учета. Если пользователь вручную впишет в документ количество товара, превышающее реальный остаток в торговом зале, система не должна позволить провести такой документ. Обязательно добавляйте проверку остатков в процедуру ОбработкаПроведения.
Для этого используется следующая схема («новая» методика проведения в 1С):
Движения.ПередачаПродукции.Записывать = Истина.КоличествоОстаток < 0), выводим сообщение об ошибке пользователю, устанавливаем параметр Отказ = Истина и отменяем транзакцию проведения.Реализация этих несложных правил позволит вашей конфигурации работать быстро, предотвратит появление логических ошибок учета (таких как «минусовые» остатки) и обеспечит мгновенный вывод актуальной информации на форму документа.