Описание примера
Пример такой: чисто мобильный, самостоятельный (т.е. работающий оффлайн) фронт NodaLogic (сервер NL в данном примере используется только для синхронизации справочников и не несет бизнес-логики), на устройстве создаем документы-контейнеры, в которые оффлайн сканируем товар, редактируем, распознаем по справочнику по штрихкоду, а потом выгружаем в 1С через онлайн-обработчик в любой документ, имеющий табличную часть «Товары». Т.е. работа с документами на устройстве – оффлайн, а выгрузка – онлайн через онлайн-обработчики на стороне 1С. Этот пример легко развить и переделать.
Пример рассчитан на внушительную производительность и легко может насчитывать миллион строк и больше в документе (главное, чтобы в 1С это загрузилось потом) и быстрый поиск по миллионным справочникам.
Естественно, сканирование работает как с ТСД, так и через камеру.
Естественно, оффлайн: нет связи, не будет синхронизации, есть связь – выгрузили в 1С.
Цель примера - показать:
- работу с контрактами (синхронизацию объектов из 1С) и чем отличается от датасетов и других способов синхронизации.
- работу с онлайн-обработчиками на стороне 1С
- как реализуется переключение экранов
- подход к реализации больших документов
- работу с индексами
Данный пример можно было бы и целиком реализовать на онлайн-обработчиках 1С, не использовать сервер и синхронизацию вообще, но тогда пропадает весомое преимущество - производительность и независимость от канала связи.
Сами онлайн обработчики в свою очередь добавлены для совместимости с SimpleUI (//infostart.ru/1c/tools/1153616/) поскольку проект SimpleUI постепенно закрывается спустя 7 лет свое существования в пользу NodaLogic.

Основное описание релиза на Хабр, даю его в качестве ознакомления https://habr.com/ru/articles/1046792/
И также нам понадобится справочник по онлайн-командам из документации NodaLogic https://nodalogic-txt-ru.readthedocs.io/ru/latest/http.html
Серверную часть, как и веб-клиент, мы не используем в этом примере, хотя можно легко подключить.
Сам пример можно делать как на nmaker.pw, так и на своем сервере, скачав его с GitHub: https://github.com/dvdocumentation/nodalogic
И готовая конфигурация (файл *.nod) лежит в Samples: Пример гибрида оффлайн/онлайн с онлайн-обработчиками 1С
Шаг 1. Создадим конфигурацию в конструкторе NodaLogic
Создадим конфигурацию и классы - где будут хранитьcя данные. Товары (класс Goods) у нас в данном примере – узел, далее расскажу, чем отличается от решения «товары = датасет» в предыдущих примерах.

В классах появился мастер язык описания структуры данных. Это необязательный механизм, больше сделано для ИИ-агентов, которые будут генерить узлы-документы, чтобы они понимали, из чего потенциально может состоять узел, какие в нем данные. Так как NL – по сути JSON-NoSQL, то в отличие от SQL тут нет жесткой структуры как в 1С, поэтому так.
Кроме того, для человека-разработчика это добавляет читаемости. И также форму можно сгенерить по этой структуре, что облегчает работу.

Делаем классы по такому принципу:
Container - это шапка документа, кроме каких-то общих реквизитов (дата и комментарий) в ней ничего нет.
ContainerLine – строка документа, ссылающаяся на шапку через реквизит … . Для нее нарисуем экранчик, потому что редактирование будет в форме узла, нужен интерфейс с полями редактирования
Обложка для строки необязательна, так как «табличная часть» у нас будет в форме плоской таблицы, а не списка карточек, но можно вывести эти узлы в разделе, чтобы было видно, а для этого лучше нарисовать обложку.
Для строки мы заведем хэш-индекс (индекс по точному значению) в поле, где будет храниться ссылка на документ – шапку (корневой документ – Container), чтобы потом быстро отобрать все строки по этой ссылке. Т.е. узлы строки у нас независимые. Нетрудно догадаться, что добавлять такие строки можно в принципе бесконечно, ограничение только по размеру диска. Кроме того, работа с такими строками идет не через документ и при добавлении строки нам не надо перезаписывать корневой документ, что положительно сказывается на быстродействии. Поиск строк через хэш индекс тоже быстродействующая штука. С какой стороны ни посмотри – замечательный вариант, быстродействие – максимальное. Правда, с точки зрения синхронизации вариант «строки в корневом узле» попроще, чем этот. Поэтому ранее я показывал примеры, где строки внутри узла или механизм подчиненных узлов (он тоже хранит внутри).
Goods - сюда приедут товары со штрихкодами из 1С. Поэтому мы сделаем хэш-индекс в разделе Индексы, чтобы можно было искать по штрихкоду.
Кстати, еще можно сделать Триграмм-индекс для наименования, для того, чтобы искать товары по нечеткому поиску.
Вот так это работает, если захотите использовать

Сделаем раздел и укажем его у класса Container.
Можно для отладки и понимания также сделать еще вспомогательный раздел и поместить туда Goods и ContainerLine, хотя оно и без этого будет работать.
Так как наш пример с оффлайн-возможностями, то обработчики на python. Все то же самое можно сделать на NodaScript, который имеет синтаксис 1С. Можно и на онлайн-обработчиках целиком, но тогда это будет привязано к связи, а это сужает возможности.
Шаг 2. Оффлайновые обработчики
Смысл python-обработчиков в Container такой:
Когда мы добавляем строку кнопкой, мы создаём узел класса ContainerLine и прописываем в нем ссылку на документ шапку parent_doc, и потом открываем
new_line = ContainerLine.create(data={
"parent_doc": self._data["_id"],
"qty": 1
})
#открываем строку-узел
new_line._open()
Когда сканируем штрихкод, мы просто ищем в Goods по индексу и перерисовываем экран, чтобы обновить таблицу.
#Ищем товар по индексу-штрихкоду
sku = getByIndex("Goods","barcode",self._data.get("barcode"))
if not sku:
speak("Товар не найден")
#Но все равно добавим строку
new_line = ContainerLine.create(data={
"parent_doc": self._data["_id"],
"qty": 1,
"barcode":self._data.get("barcode")
})
new_line._save()
else:
new_line = ContainerLine.create(data={
"parent_doc": self._data["_id"],
"qty": 1,
"barcode":self._data.get("barcode"),
"sku":sku._data["_id"]
})
new_line._save()
#Это для перерисовки всего экрана-вызовем метод Open. Можно только обновить таблицу, но на экране по сути почти ничего больше нет
self.Open()
При Открытии (и сразу «при возврате на экран», onResume, потому что у нас будет отрываться строка в отдельном экране и надо перерисовать при этом экран корневого документа) у нас работает обработчик Open.
Что он делает: сначала он отбирает по индексу и помещает в _data массив ссылок на узлы-строки
rows = findByIndex("ContainerLine", "parent_doc", self._data["_id"]) or [] #находим по индексу строки документа
row_uids = to_uid(rows) #таблица принимает "массив ИД" поэтому преобразуем массив узлов в него
self._data["container_lines"] = row_uids
Ну и потом, он выводит начальный экран. Тут я сделал через Show, хотя в других примерах разметку использую на закладке Отображение, потому что на этом узле предполагается 2 экрана. Таблица по умолчанию выводится как список карточек, но тут я решил использовать вид «плоской таблицы» в элементе Table. Если не нравится – можно убрать.
self.Show(
[
[ {"type":"Input","caption":"Дата","id":"date","value":"@date","input_type":"date"} ],
[ {"type":"Input","caption":"Комментарий","id":"description","value":"@description"} ],
[ {"type":"Button","id":"btn_add_positions_ContainerLine","caption":"Добавить строку","target_class":"ContainerLine","target_field":"positions","target_relation":"childnode"} ],
[ {"type":"Parameters","w":1,"height":-1}, {"type":"Table","table":True,"table_header":["Штрихкод|barcode|2","Товар|sku_view|2","Кол-во|qty|1"],"id":"positions","nodes_source":True,"value":"@container_lines"} ]
]
)
Шаг 3. Синхронизация товаров
Далее нам надо как то передать Goods из 1С. В нашем решении товары - это узлы класса Goods, а ранее в примерах товары были не узлы, а датасеты. Теперь появился режим пакетной передачи - контракты. Он передает узлы не сильно оперативно, зато может прокачивать большие данные с учетом обрывов, докачек и поддерживая высокую производительность. Датасеты - это неизменяемые внешние данные, узлы - обычные документы системы, т.е. данные, интерфейсы, бизнес-логика, обработчики. Что из этого выбрать - решать вам. Опять же ранее в примерах показывал синхронизацию узлов через Rooms или мессенджинг - но это больше про оперативную "мгновенную" доставку, а не про "закачать миллион-другой товаров".
Для этого мы сделаем шаги:
1. Подключим расширение из //infostart.ru/1c/articles/2614496/
2. Сделаем в 1С настройки соединения с сервером – логин и пароль пользователя на ресурсе, URL – локального инстанса или nmaker.pw
3. И нам надо создать контракт на сервере – куда будет выгружаться - и прописать его на сервере и клиенте. Контракты – это не часть конфигурации – это такие самостоятельные API, которые принимают данные из внешних систем, накапливают их, отслеживают реальные изменения в данных (чтобы не выгружать все подряд) и отдают это клиентам, подписанным на контракт. Т.е. для 1С это просто API, куда слать данные и не задумываться, как они будут доставлены. Об этом позаботится контракт. С точки зрения аналогий в мире 1С – это типа РИБ.

Для этого перейдем в режим Контракты, создадим контракт, выберем источник класса – class и добавим нашу конфигурацию и класс Goods. Что это значит? Контракт может принимать данные и реплицировать их для нескольких конфигураций. В данном случае мы сказали, что надо эти данные передавать в Goods конкретной нашей конфигурации.

4. Также добавим контракт на устройство. К этому моменту конфигурация должна быть в репозитории на устройстве. Сделать это можно через меню Контракты, надо отсканировать QR контракта. Также можно еще в настройках указать таймер для скачивания контракта по расписанию. Можно также запустить скачивание вручную через менюшку карточки


5. Продолжим в 1С. Что надо сделать. Сначала надо зайти в Настройки обмена и «Загрузить конфигурации NodaLogic» - она скачает все, что нам нужно – конфигурации, классы и контракты тоже тут. Это наш получатель

и вот он загруженный

6. Далее надо настроить Правила выгрузки NodaLogic (контракты), указав в качестве получателя наш контракт. В отборе можно указать какой-то отбор или ничего не указывать. Но как минимум лучше указать, что ЭтоГруппа = Ложь. Группы нам не нужны, будут мешаться.

Текст вот
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| УНИКАЛЬНЫЙИДЕНТИФИКАТОР(Источник.Ссылка) КАК _id,
| Источник.Наименование КАК name,
| Источник.Артикул КАК code,
| ШтрихкодыНоменклатуры.Штрихкод КАК barcode
|ИЗ
| Справочник.Номенклатура КАК Источник
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ШтрихкодыНоменклатуры КАК ШтрихкодыНоменклатуры
| ПО Источник.Ссылка = ШтрихкодыНоменклатуры.Номенклатура
|ГДЕ
| Источник.Ссылка В(&МассивОбъектов)";
Запрос.УстановитьПараметр("МассивОбъектов", МассивОбъектов);
Таблица = Запрос.Выполнить().Выгрузить();
МассивДляJSON = Новый Массив;
Для Каждого СтрокаТЗ Из Таблица Цикл
СтруктураСтроки = Новый Структура;
СтруктураСтроки.Вставить("_id",СокрЛП(СтрокаТЗ._id));
СтруктураСтроки.Вставить("name",СокрЛП(СтрокаТЗ.name));
СтруктураСтроки.Вставить("code",СокрЛП(СтрокаТЗ.code));
СтруктураСтроки.Вставить("barcode",СокрЛП(СтрокаТЗ.barcode));
МассивДляJSON.Добавить(СтруктураСтроки);
КонецЦикла;
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, МассивДляJSON);
РезультатВыгрузки = ЗаписьJSON.Закрыть();
//Дла классов можно взять из Правило.Получатель.UrlPost для датасетов - Правило.Получатель.url
ОтправитьЗапрос("", РезультатВыгрузки,Правило)
7. После этого перейти в NodaLogic: настройки обмена – выгрузить по правилам. Если все нормально, товары ушли на сервер. Можно увидеть в бейджике количество объектов

