"""Printer supply classification and part-number lookup. Part numbers now live in the modelsupplies table (see seed_supplies.py), managed through the API/UI. This module keeps the runtime logic that is not per-model data: classifying a reported level into ok/low/critical (waste cartridges invert), deriving supply type and color from a Zabbix item name, and reading the matching part numbers out of the database. """ from typing import Dict, List, Optional # display thresholds (percent remaining) for the ok/low/critical report badge CRITICAL_THRESHOLD = 5 LOW_THRESHOLD = 10 # toner email-alert tiers (percent remaining). Distinct from the display # badge above: a warning email fires at or below TONER_WARNING_THRESHOLD, a # critical email at or below TONER_CRITICAL_THRESHOLD (empty). TONER_WARNING_THRESHOLD = 5 TONER_CRITICAL_THRESHOLD = 0 def alerttier(remaining: float, warning: float = TONER_WARNING_THRESHOLD, critical: float = TONER_CRITICAL_THRESHOLD) -> str: """Map a toner percent-remaining to an email-alert tier. critical at or below the critical threshold (default 0 = empty), warning at or below the warning threshold (default 5), else ok. Both are configurable (printers_alert_critical_threshold / printers_alert_warning_threshold).""" if remaining <= critical: return 'critical' if remaining <= warning: return 'warning' return 'ok' # tier severity rank; an alert fires only when the rank increases (worsens) TIER_RANK = {'ok': 0, 'warning': 1, 'critical': 2} def derivesupplytype(name: str) -> str: """Map a Zabbix item name to a supply type.""" lowername = (name or "").lower() if "waste" in lowername: return "waste" if "drum" in lowername or "imaging" in lowername: return "drum" if "maintenance" in lowername or "fuser" in lowername: return "maintenance" return "toner" def derivecolor(name: str, tagcolor: Optional[str] = None) -> str: """Best-effort supply color from a Zabbix color tag, then the item name.""" color = (tagcolor or "").lower() if "black" in color: return "black" if color in ("cyan", "magenta", "yellow"): return color if color in ("grey", "gray"): return "gray" lowername = (name or "").lower() for candidate in ("cyan", "magenta", "yellow", "black"): if candidate in lowername: return candidate return "none" def classifysupply(level: float, name: str, vendor: Optional[str]) -> Dict: """Classify one supply item into ok/low/critical. Waste cartridge fill is inverted vs a toner level: a full waste cartridge is bad. Standard vendors report waste as percent FULL (high = bad). Xerox EC/AltaLink series report waste as percent capacity REMAINING (low = bad), same direction as toner. Normalise everything to percent remaining first. """ lowername = (name or "").lower() iswaste = "waste" in lowername isdrum = "drum" in lowername or "imaging" in lowername isxerox = bool(vendor) and "xerox" in vendor.lower() if iswaste and not isxerox: remaining = 100 - level else: remaining = level if remaining <= CRITICAL_THRESHOLD: status = "critical" elif remaining <= LOW_THRESHOLD: status = "low" else: status = "ok" return { "status": status, "remaining": round(remaining, 1), "iswaste": iswaste, "isdrum": isdrum, } def lookupsupplies(modelnumberid: Optional[int], color: str, supplytype: str) -> List[Dict]: """Part-number options for a model + color + supply type, from the DB. Returns every matching capacity tier (standard / high / metered / ...) so the report can show all reorder options, like the classic report did. """ if not modelnumberid: return [] from ..models import ModelSupply query = ModelSupply.query.filter_by( modelnumberid=modelnumberid, supplytype=supplytype, isactive=True, ) # toners are color-specific; drum/waste/maintenance are not if supplytype == 'toner' and color and color != 'none': query = query.filter_by(color=color) rows = query.order_by(ModelSupply.capacitytier).all() return [{ 'partnumber': row.partnumber, 'marketingname': row.marketingname, 'capacitytier': row.capacitytier, 'pageyield': row.pageyield, } for row in rows]