При разработке подсистем планирования и расписания (например, записи пациентов к врачам, бронирования рабочих мест или оборудования) часто возникает необходимость проверки занятости ресурсов. Разработчики сталкиваются с ошибками при попытке прочитать или перезаписать записи регистра сведений, если у регистра включен режим записи Подчинение регистратору.
Разберем типичные ошибки, возникающие при работе с набором записей подчиненного регистра, выясним причины их появления и рассмотрим архитектурно правильный способ проверки занятости рабочего места.
Проанализируем распространенную ошибку: попытку установить отбор набора записей по измерениям или ресурсам в регистре с подчинением регистратору. При выполнении подобного кода платформа выдает исключение:
Ошибка записи! Не установлен отбор по регистратору (Регистр сведений: ...)
В платформе 1С действует жесткое системное ограничение: для объекта РегистрСведенийНаборЗаписей, подчиненного регистратору, отбор программно может быть установлен только по одному полю — Регистратор. Установить отбор по измерениям (например, Кабинет, Кресло или Дата) через свойства НаборЗаписей.Отбор невозможно.
Рассмотрим, как выглядит синтаксически корректная работа с набором записей подчиненного регистра сведений:
НаборЗаписей = РегистрыСведений.ПроверкаРабочегоМеста.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(ВыбранныйРегистратор);
НаборЗаписей.Прочитать();
Для Каждого Запись Из НаборЗаписей Цикл
// Чтение или изменение реквизитов записи
Сообщить(Строка(Запись.ВремяНачала) + " - " + Строка(Запись.ВремяОкончания));
КонецЦикла;
// Запись набора выполняется только при необходимости модификации движений
НаборЗаписей.Записать();
Если требуется прочитать данные с фильтрацией по конкретным измерениям (например, по креслу или кабинету), использовать набор записей не следует. В таких случаях мы используем объект Запрос, где условия накладываются свободно в секции ГДЕ.
Выясним причину, почему перезапись набора записей вручную через НаборЗаписей.Записать() в модулях форм или перед записью документа приводит к коллизиям. Подчиненный регистр сведений предназначен для хранения движений документов. Жизненный цикл этих записей должен управляться проведением:
ОбработкаПроведения модуля документа через коллекцию Движения.ПроверкаРабочегоМеста.ПередЗаписью) или модуле объекта не должны модифицировать регистр вручную. Они должны только считывать существующие данные запросом для выявления пересечений.Когда требуется исключить назначение двух сотрудников на одно рабочее место в одно и то же время, важно корректно составить математическое условие пересечения отрезков времени. Попытка перечислять множество условий с операторами И и ИЛИ часто приводит к пропускам пересечений.
Два интервала времени [Начало1, Окончание1] и [Начало2, Окончание2] гарантированно пересекаются тогда и только тогда, когда выполняется строгое условие:
Начало1 < Окончание2 И Окончание1 > Начало2
Если соприкосновение интервалов "стык в стык" (например, один прием заканчивается в 14:00, а второй начинается в 14:00) также считается недопустимым, строгие знаки заменяются на нестрогие (<= и >=).
Кроме того, при повторном проведении документа необходимо исключать его собственные движения из выборки с помощью условия Регистр.Регистратор <> &Ссылка, иначе документ будет блокировать сам себя.
Разберем по шагам надежный алгоритм проверки расписания, выполняемый при проведении документа ПакетРасписаний:
Кабинет, Кресло).Отказ = Истина.Посмотрим на пример процедуры проведения в модуле объекта документа:
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
// 1. Устанавливаем управляемую блокировку для защиты от параллельной записи
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.ПроверкаРабочегоМеста");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.ИсточникДанных = ТабПриемов;
ЭлементБлокировки.ИспользоватьПоляИзИсточникаДанных("Кабинет", "Кабинет");
ЭлементБлокировки.ИспользоватьПоляИзИсточникаДанных("НомерКресла", "Кресло");
Блокировка.Заблокировать();
// 2. Проверяем занятость кресел через запрос
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТЧ.НомерСтроки КАК НомерСтроки,
| ТЧ.Кабинет КАК Кабинет,
| ТЧ.НомерКресла КАК Кресло,
| ТЧ.Время1 КАК ВремяНачала,
| ТЧ.Время2 КАК ВремяОкончания
|ПОМЕСТИТЬ ВТ_ТекущиеПриемы
|ИЗ
| &ТабПриемов КАК ТЧ
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Текущие.НомерСтроки КАК НомерСтроки,
| Текущие.Кабинет КАК Кабинет,
| Текущие.Кресло КАК Кресло,
| Регистр.ВремяНачала КАК ЗанятоС,
| Регистр.ВремяОкончания КАК ЗанятоПо
|ИЗ
| ВТ_ТекущиеПриемы КАК Текущие
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ПроверкаРабочегоМеста КАК Регистр
| ПО Текущие.Кабинет = Регистр.Кабинет
| И Текущие.Кресло = Регистр.Кресло
| И (Текущие.ВремяНачала < Регистр.ВремяОкончания)
| И (Текущие.ВремяОкончания > Регистр.ВремяНачала)
| И (Регистр.Регистратор <> &Ссылка)";
Запрос.УстановитьПараметр("ТабПриемов", ТабПриемов.Выгрузить());
Запрос.УстановитьПараметр("Ссылка", Ссылка);
РезультатЗапроса = Запрос.Выполнить();
Если НЕ РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = СтрШаблон(
"Строка %1: Кресло %2 в кабинете %3 уже занято на интервале с %4 по %5!",
Выборка.НомерСтроки,
Выборка.Кресло,
Выборка.Кабинет,
Формат(Выборка.ЗанятоС, "ДФ='dd.MM.yyyy HH:mm'"),
Формат(Выборка.ЗанятоПо, "ДФ='dd.MM.yyyy HH:mm'")
);
Сообщение.Поле = "ТабПриемов";
Сообщение.Сообщить();
КонецЦикла;
Отказ = Истина;
Возврат;
КонецЕсли;
// 3. Формируем движения документа
Движения.ПроверкаРабочегоМеста.Записывать = Истина;
Для Каждого СтрТЧ Из ТабПриемов Цикл
Движение = Движения.ПроверкаРабочегоМеста.Добавить();
Движение.Врач = Врач;
Движение.Кабинет = СтрТЧ.Кабинет;
Движение.Кресло = СтрТЧ.НомерКресла;
Движение.ДатаПриема = НачалоДня(СтрТЧ.Время1);
Движение.ВремяНачала = СтрТЧ.Время1;
Движение.ВремяОкончания = СтрТЧ.Время2;
КонецЦикла;
КонецПроцедуры
Для упрощения логики расписания рассмотрим две архитектурные рекомендации:
Кресла подчиненным справочнику Кабинеты. Тогда кабинет является владельцем кресла, и хранить кабинет отдельным измерением в регистре больше не требуется — он однозначно получается из реквизита Кресло.Владелец.Кресло и ВремяСлота. Это исключает сложные проверки пересечений интервалов: попытка двух документов записать данные на один и тот же слот будет пресекаться платформой автоматически по контролю уникальности измерений.