Быстрый старт

Памятка по переносу решений

После переноса решения с сервера на сервер или загрузки конфигурации из файла, возможно понадобится проделать несколько действий:

  1. В файле поменять current_module_name. Его можно взять например из URL строки этого же конфигуратора

  2. Если используются выгрузки с моб. Клиента на сервер через _upload или RemoteClass то надо прописать Сервер по умолчанию (при загрузке из файла там может стоять другой сервер)

  3. Если используется миграция на мобильные устройства то надо зайти в раздел Rooms конфигурации (именно в самой конфигурации) и прописать там соответствие псевдонимов и реальных комнат.

Конфигурации-примеры по всем возможностям NodaLogic есть в папке Samples на GitHub : https://github.com/dvdocumentation/nodalogic

Вводный пример (мобильная платформа)

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

Для начала авторизируемся в https://nmaker.pw/. Я рекомендую также зайти в свой профиль и установить Отображаемое имя – это не обязательно, но будет красиво отображаться в репозитории. Далее мы создаем новую конфигурацию, зададим ей какое то имя и сохраним его.

_images/noda_config_name.png

И ниже в «Разделы» добавим новый раздел. Кстати, крайне рекомендую посмотреть в примерах использование команд в разделах – очень удобная штука. Можно не заходя в процесс/узел делать какие то вещи. Ну а сейчас мы просто создали раздел

_images/noda_conf_section.png

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

Зайдем в Настройки-Репо конфигураций-Добавить конфигурацию и отсканируем QR-код. В меню есть еще один QR-код, он для rooms, он пока не нужен.

_images/noda_repo.png

В результате должна загрузиться конфигурация и появиться раздел в котором ничего нет.

Сделаем процесс, который сложит 2 числа в полях ввода:

  1. Зайдем в Классы и создадим класс Calculator. Тип класса – Пользовательский процесс. Это как раз аналог «процесса» в Simple. Это тоже узел, но существующий в единственном экземляре и его не надо создавать – он создается при загрузке конфигурации. Во всех остальных отношениях – это такой же узел как и остальные.

  2. Без обложки классы выглядят непрезентабельно. Обложка как и остальные места где есть UI задаются в виде общей «строковой» разметки. О ней на следующем шаге а пока сделайте вот что: скопируйте текст из подсказки ниже и поправьте примерно так:

[[{"type":"Text","value":"Сложение чисел","bold":true}],[{"type":"Text","value":"Знакомство с UI/UX"}]]``

Также укажем наш раздел

И надо нажать «Сохранить»

_images/noda_section.png
  1. Далее добавим метод, назовем его Open, тип engine – Android/Python

_images/noda_method.png

И напишем в нем такую команду

self.Show(  [
 [{"type":"Input","input_type":"number","id":"a","caption":"A"},{"type":"Input","input_type":"number","id":"b","caption":"B"}],
 [{"type":"Button","id":"calculate","caption":"Сложить"}]
 ])

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

Это так называемая основная («строковая») разметка в Noda. Она везде – в экранах, списках, диалогах, обложках. Есть еще альтернатива – разметка контейнерами, но о ней мы сейчас говорить не будем.

Суть разметки такова:

[#вертикальный контейнер
[{объект 1},{ объект 2}], #строка 1 по высоте элементов
[] #строка 2 по высоте элементов

]

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

Почему я использовал динамическую разметку, а в Симпле была статическая в конфигураторе? Вот причины: 1) ее нагляднее читать 2) она проще понимается и генерируется LLM 3)она сразу динамическая, что надо то и нарисуешь. Соответственно экраны можно собирать из переопределяемых блоков.

Примечание

Кстати об экранах – в Симпле они были, а тут их нет? Да, они логические. Фактически у узла 1 экран на котором можно выводить сколько угодно Show, переключаемых например по кнопке, а для удобство их можно упрятать на разные методы. Посмотрите первый пример из конфигурации Android – там есть 3 экрана.

Но метод еще не все. Мы должны повесить его на событие. Почему в Simple это было вместе, а тут надо делать дополнительное действие? Потому что это более гибкий подход. Например, метод, который мы сделали – его можно вызвать из кода другого обработчика, он сделает то же что и по событию – перерисует экран Поэтому добавим

