Как исправить ошибку нескольких владельцев у одного штрихкода в 1С и правильно перепривязать его к новой номенклатуре?

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

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

Для исправления этой проблемы разработчики часто создают внешние обработки. Сценарий прост: система выводит список проблемных штрихкодов, пользователь создает новую номенклатуру, и обработка должна автоматически перепривязать выбранный штрихкод к этой новой номенклатуре, попутно скорректировав связанные регистры (например, ор_ПрайсЛистыКонтрагентов). Однако при реализации этого механизма начинающие разработчики сталкиваются с целым букетом классических ошибок платформы 1С. В этой статье мы подробно разберем каждую ошибку, выясним причины их возникновения и напишем надежный, отказоустойчивый код.

Ошибка 1. Доступ к данным формы на сервере (ТекущиеДанные таблицы недоступны)

Проанализируем первую проблему, с которой столкнулся автор темы. При попытке выполнить серверный метод система выдает ошибку: «Текущие данные таблицы недоступны на сервере». Источником проблемы является следующая строка кода:


Запрос.УстановитьПараметр("ПараметрШК", Элементы.ШтрихкодыДС.ТекущиеДанные.Штрихкод);

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

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


&НаКлиенте
Процедура СоздатьНоменклатуру(Команда)
    Если Элементы.ШтрихкодыДС.ТекущаяСтрока <> Неопределено Тогда
        // Получаем штрихкод на клиенте, где контекст формы полностью доступен
        ТекущийШтрихкод = Элементы.ШтрихкодыДС.ТекущиеДанные.Штрихкод;
        
        ПараметрПередачиВФорму = Новый Структура("ПараметрШК", ТекущийШтрихкод);
        
        // Открываем форму создания номенклатуры
        ОткрытьФорму("ВнешняяОбработка.РаботаСНоменклатурой.Форма.ФормаЭлемента",, 
            ЭтаФорма,,,, 
            Новый ОписаниеОповещения("ПослеЗаписиНоменклатуры", ЭтаФорма, ПараметрПередачиВФорму), 
            РежимОткрытияОкнаФормы.БлокироватьОкноВладельца);
    Иначе
        ПоказатьПредупреждение(, "Ошибка. Выберите штрихкод из списка!");
    КонецЕсли;
КонецПроцедуры

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

Ошибка 2. Обращение к полям выборки запроса без позиционирования

Вторая серьезная ошибка, приводящая к падению программы, заключается в попытке напрямую присвоить новое значение результату запроса:


РезультатЗапроса = Запрос.Выполнить.Выбрать;
РезультатЗапроса.Владелец.Ссылка = Результат;

Проанализируем ситуацию подробнее. Метод Выбрать() возвращает объект типа ВыборкаИзРезультатаЗапроса. По своей сути выборка — это коллекция, указатель на множество строк, возвращенных базой данных. Чтобы прочитать конкретные данные, выборку необходимо сначала позиционировать на нужную запись. Даже если мы абсолютно уверены, что запрос вернул ровно одну строку, платформа требует обязательного вызова метода Следующий(). Без этого обращение к полям выборки вызовет ошибку времени выполнения.

Кроме того, результат запроса всегда открывается только для чтения. Вы не можете изменить значение поля прямо в выборке (например, написав Выборка.Владелец = Ссылка) и ожидать, что СУБД обновит эти данные в базе. Изменение физических таблиц в 1С выполняется исключительно через специализированные объекты (справочники, документы, наборы записей регистров).

Ошибка 3. Попытка перезаписи измерений независимого регистра сведений

В сообщениях форума автору предлагают использовать метод ПолучитьОбъект() для перезаписи штрихкода. Разберем, почему этот совет ошибочен для нашего случая.

Регистр сведений Штрихкоды является независимым и неподчиненным регистратором. У него нет регистратора, а значит, мы управляем его записями напрямую. В отличие от справочников или документов, у записей регистра сведений нет уникального идентификатора (ссылки), поэтому применить к ним метод ПолучитьОбъект() невозможно.

Для изменения данных в таком регистре используется либо РегистрСведенийМенеджерЗаписи, либо РегистрСведенийНаборЗаписей. Но здесь нас подстерегает главная архитектурная ловушка: поле Владелец в регистре Штрихкоды является измерением (ключевым полем, определяющим уникальность записи).

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

Правильный алгоритм действий для изменения измерений регистра сведений:

  1. Создать объект НаборЗаписей соответствующего регистра.
  2. Установить жесткий отбор по старому значению (в нашем случае — по значению штрихкода).
  3. Прочитать существующие записи из базы данных в оперативную память.
  4. Модифицировать необходимые поля (заменить старого владельца на ссылку новой номенклатуры) в цикле по записям набора.
  5. Записать измененный набор. При этом платформа автоматически очистит старые записи, попавшие под отбор, и запишет новые скорректированные строки.

