При автоматизации рабочих мест кладовщиков, операторов склада и кассиров в конфигурациях на обычных формах (таких как «Управление торговлей 10.3», «Управление производственным предприятием 1.3» или «Комплексная автоматизация 1.1») часто возникает задача пошагового сканирования. Типичный сценарий: оператор сканирует штрихкод товара, система идентифицирует номенклатуру, а затем требует отсканировать определенное количество серийных номеров для этой позиции. Только после ввода всех серийных номеров система разрешает перейти к сканированию следующего товара.
Разработчики, особенно начинающие, часто совершают архитектурную ошибку: пытаются реализовать ожидания ввода серийных номеров через модальные окна, циклы ожидания внутри процедуры ВнешнееСобытие или принудительный программный вызов этой процедуры. В этой статье мы подробно разберем, почему такой подход приводит к зависанию интерфейса и ошибкам, и как правильно реализовать эту задачу на основе архитектуры конечного автомата (State Machine) с использованием штатных механизмов торгового оборудования 1С.
Давайте проанализируем ситуацию, с которой столкнулся автор темы. В коде была сделана попытка запустить цикл ожидания ввода серийного номера прямо внутри обработчика внешнего события:
Пока Количество < КоличествоФакт Цикл
Количество = Количество + 1;
ТребоватьСерийныеНомера = Истина;
Подсказка = "Введите Серийный номер";
ВнешнееСобытие("Сканер штрихкода", "ПолученШтрихкод", Данные);
КонецЦикла;
Этот код принципиально неверен по двум причинам:
ВнешнееСобытие(Источник, Событие, Данные) модуля формы. Пытаться вызвать эту процедуру программно в цикле бессмысленно — реального сканирования в этот момент не происходит, а переменная Данные будет содержать старое значение, что приведет к бесконечному циклу или ошибкам переполнения стека.Предупреждение(), поток выполнения интерфейса заблокируется. Пользователь не сможет комфортно работать, а данные со сканера, настроенного в режиме эмуляции клавиатуры, начнут сыпаться мимо активных полей ввода.Чтобы решить задачу корректно, нам нужно перевести форму документа в режим «ожидания ввода серийных номеров» и сохранять информацию о том, для какого именно товара мы сейчас ожидаем ввод. Каждое физическое сканирование (будь то товар или серия) должно обрабатываться как изолированное событие в процедуре ВнешнееСобытие.
Для реализации этого подхода нам потребуются переменные модуля формы, которые будут сохранять состояние между вызовами событий сканирования.
Разберем по шагам, какие переменные нам понадобятся в объявлении модуля формы:
Перем ТекущаяСтрокаНоменклатуры; // Хранит ссылку на строку табличной части "Товары"
Перем РежимВводаСерийныхНомеров; // Булев флаг: Истина, если сейчас ожидается ввод серийных номеров
Перем ТребуемоеКоличествоСерий; // Число серийных номеров, которое необходимо отсканировать
Перем ВведеноСерийныхНомеров; // Число уже успешно отсканированных серийных номеров
Теперь рассмотрим, как должна выглядеть основная логика обработки штрихкода при получении внешнего события.
Ниже представлен чистый и надежный алгоритм обработки событий от сканера штрихкодов:
Процедура ВнешнееСобытие(Источник, Событие, Данные)
// Получаем данные от стандартной библиотеки торгового оборудования (если используется)
// ПолучитьСерверТО().ОбработатьВнешнееСобытие(Источник, Событие, Данные, ЭтаФорма);
// Если сканер настроен напрямую или данные обрабатываются вручную:
Штрихкод = СокрЛП(Данные);
Если РежимВводаСерийныхНомеров Тогда
// Сработал сканер, и система находится в режиме ожидания серийного номера
ОбработатьСканированиеСерийногоНомера(Штрихкод);
Иначе
// Обычный режим: сканируем штрихкод товара
ОбработатьСканированиеТовара(Штрихкод);
EndIf;
КонецПроцедуры
Рассмотрим детально вспомогательную процедуру ОбработатьСканированиеТовара, которая отвечает за поиск номенклатуры по штрихкоду и перевод формы в режим ожидания серийных номеров:
Процедура ОбработатьСканированиеТовара(Штрихкод)
// 1. Ищем номенклатуру в базе данных по штрихкоду
НайденнаяНоменклатура = НайтиНоменклатуруПоШтрихкоду(Штрихкод);
Если НайденнаяНоменклатура = Неопределено Тогда
ОбщегоНазначения.СообщитьПользователю("Товар со штрихкодом " + Штрихкод + " не найден в базе данных!");
Возврат;
КонецЕсли;
// 2. Ищем товар в табличной части документа
ПараметрыОтбора = Новый Структура("Номенклатура", НайденнаяНоменклатура);
НайденныеСтроки = Товары.НайтиСтроки(ПараметрыОтбора);
Если НайденныеСтроки.Количество() = 0 Тогда
// Если товара нет в документе, добавляем новую строку
НоваяСтрока = Товары.Добавить();
НоваяСтрока.Номенклатура = НайденнаяНоменклатура;
НоваяСтрока.Количество = 1;
ТекущаяСтрокаНоменклатуры = НоваяСтрока;
Иначе
// Если товар найден, позиционируемся на первой строке
ТекущаяСтрокаНоменклатуры = НайденныеСтроки[0];
КонецЕсли;
// 3. Проверяем, требует ли данный товар ввода серийных номеров
Если ТребуетВводаСерийныхНомеров(НайденнаяНоменклатура) Тогда
// Включаем режим ввода серийных номеров
РежимВводаСерийныхНомеров = Истина;
ТребуемоеКоличествоСерий = ТекущаяСтрокаНоменклатуры.Количество;
ВведеноСерийныхНомеров = 0;
// Визуально оповещаем пользователя и переносим фокус
ЭлементыФормы.НадписьПодсказка.Заголовок = "ОЖИДАНИЕ СЕРИЙНЫХ НОМЕРОВ! Отсканируйте серии для товара: " + НайденнаяНоменклатура;
АктивизироватьТабличнуюЧастьСерий(ТекущаяСтрокаНоменклатуры);
Сообщить("Товар требует ввода серийных номеров в количестве: " + ТребуемоеКоличествоСерий + " шт.");
Иначе
// Если серийные номера не нужны, просто увеличиваем количество товара
ТекущаяСтрокаНоменклатуры.Количество = ТекущаяСтрокаНоменклатуры.Количество + 1;
ЭлементыФормы.НадписьПодсказка.Заголовок = "Товар успешно добавлен: " + НайденнаяНоменклатура;
КонецЕсли;
КонецПроцедуры
Теперь разберем процедуру ОбработатьСканированиеСерийногоНомера, которая вызывается при сканировании штрихкода в режиме ожидания ввода серийных номеров:
Процедура ОбработатьСканированиеСерийногоНомера(ШтрихкодСерии)
// 1. Проверяем серийный номер на уникальность в текущем документе
ПараметрыОтбора = Новый Структура;
ПараметрыОтбора.Вставить("Номенклатура", ТекущаяСтрокаНоменклатуры.Номенклатура);
ПараметрыОтбора.Вставить("СерийныйНомер", ШтрихкодСерии);
ДублирующиеСтроки = СерийныеНомера.НайтиСтроки(ПараметрыОтбора);
Если ДублирующиеСтроки.Количество() > 0 Тогда
ОбщегоНазначения.СообщитьПользователю("Этот серийный номер уже был отсканирован для данного товара!");
Возврат;
КонецЕсли;
// 2. Добавляем серийный номер в подчиненную табличную часть "СерийныеНомера"
НоваяСтрокаСерии = СерийныеНомера.Добавить();
НоваяСтрокаСерии.Номенклатура = ТекущаяСтрокаНоменклатуры.Номенклатура;
НоваяСтрокаСерии.СерийныйНомер = ШтрихкодСерии;
// Позиционируем курсор на добавленной строке серии
ЭлементыФормы.СерийныеНомера.ТекущаяСтрока = НоваяСтрокаСерии;
// 3. Увеличиваем счетчики
ВведеноСерийныхНомеров = ВведеноСерийныхНомеров + 1;
ТекущаяСтрокаНоменклатуры.КоличествоФакт = ВведеноСерийныхНомеров; // Например, фиксируем факт
// 4. Проверяем, все ли серийные номера введены
Если ВведеноСерийныхНомеров >= ТребуемоеКоличествоСерий Тогда
// Все требуемые серийные номера введены, выходим из режима ввода серий
РежимВводаСерийныхНомеров = Ложь;
ТекущаяСтрокаНоменклатуры = Неопределено;
ЭлементыФормы.НадписьПодсказка.Заголовок = "Все серии успешно введены. Отсканируйте следующий товар.";
Сообщить("Ввод серийных номеров для текущего товара успешно завершен.");
Иначе
// Ожидаем ввод следующего серийного номера
ОсталосьВвести = ТребуемоеКоличествоСерий - ВведеноСерийныхНомеров;
ЭлементыФормы.НадписьПодсказка.Заголовок = "Отсканировано серий: " + ВведеноСерийныхНомеров + " из " + ТребуемоеКоличествоСерий + ". Осталось ввести: " + ОсталосьВвести;
КонецЕсли;
КонецПроцедуры
Для удобства работы кладовщика критически важно, чтобы интерфейс формы автоматически переключался на нужные элементы ТЧ. В обычных формах (толстый клиент) управление фокусом ввода и активными элементами отличается от управляемых форм.
Выясним, как правильно активизировать табличную часть и установить курсор в нужную ячейку. Для этого напишем процедуру АктивизироватьТабличнуюЧастьСерий:
Процедура АктивизироватьТабличнуюЧастьСерий(СтрокаТовара)
// 1. Делаем элемент управления табличной части "СерийныеНомера" активным на форме
ЭлементыФормы.ТекущийЭлемент = ЭлементыФормы.СерийныеНомера;
// 2. Пытаемся позиционироваться на первой строке серийных номеров этого товара
ПараметрыОтбора = Новый Структура("Номенклатура", СтрокаТовара.Номенклатура);
НайденныеСерии = СерийныеНомера.НайтиСтроки(ПараметрыОтбора);
Если НайденныеСерии.Количество() > 0 Тогда
// Если серии уже были введены ранее, выделяем первую из них
ЭлементыФормы.СерийныеНомера.ТекущаяСтрока = НайденныеСерии[0];
Иначе
// Если серий еще нет, добавляем пустую строку для визуального ориентира (необязательно)
// НоваяСерия = СерийныеНомера.Добавить();
// НоваяСерия.Номенклатура = СтрокаТовара.Номенклатура;
// ЭлементыФормы.СерийныеНомера.ТекущаяСтрока = НоваяСерия;
КонецЕсли;
// 3. Устанавливаем активную колонку в ТЧ "СерийныеНомера" для удобства ручной корректировки
ЭлементыФормы.СерийныеНомера.ТекущаяКолонка = ЭлементыФормы.СерийныеНомера.Колонки.СерийныйНомер;
КонецПроцедуры
Обратите внимание на ошибку, с которой столкнулся автор в сообщении 18 и 24: Поле объекта не обнаружено (ТекущаяСтрока). Эта ошибка возникает, если имя элемента формы ТЧ не совпадает с именем реквизита объекта, либо если вы пытаетесь обратиться к реквизиту формы, имеющему тип «Флажок» или иной простой тип, вместо элемента управления типа ТабличноеПоле. Всегда проверяйте свойства элемента формы в конфигураторе и убеждайтесь, что свойство Данные элемента управления указывает на нужную табличную часть.
При сканировании серийных номеров важно реализовать проверку уникальности не только в локальном документе, но и в глобальной базе данных (если бизнес-логика требует, чтобы один серийный номер не мог быть продан дважды или присутствовать на складе в нескольких экземплярах).
Проанализируем пример функции глобальной проверки уникальности серийного номера в базе данных:
Функция СерийныйНомерУникаленВБазеДанных(Номенклатура, СерийныйНомер) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СерийныеНомера.Ссылка КАК СерияСсылка
|ИЗ
| Справочник.СерийныеНомера КАК СерийныеНомера
|ГДЕ
| СерийныеНомера.Владелец = &Номенклатура
| И СерийныеНомера.Наименование = &СерийныйНомер";
Запрос.УстановитьПараметр("Номенклатура", Номенклатура);
Запрос.УстановитьПараметр("СерийныйНомер", СерийныйНомер);
РезультатЗапроса = Запрос.Выполнить();
Возврат РезультатЗапроса.Пустой();
КонецФункции
Если серийный номер уникален, но отсутствует в справочнике, его необходимо программно создать и записать перед добавлением в табличную часть документа:
Если СерийныйНомерУникаленВБазеДанных(ТекущаяСтрокаНоменклатуры.Номенклатура, ШтрихкодСерии) Тогда
НовыйСерийник = Справочники.СерийныеНомера.СоздатьЭлемент();
НовыйСерийник.Владелец = ТекущаяСтрокаНоменклатуры.Номенклатура;
НовыйСерийник.Наименование = ШтрихкодСерии;
НовыйСерийник.Записать();
Иначе
// Если серийный номер уже существует, можно получить на него ссылку
// СерияСсылка = Справочники.СерийныеНомера.НайтиПоНаименованию(ШтрихкодСерии, Истина, , ТекущаяСтрокаНоменклатуры.Номенклатура);
КонецЕсли;
В сообщениях 33-36 обсуждается проблема зависания диалоговых окон (например, ВвестиСтроку) при использовании сканера, настроенного в режиме эмуляции клавиатуры. Выясним причину этой проблемы и способы ее решения:
Enter. Для этого в настройках сканера (путем сканирования управляющих штрихкодов из инструкции к устройству) нужно обязательно включить суффикс возврата каретки (CR/LF или Enter). Без этого суффикса штрихкод запишется в поле ввода, но форма ввода останется висеть на экране.ВнешнееСобытие независимо от того, где сейчас находится курсор на форме. При использовании режима COM-порта зависания окон ввода исключены, так как данные обрабатываются платформой на уровне событий.Разработав логику на основе событийного механизма (конечного автомата) без блокирующих циклов, мы получаем гибкое и стабильное решение. Основные правила, которые необходимо соблюдать:
ВнешнееСобытие завершиться, сохранив состояние формы в переменные модуля.