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