Add USB, Notifications, Network plugins and reusable EmployeeSearch component
New Plugins: - USB plugin: Device checkout/checkin with employee lookup, checkout history - Notifications plugin: Announcements with types, scheduling, shopfloor display - Network plugin: Network device management with subnets and VLANs - Equipment and Computers plugins: Asset type separation Frontend: - EmployeeSearch component: Reusable employee lookup with autocomplete - USB views: List, detail, checkout/checkin modals - Notifications views: List, form with recognition mode - Network views: Device list, detail, form - Calendar view with FullCalendar integration - Shopfloor and TV dashboard views - Reports index page - Map editor for asset positioning - Light/dark mode fixes for map tooltips Backend: - Employee search API with external lookup service - Collector API for PowerShell data collection - Reports API endpoints - Slides API for TV dashboard - Fixed AppVersion model (removed BaseModel inheritance) - Added checkout_name column to usbcheckouts table Styling: - Unified detail page styles - Improved pagination (page numbers instead of prev/next) - Dark/light mode theme improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
"""Printers plugin API."""
|
||||
|
||||
from .routes import printers_bp
|
||||
from .routes import printers_bp # Legacy Machine-based API
|
||||
from .asset_routes import printers_asset_bp # New Asset-based API
|
||||
|
||||
__all__ = ['printers_bp']
|
||||
__all__ = [
|
||||
'printers_bp', # Legacy
|
||||
'printers_asset_bp', # New
|
||||
]
|
||||
|
||||
472
plugins/printers/api/asset_routes.py
Normal file
472
plugins/printers/api/asset_routes.py
Normal file
@@ -0,0 +1,472 @@
|
||||
"""Printers API routes - new Asset-based architecture."""
|
||||
|
||||
from flask import Blueprint, request
|
||||
from flask_jwt_extended import jwt_required
|
||||
|
||||
from shopdb.extensions import db
|
||||
from shopdb.core.models import Asset, AssetType, Vendor, Model, Communication, CommunicationType
|
||||
from shopdb.utils.responses import (
|
||||
success_response,
|
||||
error_response,
|
||||
paginated_response,
|
||||
ErrorCodes
|
||||
)
|
||||
from shopdb.utils.pagination import get_pagination_params, paginate_query
|
||||
|
||||
from ..models import Printer, PrinterType
|
||||
from ..services import ZabbixService
|
||||
|
||||
printers_asset_bp = Blueprint('printers_asset', __name__)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Printer Types
|
||||
# =============================================================================
|
||||
|
||||
@printers_asset_bp.route('/types', methods=['GET'])
|
||||
@jwt_required()
|
||||
def list_printer_types():
|
||||
"""List all printer types."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
|
||||
query = PrinterType.query
|
||||
|
||||
if request.args.get('active', 'true').lower() != 'false':
|
||||
query = query.filter(PrinterType.isactive == True)
|
||||
|
||||
if search := request.args.get('search'):
|
||||
query = query.filter(PrinterType.printertype.ilike(f'%{search}%'))
|
||||
|
||||
query = query.order_by(PrinterType.printertype)
|
||||
|
||||
items, total = paginate_query(query, page, per_page)
|
||||
data = [t.to_dict() for t in items]
|
||||
|
||||
return paginated_response(data, page, per_page, total)
|
||||
|
||||
|
||||
@printers_asset_bp.route('/types/<int:type_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_printer_type(type_id: int):
|
||||
"""Get a single printer type."""
|
||||
t = PrinterType.query.get(type_id)
|
||||
|
||||
if not t:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Printer type with ID {type_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
return success_response(t.to_dict())
|
||||
|
||||
|
||||
@printers_asset_bp.route('/types', methods=['POST'])
|
||||
@jwt_required()
|
||||
def create_printer_type():
|
||||
"""Create a new printer type."""
|
||||
data = request.get_json()
|
||||
|
||||
if not data or not data.get('printertype'):
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'printertype is required')
|
||||
|
||||
if PrinterType.query.filter_by(printertype=data['printertype']).first():
|
||||
return error_response(
|
||||
ErrorCodes.CONFLICT,
|
||||
f"Printer type '{data['printertype']}' already exists",
|
||||
http_code=409
|
||||
)
|
||||
|
||||
t = PrinterType(
|
||||
printertype=data['printertype'],
|
||||
description=data.get('description'),
|
||||
icon=data.get('icon')
|
||||
)
|
||||
|
||||
db.session.add(t)
|
||||
db.session.commit()
|
||||
|
||||
return success_response(t.to_dict(), message='Printer type created', http_code=201)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Printers CRUD
|
||||
# =============================================================================
|
||||
|
||||
@printers_asset_bp.route('', methods=['GET'])
|
||||
@jwt_required()
|
||||
def list_printers():
|
||||
"""
|
||||
List all printers with filtering and pagination.
|
||||
|
||||
Query parameters:
|
||||
- page, per_page: Pagination
|
||||
- active: Filter by active status
|
||||
- search: Search by asset number, name, or hostname
|
||||
- type_id: Filter by printer type ID
|
||||
- vendor_id: Filter by vendor ID
|
||||
- location_id: Filter by location ID
|
||||
- businessunit_id: Filter by business unit ID
|
||||
"""
|
||||
page, per_page = get_pagination_params(request)
|
||||
|
||||
# Join Printer with Asset
|
||||
query = db.session.query(Printer).join(Asset)
|
||||
|
||||
# Active filter
|
||||
if request.args.get('active', 'true').lower() != 'false':
|
||||
query = query.filter(Asset.isactive == True)
|
||||
|
||||
# Search filter
|
||||
if search := request.args.get('search'):
|
||||
query = query.filter(
|
||||
db.or_(
|
||||
Asset.assetnumber.ilike(f'%{search}%'),
|
||||
Asset.name.ilike(f'%{search}%'),
|
||||
Asset.serialnumber.ilike(f'%{search}%'),
|
||||
Printer.hostname.ilike(f'%{search}%'),
|
||||
Printer.windowsname.ilike(f'%{search}%')
|
||||
)
|
||||
)
|
||||
|
||||
# Type filter
|
||||
if type_id := request.args.get('type_id'):
|
||||
query = query.filter(Printer.printertypeid == int(type_id))
|
||||
|
||||
# Vendor filter
|
||||
if vendor_id := request.args.get('vendor_id'):
|
||||
query = query.filter(Printer.vendorid == int(vendor_id))
|
||||
|
||||
# Location filter
|
||||
if location_id := request.args.get('location_id'):
|
||||
query = query.filter(Asset.locationid == int(location_id))
|
||||
|
||||
# Business unit filter
|
||||
if bu_id := request.args.get('businessunit_id'):
|
||||
query = query.filter(Asset.businessunitid == int(bu_id))
|
||||
|
||||
# Sorting
|
||||
sort_by = request.args.get('sort', 'hostname')
|
||||
sort_dir = request.args.get('dir', 'asc')
|
||||
|
||||
if sort_by == 'hostname':
|
||||
col = Printer.hostname
|
||||
elif sort_by == 'assetnumber':
|
||||
col = Asset.assetnumber
|
||||
elif sort_by == 'name':
|
||||
col = Asset.name
|
||||
else:
|
||||
col = Printer.hostname
|
||||
|
||||
query = query.order_by(col.desc() if sort_dir == 'desc' else col)
|
||||
|
||||
items, total = paginate_query(query, page, per_page)
|
||||
|
||||
# Build response with both asset and printer data
|
||||
data = []
|
||||
for printer in items:
|
||||
item = printer.asset.to_dict() if printer.asset else {}
|
||||
item['printer'] = printer.to_dict()
|
||||
|
||||
# Add primary IP address
|
||||
if printer.asset:
|
||||
primary_comm = Communication.query.filter_by(
|
||||
assetid=printer.asset.assetid,
|
||||
isprimary=True
|
||||
).first()
|
||||
if not primary_comm:
|
||||
primary_comm = Communication.query.filter_by(
|
||||
assetid=printer.asset.assetid
|
||||
).first()
|
||||
item['ipaddress'] = primary_comm.ipaddress if primary_comm else None
|
||||
|
||||
data.append(item)
|
||||
|
||||
return paginated_response(data, page, per_page, total)
|
||||
|
||||
|
||||
@printers_asset_bp.route('/<int:printer_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_printer(printer_id: int):
|
||||
"""Get a single printer with full details."""
|
||||
printer = Printer.query.get(printer_id)
|
||||
|
||||
if not printer:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Printer with ID {printer_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
result = printer.asset.to_dict() if printer.asset else {}
|
||||
result['printer'] = printer.to_dict()
|
||||
|
||||
# Add communications
|
||||
if printer.asset:
|
||||
comms = Communication.query.filter_by(assetid=printer.asset.assetid).all()
|
||||
result['communications'] = [c.to_dict() for c in comms]
|
||||
|
||||
return success_response(result)
|
||||
|
||||
|
||||
@printers_asset_bp.route('/by-asset/<int:asset_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_printer_by_asset(asset_id: int):
|
||||
"""Get printer data by asset ID."""
|
||||
printer = Printer.query.filter_by(assetid=asset_id).first()
|
||||
|
||||
if not printer:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Printer for asset {asset_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
result = printer.asset.to_dict() if printer.asset else {}
|
||||
result['printer'] = printer.to_dict()
|
||||
|
||||
return success_response(result)
|
||||
|
||||
|
||||
@printers_asset_bp.route('', methods=['POST'])
|
||||
@jwt_required()
|
||||
def create_printer():
|
||||
"""
|
||||
Create new printer (creates both Asset and Printer records).
|
||||
|
||||
Required fields:
|
||||
- assetnumber: Business identifier
|
||||
|
||||
Optional fields:
|
||||
- name, serialnumber, statusid, locationid, businessunitid
|
||||
- printertypeid, vendorid, modelnumberid, hostname
|
||||
- windowsname, sharename, iscsf, installpath, pin
|
||||
- iscolor, isduplex, isnetwork
|
||||
- mapleft, maptop, notes
|
||||
"""
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
||||
|
||||
if not data.get('assetnumber'):
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'assetnumber is required')
|
||||
|
||||
# Check for duplicate assetnumber
|
||||
if Asset.query.filter_by(assetnumber=data['assetnumber']).first():
|
||||
return error_response(
|
||||
ErrorCodes.CONFLICT,
|
||||
f"Asset with number '{data['assetnumber']}' already exists",
|
||||
http_code=409
|
||||
)
|
||||
|
||||
# Get printer asset type
|
||||
printer_type = AssetType.query.filter_by(assettype='printer').first()
|
||||
if not printer_type:
|
||||
return error_response(
|
||||
ErrorCodes.INTERNAL_ERROR,
|
||||
'Printer asset type not found. Plugin may not be properly installed.',
|
||||
http_code=500
|
||||
)
|
||||
|
||||
# Create the core asset
|
||||
asset = Asset(
|
||||
assetnumber=data['assetnumber'],
|
||||
name=data.get('name'),
|
||||
serialnumber=data.get('serialnumber'),
|
||||
assettypeid=printer_type.assettypeid,
|
||||
statusid=data.get('statusid', 1),
|
||||
locationid=data.get('locationid'),
|
||||
businessunitid=data.get('businessunitid'),
|
||||
mapleft=data.get('mapleft'),
|
||||
maptop=data.get('maptop'),
|
||||
notes=data.get('notes')
|
||||
)
|
||||
|
||||
db.session.add(asset)
|
||||
db.session.flush() # Get the assetid
|
||||
|
||||
# Create the printer extension
|
||||
printer = Printer(
|
||||
assetid=asset.assetid,
|
||||
printertypeid=data.get('printertypeid'),
|
||||
vendorid=data.get('vendorid'),
|
||||
modelnumberid=data.get('modelnumberid'),
|
||||
hostname=data.get('hostname'),
|
||||
windowsname=data.get('windowsname'),
|
||||
sharename=data.get('sharename'),
|
||||
iscsf=data.get('iscsf', False),
|
||||
installpath=data.get('installpath'),
|
||||
pin=data.get('pin'),
|
||||
iscolor=data.get('iscolor', False),
|
||||
isduplex=data.get('isduplex', False),
|
||||
isnetwork=data.get('isnetwork', True)
|
||||
)
|
||||
|
||||
db.session.add(printer)
|
||||
|
||||
# Create communication record if IP provided
|
||||
if data.get('ipaddress'):
|
||||
ip_comtype = CommunicationType.query.filter_by(comtype='IP').first()
|
||||
if ip_comtype:
|
||||
comm = Communication(
|
||||
assetid=asset.assetid,
|
||||
comtypeid=ip_comtype.comtypeid,
|
||||
ipaddress=data['ipaddress'],
|
||||
isprimary=True
|
||||
)
|
||||
db.session.add(comm)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
result = asset.to_dict()
|
||||
result['printer'] = printer.to_dict()
|
||||
|
||||
return success_response(result, message='Printer created', http_code=201)
|
||||
|
||||
|
||||
@printers_asset_bp.route('/<int:printer_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
def update_printer(printer_id: int):
|
||||
"""Update printer (both Asset and Printer records)."""
|
||||
printer = Printer.query.get(printer_id)
|
||||
|
||||
if not printer:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Printer with ID {printer_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
||||
|
||||
asset = printer.asset
|
||||
|
||||
# Check for conflicting assetnumber
|
||||
if 'assetnumber' in data and data['assetnumber'] != asset.assetnumber:
|
||||
if Asset.query.filter_by(assetnumber=data['assetnumber']).first():
|
||||
return error_response(
|
||||
ErrorCodes.CONFLICT,
|
||||
f"Asset with number '{data['assetnumber']}' already exists",
|
||||
http_code=409
|
||||
)
|
||||
|
||||
# Update asset fields
|
||||
asset_fields = ['assetnumber', 'name', 'serialnumber', 'statusid',
|
||||
'locationid', 'businessunitid', 'mapleft', 'maptop', 'notes', 'isactive']
|
||||
for key in asset_fields:
|
||||
if key in data:
|
||||
setattr(asset, key, data[key])
|
||||
|
||||
# Update printer fields
|
||||
printer_fields = ['printertypeid', 'vendorid', 'modelnumberid', 'hostname',
|
||||
'windowsname', 'sharename', 'iscsf', 'installpath', 'pin',
|
||||
'iscolor', 'isduplex', 'isnetwork']
|
||||
for key in printer_fields:
|
||||
if key in data:
|
||||
setattr(printer, key, data[key])
|
||||
|
||||
db.session.commit()
|
||||
|
||||
result = asset.to_dict()
|
||||
result['printer'] = printer.to_dict()
|
||||
|
||||
return success_response(result, message='Printer updated')
|
||||
|
||||
|
||||
@printers_asset_bp.route('/<int:printer_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
def delete_printer(printer_id: int):
|
||||
"""Delete (soft delete) printer."""
|
||||
printer = Printer.query.get(printer_id)
|
||||
|
||||
if not printer:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Printer with ID {printer_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
# Soft delete the asset
|
||||
printer.asset.isactive = False
|
||||
db.session.commit()
|
||||
|
||||
return success_response(message='Printer deleted')
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Supply Levels (Zabbix Integration)
|
||||
# =============================================================================
|
||||
|
||||
@printers_asset_bp.route('/<int:printer_id>/supplies', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
def get_printer_supplies(printer_id: int):
|
||||
"""Get supply levels from Zabbix (real-time lookup)."""
|
||||
printer = Printer.query.get(printer_id)
|
||||
|
||||
if not printer:
|
||||
return error_response(ErrorCodes.NOT_FOUND, 'Printer not found', http_code=404)
|
||||
|
||||
# Get IP address from communications
|
||||
comm = Communication.query.filter_by(
|
||||
assetid=printer.assetid,
|
||||
isprimary=True
|
||||
).first()
|
||||
if not comm:
|
||||
comm = Communication.query.filter_by(assetid=printer.assetid).first()
|
||||
|
||||
if not comm or not comm.ipaddress:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'Printer has no IP address')
|
||||
|
||||
service = ZabbixService()
|
||||
if not service.isconfigured:
|
||||
return error_response(ErrorCodes.BAD_REQUEST, 'Zabbix not configured')
|
||||
|
||||
supplies = service.getsuppliesbyip(comm.ipaddress)
|
||||
|
||||
return success_response({
|
||||
'ipaddress': comm.ipaddress,
|
||||
'supplies': supplies or []
|
||||
})
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Dashboard
|
||||
# =============================================================================
|
||||
|
||||
@printers_asset_bp.route('/dashboard/summary', methods=['GET'])
|
||||
@jwt_required()
|
||||
def dashboard_summary():
|
||||
"""Get printer dashboard summary data."""
|
||||
# Total active printers
|
||||
total = db.session.query(Printer).join(Asset).filter(
|
||||
Asset.isactive == True
|
||||
).count()
|
||||
|
||||
# Count by printer type
|
||||
by_type = db.session.query(
|
||||
PrinterType.printertype,
|
||||
db.func.count(Printer.printerid)
|
||||
).join(Printer, Printer.printertypeid == PrinterType.printertypeid
|
||||
).join(Asset, Asset.assetid == Printer.assetid
|
||||
).filter(Asset.isactive == True
|
||||
).group_by(PrinterType.printertype
|
||||
).all()
|
||||
|
||||
# Count by vendor
|
||||
by_vendor = db.session.query(
|
||||
Vendor.vendor,
|
||||
db.func.count(Printer.printerid)
|
||||
).join(Printer, Printer.vendorid == Vendor.vendorid
|
||||
).join(Asset, Asset.assetid == Printer.assetid
|
||||
).filter(Asset.isactive == True
|
||||
).group_by(Vendor.vendor
|
||||
).all()
|
||||
|
||||
return success_response({
|
||||
'total': total,
|
||||
'by_type': [{'type': t, 'count': c} for t, c in by_type],
|
||||
'by_vendor': [{'vendor': v, 'count': c} for v, c in by_vendor],
|
||||
})
|
||||
@@ -1,7 +1,10 @@
|
||||
"""Printers plugin models."""
|
||||
|
||||
from .printer_extension import PrinterData
|
||||
from .printer_extension import PrinterData # Legacy model for Machine-based architecture
|
||||
from .printer import Printer, PrinterType # New Asset-based models
|
||||
|
||||
__all__ = [
|
||||
'PrinterData',
|
||||
'PrinterData', # Legacy
|
||||
'Printer', # New
|
||||
'PrinterType', # New
|
||||
]
|
||||
|
||||
122
plugins/printers/models/printer.py
Normal file
122
plugins/printers/models/printer.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""Printer plugin models - new Asset-based architecture."""
|
||||
|
||||
from shopdb.extensions import db
|
||||
from shopdb.core.models.base import BaseModel
|
||||
|
||||
|
||||
class PrinterType(BaseModel):
|
||||
"""
|
||||
Printer type classification.
|
||||
|
||||
Examples: Laser, Inkjet, Label, MFP, Plotter, etc.
|
||||
"""
|
||||
__tablename__ = 'printertypes'
|
||||
|
||||
printertypeid = db.Column(db.Integer, primary_key=True)
|
||||
printertype = db.Column(db.String(100), unique=True, nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
icon = db.Column(db.String(50), comment='Icon name for UI')
|
||||
|
||||
def __repr__(self):
|
||||
return f"<PrinterType {self.printertype}>"
|
||||
|
||||
|
||||
class Printer(BaseModel):
|
||||
"""
|
||||
Printer-specific extension data (new Asset architecture).
|
||||
|
||||
Links to core Asset table via assetid.
|
||||
Stores printer-specific fields like type, Windows name, share name, etc.
|
||||
"""
|
||||
__tablename__ = 'printers'
|
||||
|
||||
printerid = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# Link to core asset
|
||||
assetid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('assets.assetid', ondelete='CASCADE'),
|
||||
unique=True,
|
||||
nullable=False,
|
||||
index=True
|
||||
)
|
||||
|
||||
# Printer classification
|
||||
printertypeid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('printertypes.printertypeid'),
|
||||
nullable=True
|
||||
)
|
||||
|
||||
# Vendor
|
||||
vendorid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('vendors.vendorid'),
|
||||
nullable=True
|
||||
)
|
||||
modelnumberid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('models.modelnumberid'),
|
||||
nullable=True
|
||||
)
|
||||
|
||||
# Network identity
|
||||
hostname = db.Column(
|
||||
db.String(100),
|
||||
index=True,
|
||||
comment='Network hostname'
|
||||
)
|
||||
|
||||
# Windows/Network naming
|
||||
windowsname = db.Column(
|
||||
db.String(255),
|
||||
comment='Windows printer name (e.g., \\\\server\\printer)'
|
||||
)
|
||||
sharename = db.Column(
|
||||
db.String(100),
|
||||
comment='CSF/share name'
|
||||
)
|
||||
|
||||
# Installation
|
||||
iscsf = db.Column(db.Boolean, default=False, comment='Is CSF printer')
|
||||
installpath = db.Column(db.String(255), comment='Driver install path')
|
||||
|
||||
# Printer PIN (for secure print)
|
||||
pin = db.Column(db.String(20))
|
||||
|
||||
# Features
|
||||
iscolor = db.Column(db.Boolean, default=False, comment='Color capable')
|
||||
isduplex = db.Column(db.Boolean, default=False, comment='Duplex capable')
|
||||
isnetwork = db.Column(db.Boolean, default=True, comment='Network connected')
|
||||
|
||||
# Relationships
|
||||
asset = db.relationship(
|
||||
'Asset',
|
||||
backref=db.backref('printer', uselist=False, lazy='joined')
|
||||
)
|
||||
printertype = db.relationship('PrinterType', backref='printers')
|
||||
vendor = db.relationship('Vendor', backref='printer_items')
|
||||
model = db.relationship('Model', backref='printer_items')
|
||||
|
||||
__table_args__ = (
|
||||
db.Index('idx_printer_type', 'printertypeid'),
|
||||
db.Index('idx_printer_hostname', 'hostname'),
|
||||
db.Index('idx_printer_windowsname', 'windowsname'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Printer {self.hostname or self.assetid}>"
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert to dictionary with related names."""
|
||||
result = super().to_dict()
|
||||
|
||||
# Add related object names
|
||||
if self.printertype:
|
||||
result['printertype_name'] = self.printertype.printertype
|
||||
if self.vendor:
|
||||
result['vendor_name'] = self.vendor.vendor
|
||||
if self.model:
|
||||
result['model_name'] = self.model.modelnumber
|
||||
|
||||
return result
|
||||
@@ -11,9 +11,10 @@ import click
|
||||
from shopdb.plugins.base import BasePlugin, PluginMeta
|
||||
from shopdb.extensions import db
|
||||
from shopdb.core.models.machine import MachineType
|
||||
from shopdb.core.models import AssetType
|
||||
|
||||
from .models import PrinterData
|
||||
from .api import printers_bp
|
||||
from .models import PrinterData, Printer, PrinterType
|
||||
from .api import printers_bp, printers_asset_bp
|
||||
from .services import ZabbixService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -21,11 +22,15 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class PrintersPlugin(BasePlugin):
|
||||
"""
|
||||
Printers plugin - extends machines with printer-specific functionality.
|
||||
Printers plugin - manages printer assets.
|
||||
|
||||
Printers use the unified Machine model with machinetype.category = 'Printer'.
|
||||
This plugin adds:
|
||||
- PrinterData table for printer-specific fields (windowsname, sharename, etc.)
|
||||
Supports both legacy Machine-based architecture and new Asset-based architecture:
|
||||
- Legacy: PrinterData table linked to machines
|
||||
- New: Printer table linked to assets
|
||||
|
||||
Features:
|
||||
- PrinterType classification
|
||||
- Windows/network naming
|
||||
- Zabbix integration for real-time supply level lookups
|
||||
"""
|
||||
|
||||
@@ -46,7 +51,7 @@ class PrintersPlugin(BasePlugin):
|
||||
"""Return plugin metadata."""
|
||||
return PluginMeta(
|
||||
name=self._manifest.get('name', 'printers'),
|
||||
version=self._manifest.get('version', '1.0.0'),
|
||||
version=self._manifest.get('version', '2.0.0'),
|
||||
description=self._manifest.get(
|
||||
'description',
|
||||
'Printer management with Zabbix integration'
|
||||
@@ -58,12 +63,21 @@ class PrintersPlugin(BasePlugin):
|
||||
)
|
||||
|
||||
def get_blueprint(self) -> Optional[Blueprint]:
|
||||
"""Return Flask Blueprint with API routes."""
|
||||
return printers_bp
|
||||
"""
|
||||
Return Flask Blueprint with API routes.
|
||||
|
||||
Returns the new Asset-based blueprint.
|
||||
Legacy Machine-based blueprint is registered separately in init_app.
|
||||
"""
|
||||
return printers_asset_bp
|
||||
|
||||
def get_models(self) -> List[Type]:
|
||||
"""Return list of SQLAlchemy model classes."""
|
||||
return [PrinterData]
|
||||
return [
|
||||
PrinterData, # Legacy Machine-based
|
||||
Printer, # New Asset-based
|
||||
PrinterType, # New printer type classification
|
||||
]
|
||||
|
||||
def get_services(self) -> Dict[str, Type]:
|
||||
"""Return plugin services."""
|
||||
@@ -82,16 +96,63 @@ class PrintersPlugin(BasePlugin):
|
||||
"""Initialize plugin with Flask app."""
|
||||
app.config.setdefault('ZABBIX_URL', '')
|
||||
app.config.setdefault('ZABBIX_TOKEN', '')
|
||||
|
||||
# Register legacy blueprint for backward compatibility
|
||||
app.register_blueprint(printers_bp, url_prefix='/api/printers/legacy')
|
||||
|
||||
logger.info(f"Printers plugin initialized (v{self.meta.version})")
|
||||
|
||||
def on_install(self, app: Flask) -> None:
|
||||
"""Called when plugin is installed."""
|
||||
with app.app_context():
|
||||
self._ensureprintertypes()
|
||||
self._ensure_asset_type()
|
||||
self._ensure_printer_types()
|
||||
self._ensure_legacy_machine_types()
|
||||
logger.info("Printers plugin installed")
|
||||
|
||||
def _ensureprintertypes(self) -> None:
|
||||
"""Ensure basic printer machine types exist."""
|
||||
def _ensure_asset_type(self) -> None:
|
||||
"""Ensure printer asset type exists."""
|
||||
existing = AssetType.query.filter_by(assettype='printer').first()
|
||||
if not existing:
|
||||
at = AssetType(
|
||||
assettype='printer',
|
||||
plugin_name='printers',
|
||||
table_name='printers',
|
||||
description='Printers (laser, inkjet, label, MFP, plotter)',
|
||||
icon='printer'
|
||||
)
|
||||
db.session.add(at)
|
||||
logger.debug("Created asset type: printer")
|
||||
db.session.commit()
|
||||
|
||||
def _ensure_printer_types(self) -> None:
|
||||
"""Ensure basic printer types exist (new architecture)."""
|
||||
printer_types = [
|
||||
('Laser', 'Standard laser printer', 'printer'),
|
||||
('Inkjet', 'Inkjet printer', 'printer'),
|
||||
('Label', 'Label/barcode printer', 'barcode'),
|
||||
('MFP', 'Multifunction printer with scan/copy/fax', 'printer'),
|
||||
('Plotter', 'Large format plotter', 'drafting-compass'),
|
||||
('Thermal', 'Thermal printer', 'temperature-high'),
|
||||
('Dot Matrix', 'Dot matrix printer', 'th'),
|
||||
('Other', 'Other printer type', 'printer'),
|
||||
]
|
||||
|
||||
for name, description, icon in printer_types:
|
||||
existing = PrinterType.query.filter_by(printertype=name).first()
|
||||
if not existing:
|
||||
pt = PrinterType(
|
||||
printertype=name,
|
||||
description=description,
|
||||
icon=icon
|
||||
)
|
||||
db.session.add(pt)
|
||||
logger.debug(f"Created printer type: {name}")
|
||||
|
||||
db.session.commit()
|
||||
|
||||
def _ensure_legacy_machine_types(self) -> None:
|
||||
"""Ensure basic printer machine types exist (legacy architecture)."""
|
||||
printertypes = [
|
||||
('Laser Printer', 'Printer', 'Standard laser printer'),
|
||||
('Inkjet Printer', 'Printer', 'Inkjet printer'),
|
||||
|
||||
Reference in New Issue
Block a user