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

  • Механизм датасетов (писал о нем тут)

  • Новые подходы к локальному хранению – Pelican и key-value

  • Обновленные CV-возможности ActiveCV

  • Новый подход к макетам элементов и спискам

  • Обработчики pythonscript вместо python

Пример такой:

  1. Мы забираем из 1С (через OData) необходимые справочники и документы: товары, единицы, штрихкоды, инвентаризации и храним их локально на устройстве в новом "механизме датасетов". Во-первых для того, чтобы работать с ними локально, независимо от наличия связи с базой, во-вторых для повышения быстродействия (поиски, отборы по датасетам - мгновенные)

  2. Настройки подключения к 1С хранятся в key-value, тут заодно я хочу показать сохранение из полей при изменении текста

  3. Сканируя штрихкоды, мы работаем с товаром, вводим количество вручную. Вся работа оффлайн, без связи с 1С. Данные хранятся в СУБД Pelican. Пример рассчитан на большие объемы и высокую производительность.

  4. По окончанию работы, когда связь доступна, результаты можно выгрузить в документы Пересчет товаров в 1С используя онлайн-обработчик для этого.

  5. Для контроля что проинвентаризировано, что не проинвентаризировано используется ActiveCV и принцип светофора:

    • Зеленые – проинвентаризировано, все ок, с товаром ничего не надо делать.

    • Желтые – еще не обработаны, но по учету должны быть на складе (есть в документе Пересчет)

    • Красные – не должны находится на складе либо товар не распознан. Нужно принять меры.

Пример можно скачать в конце статьи.

Если с Simple никогда раньше не сталкивались, то есть более простые примеры для более плавного старта https://uitxt.readthedocs.io/ru/latest/gettingstarted.html. Этот пример является уже более сложным и учитывает опыт предыдущих.

Первым делом, сделаем экран для настроек подключения к 1С и загрузки/выгрузки. На этом этапе от 1С нам нужно:

  • Чтобы был опубликован Odata – сервис и он был доступен из той же сети что и устройство.

  • Чтобы были доступны в OData: Номенклатура, УпаковкиЕдиницыиИзмерения, ШтрихкодыНоменклатуры, ПересчетТоваров

Создадим процесс, экран настроек. Разместим там три поля ввода для URL, пользователя, пароля, галку Обновлять при старте (необязательно, но удобно чтобы справочники обновлялись при запуске) и пару кнопок для выгрузки и загрузки. Они нам понадобятся

Содержимое экрана в конструкторе
Содержимое экрана в конструкторе

И сразу проверим как это выглядит на устройстве

Экран на устройстве
Экран на устройстве

Тут я хочу показать обновленную работу с key-value базой данных. Для хранения констант, настроек или просто какого то хранения значений (например кеш экрана) удобно использовать не полноценную СУБД а key-value. В pythonscript она доступна через объект _local у которого есть методы put(key,value), get(key). Значения хранятся в папке конфигурации. Подробнее об этом тут .

В onStart(при отрисовке экрана) создадим переменную для поля ввода url (остальное аналогично)

url=_local.get("url")

if url==None:
   url=""  

hashMap.put("url",json_to_str({"hint":"URL OData (например http://192.168.1.1:80/ut11)","events":True,"default_text":url}))

Тут мы читаем url из key-value и передаем в default_text, установим hint. И включим events – при каждом изменении поля будет генерироваться событие с listener=имя переменной поля, т.е. url

Создадим событие для поля url, которое будет генерироваться при изменении

_local.put("url",hashMap.get("url")) #пишем в key-value базу в ключ url значение поля url

Все, мы сделали запись в локальное хранилище URL без кнопки сохранения. Остальные поля – аналогично. Потом будем брать этот URL в алгоритме загрузки через _local.get("url")

Следующим шагом напишем загрузку данных в датасеты для хранения справочников и документов ПересчетТоваров. Но сначала нам надо создать эти датасеты. Это лучше сделать в обработчике onLaunch. Там же сразу поместим загрузку данных в память load(), так как мы собираемся их хранить локально. Так как у нас есть галка «Обновлять при старте» там же пропишем обновление если она включена.

datasrv = CreateDataSet("goods")

datasrv.setOptions(json_to_str({"hash_keys":["article","barcode"],"search_keys":"name","view_template":"{name} , {article}"})) 

