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