Классы¶
Класс узла – это с одной стороны аспект хранения данных (структура, где экземпляры класса будут храниться и иметь API для доступа к классу и узлам), с другой – это как классический класс в ООП – инкапсулирует методы и наследует методы родителя (класса Node). Также класс описывает поведение объектов – то к какому разделу они относятся, какие события генерируются, обложка и т.д. Свойства класса относящиеся к клиенту разобраны в разделе Мобильный клиент.
В этом разделе описана работа с методами классов отдельно для клиента и сервера, а также работа с сервером через «удаленный класс». И, как в стандартном ООП есть методы класса (создание объекта, поиск объекта, получить все объекты и т.д.) и методы самого экземпляра класса (в NL это называется «узел»), наследуемые от объекта Node.
Выполнение методов и обработчики¶
Метод может выполняться:
Локально на сервере(и веб клиенте) и на мобильном клиенте
В мобильном клиенте можно создать RemoteClass и получить «двойника» сервера с методами и данными. Под капотом там – HTTP запросы
Внешняя система может выполнить метод на сервере через REST-API
Внешняя система может выполнить метод даже на клиенте через Room, послав запрос и потом еще один для получения результата
Выполнение событий узла¶
На клиенте возникают события (закладка События) . У каждого события есть тип и может быть еще уточняющее свойство listener. Например есть 2 кнопки с id=button_1 и id=button_2, при нажатии в обоих случаях будет событие onInput но разный listener button_1/button_2 На событие onInput можно назначить один обработчик без указания Listener, тогда по обоим кнопкам мы получим выполнение этого обработчика, либо сделать 2 обработчика событий с указанием listener, таким образом listener – это фильтр
На одно событие можно подписать массив обработчиков – они будут выполняться друг за другом
Свойство action отвечает за то, как будет выполнено событие в системе – run- синхронно, runasync – асинхронно (имеет смысл заполнить свойство postExecuteMethod - выполнение коллбека после выполнения асинхронного метода) и runprogress - по сути, то же что и runasync но с блокировкой интерфейса
Тут нужно заметить, что в NodaLogic с обработчиками python/android можно в принципе использовать всегда run, а асинхронность и показ прогресс-баров запускать методами клиента, что дает гибкий подход (можно, например сделать прогресс-бар только на кнопке)
В качестве движка выполнения везде рассматриваем python
В событии мы указываем методы, которые надо прописать у класса. Это можно сделать как режиме конфигуратора, на закладке Методы, так и просто в коде обработчиков в теле класса - система подтянет методы из кода. Кстати, можно и классы создавать из кода - они в конфигурацию подтянутся конструктором автоматически.
Пример метода как он выглядит в конструкторе (если работать с методом из конструктора):
self.Show(
[
[{"type":"Input","id":"input1","caption":"input 1","value":"@input1"}],
[{"type":"Button","id":"button1","caption":"Get result"}]
]
)
return True,{}
А вот так выглядит весь класс:
class MyClass(Node):
def __init__(self, modules, jNode, modulename, uid, _data):
super().__init__(modules, jNode, modulename, uid, _data)
"""Class MyClass"""
def Open(self, input_data=None):
self.Show(
[
[{"type":"Input","id":"input1","caption":"input 1","value":"@input1"}],
[{"type":"Button","id":"button1","caption":"Get result"}]
]
)
return True,{}
def Input(self, input_data=None):
toast(self._data["input1"])
return True,{}
В самом методе у нас есть параметры:
self – ссылка на объект класса. Можно обращаться как к собственным методам класса, так и к методам родителя (подробнее тут).
Особо стоит отметить свойство любого узла self._data - хранилище данных узла
На вход метода также может быть передано input_data – словарь с входными данными. UI-события туда ничего не передают, но например если вы вызовете метод из другого метода, то можно передать туда данные в виде словаря.
Также, результат метода возвращается через кортеж True/False, выходные_данные. Первая часть кортежа может влиять на выполнение массива обработчиков события (если False то остальные обработчики не выполнятся) либо можно использовать по своему усмотрению
Из обработчика можно работать не только с текущим узлом, но и с другими узлами через объект класса и методы этих узлов.
Типы событий узла¶
В данном разделе указаны события, относящиемя к конкретному экземпляру узла. Прочие события (не относящиеся к узлу) называются «Общие события» и описаны в соответствующем разделе.
onShow/onShowWeb - запуск формы узла на мобильном/веб-клиенте при открытии любым способом (из списка, методом _open и т.д.)
onResume - только для мобильного клиента, событие при возврате на форму (например, при возврате с другой формы узла)
onInput/onInputWeb - основное событие ввода мобильного/веб-клиента. Через это соыбытие проходит как пользовательский ввод, так и перехват событий, таких как например событие сканера
onAcceptServer - событие перед записью данных. Выполняется только на сервере, независимо от того, где инициирована запись (клиенты или выполнение API-запросов). Это основное событие для построение бизнес-логики сервера. В input_data обработчика передается переменная _saved_state в которой храниться состояние _data до изменения, а в _data при этом - текущее состояние перед записью. Разработчик может повлиять на данные в _data перед записью - дописать, изменить данные. Также можно прервать запись, вернув в кортеже False. Например: return False,{"message":"wrong data"}
Выполнение методов на сервере¶
На сервере выполняются события веб-клиента, событие onAcceptServer для сохраняемых данных, и в будущем будут другие общие события, такие как регламентные задания. Также к любому узлу можно обратиться через API (которое генерируется автоматически при создании класса в конфигурации) и выполнить любой метод.
Можно обращаться к их методам с помощью REST-команды POST /node/<class_name> /<node_id>/<method_name> , которая генерируется для каждого класса своя (на закладке API в классе). Можно передать параметры (попадут в input_data метода) и получить результат.
Для класса таже есть другие команды API (описано в разделе Синхронизация), которые позволяют:
создать/обновить узел выбранного класса
зарегистрировать узел в выбранной комнате для передаче на устройства
получить все узлы
получить доступ к конкретному узлу для получения/обновления/удаления
Работа с узлом на сервере через RemoteClass¶
Не смотря на наличие REST-API удобнее в python-обработчике работать через обертку в виде объекта RemoteClass Через нее можно получить класс на сервере, создавать/обновлять узлы на сервере, выполнять их методы.
Для работы сначала следует получить объект удаленного класса GetRemoteClass(class_name,server_url=»») где надо указать имя класса и можно указать псевдоним сервера. URL – сервера и псевдоним (а также сервер по умолчанию) следует укзаать в секции Servers конфигурации … Как минимум один сервер должен быть по умолчанию). Например так выполучим доступ к классу узлов на сервере:
Warehouse = GetRemoteClass("Warehouse")
У удаленного класса (объект RemoteClass ) есть методы:
get(self, uid) – возвращает удаленный узел по id
create(self, data=None) – создает узел на сервере, можно передать _data для инициализации
all(self) – возвращает список всех узлов класса
Пример:
# Получаем удаленный класс
Warehouse = GetRemoteClass("Warehouse")
# Получаем объект
wh1 = Warehouse.get("4503")
if wh1:
toast("Склад найден")
# Вызываем метод income
result = wh1.income({"qty": 1})
message("Method result:"+str(result))
# Доступ к данным
#message("Quantity:"+ str(wh1._data["qty"]))
#message("Name:"+str(wh1._data["name"]))
# Обновление данных
wh1._data["name"] = "Обновленный склад #1"
wh1._save()
toast("Updated name:"+wh1._data["name"])
# Создание нового объекта
new_wh = Warehouse.create({
"name": "Новый склад",
"qty": 50,
"_id":"2"
})
message("New warehouse ID:"+ new_wh._data["_id"])
# Получение всех объектов
all_warehouses = Warehouse.all()
"""for wh in all_warehouses:
print(f"Warehouse {wh['_id']}: {wh['name']} - {wh['qty']} units")"""
У удаленного узла (RemoteObject) есть общие методы и также польщзвоательские методы и _data
_save(self) - сохраняет данные на диск
_register(self, room_uid) – регистирует узел в комнате для того, чтобы другие устройства его получили через Room
Выполнение методов узлов через Room¶
Наконец есть возможность из внешней системы обратиться непосредственно к узлу на устройстве (напомню, устройство подключено через WebSocket). В данном случае через HTTP мы обращаемся к механизму комнат и передаем запрос через WebSocket
В ответе на запрос, мы получаем request_id по которому позже (когда удаленный узел ответит) мы может прочитать статус выполнения и результат
Методы классы и узлов на клиенте.¶
В решении пользовательские классы являются наследником Node и помимо свои методов, наследуют методы класса и объекта, описанные в данном разделе. Вот пример, показывающий принцип работы с узлами.
class Note(Node):
def __init__(self, modules, jNode, modulename, uid, _data): #обязательно добавляется конструкторм
super().__init__(modules, jNode, modulename, uid, _data)
def MyMethod(self, input_data=None): #пользовательский метод
pass
#...
class_obj = Note() #Объект пользовательского класса
new_note = class_obj.create() #Новый узел
new_note._data["some_variable"]= 1
new_note.MyMethod({"a":5,"b":2})
new_note._save() #Новый узел
Методы класса мобильный клиент¶
create(uid=None, data=None) – создает узел и возвращает объект узла. Можно передать uid
get(uid) – получить объект по UID
get_all() – получить все узлы класса
sort( key=None, reverse=False) – возврат объектов с сортировкой по ключу. И также вспомогательные:
sort_by_field(field_name, reverse=False), sort_by_numeric_field(field_name, reverse=False), ort_by_date_field(field_name, reverse=False, date_format='%Y-%m-%d')find(condition) – поиск объектов по условию в виде lambda-выражения типа lambda node: node[„price“] > 100
count() - возвращает общее количество объектов класса
_upload_all(cls, server_url=None, config_uid=None, condition=None) – выгружает все узлы класса на сервере по умолчанию или на определенный сервер
register_all(cls, room_uid, server_url=None, config_uid=None, condition=None) – регистрирует все узлы класса в комнате для других устройств
Методы узла мобильный клиент¶
_save() – записывает узел на диск. Также можно в классе поставить галочку Автосохрание и ввод из элементов ввода будет записываться автоматически. Важно! при Автосохранении, если не нужно чтобы некоторые переменные записывались, надо чтобы у них название начиналось с «!»
delete() - удаляет узел и все его подчиненные узлы
_upload(self, server_alias=None, config_uid=None) - выгружает/обновляет узел на сервер.
_delete_from_server(self, server_alias=None, config_uid=None) - удаляет объект на сервере
_register(self, room_uid, server_alias=None, config_uid=None) – регистрирует объект в комнате для других устройств
AddChild(parent,_class,uid=None,_data=None) – добавляет подчиненный узел выбранного класса.
new_line = self.AddChild("OrderLine")RemoveChild(parent,uid) – удаляет потомка и всех его потомков рекуррентно
GetChildren(self, level=None) – получает все подчиненные узлы и их подчиненные. Можно выбрать до какого уровня делать срез.
Вспомогательные функции для работы с узлами¶
to_uid(nodes_list) – преобразует список узлов в список UID-ов
from_uid(uids_list) – преобразует список UID-ов в список узлов
Методы класса и узла сервера и веб-клиента
События сервера¶
События сервера происходят независимо от того, где узел подвергается модификации - на клиенте, в API или через другой узел
onAcceptServer - событие перед модификацией данных узла. Позволяет реализовать в обработчике свою логику, менять данные в _data, отменить проведение. Вернув False, можно отменить проведение. Дополнительно в message можно передать сообщение ошибки. В input_data поступает состояние данных ДО изменения в ключ _saved_state, данные которые будут записаны - в self._data
OnAterAcceptServer - событие после записи данных на сервере. в _data - записанное состояние объекта
Методы класса сервера и веб-клиента¶
create(uid=None, data=None) – создает узел и возвращает объект узла. Можно передать uid и начальное значение _data
get(uid) – получить объект по UID
get_all() – получить все узлы класса
sort( key=None, reverse=False) – возврат объектов с сортировкой по ключу. И также вспомогательные: sort_by_field(field_name, reverse=False), sort_by_numeric_field(field_name, reverse=False), ort_by_date_field(field_name, reverse=False, date_format=“%Y-%m-%d“)
find(condition) – поиск объектов по условию в виде lambda-выражения типа lambda node: node[„price“] > 100
Методы узла сервера и веб-клиента¶
_save() – записывает узел на диск.
delete() - удаляет узел и все его подчиненные узлы
_register(alias) - регистрирует узел в комнате по псевдониму
AddChild(parent,_class,uid=None,_data=None) – добавляет подчиненный узел выбранного класса.
new_line = self.AddChild("OrderLine")RemoveChild(parent,uid) – удаляет потомка и всех его потомков рекуррентно
GetChildren(self, level=None) – получает все подчиненные узлы и их подчиненные. Можно выбрать до какого уровня делать срез.
Особые переменные класса (флаги)¶
_background - задает подстветку класса в списках, где он отобажается
_read_only - запрет редактирования для формы узла (на уровне формы)
_skip_accept_handler - отключает 1 раз обработчик onAcceptHandler
Вспомогательные функции на клиенте для работы с узлами¶
to_uid(nodes_list) – преобразует список узлов в список UID-ов
from_uid(uids_list) – преобразует список UID-ов в список узлов
Транзакции¶
Общие для клиент и сервера методы, которые позволяют организовать учет в узле (в _data) ведение журнала защищенных транзакций и вычисления итогов в разрезе аналитик. Например можно в узле «Склад готовой продукции» вести учет товаров в разрезе упаковок, а в узле «Товар» - учет последних цен. Смысл транзакций в том, что они:
всегда накопительные (транзакции всегда только добавляются)
неразрывно связаны с итогом (остаток показателя – всегда результат последней транзакции, а ее остаток – результат предыдущей и т.д.)
защищенные (транзакции образуют цепочку, защищенную хешем)
этот подход обеспечивает наибольшую производительность – для вычисления остатка не надо делать sum по таблице, достаточно взять последнюю транзакцию и прибавить к значению новое значение. Для получения остатков – нужно просто взять последние транзакции
В общем, можно сделать учетную систему чисто на узлах (без транзакций), но транзакции – механизм, заточенный на учетные цели.
Типов итогов два:
Суммовые итоги (по суммовым транзакциям). Это когда просто считается сумма – т.е. новое значение прибавляется к предыдущему. Например остаток товара – приход +5, Расход -1. Баланс - 4
Срез последних значений, по транзакциям состояний. Новое значение, заменя предыдущее. Например цена 100, новая цена 105. Баланс – 105.
Транзакции и балансы разделяются по схемам. Это просто строковый идентификатор, который позволяет разделить типы балансов. Например можно одновременно вести остатки товаров в разрезе «товар_единица» (схема _sku_unit) и «товар целиком» (схема _sku). Схема должна соответствовать ключам транзакции – массиву ключей
Методы:
_sum_transaction(self, scheme_name, period=None, keys=None, values=None, meta=None)
scheme_name – идентификатор схемы,
period – дата/время транзакции,
keys – массив ключей
values – массив значений
meta – словарь произвольных данных
_get_balance(self, scheme_name) – получает баланс по суммовым транзакциям
3._get_sum_transactions(self, scheme_name) – получает массив суммовых транзакций
_state_transaction, _get_state_balance, _get_state_transactions – полностью аналогичны суммовым транзакциям, отличается только принцип вычисления баланса.
_sum_transaction_unique(self, scheme_name: str, *, unique_key: str, period: str, keys: list, values: list, meta: dict | None = None, ) - добавляет уникальную (идемпотентную) суммовую транзакцию, которая не может быть изменена при последующих вызовах метода (кроме специальных методов)
_remove_sum_transaction_unique(self, scheme_name: str, *, unique_key: str) - удаляет уникальную транзакцию по ключу
_rebuild_sum_transactions(self, scheme_name: str) - полный пересчет схем
Пример
WClass = GetRemoteClass("Warehouse") #получаем на клиенте класс на сервере
wh = WClass.get(self._data.get("_id"))
res = wh._sum_transaction(
"sku_balance", #название схемы
"2025-09-05", #дата транзакции
[self._data.get("sku")], #ключи аналитики
[5], #значения (количество товара)
meta={"comment": "Приходная накладная #1"}
)
balance = wh._get_balance("sku_balance") #получение остатков