Как в 1С создать оборотный регистр накопления и регистр сведений с контролем уникальности записей

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

В процессе разработки бизнес-приложений на платформе 1С:Предприятие 8 начинающие специалисты часто сталкиваются с задачами проектирования регистров. Типичный пример — необходимость фиксировать хозяйственные операции (например, перевозки грузов) без ведения остатков, а также контролировать уникальность определенных событий во времени (например, планирование не более одного рейса для водителя в день). В этой статье мы подробно разберем, как правильно спроектировать такие объекты метаданных в конфигураторе, объясним принципы их работы и напишем программный код для проведения документов и контроля бизнес-логики.

Можно ли создать регистр кодом в 1С

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

Платформа 1С:Предприятие разделяет разработку (конфигурирование) и исполнение (работу с данными). Вся структура базы данных — декларативное описание таблиц, индексов, связей и типов данных — жестко описывается в Конфигураторе. В режиме предприятия встроенный язык позволяет нам работать только со значениями объектов метаданных (создавать элементы справочников, записывать документы, делать движения по регистрам), но не менять саму структуру хранения данных.

Существуют обходные пути (например, выгрузка конфигурации в XML-файлы, программная модификация файлов схемы и обратная загрузка через командную строку), однако данный подход требует монопольного доступа к базе данных, полной реструктуризации СУБД и неприменим в рамках выполнения стандартных учетных задач. Создание структуры регистра всегда выполняется разработчиком вручную в конфигураторе.


Проектирование регистра накопления «Перевозка материальных ценностей»

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

Выясним причину, почему в данном случае нам категорически не подходит регистр остатков. Регистр накопления остатков хранит в СУБД две основные таблицы: таблицу движений (приход/расход) и таблицу итогов (актуальные остатки на каждую контрольную точку времени). Если мы будем использовать регистр остатков для простой фиксации фактов перевозок, то у нас не будет операции «расхода» — мы будем постоянно делать только «приход». В результате таблица итогов будет бесконечно расти, увеличивая нагрузку на базу данных и замедляя работу системы, при этом сами остатки не будут нести никакого экономического смысла. Нам не нужно знать, сколько «весит» водитель в деньгах на текущий момент, нам важен лишь факт совершения перевозки и обороты за период.

Поэтому мы выбираем Регистр накопления оборотного типа. Он хранит только таблицу движений и оптимизированную таблицу периодических итогов (оборотов за месяц/день), не вычисляя нарастающий итог. Это существенно экономит ресурсы системы.

Спроектируем структуру метаданных в конфигураторе по шагам:

  1. Откроем Конфигуратор, в дереве метаданных найдем ветку Регистры накопления, нажмем кнопку «Добавить».
  2. Зададим имя регистра: ПеревозкаМатериальныхЦенностей.
  3. На вкладке «Основные» переключим свойство Вид регистра в положение Обороты.
  4. Перейдем на вкладку «Данные» и определим состав полей:
    • Измерения:
      • Водитель (Тип: СправочникСсылка.ФизическиеЛица или СправочникСсылка.Сотрудники) — разрез учета, определяющий ответственное лицо.
      • Рейс (Тип: СправочникСсылка.Рейсы или ДокументСсылка.ПутевойЛист) — аналитика, определяющая конкретную поездку.
    • Ресурсы:
      • Сумма (Тип: Число, длина 15, точность 2, флаг «Неотрицательное») — числовая величина, отражающая стоимость перевезенного груза.

Обратим внимание на важное требование задачи: регистр должен хранить информацию о том, по какому документу перемещены ценности. Нам не нужно создавать отдельное измерение для этого. В платформе 1С каждый регистр накопления жестко связан с понятием регистратора. Любое движение в регистре накопления физически не может существовать без документа-регистратора. Платформа автоматически добавляет в таблицу регистра стандартный реквизит Регистратор, который содержит ссылку на документ, создавший запись. При формировании отчетов мы всегда сможем получить этот документ из стандартного поля, не перегружая структуру регистра лишними измерениями.


Реализация записи в регистр накопления (проведение документа)

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

Сначала укажем в свойствах регистра накопления на вкладке «Регистраторы» галочку напротив нашего документа. После этого в модуле документа станет доступна коллекция движений этого регистра.

Для генерации базового кода проведения мы можем воспользоваться интерактивным инструментом платформы — Конструктором движений. Чтобы запустить его, откроем форму документа в конфигураторе, перейдем на вкладку «Движения» и нажмем кнопку «Конструктор движений». Выберем наш регистр, укажем таблицу-источник (например, табличную часть документа или реквизиты шапки) и сопоставим поля. Конструктор автоматически сгенерирует код в модуле объекта документа.

Рассмотрим пример программного кода, написанного вручную или доработанного после конструктора, в процедуре ОбработкаПроведения модуля документа:


