From 85f98e87cb9cf3959803ca1ccdbe3e4415bab4ba Mon Sep 17 00:00:00 2001 From: cproudlock Date: Fri, 26 Jun 2026 21:51:59 -0400 Subject: [PATCH] Add printer installer endpoint (data + floor-map positions) Shopfloor 2.0 PCs can't run unsigned .bat printer maps, so the signed printer installer EXE pulls printer data + floor-map positions from the API and renders the picker itself. Adds the endpoint the EXE consumes. GET /api/printers/install-list - flat, unpaginated list of NETWORK printers (USB-only excluded), each with: printerid, name, machinenumber, windowsname, sharename, hostname, ipaddress, vendorname, modelnumber, installpath, iscsf, locationname, mapx, mapy. Replaces the classic apiprinters.asp contract and adds the map position (mapx/mapy) the EXE needs. Tests: network printer appears with map position + installer fields; USB-only printer excluded. 188 tests pass, naming green. Co-Authored-By: Claude Opus 4.8 --- plugins/printers/api/asset_routes.py | 47 ++++++++++++++++++ .../test_plugins/test_printer_install_list.py | 48 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 tests/test_plugins/test_printer_install_list.py diff --git a/plugins/printers/api/asset_routes.py b/plugins/printers/api/asset_routes.py index d5f7c91..f662122 100644 --- a/plugins/printers/api/asset_routes.py +++ b/plugins/printers/api/asset_routes.py @@ -188,6 +188,53 @@ def list_printers(): return paginated_response(data, page, per_page, total) +@printers_asset_bp.route('/install-list', methods=['GET']) +@jwt_required(optional=True) +def printer_install_list(): + """Flat, unpaginated list of network printers for the printer installer. + + Shopfloor 2.0 PCs cannot run unsigned .bat maps, so the signed installer EXE + pulls printer data + floor-map positions from here and renders the picker. + Replaces the classic apiprinters.asp contract, adding mapx/mapy. Network + printers only (USB-only printers are excluded). + """ + rows = [] + query = db.session.query(Printer).join(Asset).filter(Asset.isactive == True) + for printer in query.all(): + asset = printer.asset + if not asset: + continue + primary = Communication.query.filter_by( + assetid=asset.assetid, isprimary=True).first() \ + or Communication.query.filter_by(assetid=asset.assetid).first() + ipaddress = primary.ipaddress if primary else None + + # Network printers only: must have a hostname or a non-USB IP. + is_network = bool(printer.hostname) or (ipaddress and ipaddress != 'USB') + if not is_network: + continue + + data = printer.to_dict() + rows.append({ + 'printerid': printer.printerid, + 'name': asset.name or asset.assetnumber, + 'machinenumber': asset.assetnumber, + 'windowsname': printer.windowsname, + 'sharename': printer.sharename, + 'hostname': printer.hostname, + 'ipaddress': ipaddress, + 'vendorname': data.get('vendorname'), + 'modelnumber': data.get('modelname'), + 'installpath': printer.installpath, + 'iscsf': printer.iscsf, + 'locationname': asset.location.locationname if asset.location else None, + 'mapx': asset.mapx, + 'mapy': asset.mapy, + }) + + return success_response(rows) + + @printers_asset_bp.route('/', methods=['GET']) @jwt_required(optional=True) def get_printer(printer_id: int): diff --git a/tests/test_plugins/test_printer_install_list.py b/tests/test_plugins/test_printer_install_list.py new file mode 100644 index 0000000..1a61a6e --- /dev/null +++ b/tests/test_plugins/test_printer_install_list.py @@ -0,0 +1,48 @@ +"""Tests for the printer installer endpoint (/api/printers/install-list). + +The signed printer installer EXE pulls network-printer data + floor-map +positions from here (shopfloor 2.0 cannot run unsigned .bat maps). +""" + +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 test_install_list_returns_network_printer_with_map(client, db, auth_headers, + printer_assettype): + """A network printer appears with flat installer fields incl map position.""" + created = client.post('/api/printers', json={ + 'assetnumber': 'CSF01-Materials-HP', + 'hostname': 'wjprn01', + 'installpath': r'\\srv\drivers\hp', + 'mapx': 120, 'mapy': 240, + }, headers=auth_headers) + assert created.status_code == 201, created.get_json() + + resp = client.get('/api/printers/install-list', headers=auth_headers) + assert resp.status_code == 200 + rows = resp.get_json()['data'] + row = next((r for r in rows if r['machinenumber'] == 'CSF01-Materials-HP'), None) + assert row is not None + assert row['hostname'] == 'wjprn01' + assert row['installpath'] == r'\\srv\drivers\hp' + assert row['mapx'] == 120 and row['mapy'] == 240 + + +def test_install_list_excludes_usb_only_printer(client, db, auth_headers, + printer_assettype): + """A printer with no hostname and no network IP is excluded.""" + client.post('/api/printers', json={'assetnumber': 'USB-LABELER'}, + headers=auth_headers) + resp = client.get('/api/printers/install-list', headers=auth_headers) + machinenumbers = [r['machinenumber'] for r in resp.get_json()['data']] + assert 'USB-LABELER' not in machinenumbers