О плагине

Плагин учёта сетевого оборудования и складских остатков ТМЦ (материалов), 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:{null,queueFilter,queueShow}), приход (movement:receipt), корректировка/инвентаризация (movement:adjust), списание единицы со склада по серийному номеру (movement:writeoffUnit, см. Сохранение устройства и остаток)

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

Заведение, правка, удаление и восстановление — на карточке записи (/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: устройство без наименования ничем не идентифицируется. Модель раньше тоже была обязательной, но у складской техники модель — это её позиция номенклатуры: устройство, заведённое приходом или загруженное с номенклатурой, модели не имеет, и его карточку нельзя было сохранить. Пустое значение отключает проверку.

Допустимые имена (как у полей формы): 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.

Доступ к складам — роли

Права в наборе прав (Administration → Permission Sets) отвечают на вопрос «ЧТО роль может делать» — править склад, удалять оборудование, делать приход. На какие именно склады это распространяется, решает плагин сам, по связи пользователя со складом. Связей две, и они дают разное:

Роль Как задаётся Что даёт

Монтажник — работает со складом

владелец склада (store.user_id, личный склад-«рюкзак») или выданный доступ (inventoru_store_access: сотруднику или группе, на период; date_to IS NULL — бессрочно)

видит склад, его остатки и оборудование; резервирует, возвращает, перемещает материалы во вкладке «Materials» процесса; проводит «Ожидающие поступления» из 1С по этому складу. Склад, его параметры и оборудование не правит, даже если в наборе прав есть узлы правки.

Кладовщик — управляет складом

участник «группы редактирования» склада (store.edit_group_id, одна группа на склад)

всё, что монтажник, и то, что разрешают его права: правка и удаление карточки склада, его параметры и минимальные остатки, оборудование на нём (в том числе массовый импорт), загрузка CSV остатков, приход и корректировка.

Администратор складов

право /admin/plugin/inventoru/store:accessList (Administration → Warehouses → Executor access → List)

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

Обе меры считаются в одном месте — StoreAccessService: storeIds() / allowed() для узлов, меняющих склад (умолчание — управляет: группа редактирования), workStoreIds() для работы с материалами (умолчание — работает: владелец ∪ доступ лично ∪ доступ на группу ∪ группа редактирования); обе читают опции своего узла, см. Опции узлов прав — где действует узел. Видимость складов в списках — accessibleStoreIds(). Своих копий у экранов нет намеренно.

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

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

Независимо от ролей удаление отказывает, пока объекту есть что терять:

  • склад — пока на нём есть остаток, резерв, оборудование или не выгруженные в 1С списания;

  • позиция номенклатуры — пока по ней есть остаток, резерв или оборудование;

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

Опции узлов прав — где действует узел

Как у процессов в ядре (onlyPermittedTypes, allowOnlyGroups), у узлов плагина есть опции, которые задаются в редакторе набора прав (Administration → Permission Sets, текст под узлом описывает их там же). Умолчания — роли выше; опциями их расширяют или сужают для конкретного набора прав без правки кода.

Опция Значение

stores

На каких складах действует узел: run — склады групп редактирования пользователя, work — ещё и те, что ему принадлежат или выданы (лично или его группе), all — все склады. Умолчание: run у узлов, меняющих склад (карточка, параметры, мин. остаток, оборудование и его импорт, загрузка CSV, приход, корректировка, списание единицы, «Ожидающие» в админке), work — у узлов работы с материалами (резерв, снятие резерва, перемещение, возврат, скан, «Ожидающие» монтажника). У узла Warehouses → Create stores=all разрешает заводить склады не только администратору.

storeTypeIds

Через запятую — типы складов, которыми узел ограничен; пусто — любые.

returnLimit

Только у Return to warehouse. issued (умолчание) — вернуть можно не больше выданного пользователю ранее: списанного по его резервам с этого склада за вычетом отменённых списаний и уже возвращённого им; none — любое количество (например, кладовщику на приёмке).

Администратор складов (Executor access → List) опциями не ограничивается: у узлов, меняющих склад, для него — все склады. В работе с материалами — наоборот: во вкладке «Materials» процесса и у возврата он, как и все, видит свои склады, пока у узла нет stores=all.

Пример: кладовщик филиала правит склады только своего типа — /user/plugin/inventoru/store:update с опциями stores=all и storeTypeIds=3.

Как завести кладовщика

  1. Administration → Permission Sets — набор прав кладовщика: /user/plugin/inventoru/store:{null,show,my,queueFilter,queueShow,edit,update,delete,restore}, /user/plugin/inventoru/device:{null,show,queueFilter,queueShow,edit,update,delete,restore}, /user/plugin/inventoru/import_pending:{null,apply,applyAll,reject}, /admin/plugin/inventoru/store:minLevelUpdate, /admin/plugin/inventoru/movement:{null,queueFilter,queueShow,receipt,adjust,writeoffUnit}; если нужна загрузка CSV — /admin/plugin/inventoru/sync1c/instance:{importFileForm, importFilePreview,importFileConfirm,importFileAdd} (эти права независимы от остальной админки sync1c, см. sync1c); если он заполняет параметры складов — /user/plugin/inventoru/storeParam:update (см. Право на изменение параметров склада). Все эти узлы действуют только на склады его группы редактирования. НЕ включать /admin/plugin/inventoru/store:accessList и /admin/plugin/inventoru/sync1c/instance:null — они превращают пользователя в администратора.

  2. Administration → Users → Groups — группа, в неё нужные сотрудники; набор прав из шага 1 привязать к группе.

  3. Инвентарь → Склады — карточка склада, кнопка Доступ, комбо Группа редактирования — выбрать группу. Состав группы можно менять: принятый сотрудник получает склад сразу, исключённый теряет.

Выданный доступ на том же экране (сотруднику или группе, на период, закрывается кнопкой «Закрыть период» без удаления истории) — это доступ монтажника: работать со складом, а не управлять им. Годится для бригады на сезон, подмены на время отпуска.

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

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

Значения параметров пишет ядровой экшен /user/parameter:parameterUpdate — один на все типы объектов сразу. Право Parameters → Edit в наборе прав выдаётся, как правило, ради параметров процессов, и вместе с ними молча отдаёт параметры складов: монтажник, заполняющий свои процессы, мог переписать адрес, категорию и код 1С любого склада, к которому у него есть доступ.

Поэтому у плагина есть свой узел: Plugin Inventory → User → Warehouses → Edit parameters (/user/plugin/inventoru/storeParam:update). Он ничего не диспетчеризует — экшена storeParam не существует, — право проверяет сам плагин в двух местах:

  • StoreAction.show — без права карточка отдаёт параметры только на чтение (readOnly для parameter_list.jsp): ни форм правки, ни карандашей;

  • StoreParamAccessListener — без права запись отклоняется с сообщением, даже если запрос собран руками в обход экрана.

Склад — отдельный вопрос и проверяется сверх этого: право даёт возможность править параметры складов вообще, а каких именно — те, которыми пользователь управляет (Доступ к складам — роли).

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

Очереди

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

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

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

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

Значения column.<ID>.value зависят от сущности очереди. Это логические имена, а не имена колонок БД: заголовок записи всегда title, на какую колонку он ложится — дело сущности (у оборудования это device.name, у модели device_type.model, у движения movement.comment). Незнакомое значение колонкой не становится — она пропускается, а в log/bgerp.log пишется Unknown column value '<что было>' for entity '<сущность>', available: […​] со списком допустимых. Экран при этом открывается — просто без этой колонки, поэтому пропажу видно только в логе.

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

Склады

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, sw_version, 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, related_store_id (вторая сторона перемещения, у остальных движений пусто), user_id, process_id (номер процесса ссылкой), process_title (его название — отдельной колонкой рядом, колонка отдаёт одно значение), serial_number

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

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

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

Фильтры по ключу и по тексту заводятся у той сущности, у которой есть соответствующее поле, — список полей у каждой сущности в таблице колонок очереди:

Фильтр Что отбирает

item

Позиция номенклатуры — прежде всего в журнале движений: «что происходило с этой позицией»

manufacturer

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

serialNumber

Поиск подстрокой по серийному номеру: у оборудования и у движений ТМЦ (движение помнит серийник единицы, которую двигали)

ip

Поиск подстрокой по адресу оборудования

country

Поиск подстрокой по стране производителя

macAddress

Поиск подстрокой по MAC-адресу оборудования

assetTag

Поиск подстрокой по инвентарному номеру оборудования

Фильтр, для которого у сущности нет поля, в очередь не попадает — в лог уходит строка «Entity '…' has no field '…', filter '…' skipped».

Фильтр «владелец» на складах первой строкой предлагает Общий склад — так отбираются склады без владельца (user_id = 0), это обычный случай, а не край.

Фильтры по типу отбирают вместе с подтипами: выбранное «Оборудование» находит и позиции, заведённые под его дочерними типами. Иерархия читается в момент применения фильтра, поэтому подтип, добавленный после настройки очереди, сразу считается своим. Тип, которого у сущности нет (itemType в очереди складов), в очередь не попадает — в лог уходит строка «Filter 'itemType' is only available for nomenclature».

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

Имена легко спутать: type — это операция в очереди движений ТМЦ, а тип сущности называется itemType, storeType, equipType. Неизвестное имя фильтра экран не роняет — фильтр молча пропускается, в лог уходит «Unknown store queue filter type: …».

Фильтр-диапазон по дате называется именем своего поля и читает <поле>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>,…​

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

inventoru:device.requiredFields=…​

Обязательные поля карточки оборудования (см. Обязательные поля устройства)

inventoru:device.mainStoreId=<STORE_ID>

Главный склад: оборудование, выведенное из эксплуатации, возвращается сюда (см. Сохранение устройства и остаток)

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 записывает — строки с ошибкой пропускаются, остальные применяются.

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

Два применения одновременно. Кнопка Применить блокируется на время запроса, но два запроса могут прийти и иначе — из двух вкладок или от человека одновременно с загрузкой остатков из 1С. Единица читается под блокировкой БД, поэтому «такой ещё нет, создаём» не может решиться дважды: один запрос проходит, второй либо обновляет уже созданную карточку, либо отклоняется базой, и тогда экран говорит «Импорт этого файла уже выполняется — подождите несколько секунд и посмотрите список оборудования, прежде чем применять снова». Ничего из отклонённого запроса в базе не остаётся. Двойной клик по Применить однажды сделал 158 карточек на 79 серийников — вот от чего защита.

Номенклатура и остатки. Если у строки указана номенклатура с поштучным учётом и склад, строка проводится через тот же учёт, что и карточка (см. Сохранение устройства и остаток): единица привязывается к непокрытому остатку склада — остатку, который пришёл раньше серийных номеров (например, из 1С), — а если его нет, приходуется; перенос числящейся единицы на другой склад — перемещение. Предпросмотр показывает под каждой строкой, что она сделает с остатком, или почему строка не будет применена. При применении строки заново разбираются на сервере: если за время между предпросмотром и применением что-то изменилось, строка с ошибкой пропускается и после применения показывается вместе с причиной, а отказ при записи отменяет всю загрузку и называет строку.

Склады, которые ведёт 1С, файл не расставляет: на складе, сопоставленном с включённым коннектором hs, серийные номера расставляет импорт остатков 1С. Строка с таким складом дополняет устройство — номенклатура, MAC, модель, инвентарный номер, статус, — а склад ему поставит 1С; предпросмотр так и пишет.

Колонка «Номенклатура» необязательна, но без неё единица остаётся вне складского учёта: она видна в разделе «Оборудование» и в блоке оборудования на карточке склада, но не входит в остатки, не резервируется под процесс и не списывается при монтаже. Если оборудование должно выдаваться абоненту, номенклатуру в файле заполняют, а у её типа включают поштучный учёт.

Как понимать колонку «Номенклатура» выбирается на форме загрузки: по названию позиции или по её коду в 1С — значению параметра внешнего кода конкретного коннектора. Название может совпадать у нескольких позиций или отличаться от 1С-ного на пробел; код — нет. Название, которое носят несколько позиций, — ошибка строки, а не «последняя из одноимённых».

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

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

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

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

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

Модель

model, Модель

Нет такой — создаётся; если у строки указана номенклатура — только связывается с существующей: модель складской техники — её позиция, заводить по тексту копию не нужно

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

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

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

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

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

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

Склад

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

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

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

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

По названию или по коду 1С — как выбрано на форме; не найдена или неоднозначна — ошибка строки

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

Выведено из эксплуатации (требует указания причины). Устройство возвращается на главный склад (inventoru:device.mainStoreId) и лежит там — см. Сохранение устройства и остаток.

Сохранение устройства и остаток

Если номенклатура устройства ведётся поштучно, склад и номенклатура на карточке — это учёт, а не просто поля. Сохранение карточки (а также импорт оборудования и импорт остатков из 1С) проходит через DeviceAccountingService, который решает, какое движение нужно, или отказывает — до какой-либо записи:

Что меняется Что происходит

Устройство впервые встаёт на склад, у позиции там есть непокрытый остаток

Привязка без движения: остаток уже учитывал эту единицу, не хватало её серийного номера

Устройство впервые встаёт на склад, непокрытого остатка нет

Приход на склад

Устройство, числящееся на складе, переносится на другой склад

Перемещение; отказ, если единица зарезервирована в процессе

Числящемуся устройству меняют номенклатуру или серийный номер

Отказ: номенклатуру меняют списанием и новым приходом, серийный номер назван в движениях, резервах и списании в 1С

Числящееся устройство снимают со склада или удаляют

Отказ: со склада единица уходит только списанием — кнопкой Списать со склада на её карточке

Удалённое устройство правят

Отказ: сначала восстановить. Восстановленное встаёт на свой склад — на непокрытый остаток, если он есть, иначе приходом

Серийный номер уже у другого живого устройства

Отказ

Ошибочно заведённая единица. На карточке устройства есть кнопка Списать со склада (право /admin/plugin/inventoru/movement:writeoffUnit, опции stores/storeTypeIds — как у прихода и корректировки). Она спрашивает причину и пишет списание этой единицы по её серийному номеру: остаток позиции уменьшается на единицу, устройство уходит со склада, после чего карточка удаляется как любая другая. Это единственный путь убрать единицу, заведённую по ошибке: удаление карточки держится за склад, склад на карточке нельзя очистить, перенос на другой склад ничего не меняет, а корректировка остатка не опускает его ниже числа стоящих на складе единиц.

Кнопка показывается только там, где списание и есть выход: у позиции с поштучным учётом, стоящей на складе. Отказы:

  • единица под активным резервом — сначала снять резерв;

  • позиция не ведётся поштучно — её остаток правится корректировкой, а карточка удаляется как есть;

  • склад ведёт 1С — списание проводится в 1С, следующий импорт остатков уберёт единицу здесь; иначе списание отменил бы сам импорт, увидев единицу на складе у 1С.

Склады, которые ведёт 1С. Склад, сопоставленный с включённым коннектором 1С по протоколу hs, получает свою расстановку серийных номеров из 1С — через импорт остатков. На таком складе ERP сама не ставит единицу и не переносит её: следующий импорт 1С вернул бы всё как было. Карточка и импорт оборудования на таком складе отказывают с объяснением; меняются только остальные поля.

Вывод из эксплуатации. При переводе в статус «Выведено из эксплуатации» устройство переезжает на главный склад (inventoru:device.mainStoreId) — перемещением, если оно числится. Если оно стоит на складе, который ведёт 1С, статус ставится, а переезд делает 1С: карточка подсказывает вернуть устройство на главный склад документом 1С, импорт остатков его переместит. Без настроенного главного склада устройство остаётся, где стояло. Возврат на главный склад — правило, а не выбор того, кто ставит статус: монтажник, у которого нет доступа к главному складу, выводит устройство из эксплуатации наравне с остальными, и после сохранения оно уходит из его доступа.

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

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

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

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

Единица измерения позиции — та, в которой её ведёт 1С («шт», «м»…); приходит из 1С при импорте или задаётся на карточке. Позиция в штуках не принимает дробного количества ни в одном движении; строки импорта в другой единице не складываются с позицией — см. sync1c.

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

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

Поштучный учёт (серийные номера)

У типа номенклатуры задаётся serial.tracked=1 (наследуется дочерними типами). Позиция такого типа ведётся поштучно: каждая единица — отдельная запись оборудования с серийным номером и ссылкой на эту позицию. Номенклатура отвечает на вопрос «что и сколько», оборудование — «какая именно единица»; так же это устроено в 1С, где у одной карточки номенклатуры много серий.

Единица числится на складе, если её позиция ведётся поштучно, у неё указан склад и она не удалена. Правила, которые держит сам журнал движений (MovementDAO.append), кто бы движение ни создавал:

  • Движение поштучной позиции проводится по одной единице и называет её серийный номер.

  • Забрать единицу со склада (списание, перемещение, корректировка в минус) можно, только если она числится на этом складе и по этой позиции. Приход, возврат и корректировка в плюс — только если единица не числится нигде: неизвестна, стоит вне склада или под позицией без поштучного учёта. Раньше устройство искалось только по серийному номеру, и списание на одном складе единицы, стоящей на другом, молча снимало её с того склада.

  • Серийный номер, заведённый у нескольких устройств сразу, движение не проводит — какую из единиц двигать, система не угадывает.

  • Остаток может опережать единицы, но не наоборот. Устройств на складе не больше остатка позиции. Разница — непокрытый остаток: количество, пришедшее раньше серийных номеров (позиция с остатком, переведённая на поштучный учёт). Серийные номера дозаполняются, и непокрытый остаток уменьшается. Устройств больше остатка — ошибка: единица посчитана дважды или взята без движения.

  • Корректировка без серийного номера меняет только непокрытый остаток — ниже числа устройств на складе её не провести; лишние единицы убираются списанием по серийным номерам.

Включение поштучного учёта и сверка

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

  • на складе устройств больше, чем остаток, — единица посчитана дважды;

  • есть активный резерв без серийного номера — выгрузка в 1С списывает ровно ту единицу, которую назвал резерв, а такой резерв не называет ни одной, и списание процесса бы не прошло.

Отказ перечисляет такие строки. Остаток без единиц включению не мешает — это переходное состояние, серийные номера дозаполняются.

Посмотреть то же до попытки — кнопка Сверка остатков с единицами в свойствах типа номенклатуры (право /admin/plugin/inventoru/itemType:serialCheck): по позициям типа и его подтипов — склад, остаток, единиц, сколько без серийных номеров, лишних единиц и резервов без серийного номера; строки, которые не дают включить учёт, выделены.

Импорт остатков 1С, назначающий позиции тип по настройке коннектора (import.itemTypeId), такой тип не назначает, если позиция не проходит ту же проверку, — пишет строку в лог.

Порядок для клиента с 1С: остатки из 1С количеством → включить поштучный учёт на типе ONU → импорт 1С с серийными номерами расставляет единицы по складам → список оборудования дополняет их (MAC, модель, инвентарный номер) → сверка: без серийных номеров не осталось ничего.

Склады

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

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

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

Владелец задаётся на форме склада полем Личный склад сотрудника — выбором сотрудника из списка, первой строкой которого идёт Общий склад (она же снимает владельца). Владение само по себе даёт человеку рабочий доступ к складу: отдельная запись inventoru_store_access для этого не нужна (см. Доступ к складам — роли). Массового назначения владельцев нет — склад заводится и правится по одному; при переносе из внешней системы владельцев проставляют скриптом по inventoru_store.user_id.

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

Блок Оборудование на складе перечисляет единицы, стоящие на складе (inventoru_device.store_id): наименование со ссылкой на карточку устройства, серийный номер, MAC, позицию номенклатуры и статус. Единица, не связанная с номенклатурой, помечена «Без номенклатуры» — такая единица вне складского учёта: она не входит в остатки, не резервируется и не списывается при монтаже (см. Поштучный учёт (серийные номера)). Блок не зависит от поштучного учёта: оборудование, загруженное импортом без колонки номенклатуры, видно здесь сразу. Надпись «Нет остатков номенклатуры» относится только к таблице остатков — склад с оборудованием, но без остатков, пустым не считается.

Мой склад (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 журнал операций по складу — движения никогда не изменяются и не удаляются, только добавляются. В таблице движений на карточке склада есть колонка серийного номера: движения поштучных позиций иначе неразличимы — по строке не видно, какую именно единицу двигали.

Журнал открывается двумя экранами, и оба — очередь со своими колонками, фильтрами, сортировкой и страницами: Инвентарь → Движения ТМЦ и админский /admin/plugin/inventoru/movement, где сверху ещё приход, корректировка и загрузка CSV. Админский до 29.09.2026 показывал последние 500 записей одного выбранного склада, молча обрезая остальное и не давая ни фильтра, ни страниц. Склад теперь спрашивает каждая форма отдельно: где человек берёт стоящее на складе и что он смотрит в журнале — разные вопросы, а один переключатель отвечал на оба.

Читать журнал и писать в него — разные права, и это намеренно. Какие движения человек видит, решает общее правило плагина — склады, с которыми он работает (work: свои, выданные ему или его группе, плюс склады его группы редактирования), одинаково на обоих экранах: один и тот же журнал не должен отвечать по-разному в зависимости от того, с какой стороны в него зашли. Приход и корректировка уже́: только склады группы редактирования (run), потому что положить на склад — заявка более сильная, чем знать, что там лежит. Сам журнал в админке закрыт своими узлами movement:{queueFilter,queueShow}. Какие очереди человеку предлагаются, решает назначение очереди на него или его группу — к этим узлам оно отношения не имеет, это две разные админские страницы. Поэтому без узлов выбор очереди не показывается вовсе: приход и корректировка на месте, журнала нет. Раньше экран в такой связке (очередь назначена, узел не выдан) сам подгружал панель фильтров при открытии и отвечал окном «HTTP STATUS: 500» с именем запрещённого действия.

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

Приход (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. Вернуть можно не больше выданного пользователю ранее и ещё не возвращённого — опция returnLimit, см. Опции узлов прав — где действует узел.

«Активный резерв» — 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).

Вкладка называет причину, а не просто прячет кнопки: «списание уже проведено» или «в этом статусе материалы не изменяются». Те же слова возвращают и отказы сервера — раньше все три говорили «процесс уже выгружен в 1С» независимо от того, какая из двух причин сработала.

Почему резерв ещё висит

Списание в ERP появляется только после того, как 1С приняла экспорт (см. sync1c), поэтому у завершённого процесса резерв может стоять дальше — и раньше вкладка об этом молчала, а ответ лежал на админском экране, куда кладовщик не ходит. Теперь вкладка говорит, когда резерв кого-то ждёт:

  • запись очереди экспорта ждёт, а обмен с 1С выключен — сама не отправится;

  • попытки отправки исчерпаны — автоматически больше не пойдёт;

  • 1С списание приняла, а в ERP оно не проведено (статус unposted) — резерв висит так же; вкладка в этом случае и закрыта отдельными словами («1С приняла списание по процессу»), а не «списание уже проведено»: иначе она сама себе противоречила бы. Закрывается пунктом «Провести в ERP»;

  • запись просто стоит в очереди — тогда сказано спокойно, без красного.

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

Спокойный случай («запись просто стоит в очереди, обмен включён») и выглядит спокойно — остальные три выделены красным.

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

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

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

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

Отчёт в плагине 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: журнал очередью
│   │                                (QueueScreenAction) + receipt, adjust, writeoffUnit
│   ├── 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/                      — 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 ИЛИ group_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/EquipType — TreeItem, свойства (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=?;