При проектировании систем назначения сотрудников на определенные роли (например, ответственный менеджер за клиента, куратор проекта, ведущий специалист) разработчики часто сталкиваются с бизнес-требованием: «Если сотрудник увольняется, его нужно автоматически убрать с роли через 3 дня, но у руководителя должна быть возможность в любой момент заменить его вручную».
Если решать эту задачу «в лоб» с помощью периодического регистра сведений (РС) и просто записывать будущее «пустое» значение на третий день, мы неизбежно попадем в «ловушку будущих записей». Если руководитель решит заменить уволенного сотрудника на первый же день после его увольнения, то созданная им запись будет перезаписана (затерта) запланированной пустой записью на третий день. В результате клиент останется без ответственного лица.
В этой статье мы подробно разберем три профессиональных архитектурных паттерна, которые позволяют элегантно решить эту задачу в среде 1С:Предприятие 8.3.
---
Этот подход основывается на принципе: «Никаких записей на будущее время». База данных должна отражать только текущее состояние или свершившиеся факты. Вместо того чтобы планировать отложенную запись в регистр сведений при увольнении сотрудника, система делегирует эту задачу регламентному заданию.
Алгоритм работы решения:
СвязиСотрудников никаких изменений не происходит. Сотрудник продолжает числиться за клиентом.Сотрудники (или кадровых регистров).ТекущаяДата() - 3 дня, система программно создает новую запись в регистре сведений с текущей датой и пустым значением сотрудника.Проанализируем программный код общего модуля, который будет вызываться регламентным заданием:
Процедура ОчиститьУволенныхСотрудниковРегламентное() Экспорт
// Шаг 1. Формируем запрос для поиска зависших уволенных сотрудников
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СвязиСрезПоследних.Клиент КАК Клиент,
| СвязиСрезПоследних.Роль КАК Роль,
| СвязиСрезПоследних.Сотрудник КАК Сотрудник
|ИЗ
| РегистрСведений.СвязиСотрудников.СрезПоследних(&ТекущаяДата, ) КАК СвязиСрезПоследних
|ГДЕ
| НЕ СвязиСрезПоследних.Сотрудник.ДатаУвольнения = ДАТАВРЕМЯ(1, 1, 1)
| И СвязиСрезПоследних.Сотрудник.ДатаУвольнения <= &ПредельнаяДатаУвольнения
| И НЕ СвязиСрезПоследних.Сотрудник.Пустая";
// Вычисляем дату: 3 дня назад от текущей
ПредельнаяДата = НачалоДня(ТекущаяДата() - (3 * 24 * 60 * 60));
Запрос.УстановитьПараметр("ТекущаяДата", ТекущаяДата());
Запрос.УстановитьПараметр("ПредельнаяДатаУвольнения", ПредельнаяДата);
Выборка = Запрос.Выполнить().Выбрать();
// Шаг 2. Записываем пустые значения текущей датой, освобождая роли
Пока Выборка.Следующий() Цикл
МенеджерЗаписи = РегистрыСведений.СвязиСотрудников.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Период = ТекущаяДата();
МенеджерЗаписи.Клиент = Выборка.Клиент;
МенеджерЗаписи.Роль = Выборка.Роль;
МенеджерЗаписи.Сотрудник = Справочники.Сотрудники.ПустаяСсылка();
Попытка
МенеджерЗаписи.Записать(Истина);
Исключение
// Записываем ошибку в Журнал регистрации, чтобы не прерывать общий процесс
ЗаписьЖурналаРегистрации("РегламентноеВедениеСотрудников",
УровеньЖурналаРегистрации.Ошибка, , ,
"Не удалось очистить роль уволенного сотрудника: " + ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
КонецПроцедуры
Преимущества подхода:
---
Этот вариант базируется на совете из сообщения 9 («Добавьте период действия»). Вместо использования стандартного платформенного механизма периодичности регистра 1С (который умеет работать только с одной датой — Период), мы переводим регистр в статус непериодического и самостоятельно управляем интервалами дат.
Структура регистра сведений СвязиСотрудниковИнтервалы:
Клиент (СправочникСсылка.Клиенты), Роль (СправочникСсылка.Роли), ДатаНачала (ДатаВремя).Сотрудник (СправочникСсылка.Сотрудники).ДатаОкончания (ДатаВремя).Как это работает по шагам:
ДатаНачала = Сегодня() и ДатаОкончания = 31.12.3999 (бесконечность).ДатаОкончания на ДатаУвольнения + 3 дня.ДатаОкончания = ТекущаяДата()) и открывает новую запись для нового сотрудника с ДатаНачала = ТекущаяДата().Рассмотрим пример кода для ручного назначения нового сотрудника:
Процедура НазначитьНовогоСотрудникаСКонтролемИнтервалов(Клиент, Роль, НовыйСотрудник) Экспорт
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.СвязиСотрудниковИнтервалы");
ЭлементБлокировки.УстановитьЗначение("Клиент", Клиент);
ЭлементБлокировки.УстановитьЗначение("Роль", Роль);
НачалоТранзакции();
Попытка
Блокировка.Заблокировать();
// Шаг 1. Ищем и закрываем текущую активную запись
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Связи.ДатаНачала КАК ДатаНачала
|ИЗ
| РегистрСведений.СвязиСотрудниковИнтервалы КАК Связи
|ГДЕ
| Связи.Клиент = &Клиент
| И Связи.Роль = &Роль
| И Связи.ДатаОкончания > &ТекущаяДата";
Запрос.УстановитьПараметр("Клиент", Клиент);
Запрос.УстановитьПараметр("Роль", Роль);
Запрос.УстановитьПараметр("ТекущаяДата", ТекущаяДата());
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// Перезаписываем запись, устанавливая дату окончания в Текущую дату
НаборЗаписей = РегистрыСведений.СвязиСотрудниковИнтервалы.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Клиент.Установить(Клиент);
НаборЗаписей.Отбор.Роль.Установить(Роль);
НаборЗаписей.Отбор.ДатаНачала.Установить(Выборка.ДатаНачала);
НаборЗаписей.Прочитать();
Если НаборЗаписей.Количество() > 0 Тогда
Запись = НаборЗаписей[0];
Запись.ДатаОкончания = ТекущаяДата();
НаборЗаписей.Записать(Истина);
КонецЕсли;
КонецЦикла;
// Шаг 2. Создаем новую запись для нового сотрудника
НовыйНабор = РегистрыСведений.СвязиСотрудниковИнтервалы.СоздатьНаборЗаписей();
НовыйНабор.Отбор.Клиент.Установить(Клиент);
НовыйНабор.Отбор.Роль.Установить(Роль);
НовыйНабор.Отбор.ДатаНачала.Установить(ТекущаяДата());
НоваяЗапись = НовыйНабор.Добавить();
НоваяЗапись.Клиент = Клиент;
НоваяЗапись.Роль = Роль;
НоваяЗапись.ДатаНачала = ТекущаяДата();
НоваяЗапись.Сотрудник = НовыйСотрудник;
НоваяЗапись.ДатаОкончания = Дата("39991231235959");
НовыйНабор.Записать(Истина);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение ОписаниеОшибки();
КонецПопытки;
КонецПроцедуры
Чтобы получить активного сотрудника на любую дату (например, на сегодня), мы пишем простой запрос:
|ГДЕ
| &ВыбраннаяДата МЕЖДУ Связи.ДатаНачала И Связи.ДатаОкончания
Преимущества подхода:
---
Если в вашей конфигурации уже развернута сложная логика на периодическом регистре сведений и перестраивать структуру метаданных под непериодический регистр слишком трудоемко, вам поможет метод компенсации будущих записей.
При увольнении мы продолжаем создавать будущую запись (пустую на дату ДатаУвольнения + 3 дня). Но при ручном (инициативном) назначении нового сотрудника мы пишем код, который проверяет наличие «будущих» запланированных пустых записей и удаляет их.
Проанализируем логику этого процесса в процедуре записи нового сотрудника:
Процедура ЗаписатьСотрудникаСКорректировкойБудущего(Клиент, Роль, НовыйСотрудник, ДатаНазначения) Экспорт
НачалоТранзакции();
Попытка
// Шаг 1. Ищем запланированные пустые записи в будущем периоде
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Связи.Период КАК Период
|ИЗ
| РегистрСведений.СвязиСотрудников КАК Связи
|ГДЕ
| Связи.Клиент = &Клиент
| И Связи.Роль = &Роль
| И Связи.Период > &ДатаНазначения";
Запрос.УстановитьПараметр("Клиент", Клиент);
Запрос.УстановитьПараметр("Роль", Роль);
Запрос.УстановитьПараметр("ДатаНазначения", ДатаНазначения);
Выборка = Запрос.Выполнить().Выбрать();
// Шаг 2. Очищаем найденные будущие записи, чтобы они не перетерли нашего нового сотрудника
Пока Выборка.Следующий() Цикл
МенеджерЗаписи = РегистрыСведений.СвязиСотрудников.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Клиент = Клиент;
МенеджерЗаписи.Роль = Роль;
МенеджерЗаписи.Период = Выборка.Период;
// Удаляем будущую пустую или неактуальную запись
МенеджерЗаписи.Удалить();
КонецЦикла;
// Шаг 3. Записываем нашего нового сотрудника текущей датой
НовыйМенеджер = РегистрыСведений.СвязиСотрудников.СоздатьМенеджерЗаписи();
НовыйМенеджер.Клиент = Клиент;
НовыйМенеджер.Роль = Роль;
НовыйМенеджер.Период = ДатаНазначения;
НовыйМенеджер.Сотрудник = НовыйСотрудник;
НовыйМенеджер.Записать(Истина);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение ОписаниеОшибки();
КонецПопытки;
КонецПроцедуры
Преимущества подхода:
СрезПоследних платформы 1С.---
Мы проанализировали три различных подхода к решению коллизии отложенных изменений. Какой из них выбрать для вашей системы?