Как исправить ошибки при расчете себестоимости в документе «Оказание услуг» по 14 занятию книги Радченко?

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

При изучении 14-го занятия популярного учебника М. Г. Радченко «1С:Предприятие 8. Практическое пособие разработчика», посвященного оптимизации проведения документов и автоматическому расчету себестоимости списания материалов, начинающие разработчики часто сталкиваются со специфическими ошибками. Эти ошибки вызваны как невнимательностью при ручном вводе кода, так и непониманием разницы между полями выборки запроса и локальными переменными модуля.

В этой статье мы подробно разберем, почему возникают ошибки «Поле не найдено "НоменклатураДокумента.ВидНоменклатура"» и «Поле объекта не обнаружено (СтоимостьМатериала)», как их правильно исправить, а также рассмотрим важные концепции 14-го занятия, которые часто упускаются из виду.

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

Ошибка 1. Поле не найдено "НоменклатураДокумента.ВидНоменклатура"

Сначала разберем первую ошибку, с которой столкнулся автор темы при выполнении второго запроса:


{(3, 24)}: Поле не найдено "НоменклатураДокумента.ВидНоменклатура"
НоменклатураДокумента.<<?>>ВидНоменклатура КАК ВидНоменклатура

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


"ВЫБРАТЬ
|    ОказаниеУслугПереченьНоменклатуры.Номенклатура КАК Номенклатура,
|    ОказаниеУслугПереченьНоменклатуры.Номенклатура.ВидНоменклатуры КАК ВидНоменклатуры,
|    СУММА(ОказаниеУслугПереченьНоменклатуры.Количество) КАК КоличествоВДокументе,
|    СУММА(ОказаниеУслугПереченьНоменклатуры.Сумма) КАК СуммаВДокументе
|ПОМЕСТИТЬ НоменклатураДокумента
|ИЗ
|    Документ.ОказаниеУслуг.ПереченьНоменклатуры КАК ОказаниеУслугПереченьНоменклатуры
..."

Обратите внимание на вторую строку запроса. В ней выбирается поле ВидНоменклатуры (во множественном числе) и помещается во временную таблицу именно под этим именем.

Теперь посмотрим на текст второго запроса, где автор пытается прочитать данные из созданной временной таблицы:


Запрос2.Текст = "ВЫБРАТЬ
|    НоменклатураДокумента.Номенклатура КАК Номенклатура,
|    НоменклатураДокумента.ВидНоменклатура КАК ВидНоменклатура, // Ошибка здесь!
..."

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

Решение: необходимо заменить во втором запросе имя поля ВидНоменклатура на правильное — ВидНоменклатуры. Строка должна выглядеть следующим образом:

НоменклатураДокумента.ВидНоменклатуры КАК ВидНоменклатуры,

Ошибка 2. Поле объекта не обнаружено (СтоимостьМатериала)

После успешного исправления первой опечатки разработчики часто наступают на вторые «грабли». Платформа выдает ошибку при обходе выборки в цикле:


{Документ.ОказаниеУслуг.МодульОбъекта(105)}: Поле объекта не обнаружено (СтоимостьМатериала)
Движение.Стоимость = ВыборкаДетальныеЗаписи.СтоимостьМатериала * ВыборкаДетальныеЗаписи.КоличествоВДокументе;

Рассмотрим подробнее механизм работы цикла, написанного автором. Внутри цикла происходит расчет стоимости единицы списываемого материала:


Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
    Если ВыборкаДетальныеЗаписи.Количество = 0 Тогда
        СтоимостьМатериалов = 0; // Создается локальная переменная СтоимостьМатериалов (множ. число)
    Иначе
        СтоимостьМатериалов = ВыборкаДетальныеЗаписи.Стоимость / ВыборкаДетальныеЗаписи.Количество;
    КонецЕсли;
    ...

Здесь допущены сразу две серьезные методологические ошибки:

  1. Неверная область видимости при обращении к переменной. В строке записи движения по регистру стоимости автор пишет: ВыборкаДетальныеЗаписи.СтоимостьМатериала. Приставка ВыборкаДетальныеЗаписи. заставляет платформу искать поле с именем СтоимостьМатериала в самом результате запроса (в выборке). Но в запросе такого поля нет — себестоимость единицы рассчитывается динамически в коде! К рассчитанной переменной необходимо обращаться напрямую, без указания объекта выборки.
  2. Опечатка в названии переменной (единственное и множественное число). При расчете налога или себестоимости автор присваивает результат переменной СтоимостьМатериалов (во множественном числе), а при формировании движений пытается перемножить количество на переменную СтоимостьМатериала (в единственном числе). В языке 1С это две абсолютно разные, несвязанные между собой переменные.

Решение: Нам нужно привести имена переменных к единому стандарту (строго по книге Радченко — в единственном числе СтоимостьМатериала) и убрать ошибочное обращение к выборке запроса при расчете себестоимости списания.

Посмотрим на правильный вариант реализации этого логического блока:


// Корректный расчет стоимости единицы материала
Если ВыборкаДетальныеЗаписи.Количество = 0 Тогда
    СтоимостьМатериала = 0;
Иначе
    СтоимостьМатериала = ВыборкаДетальныеЗаписи.Стоимость / ВыборкаДетальныеЗаписи.Количество;
КонецЕсли;

...

// Правильное формирование движений по регистру "СтоимостьМатериалов"
Движение.Стоимость = ВыборкаДетальныеЗаписи.КоличествоВДокументе * СтоимостьМатериала;

Что еще упущено? Важные концепции 14-го занятия

Если мы внимательно посмотрим на код, предоставленный автором темы на форуме, то заметим, что он не просто совершил опечатки, но и полностью упустил ключевые архитектурные концепции 14-го занятия. Цель этого урока у Радченко — не просто посчитать цифры, а оптимизировать проведение по «новой методике» и обеспечить стабильную работу системы в многопользовательском режиме.

Проанализируем, каких важных механизмов не хватает в коде автора:

1. Настройка управляемых блокировок

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

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


Движения.ОстаткиМатериалов.Записывать = Истина;
Движения.СтоимостьМатериалов.Записывать = Истина;
Движения.Продажи.Записывать = Истина;

// Установка управляемой блокировки данных
Движения.ОстаткиМатериалов.БлокироватьДляИзменения = Истина;
Движения.СтоимостьМатериалов.БлокироватьДляИзменения = Истина;

// И только после этого производим запись пустых наборов
Движения.СтоимостьМатериалов.Записать();
Движения.ОстаткиМатериалов.Записать();

2. Реализация «Новой методики» проведения и контроль отрицательных остатков

Старая методика проведения сначала проверяла наличие остатков на складе запросом, и только в случае их достаточности делала движения по расходу. Это требовало избыточных блокировок «на чтение».

Новая методика, рассматриваемая на 14-м занятии, работает по иному, более эффективному принципу:

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

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

Полный корректный код процедуры «ОбработкаПроведения»

Давайте объединим все исправления, устраним опечатки, добавим управляемые блокировки и контроль отрицательных остатков согласно методическим рекомендациям М. Г. Радченко. В результате мы получим чистый, работающий и отказоустойчивый код.

Посмотрим на итоговый вариант процедуры ОбработкаПроведения в модуле объекта документа ОказаниеУслуг:


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

КонецПроцедуры

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

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

← На главную