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