Почему запрос к виртуальной таблице остатков в модуле объекта возвращает неверные данные и как правильно обновлять статусы связанных документов?

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

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

Выясняем причину: особенности работы виртуальных таблиц в транзакции

Рассмотрим классическую ситуацию. У нас есть запрос, который обращается к виртуальной таблице остатков регистра накопления:


Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ
|	ЛимитныеКартыОстатки.Номенклатура КАК Номенклатура,
|	ЛимитныеКартыОстатки.КоличествоПервоначальноеОстаток КАК КоличествоПервоначальноеОстаток
|ИЗ
|	РегистрНакопления.ЛимитныеКарты.Остатки(, ) КАК ЛимитныеКартыОстатки";
			
РезультатЗапроса = Запрос.Выполнить(); 
Таблица = РезультатЗапроса.Выгрузить();

Когда мы выполняем этот запрос из модуля формы, он возвращает актуальные остатки с учетом всех проведенных документов. Однако при вызове этого же запроса из обработчика ОбработкаПроведения проводимого документа мы обнаруживаем, что новые движения текущего документа не учитываются в результатах запроса, либо данные выглядят устаревшими. Почему так происходит?

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

Если мы выполняем запрос к виртуальной таблице остатков внутри ОбработкаПроведения, СУБД делает выборку из физических таблиц базы данных. Поскольку текущие движения еще не записаны в БД, СУБД просто "не знает" о них, и мы получаем остаток без учета проводимого документа.

Для решения этой проблемы существует метод Движения.Записать() (или запись конкретного набора, например Движения.ЛимитныеКарты.Записать()). Если вызвать его перед выполнением запроса, движения будут принудительно записаны в СУБД в рамках открытой транзакции, и последующий запрос их увидит. Однако использовать этот метод в обработчиках проведения крайне не рекомендуется.

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

Опасный антипаттерн: проведение документа «А» из модуля проведения документа «Б»

Часто для решения задачи "отгрузки по заказу" разработчики пытаются применить следующую схему: первый документ (например, "Заказ") делает движение в плюс. Второй документ ("Отгрузка") делает движение в минус. В процедуре ОбработкаПроведения документа "Отгрузка" разработчик пытается программно перепровести или перезаписать "Заказ", чтобы обновить его статус (например, пометить его как "полностью отгруженный" или изменить цвет его строки в списке на серый).

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

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

Запомним правило архитектуры 1С: Проведение одного документа не должно неявно приводить к перезаписи или перепроведению других документов.

Правильные архитектурные решения задачи изменения статусов

Как же правильно решить задачу изменения визуального статуса заказа при его полной отгрузке? Рассмотрим два наиболее надежных и производительных способа.

Способ 1. Использование независимого регистра сведений

Это самый простой и надежный вариант. Мы будем фиксировать статус выполнения заказа в специальном регистре сведений, а затем использовать эти данные для динамического оформления списка.

Создадим новый независимый регистр сведений, назовем его, например, СтатусыЗаказов. Установим следующие свойства:

В модуле проведения документа "Отгрузка" (наш документ "Б"), после формирования движений по регистрам накопления, мы определяем, закрыт ли заказ полностью. Если заказ закрыт, мы делаем запись в созданный регистр сведений. Обратите внимание, что мы просто пишем статус в регистр сведений, но не трогаем сам документ "Заказ"!

Приведем пример кода в обработчике проведения документа "Отгрузка":


// Определяем, полностью ли закрыт заказ (например, на основе анализа остатков)
// ... код расчета остатка по заказу ...

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

Теперь нам нужно визуально выделить такой заказ в форме списка документов "Заказ покупателя". Для этого мы настроим динамический список формы.

Откроем свойства динамического списка формы списка заказов и перейдем к его запросу. Изменим запрос, добавив ЛЕВОЕ СОЕДИНЕНИЕ с нашей таблицей регистра сведений:


