О плагине

Плагин учёта сетевого оборудования и складских остатков ТМЦ (материалов), ID — inventoru. Два основных направления:

  • Equipment — сетевые устройства (OLT, ONU, коммутаторы, роутеры и т.д.) с типами, производителями, статусами и атрибутами.

  • Складской учёт — номенклатура (ТМЦ), склады, остатки, движения (приход, списание, резерв, возврат, перемещение, корректировка), штрихкоды.

Плагин интегрируется с процессами BGERP через отдельную вкладку «Materials» — резерв и списание прямо из карточки процесса, с блокировкой формы после согласования (см. Вкладка процесса «Materials»).

Есть отдельная двусторонняя интеграция остатков с 1С — реализована как sub-package sync1c внутри этого же плагина (не отдельный плагин: таблицы inventoru_sync1c_*, свой namespace классов, но общий Plugin.java/db.sql/action.xml/l10n.xml). Полностью описана в отдельном документе: Inventory ↔ 1С (sync1c).

Иерархии типов

Склады, номенклатура и оборудование организованы в древовидные иерархии типов (StoreType/ItemType/EquipType) — по тому же паттерну, что и типы процессов в ядре: у каждого типа есть подтипы, набор параметров (param.ids) и (для складов) набор статусов (status.ids), задаваемые независимо на каждом уровне иерархии. У типов номенклатуры дополнительно задаётся вид учёта accounting.kind (см. Вид учёта: расходники и «числится за складом»). См. Паттерн иерархии типов.

Настройка

Включение плагина

Добавить в конфигурацию (config_global):

inventoru:enable=1

Требуется полный рестарт сервера — список включённых плагинов вычисляется один раз при старте, добавление ключа на работающем сервере не подхватывается (без ошибок в логе). Проверка: строка Plugin 'inventoru' …​ init в log/bgerp.log после рестарта.

Права доступа

Настраиваются через Administration → Permission Sets. Основные группы (полный список — action.xml плагина):

Раздел Что даёт

Устройства

Просмотр, карточка, создание, редактирование, удаление, восстановление, производители, типы устройств

Items

Просмотр, создание, редактирование, удаление

Warehouses

Просмотр, карточка, «My warehouse» (store:my, см. Мой склад (store:my)), создание, редактирование, удаление

Stock Movements

Запись движения (одна/несколько), снятие резерва, вкладка процесса «Materials», перемещение между складами (movement:transfer), возврат на склад (movement:returnStock), резолв кода со сканера (movement:resolveCode, см. Штрихкоды и сканирование)

Администрирование движений ТМЦ

Просмотр по складу, приход (movement:receipt), корректировка/инвентаризация (movement:adjust)

Администрирование складов / номенклатуры / оборудования

