Как правильно привязать статусы к документам в 1С без перезаписи данных?

Программист 1С v8.3 (Обычные формы) Торговля и дистрибуция
← На главную

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

В процессе реализации этой задачи начинающие разработчики часто совершают архитектурную ошибку: пытаются использовать хранилище настроек для хранения бизнес-данных или генерируют новые уникальные идентификаторы (UUID) для каждой строки при каждой записи. Это приводит к тому, что данные либо перезаписывают друг друга, либо множатся в геометрической прогрессии, засоряя базу данных. Выясним причину этой проблемы и разберем по шагам, как построить надежную и производительную архитектуру для хранения статусов документов.

В чем заключается главная ошибка автора темы?

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

мУникальный = Новый УникальныйИдентификатор;

Этот код выполняется в цикле при каждом нажатии кнопки «Записать». В результате программа генерирует абсолютно случайную последовательность символов, которая никак не связана с конкретной накладной в базе данных. При повторном открытии формы или перезаписи статуса система генерирует новый UUID, из-за чего старая запись в регистре сведений не обновляется, а в базу записывается новая дублирующая строка. Связать эти данные обратно с табличной частью документа становится невозможно.

Главное правило работы со ссылочными типами в 1С: любой ссылочный объект (справочник, документ) уже обладает системным уникальным идентификатором. Ссылка на документ (например, ДокументСсылка.РеализацияТоваровУслуг) сама по себе является идеальным и единственным идентификатором. Нам не нужно изобретать велосипед и генерировать новые UUID — достаточно использовать ссылку на документ в качестве измерения регистра сведений.

Также грубейшей ошибкой является использование ХранилищеНастроекДанныхФорм для хранения статусов. Хранилище настроек предназначено исключительно для сохранения индивидуального интерфейса (например, ширина колонок, положение окон конкретного пользователя). Бизнес-данные, важные для управленческого или оперативного учета, должны храниться строго в специализированных таблицах базы данных — Регистрах сведений.

---

Шаг 1. Проектирование регистра сведений «Статусы документов»

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

Измерения (уникальные ключи записи):

  1. Документ (Тип: ДокументСсылка.РеализацияТоваровУслуг) — главное измерение. В регистре не может существовать двух записей с одинаковым значением в этой колонке. Именно ссылка на документ выступает тем самым неизменяемым идентификатором, который искал автор темы.

Ресурсы (хранят изменяемые значения статусов):

Реквизиты (служебная информация):

---

Шаг 2. Как правильно записать статусы накладных

Для записи изменений из таблицы на форме в созданный регистр сведений лучше всего использовать объект МенеджерЗаписи. Он позволяет адресно обратиться к конкретной строке регистра по значению измерения (нашей ссылке на документ), прочитать текущее состояние, обновить его и записать обратно. Рассмотрим подробнее код процедуры записи:


Процедура КнопкаЗаписатьНажатие(Кнопка)
    
    // Начнем транзакцию для обеспечения целостности данных при массовой записи
    НачатьТранзакцию();
    
    Попытка
        Для Каждого СтрокаТЗ Из ТаблицаСтатуса Цикл
            
            // Проверим, заполнена ли ссылка на документ в строке
            Если Не ЗначениеЗаполнено(СтрокаТЗ.Документ) Тогда
                Продолжить;
            КонецЕсли;
            
            // Создаем менеджер записи для работы с конкретной строкой регистра
            МенеджерЗаписи = РегистрыСведений.СтатусыДокументов.СоздатьМенеджерЗаписи();
            
            // Устанавливаем ключевое измерение — это свяжет запись с нашей накладной
            МенеджерЗаписи.Документ = СтрокаТЗ.Документ;
            
            // Пытаемся прочитать существующую запись из базы данных
            МенеджерЗаписи.Прочитать();
            
            // Если запись существует, ее ресурсы будут заполнены. 
            // Перезаписываем их актуальными значениями из строки нашей таблицы на форме
            ЗаполнитьЗначенияСвойств(МенеджерЗаписи, СтрокаТЗ);
            
            // Заполняем служебные реквизиты
            МенеджерЗаписи.Пользователь = ПараметрыСеанса.ТекущийПользователь;
            МенеджерЗаписи.ПериодРегистрации = ТекущаяДата();
            
            // Записываем данные. Параметр Истина означает замещение существующей записи
            МенеджерЗаписи.Записать(Истина);
            
        КонецЦикла;
        
        ЗафиксироватьТранзакцию();
        ПоказатьОповещениеПользователя("Успех", , "Статусы документов успешно сохранены!");
        
    Исключение
        ОтменитьТранзакцию();
        ВызватьИсключение "Ошибка при сохранении статусов документов: " + ОписаниеОшибки();
    КонецПопытки;
    