ВЫБРАТЬ
	ДокументЗаказ.Ссылка КАК Ссылка,
	ДокументЗаказ.Дата КАК Дата,
	ДокументЗаказ.Номер КАК Номер,
	ЕСТЬNULL(Статусы.ПолностьюОтгружен, ЛОЖЬ) КАК ПолностьюОтгружен
ИЗ
	Документ.ЗаказПокупателя КАК ДокументЗаказ
	ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СтатусыЗаказов КАК Статусы
		ПО ДокументЗаказ.Ссылка = Статусы.Заказ

Далее перейдем в настройки динамического списка во вкладку Условное оформление и добавим новое правило:

  1. Оформление: Установим цвет текста в серый (например, Web-цвет "Серый" или конкретный оттенок RGB).
  2. Условие: Добавим условие ПолностьюОтгружен Равно Истина.
  3. Оформляемые поля: Выберем поля строки, которые должны окрашиваться (или оставим пустым для окрашивания всей строки).

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

Способ 2. Динамический расчет остатков непосредственно в списке

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

Посмотрим, как настроить запрос динамического списка в форме списка заказов, используя виртуальную таблицу остатков регистра накопления ЗаказыПокупателей.Остатки:


ВЫБРАТЬ
	ДокументЗаказ.Ссылка КАК Ссылка,
	ДокументЗаказ.Дата КАК Дата,
	ДокументЗаказ.Номер КАК Номер,
	ВЫБОР 
		КОГДА Остатки.КоличествоОстаток ЕСТЬ NULL ТОГДА ИСТИНА 
		ИНАЧЕ ЛОЖЬ 
	КОНЕЦ КАК ПолностьюОтгружен
ИЗ
	Документ.ЗаказПокупателя КАК ДокументЗаказ
	ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ЗаказыПокупателей.Остатки(, ) КАК Остатки
		ПО ДокументЗаказ.Ссылка = Остатки.Заказ

Важная техническая особенность: Виртуальные таблицы остатков не хранят строки с нулевыми итогами. Если по заказу весь товар успешно отгружен, остаток становится равным нулю, и СУБД вообще не возвращает строку для данного заказа. В запросе динамического списка мы перехватываем это состояние с помощью проверки на NULL (Остатки.КоличествоОстаток ЕСТЬ NULL).

После этого аналогичным образом настраиваем условное оформление динамического списка: если поле ПолностьюОтгружен равно Истина, красим строку списка в серый цвет. Данный подход является полностью динамическим и не требует хранения избыточных статусов в базе данных.

Решаем проблему с неработающими обработчиками событий формы

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

Проанализируем причину этого поведения управляемых форм. В платформе 1С:Предприятие 8 простого описания процедуры в модуле формы недостаточно для ее выполнения. Каждое событие должно быть жестко связано с элементом интерфейса через палитру свойств.

Если вы скопировали текст процедуры в модуль вручную, связь между интерфейсным событием и кодом не создалась. Чтобы решить эту проблему, выполните следующие шаги:

  1. Откройте форму документа или списка в конфигураторе.
  2. В окне элементов или в структуре формы выберите сам корневой элемент (Форма) или конкретный элемент управления.
  3. Откройте палитру свойств (клавиша F4).
  4. Найдите раздел "События" и найдите нужное событие (например, ПриСозданииНаСервере или ПриОткрытии).
  5. Нажмите на кнопку с лупой рядом с полем события. Платформа предложит связать событие с существующей процедурой модуля или создать новую. Выберите вашу скопированную процедуру из списка.

Также стоит помнить о методологическом правиле разработки управляемых форм: все первоначальные настройки динамических списков, условного оформления, установку параметров запросов и отборов следует производить строго в обработчике ПриСозданииНаСервере, а не в клиентском событии ПриОткрытии. Это позволяет выполнить всю подготовительную работу на стороне сервера за один сеанс обращения и передать клиенту уже полностью готовую, оптимизированную форму, снижая нагрузку на сеть и повышая скорость работы интерфейса.

← На главную