Если запустить скачивание на устройстве (перезагрузить или вручную), то там тоже что-то должно скачаться. Как минимум можно кликнуть на контракт и посмотреть, что там есть. Если вы разместили Goods в разделе, то там это тоже появится. Еще раз напомню – это обычные узлы, а контракты – просто пакетный способ доставки. Того же самого результата можно добиться другими способами – Rooms, мессенджинг или своей синхронизацией через API.
Далее нам надо добавить кнопку выгрузки в документ 1С (любой) и заставить ее работать. Это будут онлайн обработчики в 1С.
Пропишем кнопку в PlugIn (можно и в коде) в тулбаре.

В расширении уже есть обработчики этого примера в общем модуле nl_Обработчики. Важно опубликовать HTTP-сервис расширения.
В устройствах необходимо в настройках прописать параметры соединения с онлайн-обработчиками.

Первым обработчиком у нас будет nlОбработчики. upload_to_documents_open
Вешаем его на событие

И что он делает: он добавляет в _data переменную types – это выпадающий список с типами документов, как они заданы в 1С – метаданные, синоним, а второе действие он рисует 2й экран вместо первого. На самом деле в NL нет экранов, есть только «холст», который перерисовывается, т.е. все динамическое. Второй экран мне было недосуг делать в виде структуры, я набросал его в конструкторе и вытащил в 1С как JSON-строку, но так делать необязательно, параметр Show – разметка экрана (т.н. «массив строк»), о ней написано тут … И можно тут поступать по разному. Например, я сделал макет и меняю переменные, но можно, например, динамически менять сам макет. К примеру, в Spinner value указать не ссылку на переменные, а сам список.
Вот так выглядит обработчик. Из Данные он читает и туда же пишет данные. Это не строковый hashMap, как был в Simple, а Структура с типизацией. А в Команды он пишет список того, что клиент должен сделать, когда получит ответ от 1С.
Процедура upload_to_documents_open(Данные,Команды) Экспорт
Данные.Вставить("types",ПолучитьДокументыСТЧТоварыВФорматеJSON());
Команда = Новый Структура;
Команда.Вставить("command","Show");
Команда.Вставить("argument",ЭкранВыгрузки());
Команды.Добавить(Команда);
КонецПроцедуры
На выбор из выпадающего списка мы вешаем upload_to_documents_select, в selected_type попадает выбранное значение. Задача обработчика – заполнить список документов выбранного типа. И также делаем Show, чтобы перерисовать экран опять. Обработчик похож на предыдущий:
Процедура upload_to_documents_select(Данные,Команды) Экспорт
Данные.Вставить("documents",ПолучитьДокументыПоТипу(Данные.selected_type));
Команда = Новый Структура;
Команда.Вставить("command","Show");
Команда.Вставить("argument",ЭкранВыгрузки());
Команды.Добавить(Команда);
КонецПроцедуры
И наконец, когда мы кликаем на карточку документа, в него должны загрузиться товары из Conainer. Тут на событие повешено 2 обработчика друг за другом. Сначала идет python-обработчик, он готовит в _data массив row_data – читает подчиненные строки и записывает туда JSON для 1С. Далее выполняется upload_to_documents_select_line. Его задача взять документ, пробежаться по row_data и добавить строки. Пару слов о ссылках. В documents мы клали УИД документа, при клике на карточку в переменную <имя таблицы>_selected_data возвращаются данные карточки, там есть и _id, по нему находим документ. С номенклатурой чуток посложнее. Надо было при выгрузке в контракт добавить поле uid1C, но я не догадался это сделать, поэтому УИД 1С выковыряем из _id . _id узла в NodaLogic всегда UID_конфигурации$ИмяКласса$id, вот последнее id нам и надо.
Все. Записали документ и последний штрих, надо надо вернуться на «экран 1», а первый экран отрисовывается методом Open, поэтому мы просто вызовем из 1С питоновский метод Open через RunPython. На самом деле это мощный инструмент – вызов метода из 1С, с помощью него можно сделать многое.
Процедура upload_to_documents_select_line(Данные,Команды) Экспорт
Док =Документы[Данные.selected_type].ПолучитьСсылку(Новый УникальныйИдентификатор(Данные.table_upload_documents_selected_data._id));
ОДок = Док.ПолучитьОбъект();
ОДок.Товары.Очистить();
Для каждого стр из Данные.row_data Цикл
Если стр.Свойство("sku") Тогда
Если ЗначениеЗаполнено(стр.sku) Тогда
МассивЧастей = СтрРазделить(стр.sku, "$");
ПоследняяЧасть = МассивЧастей[МассивЧастей.Количество() - 1];
Номенклатура = Справочники.Номенклатура.ПолучитьСсылку(Новый УникальныйИдентификатор(ПоследняяЧасть));
Если ЗначениеЗаполнено(Номенклатура) Тогда
НовСтр = ОДок.Товары.Добавить();
НовСтр.Номенклатура = Номенклатура;
НовСтр.Количество = стр.qty;
НовСтр.КоличествоУпаковок = стр.qty;
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецЦикла;
Попытка
ОДок.Записать();
Команда = Новый Структура;
Команда.Вставить("command","toast");
Команда.Вставить("argument","Загрузили норм");
Команды.Добавить(Команда);
Исключение
Команда = Новый Структура;
Команда.Вставить("command","toast");
Команда.Вставить("argument","Ошибка записи");
Команды.Добавить(Команда);
КонецПопытки;
Команда = Новый Структура;
Команда.Вставить("command","RunPython");
Команда.Вставить("argument","Open");
Команды.Добавить(Команда);
КонецПроцедуры
Ссылки и где скачать
Пример к этой статье лежит на GitHub в Samples: https://github.com/dvdocumentation/nodalogic
А онлайн часть этого примера и все остальное в статье: //infostart.ru/1c/articles/2614496/
Вступайте в нашу телеграмм-группу Инфостарт