refactor: Convert to UV Python project with proper package structure
- Restructured project to use src/stocktool package layout - Migrated to UV for dependency management - Added pyproject.toml with all dependencies (sv-ttk, pillow, requests, pyyaml) - Organized test files into tests/ directory - Updated .gitignore for UV projects - Comprehensive README with installation and usage instructions - Removed old unused files (main.py, stock_tool_gui.py, duplicate copy) - Added CLI entry point: stock-tool command 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
67
tests/test_add_stock.py
Normal file
67
tests/test_add_stock.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test script to verify the add stock response handling."""
|
||||
|
||||
def handle_add_stock_response(response_data):
|
||||
"""
|
||||
Simulate the add stock response handling logic.
|
||||
|
||||
Args:
|
||||
response_data: The response from POST /api/stock/
|
||||
|
||||
Returns:
|
||||
Stock item ID
|
||||
"""
|
||||
if isinstance(response_data, list):
|
||||
# If it's a list, get the first item
|
||||
stock_item = response_data[0] if response_data else {}
|
||||
else:
|
||||
# If it's a dict, use it directly
|
||||
stock_item = response_data
|
||||
|
||||
sid = stock_item.get('pk', stock_item.get('id'))
|
||||
return sid
|
||||
|
||||
|
||||
print("Testing add stock response handling:\n")
|
||||
|
||||
# Test 1: Response is a dict with 'pk' key
|
||||
print("Test 1: API returns dict with 'pk'")
|
||||
response = {'pk': 123, 'quantity': 150, 'part': 456, 'location': 476}
|
||||
sid = handle_add_stock_response(response)
|
||||
print(f" Response: {response}")
|
||||
print(f" Stock ID: {sid}")
|
||||
print(f" Expected: 123, Got: {sid} - {'PASS' if sid == 123 else 'FAIL'}\n")
|
||||
|
||||
# Test 2: Response is a dict with 'id' key
|
||||
print("Test 2: API returns dict with 'id'")
|
||||
response = {'id': 456, 'quantity': 150, 'part': 789, 'location': 476}
|
||||
sid = handle_add_stock_response(response)
|
||||
print(f" Response: {response}")
|
||||
print(f" Stock ID: {sid}")
|
||||
print(f" Expected: 456, Got: {sid} - {'PASS' if sid == 456 else 'FAIL'}\n")
|
||||
|
||||
# Test 3: Response is a list with dict containing 'pk'
|
||||
print("Test 3: API returns list with dict containing 'pk'")
|
||||
response = [{'pk': 789, 'quantity': 150, 'part': 123, 'location': 476}]
|
||||
sid = handle_add_stock_response(response)
|
||||
print(f" Response: {response}")
|
||||
print(f" Stock ID: {sid}")
|
||||
print(f" Expected: 789, Got: {sid} - {'PASS' if sid == 789 else 'FAIL'}\n")
|
||||
|
||||
# Test 4: Response is a list with dict containing 'id'
|
||||
print("Test 4: API returns list with dict containing 'id'")
|
||||
response = [{'id': 999, 'quantity': 150, 'part': 123, 'location': 476}]
|
||||
sid = handle_add_stock_response(response)
|
||||
print(f" Response: {response}")
|
||||
print(f" Stock ID: {sid}")
|
||||
print(f" Expected: 999, Got: {sid} - {'PASS' if sid == 999 else 'FAIL'}\n")
|
||||
|
||||
# Test 5: Empty response
|
||||
print("Test 5: API returns empty dict")
|
||||
response = {}
|
||||
sid = handle_add_stock_response(response)
|
||||
print(f" Response: {response}")
|
||||
print(f" Stock ID: {sid}")
|
||||
print(f" Expected: None, Got: {sid} - {'PASS' if sid is None else 'FAIL'}\n")
|
||||
|
||||
print("All tests completed!")
|
||||
106
tests/test_duplicate_handling.py
Normal file
106
tests/test_duplicate_handling.py
Normal file
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to demonstrate the updated _add_stock logic.
|
||||
This shows how duplicates are prevented by updating existing stock items.
|
||||
"""
|
||||
|
||||
def simulate_add_stock(part_id, location_id, quantity, existing_stock_items):
|
||||
"""
|
||||
Simulate the add stock workflow.
|
||||
|
||||
Args:
|
||||
part_id: Part ID
|
||||
location_id: Location ID
|
||||
quantity: Quantity to add
|
||||
existing_stock_items: List of existing stock items (for simulation)
|
||||
|
||||
Returns:
|
||||
Tuple of (action, stock_id, old_stock, new_stock)
|
||||
"""
|
||||
# Find existing stock item for this part at this location
|
||||
existing = None
|
||||
for item in existing_stock_items:
|
||||
if item['part'] == part_id and item['location'] == location_id:
|
||||
existing = item
|
||||
break
|
||||
|
||||
if existing:
|
||||
# Stock item exists - add to it
|
||||
stock_id = existing['pk']
|
||||
old_stock = existing['quantity']
|
||||
new_stock = old_stock + quantity
|
||||
|
||||
# Update the existing item (simulate)
|
||||
existing['quantity'] = new_stock
|
||||
|
||||
return ('updated', stock_id, old_stock, new_stock)
|
||||
else:
|
||||
# No existing stock - create new item
|
||||
new_item = {
|
||||
'pk': len(existing_stock_items) + 100, # Simulate new ID
|
||||
'part': part_id,
|
||||
'location': location_id,
|
||||
'quantity': quantity
|
||||
}
|
||||
existing_stock_items.append(new_item)
|
||||
|
||||
return ('created', new_item['pk'], 0, quantity)
|
||||
|
||||
|
||||
print("Testing Add Stock Duplicate Prevention\n")
|
||||
print("=" * 60)
|
||||
|
||||
# Simulate existing stock items
|
||||
stock_items = []
|
||||
|
||||
print("\nScenario 1: Adding stock when NO existing item exists")
|
||||
print("-" * 60)
|
||||
action, sid, old, new = simulate_add_stock(part_id=456, location_id=476, quantity=150, existing_stock_items=stock_items)
|
||||
print(f"Action: {action}")
|
||||
print(f"Stock ID: {sid}")
|
||||
print(f"Stock change: {old} -> {new} (+{new - old})")
|
||||
print(f"Total stock items: {len(stock_items)}")
|
||||
print(f"Expected: Created new stock item PASS" if action == 'created' else "FAIL")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("\nScenario 2: Adding MORE stock to EXISTING item (should UPDATE, not duplicate)")
|
||||
print("-" * 60)
|
||||
print(f"Current stock items before scan: {len(stock_items)}")
|
||||
action, sid, old, new = simulate_add_stock(part_id=456, location_id=476, quantity=100, existing_stock_items=stock_items)
|
||||
print(f"Action: {action}")
|
||||
print(f"Stock ID: {sid}")
|
||||
print(f"Stock change: {old} -> {new} (+{new - old})")
|
||||
print(f"Total stock items: {len(stock_items)}")
|
||||
print(f"Expected: Updated existing item, NO duplicate PASS" if action == 'updated' and len(stock_items) == 1 else "FAIL")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("\nScenario 3: Adding stock AGAIN (should continue to UPDATE)")
|
||||
print("-" * 60)
|
||||
print(f"Current stock items before scan: {len(stock_items)}")
|
||||
action, sid, old, new = simulate_add_stock(part_id=456, location_id=476, quantity=50, existing_stock_items=stock_items)
|
||||
print(f"Action: {action}")
|
||||
print(f"Stock ID: {sid}")
|
||||
print(f"Stock change: {old} -> {new} (+{new - old})")
|
||||
print(f"Total stock items: {len(stock_items)}")
|
||||
print(f"Expected: Updated existing item, still NO duplicate PASS" if action == 'updated' and len(stock_items) == 1 else "FAIL")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("\nScenario 4: Adding same part to DIFFERENT location (should create new)")
|
||||
print("-" * 60)
|
||||
print(f"Current stock items before scan: {len(stock_items)}")
|
||||
action, sid, old, new = simulate_add_stock(part_id=456, location_id=999, quantity=75, existing_stock_items=stock_items)
|
||||
print(f"Action: {action}")
|
||||
print(f"Stock ID: {sid}")
|
||||
print(f"Stock change: {old} -> {new} (+{new - old})")
|
||||
print(f"Total stock items: {len(stock_items)}")
|
||||
print(f"Expected: Created new item for different location PASS" if action == 'created' and len(stock_items) == 2 else "FAIL")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("\nFinal Summary:")
|
||||
print("-" * 60)
|
||||
for i, item in enumerate(stock_items, 1):
|
||||
print(f"Stock Item {i}: ID={item['pk']}, Part={item['part']}, Location={item['location']}, Qty={item['quantity']}")
|
||||
|
||||
print("\nSUCCESS All scenarios demonstrate proper duplicate prevention!")
|
||||
print(" - Same part + same location = UPDATE existing")
|
||||
print(" - Same part + different location = CREATE new")
|
||||
95
tests/test_parse_fix.py
Normal file
95
tests/test_parse_fix.py
Normal file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test script to verify parse_scan fix for encoding issues."""
|
||||
|
||||
from typing import Tuple, Optional
|
||||
import re
|
||||
|
||||
SEP_RE = re.compile(r'[\x1D\x1E]')
|
||||
|
||||
def parse_scan(raw: str) -> Tuple[Optional[str], Optional[int]]:
|
||||
"""
|
||||
Parse scanned barcode to extract part code and quantity.
|
||||
|
||||
Args:
|
||||
raw: Raw barcode string
|
||||
|
||||
Returns:
|
||||
Tuple of (part_code, quantity) or (None, None) if parsing fails
|
||||
"""
|
||||
def clean_part_code(code: str) -> str:
|
||||
"""Clean part code by removing non-ASCII and invalid characters."""
|
||||
# Keep only ASCII printable characters (excluding control chars)
|
||||
# This removes characters like ¡ and other encoding artifacts
|
||||
cleaned = ''.join(char for char in code if 32 <= ord(char) <= 126)
|
||||
return cleaned.strip()
|
||||
|
||||
# Handle JSON-like format
|
||||
if raw.startswith('{') and '}' in raw:
|
||||
content = raw.strip()[1:-1]
|
||||
part = None
|
||||
qty = None
|
||||
|
||||
for kv in content.split(','):
|
||||
if ':' not in kv:
|
||||
continue
|
||||
k, v = kv.split(':', 1)
|
||||
key = k.strip().upper()
|
||||
val = v.strip()
|
||||
|
||||
if key == 'PM':
|
||||
part = clean_part_code(val)
|
||||
elif key == 'QTY':
|
||||
try:
|
||||
qty = int(val)
|
||||
except ValueError:
|
||||
pass
|
||||
return part, qty
|
||||
|
||||
# Handle separator-based format
|
||||
part = None
|
||||
qty = None
|
||||
fields = SEP_RE.split(raw)
|
||||
|
||||
for f in fields:
|
||||
if not f:
|
||||
continue
|
||||
if f.startswith('30P'):
|
||||
part = clean_part_code(f[3:])
|
||||
elif f.lower().startswith('1p'):
|
||||
part = clean_part_code(f[2:])
|
||||
elif f.lower().startswith('q') and f[1:].isdigit():
|
||||
qty = int(f[1:])
|
||||
|
||||
return part, qty
|
||||
|
||||
|
||||
# Test with the provided scan data
|
||||
test_scan = "{pbn:PICK251017100019,on:WM2510170196,pc:C18548292,pm:STHW4-DU-HS24041¡,qty:150,mc:,cc:1,pdi:180368458,hp:null,wc:JS}"
|
||||
|
||||
print("Testing parse_scan with problematic barcode data:")
|
||||
print(f"Input: {test_scan}")
|
||||
print()
|
||||
|
||||
part, qty = parse_scan(test_scan)
|
||||
|
||||
print(f"Parsed part code: '{part}'")
|
||||
print(f"Parsed quantity: {qty}")
|
||||
print()
|
||||
|
||||
# Verify the fix
|
||||
expected_part = "STHW4-DU-HS24041"
|
||||
expected_qty = 150
|
||||
|
||||
if part == expected_part and qty == expected_qty:
|
||||
print("✅ SUCCESS! Part code is correctly parsed.")
|
||||
print(f" Expected: '{expected_part}'")
|
||||
print(f" Got: '{part}'")
|
||||
else:
|
||||
print("❌ FAILURE! Parse result doesn't match expected values.")
|
||||
print(f" Expected part: '{expected_part}', Got: '{part}'")
|
||||
print(f" Expected qty: {expected_qty}, Got: {qty}")
|
||||
|
||||
# Show character codes for verification
|
||||
print("\nCharacter analysis of parsed part code:")
|
||||
for i, char in enumerate(part or ""):
|
||||
print(f" [{i}] '{char}' (ASCII {ord(char)})")
|
||||
77
tests/test_stock_level.py
Normal file
77
tests/test_stock_level.py
Normal file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test script to verify find_stock_item and get_stock_level work correctly."""
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
def find_stock_item(data_response: Any) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Simulate find_stock_item parsing logic.
|
||||
|
||||
Args:
|
||||
data_response: The response from the API
|
||||
|
||||
Returns:
|
||||
Stock item dictionary or None if not found
|
||||
"""
|
||||
data = data_response
|
||||
results = data.get('results', data) if isinstance(data, dict) else data
|
||||
return results[0] if results else None
|
||||
|
||||
|
||||
def get_stock_level(item: Optional[Dict[str, Any]]) -> float:
|
||||
"""
|
||||
Get stock level from item.
|
||||
|
||||
Args:
|
||||
item: Stock item dictionary
|
||||
|
||||
Returns:
|
||||
Stock quantity
|
||||
"""
|
||||
return item.get('quantity', 0) if item else 0
|
||||
|
||||
|
||||
# Test cases
|
||||
print("Test 1: API returns dict with 'results' key")
|
||||
api_response_1 = {
|
||||
'results': [
|
||||
{'pk': 123, 'quantity': 50, 'part': 456, 'location': 789}
|
||||
]
|
||||
}
|
||||
item = find_stock_item(api_response_1)
|
||||
stock = get_stock_level(item)
|
||||
print(f" Item: {item}")
|
||||
print(f" Stock level: {stock}")
|
||||
print(f" Expected: 50, Got: {stock} - {'PASS' if stock == 50 else 'FAIL'}")
|
||||
print()
|
||||
|
||||
print("Test 2: API returns dict with empty results")
|
||||
api_response_2 = {'results': []}
|
||||
item = find_stock_item(api_response_2)
|
||||
stock = get_stock_level(item)
|
||||
print(f" Item: {item}")
|
||||
print(f" Stock level: {stock}")
|
||||
print(f" Expected: 0, Got: {stock} - {'PASS' if stock == 0 else 'FAIL'}")
|
||||
print()
|
||||
|
||||
print("Test 3: API returns list directly")
|
||||
api_response_3 = [
|
||||
{'pk': 124, 'quantity': 100, 'part': 456, 'location': 789}
|
||||
]
|
||||
item = find_stock_item(api_response_3)
|
||||
stock = get_stock_level(item)
|
||||
print(f" Item: {item}")
|
||||
print(f" Stock level: {stock}")
|
||||
print(f" Expected: 100, Got: {stock} - {'PASS' if stock == 100 else 'FAIL'}")
|
||||
print()
|
||||
|
||||
print("Test 4: API returns empty list")
|
||||
api_response_4 = []
|
||||
item = find_stock_item(api_response_4)
|
||||
stock = get_stock_level(item)
|
||||
print(f" Item: {item}")
|
||||
print(f" Stock level: {stock}")
|
||||
print(f" Expected: 0, Got: {stock} - {'PASS' if stock == 0 else 'FAIL'}")
|
||||
print()
|
||||
|
||||
print("All tests completed!")
|
||||
Reference in New Issue
Block a user