Как записать новые цены в периодический регистр сведений и избежать ошибки уникальности записей

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

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

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

Архитектурная причина конфликта уникальности записей

Для начала выясним причину, по которой СУБД блокирует проведение второго документа. В периодическом регистре сведений, подчиненном регистратору, уникальность каждой записи на уровне базы данных определяется не самим документом-регистратором, а строго определенной комбинацией полей: Период + Измерения.

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

Рассмотрим конкретную ситуацию на примере товара «Ручка» и «Диван»:

  1. Первый документ «Учет цен» делает запись по «Ручке» на текущую дату. В регистре создается запись с уникальным ключом: [НачалоДня(ТекущаяДата), "Ручка", ВидЦены].
  2. Позже создается второй документ, в котором указаны и «Ручка», и «Диван».
  3. При проведении второго документа платформа пытается записать «Ручку» на ту же дату. СУБД обнаруживает попытку создания дублирующего ключа [НачалоДня(ТекущаяДата), "Ручка", ВидЦены] и возвращает ошибку нарушения уникальности.
  4. Транзакция проведения второго документа полностью откатывается. В результате в базу данных не попадает ни «Ручка», ни абсолютно новый «Диван».

Анализ неэффективных решений: почему запрос в цикле — это табу

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


// НЕПРАВИЛЬНО! Пример неоптимального кода с запросом в цикле
Для Каждого СтрокаТЧ Из ТабличнаяЧасть1 Цикл
    Если Не ЗаписьСуществуетВРегистре(Дата, СтрокаТЧ.Номенклатура) Тогда
        // Добавление движения...
    КонецЕсли;
КонецЦикла;

Проанализируем, почему этот метод категорически не рекомендуется стандартами разработки «1С». Каждый вызов функции с запросом внутри цикла порождает отдельное обращение к СУБД. Если в документе будет 100 или 1000 строк, система выполнит 1000 микрозапросов. Это приводит к колоссальному снижению производительности базы данных, возникновению избыточных блокировок и замедлению работы других пользователей. Подобный код никогда не пройдет аудит качества.

Оптимальное программное решение: пакетная проверка перед циклом

Чтобы решить задачу эффективно и быстро, выполним проверку одним запросом перед началом формирования движений. Для этого мы сначала соберем все проверяемые товары из табличной части в массив, передадим его параметром в запрос, а результат запроса поместим в коллекцию Соответствие для мгновенного поиска в памяти.

Разберем по шагам правильную реализацию этого алгоритма в процедуре ОбработкаПроведения модуля документа УчетЦен:


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

КонецПроцедуры

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

Методологические риски «тихого пропуска» записей

Рассмотрим ситуацию с точки зрения бизнес-логики. Допустим, в первом утреннем документе пользователь установил цену на «Ручку» в размере 100 рублей. Днем руководство решило изменить цену до 120 рублей, и оператор создал второй документ «Учет цен» на текущую дату.

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

Как правильно поступить методологически?

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

Кардинальное архитектурное решение: изменение периодичности регистра

Если специфика бизнеса предполагает регулярное изменение цен в течение дня (например, динамическое ценообразование в интернет-магазинах или розничных сетях), то ограничение «один раз в день» становится критическим препятствием.

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

  1. При такой настройке уникальность записей будет привязана к точному времени проведения документа на оси времени.
  2. Пользователи смогут создавать и проводить неограниченное количество документов установки цен по одним и тем же товарам в течение одного дня.
  3. Каждая последующая запись будет просто вытеснять предыдущую на временной шкале. При получении актуальных цен методом СрезПоследних() система вернет именно ту цену, которая была установлена самым последним по времени документом.

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

← На главную