fix: Robust barcode scan handling for ANSI MH10.8.2 and InvenTree 1.x
- Capture scanner control keystrokes (Ctrl+]/^/\/_ → GS/RS/FS/US) in the scan input so ANSI MH10.8.2 field separators survive the HTML input filter, eliminating the Q-quantity-vs-next-DI ambiguity. - Fall back to a DI-aware lazy regex when separators are stripped (e.g. pasted scans), so Q digits stop at the next data identifier instead of greedily eating into 11Z/12Z/etc. - Make pending-part dicts JSON-serializable by isoformat-ing the timestamp; without this the worker's import_complete socket emit threw and the entry was never removed from the queue, causing every re-scan to 400 with "already queued" forever. - Make /api/part/import idempotent: a re-scan of an already-queued part updates qty/location and returns 200 with already_queued=true instead of 400. - Surface search/queue errors in the client log instead of silently swallowing them, and stop treating a 500 from /api/part/search as "not found" (which was causing re-queue loops). - Log full tracebacks for /api/part/search failures and split the get_part_info / get_part_parameters error paths so failures can be attributed. - Migrate get_part_parameters to the InvenTree 1.x endpoint /api/parameter/?model_type=part.part&model_id=<id>. The old /api/part/parameter/?part=<id> returns 404 on this instance, and even on the new endpoint the ?part= filter is silently ignored (would have returned every parameter in the database). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -290,20 +290,28 @@ def get_part_info(host: str, token: str, part_id: int) -> Dict[str, Any]:
|
||||
def get_part_parameters(host: str, token: str, part_id: int) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get part parameters from InvenTree API.
|
||||
|
||||
|
||||
Args:
|
||||
host: InvenTree server URL
|
||||
token: API authentication token
|
||||
part_id: Part ID to get parameters for
|
||||
|
||||
|
||||
Returns:
|
||||
List of parameter dictionaries
|
||||
"""
|
||||
url = f"{host}/api/part/parameter/"
|
||||
# InvenTree 1.x consolidated parameters under /api/parameter/ keyed by
|
||||
# generic (model_type, model_id). The old /api/part/parameter/?part=<id>
|
||||
# endpoint no longer exists, and just `?part=<id>` on the new endpoint
|
||||
# is silently ignored (returns every parameter in the database).
|
||||
url = f"{host}/api/parameter/"
|
||||
headers = {'Authorization': f'Token {token}'}
|
||||
resp = requests.get(url, headers=headers, params={'part': part_id})
|
||||
resp = requests.get(
|
||||
url,
|
||||
headers=headers,
|
||||
params={'model_type': 'part.part', 'model_id': part_id},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
|
||||
|
||||
data = resp.json()
|
||||
return data.get('results', data) if isinstance(data, dict) and 'results' in data else data
|
||||
|
||||
@@ -461,12 +469,26 @@ def parse_scan(raw: str) -> Tuple[Optional[str], Optional[int]]:
|
||||
part = None
|
||||
qty = None
|
||||
|
||||
# Extract part number - it's after [)>06P and before the next field marker
|
||||
# Look for common field markers: 1P, 30P (these are explicit markers)
|
||||
# If GS/RS separators survived, parse field-by-field — unambiguous.
|
||||
fields = SEP_RE.split(raw)
|
||||
if len(fields) > 1:
|
||||
for f in fields:
|
||||
if not f:
|
||||
continue
|
||||
if f.startswith('30P'):
|
||||
part = clean_part_code(f[3:])
|
||||
elif f.startswith('1P'):
|
||||
if not part:
|
||||
part = clean_part_code(f[2:])
|
||||
elif f.startswith('Q') and f[1:].isdigit():
|
||||
qty = int(f[1:])
|
||||
if part or qty:
|
||||
return part, qty
|
||||
|
||||
# Separators stripped — extract part after [)>06P up to next field marker.
|
||||
if len(raw) > 6 and raw[5] == 'P':
|
||||
after_p = raw[6:]
|
||||
|
||||
# Find the next explicit field marker
|
||||
markers_to_find = ['1P', '30P']
|
||||
end_idx = len(after_p)
|
||||
|
||||
@@ -477,11 +499,11 @@ def parse_scan(raw: str) -> Tuple[Optional[str], Optional[int]]:
|
||||
|
||||
part = clean_part_code(after_p[:end_idx])
|
||||
|
||||
# Extract quantity after Q
|
||||
if 'Q' in raw:
|
||||
q_matches = re.findall(r'Q(\d+)', raw)
|
||||
if q_matches:
|
||||
qty = int(q_matches[0])
|
||||
# Lazy-match Q digits, stopping at the next data identifier
|
||||
# (e.g. 11Z, 12Z, 20Z, 1T, 9D, 4L) or end-of-string.
|
||||
q_match = re.search(r'Q(\d+?)(?=\d{0,2}[A-Z]|$)', raw)
|
||||
if q_match:
|
||||
qty = int(q_match.group(1))
|
||||
|
||||
if part or qty:
|
||||
return part, qty
|
||||
|
||||
Reference in New Issue
Block a user