printers: printer installer map + install-batch endpoint

Rebuilds the classic printer-installer feature: pick printers on the shopfloor
map, download a .bat that installs them.

Backend (asset_routes.py): GET /api/printers/install-batch?printerids=1,2,3
returns a .bat attachment. Groups printers the way the classic installprinter.asp
did - HP/Xerox via the universal PrinterInstaller.exe /PRINTER="a,b,c", printers
with a .exe installpath via that installer /SILENT, and anything else (no
installpath, or a .zip) listed for manual install instead of being run blindly.
Download URLs derive from the site_base_url setting + the IIS-served /installers
folder (no hardcoded host). Reuses the existing install-list query shape.

Frontend: PrinterInstallerMap.vue - full-screen Leaflet shopfloor map (reuses
mapConfig), a marker per network printer at its mapx/mapy, click to toggle-select,
sidebar with the selection + an Install button that downloads the batch. Toplevel
route /printer-installer, printersApi.installList(), and an Installer Map button
on the printers list.

Tests: install-batch grouping (universal/specific/manual) + requires-ids.
This commit is contained in:
cproudlock
2026-07-29 12:38:51 -04:00
parent 1ee9328bf9
commit 0d40780f53
6 changed files with 503 additions and 2 deletions

View File

@@ -0,0 +1,77 @@
"""Tests for the printer installer batch endpoint (/api/printers/install-batch).
The printer installer map lets a user pick printers on the shopfloor map and
download a .bat that installs them, grouping the same way the classic
installprinter.asp did (HP/Xerox -> universal PrinterInstaller.exe; other
printers with a .exe installpath -> that installer /SILENT; anything else ->
manual).
"""
import pytest
@pytest.fixture
def printer_assettype(db):
from shopdb.core.models import AssetType
at = AssetType(assettype='printer', pluginname='printers',
tablename='printers', description='Printers')
db.session.add(at)
db.session.commit()
return at
def _vendor(db, name):
from shopdb.core.models import Vendor
v = Vendor(vendor=name)
db.session.add(v)
db.session.commit()
return v
def _make_printer(client, auth_headers, **payload):
resp = client.post('/api/printers', json=payload, headers=auth_headers)
assert resp.status_code == 201, resp.get_json()
return resp.get_json()['data']['printer']['printerid']
def test_install_batch_groups_universal_specific_and_manual(
client, db, auth_headers, printer_assettype):
hp = _vendor(db, 'HP')
zebra = _vendor(db, 'Zebra')
epson = _vendor(db, 'Epson')
# HP -> universal installer
hp_id = _make_printer(client, auth_headers, assetnumber='CSF04-WJRP2035-HP',
hostname='wjprn04', vendorid=hp.vendorid)
# Zebra with a .exe installpath -> specific silent install
zebra_id = _make_printer(client, auth_headers, assetnumber='LABELER-ZEBRA',
hostname='wjprn05', vendorid=zebra.vendorid,
installpath='./installers/printers/zddriver.exe')
# Epson with a .zip installpath -> manual (we do not run a .zip /SILENT)
epson_id = _make_printer(client, auth_headers, assetnumber='RECEIPT-EPSON',
hostname='wjprn06', vendorid=epson.vendorid,
installpath='./installers/printers/c350navi.zip')
ids = '%d,%d,%d' % (hp_id, zebra_id, epson_id)
resp = client.get('/api/printers/install-batch?printerids=' + ids,
headers=auth_headers)
assert resp.status_code == 200
assert 'application/octet-stream' in resp.headers['Content-Type']
assert '.bat' in resp.headers['Content-Disposition']
bat = resp.get_data(as_text=True)
# Universal group: HP printer name in a /PRINTER list + PrinterInstaller.exe
assert 'PrinterInstaller.exe' in bat
assert '/PRINTER="CSF04-WJRP2035-HP"' in bat
# Specific: the .exe installpath resolved + run /SILENT
assert 'installers/printers/zddriver.exe' in bat
assert '/SILENT' in bat
# Manual: the .zip printer is flagged, NOT executed
assert 'MANUAL' in bat
assert 'RECEIPT-EPSON' in bat
assert 'c350navi.zip' not in bat # never handed to the runner
def test_install_batch_requires_ids(client, db, auth_headers, printer_assettype):
resp = client.get('/api/printers/install-batch', headers=auth_headers)
assert resp.status_code == 400