Как обеспечить уникальность значений в разных колонках одной строки табличной части в 1С?

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

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

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

Способ №1. Проверка при записи документа на сервере (Базовый алгоритм)

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

Рассмотрим классический вариант обхода строк табличной части и проверки заполнения через массив. Мы будем обходить каждую строку, собирать значения контролируемых колонок в массив и проверять наличие дубликатов с помощью метода Найти().

Для реализации этого метода в модуле объекта документа добавим следующий код в обработчик ПередЗаписью или ОбработкаПроверкиЗаполнения:


Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
    
    // Подготовим массив с именами проверяемых колонок
    МассивКолонок = Новый Массив;
    МассивКолонок.Добавить("ВидТовара1");
    МассивКолонок.Добавить("ВидТовара2");
    МассивКолонок.Добавить("ВидТовара3");
    МассивКолонок.Добавить("ВидТовара4");
    МассивКолонок.Добавить("ВидТовара5");
    
    // Обходим строки табличной части
    Для Каждого СтрокаТЧ Из Товары Цикл
        
        МассивЗначений = Новый Массив;
        
        Для Каждого ИмяКолонки Из МассивКолонок Цикл
            
            ЗначениеРеквизита = СтрокаТЧ[ИмяКолонки];
            
            // Проверяем только заполненные значения
            Если ЗначениеЗаполнено(ЗначениеРеквизита) Тогда
                
                // Если значение уже есть в нашем массиве, значит это дубль
                Если МассивЗначений.Найти(ЗначениеРеквизита) <> Неопределено Тогда
                    ТекстОшибки = СтрШаблон("В строке № %1 обнаружены дублирующиеся значения вида товара: %2!", 
                        СтрокаТЧ.НомерСтроки, ЗначениеРеквизита);
                    ОбщегоНазначенияКлиентСервер.СообщитьПользователю(ТекстОшибки, , , , Отказ);
                    Отказ = Истина;
                Иначе
                    МассивЗначений.Добавить(ЗначениеРеквизита);
                КонецЕсли;
                
            КонецЕсли;
            
        КонецДля;
        
    КонецДля;
    
КонецПроцедуры

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

Недостатки метода: Линейный поиск в массиве на больших табличных частях может работать медленно. Для оптимизации вместо массива можно использовать объект Соответствие, так как поиск ключа в нем выполняется за константное время $O(1)$.

Способ №2. Использование Таблицы Значений и метода Свернуть

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

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

Посмотрим на пример реализации данного алгоритма:


Процедура ПроверитьДублиЧерезТЗ(Отказ)
    
    Для Каждого СтрокаТЧ Из Товары Цикл
        
        // Создаем временную таблицу для анализа текущей строки
        ТЗАнализа = Новый ТаблицаЗначений;
        ТЗАнализа.Колонки.Добавить("ЗначениеВида");
        
        КоличествоЗаполненных = 0;
        
        // Заполняем ТЗ значениями из пяти колонок текущей строки
        Для НомерКолонки = 1 По 5 Цикл
            ИмяПоля = "ВидТовара" + НомерКолонки;
            ТекущееЗначение = СтрокаТЧ[ИмяПоля];
            
            Если ЗначениеЗаполнено(ТекущееЗначение) Тогда
                НоваяСтрока = ТЗАнализа.Добавить();
                НоваяСтрока.ЗначениеВида = ТекущееЗначение;
                КоличествоЗаполненных = КоличествоЗаполненных + 1;
            КонецЕсли;
        КонецДля;
        
        // Сворачиваем таблицу по колонке значений
        ТЗАнализа.Свернуть("ЗначениеВида");
        
        // Если количество уникальных строк после сворачивания меньше, 
        // чем было добавлено изначально — у нас есть дубли!
        Если ТЗАнализа.Количество() < КоличествоЗаполненных Тогда
            ОбщегоНазначенияКлиентСервер.СообщитьПользователю(
                СтрШаблон("Ошибка в строке %1: виды товаров не должны повторяться!", СтрокаТЧ.НомерСтроки),
                , , , Отказ
            );
            Отказ = Истина;
        КонецЕсли;
        
    КонецДля;
    
КонецПроцедуры

Этот способ элегантен, но требует создания объекта ТаблицаЗначений на каждой итерации цикла, что создает дополнительную нагрузку на память при обработке многострочных документов.

Способ №3. Профессиональная валидация с подсветкой ячеек формы

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

Выясним, как это реализовать с помощью объекта СообщениеПользователю в обработчике модуля объекта ОбработкаПроверкиЗаполнения:


Процедура ОбработкаПроверкиЗаполнения(Отказ, ПроверяемыеРеквизиты)
    
    МассивКолонок = Новый Массив;
    Для Индекс = 1 По 5 Цикл
        МассивКолонок.Добавить("ВидТовара" + Индекс);
    КонецДля;
    
    Для ИндексСтроки = 0 По Товары.Количество() - 1 Цикл
        
        СтрокаТЧ = Товары[ИндексСтроки];
        ПроверенныеЗначения = Новый Соответствие;
        
        Для Каждого ИмяКолонки Из МассивКолонок Цикл
            
            ЗначениеРеквизита = СтрокаТЧ[ИмяКолонки];
            
            Если ЗначениеЗаполнено(ЗначениеРеквизита) Тогда
                
                Если ПроверенныеЗначения[ЗначениеРеквизита] <> Неопределено Тогда
                    
                    // Формируем адресное сообщение для конкретной ячейки на форме
                    Сообщение = Новый СообщениеПользователю;
                    Сообщение.Текст = СтрШаблон("Значение ""%1"" уже выбрано в другой колонке этой строки!", ЗначениеРеквизита);
                    Сообщение.Поле = СтрШаблон("Товары[%1].%2", ИндексСтроки, ИмяКолонки);
                    Сообщение.УстановитьДанные(ЭтотОбъект);
                    Сообщение.Сообщить();
                    
                    Отказ = Истина;
                    
                Иначе
                    ПроверенныеЗначения.Вставить(ЗначениеРеквизита, Истина);
                КонецЕсли;
                
            КонецЕсли;
            
        КонецДля;
        
    КонецДля;
    
