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

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

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

Анализ типовых ошибок при реализации проверок в управляемых формах

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


&НаКлиенте
Процедура ПроверкаПриПроведении(Отказ, РежимЗаписи, РежимПроведения)
    НомерКресла = ""; 
    Если Не ЭтоНовый() Тогда 
        Запрос = Новый Запрос; 
        // ...

При выполнении такого кода платформа 1С выдает две последовательные ошибки:

  1. Тип не определен (Запрос): объект Запрос существует только в серверном контексте. На тонком клиенте создание экземпляра этого типа невозможно. Любые обращения к базе данных через язык запросов необходимо выносить в процедуры и функции с директивой &НаСервере или &НаСервереБезКонтекста.
  2. Процедура или функция с указанным именем не определена (ЭтоНовый): метод ЭтоНовый() принадлежит объекту прикладного уровня (например, ДокументОбъект). В модуле управляемой формы обращение идет к реквизиту формы Объект (тип ДанныеФормыСтруктураСКоллекцией), у которого такого метода нет. Для проверки того, записан ли документ в базу данных, в модуле формы используется конструкция Объект.Ссылка.Пустая().

Архитектура хранения данных: регистр сведений или регистр накопления?

Выясним причину, почему регистр накопления (РН) с концепцией остатков «Приход/Расход» в данной задаче избыточен. Речь идет не о количественном списании со склада, а о фиксации состояния объекта в определенный момент времени. Для таких целей оптимально подходит Регистр сведений (РС).

Однако при проектировании регистра сведений критически важно правильно распределить измерения и ресурсы. В регистре сведений уникальность записей на определенную дату (или в пределах таблицы непериодического регистра) контролируется исключительно по комбинации всех его измерений.

  1. Ошибочный вариант: сделать измерениями и Сотрудник, и Кресло. В таком случае платформа позволит записать для одного и того же кресла разных сотрудников, так как комбинация значений измерений будет уникальной (например, «Иванов — Кресло 1» и «Петров — Кресло 1»). Контроль уникальности кресла на уровне СУБД работать не будет.
  2. Корректный вариант: назначить Кресло измерением, а Сотрудник — ресурсом. В таком случае на одну дату (или точку периодичности) платформа физически не позволит продублировать запись с тем же самым креслом: новое значение сотрудника либо вызовет ошибку записи, либо перезапишет существующее.

Посмотрим на структуру регистра сведений с периодичностью «Позиция регистратора» или «В пределах дня/секунды»:

  1. Измерения: Кресло (тип СправочникСсылка.Кресла).
  2. Ресурсы: Сотрудник (тип СправочникСсылка.Сотрудники).
  3. Реквизиты/Ресурсы времени: ВремяНачала, ВремяОкончания (если график работы внутри одного дня делится на интервалы).

Реализация оперативной проверки при выборе кресла в форме

Для удобства пользователя проверку следует выполнять не только в момент проведения документа, но и непосредственно при заполнении строки табличной части в событии НомерКреслаПриИзменении. Рассмотрим, как организовать серверный вызов без передачи лишнего контекста формы:


&НаКлиенте
Процедура ТабПриемовНомерКреслаПриИзменении(Элемент)
    СтрокаТЧ = Элементы.ТабПриемов.ТекущиеДанные;
    Если СтрокаТЧ = Неопределено Или Не ЗначениеЗаполнено(СтрокаТЧ.НомерКресла) Тогда
        Возврат;
    КонецЕсли;
    
    // Проверяем дублирование внутри текущего документа
    КоличествоДублей = 0;
    Для Каждого Стр Из Объект.ТабПриемов Цикл
        Если Стр.НомерКресла = СтрокаТЧ.НомерКресла Тогда
            КоличествоДублей = КоличествоДублей + 1;
        КонецЕсли;
    КонецЦикла;
    
    Если КоличествоДублей > 1 Тогда
        ПоказатьОповещениеПользователя("Ошибка выбора",, "Это кресло уже выбрано в текущем документе!");
        СтрокаТЧ.НомерКресла = ПредопределенноеЗначение("Справочник.Кресла.ПустаяСсылка");
        Возврат;
    КонецЕсли;
    
    // Проверяем занятость кресла в базе данных
    ЗанявшийСотрудник = ПроверитьЗанятостьКреслаНаСервере(СтрокаТЧ.НомерКресла, Объект.Ссылка, Объект.Дата);
    Если ЗначениеЗаполнено(ЗанявшийСотрудник) Тогда
        ТекстСообщения = СтрШаблон("Кресло уже занято сотрудником: %1", ЗанявшийСотрудник);
        ПоказатьОповещениеПользователя("Внимание",, ТекстСообщения);
        СтрокаТЧ.НомерКресла = ПредопределенноеЗначение("Справочник.Кресла.ПустаяСсылка");
    КонецЕсли;
КонецПроцедуры

&НаСервереБезКонтекста
Функция ПроверитьЗанятостьКреслаНаСервере(Кресло, ТекущийДокумент, ДатаРасписания)
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ ПЕРВЫЕ 1
        |    ЗанятостьКреселСрезПоследних.Сотрудник КАК Сотрудник
        |ИЗ
        |    РегистрСведений.ЗанятостьКресел.СрезПоследних(&Период, Кресло = &Кресло) КАК ЗанятостьКреселСрезПоследних
        |ГДЕ
        |    ЗанятостьКреселСрезПоследних.Регистратор <> &ТекущийДокумент
        |    И ЗанятостьКреселСрезПоследних.Сотрудник <> ЗНАЧЕНИЕ(Справочник.Сотрудники.ПустаяСсылка)";
        
    Запрос.УстановитьПараметр("Период", ДатаРасписания);
    Запрос.УстановитьПараметр("Кресло", Кресло);
    Запрос.УстановитьПараметр("ТекущийДокумент", ТекущийДокумент);
    
    Выборка = Запрос.Выполнить().Выбрать();
    Если Выборка.Следующий() Тогда
        Возврат Выборка.Сотрудник;
    КонецЕсли;
    
    Возврат Неопределено;
КонецФункции

Контроль занятости при проведении документа

Интерактивная проверка на форме не защищает от коллизий при одновременной работе нескольких пользователей, программной загрузке или проведении документов списком. Окончательный контроль целостности данных обязательно выполняется в модуле объекта документа в процедуре ОбработкаПроведения.

Разберем логику обработки проведения с установкой управляемой блокировки для предотвращения параллельной записи одного и того же кресла:


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

Такой двухэтапный подход обеспечивает как комфортную работу пользователя на форме с мгновенным оповещением о занятости, так и стопроцентную транзакционную защиту от противоречивых данных в базе.

← На главную