_images/noda_event.png

Теперь можно обновить конфу (меню опций - Перезапустить). И посмотреть что получилось.

_images/noda_result.png
  1. Кнопка есть, но не работает. Добавим метод calculate.

С таким кодом:

res = self._data["a"]+self._data["b"]
toast(str(res))

Обратите внимание на то что происходит. self – это сам узел. У узла есть _data – это его динамическая и одновременно хранимая (если сохранять) память. В Simple это hashMap, но в Simple hashMap был строковый, а тут – JSON-совместимый.

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

И надо повесить событие. Мы делали кнопку с id=calculate, поэтому мы сделаем соытие onInput с фильтром по listener= calculate

_images/noda_calculate.png

Обновим конфу и проверим как работает. Это все.

_images/noda_editor.png

Примечание

Но для понимания как все устроено, зайдите в Конфигурация-Обработчики Андроид. Там вы увидите код нашего класса Calculator. Видно что он наследник Node, т.е. может пользоваться методами Node. Еще мы видим то, что это отельный модуль python который грузится когда грузится конфа, т.е. мы можем там размещать глобальные переменные и функции и пользоваться из разных классов. Но при этом редактировать из окна класса. На самом деле можно писать код прямо в модуле (или в своей IDE) – классы его подхватят. Это по сути как python в Simple, но с поведением как у pythonscript

Пример сервера-хранилища решения с оффлайновыми клиентами

В предыдущем примере был узел, но не обычный. Это был узел-процесс, а основа решений NodaLogic – узел с данными, а решение представляет из себя совокупность таких узлов. Поэтому текущий пример будет именно на обычных узлах – узлах типа «Узел данных» (Data node) – совокупность порции хранимых данных и методов класса.

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

Шаг 1. Первым делом сделаем просто класс, в который будем присылать задачи и в него собирать штрихкоды и остальное. Не нужно пока методов и чего-то еще. Как только вы сделаете класс, сразу же доступно API по которому узлы (объекты класса) можно передать внешними запросами.

Подсказка: по API с готовыми запросами на закладке API. Но мы воспользуемся чуть позже.

_images/qs_class.png

Шаг 2. Добавим конфигурацию в репозиторий на устройство если она еще не добавлена.

_images/qs_repo.png

Шаг 3. Для того чтобы обмениваться с устройствами есть Rooms. Это по сути – WebSocket. Создадим комнату и подключим ее на устройстве (устройствах) .. attention:: Помимо сканирования QR-кода комнаты также в настройках надо ввести ваш Логин/Пароль к сервису. Он не передается в QR в целях безопасности. .. image:: _static/ qs_settings.png

scale:

55%

align:

center

Шаг 4. Теперь нам надо передать несколько пробных узлов на сервер и далее на устройства в комнате. Передадим запрос с парой объектов (я использую Postman для примера) воспользовавшись API с закладки. Используем запрос с регистрацией записи в комнату(это совмещение двух действий – передача узлов на сервер и регистрация в комнате для устройств. Можно сделать двумя запросами также.). Для меня это команда такая, у вас будут свои идентификаторы. Также не забудьте про авторизацию. POST https://nmaker.pw/api/config/348fa9a9-dd2f-4a76-8c73-b5c15bbb3ea1/node/MyDoc?room=db73145a-2d3d-4afb-954f-e158e7c86e02 *

*Тут надо указать room_id созданной комнаты.

Сделаем 2 вот таких задания без табличных частей для простоты, а просто словесное описание

 [{
   "_id": "001",
   "_data":{
       "_id": "001",
       "title": "Barcode scan #001",
       "instruction": "Scan barcodes of electrical products"
   }
},
{
   "_id": "002",
   "_data":{
       "_id": "002",
       "title": "Barcode scan #002",
       "instruction": "Scan battery barcodes"
   }
}
]

Шаг 5. Зайдем на устройство и проверим что на закладке My documents появились узлы.

_images/qs_nodes.png