if _local.get("update")==True:

    import general

    general.load()

else:    

    if datasrv.isSaved():

        datasrv.load()

        toast(str(datasrv.last_saved())) 

 

datasrv2 = CreateDataSet("documents")

if _local.get("update")==True:

    import general

    general.load()

else:    

    if datasrv2.isSaved():

        datasrv2.load()

Саму загрузку из odata разместим в общем модуле, так как обращение к ней есть и из экрана и из onLaunch

Покажу на примере справочника Номенклатура (весь текст посмотрите в прилагаемом примере)

1.      Используя requests обращаемся к OData в 1с, читаем данные в словарь nom

#Подгружаем Номенклатуру         
nom = {}

r = requests.get(host+"/odata/standard.odata/Catalog_Номенклатура?$format=json",auth=HTTPBasicAuth(user, password))
if r.status_code==200:
    result = r.json()
    for record_1c in result["value"]:
        if not record_1c['IsFolder']==True and record_1c["ИспользованиеХарактеристик"]=="НеИспользовать":
          nom[record_1c['Ref_Key']]={"name":record_1c['НаименованиеПолное'],"article":record_1c['Артикул'],"unit":units.get(record_1c['ЕдиницаИзмерения_Key']),"barcode":""}
else:
    speak("Не удалось подключиться")

              

2.      Записываем nom в датасет. У нас датасет goods объединяет в себе Номенклатуру, Единицы и Штрихкоды. Своеобразная плоская таблица. Соответственно три запроса к OData собирается в словарь nom, потом словарь преобразуется в список (у датасетов структура – список) и записывается в датасет.

        dataset =[]
        for key, value in nom.items():
              dataset.append({"_id":key,"name":value["name"],"article":value["article"],"unit":value["unit"],"barcode":value["barcode"]})

        goods =GetDataSet("goods")
        goods.put(json_to_str(dataset)) 
        #сохраним датасет локально
        goods.save()

Документы (датасет documents) – аналогично.

Вот и все, теперь у нас все хранится локально, мы можем это использовать для представления данных, идентификации, поиска.

Теперь сделаем процесс Инвентаризация

Он будет состоять из

  1. списка документов Инвентаризации (которые мы загрузили в датасет)

  2. формы документа (т.е. срок табличной части) и количество будет вводиться в диалоге

Вот такой интерфейс процесса "Инвентаризация". Без изысков (их можно добавить по желанию)
Вот такой интерфейс процесса "Инвентаризация". Без изысков (их можно добавить по желанию)


На первом экране размещаем список документов инвентаризация. Список документов с датасетом сделать очень просто. Нужно разместить на экране элемент списка (у нас это будет Список карточек со значением @docs) и в onStart написать заполнение переменной списка:

j = { "customcards":         {
             "layout": "^document",
             "cardsdata":"~documents"}
}
hashMap.put("docs",json_to_str(j))

~documents – указывает программе что истоник данных – ранее загруженный датасет documents (префикс ~ указывает ссылку на датасет как источник данных)

^document. Это ссылка на макет элемента списка через префикс ^. Теперь макеты(списков, диалогов и т.д.) можно определять не в коде обработчиков, а в секции Контейнеры, а потом на них просто ссылаться. Это удобно.

список общих контейнеров конфигурации (раздел Контейнеры)
список общих контейнеров конфигурации (раздел Контейнеры)
контейнер "document"
контейнер "document"

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

Еще нам понадобится обработчик клика по таблице, но так как на этом этапе нам нужно только открыть экран и запомнить выбранную ссылку на документ, то даже в python заходить не нужно, сделаем это обработчиком set

current_document=@selected_card_key;ShowScreen=ФормаДокумента

Создадим экран документа ФормаДокумента

В экране ФормаДокумента размещена только Таблица
В экране ФормаДокумента размещена только Таблица

Да, для навигации, переопределим кнопку Назад

И пропишем в экране списка документов

А в  экране ФормаДокумента

Таким образом, с формы мы возвращаемся на список, а со списка – выходим из процесса в меню.

В экране ФормаДокументов разместим Штрихкод в экране. Мы хотим, подключить сканер и искать штрихкоды по датасету goods, потом, когда товар найден вводить количество в диалоге. Для диалога тоже сделаем макет input_qty. Аналогично экранам. И также у диалога пропишем listener события, которое будет генерироваться, когда диалог будет открываться – dialog