КонецПроцедуры

При использовании этого подхода данные гарантированно не будут задваиваться. Если для накладной №15 уже существовали статусы в базе, МенеджерЗаписи найдет их по измерению Документ и просто обновит значения ресурсов. Если записи еще не было — система создаст новую строку.

---

Шаг 3. Как быстро загрузить статусы при выборе Маршрутного Задания

Еще одна частая ошибка — это построчное считывание статусов из базы данных в цикле. Это создает колоссальную нагрузку на СУБД (проблема «N+1 запросов»). Правильным решением является использование одного запроса с механизмом ЛЕВОГО СОЕДИНЕНИЯ (Left Join).

Когда пользователь выбирает определенное Маршрутное Задание, мы должны запросом получить все накладные из табличной части этого задания и одновременно соединить их с актуальными статусами из нашего регистра сведений. Проанализируем правильный алгоритм получения данных:


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

Функция ЕСТЬNULL в запросе играет важную роль: если для конкретной накладной в регистре еще нет записи (статусы никогда не проставлялись), левое соединение вернет значение NULL. С помощью ЕСТЬNULL мы заменяем его на стандартное значение ЛОЖЬ для булевых полей и пустую строку для комментариев, что предотвращает ошибки интерфейса.

---

Шаг 4. Настройка визуального отображения флажков (для Обычных форм)

Поскольку автор использует обычные формы (на что указывают конструкции типа ЭлементыФормы.ТаблицаСтатуса и процедура ПриОткрытии), колонки с булевыми типами данных по умолчанию будут отображаться в виде текста «Да/Нет». Чтобы превратить их в удобные интерактивные чекбоксы (галочки), которые переключаются в один клик мышкой, напишем вспомогательную процедуру настройки колонок:


Процедура НастроитьОтображениеФлажков()
    
    МассивКолонокФлажков = Новый Массив;
    МассивКолонокФлажков.Добавить("Вернул");
    МассивКолонокФлажков.Добавить("НакладнаяЗакрыта");
    МассивКолонокФлажков.Добавить("БезПечати");
    МассивКолонокФлажков.Добавить("ИзменениеЦены");
    МассивКолонокФлажков.Добавить("НакладнаяСКорректировкой");
    
    Для Каждого ИмяКолонки Из МассивКолонокФлажков Цикл
        Колонка = ЭлементыФормы.ТаблицаСтатуса.Колонки.Найти(ИмяКолонки);
        Если Колонка <> Неопределено Тогда
            // Настраиваем режим быстрого редактирования кликом
            Колонка.РежимРедактирования = РежимРедактированияКолонки.Непосредственно;
            // Указываем элемент управления "Флажок"
            Колонка.УстановитьЭлементУправления(Тип("Флажок"));
            // Связываем состояние флажка с данными колонки
            Колонка.ДанныеФлажка = ИмяКолонки;
            // Очищаем текстовое представление данных, чтобы не выводилось "Да" или "Нет" рядом с флажком
            Колонка.Данные = "";
        КонецЕсли;
    КонецЦикла;
    
КонецПроцедуры

Эту процедуру необходимо вызывать каждый раз после того, как табличное поле на форме перестраивает свои колонки (например, после выполнения метода СоздатьКолонки() при динамической смене маршрутного задания).

---

Подведем итоги

Мы полностью перепроектировали логику работы со статусами документов, исправив архитектурные изъяны первоначального кода автора. Резюмируем ключевые выводы:

← На главную