The section headings were derived from position ranges: 10-29 became Assets, 30-49 became Information. A number is for ORDER; it was never a statement about meaning, and using it as one meant a plugin landed in whichever bucket its number happened to fall in. That is how USB Devices - an asset - came to sit under Information beside GE-Enforce and Tech Tools, and 3D Parts with it. Nobody chose that. It also meant two plugins picking the same number silently reordered each other: Warranties and Guides both held 8, the tie broke on plugin load order, and Guides rendered under Warranties rather than under Slides where it was asked to go. Warranties moves to 9. Each nav item now names its section and the renderer groups by what was declared. An item with no section stays in the unlabelled group at the top, which is the safe default: it appears, it is simply not filed. Assets gains USB Devices and 3D Parts. GE-Enforce and Tech Tools move to a new Fleet section - they are neither assets nor reference material, and calling them Information was only ever an artefact of their position numbers. Also: sorting used `position || 99`, and 0 is falsy in JavaScript, so Dashboard - the only item that uses 0 - sorted to the end of its own group.
191 lines
7.3 KiB
Python
191 lines
7.3 KiB
Python
"""Printedparts plugin main class.
|
|
|
|
3D-printed parts inventory + kiosk checkout. Quantity-based consumables:
|
|
one row is a KIND of part with a count, not an individually tracked asset,
|
|
so unlike most plugins this one seeds NO AssetType (ADR-001 assets are
|
|
one-row-per-physical-thing). See docs/proposals/printedparts-plugin.md.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import List, Optional, Type
|
|
|
|
from flask import Flask, Blueprint
|
|
|
|
from shopdb.plugins.base import BasePlugin, PluginMeta
|
|
|
|
from .models import PrintedItem, PrintedItemTransaction, PrintedItemFile
|
|
from .api import printedparts_bp
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PrintedpartsPlugin(BasePlugin):
|
|
"""3D-printed parts inventory + kiosk checkout."""
|
|
|
|
def __init__(self):
|
|
manifest_path = Path(__file__).parent / 'manifest.json'
|
|
with open(manifest_path) as f:
|
|
self._manifest = json.load(f)
|
|
|
|
@property
|
|
def meta(self) -> PluginMeta:
|
|
return PluginMeta(
|
|
name=self._manifest['name'],
|
|
version=self._manifest['version'],
|
|
description=self._manifest['description'],
|
|
author=self._manifest.get('author', ''),
|
|
dependencies=self._manifest.get('dependencies', []),
|
|
core_version=self._manifest.get('core_version', '>=0.1.0'),
|
|
api_prefix=self._manifest.get('api_prefix'),
|
|
)
|
|
|
|
def get_blueprint(self) -> Optional[Blueprint]:
|
|
return printedparts_bp
|
|
|
|
def get_models(self) -> List[Type]:
|
|
return [PrintedItem, PrintedItemTransaction, PrintedItemFile]
|
|
|
|
def init_app(self, app: Flask, db_instance) -> None:
|
|
logger.info(f'Printedparts plugin initialized (v{self.meta.version})')
|
|
|
|
def get_permissions(self) -> List:
|
|
"""RBAC permissions this plugin owns (seeded on install/enable)."""
|
|
return [
|
|
('printedparts.view', 'View 3D printed parts', 'printedparts'),
|
|
('printedparts.create', 'Create printed parts', 'printedparts'),
|
|
('printedparts.edit', 'Edit printed parts', 'printedparts'),
|
|
('printedparts.delete', 'Retire printed parts', 'printedparts'),
|
|
('printedparts.restock', 'Restock and adjust stock counts',
|
|
'printedparts'),
|
|
]
|
|
|
|
def get_settings_cards(self) -> List[dict]:
|
|
return [
|
|
{
|
|
'group': '3D Printed Parts',
|
|
'to': '/settings/printedparts',
|
|
'icon': 'box',
|
|
'title': '3D Parts Settings',
|
|
'description': 'Item code prefix, default threshold, kiosk '
|
|
'badge policy, low-stock alert recipients',
|
|
'position': 47,
|
|
},
|
|
]
|
|
|
|
def get_reports(self) -> List[dict]:
|
|
return [
|
|
{
|
|
'id': 'printedparts-stock',
|
|
'name': '3D Parts Stock',
|
|
'description': 'Stock levels with low-stock flags and the '
|
|
'cache-vs-ledger reconcile check',
|
|
'category': 'inventory',
|
|
'endpoint': '/api/printedparts/reports/stock',
|
|
},
|
|
{
|
|
'id': 'printedparts-consumption',
|
|
'name': '3D Parts Consumption',
|
|
'description': 'Takes per item over a date range',
|
|
'category': 'usage',
|
|
'endpoint': '/api/printedparts/reports/consumption',
|
|
},
|
|
{
|
|
'id': 'printedparts-by-person',
|
|
'name': '3D Parts by Person',
|
|
'description': 'Takes grouped by employee',
|
|
'category': 'usage',
|
|
'endpoint': '/api/printedparts/reports/by-person',
|
|
},
|
|
]
|
|
|
|
def get_navigation_items(self) -> List[dict]:
|
|
return [
|
|
{
|
|
'name': '3D Parts',
|
|
'icon': 'box',
|
|
'route': '/printedparts',
|
|
'position': 27,
|
|
'section': 'assets',
|
|
},
|
|
]
|
|
|
|
def on_install(self, app: Flask) -> None:
|
|
logger.info('Printedparts plugin installed')
|
|
|
|
def get_settings_defaults(self) -> List[dict]:
|
|
# The framework seeds these at install, at enable, and on every
|
|
# `flask plugin upgrade-all`, so a key added in a later version reaches
|
|
# a site that installed an earlier one.
|
|
return [
|
|
{
|
|
'key': 'printedparts_code_prefix',
|
|
'value': '3DP',
|
|
'valuetype': 'string',
|
|
'category': 'printedparts',
|
|
'description': 'Prefix for generated item codes',
|
|
},
|
|
{
|
|
'key': 'printedparts_label_prefix',
|
|
'value': '',
|
|
'valuetype': 'string',
|
|
'category': 'printedparts',
|
|
# Public: the parts kiosk runs logged out, so the anonymous
|
|
# read has to be allowed or the prefix never renders there.
|
|
'public': True,
|
|
'description': 'Leading text on the physical gage-lab labels, '
|
|
'shown at the kiosk before the number box so '
|
|
'operators type only the digits. Empty shows no '
|
|
'prefix. Site-specific: set it to whatever this '
|
|
"site's labels are printed with, e.g. WJ",
|
|
},
|
|
{
|
|
'key': 'printedparts_default_threshold',
|
|
'value': '5',
|
|
'valuetype': 'integer',
|
|
'category': 'printedparts',
|
|
'description': 'Default low-stock threshold for new items',
|
|
},
|
|
{
|
|
'key': 'printedparts_unknown_badge',
|
|
'value': 'deny',
|
|
'valuetype': 'string',
|
|
'category': 'printedparts',
|
|
'description': 'Kiosk policy when a badge resolves to no '
|
|
'employee: allow or deny',
|
|
},
|
|
{
|
|
'key': 'printedparts_alert_email',
|
|
'value': '',
|
|
'valuetype': 'string',
|
|
'category': 'printedparts',
|
|
'description': 'Comma-separated low-stock alert recipients; '
|
|
'empty uses the site alert_recipients',
|
|
},
|
|
{
|
|
'key': 'printedparts_alert_userids',
|
|
'value': '',
|
|
'valuetype': 'string',
|
|
'category': 'printedparts',
|
|
'description': 'Comma-separated shopdb user ids whose account '
|
|
'emails receive low-stock alerts',
|
|
},
|
|
{
|
|
'key': 'printedparts_alert_roleids',
|
|
'value': '',
|
|
'valuetype': 'string',
|
|
'category': 'printedparts',
|
|
'description': 'Comma-separated role ids; every active member '
|
|
'of these roles receives low-stock alerts',
|
|
},
|
|
{
|
|
'key': 'printedparts_alert_supportteamid',
|
|
'value': '',
|
|
'valuetype': 'string',
|
|
'category': 'printedparts',
|
|
'description': 'Support team whose webhook receives low-stock '
|
|
'alerts; empty uses the site alert_webhook_url',
|
|
},
|
|
]
|