Заведение, правка, удаление и восстановление — на карточке записи (/user/plugin/inventoru/*), админского списка у них нет. В админке остались только действия, которых на карточке быть не может: у складов минимальный остаток (store:minLevelUpdate), доступ исполнителей и группа редактирования (store:accessList, accessAdd, accessDelete, accessClose, editGroupUpdate), у оборудования — загрузка из файла (device:importForm, importPreview, importApply)

Производители и модели оборудования

Пользовательские экраны-очереди (/user/plugin/inventoru/manufacturer, /model — открываются кнопками с экрана оборудования), плюс полный CRUD в админке (/admin/plugin/inventoru/manufacturer, /deviceType)

Типы складов / номенклатуры / оборудования / производителей / моделей

CRUD иерархии и свойства типа (параметры). Статусы складов — отдельный экран (/admin/plugin/inventoru/storeStatus), общий каталог на все типы

Очереди (админ + пользователь)

Админский экран один на все сущности (/admin/plugin/inventoru/queue), сущность выбирается в форме очереди. Пользовательская часть — по два узла на каждую сущность (queueFilter, queueShow) в её собственном разделе

Incoming from 1C (пользователь)

Просмотр/проведение/отклонение pending-остатков от 1С — доступно любому с доступом к складу (владелец, store_access или «группа редактирования»), не только формальному владельцу

1C Sync

Админка sync1c: инстансы, маппинг, импорт/экспорт, OData, штрихкоды — см. sync1c

Вкладка «Materials» на процессе

Включается для конкретного типа процесса (не глобально), в конфигурации типа:

inventoru:process.showTab=1

Префикс у ключа общий с остальным конфигом плагина, но лежит он не в config_global — его место в поле Конфигурация карточки типа процесса (колонка process_type.config, её разбирает TypeProperties.getConfigMap(), откуда ключ и читает вкладка). Соседняя колонка process_type.data — не то же самое: там живут create.status, status.ids, param.ids, и ключа вкладки в ней быть не должно. Дополнительно у пользователя должно быть право /user/plugin/inventoru/movement:processTab.

Обязательные поля устройства

Что считать заполненной карточкой оборудования, зависит от учёта: где-то технику ведут по серийному номеру, где-то по инвентарному, где-то достаточно наименования и модели. Поэтому список обязательных полей задаётся конфигом (config_global):

inventoru:device.requiredFields=name, deviceTypeId, serialNumber

По умолчанию — name, deviceTypeId: устройство без наименования и модели ничем не идентифицируется. Пустое значение отключает проверку.

Допустимые имена (как у полей формы): name, deviceTypeId, equipTypeId, itemId, storeId, serialNumber, macAddress, assetTag, ip, swVersion. Незнакомое имя — ошибка, а не молча выполненное условие: опечатка в конфиге иначе тихо отключила бы проверку.

Проверка выполняется на сервере, в обоих путях сохранения — админском и пользовательском. Атрибут required в HTML тут не работает в принципе: форма отправляется через $$.ajax.post, а не нативной отправкой, поэтому браузер её не проверяет. Обязательные поля помечаются в форме звёздочкой по тому же списку.

В каких статусах можно резервировать материалы

Статус — это просто запись в глобальном каталоге (process_status_title), без собственной семантики. Финальность статуса, разрешённые переходы и то, что можно делать в этом статусе — решает каждый тип процесса независимо, в своей собственной конфигурации (process_type.data: create.status, close.status, status.ids). Один и тот же id статуса может быть финальным у одного типа и промежуточным у другого — это нормально, не рассинхронизация данных.

Список статусов, в которых разрешено резервировать/отменять резерв материалов — per-type ключ в том же config_global, где уже лежит sync1c:processType.<ID>.statusWriteoff (см. sync1c):

inventoru:processType.<TYPE_ID>.reserveStatuses=<ID1>,<ID2>,...

Пусто/не задано для типа — резерв разрешён в любом статусе (поведение по умолчанию, обратная совместимость). Правится в Administration → Configuration → строка плагина, применяется без пересборки и рестарта сервера (SetupChangedEvent).

Дополнительно, независимо от статуса: если по процессу уже есть успешно отправленный в 1С экспорт списания, вкладка «Materials» заблокирована в любом случае — см. sync1c.

Доступ к складам — монтажники и «кладовщики»

Два независимых, дополняющих друг друга механизма ограничивают, какие склады видит и может редактировать пользователь (во вкладке «Materials» процесса, на складских формах, в «Моём складе», при загрузке CSV/OData-импорте из 1С и в «Ожидающих поступлениях»):

Механизм Когда использовать

inventoru_store_access (store_id, user_id, date_from, date_to — date_to IS NULL = бессрочно)

Точечный, персональный доступ одного пользователя к одному складу на период — типовой случай: монтажник и его личный склад-«рюкзак». Управляется на карточке склада, кнопка Доступ (пользовательский экран Инвентарь → Склады, а не админка).

inventoru_store.edit_group_id — группа пользователей (та же ролевая модель, что и везде в BGERP: пользователь → группа → набор прав), которой разрешено редактировать конкретный склад

Роль «кладовщик» — несколько человек одновременно управляют ОДНИМ складом (принимают/списывают/ редактируют), не обязательно являясь его персональным владельцем и не заводя каждому отдельную запись в store_access. Задаётся полем «Группа редактирования» там же, на экране Доступ карточки склада.

Итоговый доступ пользователя к складу = владелец (store.user_id) ∪ активная запись store_access ∪ членство в группе edit_group_id этого склада. Проверяется одинаково на сервере в StoreAction/MovementAction/UserImportPendingAction/AdminSyncAction — единая логика getAccessRestriction()/getAccessibleStoreIds()/hasStoreAccess() в каждом из них, независимо от выданных прав на сами действия.

  • Если у пользователя нет маркерного права админ-уровня/admin/plugin/inventoru/store:accessList (в дереве прав это Administration → Warehouses → Executor access → List) или, для sync1c-действий, /admin/plugin/inventoru/sync1c/instance:null — видны только склады, попадающие в объединение выше (может быть пустым множеством — тогда не видно ни одного склада, а не все).

  • Если такое право есть — считается «админ-режимом», видны все склады без ограничения. Оно же снимает ограничение и с оборудования: единица техники лежит на складе, доступ к ней определяется доступом к складу.

Право Executor access → List выглядит безобидно — «посмотреть список доступов» — но именно оно и есть маркер «видит и правит ВСЕ склады». Кладовщику его выдавать нельзя. Раньше маркером был /admin/plugin/inventoru/store:null, узел удалённого админского списка складов; в старых наборах прав его можно снимать, он больше ни на что не влияет.

Как завести роль «кладовщик» для нескольких человек на одном складе

  1. Administration → Permission Sets — создать набор прав со scoped-разрешениями (НЕ полный админский CRUD): /user/plugin/inventoru/store:{null,show,my,queueFilter,queueShow}, /user/plugin/inventoru/import_pending:{null,apply,applyAll,reject}, и если нужна загрузка CSV — отдельно /admin/plugin/inventoru/sync1c/instance:{importFileForm,importFilePreview,importFileConfirm} (эти три права независимы от остальной админки sync1c, см. sync1c). Важно НЕ включать маркерные права /admin/plugin/inventoru/store:accessList или /admin/plugin/inventoru/sync1c/instance:null — они превращают пользователя в «видит вообще все склады».

  2. Administration → Users → Groups — создать группу, включить в неё нужных сотрудников.

  3. Привязать набор прав из шага 1 к группе из шага 2 (там же, в редакторе группы).

  4. Инвентарь → Склады — открыть карточку нужного склада, кнопка Доступ, выбрать эту группу в поле «Группа редактирования».

После этого все участники группы видят склад в обычном пользовательском меню (не в админке), работают с его остатками, грузят туда CSV с остатками из 1С и подтверждают/отклоняют поступления — независимо друг от друга, без необходимости заводить каждому отдельную запись store_access.

Кладовщик складом пользуется, а настраивает его администратор. Права store:{create,edit,update,delete} — админские, в набор кладовщика они не входят: иначе он заведёт склад на чужое имя или удалит тот, к которому у него есть доступ. По той же причине не входят item:{create,update,delete} — номенклатура общая для всей компании, и удаление позиции задевает всех; кладовщику достаточно item:{null,show,queueFilter,queueShow}, чтобы видеть позицию и её параметры. По оборудованию кладовщику нужны device:{null,show,update, queueFilter,queueShow} — заводить и удалять единицы техники тоже дело администратора.

Поле «Группа редактирования» при этом остаётся осмысленным и без права на правку карточки: оно определяет доступ — какие склады сотрудник вообще видит и с остатками каких работает (StoreAccessService), а не право менять их настройки.

Очереди

Очередь — настраиваемый список сущности: складов, номенклатуры, оборудования, производителей, моделей оборудования или движений ТМЦ. Заводится в оснастке Администрирование / Инвентарь / Очереди, там же выбирается сущность, которую очередь показывает. Конфигурация устроена так же, как конфигурация очереди процессов.

Кому доступна очередь, задаётся не конфигурацией, а в самой форме очереди — списками Группы пользователей и Пользователи. Действует объединение: сотрудник видит очередь, если она выдана лично ему или любой из его групп. Так же выдаются очереди процессов в ядре (user_queue и user_group_queue), с той разницей, что там это делается в карточке пользователя и группы, а здесь — в очереди. Очередь, не выданная никому, не показывается никому.

column.<ID>.value=<ЧТО ПОКАЗЫВАТЬ>
column.<ID>.title=<ЗАГОЛОВОК>

Колонка, названная внешним ключом (item_id, store_id, manufacturer_id, user_id), показывает не код, а наименование того, на что ключ указывает. Колонка type у движений и status у оборудования показывает состояние словом.

Значения column.<ID>.value зависят от сущности очереди:

Сущность Значения

Склады

id, title, status_id, user_id, deleted, param:<ID параметра>

Номенклатура

id, title, characteristic, item_type_id, deleted, param:<ID параметра>

Оборудование

id, title, serial_number, mac_address, asset_tag, ip, status, store_id, device_type_id, equip_type_id, deleted, param:<ID параметра>

Производители

id, title, slug, country, manufacturer_type_id, param:<ID параметра>

Модели оборудования

id, title (наименование модели), manufacturer_id, device_class, part_number, unit_height, model_type_id, param:<ID параметра>

Движения ТМЦ

id, title (комментарий движения), dt, type, quantity, item_id, store_id, user_id, process_id, serial_number

Мягкого удаления нет у производителей, моделей и движений — колонки deleted у них не бывает. Движения к тому же не имеют собственных параметров, поэтому и param:<ID> для них недоступен: такая колонка в конфигурации пропускается с ошибкой в лог.

filter.<ID>.type=<ТИП>
filter.<ID>.title=<ПОДПИСЬ>
# скрытый фильтр: не предлагается в списке, но применяется
#filter.<ID>.show=0

Типы фильтров: title — у всех; deleted — у сущностей с мягким удалением (склады, номенклатура, оборудование); param:<ID параметра> — у всех, кроме движений ТМЦ, у которых нет своих параметров; для складов ещё status, для оборудования — equipType (выбор типов оборудования, тот самый фильтр, что раньше был единственным фильтром админского списка). Для сущностей, живущих на складе, — store; для движений ТМЦ — type (операция) и dt (диапазон дат операции). Фильтр-диапазон по дате называется именем своего поля и читает <поле>From / <поле>To, как create_date у очереди процессов.

Фильтр по параметру берёт подпись без title из названия самого параметра, а вид контрола — из типа параметра, ровно как очередь процессов:

Тип параметра Что показывается и как ищет

text, blob

Строка поиска по вхождению

list, listcount

Выбор значений; отдельный пункт «Нет значения» находит записи, у которых параметр не заполнен

date, datetime

Два поля «с» и «по»; верхняя граница включает весь названный день

money

Диапазон «От»/«До» и флажок «Нет значения»

address

Выбор города, квартала, улицы, дома и квартиры с подсказками адресной базы

Поддержаны те же типы параметров и те же необязательные ключи фильтра, что перечислены в документации очереди процессовorEmpty, valueFrom/valueTo=curdate, values, availableValues, defaultValues, onEmptyValues, width, fields. Контролы на экране рисует сам ядровый фрагмент, поэтому расхождений с очередью процессов не возникает.

Типы file и email фильтром не поддерживаются — как и в ядре; такой param:<ID> в конфигурации пропускается с ошибкой в лог, чтобы на экране не появлялся контрол, который ничего не отбирает.

Сортировка настраивается ровно как у процессов:

sort.mode.<ID>.columnId=<ID КОЛОНКИ>
sort.mode.<ID>.title=<НАЗВАНИЕ РЕЖИМА>
#sort.mode.<ID>.desc=1
sort.combo.count=<СКОЛЬКО ВЫПАДАЮЩИХ СПИСКОВ>
sort.combo.<ID>.default=<РЕЖИМ ПО УМОЛЧАНИЮ>
Параметр в колонке или фильтре должен принадлежать той же сущности, что и очередь: у параметров склада и номенклатуры значения лежат в общих таблицах, и параметр чужой сущности показал бы значение постороннего объекта. Такая колонка или фильтр пропускается с ошибкой в лог.

Конфигурация — полный список ключей

Все DB-ключи лежат в одной строке config_global («Plugin Inventory», Administration → Configuration), применяются без рестарта (SetupChangedEvent), кроме enable:

Ключ Назначение

inventoru:enable=1

Включение плагина — единственный ключ, требующий рестарта сервера (см. Включение плагина)

inventoru:process.showTab=1

В конфигурации типа процесса (не в config_global) — показывать вкладку «Materials» (см. Вкладка «Materials» на процессе)

inventoru:processType.<ID>.reserveStatuses=<ID1>,<ID2>,…​

Статусы типа процесса, в которых разрешён резерв (см. В каких статусах можно резервировать материалы)

sync1c:processTypeIds=<ID1>,<ID2>,…​

Типы процессов с интеграцией 1С (см. sync1c)

sync1c:processType.<ID>.statusWriteoff=<STATUS_ID>

Статус, при входе в который отправляется списание в 1С

sync1c:export.maxAttempts=3

Максимум попыток отправки экспорта (по умолчанию 3)

sync1c:import.alarmConsecutiveErrors=3

Алярм админу после N подряд неуспешных импортов инстанса (0 — отключить; по умолчанию 3)

odata.*

В поле «Config» конкретного 1С-коннектора, не в config_global (см. sync1c)

Scheduler-таски (в bgerp.properties, scheduler.start=1):

scheduler.task.sync1c_import.class=org.bgerp.plugin.inventoru.sync1c.exec.ImportPoller
scheduler.task.sync1c_import.minutes=*/30
scheduler.task.sync1c_export.class=org.bgerp.plugin.inventoru.sync1c.exec.ExportPoller
scheduler.task.sync1c_export.minutes=*/1

Использование

Плагин добавляет группу Inventory в главное меню: Оборудование, Номенклатура, Склады, Движения ТМЦ, Поступления, Мой склад.

Оборудование

Экран оборудования — очередь с настраиваемыми колонками, фильтрами и сортировкой (см. Очереди). Контекстное меню (▾) на строке открывает карточку устройства.

Карточка: атрибуты, параметры типа оборудования, кнопки Редактировать, Удалить (мягкое) и Восстановить у удалённого. Кнопки тулбара очереди: добавление, Производители, Модели оборудования и Импорт оборудования — загрузка списка единиц из файла, доступна по праву /admin/plugin/inventoru/device:importForm.

Удалённые единицы показываются, если в конфигурации очереди заведён фильтр deleted и он включён.

Импорт оборудования из файла

Загрузка списка единиц из CSV — Импорт оборудования на экране оборудования, право /admin/plugin/inventoru/device:importForm. Идёт в два шага: предпросмотр (importPreview, ничего не пишет) показывает, что нашлось и что нет, и только importApply записывает — строки с ошибкой пропускаются, остальные применяются.

Ключ строки — серийный номер. Единица с таким серийником уже есть — обновляется, нет — создаётся. Поэтому повторная загрузка того же файла ничего не задваивает.

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

Что Принимаемые имена Как разбирается

Серийный номер (обязательна)

serial, serial_number, Серийный номер, Серийный №, Серийник, Серия

Ключ строки. Строка с пустым серийником пропускается — так не спотыкается хвост файла

Модель

model, Модель

Нет такой — создаётся

Производитель

manufacturer, Производитель, Вендор

Нет такого — создаётся

Тип оборудования

equip_type, Тип оборудования, Тип

Ищется по названию среди существующих; не найден — ошибка строки

Склад

store, Склад, Местонахождение

Ищется по названию; не найден — ошибка строки

Номенклатура

item, Номенклатура, Позиция

Ищется по названию; не найдена — ошибка строки

MAC-адрес

mac, mac_address, MAC-адрес, Мак

Как есть

Инвентарный номер

inv_no, asset_tag, Инвентарный номер, Инв. №, Инв.№

Как есть

Статус

status, Статус

Только planned, active, maintenance, decommissioned; иное — ошибка строки

Наименование

name, Наименование, Название

Как есть

Разница между «создаётся» и «ошибка строки» намеренная: каталог моделей и производителей импорт как раз и должен наполнять, а склад, тип оборудования и номенклатура — это учётные сущности со своими настройками, и молча заводить их по строке файла нельзя.

Разделитель и кодировка определяются по содержимому — тот же разбор, что у импорта остатков (см. sync1c): UTF-8 и windows-1251, разделитель ;, , или табуляция, BOM снимается.

Пример файла — example_devices.csv рядом с этим документом.

Статусы устройства

Planned

Готовится к вводу в эксплуатацию.

Active

В работе.

Maintenance

Временно выведено на обслуживание.

Decommissioned

Выведено из эксплуатации (требует указания причины).

Номенклатура

Список учитываемых позиций ТМЦ (кабель, патч-корды, SFP и т.д.) — очередь с настраиваемыми колонками, фильтрами и сортировкой (см. Очереди). У каждой позиции: наименование, характеристика, тип номенклатуры, внешний ID (для интеграции с 1С — хранится как text-параметр, id параметра задаётся в настройках инстанса 1С), штрихкоды (см. Штрихкоды и сканирование).

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

Тип номенклатуры — обязательная часть формы: именно он даёт позиции набор параметров и вид учёта (Вид учёта: расходники и «числится за складом»). Позиция без типа не имеет ни одного параметра — значение может быть записано в базе (например, код 1С, проставленный синхронизацией) и всё равно не показываться на карточке. Позициям, которые заводит импорт остатков, тип назначается настройкой коннектора import.itemTypeId — см. sync1c.

Вид учёта: расходники и «числится за складом»

У типа номенклатуры задаётся accounting.kind (ItemTypeProperties): consumable (по умолчанию) — расходники, резервируются и списываются по процессу; asset — инструмент/оборудование, выданное надолго. Asset-позиции не попадают в форму резервирования вкладки «Materials» — они перемещаются только через перемещение/возврат на карточке склада, и показываются там отдельным блоком «Held by warehouse».

Склады

Склад может быть:

  • General warehouse — без конкретного владельца

  • Личный склад (монтажника) — закреплён за конкретным сотрудником, обычно создаётся автоматически при первом импорте остатков с 1С (см. sync1c)

Карточка склада показывает остатки (расходники и asset-позиции раздельно), историю движений, формы перемещения/возврата, параметры склада и кнопки его администрирования: Редактировать, Удалить (мягкое, отменяется кнопкой Восстановить) и Доступ — доступ исполнителей и группа редактирования («кладовщик», см. Доступ к складам — монтажники и «кладовщики»). Там же, у каждой позиции остатков, задаётся минимальный остаток (inventoru_min_level) — порог подсветки «заканчивается»; отсутствие записи = порог не задан. Кнопки видны по правам: правка и удаление — пользовательские узлы склада, доступ и минимальный остаток — админские.

Мой склад (store:my)

Мобильный экран монтажника (Inventory → My warehouse): все склады, к которым у пользователя есть доступ по объединению из Доступ к складам — монтажники и «кладовщики» (владелец ∪ store_access ∪ группа редактирования), с остатками (количество/резерв) по каждому. Сверху — счётчик ожидающих поступлений из 1С по этим складам (ссылка на «Incoming», показывается только при ненулевом количестве) и кнопка Scan (см. Штрихкоды и сканирование): найденная позиция подсвечивается в списке остатков. Экран read-only — действия остаются на карточке склада и вкладке «Materials».

Потребность в материалах

Отдельного документа «заявка на материалы» в плагине нет и не планируется. Монтажник оформляет потребность обычным процессом BGERP — тип процесса заводится в оснастке, кодом плагина он не поддерживается. Кладовщик комплектует материалы и передаёт их монтажнику, а в ERP это попадает приходом и списанием из 1С штатной синхронизацией (см. документ по sync1c).

Резервировать материалы в момент подачи потребности нельзя: точка правды по остаткам — 1С, и резерв, созданный в ERP «под заявку», разъехался бы с ней при первой же выдаче мимо процесса. Там, где резерв под процесс действительно нужен, он делается явно на вкладке «Materials» карточки процесса (см. Вкладка процесса «Materials») — то есть по факту, а не по намерению.

Штрихкоды и сканирование

Таблица inventoru_item_barcode (barcode → item_id) наполняется импортом регистра штрихкодов из 1С по OData (см. sync1c); привязка «один штрихкод — одна позиция», повторный импорт перепривязывает.

Резолв кодаPOST /user/plugin/inventoru/movement.do?method=resolveCode&code=…​, порядок поиска:

  1. Штрихкод номенклатуры (inventoru_item_barcode) → type=item;

  2. GUID 1С — значение ext_id-параметра номенклатуры по всем включённым инстансам 1С → type=item;

  3. Серийный номер устройства → type=device, только если у пользователя есть доступ к складу устройства (то же объединение, что в Доступ к складам — монтажники и «кладовщики») — иначе type=none, как для несуществующего кода: без этой проверки скан произвольного серийника раскрывал бы, на каком складе лежит любое устройство компании.

Камера-скан ($$.inventoru.scan/scanResolve в pl.inventoru.js) — браузерный BarcodeDetector API поверх getUserMedia (нужен HTTPS или localhost; нативно — Chrome/Edge на Android). Если API или камера недоступны — fallback на ручной ввод кода через prompt, флоу не ломается. Кнопки скана есть в «Моём складе» и на вкладке «Materials».

Очереди

Списки складов, номенклатуры и оборудования строятся очередями — так же, как списки процессов. Колонки, фильтры и сортировка задаются в конфигурации очереди, пользователь сам выбирает, какие из настроенных фильтров показывать, и его выбор запоминается.

Движения ТМЦ

Полный append-only журнал операций по складу — движения никогда не изменяются и не удаляются, только добавляются.

Типы движений

Приход (receipt)

Поступление на склад (в админке — movement:receipt).

Списание (writeoff)

Расход/списание со склада.

Резерв (reserve)

Резервирование под процесс.

Снятие резерва (unreserve)

Отмена резерва.

Возврат (return)

Возврат — увеличивает остаток и одновременно уменьшает резерв (не использовать для «просто прихода», см. предостережение ниже).

Перемещение (transfer / transfer_in)

Между складами — связанная пара движений: transfer на складе-источнике, transfer_in на складе-назначении, у каждого в related_store_id другая сторона. Доступ проверяется только к складу-ИСТОЧНИКУ (отдать коллеге — не так чувствительно, как взять у него).

Корректировка (adjustment)

Ручная коррекция остатка (инвентаризация, movement:adjust в админке; также пишется при проведении поступлений из 1С).

return уменьшает reserved, а не только увеличивает quantity. Если нужно просто добавить остаток на склад без затрагивания чужих резервов на том же складе/номенклатуре (например, программный реверс списания) — использовать receipt, а не return. Пример — реверс writeoff в sync1c намеренно использует receipt по этой причине. Пользовательский «Returned to store» (movement:returnStock) по той же причине пишет return-семантику только на quantity — см. javadoc MovementAction.returnStock.

«Активный резерв» — reserve-движение, на которое не ссылается ни одно unreserve/writeoff через ref_movement_id. Запись остаётся в журнале навсегда, но перестаёт попадать в выборку «активных» после того, как на неё сослались отменой/списанием. При удалении процесса все его активные резервы автоматически снимаются (ProcessRemovedListener) — иначе reserved завис бы навсегда без UI-пути снять его.

Вкладка процесса «Materials»

Появляется на карточке процесса, если выполнены условия: плагин включён, inventoru:process.showTab=1 в конфигурации типа процесса, у пользователя есть право /user/plugin/inventoru/movement:processTab.

Показывает:

  • Форму резервирования (выбор склада — только из доступных пользователю, см. Доступ к складам — монтажники и «кладовщики»; выбор нескольких позиций номенклатуры и количества за один сабмит; в списке только расходники с доступным остатком > 0 — asset-позиции не резервируются, см. Вид учёта: расходники и «числится за складом»; скан-кнопка для подбора позиции по штрихкоду)

  • Историю по процессу: активные резервы («Reserved», с кнопкой отмены), списанные позиции («Written off», серым, без кнопки), возвращённые после реверса списания («Returned to store», зелёным)

Форма скрывается (read-only) и на клиенте (JSP), и на сервере (recordMultiple/record/unreserve в MovementAction отклоняют запрос с ошибкой), если:

  1. Текущий статус процесса не входит в inventoru:processType.<TYPE_ID>.reserveStatuses для его типа (см. В каких статусах можно резервировать материалы), или

  2. По процессу уже есть успешно отправленный в 1С экспорт списания — независимо от текущего статуса (откат статуса процесса назад не открывает форму сам по себе, см. sync1c).

Доступный остаток проверяется на сервере при каждом резерве, причём при мульти-сабмите остаток «вычитается» локально между строками — две строки одной позиции не пройдут проверку по одному и тому же устаревшему значению.

Отчёт «Остатки по номенклатуре»

Отчёт в плагине report (/user/plugin/report/plugin/inventoru/stock, класс StockOverviewReportAction): суммарные количество/резерв/доступно и число складов по каждой позиции по всем складам компании. Намеренно обходит per-store ACL — право на отчёт выдавать только ролям, которым положено видеть всё (руководство), не монтажникам.

Разработка

Структура файлов

src/org/bgerp/plugin/inventoru/
├── Plugin.java                    — регистрация плагина (ID = inventoru)
├── Config.java                    — inventoru:processType.<ID>.reserveStatuses и др.
├── db.sql                         — схема БД + миграция inventoru_* → inventoru_*
├── l10n.xml                       — локализация (ru/en)
├── action.xml                     — права доступа
├── action/
│   ├── DeviceAction               — /user/plugin/inventoru/device: очередь, карточка, правка,
│   │                                удаление, восстановление, справочники производителей/моделей
│   ├── DeviceAdminAction          — /admin/plugin/inventoru/device: только загрузка из файла
│   ├── ItemAction                 — /user/plugin/inventoru/item (админского экрана нет)
│   ├── StoreAction                — /user/plugin/inventoru/store (+ my)
│   ├── StoreAdminAction           — /admin/plugin/inventoru/store: доступ исполнителей, группа
│   │                                редактирования, минимальный остаток
│   ├── StoreStatusAction          — /admin/plugin/inventoru/storeStatus: каталог статусов
│   ├── MovementAction             — /user/plugin/inventoru/movement
│   │                                (processTab, record, recordMultiple, unreserve, transfer,
│   │                                returnStock, resolveCode)
│   ├── MovementAdminAction        — /admin/plugin/inventoru/movement (просмотр, receipt, adjust)
│   ├── StockOverviewReportAction  — отчёт остатков (плагин report)
│   ├── ManufacturerAdminAction, DeviceTypeAdminAction — справочники
│   ├── QueueAdminAction           — очереди всех сущностей (сущность выбирается при создании)
│   ├── QueueScreenAction          — общий экран очереди, от него наследуются Store/Item/Device
│   └── StoreTypeAction, ItemTypeAction, EquipTypeAction, ManufacturerTypeAction,
│       ModelTypeAction            — иерархии типов + свойства
├── dao/                           — DAO на каждую модель + BarcodeDAO, MinLevelDAO,
│                                    ChangeLogDAO, queue/QueueDAO
├── model/                         — Device/Item/Store/Movement/StoreAccess + *Type + *TypeProperties + queue/*
├── cache/                         — StoreTypeCache (typeMap+statusMap+statusList), ItemTypeCache,
│                                    EquipTypeCache, QueueCache
├── event/                         — ProcessRemovedListener (снятие резервов удалённого процесса)
└── sync1c/                        — интеграция с 1С, см. отдельный документ

webapps/WEB-INF/jspf/user/plugin/inventoru/
├── menu_items.jsp, process_tabs.jsp — точки расширения, регистрируются в Plugin.endpoints()
├── queue/                         — list, filter, show, back_button — общий экран очереди
│                                    для всех сущностей (см. QueueScreenAction)
├── device/                        — card, edit, toolbar + type_list, manufacturer_list
├── item/                          — card, edit, toolbar
├── movement/                      — list, process_tab (вкладка «Materials» на процессе)
├── store/                         — card, edit, toolbar, my («My warehouse»)
├── sync1c/import_pending.jsp      — «Incoming» (sync1c, но лежит в основном плагине)
└── report/stock_overview.jsp      — отчёт «Остатки по всем складам»

webapps/WEB-INF/jspf/admin/plugin/inventoru/ — админки: типы, справочники, очереди,
                                                 доступ к складу, импорт оборудования
webapps/js/pl.inventoru.js — камера-скан ($$.inventoru.scan/scanResolve)

Таблицы БД

Таблица Назначение

inventoru_manufacturer

Производители устройств

inventoru_device_type

Модели устройств (шаблон)

inventoru_device

Экземпляры устройств

inventoru_device_file

Файлы/фото устройств (бинарные данные — в ядровом file_data)

inventoru_item

Номенклатура ТМЦ

inventoru_item_barcode

Штрихкоды номенклатуры (barcode → item_id), см. Штрихкоды и сканирование

inventoru_item_type, inventoru_store_type, inventoru_equip_type

Иерархии типов (id, title, parent_id, use_parent_props, data, config)

inventoru_store_status

Глобальный каталог статусов складов (см. В каких статусах можно резервировать материалы про семантику per-type)

inventoru_store

Склады; edit_group_id — группа, которой разрешено редактировать склад в дополнение к владельцу/store_access (см. Доступ к складам — монтажники и «кладовщики»)

inventoru_store_access

Точечный доступ пользователя к складу (store_id, user_id, date_from, date_to)

inventoru_manufacturer_type, inventoru_model_type

Иерархии типов производителей и моделей

inventoru_queue

Очереди всех сущностей плагина; колонка entity говорит, что очередь показывает (аналог очередей процессов)

inventoru_queue_user, inventoru_queue_group

Кому выдана очередь; эффективный набор пользователя — объединение личных привязок и привязок его групп (см. Очереди)

inventoru_balance

Текущие остатки: quantity (всего) и reserved; доступно = quantity − reserved

inventoru_movement

Append-only журнал движений — записи никогда не обновляются и не удаляются; related_store_id — вторая сторона для transfer/transfer_in

inventoru_min_level

Минимальные остатки (порог подсветки «заканчивается»)

inventoru_change_log

Аудит-лог изменений (pre/post JSON snapshots)

Soft-delete (deleted_at/deleted_by) — у Device/Item/Store.

Паттерн иерархии типов

StoreType/ItemType/EquipTypeTreeItem, свойства (param.ids, для складов ещё status.ids, для номенклатуры — accounting.kind) сериализуются в поле data в формате key=value построчно (Preferences/Utils.toIntegerList), парсятся в *TypeProperties. Редактор «Properties» типа берёт список доступных для выбора параметров через ParamDAO.getParameterList(<Model>.OBJECT_TYPE, 0) — обязательно использовать OBJECT_TYPE соответствующей модели (Store/Item/Device), не Process.OBJECT_TYPE.

getChildCount() считается subquery в DAO (наивная загрузка без детей всегда даёт 0).

Наследование свойств (use_parent_props=1)type.getProperties() возвращает СОБСТВЕННЫЕ свойства типа, пустые, если тип помечен наследовать от родителя. Разрешать наследование — <TypeName>Cache.getEffectiveProperties(typeId) (по одному статическому методу в каждом из StoreTypeCache/ItemTypeCache/EquipTypeCache): поднимается по parentId через typeMap, пока не найдёт предка с useParentProperties=false, или пока не упрётся в корень. Все реальные потребители параметров типа (показ параметров на карточке устройства/номенклатуры/склада, фильтр asset-позиций) обязаны звать getEffectiveProperties, а не type.getProperties() напрямую — иначе у типа с use_parent_props=1 список параметров молча окажется пустым.

Параметры item/store/device — как завести

Параметры сущностей плагина заводятся штатно, через Администрирование → Параметры: экран DirectoryAction берёт справочники ядра и добавляет по разделу на каждую сущность, объявленную включённым плагином в Plugin.getObjectTypes(). Инвентарь объявляет пять — склад, номенклатура, оборудование, производитель, модель оборудования, — и заголовки разделов приходят из его же локализатора. Редактор тот же, что у параметров процессов, ParameterCache сбрасывается самим действием.

До версии от 05.09.2026 точки расширения не было, и параметр этих сущностей заводился только прямым INSERT в param_pref со сбросом кэша вручную. Если разделов сущностей плагина в Администрирование → Параметры нет — сборка старше этого изменения, обновитесь; вставлять записи в param_pref руками больше не нужно и не следует.

Привязка параметра к типу — в редакторе свойств типа (тот же механизм для всех иерархий):

POST /admin/plugin/inventoru/itemType.do?method=propertiesUpdate&id=1&param=301
POST /admin/plugin/inventoru/storeType.do?method=propertiesUpdate&id=1&param=302
POST /admin/plugin/inventoru/equipType.do?method=propertiesUpdate&id=1&param=303

Множественный параметр param — можно несколько раз в query для нескольких id. После этого inventoru_item_type.data содержит param.ids=301, и карточка позиции этого типа начинает показывать поле. Позиция без типа не показывает ни одного параметра, даже если значение записано в базе.

Отладка

grep "MovementAction\|MovementDAO" log/bgerp.log
grep "StoreTypeAction\|ItemTypeAction\|EquipTypeAction" log/bgerp.log
-- Баланс по складу
SELECT i.title, b.quantity, b.reserved FROM inventoru_balance b
JOIN inventoru_item i ON i.id=b.item_id WHERE b.store_id=?;

-- История движений процесса (включая списанные/возвращённые)
SELECT id, type, item_id, quantity, ref_movement_id, ext_ref, dt
FROM inventoru_movement WHERE process_id=? ORDER BY id;

-- Доступ монтажника к складам на сегодня
SELECT store_id FROM inventoru_store_access
WHERE user_id=? AND date_from<=CURDATE() AND (date_to IS NULL OR date_to>=CURDATE());

-- Штрихкод → позиция
SELECT b.barcode, i.title FROM inventoru_item_barcode b
JOIN inventoru_item i ON i.id=b.item_id WHERE b.barcode=?;