Files
shopdb-flask/plugins/printers/services/supply_parts.py
cproudlock f1b3b65532 Zabbix supply backend rebuild + data-driven model supplies
Rewrite the printer Zabbix integration (Bearer auth, host-by-IP, tag-based
supply lookup, ping) and replace the hardcoded toner table with a
modelsupplies table + CRUD + seed. Add mock Zabbix server, live test
harness, and the Playwright screenshot tooling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 08:35:02 -04:00

108 lines
3.4 KiB
Python

"""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
# alert thresholds (percent remaining)
CRITICAL_THRESHOLD = 5
LOW_THRESHOLD = 10
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]