Make the toner forecast an order, not a table
The report answers a purchasing question, and it was answering it in seven columns, two tables and a rowspan. What someone actually needs from it is a short list of what to buy. So it opens with that list, grouped by part number with a quantity. Two cartridges of the same part in different printers is a quantity of two, which is the number an order needs and the one a per-printer table made the reader count by hand. It covers what is empty plus what goes within a fortnight - ordering only what is already empty means running empty. There is a copy button, because it ends up pasted into a mail. Below it the cartridges sit in urgency bands rather than in one long list sorted by a number. The question is which pile a thing is in, and a pile that is empty is worth seeing as empty. Everything past "empty" starts collapsed; the order list above already covers the same ground in a tenth of the height. The row is a cartridge now, not a printer, so it can carry its own part number, its own level bar and its own countdown. Nesting supplies under a printer meant opening a printer to find out whether anything on it needed doing. Cartridges with no part mapped are counted on a single line rather than given one each. They cannot be dropped, since that would quietly shorten the order, and they cannot be ordered from here either - the job they represent is mapping them, which is one job however many there are. Bands and the order horizon are decided server-side, next to the arithmetic that produces them, so a heading cannot disagree with what got added to the list. Checked against a fleet of 43 dev printers with real part mappings, driven by a stub Zabbix - live Zabbix is not reachable from the dev box.
This commit is contained in:
@@ -1503,26 +1503,35 @@ def supplies_forecast():
|
||||
is returned separately with the reason, rather than sorted as though it
|
||||
were fine or dropped as though it did not exist.
|
||||
"""
|
||||
from ..services.supply_history import analyse, soonest
|
||||
from ..services.supply_history import (
|
||||
ORDER_HORIZON_DAYS, analyse, band, orderlist,
|
||||
)
|
||||
from ..services.supply_parts import (
|
||||
derivecolor, derivesupplytype, lookupsupplies,
|
||||
)
|
||||
|
||||
try:
|
||||
days = max(1, min(365, int(request.args.get('days', 90))))
|
||||
except (TypeError, ValueError):
|
||||
days = 90
|
||||
|
||||
empty = {
|
||||
'cartridges': [], 'unestimated': [], 'orderlist': [], 'days': days,
|
||||
'horizondays': ORDER_HORIZON_DAYS,
|
||||
}
|
||||
|
||||
service = ZabbixService()
|
||||
if not service.isconfigured or not service.isreachable:
|
||||
return success_response({
|
||||
'printers': [], 'unestimated': [], 'days': days,
|
||||
'available': False,
|
||||
'reason': 'Zabbix is not configured or not reachable',
|
||||
})
|
||||
return success_response(dict(
|
||||
empty, available=False,
|
||||
reason='Zabbix is not configured or not reachable'))
|
||||
|
||||
rows = (
|
||||
db.session.query(Printer, Asset, Communication, Vendor)
|
||||
db.session.query(Printer, Asset, Communication, Vendor, Model)
|
||||
.join(Asset, Asset.assetid == Printer.assetid)
|
||||
.join(Communication, Communication.assetid == Asset.assetid)
|
||||
.outerjoin(Vendor, Vendor.vendorid == Printer.vendorid)
|
||||
.outerjoin(Model, Model.modelnumberid == Printer.modelnumberid)
|
||||
.filter(Asset.isactive == True,
|
||||
Communication.ipaddress.isnot(None),
|
||||
Communication.ipaddress != '')
|
||||
@@ -1530,9 +1539,9 @@ def supplies_forecast():
|
||||
)
|
||||
|
||||
seen = set()
|
||||
estimated, unestimated = [], []
|
||||
cartridges, unestimated = [], []
|
||||
|
||||
for printer, asset, comm, vendor in rows:
|
||||
for printer, asset, comm, vendor, model in rows:
|
||||
if printer.printerid in seen:
|
||||
continue
|
||||
seen.add(printer.printerid)
|
||||
@@ -1543,7 +1552,9 @@ def supplies_forecast():
|
||||
itemids = [s['itemid'] for s in supplies if s.get('itemid')]
|
||||
history = service.getlevelhistory(itemids, days=days)
|
||||
|
||||
analysed = []
|
||||
# The cartridge is what gets ordered, so the cartridge is the row.
|
||||
# Nesting supplies under a printer made the reader unpack a printer to
|
||||
# find out whether anything on it needed doing.
|
||||
for supply in supplies:
|
||||
points = history.get(str(supply.get('itemid')), [])
|
||||
# The live read is the level the report shows, so it is also the
|
||||
@@ -1551,35 +1562,50 @@ def supplies_forecast():
|
||||
# row whose level and days-left came from different moments reads
|
||||
# as broken.
|
||||
detail = analyse(points, currentlevel=supply.get('level'))
|
||||
detail['name'] = supply.get('name')
|
||||
detail['color'] = supply.get('color')
|
||||
analysed.append(detail)
|
||||
|
||||
entry = {
|
||||
'printerid': printer.printerid,
|
||||
'printername': asset.name or printer.hostname or '',
|
||||
'assetnumber': asset.assetnumber or '',
|
||||
'ipaddress': comm.ipaddress,
|
||||
'vendor': vendor.vendor if vendor else None,
|
||||
'supplies': analysed,
|
||||
'daysleft': soonest(analysed),
|
||||
'replacements': sum(s['replacements'] for s in analysed),
|
||||
}
|
||||
(estimated if entry['daysleft'] is not None else unestimated).append(entry)
|
||||
name = supply.get('name') or 'Unknown'
|
||||
color = derivecolor(name, supply.get('color'))
|
||||
supplytype = derivesupplytype(name)
|
||||
detail.update({
|
||||
'name': name,
|
||||
'color': color,
|
||||
'supplytype': supplytype,
|
||||
'partnumbers': lookupsupplies(
|
||||
model.modelnumberid if model else None, color, supplytype),
|
||||
'printerid': printer.printerid,
|
||||
'printername': asset.name or printer.hostname or '',
|
||||
'assetnumber': asset.assetnumber or '',
|
||||
'ipaddress': comm.ipaddress,
|
||||
'vendor': vendor.vendor if vendor else None,
|
||||
'model': model.modelnumber if model else None,
|
||||
'band': band(detail['daysleft']),
|
||||
})
|
||||
# The chart series is per cartridge and nothing on this report
|
||||
# draws it yet; sending it multiplies the payload for nothing.
|
||||
detail.pop('points', None)
|
||||
(cartridges if detail['band'] else unestimated).append(detail)
|
||||
|
||||
# Soonest first: the point of the report is what to order next.
|
||||
estimated.sort(key=lambda p: p['daysleft'])
|
||||
unestimated.sort(key=lambda p: p['printername'])
|
||||
cartridges.sort(key=lambda c: (c['daysleft'], c['printername']))
|
||||
unestimated.sort(key=lambda c: (c['printername'], c['name']))
|
||||
|
||||
return success_response({
|
||||
'printers': estimated,
|
||||
'unestimated': unestimated,
|
||||
'days': days,
|
||||
'available': True,
|
||||
'summary': {
|
||||
'estimated': len(estimated),
|
||||
counts = {name: 0 for name in ('empty', 'soon', 'month', 'later')}
|
||||
for cartridge in cartridges:
|
||||
counts[cartridge['band']] += 1
|
||||
toorder = orderlist(cartridges)
|
||||
|
||||
return success_response(dict(
|
||||
empty,
|
||||
cartridges=cartridges,
|
||||
unestimated=unestimated,
|
||||
orderlist=toorder,
|
||||
available=True,
|
||||
summary={
|
||||
'bands': counts,
|
||||
'estimated': len(cartridges),
|
||||
'unestimated': len(unestimated),
|
||||
'replacements': sum(p['replacements'] for p in estimated + unestimated),
|
||||
'duewithin30': sum(1 for p in estimated if p['daysleft'] <= 30),
|
||||
'replacements': sum(c['replacements']
|
||||
for c in cartridges + unestimated),
|
||||
'toorder': sum(item['quantity'] for item in toorder),
|
||||
},
|
||||
})
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user