Шаг 6. Помимо документов нам надо еще передать справочник товаров с штрихкодами. Для этого надо создать Датасет goods. Зададим для него настройку полей для поиска и хаш-индексов. И передадим «справочник товаров».

_images/qs_dataset.png

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

https://nmaker.pw/api/config/348fa9a9-dd2f-4a76-8c73-b5c15bbb3ea1/dataset/<dataset_name>/items

Вот такой JSON

 [
  {"_id": "1", "name": "WIFI wall switch 3 line", "barcode": "1000075755897"},
  {"_id": "2", "name": "EKF MRVA", "barcode": "4690216127392"},
  {"_id": "3", "name": "Smurtbuy AAA 10x", "barcode": "4690626023178"},
  {"_id": "4", "name": "Duracell CR2032", "barcode": "6911332373226"}
]

Теперь все готово чтобы сделать клиентскую часть.

Шаг 7. Наши узлы выглядят некрасиво. Давайте поменяем обложку. У нас у узлах MyDoc есть title и instruction. Давайте вывдем ее и ID узла заодно. Размещаем 3 «строки» с id, заголовком и инструкцией. О правилах разметки можно почитать подробнее в разделе Мобильный клиент.

_images/qs_cover3.png

Шаг 8. Пришла пора нарисовать форму узла . Разместим там то же что и в обложке + таблицу отсканированных. В таблице для краткости не будем делать макет записи – она сама его сгенерирует. Будем размещать в строках штрихкод, название товара и количество

Данные таблицы – list_scanned, будут браться из self._data[«scanned»] куда мы их будем помещать при сканировании

if "scanned" in self._data:
  list_scanned = self._data["scanned"]
else:
 list_scanned = []

self.Show([
     [{"type":"Text","italic":true,"value":"@_id"}],
     [{"type":"Text","bold":true,"value":"@title"}],
     [{"type":"Table","id":"t1","value":list_scanned }]
   ])

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

Шаг 9. Надо добавить сканирование и ввод количества.

Первым делом подключим сканер-камеру (аппаратный сканер подключается схожим образом) с помощью команды PlugIn self.PlugIn([{"type":"CameraBarcodeScannerButton", "id":"barcode_cam"}]) Напишем метод, который будет обрабатывать штрихкод-искать по справочнику и если найдено то запрашивать количество в диалоге. Это можно сделать и по другому (через экраны), но хочу показать работу с диалогом. Для этого мы ищем по датасету, если нашли берем название товара и показываем диалог. Штрихкод и название складываем пока просто в _data – они нам понадобятся. Это не обязательно хранить так – можно завести переменные в модуле обработчика, просто я пишу в онлайн конструкторе и мне удобней так.

Вот такой текст получился

barcode = self._data["barcode_cam"]
goods= GetDataSet("goods")
res = goods.getStr("barcode",barcode)
if res == None:
  speak("Product not found")
else:
  result = json.loads(res)
self._data["barcode"]  = barcode
self._data["sku"]  = result.get("name")

Dialog("dlg_qty","Enter  quantity","Ok",None,[[{"type":"Input","id":"qty_dialog","caption":"Quantity","input_type":"number"}]])

Вешаем на событие штрихкода наш метод.

И сразу сделаем обработку события диалога ввода количества. Обратите внимание что фильтр по listener имеет суффикс _positive – т.е. пользователь подтвердил ввод

_images/qs_process.png

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

qty = self._data.get("qty_dialog")

if "scanned" in self._data:
  scanned = self._data["scanned"]
else:
  scanned = []
scanned.append({"barcode":self._data["barcode"],"name":self._data["name"],"qty":qty})
self._data["scanned"] = scanned
#save node
self._save()
self.Open()

Проверяем сканирование

Шаг 10. Добавим фотографии с галереей. Предыдущих шаг был чудовищно сложен, поэтому с фотографиями все будет по-простому. Собственно, чтобы добавить фото с камеры и галерею достаточно их подключить через PlugIn и это будет работать. Проверьте.

self.PlugIn([{"type":"CameraBarcodeScannerButton", "id":"barcode_cam"},
 {"type":"PhotoButton", "id":"capture_photo"},
 {"type":"MediaGallery", "id":"pic_files"}
])

