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