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