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

105
QUICKSTART_WEB.md Normal file
View File

@@ -0,0 +1,105 @@
# Quick Start - Web Application
Get up and running with the InvenTree Stock Tool web interface in 5 minutes!
## Step 1: Install
```bash
cd stocktool
uv sync
```
## Step 2: Run
```bash
uv run stock-tool-web
```
You should see:
```
✅ Connected to InvenTree: https://your-server.com
🚀 Starting InvenTree Stock Tool Web Server...
📱 Open your browser to: http://localhost:5000
Press Ctrl+C to stop
```
## Step 3: Open Browser
Navigate to: **http://localhost:5000**
## Step 4: Start Using
### Quick Workflow:
1. **Select Location**
- Scan location barcode (e.g., `INV-SL123`)
- OR select from dropdown
2. **Choose Mode**
- Click "Add Stock", "Update Stock", "Check Stock", or "Locate Part"
3. **Scan Parts**
- Click in scan field
- Scan barcode or type part code
- Press Enter
### Unknown Parts:
When you scan a part that doesn't exist:
- ✅ It's automatically added to "Pending Imports"
- ✅ Import runs in background
- ✅ You can keep scanning other parts!
- ✅ Part is processed automatically when ready
## Barcode Commands
Scan these to control the app:
- `MODE:ADD` → Add Stock mode
- `MODE:CHECK` → Check Stock mode
- `LOCATE` → Locate Part mode
- `CHANGE_LOCATION` → Clear location
## Access from Phone/Tablet
1. Find your computer's IP:
```bash
# Windows
ipconfig
# Linux/Mac
hostname -I
```
2. On your device, open browser to:
```
http://YOUR_IP:5000
```
Example: `http://192.168.1.100:5000`
## Troubleshooting
### Can't connect?
- Check firewall allows port 5000
- Make sure server is running
- Try http://127.0.0.1:5000
### Import not working?
- Ensure `inventree-part-import` is installed
- Check your InvenTree API token has permissions
### WebSocket issues?
- Clear browser cache
- Try different browser
- Check browser console for errors (F12)
## What's Next?
- Read full docs: [WEB_APP.md](WEB_APP.md)
- Desktop version: `uv run stock-tool`
- Report issues: https://github.com/your-repo/issues
---
**That's it! You're ready to scan! 📱✨**

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 };

86
test_barcode_analyze.py Normal file
View File

@@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""
Analyze barcode structure
"""
test_barcode = "[)>06PSAM9019-ND1PJL-100-25-T30PSAM9019-NDK1K9530640910K1172401379D25291T34755734000711K14LCNQ1811ZPICK12Z268520113Z99999920Z00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
print("Barcode structure analysis:")
print(f"Total length: {len(test_barcode)}")
print()
# Look for patterns
import re
# Find all capital letters followed by content
pattern = r'([A-Z]+)([^A-Z]*)'
matches = re.findall(pattern, test_barcode)
print("Pattern matches (Letter + Content):")
for i, (letter, content) in enumerate(matches[:20]):
if content:
print(f" {letter}: '{content}'")
print("\n" + "="*70)
# Check if there are specific field markers
markers = ['30P', '1P', 'Q', 'K', 'D', 'T', 'Z', 'P']
for marker in markers:
if marker in test_barcode:
idx = test_barcode.find(marker)
print(f"Found '{marker}' at position {idx}: {test_barcode[idx:idx+20]}")
print("\n" + "="*70)
# Try to identify the structure
# [)>06 is ANSI MH10.8.2 format
if test_barcode.startswith('[)>06'):
print("This is ANSI MH10.8.2 barcode format")
print("Format: [)>06 + Format Identifier + GS + Data + RS + EOT")
# The data section should start after the format header
data_start = 5 # After [)>06
# Look for common field identifiers
print("\nSearching for field identifiers:")
# Common identifiers in ANSI format:
# P = Part Number
# 1P = Supplier Part Number
# 30P = Customer Part Number
# Q = Quantity
# K = Batch/Lot
# Try a different approach - look for explicit markers
text = test_barcode[5:] # Skip header
# Find 30P marker
if '30P' in text:
idx = text.index('30P')
after_30p = text[idx+3:]
# Extract until next capital letter or known marker
part_match = re.match(r'([A-Z0-9\-]+)', after_30p)
if part_match:
print(f" Part (30P): {part_match.group(1)}")
# Find 1P marker
if '1P' in text:
idx = text.index('1P')
after_1p = text[idx+2:]
part_match = re.match(r'([A-Z0-9\-]+)', after_1p)
if part_match:
print(f" Supplier Part (1P): {part_match.group(1)}")
# Find P (might be part number)
if test_barcode.startswith('[)>06P'):
after_p = test_barcode[6:]
part_match = re.match(r'([A-Z0-9\-]+)', after_p)
if part_match:
print(f" Part (P): {part_match.group(1)}")
# Find Q for quantity
if 'Q' in text:
# Look for Q followed by digits
q_matches = re.findall(r'Q(\d+)', text)
if q_matches:
print(f" Quantity (Q): {q_matches}")

44
test_barcode_parsing.py Normal file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""
Test barcode parsing with real-world examples
"""
from stocktool.stock_tool_gui_v2 import parse_scan
# Your actual barcode
test_barcode = "[)>06PSAM9019-ND1PJL-100-25-T30PSAM9019-NDK1K9530640910K1172401379D25291T34755734000711K14LCNQ1811ZPICK12Z268520113Z99999920Z00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
print("Testing barcode parsing...")
print(f"Raw barcode length: {len(test_barcode)}")
print(f"Raw barcode: {test_barcode[:100]}...")
print()
# Parse it
part_code, quantity = parse_scan(test_barcode)
print("Parsed Results:")
print(f" Part Code: {part_code}")
print(f" Quantity: {quantity}")
print()
# Show the hex representation to see separators
print("Checking for separators:")
for i, char in enumerate(test_barcode):
if char in ['\x1D', '\x1E']:
print(f" Found separator at position {i}: \\x{ord(char):02X}")
# Try to manually parse to see what's there
print("\nManual analysis:")
import re
SEP_RE = re.compile(r'[\x1D\x1E]')
fields = SEP_RE.split(test_barcode)
print(f"Number of fields after split: {len(fields)}")
for i, field in enumerate(fields[:10]): # Show first 10 fields
print(f" Field {i}: '{field}'")
if field.startswith('30P'):
print(f" → Part code: {field[3:]}")
elif field.lower().startswith('1p'):
print(f" → Part code (1P): {field[2:]}")
elif field.lower().startswith('q') and field[1:].isdigit():
print(f" → Quantity: {field[1:]}")