printedparts stage 5: the ledger - restock/adjust with badge attribution
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.
This commit is contained in:
1
plugins/printedparts/services/__init__.py
Normal file
1
plugins/printedparts/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Printedparts plugin services."""
|
||||
69
plugins/printedparts/services/badges.py
Normal file
69
plugins/printedparts/services/badges.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user