При разработке документов в системе «1С:Предприятие» часто возникает задача автоматического заполнения связанных реквизитов при изменении ключевых данных. Типичный пример — расчет и заполнение себестоимости товара при выборе номенклатуры в табличной части. Для получения актуальных и точных сведений разработчики обращаются к регистрам накопления, в которых аккумулируются данные о закупках и движениях запасов.
В этой статье мы подробно разберем, как правильно реализовать этот механизм. Мы проанализируем типичные ошибки начинающих разработчиков, рассмотрим математическую логику расчета средневзвешенной цены и напишем производительный, безопасный код, соответствующий стандартам разработки фирмы «1С».
На форумах часто встречается подход, когда расчет средней цены пытаются выполнить простым запросом к физической таблице регистра накопления. Давайте рассмотрим код, к которому пришел автор темы в процессе обсуждения:
&НаКлиенте
Процедура ПереченьНоменклатурыНоменклатураПриИзменении(Элемент)
ТекущиеДанные = Элементы.ПереченьНоменклатуры.ТекущиеДанные;
Если ТекущиеДанные = Неопределено тогда возврат КонецЕсли;
ТекущиеДанные.Себестоимость = ПолучитьСреднююЦену(ТекущиеДанные.Номенклатура);
КонецПроцедуры
Функция ПолучитьСреднююЦену(Номенклатура)
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| Закупки.Номенклатура КАК Номенклатура,
| СУММА(Закупки.Себестоимость) / СУММА (Закупки.Количество) КАК СредняяСебестоимость
|ИЗ
| РегистрНакопления.Закупки КАК Закупки
|ГДЕ
| Закупки.Номенклатура = &Номенклатура
|
|СГРУППИРОВАТЬ ПО
| Закупки.Номенклатура";
Запрос.УстановитьПараметр("Номенклатура", Номенклатура);
РезультатЗапроса = Запрос.Выполнить.Выбрать;
Если РезультатЗапроса.Следующий Тогда
Возврат РезультатЗапроса.СредняяСебестоимость;
Иначе
Возврат 0;
КонецЕсли;
КонецФункции
Этот код действительно может работать на небольших тестовых базах данных, однако он содержит ряд критических архитектурных ошибок, которые неизбежно приведут к сбоям или сильному снижению производительности в реальной рабочей базе:
РегистрНакопления.Закупки заставляет СУБД сканировать все записи регистра построчно. По мере роста базы данных скорость выполнения этого запроса будет стремительно падать. По стандартам «1С» для получения итоговых данных (оборотов) необходимо использовать только виртуальные таблицы.&НаСервереБезКонтекста платформа может передавать на сервер весь контекст формы, что создает избыточную нагрузку на сеть и замедляет отзывчивость интерфейса.Важно обратить внимание на формулу расчета средней цены. В одном из сообщений на форуме предлагался такой вариант:
СУММА(Закупки.Себестоимость / Закупки.Количество) КАК СредняяСебестоимость
Давайте выясним, почему этот вариант математически неверен для расчета себестоимости. Представим две закупки одного и того же товара:
Если мы применим ошибочную формулу СУММА(Себестоимость / Количество), то получим следующее:
(100 / 1) + (500 / 10) = 100 + 50 = 150 рублей.
Это средняя арифметическая цена единичных партий, которая не имеет ничего общего с реальной средней себестоимостью единицы товара на складе.
Правильный расчет средневзвешенной стоимости должен строиться как отношение общей суммы затрат к общему количеству закупленного товара:
СУММА(Себестоимость) / СУММА(Количество).
В нашем примере: (100 + 500) / (1 + 10) = 600 / 11 ≈ 54.54 рублей. Именно этот вариант дает точную оценку стоимости единицы запаса.
Перейдем к созданию профессионального программного кода, лишенного всех вышеперечисленных недостатков. Мы оптимизируем запрос, добавим защиту от деления на ноль, используем виртуальную таблицу оборотов и перенесем расчет на сервер без передачи контекста формы.
Для предотвращения аварийных ситуаций при нулевом количестве мы применим конструкцию ВЫБОР КОГДА ... ТОГДА ... ИНАЧЕ ... КОНЕЦ прямо внутри текста запроса:
ВЫБОР
КОГДА ЕСТЬNULL(ЗакупкиОбороты.КоличествоОборот, 0) = 0 ТОГДА 0
ИНАЧЕ ЕСТЬNULL(ЗакупкиОбороты.СебестоимостьОборот, 0) / ЗакупкиОбороты.КоличествоОборот
КОНЕЦ КАК СредняяСебестоимость
Вместо сканирования физической таблицы мы будем использовать виртуальную таблицу оборотов РегистрНакопления.Закупки.Обороты. Платформа «1С» хранит данные в ней в уже сгруппированном и подготовленном виде. Кроме того, мы наложим фильтр по конкретной номенклатуре непосредственно в параметрах виртуальной таблицы, что позволит СУБД моментально найти нужную запись по индексам:
ИЗ
РегистрНакопления.Закупки.Обороты(, &ДатаДокумента, , Номенклатура = &Номенклатура) КАК ЗакупкиОбороты
Обратите внимание: в качестве конечной границы периода мы передаем параметр &ДатаДокумента. Это гарантирует, что при расчете средней цены программа не будет учитывать закупки, совершенные позже даты текущего документа.
Для минимизации трафика между клиентом и сервером мы объявим функцию расчета цены с директивой &НаСервереБезКонтекста. В такую функцию нельзя напрямую обращаться к реквизитам формы (например, к свойствам объекта напрямую), поэтому все необходимые данные (ссылку на номенклатуру и дату документа для ограничения периода) мы передадим в качестве параметров функции.
Ниже представлен готовый к использованию код, который следует разместить в модуле управляемой формы вашего документа:
&НаКлиенте
Процедура ПереченьНоменклатурыНоменклатураПриИзменении(Элемент)
// Получаем текущую строку табличной части
ТекущиеДанные = Элементы.ПереченьНоменклатуры.ТекущиеДанные;
// Проверяем, что строка действительно выбрана и существует
Если ТекущиеДанные = Неопределено Тогда
Возврат;
КонецЕсли;
// Вызываем серверную функцию без контекста, передавая Номенклатуру и Дату документа
ТекущиеДанные.Себестоимость = ПолучитьСреднююЦенуНаСервере(ТекущиеДанные.Номенклатура, Объект.Дата);
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьСреднююЦенуНаСервере(Номенклатура, ДатаДокумента)
// Если номенклатура не заполнена, возвращаем 0 без обращения к базе данных
Если Не ЗначениеЗаполнено(Номенклатура) Тогда
Возврат 0;
КонецЕсли;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ВЫБОР
| КОГДА ЕСТЬNULL(ЗакупкиОбороты.КоличествоОборот, 0) = 0 ТОГДА 0
| ИНАЧЕ ЕСТЬNULL(ЗакупкиОбороты.СебестоимостьОборот, 0) / ЗакупкиОбороты.КоличествоОборот
| КОНЕЦ КАК СредняяСебестоимость
|ИЗ
| РегистрНакопления.Закупки.Обороты(, &ДатаДокумента, , Номенклатура = &Номенклатура) КАК ЗакупкиОбороты";
// Устанавливаем параметры запроса
Запрос.УстановитьПараметр("ДатаДокумента", ДатаДокумента);
Запрос.УстановитьПараметр("Номенклатура", Номенклатура);
РезультатЗапроса = Запрос.Выполнить().Выбрать();
Если РезультатЗапроса.Следующий() Тогда
Возврат РезультатЗапроса.MediumCost; // Возвращаем рассчитанную среднюю себестоимость
Иначе
Возврат 0; // Если движений по номенклатуре не было, возвращаем 0
КонецЕсли;
КонецФункции
Рассмотренный выше метод идеально подходит, когда пользователь вручную добавляет строки в табличную часть одну за другой. Однако, если в вашей конфигурации предусмотрен массовый импорт строк (например, загрузка из файла Excel, подбор товаров или копирование документов), обработчик ПриИзменении начнет циклически срабатывать для каждой добавленной строки.
Это вызовет эффект «зависания» интерфейса, так как система будет выполнять десятки и сотни отдельных серверных запросов к базе данных. Для таких сценариев мы рекомендуем реализовать групповую обработку табличной части. Вместо выполнения одиночных запросов при изменении каждой строки, лучше добавить на командную панель табличной части кнопку «Заполнить себестоимость».
При нажатии на эту кнопку вся табличная часть передается на сервер, где одним пакетным запросом с использованием временной таблицы собираются средние цены сразу по всем товарам из документа. Такой подход работает в десятки раз быстрее и бережет ресурсы сервера базы данных.