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