Merge remote-tracking branch 'origin/main' into wilm

# Conflicts:
#	shopdb/__init__.py
This commit is contained in:
benjamin.baker
2026-08-28 09:30:35 -04:00
112 changed files with 5502 additions and 604 deletions

View File

@@ -30,7 +30,7 @@ printers_asset_bp = Blueprint('printers_asset', __name__)
# =============================================================================
@printers_asset_bp.route('/types', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def list_printer_types():
"""List all printer types."""
page, per_page = get_pagination_params(request)
@@ -52,7 +52,7 @@ def list_printer_types():
@printers_asset_bp.route('/types/<int:type_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def get_printer_type(type_id: int):
"""Get a single printer type."""
t = db.session.get(PrinterType, type_id)
@@ -68,7 +68,6 @@ def get_printer_type(type_id: int):
@printers_asset_bp.route('/types', methods=['POST'])
@jwt_required()
@require_permission('printers.create')
def create_printer_type():
"""Create a new printer type."""
@@ -105,7 +104,6 @@ def create_printer_type():
@printers_asset_bp.route('/types/<int:type_id>', methods=['PUT'])
@jwt_required()
@require_permission('printers.edit')
def update_printer_type(type_id: int):
"""Update a printer type."""
@@ -129,7 +127,6 @@ def update_printer_type(type_id: int):
@printers_asset_bp.route('/types/<int:type_id>', methods=['DELETE'])
@jwt_required()
@require_permission('printers.delete')
def delete_printer_type(type_id: int):
"""Delete a printer type. Refused if any printer still uses it."""
@@ -150,7 +147,7 @@ def delete_printer_type(type_id: int):
# =============================================================================
@printers_asset_bp.route('/drivers', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def list_drivers():
"""List printer drivers. ?active=false includes inactive ones."""
query = PrinterDriver.query
@@ -161,7 +158,6 @@ def list_drivers():
@printers_asset_bp.route('/drivers', methods=['POST'])
@jwt_required()
@require_permission('printers.create')
def create_driver():
data = request.get_json() or {}
@@ -180,7 +176,6 @@ def create_driver():
@printers_asset_bp.route('/drivers/<int:driver_id>', methods=['PUT'])
@jwt_required()
@require_permission('printers.edit')
def update_driver(driver_id):
d = db.session.get(PrinterDriver, driver_id)
@@ -197,7 +192,6 @@ def update_driver(driver_id):
@printers_asset_bp.route('/drivers/<int:driver_id>', methods=['DELETE'])
@jwt_required()
@require_permission('printers.delete')
def delete_driver(driver_id):
d = db.session.get(PrinterDriver, driver_id)
@@ -213,7 +207,7 @@ def delete_driver(driver_id):
# =============================================================================
@printers_asset_bp.route('', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def list_printers():
"""
List all printers with filtering and pagination.
@@ -970,7 +964,7 @@ def printers_for_host(hostname: str):
@printers_asset_bp.route('/assignments/for-asset/<int:asset_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def get_asset_printer_assignment(asset_id: int):
"""What THIS asset is assigned, without inheritance.
@@ -993,7 +987,6 @@ def get_asset_printer_assignment(asset_id: int):
@printers_asset_bp.route('/assignments/for-asset/<int:asset_id>', methods=['PUT'])
@jwt_required()
@require_permission('printers.edit')
def set_asset_printer_assignment(asset_id: int):
"""Reconcile one asset's whole printer assignment in a single call.
@@ -1498,7 +1491,6 @@ def _observed_host_block(hostname, asset, index):
@printers_asset_bp.route('/observed/<hostname>', methods=['GET'])
@jwt_required()
@require_permission('printers.view')
def observed_for_host(hostname: str):
"""What a host last reported, each queue judged against what it is assigned.
@@ -1536,7 +1528,6 @@ def observed_for_host(hostname: str):
@printers_asset_bp.route('/observed/for-asset/<int:asset_id>', methods=['GET'])
@jwt_required()
@require_permission('printers.view')
def observed_for_asset(asset_id: int):
"""The same comparison, reached from an asset page: one block per host.
@@ -1649,7 +1640,6 @@ def _seed_source_hostnames(asset):
@printers_asset_bp.route('/assignments/seed-from-observed/<int:asset_id>',
methods=['POST'])
@jwt_required()
@require_permission('printers.edit')
def seed_assignment_from_observed(asset_id: int):
"""Write what a PC observed as the assignment of the asset in the path.
@@ -1788,7 +1778,7 @@ def seed_assignment_from_observed(asset_id: int):
@printers_asset_bp.route('/<int:printer_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def get_printer(printer_id: int):
"""Get a single printer with full details."""
printer = db.session.get(Printer, printer_id)
@@ -1821,7 +1811,7 @@ def get_printer(printer_id: int):
@printers_asset_bp.route('/by-asset/<int:asset_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def get_printer_by_asset(asset_id: int):
"""Get printer data by asset ID."""
printer = Printer.query.filter_by(assetid=asset_id).first()
@@ -1840,7 +1830,6 @@ def get_printer_by_asset(asset_id: int):
@printers_asset_bp.route('', methods=['POST'])
@jwt_required()
@require_permission('printers.create')
def create_printer():
"""
@@ -1944,7 +1933,6 @@ def create_printer():
@printers_asset_bp.route('/<int:printer_id>', methods=['PUT'])
@jwt_required()
@require_permission('printers.edit')
def update_printer(printer_id: int):
"""Update printer (both Asset and Printer records)."""
@@ -2031,7 +2019,6 @@ def update_printer(printer_id: int):
@printers_asset_bp.route('/<int:printer_id>', methods=['DELETE'])
@jwt_required()
@require_permission('printers.delete')
def delete_printer(printer_id: int):
"""Delete (soft delete) printer."""
@@ -2056,7 +2043,7 @@ def delete_printer(printer_id: int):
# =============================================================================
@printers_asset_bp.route('/<int:printer_id>/supplies', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def get_printer_supplies(printer_id: int):
"""Get supply levels from Zabbix (real-time lookup)."""
printer = db.session.get(Printer, printer_id)
@@ -2251,7 +2238,7 @@ def _get_low_supplies_data():
@printers_asset_bp.route('/lowsupplies', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def low_supplies():
"""Get printers with low or critical supply levels."""
data = _get_low_supplies_data()
@@ -2259,7 +2246,7 @@ def low_supplies():
@printers_asset_bp.route('/lookup', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def printer_lookup():
"""Find a printer by IP or FQDN. Parity with the classic printerlookup.asp.
@@ -2303,7 +2290,6 @@ def printer_lookup():
@printers_asset_bp.route('/supplies/refresh', methods=['POST'])
@jwt_required()
@require_permission('printers.create')
def refresh_supplies_cache():
"""Clear cached Zabbix supply data so the next read pulls fresh values.
@@ -2320,7 +2306,7 @@ def refresh_supplies_cache():
# =============================================================================
@printers_asset_bp.route('/dashboard/summary', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def dashboard_summary():
"""Get printer dashboard summary data."""
# Total active printers
@@ -2394,7 +2380,7 @@ def _validate_supply_payload(data):
@printers_asset_bp.route('/supplies/meta', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def supplies_meta():
"""Allowed values for supply type, color, and capacity tier (for the UI)."""
return success_response({
@@ -2405,7 +2391,7 @@ def supplies_meta():
@printers_asset_bp.route('/models', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def list_supply_models():
"""List models with a supply count, for the supply-management picker.
@@ -2464,7 +2450,7 @@ def list_supply_models():
@printers_asset_bp.route('/models/<int:modelnumberid>/supplies', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def list_model_supplies(modelnumberid: int):
"""List all supplies mapped to a model."""
model = db.session.get(Model, modelnumberid)
@@ -2485,7 +2471,6 @@ def list_model_supplies(modelnumberid: int):
@printers_asset_bp.route('/models/<int:modelnumberid>/supplies', methods=['POST'])
@jwt_required()
@require_permission('printers.create')
def create_model_supply(modelnumberid: int):
"""Add a supply to a model."""
@@ -2526,7 +2511,6 @@ def create_model_supply(modelnumberid: int):
@printers_asset_bp.route('/supplies/<int:modelsupplyid>', methods=['PUT'])
@jwt_required()
@require_permission('printers.edit')
def update_model_supply(modelsupplyid: int):
"""Update a model supply."""
@@ -2571,7 +2555,6 @@ def update_model_supply(modelsupplyid: int):
@printers_asset_bp.route('/supplies/<int:modelsupplyid>', methods=['DELETE'])
@jwt_required()
@require_permission('printers.delete')
def delete_model_supply(modelsupplyid: int):
"""Delete a model supply."""
@@ -2615,7 +2598,6 @@ def _reordertip(supply):
@printers_asset_bp.route('/dashboard/supplies', methods=['GET'])
@jwt_required()
@require_permission('printers.view')
def dashboard_supplies():
"""Printers needing a cartridge, flattened to one row per printer.
@@ -2685,7 +2667,7 @@ def dashboard_supplies():
# =============================================================================
@printers_asset_bp.route('/supplies/forecast', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def supplies_forecast():
"""Days-to-empty and replacement counts per printer.

View File

@@ -4,7 +4,7 @@
<h1>Toner Report</h1>
<div class="header-actions">
<button v-if="!loading && !error" class="btn btn-secondary" @click="exportCSV">Export CSV</button>
<EmailReportButton v-if="!loading && !error" subject="Toner / Supply Report"
<EmailReportButton v-if="!loading && !error && canemail" subject="Toner / Supply Report"
:columns="emailColumns" :rows="emailRows" />
<router-link to="/reports" class="btn btn-secondary">Back to Reports</router-link>
</div>
@@ -133,9 +133,16 @@
import { ref, computed, onMounted } from 'vue'
import { printersApi } from '@/api'
import EmailReportButton from '@/components/EmailReportButton.vue'
import { useAuthStore } from '@/stores/auth'
import LocationMapTooltip from '@/components/LocationMapTooltip.vue'
import { getPrinterHostnameTemplate } from '@/utils/siteSettings'
// Emailing a report is the reports.export permission the endpoint enforces.
// Offering the button to somebody who will only get a 403 is worse than not
// offering it.
const authStore = useAuthStore()
const canemail = computed(() => authStore.hasPermission('reports.export'))
const emailColumns = [
{ key: 'printer', label: 'Printer' },
{ key: 'hostname', label: 'Hostname' },

View File

@@ -385,7 +385,9 @@ class PrintersPlugin(BasePlugin):
'name': 'Printers',
'icon': 'printer',
'route': '/printers',
'permission': 'printers.view',
'position': 20,
'section': 'assets',
},
]