Решение проблемы транзакционности при создании номенклатуры

Автор темы задает крайне важный вопрос: «Как сделать всю эту процедуру перепривязки в транзакции, дабы не плодить кучу номенклатур при сбое при перезаписи штрихкода?»

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

Поэтому мы разделим процесс на два независимых этапа:

  1. Интерактивный этап: Пользователь создает, заполняет и записывает новую карточку номенклатуры в стандартном режиме. При успешной записи форма закрывается, а ее ссылка передается обратно в обработку через механизм оповещения.
  2. Транзакционный этап: В процедуре обработки оповещения мы вызываем серверный метод, который открывает короткую транзакцию. Внутри этой транзакции мы выполняем чтение, модификацию и запись регистров Штрихкоды и ор_ПрайсЛистыКонтрагентов. Если на этапе записи регистров произойдет сбой (например, из-за блокировки базы данных другим пользователем), транзакция откатит изменения в регистрах, а созданная номенклатура просто останется в базе. Это безопасное и стандартное для систем ERP поведение.

Пошаговая программная реализация комплексного решения

Объединим все исправления и лучшие практики в один монолитный, отказоустойчивый рабочий код для нашей внешней обработки.

Рассмотрим код основной управляемой формы внешней обработки:


&НаКлиенте
Процедура СоздатьНоменклатуру(Команда)
    
    // Проверяем, выделена ли строка в динамическом списке штрихкодов
    ТекущаяСтрока = Элементы.ШтрихкодыДС.ТекущаяСтрока;
    Если ТекущаяСтрока = Неопределено Тогда
        ПоказатьПредупреждение(, "Пожалуйста, выберите штрихкод из списка для перепривязки!");
        Возврат;
    КонецЕсли;
    
    // Безопасно получаем штрихкод из данных формы на клиенте
    ВыбранныйШтрихкод = Элементы.ШтрихкодыДС.ТекущиеДанные.Штрихкод;
    
    // Формируем структуру параметров, которую мы передадим в форму новой номенклатуры
    // и получим обратно в процедуре обработки оповещения
    ДополнительныеПараметры = Новый Структура;
    ДополнительныеПараметры.Вставить("ТекущийШтрихкод", ВыбранныйШтрихкод);
    
    // Описываем обработчик оповещения, который сработает после закрытия формы номенклатуры
    ОписаниеОповещения = Новый ОписаниеОповещения("ПослеЗаписиНоменклатуры", ЭтотОбъект, ДополнительныеПараметры);
    
    // Открываем форму создания нового элемента номенклатуры.
    // Параметр РежимОткрытияОкнаФормы.БлокироватьОкноВладельца не позволит пользователю 
    // совершать действия в основной обработке до тех пор, пока он не разберется с карточкой номенклатуры.
    ОткрытьФорму("Справочник.Номенклатура.Форма.ФормаЭлемента", , ЭтаФорма, , , , ОписаниеОповещения, РежимОткрытияОкнаФормы.БлокироватьОкноВладельца);
    
КонецПроцедуры

&НаКлиенте
Процедура ПослеЗаписиНоменклатуры(РезультатЗакрытия, ДополнительныеПараметры) Экспорт
    
    // Если пользователь закрыл форму без сохранения номенклатуры, 
    // РезультатЗакрытия будет равен Неопределено или типу КодВозвратаДиалога.
    Если Не ЗначениеЗаполнено(РезультатЗакрытия) Тогда
        Возврат;
    КонецЕсли;
    
    СсылкаНаНовуюНоменклатуру = РезультатЗакрытия;
    ШтрихкодДляПерепривязки = ДополнительныеПараметры.ТекущийШтрихкод;
    
    // Вызываем серверный метод для безопасной перезаписи данных в транзакции
    Попытка
        ВыполнитьПерепривязкуНаСервере(СсылкаНаНовуюНоменклатуру, ШтрихкодДляПерепривязки);
        ПоказатьОповещениеПользователя("Успешно!", , "Штрихкод " + ШтрихкодДляПерепривязки + " успешно перепривязан к новой номенклатуре.");
        
        // Обновляем динамический список на форме, чтобы убрать обработанную строку
        Элементы.ШтрихкодыДС.Обновить();
    Исключение
        ВызватьИсключение КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
    КонецПопытки;
    
КонецПроцедуры

Теперь разберем серверную процедуру перепривязки, которая инкапсулирует в себе всю сложную бизнес-логику работы с регистрами сведений:


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

Проанализируем преимущества представленного кода:

← На главную