Но у нас с вами есть задача не просто копить фотки на устройстве а еще забирать их во внешнюю систему, а галерее – пути к файлам, которые не дадут нам информации.

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

Делаем метод и повесим его на событие камеры

if "photos_base64" in self._data:
  photos_base64= self._data["photos_base64"]
else:
  photos_base64= []
base64 = getBase64FromImageFile(self._data["result_file"],50,50) #ни к чему не ведет, просто получили кроп в base64
photos_base64.append(self._data["result_file"])
self._data["photos_base64"] = photos_base64
self._save()
toast("Saved to photos")

Вот так выглядит теперь наш «документ»

_images/qs_doc_result.png

Шаг 11. Отправка на сервер. Также сделаем кнопку чтобы выгружать документ на сервер.

Добавим красивую кнопку в тулбаре

{"type":"ToolbarButton","id":"btn_upload","caption":"Upload","svg":svg,"svg_size":24,"svg_color":"#FFFFFF"}

И сделаем метод который будем выгружать. Пропишем обработчик события. Также надо чтобы в Серверах был сервер по умолчанию. Для выгрузки на сервер мы используем просто метод _upload который перезаписывает _data объекта на сервере. Тут есть также много путей реализации. Например можно в узлле на сервере сделать серверный метод accept_data в который передать через параметры нужные данные, а оно будет обрабатываться. Но тут по-простому.

status,error = self._upload()
if status:
  message("Object uploaded")
else:
  toast(error)

Шаг 12. Теперь из внешней системы нам надо получить то что мы сделали на устройстве. Воспользуемся API.

Это конечный результат: мы сделали сервер, оффлайновое решение для сбора товаров и фоток и можем забрать результат работы устройств с сервера.

_images/qs_doc_result.png

Подсказка

Важно понимать, что продемонстрированный стек «узлы + датасеты» НЕ является безальтернативным! Можно организовать хранение и задач пользователя и справочника товаров на SQLite (в python в onLaunch инициализировать базу в папке приложения и писать/читать в нее). Есть Pelican и key-value хранилище - NoSQL подход для того же самого как вариант.

Этот пример можно скачать тут: https://disk.yandex.ru/d/Gtpq4nfO7oYskA или в разделе Samples на GitHub

Еще один пример клиент-серверного приложения с веб- и мобильными клиентами, работы с фотографиями разными способами. Организация настроек.

_images/qs_photo_notes.png

В данном примере реализуются заметки (Тема/Текст) с прикрепленными фотографиями, с полноценным веб-клиентом, в дополнении к самостоятельному мобильному клиенту. Мобильный клиент отправляет фото на сервер. Показано два способа работы с фото – с base64 и чистыми файлами. И показан паттерн узла «Настройки» для констант решения.

Пример не по шагам, просто что нужно сделать.

Класс для заметок с фото для мобильного и веб-клиента

Заводим класс, в котором будут хранитсья заметки и фото к ним – PhotoNote.

Форму сделаем не через обработчик onShow, а просто в свойстве класса Init screen

_images/qs_photo_notes_class.png

Но нам надо все равно сделать обработчик onShow так как PlugIn такого пока не предусмотрено

self.PlugIn([
 {"type":"PhotoButton", "id":"capture_photo"},
 {"type":"MediaGallery", "id":"pic_files"}
])

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

 self.PlugIn([
 {"type":"MediaGallery", "id":"pic_files_web"},
 {"type":"FileGallery", "id":"files"}
])

Прежде чем двинуться дальше надо надо организовать хранение константы base64 – галочки, которая хранит способ, как хранить и передавать файлы. В данном примере реализуем 2 подхода.

Как организовать настройки?

Сделаем класс Settings типа custom_process (Пользовательский процесс). Он отлично подходит для хранения констант и настроек. В классе не надо делать никаких методов, только Обложку и Init screen.

Обложка:

[[{"type":"Text","value":"Client-server image exchange settings"}],["Use base64|@base64"]]

Экран:

[[{"type":"Switch","caption":"Use base64 compressing","id":"base64","value":"@base64"}]]