КонецПроцедуры

Используя конструкцию адресации поля вида Товары[ИндексСтроки].ИмяКолонки, мы даем платформе точные координаты ошибочных данных, повышая удобство работы пользователей с интерфейсом.

Способ №4. Динамическое ограничение выбора на клиенте (Защита "на лету")

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

Для этого мы перехватим событие НачалоВыбора для каждого из пяти реквизитов табличной части на форме документа и применим динамический отбор.

Выполним следующие шаги на форме документа:

  1. Для каждого поля ввода в табличной части (например, ТоварыВидТовара1...ТоварыВидТовара5) назначим один и тот же обработчик события НачалоВыбора.
  2. В обработчике программно сформируем массив исключений (все значения строки, кроме текущей колонки).
  3. Передадим этот массив в параметры выбора элемента управления.

Рассмотрим код клиентского модуля формы:


&НаКлиенте
Процедура ТоварыВидТовараНачалоВыбора(Элемент, ДанныеВыбора, СтандартнаяОбработка)
    
    ТекущаяСтрока = Элементы.Товары.ТекущиеДанные;
    Если ТекущаяСтрока = Неопределено Тогда
        Возврат;
    КонецЕсли;
    
    ИмяТекущейКолонки = Элемент.Имя; // Например, "ТоварыВидТовара3"
    // Извлекаем чистый реквизит: "ВидТовара3"
    ИмяТекущегоРеквизита = СтрЗаменить(ИмяТекущейКолонки, "Товары", ""); 
    
    // Собираем массив уже выбранных в строке значений
    ИсключаемыеЗначения = Новый Массив;
    
    Для Индекс = 1 По 5 Цикл
        ИмяРеквизитаСравнения = "ВидТовара" + Индекс;
        
        // Нас интересуют все колонки, КРОМЕ той, в которой мы сейчас осуществляем выбор
        Если ИмяРеквизитаСравнения <> ИмяТекущегоРеквизита Тогда
            ЗначениеВКолонке = ТекущаяСтрока[ИмяРеквизитаСравнения];
            Если ЗначениеЗаполнено(ЗначениеВКолонке) Тогда
                ИсключаемыеЗначения.Добавить(ЗначениеВКолонке);
            КонецЕсли;
        КонецЕсли;
    КонецДля;
    
    // Если исключать нечего, то стандартный выбор отработает без модификаций
    Если ИсключаемыеЗначения.Количество() > 0 Тогда
        
        // Создаем отбор "Ссылка НЕ В СПИСКЕ ИсключаемыеЗначения"
        МассивПараметров = Новый Массив;
        
        ОтборСсылка = Новый Структура("Ссылка", Новый ФиксированныйМассив(ИсключаемыеЗначения));
        ПараметрОтбора = Новый ПараметрВыбора("Отбор.Ссылка", ОтборСсылка);
        МассивПараметров.Добавить(ПараметрОтбора);
        
        // Применяем отбор к открываемой форме выбора
        Элемент.ПараметрыВыбора = Новый ФиксированныйМассив(МассивПараметров);
        
    Иначе
        // Сбрасываем параметры выбора, если строка пустая
        Элемент.ПараметрыВыбора = Новый ФиксированныйМассив(Новый Массив);
    КонецЕсли;
    
КонецПроцедуры

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

Архитектурный анализ структуры данных

Проанализируем структуру данных, предложенную в исходной задаче. Наличие полей ВидТовара1...ВидТовара5 в одной строке табличной части является классическим признаком денормализации базы данных (нарушение первой нормальной формы СУБД).

Такой подход влечет за собой ряд проблем в будущем:

  1. Жесткое ограничение бизнес-логики: Если завтра бизнесу понадобится указывать не 5, а 6 видов товаров для одной позиции, разработчику придется менять структуру метаданных конфигурации, переписывать формы, логику проведения и отчеты.
  2. Сложность написания запросов: Построение отчетов по такой структуре требует объединения таблиц или сложных условий связи, что негативно сказывается на производительности СУБД.

Как сделать правильно:

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

При такой структуре контроль уникальности на сервере сводится к одной простой команде сворачивания подчиненной таблицы:


// Пример проверки нормализованной структуры
ТаблицаВидов = ЭтотОбъект.ВидыТоваровСтроки.Выгрузить();
КоличествоДо = ТаблицаВидов.Количество();

ТаблицаВидов.Свернуть("КлючСвязи, ВидТовара");
КоличествоПосле = ТаблицаВидов.Количество();

Если КоличествоДо <> КоличествоПосле Тогда
    // Обнаружены дубли!
КонецЕсли;

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

← На главную