feat: Implement Smart Router workflow with User Input and Duplicate Logic (v0.18.0)
Major update to the scanning engine to support "Pause & Resume" workflows. The system can now halt execution to ask for user input (e.g. Weight) and resume processing seamlessly. Key Changes: - Backend (Global Actions): Added `OPEN_FORM` action type to pause pipeline and request manual input. - Backend (Routes): Updated `scan_lot` to handle `extra_data` payloads, allowing the pipeline to resume after user input. - Backend (Logic): Implemented `confirm_duplicate` gatekeeper to handle "Warn vs Block" logic dynamically. - Frontend (JS): Added `processSmartScan` to handle router signals (Open Modal, Warn Duplicate). - Frontend (JS): Added `saveSmartScanData` to send original barcode + new form data back to the engine. - UI: Fixed modal ID/Name conflicts (forcing use of `name` attribute for DB compatibility). - UI: Restored missing "Cancel" button to Details Modal. - Config: Added "User Input" rule type to the Rule Editor. Ver: 0.18.0
This commit is contained in:
125
global_actions.py
Normal file
125
global_actions.py
Normal file
@@ -0,0 +1,125 @@
|
||||
from db import query_db, execute_db
|
||||
from datetime import datetime
|
||||
|
||||
def execute_pipeline(actions, barcode, context):
|
||||
"""
|
||||
Executes the chain of actions defined in the Rule.
|
||||
Returns: {'success': bool, 'message': str, 'data': dict}
|
||||
"""
|
||||
field_values = {}
|
||||
should_save = False
|
||||
|
||||
for action in actions:
|
||||
atype = action.get('type')
|
||||
|
||||
# --- MAP (Extract) ---
|
||||
if atype == 'map':
|
||||
start = int(action.get('start', 1)) - 1
|
||||
end = int(action.get('end', len(barcode)))
|
||||
target = action.get('field')
|
||||
if target:
|
||||
safe_end = min(end, len(barcode))
|
||||
if start < len(barcode):
|
||||
field_values[target] = barcode[start:safe_end]
|
||||
|
||||
# --- CLEAN (Format) ---
|
||||
elif atype == 'clean':
|
||||
target = action.get('field')
|
||||
func = action.get('func')
|
||||
if target in field_values:
|
||||
val = str(field_values[target])
|
||||
if func == 'TRIM': field_values[target] = val.strip()
|
||||
elif func == 'REMOVE_SPACES': field_values[target] = val.replace(" ", "")
|
||||
elif func == 'UPPERCASE': field_values[target] = val.upper()
|
||||
elif func == 'REMOVE_LEADING_ZEROS': field_values[target] = val.lstrip('0')
|
||||
|
||||
# --- DUPLICATE CHECK (The Gatekeeper) ---
|
||||
elif atype == 'duplicate':
|
||||
target = action.get('field')
|
||||
behavior = action.get('behavior', 'WARN') # Default to WARN
|
||||
val = field_values.get(target)
|
||||
|
||||
if val:
|
||||
# 1. Check DB
|
||||
same_sess = query_db(f"SELECT id FROM {context['table_name']} WHERE {target} = ? AND session_id = ? AND is_deleted=0", [val, context['session_id']], one=True)
|
||||
other_sess = query_db(f"SELECT id FROM {context['table_name']} WHERE {target} = ? AND is_deleted=0", [val], one=True)
|
||||
|
||||
is_dup = False
|
||||
dup_msg = ""
|
||||
|
||||
if same_sess:
|
||||
is_dup = True
|
||||
dup_msg = f"Already scanned in THIS session ({val})"
|
||||
field_values['duplicate_status'] = 'dup_same_session'
|
||||
field_values['duplicate_info'] = 'Duplicate in same session'
|
||||
elif other_sess:
|
||||
is_dup = True
|
||||
dup_msg = f"Previously scanned in another session ({val})"
|
||||
field_values['duplicate_status'] = 'dup_other_session'
|
||||
field_values['duplicate_info'] = 'Duplicate from history'
|
||||
else:
|
||||
field_values['duplicate_status'] = 'normal'
|
||||
field_values['duplicate_info'] = None
|
||||
|
||||
# 2. Enforce Behavior
|
||||
if is_dup:
|
||||
if behavior == 'BLOCK':
|
||||
# STRICT MODE: Stop immediately.
|
||||
return {
|
||||
'success': False,
|
||||
'message': f"⛔ STRICT MODE: {dup_msg}. Entry denied.",
|
||||
'data': field_values
|
||||
}
|
||||
|
||||
elif behavior == 'WARN':
|
||||
# WARN MODE: Ask user, unless they already clicked "Yes"
|
||||
if not context.get('confirm_duplicate'):
|
||||
return {
|
||||
'success': False,
|
||||
'needs_confirmation': True,
|
||||
'message': f"⚠️ {dup_msg}",
|
||||
'data': field_values
|
||||
}
|
||||
# --- USER INPUT (The Gatekeeper) ---
|
||||
elif atype == 'input':
|
||||
# 1. Check if we received the manual data (Weight) from the Save button
|
||||
incoming_data = context.get('extra_data')
|
||||
|
||||
# 2. If data exists, MERGE it and CONTINUE (Don't stop!)
|
||||
if incoming_data:
|
||||
# Update our main data list with the user's input (e.g. weight=164)
|
||||
field_values.update(incoming_data)
|
||||
continue # <--- RESUME PIPELINE (Goes to next rule, usually SAVE)
|
||||
|
||||
# 3. If no data, STOP and ask for it
|
||||
return {
|
||||
'success': False,
|
||||
'needs_input': True,
|
||||
'message': 'Opening Details Form...',
|
||||
'data': field_values
|
||||
}
|
||||
|
||||
# --- SAVE MARKER ---
|
||||
elif atype == 'save':
|
||||
should_save = True
|
||||
|
||||
# --- RESULT ---
|
||||
if should_save:
|
||||
try:
|
||||
# Commit to DB
|
||||
cols = ['session_id', 'scanned_by', 'scanned_at']
|
||||
vals = [context['session_id'], context['user_id'], datetime.now()]
|
||||
|
||||
for k, v in field_values.items():
|
||||
cols.append(k)
|
||||
vals.append(v)
|
||||
|
||||
placeholders = ', '.join(['?'] * len(cols))
|
||||
sql = f"INSERT INTO {context['table_name']} ({', '.join(cols)}) VALUES ({placeholders})"
|
||||
execute_db(sql, vals)
|
||||
|
||||
return {'success': True, 'message': 'Saved Successfully', 'data': field_values}
|
||||
except Exception as e:
|
||||
return {'success': False, 'message': f"Database Error: {str(e)}", 'data': field_values}
|
||||
else:
|
||||
return {'success': True, 'message': f"✅ Parsed: {field_values} (No Save Action)", 'data': field_values}
|
||||
Reference in New Issue
Block a user