Initial commit: InvenTree Stock Tool v2
A comprehensive barcode scanning application for InvenTree inventory management. Features: - Multi-mode operation (Add/Update/Check/Locate stock) - Smart duplicate prevention when adding stock - Barcode scanning with automatic part code cleaning - Real-time server connection monitoring - Part information display with images - Debug mode for troubleshooting Fixes applied: - Fixed encoding issues with non-ASCII characters in barcodes - Fixed API response handling for list and dict formats - Implemented duplicate prevention using PATCH to update existing stock - Added comprehensive error handling and logging Includes test suite for verification of all fixes.
This commit is contained in:
53
.gitignore
vendored
Normal file
53
.gitignore
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Virtual Environment
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# Configuration files (may contain sensitive data)
|
||||
*.yaml
|
||||
*.yml
|
||||
!example_config.yaml
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Test files (optional - uncomment if you don't want to track tests)
|
||||
# test_*.py
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.bak
|
||||
*.cache
|
||||
196
FIXES_APPLIED.md
Normal file
196
FIXES_APPLIED.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# Stock Tool GUI v2 - Fixes Applied
|
||||
|
||||
## Summary
|
||||
Fixed three critical issues in `stock_tool_gui_v2.py`:
|
||||
1. ✅ Encoding issue with barcode part numbers
|
||||
2. ✅ API response handling causing "'list' object has no attribute 'get'" errors
|
||||
3. ✅ Duplicate stock items being created when adding to existing location
|
||||
|
||||
---
|
||||
|
||||
## Issue 1: Encoding Problem with Part Numbers
|
||||
|
||||
### Problem
|
||||
Raw scan data contained: `pm:STHW4-DU-HS24041¡`
|
||||
The non-ASCII characters (¡) were being included in the parsed part number.
|
||||
|
||||
### Root Cause
|
||||
The `parse_scan()` function was not cleaning/sanitizing the extracted part codes.
|
||||
|
||||
### Fix Applied
|
||||
Added `clean_part_code()` helper function that:
|
||||
- Filters out all non-ASCII characters
|
||||
- Keeps only printable ASCII characters (codes 32-126)
|
||||
- Removes encoding artifacts like `¡`
|
||||
|
||||
### Location
|
||||
File: `stock_tool_gui_v2.py`
|
||||
Function: `parse_scan()` (lines 294-348)
|
||||
|
||||
### Result
|
||||
- **Before**: `STHW4-DU-HS24041¡`
|
||||
- **After**: `STHW4-DU-HS24041` ✓
|
||||
|
||||
---
|
||||
|
||||
## Issue 2: API Response Handling Error
|
||||
|
||||
### Problem
|
||||
Error: `'list' object has no attribute 'get'`
|
||||
- Stock not being added to InvenTree
|
||||
- Stock levels not updating after adding items
|
||||
|
||||
### Root Cause
|
||||
The InvenTree API POST response can return either:
|
||||
- A dictionary: `{'pk': 123, 'quantity': 150, ...}`
|
||||
- A list: `[{'pk': 123, 'quantity': 150, ...}]`
|
||||
|
||||
The code was assuming it would always be a dict and calling `.get()` directly on the response, which failed when the API returned a list.
|
||||
|
||||
### Locations with the Bug
|
||||
1. `_add_stock()` function - line 806 (original)
|
||||
2. `_update_stock()` function - line 865 (original)
|
||||
3. Misplaced `get_stock_level()` function at top of file
|
||||
|
||||
### Fixes Applied
|
||||
|
||||
#### 1. Fixed `_add_stock()` function (lines 797-827)
|
||||
**Before:**
|
||||
```python
|
||||
r.raise_for_status()
|
||||
sid = r.json().get('pk', r.json().get('id')) # ❌ Fails if response is list
|
||||
```
|
||||
|
||||
**After:**
|
||||
```python
|
||||
r.raise_for_status()
|
||||
|
||||
# Handle response - might be list or dict
|
||||
response_data = r.json()
|
||||
if isinstance(response_data, list):
|
||||
stock_item = response_data[0] if response_data else {}
|
||||
else:
|
||||
stock_item = response_data
|
||||
|
||||
sid = stock_item.get('pk', stock_item.get('id')) # ✓ Works for both
|
||||
```
|
||||
|
||||
#### 2. Fixed `_update_stock()` function (lines 858-875)
|
||||
Applied the same fix for consistency.
|
||||
|
||||
#### 3. Moved `get_stock_level()` function (lines 256-270)
|
||||
- Removed misplaced definition from top of file (before imports)
|
||||
- Re-added in correct location after `find_stock_item()` function
|
||||
|
||||
---
|
||||
|
||||
## Issue 3: Duplicate Stock Items Created
|
||||
|
||||
### Problem
|
||||
When scanning a part that already exists at a storage location, the tool was creating a duplicate stock item instead of adding to the existing quantity.
|
||||
|
||||
**Example:**
|
||||
- Location C64_PSU has 150x STHW4-DU-HS24041
|
||||
- Scan barcode to add 100 more
|
||||
- **Bug**: Creates second stock item with 100 (now have two separate items)
|
||||
- **Expected**: Updates existing item to 250
|
||||
|
||||
### Root Cause
|
||||
The `_add_stock()` function was always creating a new stock item via POST without checking if one already existed at that location.
|
||||
|
||||
### Fix Applied (lines 791-845)
|
||||
|
||||
**Before:**
|
||||
```python
|
||||
def _add_stock(self, part_id: int, part_code: str, quantity: Optional[int]):
|
||||
# Always creates new stock item - WRONG!
|
||||
r = requests.post(f"{self.host}/api/stock/", ...)
|
||||
```
|
||||
|
||||
**After:**
|
||||
```python
|
||||
def _add_stock(self, part_id: int, part_code: str, quantity: Optional[int]):
|
||||
# Check if stock item already exists at this location
|
||||
existing = find_stock_item(self.host, self.token, part_id, self.current_loc)
|
||||
|
||||
if existing:
|
||||
# Stock exists - update the quantity by adding to it
|
||||
sid = existing.get('pk', existing.get('id'))
|
||||
new_stock = current_stock + quantity
|
||||
r = requests.patch(f"{self.host}/api/stock/{sid}/",
|
||||
json={'quantity': new_stock})
|
||||
# Updates existing item ✓
|
||||
else:
|
||||
# No existing stock - create new stock item
|
||||
r = requests.post(f"{self.host}/api/stock/", ...)
|
||||
# Creates new item ✓
|
||||
```
|
||||
|
||||
### Behavior Now
|
||||
- **Same part + same location** = Updates existing stock item (no duplicate)
|
||||
- **Same part + different location** = Creates new stock item (correct)
|
||||
- **New part** = Creates new stock item (correct)
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Files Created
|
||||
1. `test_parse_fix.py` - Verifies encoding fix
|
||||
2. `test_stock_level.py` - Verifies stock level retrieval
|
||||
3. `test_add_stock.py` - Verifies API response handling
|
||||
4. `test_duplicate_handling.py` - Verifies duplicate prevention logic
|
||||
|
||||
### Test Results
|
||||
All tests pass ✓
|
||||
|
||||
---
|
||||
|
||||
## Expected Behavior Now
|
||||
|
||||
### When scanning barcode:
|
||||
```
|
||||
{pbn:PICK251017100019,on:WM2510170196,pc:C18548292,pm:STHW4-DU-HS24041¡,qty:150,mc:,cc:1,pdi:180368458,hp:null,wc:JS}
|
||||
```
|
||||
|
||||
The tool will:
|
||||
1. ✅ Extract clean part number: `STHW4-DU-HS24041`
|
||||
2. ✅ Extract quantity: `150`
|
||||
3. ✅ Check if part already exists at current location
|
||||
4. ✅ If exists: Add to existing stock (no duplicate created)
|
||||
5. ✅ If new: Create new stock item
|
||||
6. ✅ Display updated stock levels
|
||||
7. ✅ Handle both dict and list API responses correctly
|
||||
|
||||
### Example Workflows
|
||||
|
||||
**Scenario 1: First time adding part to location**
|
||||
- Scan: Part STHW4-DU-HS24041, Qty: 150
|
||||
- Result: Creates new stock item
|
||||
- Log: `✔ Created new stock item for 'STHW4-DU-HS24041' → StockItem #123`
|
||||
- Log: `📊 Stock: 0 → 150 (+150)`
|
||||
|
||||
**Scenario 2: Adding more of same part to same location**
|
||||
- Scan: Part STHW4-DU-HS24041, Qty: 100
|
||||
- Result: Updates existing stock item #123
|
||||
- Log: `✔ Added 100× to existing 'STHW4-DU-HS24041' (StockItem #123)`
|
||||
- Log: `📊 Stock: 150 → 250 (+100)`
|
||||
- **No duplicate created!** ✓
|
||||
|
||||
**Scenario 3: Adding same part to different location**
|
||||
- Change location to C65_PSU
|
||||
- Scan: Part STHW4-DU-HS24041, Qty: 75
|
||||
- Result: Creates new stock item #124 at the new location
|
||||
- Log: `✔ Created new stock item for 'STHW4-DU-HS24041' → StockItem #124`
|
||||
- Log: `📊 Stock: 0 → 75 (+75)`
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
- `stock_tool_gui_v2.py` - Main application file
|
||||
|
||||
## Files Created
|
||||
- `test_parse_fix.py` - Test for encoding fix
|
||||
- `test_stock_level.py` - Test for stock level retrieval
|
||||
- `test_add_stock.py` - Test for API response handling
|
||||
- `FIXES_APPLIED.md` - This document
|
||||
219
README.md
Normal file
219
README.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# InvenTree Stock Tool
|
||||
|
||||
A comprehensive barcode scanning application for InvenTree inventory management with a modern GUI.
|
||||
|
||||

|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **Barcode Scanning**: Process barcodes in multiple formats (JSON-like, separator-based)
|
||||
- **Multiple Operation Modes**:
|
||||
- Add Stock: Create new stock items or add to existing ones
|
||||
- Update Stock: Manually update stock quantities
|
||||
- Check Stock: View current stock levels
|
||||
- Locate Part: Find all locations where a part is stored
|
||||
- **Smart Duplicate Prevention**: Automatically adds to existing stock items instead of creating duplicates
|
||||
- **Real-time Server Monitoring**: Visual connection status indicator
|
||||
- **Part Information Display**: Shows part details, parameters, and images
|
||||
- **Debug Mode**: Optional detailed logging for troubleshooting
|
||||
|
||||
## Screenshots
|
||||
|
||||
The application features a dark-themed interface with:
|
||||
- Location scanner
|
||||
- Mode selection (Add/Update/Check/Locate)
|
||||
- Part information display with images
|
||||
- Real-time activity logging
|
||||
- Server connection status
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.8 or higher
|
||||
- InvenTree server instance
|
||||
- Required Python packages:
|
||||
- `sv-ttk` - Modern themed tkinter widgets
|
||||
- `pillow` - Image handling
|
||||
- `requests` - HTTP API communication
|
||||
- `pyyaml` - Configuration file parsing
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone this repository:
|
||||
```bash
|
||||
git clone <your-gitea-url>/stocktool.git
|
||||
cd stocktool
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
pip install sv-ttk pillow requests pyyaml
|
||||
```
|
||||
|
||||
3. Create configuration file at `~/.config/scan_and_import.yaml`:
|
||||
```yaml
|
||||
host: https://your-inventree-server.com
|
||||
token: your-api-token-here
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Config File Location
|
||||
- **Linux/Mac**: `~/.config/scan_and_import.yaml`
|
||||
- **Windows**: `C:\Users\<username>\.config\scan_and_import.yaml`
|
||||
|
||||
### Config File Format
|
||||
```yaml
|
||||
host: https://inventree.example.com # Your InvenTree server URL (no trailing slash)
|
||||
token: abcdef1234567890 # Your InvenTree API token
|
||||
```
|
||||
|
||||
### Getting Your API Token
|
||||
1. Log into your InvenTree instance
|
||||
2. Navigate to Settings > User Settings > API Tokens
|
||||
3. Create a new token or copy an existing one
|
||||
|
||||
## Usage
|
||||
|
||||
### Starting the Application
|
||||
```bash
|
||||
python stock_tool_gui_v2.py
|
||||
```
|
||||
|
||||
### Basic Workflow
|
||||
|
||||
1. **Set Location**: Scan a location barcode (format: `INV-SL<location_id>`)
|
||||
2. **Select Mode**: Choose operation mode (Add/Update/Check/Locate)
|
||||
3. **Scan Parts**: Scan part barcodes to perform operations
|
||||
|
||||
### Supported Barcode Formats
|
||||
|
||||
#### JSON-like Format
|
||||
```
|
||||
{pbn:PICK251017100019,on:WM2510170196,pc:C18548292,pm:STHW4-DU-HS24041,qty:150,mc:,cc:1,pdi:180368458,hp:null,wc:JS}
|
||||
```
|
||||
- `pm`: Part code
|
||||
- `qty`: Quantity
|
||||
|
||||
#### Separator-based Format
|
||||
Uses ASCII separators (GS/RS) to delimit fields:
|
||||
- `30P<part_code>` - Part code with 30P prefix
|
||||
- `1P<part_code>` - Part code with 1P prefix
|
||||
- `Q<quantity>` - Quantity
|
||||
|
||||
### Barcode Commands
|
||||
|
||||
You can use special barcodes to control the application:
|
||||
|
||||
**Mode Switching:**
|
||||
- `MODE:ADD` / `IMPORT` - Switch to Add Stock mode
|
||||
- `MODE:UPDATE` / `UPDATE` - Switch to Update Stock mode
|
||||
- `MODE:CHECK` / `CHECK` - Switch to Check Stock mode
|
||||
- `MODE:LOCATE` / `LOCATE` - Switch to Locate Part mode
|
||||
|
||||
**Debug Control:**
|
||||
- `DEBUG:ON` - Enable debug mode
|
||||
- `DEBUG:OFF` - Disable debug mode
|
||||
|
||||
**Location Management:**
|
||||
- `CHANGE_LOCATION` - Prompt for new location
|
||||
|
||||
## Operation Modes
|
||||
|
||||
### Add Stock Mode
|
||||
Adds stock to InvenTree. If the part already exists at the location, it adds to the existing stock item instead of creating a duplicate.
|
||||
|
||||
**Example:**
|
||||
- First scan: Creates StockItem #123 with quantity 150
|
||||
- Second scan: Updates StockItem #123 to quantity 250 (adds 100)
|
||||
|
||||
### Update Stock Mode
|
||||
Manually set the stock quantity for a part. Opens a dialog to enter the exact quantity.
|
||||
|
||||
### Check Stock Mode
|
||||
Displays the current stock level for a part at the selected location.
|
||||
|
||||
### Locate Part Mode
|
||||
Shows all locations where a part is stored, with quantities at each location.
|
||||
|
||||
## Character Encoding
|
||||
|
||||
The tool automatically cleans non-ASCII characters from part codes, handling common barcode encoding issues:
|
||||
- Input: `STHW4-DU-HS24041¡`
|
||||
- Cleaned: `STHW4-DU-HS24041`
|
||||
|
||||
## API Compatibility
|
||||
|
||||
This tool works with InvenTree's REST API and handles various response formats:
|
||||
- Paginated responses with `results` key
|
||||
- Direct list responses
|
||||
- Single object responses
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
- Check that your InvenTree server URL is correct
|
||||
- Verify your API token is valid
|
||||
- Ensure the server is accessible from your network
|
||||
- Check the connection status indicator (green = connected)
|
||||
|
||||
### Import Failures
|
||||
If a part cannot be found, the tool attempts to import it using `inventree-part-import`. Ensure this tool is installed and configured.
|
||||
|
||||
### Debug Mode
|
||||
Enable debug mode via checkbox or `DEBUG:ON` barcode to see detailed operation logs.
|
||||
|
||||
## Recent Fixes
|
||||
|
||||
### Version 2.x (Latest)
|
||||
- Fixed encoding issues with non-ASCII characters in part codes
|
||||
- Fixed API response handling for both list and dict formats
|
||||
- Implemented smart duplicate prevention when adding stock
|
||||
- Moved misplaced function definitions to correct locations
|
||||
- Added comprehensive error logging with tracebacks
|
||||
|
||||
See `FIXES_APPLIED.md` for detailed information about recent bug fixes.
|
||||
|
||||
## Development
|
||||
|
||||
### Project Structure
|
||||
```
|
||||
stocktool/
|
||||
├── stock_tool_gui_v2.py # Main application
|
||||
├── FIXES_APPLIED.md # Documentation of bug fixes
|
||||
├── test_parse_fix.py # Test for encoding fixes
|
||||
├── test_stock_level.py # Test for stock level retrieval
|
||||
├── test_add_stock.py # Test for API response handling
|
||||
├── test_duplicate_handling.py # Test for duplicate prevention
|
||||
├── .gitignore # Git ignore rules
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
```bash
|
||||
python test_parse_fix.py
|
||||
python test_stock_level.py
|
||||
python test_add_stock.py
|
||||
python test_duplicate_handling.py
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
## Support
|
||||
|
||||
For issues, questions, or contributions, please use the repository's issue tracker.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Built for use with [InvenTree](https://inventree.org/)
|
||||
- Uses [sv-ttk](https://github.com/rdbende/Sun-Valley-ttk-theme) for modern theming
|
||||
9
example_config.yaml
Normal file
9
example_config.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
# InvenTree Stock Tool Configuration
|
||||
# Copy this file to ~/.config/scan_and_import.yaml and fill in your details
|
||||
|
||||
# Your InvenTree server URL (without trailing slash)
|
||||
host: https://inventree.example.com
|
||||
|
||||
# Your InvenTree API token
|
||||
# Get this from: Settings > User Settings > API Tokens in InvenTree
|
||||
token: your-api-token-here
|
||||
1149
stock_tool_gui_v2.py
Normal file
1149
stock_tool_gui_v2.py
Normal file
File diff suppressed because it is too large
Load Diff
67
test_add_stock.py
Normal file
67
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
test_duplicate_handling.py
Normal file
106
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
test_parse_fix.py
Normal file
95
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
test_stock_level.py
Normal file
77
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