Badge resolver copied from the USB contract (SSO digits, 0<digits>BZ PayNo wrap) with names from the employees directory and the unknown-badge policy setting; deliberately copied rather than cross-imported so the contract test stays green. Restock and adjust write the ledger row and move the cached quantity in one commit - the single-commit invariant every write path must use. Adjust requires a reason and refuses to drive stock below zero. Detail page gains Restock/Adjust modals. Seven tests cover minting, the cache==ledger invariant, badge shapes, policy toggle, and auth.
70 lines
2.3 KiB
Python
70 lines
2.3 KiB
Python
"""Badge resolution for the printedparts plugin.
|
|
|
|
Same input contract as the USB plugin (deliberately copied, not imported -
|
|
cross-plugin imports break the shopdb.api-only contract):
|
|
|
|
- all digits -> an SSO typed or scanned directly
|
|
- 0<digits>BZ -> a physical badge wrapping a PayNo (keyboard-wedge
|
|
scanners emit this shape)
|
|
- anything else -> unresolvable
|
|
|
|
Names come from the employees plugin's self-hosted directory, looked up by
|
|
SSO. The directory carries no PayNo column, so PayNo badges resolve only when
|
|
the wrapped digits are themselves the SSO (true at sites whose badges encode
|
|
the SSO); otherwise they fall to the unknown-badge policy.
|
|
|
|
Policy (Setting printedparts_unknown_badge): 'deny' (default) rejects a badge
|
|
with no directory match; 'allow' records the SSO with an empty name.
|
|
"""
|
|
|
|
import re
|
|
|
|
from shopdb.api import Setting
|
|
|
|
_PAYNO_BADGE = re.compile(r'^0(\d+)BZ$', re.IGNORECASE)
|
|
|
|
|
|
class BadgeError(ValueError):
|
|
"""Raised when a badge cannot be accepted under the site policy."""
|
|
|
|
|
|
def _directory_name(sso):
|
|
"""Best-effort display name from the employees plugin directory."""
|
|
try:
|
|
from plugins.employees.models import DirectoryEmployee
|
|
from shopdb.api import db
|
|
if sso and str(sso).isdigit():
|
|
employee = db.session.get(DirectoryEmployee, int(sso))
|
|
if employee:
|
|
return f'{employee.firstname} {employee.lastname}'.strip()
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
def resolve_badge(badge):
|
|
"""Return (sso, name) for a scanned badge, enforcing the site policy.
|
|
|
|
Raises BadgeError with a kiosk-displayable message when the badge shape is
|
|
unrecognized or the policy denies an unmatched badge.
|
|
"""
|
|
badge = (badge or '').strip()
|
|
if not badge:
|
|
raise BadgeError('Scan or enter a badge')
|
|
|
|
if badge.isdigit():
|
|
sso = badge
|
|
else:
|
|
match = _PAYNO_BADGE.match(badge)
|
|
if not match:
|
|
raise BadgeError('Unrecognized badge format')
|
|
sso = match.group(1)
|
|
|
|
name = _directory_name(sso)
|
|
if name is None:
|
|
policy = (Setting.get('printedparts_unknown_badge') or 'deny').lower()
|
|
if policy != 'allow':
|
|
raise BadgeError('Badge not recognized - see the parts team')
|
|
return sso, ''
|
|
return sso, name
|