При изучении 14-го занятия популярного учебника М. Г. Радченко «1С:Предприятие 8. Практическое пособие разработчика», посвященного оптимизации проведения документов и автоматическому расчету себестоимости списания материалов, начинающие разработчики часто сталкиваются со специфическими ошибками. Эти ошибки вызваны как невнимательностью при ручном вводе кода, так и непониманием разницы между полями выборки запроса и локальными переменными модуля.
В этой статье мы подробно разберем, почему возникают ошибки «Поле не найдено "НоменклатураДокумента.ВидНоменклатура"» и «Поле объекта не обнаружено (СтоимостьМатериала)», как их правильно исправить, а также рассмотрим важные концепции 14-го занятия, которые часто упускаются из виду.
Давайте пошагово проанализируем ситуацию, выясним причины сбоев и приведем код обработчика проведения к полностью рабочему и методически верному состоянию.
Сначала разберем первую ошибку, с которой столкнулся автор темы при выполнении второго запроса:
{(3, 24)}: Поле не найдено "НоменклатураДокумента.ВидНоменклатура"
НоменклатураДокумента.<<?>>ВидНоменклатура КАК ВидНоменклатура
Выясним причину ее возникновения. Для этого внимательно проанализируем первый запрос, в котором создается временная таблица НоменклатураДокумента:
"ВЫБРАТЬ
| ОказаниеУслугПереченьНоменклатуры.Номенклатура КАК Номенклатура,
| ОказаниеУслугПереченьНоменклатуры.Номенклатура.ВидНоменклатуры КАК ВидНоменклатуры,
| СУММА(ОказаниеУслугПереченьНоменклатуры.Количество) КАК КоличествоВДокументе,
| СУММА(ОказаниеУслугПереченьНоменклатуры.Сумма) КАК СуммаВДокументе
|ПОМЕСТИТЬ НоменклатураДокумента
|ИЗ
| Документ.ОказаниеУслуг.ПереченьНоменклатуры КАК ОказаниеУслугПереченьНоменклатуры
..."
Обратите внимание на вторую строку запроса. В ней выбирается поле ВидНоменклатуры (во множественном числе) и помещается во временную таблицу именно под этим именем.
Теперь посмотрим на текст второго запроса, где автор пытается прочитать данные из созданной временной таблицы:
Запрос2.Текст = "ВЫБРАТЬ
| НоменклатураДокумента.Номенклатура КАК Номенклатура,
| НоменклатураДокумента.ВидНоменклатура КАК ВидНоменклатура, // Ошибка здесь!
..."
Проанализировав эту строку, мы видим банальную опечатку: вместо поля ВидНоменклатуры (которое реально существует во временной таблице), автор пытается обратиться к полю ВидНоменклатура (в единственном числе). Поскольку платформа 1С не находит во временной таблице поля с таким именем, она прерывает выполнение транзакции и сообщает об ошибке.
Решение: необходимо заменить во втором запросе имя поля ВидНоменклатура на правильное — ВидНоменклатуры. Строка должна выглядеть следующим образом:
НоменклатураДокумента.ВидНоменклатуры КАК ВидНоменклатуры,
После успешного исправления первой опечатки разработчики часто наступают на вторые «грабли». Платформа выдает ошибку при обходе выборки в цикле:
{Документ.ОказаниеУслуг.МодульОбъекта(105)}: Поле объекта не обнаружено (СтоимостьМатериала)
Движение.Стоимость = ВыборкаДетальныеЗаписи.СтоимостьМатериала * ВыборкаДетальныеЗаписи.КоличествоВДокументе;
Рассмотрим подробнее механизм работы цикла, написанного автором. Внутри цикла происходит расчет стоимости единицы списываемого материала:
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Если ВыборкаДетальныеЗаписи.Количество = 0 Тогда
СтоимостьМатериалов = 0; // Создается локальная переменная СтоимостьМатериалов (множ. число)
Иначе
СтоимостьМатериалов = ВыборкаДетальныеЗаписи.Стоимость / ВыборкаДетальныеЗаписи.Количество;
КонецЕсли;
...
Здесь допущены сразу две серьезные методологические ошибки:
ВыборкаДетальныеЗаписи.СтоимостьМатериала. Приставка ВыборкаДетальныеЗаписи. заставляет платформу искать поле с именем СтоимостьМатериала в самом результате запроса (в выборке). Но в запросе такого поля нет — себестоимость единицы рассчитывается динамически в коде! К рассчитанной переменной необходимо обращаться напрямую, без указания объекта выборки.СтоимостьМатериалов (во множественном числе), а при формировании движений пытается перемножить количество на переменную СтоимостьМатериала (в единственном числе). В языке 1С это две абсолютно разные, несвязанные между собой переменные.Решение: Нам нужно привести имена переменных к единому стандарту (строго по книге Радченко — в единственном числе СтоимостьМатериала) и убрать ошибочное обращение к выборке запроса при расчете себестоимости списания.
Посмотрим на правильный вариант реализации этого логического блока:
// Корректный расчет стоимости единицы материала
Если ВыборкаДетальныеЗаписи.Количество = 0 Тогда
СтоимостьМатериала = 0;
Иначе
СтоимостьМатериала = ВыборкаДетальныеЗаписи.Стоимость / ВыборкаДетальныеЗаписи.Количество;
КонецЕсли;
...
// Правильное формирование движений по регистру "СтоимостьМатериалов"
Движение.Стоимость = ВыборкаДетальныеЗаписи.КоличествоВДокументе * СтоимостьМатериала;
Если мы внимательно посмотрим на код, предоставленный автором темы на форуме, то заметим, что он не просто совершил опечатки, но и полностью упустил ключевые архитектурные концепции 14-го занятия. Цель этого урока у Радченко — не просто посчитать цифры, а оптимизировать проведение по «новой методике» и обеспечить стабильную работу системы в многопользовательском режиме.
Проанализируем, каких важных механизмов не хватает в коде автора:
В реальной рабочей базе данных несколько пользователей могут одновременно проводить документы «Оказание услуг» с пересекающимися позициями номенклатуры. Если не установить блокировку данных, система может допустить ситуацию, когда две параллельные транзакции прочитают один и тот же остаток товара на складе и дважды спишут его, уведя остатки в глубокий минус.
Перед тем как записывать пустые наборы записей для очистки старых движений документа, необходимо заблокировать соответствующие регистры накопления для изменения:
Движения.ОстаткиМатериалов.Записывать = Истина;
Движения.СтоимостьМатериалов.Записывать = Истина;
Движения.Продажи.Записывать = Истина;
// Установка управляемой блокировки данных
Движения.ОстаткиМатериалов.БлокироватьДляИзменения = Истина;
Движения.СтоимостьМатериалов.БлокироватьДляИзменения = Истина;
// И только после этого производим запись пустых наборов
Движения.СтоимостьМатериалов.Записать();
Движения.ОстаткиМатериалов.Записать();
Старая методика проведения сначала проверяла наличие остатков на складе запросом, и только в случае их достаточности делала движения по расходу. Это требовало избыточных блокировок «на чтение».
Новая методика, рассматриваемая на 14-м занятии, работает по иному, более эффективному принципу:
Движения.Записать().Запрос3), который проверяет, не ушли ли остатки товаров по регистру ОстаткиМатериалов в минус после выполнения этой записи.Отказ = Истина, и платформа автоматически откатывает всю транзакцию проведения назад. Движения в базе данных не сохраняются.В коде автора темы на форуме проверка остатков после записи движений полностью отсутствует, что сводит на нет всю пользу от прохождения 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.Выбрать();
Пока ВыборкаОшибок.Следующий() Цикл
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "Недостаточно материала """ + ВыборкаОшибок.МатериалПредставление +
""" в количестве " + (-ВыборкаОшибок.КоличествоОстаток);
Сообщение.Сообщить();
Отказ = Истина;
КонецЦикла;
КонецЕсли;
КонецПроцедуры
Мы подробно разобрали причины возникновения ошибок при расчете стоимости материалов. Для успешного прохождения уроков в будущем рекомендуем придерживаться следующих правил:
ВидНоменклатура и ВидНоменклатуры — наиболее частая причина сбоев у новичков. Названия полей во всех связках запросов должны совпадать символ в символ.ВыборкаДетальныеЗаписи.Количество). К локальным переменным, которые мы рассчитали внутри самого кода 1С, нужно обращаться напрямую (например, СтоимостьМатериала).