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