printedparts stage 7: the kiosk - scan bin, scan badge, keypad, take
Two open endpoints: an item lookup by scanned code and the take POST - the product's first unauthenticated write, held to the decision record's bar (decrement-only, badge-attributed server-side, bounded, physically rate-limited; justification in the plugin README). The /parts-kiosk route is a full-screen no-auth view beside /shopfloor: a hidden always-focused input consumes keyboard-wedge scans for whichever step is active, TouchKeypad (net-new 3x4 grid) takes the quantity, and a success screen resets after a few seconds. Manual type-in fallbacks cover damaged labels. Kiosk test proves open access, the over-take guard, the badge policy, and cache==ledger afterward.
This commit is contained in:
@@ -295,3 +295,54 @@ def adjust_item(item_id: int):
|
||||
http_code=422)
|
||||
_ledger_write(item, 'adjust', quantitychange, sso, name, reason=reason)
|
||||
return success_response(item.to_dict(), message='Stock adjusted')
|
||||
|
||||
|
||||
# --- kiosk: UNauthenticated by decision record --------------------------------
|
||||
# The take endpoint is the product's first open WRITE. The proposal's decision
|
||||
# record sets the bar it must meet: decrement-only, badge-attributed, bounded,
|
||||
# physically rate-limited. It can reduce stock of an active item and nothing
|
||||
# else; identity comes from the badge resolved server-side, never the client.
|
||||
|
||||
@printedparts_bp.route('/kiosk/item/<itemcode>', methods=['GET'])
|
||||
def kiosk_item(itemcode):
|
||||
"""Item summary for a scanned bin barcode (open read for the kiosk)."""
|
||||
item = PrintedItem.query.filter(
|
||||
PrintedItem.itemcode == itemcode.strip(),
|
||||
PrintedItem.isactive == True).first()
|
||||
if not item:
|
||||
return error_response(ErrorCodes.NOT_FOUND,
|
||||
'No part matches that barcode', http_code=404)
|
||||
return success_response(item.to_dict())
|
||||
|
||||
|
||||
@printedparts_bp.route('/kiosk/take', methods=['POST'])
|
||||
def kiosk_take():
|
||||
"""Take parts from a bin. Body: {itemcode, badge, quantity}."""
|
||||
data = request.get_json() or {}
|
||||
|
||||
item = PrintedItem.query.filter(
|
||||
PrintedItem.itemcode == (data.get('itemcode') or '').strip(),
|
||||
PrintedItem.isactive == True).first()
|
||||
if not item:
|
||||
return error_response(ErrorCodes.NOT_FOUND,
|
||||
'No part matches that barcode', http_code=404)
|
||||
|
||||
quantity = data.get('quantity')
|
||||
if not isinstance(quantity, int) or quantity < 1:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR,
|
||||
'Enter how many you are taking')
|
||||
if quantity > item.quantityonhand:
|
||||
return error_response(
|
||||
ErrorCodes.VALIDATION_ERROR,
|
||||
f'Only {item.quantityonhand} on hand - take fewer or see the '
|
||||
f'parts team')
|
||||
|
||||
try:
|
||||
sso, name = resolve_badge(data.get('badge'))
|
||||
except BadgeError as badge_error:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, str(badge_error),
|
||||
http_code=422)
|
||||
|
||||
_ledger_write(item, 'take', -quantity, sso, name)
|
||||
return success_response(item.to_dict(),
|
||||
message=f'Took {quantity}, {item.quantityonhand} left')
|
||||
|
||||
Reference in New Issue
Block a user