Процедура ОбработкаПроведения(Отказ, РежимПроведения)
	
	// Устанавливаем признак необходимости записи движений в базу данных
	Движения.ПеревозкаМатериальныхЦенностей.Записывать = Истина;
	
	// Очищаем движения по этому регистру, которые могли остаться от предыдущего проведения
	Движения.ПеревозкаМатериальныхЦенностей.Очистить();
	
	// Сценарий 1: Данные берутся из шапки документа (одна перевозка на документ)
	Движение = Движения.ПеревозкаМатериальныхЦенностей.Добавить();
	
	// Заполняем стандартные реквизиты записи
	Движение.Период = Дата; // Дата проведения документа
	
	// Заполняем измерения регистра
	Движение.Водитель = Водитель; // Реквизит шапки "Водитель"
	Движение.Рейс = Рейс;         // Реквизит шапки "Рейс"
	
	// Заполняем ресурсы
	Движение.Сумма = СуммаДокумента; // Реквизит шапки "СуммаДокумента"
	
	// Сценарий 2: Если бы данные были в табличной части "СоставГруза"
	// Для каждого СтрокаГруза Из СоставГруза Цикл
	//     Движение = Движения.ПеревозкаМатериальныхЦенностей.Добавить();
	//     Движение.Период = Дата;
	//     Движение.Водитель = Водитель;
	//     Движение.Рейс = Рейс;
	//     Движение.Сумма = СтрокаГруза.Стоимость;
	// КонецЦикла;
	
КонецПроцедуры

В данном коде мы сначала взводим флаг Записывать в значение Истина, сообщая платформе, что по окончании транзакции проведения изменения в данном регистре нужно зафиксировать в СУБД. Метод Очистить() гарантирует, что если документ перепроводят с новыми данными, старые записи будут удалены, предотвращая задвоение данных. Затем мы используем метод Добавить() для формирования новой строки движений и заполняем ее свойства значениями реквизитов документа.


Проектирование регистра сведений «Рейсы водителей» с уникальностью в пределах дня

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

Проанализируем архитектурную особенность регистров сведений в 1С. Уникальность записей в любом регистре сведений определяется ключевым набором полей: Период + Измерения. Платформа физически не позволит записать в базу данных две строки, у которых полностью совпадают значения периода и всех измерений. Если мы попытаемся это сделать, система либо выдаст ошибку нарушения уникальности, либо перезапишет старую строку новой (в зависимости от режима записи).

Разберем типичную ошибку проектирования. Если разработчик добавит и водителя, и рейс в измерения регистра сведений, то ключ уникальности будет состоять из комбинации (Период, Водитель, Рейс). В этом случае система разрешит записать на одну дату для одного водителя несколько разных рейсов. Например, записи (01.10.2023, Водитель Иванов, Рейс №1) и (01.10.2023, Водитель Иванов, Рейс №2) будут считаться уникальными, так как у них отличаются значения измерения Рейс. Это прямое нарушение условия бизнес-логики.

Для обеспечения требования «один водитель — один рейс в день» мы должны спроектировать регистр сведений следующим образом:

  1. Создадим новый регистр сведений с именем РейсыВодителей.
  2. На вкладке «Основные» установим свойство Периодичность в значение В пределах дня. Это заставит платформу автоматически округлять дату записи до начала дня (время 00:00:00), убирая точность до секунд.
  3. На вкладке «Данные» добавим состав полей:
    • Измерения:
      • Водитель (Тип: СправочникСсылка.ФизическиеЛица) — это единственное измерение регистра. Именно по нему будет контролироваться уникальность в рамках выбранного периода (дня).
    • Ресурсы:
      • Рейс (Тип: СправочникСсылка.Рейсы) — переносим рейс в ресурсы. Так как рейс не входит в ключ уникальности, для одного водителя на один день сможет существовать только одна запись с конкретным рейсом.

При такой структуре ключ уникальности будет состоять из набора (День, Водитель). Если мы попытаемся записать для водителя Иванова на дату 01.10.2023 рейс №2, в то время как там уже записан рейс №1, платформа просто обновит значение ресурса, заменив рейс №1 на рейс №2, но не создаст вторую строку. Таким образом, на уровне базы данных жестко гарантируется ограничение: не более одного рейса в день для одного водителя.


Программный контроль уникальности при проведении документа

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

Решая эту задачу в многопользовательской среде, мы должны помнить о возможности коллизий блокировок. Если два диспетчера одновременно попытаются назначить разные рейсы одному и тому же водителю на один день, может возникнуть ситуация «гонки за ресурсами» (race condition). Чтобы гарантировать согласованность данных, перед чтением и проверкой информации мы должны установить управляемую блокировку.

Рассмотрим, как написать надежный код проведения в модуле документа с использованием механизма транзакционных блокировок и языка запросов 1С:


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

Проанализируем написанный код. На первом шаге мы используем объект БлокировкаДанных, чтобы заблокировать запись по конкретному водителю на начало текущего дня. Если в этот момент другой пользователь попытается провести документ по этому же водителю на этот же день, его транзакция приостановится до завершения нашей транзакции, предотвращая запись некорректных дублирующих данных.

На втором шаге мы выполняем запрос к виртуальной таблице регистра сведений. Обратите внимание, что мы используем функцию НАЧАЛОПЕРИОДА в запросе и функцию НачалоДня() во встроенном языке для приведения времени к формату 00:00:00. Это критически важно, так как периодичность регистра установлена «В пределах дня» и платформа запишет данные именно на начало суток.

Если запрос возвращает запись, мы проверяем, совпадает ли уже назначенный в базе рейс с тем рейсом, который мы пытаемся провести сейчас. Если они отличаются, мы устанавливаем переменную Отказ в значение Истина. Это стандартный механизм платформы 1С: если в процедуре ОбработкаПроведения переменная Отказ принимает значение Истина, платформа автоматически откатывает транзакцию, отменяет все сделанные движения и сообщает пользователю, что документ не проведен.

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


Резюме и лучшие практики разработки

Мы успешно решили учебную задачу и спроектировали два ключевых регистра, обеспечивающих хранение и контроль данных перевозок. Подведем итоги и выделим основные правила, которых следует придерживаться при проектировании регистров в 1С:

← На главную