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:
105
QUICKSTART_WEB.md
Normal file
105
QUICKSTART_WEB.md
Normal 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! 📱✨**
|
||||
@@ -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
|
||||
|
||||
@@ -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
86
test_barcode_analyze.py
Normal 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
44
test_barcode_parsing.py
Normal 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:]}")
|
||||
Reference in New Issue
Block a user