Как правильно рассчитать движения документа по регистру накопления по средней цене в 1С

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

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

Анализ типичных ошибок в коде проведения

Рассмотрим пример ошибочного кода, который часто пишут новички при попытке рассчитать сумму в цикле:


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

Выясним основные причины, почему данный код работать не будет или будет работать некорректно:

  1. Некорректная работа с методом Остатки(): Метод менеджера регистра накопления Остатки(Период, Отбор, Порядок, ИмяРесурса) принимает в качестве отбора объект Структура, где ключами являются точные имена измерений регистра в виде строк (например, Номенклатура, Склад), а значениями — фильтруемые ссылки. Прямое обращение через точку к свойствам менеджера недопустимо.
  2. Обращения к базе данных в цикле: Вызов методов получения остатков на каждой итерации цикла многократно увеличивает количество обращений к СУБД, что приводит к резкому падению производительности при росте строк в документе.
  3. Деление на ноль: Если товара нет на остатке к моменту проведения или количество равно нулю, вычисление средней цены завершится аварийным завершением транзакции.
  4. «Проблема копеек» (округление): При списании всего остатка позиции расчетное значение по средней формуле может разойтись с остатком в регистре на копейки. В результате на остатке может остаться нулевое количество с ненулевой суммой.
  5. Логика вида движения: Приходная сумма обычно берется из первичного документа поставщика, а списание по средней себестоимости рассчитывается для движений с видом Расход.

Классическая методика проведения документа через запрос

Разберем по шагам правильный алгоритм реализации проведения документа в процедуре ОбработкаПроведения.

  1. Очистка движений и управление записью: Перед выборкой остатков необходимо включить флаг записи набора движений, очистить его и принудительно записать пустой набор в базу. Это исключает влияние старых движений текущего документа на расчет остатков при его перепроведении.
  2. Установка управляемых блокировок: Для обеспечения согласованности данных и защиты от параллельного изменения остатков другими сеансами используется объект БлокировкаДанных по измерениям регистра.
  3. Формирование временной таблицы: Табличная часть документа передается в запрос и группируется по номенклатуре и складу с суммированием количества. Это устраняет дублирование строк.
  4. Левое соединение с виртуальной таблицей остатков: Выполняется соединение таблицы документа с таблицей РегистрНакопления.ОстаткиТоваров.Остатки на МоментВремени() документа.
  5. Обработка результатов и коррекция копеек: В запросе проверяется условие полного списания: если списываемое количество равно остатку, сумма списания принимается равной всему остатку суммы.

Пример реализации в процедуре ОбработкаПроведения

Посмотрим на пример правильного формирования движений с расчетом себестоимости:


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

Рассмотренный подход гарантирует высокую скорость проведения документов, корректный расчет себестоимости и отсутствие блокировочных конфликтов при параллельной работе пользователей.

← На главную