Подпишемся на событие barcode
Подпишемся на событие barcode

Теперь подпишемся на закрытие диалога. Тут записи уже будут писаться в СУБД Pelican, специальную NoSQL СУБД, которую я написал специально для проекта Simple: https://pypi.org/project/pelicandbms/ Про архитектуру писал тут: https://infostart.ru/1c/tools/2127994/ И видео о использовании в SimpleUI: https://youtu.be/aEAzLWPgN2c

Pelican, если в двух словах: синтаксис - 100% MongoDB, но работает локально на устройстве, оптимизирована на все CRUD операции для больших баз и высокой нагрузки.

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

Тогда в любом обработчике можно обращаться к БД так: pelicans[имя_бд]. Это уже БД с подготовленными индексами и т.д.

Из опций БД только "инициализация"
Из опций БД только "инициализация"

 Обработчик после ввода количества в диалоге:

from pelican import pelicans

db = pelicans['inventory'] 
db["fact_lines"].insert({"_id":hashMap.get("current_document")+"_"+hashMap.get("nomRef"),"docRef":hashMap.get("current_document"),"nomRef":hashMap.get("nomRef"),"nom":hashMap.get("nom"),"qty":hashMap.get("qty")},upsert=True)

 

Тут не просто insert а insert c ключом upsert=True, т.е. это insert+update. И как видно мы образуем ключ из ИД документа и ИД товара

 И нам остается только срастить «план» с «фактом» по документу и вывести это в таблицу на экране. Т.е. план – это то, что в датасете (учетное количество), а факт – то что в СУБД.

Пропишем в onStart:

from pelican import pelicans

db = pelicans['inventory'] 

documents = GetDataSet("documents")
current_document=str_to_json(DataSets.getObjectStr(hashMap.get("current_document")))

lines = current_document["lines"] #за основу берем "план" из документа
fact = db['fact_lines'].find({"docRef":hashMap.get("current_document")}) #находим то что насканировали по документу
for fact_line in fact:
    found=False
    for line in lines:
        if line["nomRef"]==fact_line["nomRef"]:
            found=True
            line["fact"] = fact_line["qty"]
            line["plan_fact"] = str(line["plan"])+"/<b>"+str(line["fact"])+"</b>"
    if not found: #добавляем строку если не нашли такую в документе
        lines.append({"pos":"new","nom":fact_line["nom"],"nomRef":fact_line["nomRef"],"plan":"","fact":fact_line["qty"],"plan_fact":"-/<b>"+str(line["fact"])+"</b>"})        

j = { "customtable":         {
                     "layout": "^line",
                     "tabledata":lines}
}

hashMap.put("lines",json_to_str(j)) #заполняем в переменную таблицы

 

 Теперь процесс у нас рабочий и автономный. И можно передавать данные в 1С по мере необходимости.  Делать это будем онлайн- обработчиком, т.е. не устройстве, а выполняя http-запрос в 1С. Старая схема, в 25м году появилась еще одна схема для онлайн-обработчиков : https://infostart.ru/1c/articles/2305445/ Но на данный момент на 8.3.27 перешли не только лишь все, поэтому пока отложим ее. Но это не важно так как старая схема от новой, для разработчика отличается только названием типа обработчика. Все остальное – ровно то же. И перейти на новую схему (у кого есть 8.3.27) можно просто перещелкнув тип обработчика с online на onlinews

Собственно, по кнопке "Выгрузить данные" происходит следующее:

  1. На стороне устройства читается факт из СУБД Pelican и упаковывается в переменную "fact" которую на своей стороне будет читать уже 1С. Также для удобства я немножко готовлю таблицу чтобы в 1С было удобнее, но это необязательно - можно сделать и на стороне 1С.

  2. Вызывается еще один обработчик уже online (на стороне 1С)

Особенности подключения в режиме онлайн разобраны в этой статье документации: https://uitxt.readthedocs.io/ru/latest/communications.html#http-online

Также особенности работы с онлайн-обработчиками разобраны в примере: https://uitxt.readthedocs.io/ru/latest/gettingstarted.html#id6

Сам обработчик в 1С такой:

 