Нам понадобится сохранение, включим Use standard commands. Все, этого доставточно.

Сразу покажу как получать настройки:

В мобильном клиенте: находим узел с настройками и берем из него значение. Чтобы обратиться к узлу по формуле <uid конфигурации>$<имя класса> (такой uid у custom_process) мы используем константы current_module_name

base64opt =False settings = Settings.get(current_module_name+»$Settings») if settings:

base64opt = settings.get(«base64»,False)

В веб-клиенте: тут нет такой константы, но можно ее вычислить из _id узла, она там всегда есть:

#get instance UID s = self._data.get(«_id») i = s.find(«$») current_config_uid = s[:i] if i > 0 else «» #get Settings node settings = Settings.get(current_config_uid+»$Settings»)

Передача картинок на сервер

Далее нам надо в зависимости от настроек организовать отправку файлов. Отправлем так же как в предыдущем примере – в асинхронном обработчике .

Способ отправки картинок через bas64 аналогичен предыдущему примеру. Этот способ хорош тем что данные инкапсулируются в узел, хранятся прямо в _data и путешествуют с узлом. Т.е. если планируется обратиться по API к узлу из внешней программы, там уже все будет. В случае с файлами, потребуется преобразование. Плох тем, что операции преобразования довольно затратные.

Способ отправки картинок «через файлы» более производителен. Тут используется обычный requests.post. Картинки отправляются на роут NodaLogic /api/userfiles/<configuration_uid>/images где сохраняются в папку и далее хранятся просто по именам файла. и вот что нам надо для того чтобы отправить картинку:

  1. Путь к файлу – он у нас есть. В self._data[«result_file»] записывается путь после сохранения фото

  2. Короткое имя файла – его можно вычислить строковым методом

  3. URL сервера. Тут понадобится getServerUrl которая читает реальный URL по альясу (про это написано в памятке в этом разделе). Это паттерн «тиражного решения», если делаете для себя, то можно просто указать URL в теле обработчика – захардкодить

  4. uid инстанса - current_module_name

Итого весь обработчик

base64opt =False
settings = Settings.get(current_module_name+"$Settings")
if settings:
 base64opt = settings.get("base64",False)


if base64opt:
 if "photos_base64" in self._data:
   photos_base64= self._data["photos_base64"]
 else:
   photos_base64= []

 base64 = getBase64FromImageFile(self._data["result_file"],50,50) #ни к чему не ведет, просто получили кроп в base64
 photos_base64.append(base64)

 self._data["photos_base64"] = photos_base64
 self._save()
 self.upload_to_server()
else:
 import requests
 from com.dv.noda import NodesCore as ncore

 server_url = ncore.getServerUrl(current_module_name,"main")

 path = self._data["result_file"]
 filename = path.split('/')[-1]
 url = f"{server_url}/api/userfiles/{current_module_name}/images"

 files = [
     ("files", (filename , open(path , "rb"), "image/png"))
 ]

 r = requests.post(url, files=files, data={"overwrite": "0"})
 if r.status_code==200:
   if "pic_files_web" in self._data:
     pic_array = self._data.get("pic_files_web")
   else:
     pic_array = []
   pic_array.append(filename )
   self._data["pic_files_web"]=pic_array
   self._save()
   self.upload_to_server()

 toast(r.status_code)

Еще для красоты можно в обложку добавить ImageSlider. В данном примере он просто получает в качестве value путь к массиву файлов. В него можно вывести captions – какие то осмысленные подписи, у меня просто имена файлов. Также у меня используется свойство cover для «вписывания» в размер элемента с заполнением. Вообще можно для этого при обработке файла также готовить миниатюры.

_images/qs_photo_notes_imageslider.png

Больше ничего делать не надо. Вы можете просматривать галерею фото в заметке и на мобильном и на вебе.

Пример можно скачать в Samples на GitHub https://github.com/dvdocumentation/nodalogic - config_PhotoNotes.

Большой клиент-серверный докуменоориентированный пример, с интеграцией с 1С

Пример учетного решения для складского учета в докумментно-ориентированной форме тут: https://infostart.ru/1c/articles/2614496/