При разработке в 1С часто возникает ситуация, когда документ должен не просто добавлять новые данные в регистр сведений, но и изменять уже существующие записи, связанные с предыдущими документами. Особенно это актуально для кадровых документов, таких как "Прием на работу", "Кадровое перемещение" или "Увольнение", которые формируют записи о состоянии сотрудников в определенный период. В этом материале мы подробно разберем, как обеспечить изменение записей в регистре сведений при проведении документа, избегая дублирования и сохраняя целостность данных.
Для начала, давайте проанализируем, как регистры сведений взаимодействуют с документами. В системе 1С, при проведении документа, движения по регистрам формируются в обработчике события ОбработкаПроведения. Обычно это происходит путем создания набора записей и его записи в базу данных. Ключевым вопросом является, как управлять этим процессом, чтобы достичь желаемого поведения — изменения вместо создания новых записей.
В нашей задаче мы сталкиваемся с тем, что документ "Увольнение" (или любой другой корректирующий документ) должен влиять на записи, которые были сформированы ранее документом "Прием на работу" (или другими вводными документами). Это требует не просто добавления новых движений, но и возможности модификации или даже удаления предыдущих, а затем создания актуальных.
Прежде чем погружаться в код, мы выясним, какой тип регистра сведений лучше всего подходит для нашей задачи. В контексте учета движений сотрудников, где важна история изменений (кто, когда, на какую должность был принят/уволен/переведен), наиболее оптимальным решением является использование периодического регистра сведений, подчиненного регистратору.
01.01.2023 15:30:00 станет 01.01.2023 00:00:00). Если регистр подчинен регистратору, то часто используют периодичность "По позиции регистратора", что позволяет отслеживать изменения с точностью до момента проведения документа, сохраняя порядок движений внутри одной даты.Если бы мы использовали независимый регистр, то записи не были бы привязаны к регистратору. В этом случае, при отмене проведения документа, нам пришлось бы программно очищать движения, что значительно усложняет логику и может приводить к ошибкам.
Для работы с подчиненными регистрами сведений в обработчике проведения документа, мы используем объект НаборЗаписей. Этот объект позволяет нам прочитать существующие записи, установить отборы, изменить или добавить записи, а затем записать весь набор обратно в базу данных.
Мы разберем шаги, которые необходимо выполнить в процедуре ОбработкаПроведения вашего документа.
НаборЗаписей:
Начнем с создания менеджера набора записей для нашего регистра. Допустим, наш регистр называется "СостоянияСотрудников".
НаборЗаписей = РегистрыСведений.СостоянияСотрудников.СоздатьНаборЗаписей();
Или, если регистр подчинен регистратору, что является нашим случаем, мы обычно инициализируем его, привязывая к текущему документу:
НаборЗаписей = РегистрыСведений.СостоянияСотрудников.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(Ссылка);
Установка отбора по регистратору — обязательное условие для корректной работы с подчиненными регистрами.
Чтобы изменить записи, а не просто добавить новые, мы должны сначала прочитать те записи, которые уже есть в базе данных по нашему отбору. В случае подчиненного регистра, это будут записи, созданные именно этим документом в его предыдущих проведениях (если документ перепроводится).
НаборЗаписей.Прочитать();
Если наша задача — найти и изменить запись, созданную другим документом (например, документом "Прием на работу"), или записи, относящиеся к определенному сотруднику, мы должны установить дополнительные отборы или программно найти нужную запись после прочтения.
Для периодических регистров, если мы хотим найти запись на конкретную дату или период, мы также добавляем отбор по измерению Период.
// Пример отбора для поиска конкретной записи
// Допустим, у нас есть измерения "Сотрудник" и "ДатаСобытия"
// И мы хотим найти запись по сотруднику на определенную дату
НаборЗаписейДляПоиска = РегистрыСведений.СостоянияСотрудников.СоздатьНаборЗаписей();
НаборЗаписейДляПоиска.Отбор.Сотрудник.Установить(СотрудникИзДокумента);
НаборЗаписейДляПоиска.Отбор.Период.Установить(ДатаУвольнения); // Или любая другая дата, по которой ищем
// Если есть другие ключевые измерения, их также нужно установить в отбор
НаборЗаписейДляПоиска.Прочитать();
После прочтения, НаборЗаписейДляПоиска будет содержать все записи, удовлетворяющие нашим условиям отбора. Мы можем перебрать их и найти ту, которую хотим изменить.
Теперь, когда у нас есть набор записей, мы можем с ним работать.
// Инициализируем наш набор для движений текущего документа
Движения = РегистрыСведений.СостоянияСотрудников;
Движения.Очистить(); // Очищаем старые движения текущего документа перед формированием новых
НоваяЗапись = Движения.Добавить();
НоваяЗапись.Период = ДатаУвольнения; // Важно: заполняем Период!
НоваяЗапись.Сотрудник = СотрудникИзДокумента;
НоваяЗапись.Состояние = ПредопределенноеЗначение("Перечисление.СостоянияСотрудников.Уволен");
НоваяЗапись.Должность = Неопределено; // Или актуальная должность на момент увольнения
НоваяЗапись.ПричинаУвольнения = ПричинаУвольненияИзДокумента;
// Заполняем остальные измерения и ресурсы
В этом примере, Движения.Очистить() удаляет все записи, ранее сформированные текущим документом при его предыдущих проведениях. Затем мы добавляем новую запись с актуальными данными. Это стандартный подход для подчиненных регистров: очистка старых движений и формирование новых при каждом проведении.
Если же мы хотим модифицировать запись, созданную *другим* документом или найти существующую для обновления её ресурсов (что является основной проблемой в вопросе), логика будет немного сложнее:
// Предполагаем, что у нас есть переменные:
// ТекущийСотрудник - Ссылка на элемент справочника Сотрудники
// ДатаСобытияДокумента - Дата события из текущего документа (например, ДатаУвольнения)
// НовоеЗначениеРесурса - Новое значение, которое мы хотим установить
// 1. Получаем набор записей для текущего документа.
// Это те записи, которые были сформированы ранее ЭТИМ ЖЕ документом.
НаборЗаписейДокумента = РегистрыСведений.СостоянияСотрудников.СоздатьНаборЗаписей();
НаборЗаписейДокумента.Отбор.Регистратор.Установить(Ссылка);
НаборЗаписейДокумента.Прочитать();
НаборЗаписейДокумента.Очистить(); // Очищаем движения этого документа, будем формировать новые
// 2. Получаем набор записей, который потенциально нужно изменить,
// но который НЕ подчинен текущему регистратору (или мы ищем по другим критериям)
// Например, ищем последнюю запись по сотруднику на дату до текущего документа.
// Здесь мы используем СрезПоследних, чтобы найти актуальное состояние сотрудника
// до проведения текущего документа.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| СостоянияСотрудниковСрезПоследних.Период,
| СостоянияСотрудниковСрезПоследних.Сотрудник,
| СостоянияСотрудниковСрезПоследних.Должность,
| СостоянияСотрудниковСрезПоследних.Состояние
|ИЗ
| РегистрСведений.СостоянияСотрудников.СрезПоследних(&ДатаСреза, Сотрудник = &Сотрудник) КАК СостоянияСотрудниковСрезПоследних";
Запрос.УстановитьПараметр("ДатаСреза", ДатаСобытияДокумента); // Срез на дату документа
Запрос.УстановитьПараметр("Сотрудник", ТекущийСотрудник);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Если Выборка.Следующий() Тогда
// Нашли предыдущее состояние сотрудника. Теперь его нужно модифицировать,
// если текущий документ изменяет ЭТО состояние.
// Однако, в случае подчиненного регистра, мы не изменяем чужие движения.
// Мы создаем НОВОЕ движение, которое ПЕРЕКРЫВАЕТ предыдущее по дате.
// Это и есть суть работы периодического регистра.
// Создаем новую запись для текущего документа
НоваяЗапись = НаборЗаписейДокумента.Добавить();
НоваяЗапись.Период = ДатаСобытияДокумента;
НоваяЗапись.Сотрудник = Выборка.Сотрудник;
// Здесь мы устанавливаем новые значения, которые привносит текущий документ
// Например, для Увольнения:
НоваяЗапись.Состояние = ПредопределенноеЗначение("Перечисление.СостоянияСотрудников.Уволен");
НоваяЗапись.Должность = Выборка.Должность; // Или Неопределено, в зависимости от логики
// Заполняем другие поля...
Иначе
// Если предыдущих состояний не найдено (маловероятно для Увольнения, но возможно для Приема)
// Создаем первую запись для сотрудника
НоваяЗапись = НаборЗаписейДокумента.Добавить();
НоваяЗапись.Период = ДатаСобытияДокумента;
НоваяЗапись.Сотрудник = ТекущийСотрудник;
НоваяЗапись.Состояние = ПредопределенноеЗначение("Перечисление.СостоянияСотрудников.Уволен"); // Или Принят
// ...
КонецЕсли;
// 3. Записываем сформированный набор движений для текущего документа
НаборЗаписейДокумента.Записать();
Важным моментом является то, что в периодических регистрах мы обычно не изменяем старые записи напрямую, если они были созданы *другими* документами. Вместо этого, мы создаем *новую* запись с актуальной датой и новыми данными. Эта новая запись по своему "Периоду" будет перекрывать предыдущие, и при запросе "среза последних" на дату после нового события, будет возвращаться именно новая запись. Это и есть принцип хранения истории изменений.
Если же мы говорим об изменении записей, созданных текущим документом (например, при его перепроведении), то НаборЗаписей.Прочитать(), НаборЗаписей.Очистить(), а затем НаборЗаписей.Добавить() и НаборЗаписей.Записать() — это стандартный и правильный подход. Система гарантирует, что при отмене проведения документа, эти записи будут удалены, а при повторном проведении будут созданы новые, соответствующие актуальному состоянию документа.
После всех изменений и добавлений, мы должны записать наш набор обратно в базу данных.
НаборЗаписей.Записать();
В версиях 1С 8.3.26 и выше для метода Записать() объекта НаборЗаписей появились дополнительные режимы замещения:
РежимЗамещения.Добавление: Добавляет записи в набор.РежимЗамещения.Замещение: Замещает все записи, совпадающие по ключу (измерения + период).РежимЗамещения.Слияние: Обновляет существующие записи, добавляет новые, не трогает записи, не указанные в наборе.РежимЗамещения.Обновление: Обновляет только те записи, которые совпадают по ключу.РежимЗамещения.Удаление: Удаляет записи, совпадающие по ключу.Эти режимы дают больше гибкости, но в контексте подчиненных регистров и стандартной логики "Очистить и Добавить" обычно достаточно простого Записать() после Очистить() и Добавить().
Как мы увидели в обсуждении, одной из частых ошибок при работе с периодическими регистрами является незаполнение поля Период. Система 1С требует, чтобы это системное измерение было заполнено. Если вы видите ошибку, связанную с незаполненным полем Период, обязательно проверьте, присвоили ли вы ему значение типа Дата перед записью.
Например:
НоваяЗапись.Период = ДатаУвольненияДокумента; // Корректно
// НоваяЗапись.Период = Неопределено; // Выдаст ошибку!
Убедитесь, что дата соответствует требуемой гранулярности регистра. Если периодичность "День", то время в дате будет проигнорировано (или приведено к началу дня). Если "Секунда" или "По позиции регистратора", то время будет иметь значение.
Автор обсуждения также спрашивал о том, как "программно обратиться к записи как к объекту". Кроме работы с НаборЗаписей, для операций с единичными записями регистра (особенно для независимых регистров или для точечных изменений) используется МенеджерЗаписи.
МенеджерЗаписи:
МенеджерЗаписи = РегистрыСведений.СостоянияСотрудников.СоздатьМенеджерЗаписи();
Для уникальной идентификации записи необходимо установить значения всех ключевых полей (измерений и, если регистр периодический, поля Период).
МенеджерЗаписи.Сотрудник = СотрудникИзДокумента;
МенеджерЗаписи.Период = ДатаУвольненияДокумента;
// Установить другие измерения, если они есть и входят в ключ
Метод Прочитать() попытается найти запись по установленным ключевым полям. Если запись найдена, её ресурсы будут заполнены в объекте МенеджерЗаписи.
Если МенеджерЗаписи.Прочитать() Тогда
// Запись найдена, теперь можно изменить её ресурсы
МенеджерЗаписи.Состояние = ПредопределенноеЗначение("Перечисление.СостоянияСотрудников.Уволен");
// ...
МенеджерЗаписи.Записать(); // Записать измененную запись
Иначе
// Запись не найдена, можно создать новую, если это необходимо
МенеджерЗаписи.Состояние = ПредопределенноеЗначение("Перечисление.СостоянияСотрудников.Уволен");
// Заполнить все измерения и ресурсы
МенеджерЗаписи.Записать();
КонецЕсли;
Важно помнить, что МенеджерЗаписи более подходит для работы с независимыми регистрами или для очень специфичных сценариев, когда нужно точечно изменить ОДНУ запись. Для формирования движений документов, особенно в подчиненных регистрах, НаборЗаписей является более предпочтительным и эффективным инструментом, так как позволяет работать с целыми группами записей за одну операцию.
На форуме возник вопрос о том, не лучше ли использовать справочник вместо регистра сведений для хранения информации о сотрудниках. Мы проанализируем разницу:
Для учета движения кадров (прием, перемещение, увольнение), изменения должностей, подразделений, графиков работы, условий оплаты труда сотрудников, регистр сведений подходит гораздо лучше. Он позволяет хранить полную историю этих изменений с привязкой к датам. Например, сотрудник Петров И.И. с 01.01.2023 был "Менеджером", а с 01.07.2023 стал "Ведущим менеджером". Справочник не может эффективно хранить такую историю без дополнительных механизмов, тогда как периодический регистр сведений делает это нативно.
Таким образом, для задачи изменения данных в регистре сведений при проведении документа, которая была изначально подтянута из другого документа (например, "Увольнение" из "Прием на работу"), наиболее подходящим и гибким решением является использование подчиненного периодического регистра сведений.
Мы подытожим основные моменты:
РегистрыСведений.<ИмяРегистра>.СоздатьНаборЗаписей().НаборЗаписей.Отбор.Регистратор.Установить(Ссылка).НаборЗаписей.Прочитать().НаборЗаписей.Очистить(). Это удалит все старые движения, сформированные текущим документом при его предыдущих проведениях.НоваяЗапись = НаборЗаписей.Добавить().Период для каждой новой записи, а также все ключевые измерения и ресурсы.НаборЗаписей.Записать().Следуя этим рекомендациям, мы сможем эффективно управлять данными в регистрах сведений, обеспечивая гибкость, целостность и историчность информации в нашей системе 1С.