fix: Add support for ANSI MH10.8.2 barcode format

Fixes barcode parsing for ANSI MH10.8.2 format barcodes that don't use GS/RS separators.

Problem:
- Barcodes like [)>06PSAM9019-ND1PJL-100-25-T... were not being parsed
- Only separator-based and JSON formats were supported
- User's real-world barcodes were being added to queue as raw strings

Solution:
- Added ANSI MH10.8.2 format detection ([)>06 prefix)
- Extract part code between P and first field marker (1P, 30P)
- Extract quantity from Q<digits> pattern
- Updated both desktop and web app parsing logic

Tested with real barcode:
- Input: [)>06PSAM9019-ND1PJL-100-25-T30PSAM9019-NDK1...Q1811...
- Parsed: Part=SAM9019-ND, Qty=1811 

Files Changed:
- src/stocktool/stock_tool_gui_v2.py - Enhanced parse_scan()
- src/stocktool/web/static/js/app.js - Enhanced parseBarcode()
- test_barcode_parsing.py - Test script for validation
- test_barcode_analyze.py - Barcode structure analysis tool
- QUICKSTART_WEB.md - Quick start guide for web app

Supported Formats Now:
1. JSON-like: {PM:PART-CODE,QTY:10}
2. Separator-based: GS/RS (\x1D, \x1E) separated fields
3. ANSI MH10.8.2: [)>06P<part>...Q<qty>... (NEW!)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-29 11:29:20 +07:00
parent 03b7a4b0c8
commit 8dadd66f45
5 changed files with 353 additions and 8 deletions

View File

@@ -416,6 +416,11 @@ def parse_scan(raw: str) -> Tuple[Optional[str], Optional[int]]:
"""
Parse scanned barcode to extract part code and quantity.
Supports multiple formats:
- JSON-like: {PM:PART-CODE,QTY:10}
- Separator-based: Fields separated by GS/RS (\x1D, \x1E)
- ANSI MH10.8.2: [)>06P<part>...Q<qty>...
Args:
raw: Raw barcode string
@@ -451,6 +456,36 @@ def parse_scan(raw: str) -> Tuple[Optional[str], Optional[int]]:
pass
return part, qty
# Handle ANSI MH10.8.2 format: [)>06P<part>...
if raw.startswith('[)>06'):
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 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)
for marker in markers_to_find:
idx = after_p.find(marker)
if idx != -1 and idx < end_idx:
end_idx = idx
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])
if part or qty:
return part, qty
# Handle separator-based format
part = None
qty = None

View File

@@ -205,19 +205,94 @@ function stockApp() {
/**
* Parse barcode input
*
* Supports multiple formats:
* - JSON-like: {PM:PART-CODE,QTY:10}
* - Separator-based: Fields separated by GS/RS (\x1D, \x1E)
* - ANSI MH10.8.2: [)>06P<part>...Q<qty>...
*/
async parseBarcode(raw) {
// Simple parsing - can be enhanced
let part_code = raw;
let part_code = null;
let quantity = null;
// Check for JSON-like format
if (raw.startsWith('{') && raw.includes('}')) {
const match = raw.match(/pm:([^,}]+)/i);
if (match) part_code = match[1];
/**
* Clean part code by removing non-ASCII characters
*/
const cleanPartCode = (code) => {
// Keep only ASCII printable characters (32-126)
return code.split('').filter(char => {
const charCode = char.charCodeAt(0);
return charCode >= 32 && charCode <= 126;
}).join('').trim();
};
const qtyMatch = raw.match(/qty:(\d+)/i);
if (qtyMatch) quantity = parseInt(qtyMatch[1]);
// Handle JSON-like format: {PM:PART-CODE,QTY:10}
if (raw.startsWith('{') && raw.includes('}')) {
const content = raw.trim().slice(1, -1);
for (const kv of content.split(',')) {
if (!kv.includes(':')) continue;
const [k, v] = kv.split(':', 2);
const key = k.trim().toUpperCase();
const val = v.trim();
if (key === 'PM') {
part_code = cleanPartCode(val);
} else if (key === 'QTY') {
const parsed = parseInt(val);
if (!isNaN(parsed)) quantity = parsed;
}
}
return { part_code, quantity };
}
// Handle ANSI MH10.8.2 format: [)>06P<part>...
if (raw.startsWith('[)>06')) {
// Extract part number - it's after [)>06P and before the next field marker
if (raw.length > 6 && raw[5] === 'P') {
const afterP = raw.substring(6);
// Find the next explicit field marker (1P or 30P)
const markers = ['1P', '30P'];
let endIdx = afterP.length;
for (const marker of markers) {
const idx = afterP.indexOf(marker);
if (idx !== -1 && idx < endIdx) {
endIdx = idx;
}
}
part_code = cleanPartCode(afterP.substring(0, endIdx));
}
// Extract quantity after Q
const qMatch = raw.match(/Q(\d+)/);
if (qMatch) {
quantity = parseInt(qMatch[1]);
}
return { part_code, quantity };
}
// Handle separator-based format (GS/RS separators: \x1D or \x1E)
const sepRegex = /[\x1D\x1E]/g;
const fields = raw.split(sepRegex);
for (const field of fields) {
if (!field) continue;
// Part code patterns: 30P, 1P
if (field.startsWith('30P')) {
part_code = cleanPartCode(field.substring(3));
} else if (field.toLowerCase().startsWith('1p')) {
part_code = cleanPartCode(field.substring(2));
}
// Quantity pattern: Q followed by digits
else if (field.toLowerCase().startsWith('q') && /^\d+$/.test(field.substring(1))) {
quantity = parseInt(field.substring(1));
}
}
return { part_code, quantity };