PRINTER CARD is now supplies at or below 5 percent, a new printers_dashboardpercent setting. The report and the card want different scopes: the report lists anything the thresholds call low, which is right for planning an order, while the dashboard is asking what to walk out and change today - and a cartridge at 18 percent is not that. Lowest first. PRINTER LOCATION WAS ALWAYS EMPTY. The lookup went through db.session.get on locationid and produced nothing even where a location is set; the printers list has always read it through the asset relationship, so the card does too now. WARRANTY ROWS are identified the way the floor identifies them: the PC's hostname and the MACHINE it drives, reusing the same lookup behind the warranty page's machine column so the board and the report cannot disagree about which bay a PC belongs to. No dates - expired or expiring is the whole decision when scanning a board, and the exact day belongs on the report you order from. NOTIFICATIONS are stacked: the type in full on one line, the message beneath, trimmed to 100 characters with the rest on hover. Inline, the type was truncated to make room for prose that was then truncated anyway, and neither read. A tooltip is omitted when the text was not trimmed, because one repeating what is already on screen is noise. Two general additions: layout: 'stacked' on a card, and map.detailtooltip.
173 lines
6.3 KiB
Python
173 lines
6.3 KiB
Python
"""Warranty plugin main class.
|
|
|
|
Asset-general plugin: owns its warranties + warrantyassets tables, an API
|
|
surface, and a sidebar entry. Not tied to any one asset type - a warranty can
|
|
cover a PC, printer, network device, or machine.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import List, Dict, Optional, Type
|
|
|
|
from flask import Flask, Blueprint
|
|
|
|
from shopdb.plugins.base import BasePlugin, PluginMeta
|
|
|
|
from .api import warranty_bp
|
|
from .models import Warranty, WarrantyAsset
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class WarrantyPlugin(BasePlugin):
|
|
"""Warranty tracking plugin."""
|
|
|
|
def __init__(self):
|
|
self._manifest = self._load_manifest()
|
|
|
|
def _load_manifest(self) -> Dict:
|
|
manifest_path = Path(__file__).parent / 'manifest.json'
|
|
if manifest_path.exists():
|
|
with open(manifest_path, 'r') as f:
|
|
return json.load(f)
|
|
return {}
|
|
|
|
@property
|
|
def meta(self) -> PluginMeta:
|
|
return PluginMeta(
|
|
name=self._manifest.get('name', 'warranty'),
|
|
version=self._manifest.get('version', '1.0.0'),
|
|
description=self._manifest.get('description', 'Asset warranty tracking'),
|
|
author=self._manifest.get('author', 'ShopDB Team'),
|
|
dependencies=self._manifest.get('dependencies', []),
|
|
core_version=self._manifest.get('core_version', '>=0.2.0,<1.0.0'),
|
|
api_prefix=self._manifest.get('api_prefix', '/api/warranty'),
|
|
)
|
|
|
|
def get_blueprint(self) -> Optional[Blueprint]:
|
|
return warranty_bp
|
|
|
|
def get_models(self) -> List[Type]:
|
|
return [Warranty, WarrantyAsset]
|
|
|
|
def get_navigation_items(self) -> List[Dict]:
|
|
return [
|
|
{
|
|
'name': 'Warranties',
|
|
'icon': 'shield',
|
|
'route': '/warranties',
|
|
'position': 8,
|
|
},
|
|
]
|
|
|
|
def get_reports(self) -> List[Dict]:
|
|
return [
|
|
{
|
|
'id': 'warranty',
|
|
'name': 'Warranty Report',
|
|
'description': 'Assets bucketed by coverage: expired, expiring soon, active',
|
|
'category': 'warranty',
|
|
'route': '/reports/warranty',
|
|
},
|
|
]
|
|
|
|
def get_asset_panels(self) -> List[Dict]:
|
|
# ADR-010 pilot. Warranty is asset-general so it shows on every detail
|
|
# page (['*']). Declares the existing per-asset warranty endpoint the
|
|
# WarrantyPanel already reads; the generic core panel renderer consumes
|
|
# this instead of each detail view hand-composing the panel.
|
|
return [
|
|
{
|
|
'id': 'warranty',
|
|
'title': 'Warranty',
|
|
'assettypes': ['*'],
|
|
'endpoint': '/api/warranty/asset/{assetid}',
|
|
'render': 'list',
|
|
# Field map: the generic renderer builds each list item from the
|
|
# warranty payload without any warranty-specific frontend code.
|
|
'map': {
|
|
'title': 'vendor',
|
|
'badge': {
|
|
'label': 'status',
|
|
'color': 'statuscolor',
|
|
'labelmap': {
|
|
'active': 'Active',
|
|
'expiring': 'Expiring Soon',
|
|
'expired': 'Expired',
|
|
'unknown': 'Unknown',
|
|
},
|
|
},
|
|
'meta': [
|
|
{'key': 'servicelevel'},
|
|
{'key': 'enddate', 'label': 'Ends', 'format': 'date'},
|
|
{'key': 'servicetag', 'label': 'Tag', 'mono': True},
|
|
],
|
|
},
|
|
'empty': 'No warranty on record.',
|
|
'manage': {
|
|
'to': '/warranties?addfor={assetid}',
|
|
'label': 'Add / manage',
|
|
'emptylabel': 'Add one',
|
|
},
|
|
'position': 30,
|
|
},
|
|
]
|
|
|
|
def get_dashboard_widgets(self) -> List[Dict]:
|
|
"""Dashboard card: warranties running out, and ones already expired.
|
|
|
|
Expired stays listed rather than dropping off. A machine out of
|
|
warranty is a purchasing decision someone still has to make, and
|
|
removing it the day it lapses is how it gets missed entirely.
|
|
"""
|
|
return [
|
|
{
|
|
'id': 'warranty-expiring',
|
|
'viewall': '/reports/warranty',
|
|
'title': 'Warranties expiring',
|
|
'endpoint': '/api/warranty/dashboard/expiring',
|
|
'render': 'exceptions',
|
|
'severity': 'info',
|
|
'permission': 'warranty.view',
|
|
'empty': 'hide',
|
|
'position': 50,
|
|
'map': {
|
|
'title': 'hostname',
|
|
'detail': 'state',
|
|
# The bay, which is how the floor identifies the PC. No
|
|
# date: expired or expiring is the whole decision when
|
|
# scanning a board, and the exact day belongs on the report
|
|
# you order from.
|
|
'meta': [{'key': 'machinenumber', 'label': 'machine'}],
|
|
'link': '/assets/{assetid}',
|
|
},
|
|
},
|
|
]
|
|
|
|
def get_settings_defaults(self) -> List[Dict]:
|
|
"""Settings this plugin owns."""
|
|
return [
|
|
{
|
|
'key': 'warranty_expiringdays',
|
|
'value': '90',
|
|
'valuetype': 'integer',
|
|
'category': 'warranty',
|
|
'description': 'Days ahead to list an expiring warranty on the '
|
|
'dashboard. Already-expired warranties are always '
|
|
'listed.',
|
|
},
|
|
]
|
|
|
|
def get_permissions(self) -> List:
|
|
"""Return the RBAC permissions this plugin owns."""
|
|
return [
|
|
('warranty.view', 'View warranties', 'warranty'),
|
|
('warranty.create', 'Create warranties', 'warranty'),
|
|
('warranty.edit', 'Edit warranties', 'warranty'),
|
|
('warranty.delete', 'Delete warranties', 'warranty'),
|
|
]
|
|
|
|
def init_app(self, app: Flask, db_instance) -> None:
|
|
logger.info(f"Warranty plugin initialized (v{self.meta.version})")
|