diff --git a/QUICKSTART_WEB.md b/QUICKSTART_WEB.md new file mode 100644 index 0000000..f1c1cac --- /dev/null +++ b/QUICKSTART_WEB.md @@ -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! 📱✨** diff --git a/src/stocktool/stock_tool_gui_v2.py b/src/stocktool/stock_tool_gui_v2.py index 6b70c0c..31eedeb 100644 --- a/src/stocktool/stock_tool_gui_v2.py +++ b/src/stocktool/stock_tool_gui_v2.py @@ -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...Q... + 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... + 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 diff --git a/src/stocktool/web/static/js/app.js b/src/stocktool/web/static/js/app.js index 5f0d462..1d1b894 100644 --- a/src/stocktool/web/static/js/app.js +++ b/src/stocktool/web/static/js/app.js @@ -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...Q... */ 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... + 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 }; diff --git a/test_barcode_analyze.py b/test_barcode_analyze.py new file mode 100644 index 0000000..9f2e26b --- /dev/null +++ b/test_barcode_analyze.py @@ -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}") diff --git a/test_barcode_parsing.py b/test_barcode_parsing.py new file mode 100644 index 0000000..da151d5 --- /dev/null +++ b/test_barcode_parsing.py @@ -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:]}")