Как правильно и оптимально списать себестоимость по средней цене в 1С без запросов в цикле

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

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

В чем опасность «интуитивного» кода начинающих разработчиков?

Давайте проанализируем код, который часто приходит в голову новичкам на первых этапах работы. В нем, как правило, совершаются три классические ошибки:

  1. Запрос внутри цикла по строкам табличной части. Если в документе будет 100 или 1000 строк, система выполнит ровно столько же отдельных запросов к базе данных. Это создает колоссальную нагрузку на СУБД и приводит к жестким зависаниям системы при одновременной работе нескольких пользователей.
  2. Обращение к реальной (физической) таблице регистра накопления. Использование таблицы РегистрНакопления.ОстаткиТоваров вместо виртуальной таблицы Остатки заставляет систему сканировать абсолютно все записи движений за всю историю ведения учета, чтобы вычислить остаток на текущий момент. Виртуальные таблицы используют заранее рассчитанные итоги, что работает в десятки раз быстрее.
  3. Неверный алгоритм расчета средней цены. Простое суммирование цен или деление без учета партий и остатков приводит к искажению финансового результата компании.

Ниже мы подробно разберем, как по шагам избавиться от этих проблем и написать идеальный алгоритм проведения документа.

Шаг 1. Перенос запроса из цикла и подготовка движений

Основное правило оптимизации в 1С — никогда не писать запросы в цикле. Нам необходимо получить данные по всем товарам документа одним-единственным запросом.

Прежде чем выполнять запрос к остаткам, нам необходимо подготовить сам регистр. При перепроведении документа старые движения не должны влиять на расчет новых остатков. Для этого мы сначала очищаем движения документа и записываем пустые наборы:


Движения.ОстаткиТоваров.Записывать = Истина;
Движения.ОстаткиТоваров.Записать();

Этот простой прием гарантирует, что при повторном проведении документа система не заблокирует сама себя и не учтет свои собственные старые движения при расчете новой себестоимости.

Шаг 2. Наложение управляемых блокировок

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


Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ОстаткиТоваров");
ЭлементБлокировки.ИсточникДанных = Товары;
ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Номенклатура", "Номенклатура");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();

Шаг 3. Составление оптимального пакетного запроса

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

Рассмотрим структуру нашего пакетного запроса:

  1. Первый запрос пакета: Выбирает данные из табличной части документа, группирует их по номенклатуре (чтобы исключить дубли и просуммировать количество) и помещает результат во временную таблицу ВТ_Товары.
  2. Второй запрос пакета: Выполняет левое соединение временной таблицы с виртуальной таблицей остатков регистра на указанный МоментВремени. Мы обязательно используем функцию ЕСТЬNULL, чтобы избежать ошибок при отсутствии товара на складе.

Посмотрим на пример реализации такого запроса:


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

Запрос.УстановитьПараметр("Ссылка", Ссылка);
// Передаем МоментВремени() для точного позиционирования на оси времени
Запрос.УстановитьПараметр("МоментВремени", МоментВремени());

РезультатЗапроса = Запрос.Выполнить();

Шаг 4. Обход выборки, контроль остатков и решение «проблемы копеек»

Теперь мы приступаем к обработке результатов запроса в цикле. Здесь нам необходимо решить две важнейшие задачи:

Проанализируем программную реализацию этого алгоритма:


Выборка = РезультатЗапроса.Выбрать();

Пока Выборка.Следующий() Цикл
    
    // Проверка на достаточность остатка на складе
    Если Выборка.КоличествоДокумента > Выборка.КоличествоОстаток Тогда
        
        Отказ = Истина;
        
        // Выводим информативное сообщение пользователю
        Сообщение = Новый СообщениеПользователю;
        Сообщение.Текст = СтрШаблон(
            "Недостаточно товара ""%1"" на складе. Требуется: %2, в наличии: %3",
            Выборка.Номенклатура,
            Выборка.КоличествоДокумента,
            Выборка.КоличествоОстаток
        );
        Сообщение.Сообщить();
        
    КонецЕсли;
    
    // Если уже обнаружена нехватка, расчет себестоимости делать бессмысленно
    Если Отказ Тогда
        Продолжить;
    КонецЕсли;
    
    // Алгоритм расчета себестоимости с защитой от ошибок округления
    СуммаСписания = 0;
    
    Если Выборка.КоличествоДокумента = Выборка.КоличествоОстаток Тогда
        // Списываем товар под ноль — забираем всю сумму остатка до копейки
        СуммаСписания = Выборка.СуммаОстаток;
    Иначе
        // Рассчитываем себестоимость по средней цене и округляем до 2 знаков
        СредняяЦена = Выборка.СуммаОстаток / Выборка.КоличествоОстаток;
        СуммаСписания = Окр(СредняяЦена * Выборка.КоличествоДокумента, 2);
    КонецЕсли;
    
    // Формируем движение по расходу в регистр накопления
    Движение = Движения.ОстаткиТоваров.Добавить();
    Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
    Движение.Период = Дата;
    Движение.Номенклатура = Выборка.Номенклатура;
    Движение.Количество = Выборка.КоличествоДокумента;
    Движение.Сумма = СуммаСписания;
    
КонецЦикла;

Подведем итоги

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

← На главную