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:
@@ -185,6 +185,76 @@ def analyse(points, rise=REPLACEMENT_RISE, currentlevel=None):
|
||||
return result
|
||||
|
||||
|
||||
# Urgency bands. The report groups by these and the order list is drawn from
|
||||
# the first two, so they are defined once here rather than in the view - a
|
||||
# heading that disagrees with what got added to the list is worse than either.
|
||||
SOON_DAYS = 14
|
||||
MONTH_DAYS = 30
|
||||
|
||||
# What goes on the order list. Two weeks is the horizon that survives a
|
||||
# delivery: ordering only what is already empty means running empty.
|
||||
ORDER_HORIZON_DAYS = SOON_DAYS
|
||||
|
||||
BANDS = ('empty', 'soon', 'month', 'later')
|
||||
|
||||
|
||||
def band(daysleft):
|
||||
"""Which urgency band a cartridge belongs in, or None with no estimate."""
|
||||
if daysleft is None:
|
||||
return None
|
||||
if daysleft <= 0:
|
||||
return 'empty'
|
||||
if daysleft <= SOON_DAYS:
|
||||
return 'soon'
|
||||
if daysleft <= MONTH_DAYS:
|
||||
return 'month'
|
||||
return 'later'
|
||||
|
||||
|
||||
def orderlist(cartridges, horizon=ORDER_HORIZON_DAYS):
|
||||
"""What to buy, grouped by part number: [{partnumber, quantity, ...}].
|
||||
|
||||
The report's whole purpose reduced to a list someone can hand to
|
||||
purchasing. 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 makes you count by hand.
|
||||
|
||||
A cartridge with no part mapped is still listed, under its printer's model
|
||||
and colour. Dropping it would quietly shorten the order.
|
||||
"""
|
||||
groups = {}
|
||||
for cartridge in cartridges:
|
||||
if cartridge.get('daysleft') is None or cartridge['daysleft'] > horizon:
|
||||
continue
|
||||
parts = cartridge.get('partnumbers') or []
|
||||
# Several capacity tiers can match; the first is the standard one and
|
||||
# is what the low-supplies report shows first too.
|
||||
partnumber = parts[0]['partnumber'] if parts else None
|
||||
key = (partnumber, cartridge.get('color'), cartridge.get('model'))
|
||||
group = groups.setdefault(key, {
|
||||
'partnumber': partnumber,
|
||||
'color': cartridge.get('color'),
|
||||
'supplytype': cartridge.get('supplytype'),
|
||||
'model': cartridge.get('model'),
|
||||
'marketingname': parts[0].get('marketingname') if parts else None,
|
||||
'alternates': [p['partnumber'] for p in parts[1:]],
|
||||
'quantity': 0,
|
||||
'printers': [],
|
||||
})
|
||||
group['quantity'] += 1
|
||||
group['printers'].append({
|
||||
'printerid': cartridge.get('printerid'),
|
||||
'printername': cartridge.get('printername'),
|
||||
'daysleft': cartridge.get('daysleft'),
|
||||
})
|
||||
|
||||
# Unmapped parts last: they need a decision before they can be ordered.
|
||||
return sorted(groups.values(),
|
||||
key=lambda g: (g['partnumber'] is None,
|
||||
-g['quantity'],
|
||||
g['partnumber'] or ''))
|
||||
|
||||
|
||||
def soonest(supplies):
|
||||
"""Days-left of the supply that runs out first, or None if none estimate.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user