Процедура ЗагрузитьРезультаты(Переменные) Экспорт
	
	ЧтениеJSON = Новый ЧтениеJSON;
    ЧтениеJSON.УстановитьСтроку(Переменные.fact);
    Факт = ПрочитатьJSON(ЧтениеJSON);
    ЧтениеJSON.Закрыть();
	
	
	ТекущийИД="";
	ДокДляЗаписи=Неопределено;
	Для каждого стр из Факт Цикл
		Если стр.doc1c<>ТекущийИД Тогда
			Если ДокДляЗаписи<>Неопределено Тогда
				ДокДляЗаписи.ОбменДанными.Загрузка=Истина;
				ДокДляЗаписи.Записать();
			КонецЕсли;	                
			
			ДокДляЗаписиСсылка = Документы.ПересчетТоваров.ПолучитьСсылку(Новый УникальныйИдентификатор(стр.doc1c));
			ДокДляЗаписи = ДокДляЗаписиСсылка.ПолучитьОбъект();
			
		КонецЕсли;	
		
		Товар = Справочники.Номенклатура.ПолучитьСсылку(Новый УникальныйИдентификатор(стр.nomRef)); 
		Найдено=Ложь;
		Для каждого с из ДокДляЗаписи.Товары Цикл
			Если с.Номенклатура  = Товар Тогда
				с.КоличествоУпаковокФакт = Число(стр.qty);
				с.КоличествоФакт = Число(стр.qty);
				Найдено=Истина;
				Прервать;
			КонецЕсли;	
		КонецЦикла;	
		
		Если Не Найдено Тогда
			НовСтр = ДокДляЗаписи.Товары.Добавить();
			НовСтр.КоличествоУпаковокФакт = Число(стр.qty);
			НовСтр.КоличествоФакт = Число(стр.qty);

		КонецЕсли;	
		
	КонецЦикла;	  
	
	Если ДокДляЗаписи<>Неопределено Тогда
		ДокДляЗаписи.ОбменДанными.Загрузка=Истина;
		ДокДляЗаписи.Записать();
	КонецЕсли;	 
	
	Переменные.Вставить("toast","Данные загружены");
	
КонецПроцедуры 

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

Так как ActiveCV теперь можно вписывать в экран как обычный элемент, покажем перечень того, что еще нужно отсканировать также в списке на экране (под экраном камеры).

Процесс у нас будет состоять из двух экранов:

  • Выбор инвентаризации

  • После выбора сразу переключаемся на экран с ActiveCV

Для выбора используем Поле датасета. Это очень мощный инструмент для размещения ссылок на датасеты, но сейчас сделаем по простому:

Для выбора документа разместим поле датасета.
Для выбора документа разместим поле датасета.

И после выбора генерируется событие, и мы по этому событию запускаем экран "CV" (я везде делаю обработчики переключения экранов в set, но это не обязательно, можно и в python, да даже в 1С. Просто так быстрее)

Если просто запустить как есть, то получится не очень красиво: так как макет списка не указан, он сгенерирован автоматически

Список в режиме AUTO нечитаемый, нужен макет.
Список в режиме AUTO нечитаемый, нужен макет.

Чтобы список не был таким ужасным и при выборе в поле появлялось что то осмысленное можно задать макет документа в списке в onLaunch. Зададим тот же макет, который уже делали для списка в предыдущем макете:

datasrv2.setOptions(json_to_str({"list_layout":"document"}))

Основной экран у нас содержит по сути 2 элемента: ActiveCV и Таблицу куда выводятся "желтые" товары. Но мы таблицу обернем в контейнер, так как в симпле есть команда UpdateLayout которая обновляет не весь экран, а только контейнер. Зачем? Если обновлять весь экран видеопоток дергается, его инициализация не мгновенная.

структура экрана CV
структура экрана CV

При открытии основного экрана (onStart) устанавливаем настройки AciveCV:

hashMap.put("CameraSetResolutionAnalysis","480") #разрешение детектора
hashMap.put("CameraSetDetector","BARCODE") #тип детектора - штрихкоды

Мы не используем валидатор, я хочу чтобы попадали все штрихкоды, а не только те которые есть в датасете.

 

Этот пример достаточно примитивен, работает только с штрихкодами. Вот тут можно посмотреть всю ширь и глубину доступных инструментов: https://infostart.ru/1c/tools/2364633/

При попадании штрихкодов в кадр нам надо раскрасить и подписать объекты. Для этого используем событие new_barcodes_detected. Берем список штрихкодов в кадре, и перебираем. value – это штрихкод (есть еще, например формат)

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

 Также записи которые надо еще обработать выводим в табличку.

