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