О плагине

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

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

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

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

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

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

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

Настройка

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

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

inventory:enable=1

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

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

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

Устройства

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

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

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

Склады

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

Движения ТМЦ

Запись движения (одна/несколько), снятие резерва, вкладка процесса «Материалы»

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

Полный CRUD в админке (/admin/plugin/inventory/*)

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

CRUD иерархии, свойства типа (параметры), для складов — ещё и статусы

Доступ исполнителей (у складов)

Список/добавление/удаление/закрытие периода доступа монтажников к складу

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

Управление и просмотр очередей складов (аналог очередей процессов)

Поступления с 1С (пользователь)

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

Вкладка «Материалы» на процессе

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

inventory:process.showTab=1

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

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

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

inventory:processType.<typeId>.reserveStatuses=<id1>,<id2>,...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Плагин добавляет группу Инвентарь в главное меню.

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

Список оборудования: модель, IP, серийный номер, статус.

Контекстное меню (▾) на строке:

  • Просмотр — карточка устройства

  • Редактировать — изменить атрибуты

  • Удалить — soft-delete

Флаг Показать удалённые — показывает удалённые устройства (серым), с возможностью восстановить через контекстное меню.

Кнопки тулбара: Производители, Типы устройств.

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

Planned

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

Active

В работе.

Maintenance

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

Decommissioned

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

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

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

Склады

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

  • Общий склад — без конкретного владельца

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

Карточка склада показывает историю движений и (в админке) доступ исполнителей + группу редактирования («кладовщик», см. Доступ к складам — монтажники и «кладовщики»).

Очереди складов

Аналог очередей процессов (SQL_CALC_FOUND_ROWS + Pageable, настраиваемые колонки и фильтры) — списки складов с гибкой конфигурацией отображаемых полей.

Движения ТМЦ

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

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

Приход (receipt)

Поступление на склад.

Списание (writeoff)

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

Резерв (reserve)

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

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

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

Возврат (return)

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

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

Между складами.

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

Ручная коррекция остатка.

return уменьшает reserved, а не только увеличивает quantity. Если нужно просто добавить остаток на склад без затрагивания чужих резервов на том же складе/номенклатуре (например, программный реверс списания) — использовать receipt, а не return. Пример — реверс writeoff в sync1c намеренно использует receipt по этой причине.

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

Вкладка процесса «Материалы»

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

Показывает:

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

  • Историю по процессу: активные резервы («В резерве», с кнопкой отмены), списанные позиции («Списано», серым, без кнопки), возвращённые после реверса писания («Возврат на склад», зелёным)

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

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

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

Разработка

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

src/org/bgerp/plugin/inventory/
├── Plugin.java                    — регистрация плагина
├── Config.java                    — inventory:process.showTab, inventory:processType.<id>.reserveStatuses
├── db.sql                         — схема БД
├── l10n.xml                       — локализация (ru/en)
├── action.xml                     — права доступа
├── action/
│   ├── DeviceAction(AdminAction)  — /user|admin/plugin/inventory/device
│   ├── ItemAction(AdminAction)    — /user|admin/plugin/inventory/item
│   ├── StoreAction(AdminAction)   — /user|admin/plugin/inventory/store
│   ├── MovementAction             — /user/plugin/inventory/movement (processTab, record, recordMultiple, unreserve)
│   ├── MovementAdminAction        — /admin/plugin/inventory/movement (просмотр по складу)
│   ├── ManufacturerAdminAction, StoreQueueAdminAction
│   └── StoreTypeAction, ItemTypeAction, EquipTypeAction — иерархии типов + свойства
├── dao/                           — DAO на каждую модель + queue/StoreQueueDAO
├── model/                         — Device/Item/Store/Movement/StoreAccess + *Type + *TypeProperties + queue/*
├── cache/                         — StoreTypeCache (typeMap+statusMap+statusList), ItemTypeCache, EquipTypeCache, StoreQueueCache
└── sync1c/                        — интеграция с 1С, см. отдельный документ

webapps/WEB-INF/jspf/user/plugin/inventory/
├── menu_items.jsp, process_tabs.jsp
├── device_*.jsp, item_*.jsp, store_*.jsp
├── movement_list.jsp, movement_process_tab.jsp
└── import_pending.jsp             — «Поступления» (sync1c, но лежит в основном плагине)

webapps/WEB-INF/jspf/admin/plugin/inventory/ — админки: списки/формы/типы/очереди/доступ

Таблицы БД

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

inventory_manufacturer

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

inventory_device_type

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

inventory_device

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

inventory_item

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

inventory_item_type, inventory_store_type, inventory_equip_type

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

inventory_store_status

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

inventory_store

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

inventory_store_access

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

inventory_store_queue

Очереди складов (аналог очередей процессов)

inventory_balance

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

inventory_movement

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

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

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

StoreType/ItemType/EquipTypeTreeItem, свойства (param.ids, для складов ещё status.ids) сериализуются в поле data в формате key=value построчно (Preferences/Utils.toIntegerList), парсятся в *TypeProperties. Редактор «Свойства» типа берёт список доступных для выбора параметров через 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, или пока не упрётся в корень. Все реальные потребители параметров типа (DeviceAction/DeviceAdminAction/ItemAdminAction/StoreAction, показ параметров на карточке устройства/номенклатуры/склада) обязаны звать getEffectiveProperties, а не type.getProperties() напрямую — иначе у типа с use_parent_props=1 список параметров молча окажется пустым.

Параметры item/store/device — как завести, рабочий пример

Готча: общий экран Администрирование → Параметры (/admin/directory, DirectoryAction) работает только с фиксированным списком object type — Process/User/Customer/AddressCity/ AddressStreet/AddressHouse (см. DIRECTORY_LIST в DirectoryAction.java), захардкоженным в классе, БЕЗ точки расширения для плагинов. Для Store.OBJECT_TYPE/Item.OBJECT_TYPE/ Device.OBJECT_TYPE (инвентарный плагин) создать параметр через эту админку нельзя — своего пункта меню/directoryId у инвентаря нет. Единственный способ завести параметр для этих трёх типов сегодня — прямой INSERT в param_pref (та же таблица, тот же формат, что использует ParamDAO.updateParameter() изнутри, см. src/org/bgerp/dao/param/ParamDAO.java:168):

INSERT INTO param_pref (object, type, title, `order`, config, comment) VALUES
('item',   'money', 'Длина, м',       10, '', 'Длина кабеля/провода в метрах'),
('store',  'text',  'Адрес объекта',  10, '', 'Физический адрес склада/объекта монтажа'),
('device', 'money', 'Кол-во портов',  10, '', 'Количество портов на устройстве');

После INSERT обязательно сбросить ParameterCache (или перезапустить сервер) — иначе ParamValueDAO.loadParameters() упадёт с NullPointerException (не найдёт свежедобавленный param_id в закэшированном списке), при первом же открытии карточки объекта с этим параметром.

Дальше — привязка параметра к типу делается уже штатным, работающим UI/API (редактор «Свойства» типа, тот же самый механизм для всех трёх иерархий): POST propertiesUpdate?id=<typeId>&param=<paramId> (множественный параметр param — можно несколько раз в query для нескольких id). Например, для демо-иерархии в этом же репозитории (inventory_item_type/inventory_store_type/inventory_equip_type, id=1 у каждой — корневые демо-типы "Типы номенклатуры/складов/оборудования 1"):

POST /admin/plugin/inventory/itemType.do?method=propertiesUpdate&id=1&param=301   -- "Длина, м"
POST /admin/plugin/inventory/storeType.do?method=propertiesUpdate&id=1&param=302  -- "Адрес объекта"
POST /admin/plugin/inventory/equipType.do?method=propertiesUpdate&id=1&param=303  -- "Кол-во портов"

После этого inventory_item_type.data содержит param.ids=301 и т.д. — карточка номенклатуры типа "Типы номенклатуры 1" начинает показывать поле "Длина, м" (аналогично для склада/устройства). Реализовано и проверено на этом самом repo — параметры 301/302/303 и привязки уже стоят.

Отладка

grep "MovementAction\|MovementDAO" log/bgerp.log
grep "StoreTypeAction\|ItemTypeAction\|EquipTypeAction" log/bgerp.log
-- Баланс по складу
SELECT i.title, b.quantity, b.reserved FROM inventory_balance b
JOIN inventory_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 inventory_movement WHERE process_id=? ORDER BY id;

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