.. NodaLogic documentation master file, created by sphinx-quickstart on Wed Nov 5 07:29:33 2025. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Классы ============== Класс узла – это с одной стороны аспект хранения данных (структура, где экземпляры класса будут храниться и иметь API для доступа к классу и узлам), с другой – это как классический класс в ООП – инкапсулирует методы и наследует методы родителя (класса Node). Также класс описывает поведение объектов – то к какому разделу они относятся, какие события генерируются, обложка и т.д. Свойства класса относящиеся к клиенту разобраны в разделе Мобильный клиент. В этом разделе описана работа с методами классов отдельно для клиента и сервера, а также работа с сервером через «удаленный класс». И, как в стандартном ООП есть методы класса (создание объекта, поиск объекта, получить все объекты и т.д.) и методы самого экземпляра класса (в NL это называется «узел»), наследуемые от объекта Node. Выполнение методов и обработчики ----------------------------------- Метод может выполняться: * Локально на сервере(и веб клиенте) и на мобильном клиенте * В мобильном клиенте можно создать RemoteClass и получить «двойника» сервера с методами и данными. Под капотом там – HTTP запросы * Внешняя система может выполнить метод на сервере через REST-API * Внешняя система может выполнить метод даже на клиенте через Room, послав запрос и потом еще один для получения результата Выполнение событий узла ---------------------------------- .. image:: _static/c_events.png :scale: 70% :align: center На клиенте возникают события (закладка События) . У каждого события есть тип и может быть еще уточняющее свойство listener. Например есть 2 кнопки с id=button_1 и id=button_2, при нажатии в обоих случаях будет событие onInput но разный listener button_1/button_2 На событие onInput можно назначить один обработчик без указания Listener, тогда по обоим кнопкам мы получим выполнение этого обработчика, либо сделать 2 обработчика событий с указанием listener, таким образом listener – это фильтр .. image:: _static/c_actions.png :scale: 70% :align: center На одно событие можно подписать массив обработчиков – они будут выполняться друг за другом Свойство action отвечает за то, как будет выполнено событие в системе – **run**- синхронно, **runasync** – асинхронно (имеет смысл заполнить свойство postExecuteMethod - выполнение коллбека после выполнения асинхронного метода) и **runprogress** - по сути, то же что и runasync но с блокировкой интерфейса Тут нужно заметить, что в NodaLogic с обработчиками python/android можно в принципе использовать всегда run, а асинхронность и показ прогресс-баров запускать методами клиента, что дает гибкий подход (можно, например сделать прогресс-бар только на кнопке) В качестве движка выполнения везде рассматриваем python В событии мы указываем методы, которые надо прописать у класса. Это можно сделать как режиме конфигуратора, на закладке Методы, так и просто в коде обработчиков в теле класса - система подтянет методы из кода. Кстати, можно и классы создавать из кода - они в конфигурацию подтянутся конструктором автоматически. Пример метода как он выглядит в конструкторе (если работать с методом из конструктора): .. code-block:: Python self.Show( [ [{"type":"Input","id":"input1","caption":"input 1","value":"@input1"}], [{"type":"Button","id":"button1","caption":"Get result"}] ] ) return True,{} А вот так выглядит весь класс: .. code-block:: Python 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/ //`` , которая генерируется для каждого класса своя (на закладке 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)** – возвращает список всех узлов класса Пример: .. code-block:: Python # Получаем удаленный класс 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 и помимо свои методов, наследуют методы класса и объекта, описанные в данном разделе. Вот пример, показывающий принцип работы с узлами. .. code-block:: Python 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). Схема должна соответствовать ключам транзакции – массиву ключей Методы: 1. _sum_transaction(self, scheme_name, period=None, keys=None, values=None, meta=None) * scheme_name – идентификатор схемы, * period – дата/время транзакции, * keys – массив ключей * values – массив значений * meta – словарь произвольных данных 2. _get_balance(self, scheme_name) – получает баланс по суммовым транзакциям 3._get_sum_transactions(self, scheme_name) – получает массив суммовых транзакций 4. _state_transaction, _get_state_balance, _get_state_transactions – полностью аналогичны суммовым транзакциям, отличается только принцип вычисления баланса. 5. _sum_transaction_unique(self, scheme_name: str, *, unique_key: str, period: str, keys: list, values: list, meta: dict | None = None, ) - добавляет уникальную (идемпотентную) суммовую транзакцию, которая не может быть изменена при последующих вызовах метода (кроме специальных методов) 6. _remove_sum_transaction_unique(self, scheme_name: str, *, unique_key: str) - удаляет уникальную транзакцию по ключу 7. _rebuild_sum_transactions(self, scheme_name: str) - полный пересчет схем Пример .. code-block:: Python 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") #получение остатков