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