from pelican import pelicans
#БД где хранится "факт"
db = pelicans['inventory'] 

#получаем объект датасета по ссылке
documents = GetDataSet("documents")
current_document=str_to_json(DataSets.getObjectStr(hashMap.get("current_document")))

#датасет с товарами
dataset_goods = GetDataSet("goods")

#штрихкоды, которые есть в кадре
jvalues = str_to_json(hashMap.get("detected_values"))
#в стеке храним раскраску объектов и извлекаем ее для дополнения новыми объектами
if hashMap.containsKey("SetObjectsView"):
	objects = json.loads(hashMap.get("SetObjectsView"))
else:	
	objects = []

if hashMap.containsKey("tlines"): #строчки, которые выводятся внизу (объекты, которые надо обработать)
    tlines = str_to_json(hashMap.get("tlines")) 
else:    
    tlines = [{"name":"products to check:","_layout":"^cv_header"}]

for item in jvalues :
    barcode = item["value"]
    sku_result = dataset_goods.getStr("barcode",barcode)
    color = "F000000"
    plan=""
    fact=""
    nom = ""

    if sku_result ==None:
        color = "#e74c3c" #штрхкод не найден - сразу красный
        caption = "Product not found!"
    else:
        sku = str_to_json(sku_result )
        #ищем среди товаров документа
        lines = current_document["lines"]
        nom = sku["name"]
        in_document=False
        for line in lines:
            if line["nomRef"]==sku["_id"]:
                color = "#f1c40f" #есть в документе, цвет пока желтый 
                plan = line["plan"]
                caption = nom+", <b>"+str(plan)+"/"+str(fact)+"</b>" 
                in_document=True
                break

        # ищем в "факте" 
        # весь синтаксис в Pelican - MongoDB
        fact_records = db['fact_lines'].find(
             {"$and":[
                    {"docRef":{"$eq":hashMap.get("current_document")}},
                    {"nomRef":{"$eq":sku["_id"]}}
                    ]})         
        if len(fact_records)>0:
          fact= fact_records[0]["qty"]
          color = "#1e8449"
          in_document=True
          caption = nom+", <b>"+str(plan)+"/"+str(fact)+"</b>" 
        
        if color=="#f1c40f": #есть в документе, нет в факте, добавим в табличку 
            tlines.append({"name":nom})

        if in_document == False:    
            caption = nom+" : OUT OF PLACE!"
            color = "#e74c3c"

    cv = {"id":item["value"],"color":color,"caption":caption} #устанавливаем расцветку и заголовок
    id = item["value"]
    itemarr = next((itemo for itemo in objects if itemo["id"] == id), None)

    if itemarr == None:  
        objects.append(cv)
    else:
        itemarr["color"] = cv["color"]
        itemarr["caption"] = cv["caption"]

j = { "customtable":         {
			"layout": "^cv",
	        "tabledata":tlines}
}

hashMap.put("table",json_to_str(j))
hashMap.put("UpdateLayout","tab") #обновляем не весь экран, а только таблицу для плавности
hashMap.put("SetObjectsView",json_to_str(objects)) #записываем в ActiveCV расцветку и заголовки объектов
hashMap.put("noRefresh","") #запретим обновлять экран
hashMap.put("tlines",json_to_str(tlines)) #храним в стеке между вызовами

Получился довольно большой (нетипично для решений SImpleUI) обработчик, потому что нам надо разобрать объекты, посмотреть в СУБД и датасетах, сформировать табличку.

Ссылки к статье

Примеры (конфигурация SimpleUI+расширение 1С) - тут

Конфигурацию можно открыть в онлайн-редакторе https://seditor.ru:1555/ (либо развернуть локальную версию редактора https://github.com/dvdocumentation/web_simple_editor) и посмотреть на своем Android-устройстве

Мой новостной телеграмм канал проекта (новости разработки, статьи, примеры): https://t.me/devsimpleui Рекомендую подписаться, там много полезного.

Напоминаю, что вся экосистема продуктов Simple полностью бесплатна(зарабатываю только с проектов внедрения) По всем вопросам со мной можно связаться через форму обратной связи на https://simpleui.ru/

Комментарии (1)


  1. iliabvf
    30.05.2025 06:50

    Хабр вызывай Асинизатор