Разработчики 1С часто сталкиваются с ситуацией, когда один и тот же запрос, выполненный в модуле управляемой формы и в модуле объекта (например, в обработчике ОбработкаПроведения), возвращает совершенно разные результаты. Особенно это заметно при работе с виртуальными таблицами остатков регистров накопления. В этой статье мы подробно разберем, почему так происходит, какие подводные камни таит в себе транзакционный механизм платформы, почему перепроведение документов из кода других документов является опасным антипаттерном, и как технически грамотно реализовать задачу динамического изменения визуального статуса (цвета) документов в списках.
Рассмотрим классическую ситуацию. У нас есть запрос, который обращается к виртуальной таблице остатков регистра накопления:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЛимитныеКартыОстатки.Номенклатура КАК Номенклатура,
| ЛимитныеКартыОстатки.КоличествоПервоначальноеОстаток КАК КоличествоПервоначальноеОстаток
|ИЗ
| РегистрНакопления.ЛимитныеКарты.Остатки(, ) КАК ЛимитныеКартыОстатки";
РезультатЗапроса = Запрос.Выполнить();
Таблица = РезультатЗапроса.Выгрузить();
Когда мы выполняем этот запрос из модуля формы, он возвращает актуальные остатки с учетом всех проведенных документов. Однако при вызове этого же запроса из обработчика ОбработкаПроведения проводимого документа мы обнаруживаем, что новые движения текущего документа не учитываются в результатах запроса, либо данные выглядят устаревшими. Почему так происходит?
Всё дело в механизме фиксации транзакций и записи движений. По умолчанию платформа 1С:Предприятие производит фактическую запись сформированных движений документа в базу данных после успешного завершения процедуры ОбработкаПроведения (непосредственно перед фиксацией транзакции базы данных). В процессе выполнения самого кода процедуры движения находятся только в оперативной памяти — в коллекции Движения текущего объекта.
Если мы выполняем запрос к виртуальной таблице остатков внутри ОбработкаПроведения, СУБД делает выборку из физических таблиц базы данных. Поскольку текущие движения еще не записаны в БД, СУБД просто "не знает" о них, и мы получаем остаток без учета проводимого документа.
Для решения этой проблемы существует метод Движения.Записать() (или запись конкретного набора, например Движения.ЛимитныеКарты.Записать()). Если вызвать его перед выполнением запроса, движения будут принудительно записаны в СУБД в рамках открытой транзакции, и последующий запрос их увидит. Однако использовать этот метод в обработчиках проведения крайне не рекомендуется.
Согласно стандартам разработки фирмы 1С, явный вызов метода Записать() для движений внутри транзакции проведения — это частая причина возникновения взаимных блокировок (deadlocks) при параллельной работе пользователей. Платформа оптимизирует порядок записи таблиц при автоматическом проведении, а ручной вызов нарушает эту последовательность. Использовать явную запись допускается только в редких, исключительных случаях, тщательно проектируя управляемые блокировки.
Часто для решения задачи "отгрузки по заказу" разработчики пытаются применить следующую схему: первый документ (например, "Заказ") делает движение в плюс. Второй документ ("Отгрузка") делает движение в минус. В процедуре ОбработкаПроведения документа "Отгрузка" разработчик пытается программно перепровести или перезаписать "Заказ", чтобы обновить его статус (например, пометить его как "полностью отгруженный" или изменить цвет его строки в списке на серый).
Проанализируем, почему этот подход является крайне опасным для стабильности системы:
Запомним правило архитектуры 1С: Проведение одного документа не должно неявно приводить к перезаписи или перепроведению других документов.
Как же правильно решить задачу изменения визуального статуса заказа при его полной отгрузке? Рассмотрим два наиболее надежных и производительных способа.
Это самый простой и надежный вариант. Мы будем фиксировать статус выполнения заказа в специальном регистре сведений, а затем использовать эти данные для динамического оформления списка.
Создадим новый независимый регистр сведений, назовем его, например, СтатусыЗаказов.
Установим следующие свойства:
Заказ (Тип: ДокументСсылка.ЗаказПокупателя) — сделаем его ведущим.ПолностьюОтгружен (Тип: Булево).В модуле проведения документа "Отгрузка" (наш документ "Б"), после формирования движений по регистрам накопления, мы определяем, закрыт ли заказ полностью. Если заказ закрыт, мы делаем запись в созданный регистр сведений. Обратите внимание, что мы просто пишем статус в регистр сведений, но не трогаем сам документ "Заказ"!
Приведем пример кода в обработчике проведения документа "Отгрузка":
// Определяем, полностью ли закрыт заказ (например, на основе анализа остатков)
// ... код расчета остатка по заказу ...
Если ЗаказПолностьюЗакрыт Тогда
// Создаем менеджер записи для быстрой и безопасной работы с регистром сведений
МенеджерЗаписи = РегистрыСведений.СтатусыЗаказов.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Заказ = СсылкаНаЗаказ;
МенеджерЗаписи.ПолностьюОтгружен = Истина;
МенеджерЗаписи.Записать(Истина); // Запись происходит в текущей транзакции
КонецЕсли;
Теперь нам нужно визуально выделить такой заказ в форме списка документов "Заказ покупателя". Для этого мы настроим динамический список формы.
Откроем свойства динамического списка формы списка заказов и перейдем к его запросу. Изменим запрос, добавив ЛЕВОЕ СОЕДИНЕНИЕ с нашей таблицей регистра сведений:
ВЫБРАТЬ
ДокументЗаказ.Ссылка КАК Ссылка,
ДокументЗаказ.Дата КАК Дата,
ДокументЗаказ.Номер КАК Номер,
ЕСТЬNULL(Статусы.ПолностьюОтгружен, ЛОЖЬ) КАК ПолностьюОтгружен
ИЗ
Документ.ЗаказПокупателя КАК ДокументЗаказ
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СтатусыЗаказов КАК Статусы
ПО ДокументЗаказ.Ссылка = Статусы.Заказ
Далее перейдем в настройки динамического списка во вкладку Условное оформление и добавим новое правило:
ПолностьюОтгружен Равно Истина.Этот способ работает мгновенно, не вызывает конфликтов блокировок и не зависит от закрытых периодов, так как регистр сведений является независимым.
Если статус "полностью отгружен" можно однозначно определить по регистру накопления (например, когда количество заказанного товара минус количество отгруженного равно нулю), мы можем вообще обойтись без создания дополнительных регистров сведений.
Посмотрим, как настроить запрос динамического списка в форме списка заказов, используя виртуальную таблицу остатков регистра накопления ЗаказыПокупателей.Остатки:
ВЫБРАТЬ
ДокументЗаказ.Ссылка КАК Ссылка,
ДокументЗаказ.Дата КАК Дата,
ДокументЗаказ.Номер КАК Номер,
ВЫБОР
КОГДА Остатки.КоличествоОстаток ЕСТЬ NULL ТОГДА ИСТИНА
ИНАЧЕ ЛОЖЬ
КОНЕЦ КАК ПолностьюОтгружен
ИЗ
Документ.ЗаказПокупателя КАК ДокументЗаказ
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ЗаказыПокупателей.Остатки(, ) КАК Остатки
ПО ДокументЗаказ.Ссылка = Остатки.Заказ
Важная техническая особенность: Виртуальные таблицы остатков не хранят строки с нулевыми итогами. Если по заказу весь товар успешно отгружен, остаток становится равным нулю, и СУБД вообще не возвращает строку для данного заказа. В запросе динамического списка мы перехватываем это состояние с помощью проверки на NULL (Остатки.КоличествоОстаток ЕСТЬ NULL).
После этого аналогичным образом настраиваем условное оформление динамического списка: если поле ПолностьюОтгружен равно Истина, красим строку списка в серый цвет. Данный подход является полностью динамическим и не требует хранения избыточных статусов в базе данных.
В процессе реализации интерфейсных решений программисты часто сталкиваются с техническим нюансом: скопированный из другого места код обработчика события (например, процедуры ПриОткрытии или ПриСозданииНаСервере) просто перестает работать, и точки останова в нем не срабатывают.
Проанализируем причину этого поведения управляемых форм. В платформе 1С:Предприятие 8 простого описания процедуры в модуле формы недостаточно для ее выполнения. Каждое событие должно быть жестко связано с элементом интерфейса через палитру свойств.
Если вы скопировали текст процедуры в модуль вручную, связь между интерфейсным событием и кодом не создалась. Чтобы решить эту проблему, выполните следующие шаги:
ПриСозданииНаСервере или ПриОткрытии).Также стоит помнить о методологическом правиле разработки управляемых форм: все первоначальные настройки динамических списков, условного оформления, установку параметров запросов и отборов следует производить строго в обработчике ПриСозданииНаСервере, а не в клиентском событии ПриОткрытии. Это позволяет выполнить всю подготовительную работу на стороне сервера за один сеанс обращения и передать клиенту уже полностью готовую, оптимизированную форму, снижая нагрузку на сеть и повышая скорость работы интерфейса.