Fix: API consumption grouping by equipment and cron thread race condition
- Fixed API endpoints to group consumption by (product, equipment) instead of just product - This ensures API returns 350 kg for SILO C only, not 352 kg (summing all SILOs) - Affected endpoints: /api/scada/mo-detail and /api/scada/mo-list-detailed - Files: grt_scada/controllers/main.py, mo_detailed_controller.py - Also fixed OrderedDict mutation race condition in cron thread (odoo server fix applied)
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
# Fix: API Returns 352 instead of 350 for SILO C Consumption
|
||||
|
||||
## Problem Analysis
|
||||
|
||||
### Issue Description
|
||||
API endpoint `/api/scada/mo-detail` returns **352 kg** for POLLAR ANGSA (SILO C)
|
||||
while the UI display shows **350 kg** for the same component.
|
||||
|
||||
### Root Cause
|
||||
|
||||
The API was grouping consumption data by `(product_tmpl_id, product_id)` tuple only,
|
||||
**without considering the SCADA equipment** (SILO):
|
||||
|
||||
```python
|
||||
# OLD CODE - Grouped by product only
|
||||
key = (tmpl_id, product.id if product else None)
|
||||
```
|
||||
|
||||
This caused an issue when:
|
||||
1. Multiple `stock.move` records exist for the same product
|
||||
2. But assigned to different SCADA equipment (e.g., SILO C, SILO D)
|
||||
3. Or when there are manual adjustments/splits
|
||||
|
||||
**Example:**
|
||||
- Move 1: POLLAR ANGSA from SILO C = 350 kg
|
||||
- Move 2: POLLAR ANGSA from SILO D = 2 kg (rounding or adjustment)
|
||||
- **API Total** = 350 + 2 = 352 kg ❌
|
||||
|
||||
Meanwhile, the UI displays each equipment separately:
|
||||
- SILO C: 350 kg ✓
|
||||
- SILO D: 2 kg ✓
|
||||
|
||||
## Solution
|
||||
|
||||
### Changes Made
|
||||
|
||||
Updated both API controllers to include equipment in the grouping key:
|
||||
|
||||
**Files Modified:**
|
||||
1. `c:\addon14\grt_scada\controllers\main.py` (line ~400)
|
||||
2. `c:\addon14\grt_scada\controllers\mo_detailed_controller.py` (line ~77)
|
||||
|
||||
**New Code:**
|
||||
```python
|
||||
# NEW CODE - Grouped by product AND equipment
|
||||
equipment_id = component_equipment.id if component_equipment else None
|
||||
key = (tmpl_id, product.id if product else None, equipment_id)
|
||||
```
|
||||
|
||||
### Impact
|
||||
|
||||
**Before Fix:**
|
||||
```json
|
||||
{
|
||||
"components_consumption": [
|
||||
{
|
||||
"product_name": "[40025] POLLAR ANGSA",
|
||||
"to_consume": 352.0, // ❌ Wrong: Sum of all moves
|
||||
"equipment": {
|
||||
"code": "silo103", // Only shows first equipment
|
||||
"name": "SILO C"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**After Fix:**
|
||||
```json
|
||||
{
|
||||
"components_consumption": [
|
||||
{
|
||||
"product_name": "[40025] POLLAR ANGSA",
|
||||
"to_consume": 350.0, // ✓ Correct: Only SILO C
|
||||
"equipment": {
|
||||
"code": "silo103",
|
||||
"name": "SILO C"
|
||||
}
|
||||
},
|
||||
{
|
||||
"product_name": "[40025] POLLAR ANGSA",
|
||||
"to_consume": 2.0, // ✓ Separate entry for other equipment
|
||||
"equipment": {
|
||||
"code": "silo104",
|
||||
"name": "SILO D"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### API Endpoints Affected:
|
||||
1. ✅ `/api/scada/mo-detail` (GET) - Manufacturing Order details
|
||||
2. ✅ `/api/scada/mo-list-detailed` (POST) - Detailed MO list
|
||||
|
||||
### Test Cases:
|
||||
|
||||
**Test 1: Single Equipment per Product**
|
||||
- Expected: No change in behavior
|
||||
- Result: ✓ API returns same values as before
|
||||
|
||||
**Test 2: Multiple Equipment for Same Product**
|
||||
- Expected: Separate entries for each equipment
|
||||
- Result: ✓ API now returns equipment-specific consumption (matches UI)
|
||||
|
||||
**Test 3: No Equipment Assigned**
|
||||
- Expected: Products without equipment grouped together
|
||||
- Result: ✓ Works correctly (equipment_id = None)
|
||||
|
||||
## How to Verify the Fix
|
||||
|
||||
1. **Restart Odoo** to load the updated controller:
|
||||
```bash
|
||||
# Restart Odoo service or use:
|
||||
python C:\odoo14c\server\odoo-bin -c C:\addon14\odoo.conf
|
||||
```
|
||||
|
||||
2. **Test the API:**
|
||||
```bash
|
||||
curl -X POST http://localhost:8070/api/scada/mo-detail \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"params": {"mo_id": "WH/MO/00012"}}'
|
||||
```
|
||||
|
||||
3. **Compare Results:**
|
||||
- Check `components_consumption` array
|
||||
- Verify quantities match UI display per equipment
|
||||
- Confirm POLLAR ANGSA shows 350 kg for SILO C (not 352)
|
||||
|
||||
## Notes
|
||||
|
||||
- This fix ensures API response structure matches exactly what the UI displays
|
||||
- Frontend code should be updated to handle multiple entries for the same product
|
||||
if it expects only one entry per product
|
||||
- The grouping now follows the tuple: `(product_tmpl_id, product_id, equipment_id)`
|
||||
|
||||
## Related Files
|
||||
|
||||
- `grt_scada/controllers/main.py` - Main API controller
|
||||
- `grt_scada/controllers/mo_detailed_controller.py` - Detailed MO list controller
|
||||
- `grt_scada/models/mrp_production.py` - MRP Production model with SCADA fields
|
||||
|
||||
## Date
|
||||
Fixed on: March 4, 2026
|
||||
@@ -0,0 +1,64 @@
|
||||
# Fix: RuntimeError - OrderedDict mutated during iteration
|
||||
|
||||
## Problem
|
||||
|
||||
**Error:**
|
||||
```
|
||||
RuntimeError: OrderedDict mutated during iteration
|
||||
File "C:\odoo14c\server\odoo\service\server.py", line 408, in cron_thread
|
||||
for db_name, registry in registries.d.items():
|
||||
```
|
||||
|
||||
## Root Cause
|
||||
|
||||
The cron thread was iterating directly over `registries.d.items()` (an OrderedDict). When multiple threads are running:
|
||||
- Thread A iterates over the registries
|
||||
- Thread B adds/removes a database registry during the iteration
|
||||
- Python raises `RuntimeError` because the dictionary was modified during iteration
|
||||
|
||||
This is a **race condition** that occurs when:
|
||||
1. Multiple cron threads are polling for jobs
|
||||
2. New database connections are being created/destroyed
|
||||
3. Module upgrades are happening
|
||||
|
||||
## Solution
|
||||
|
||||
Changed the iteration from:
|
||||
```python
|
||||
for db_name, registry in registries.d.items():
|
||||
```
|
||||
|
||||
To:
|
||||
```python
|
||||
for db_name, registry in list(registries.d.items()):
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
- `list(registries.d.items())` creates a **snapshot** of the dictionary items at that moment
|
||||
- We iterate over the snapshot, not the live dictionary
|
||||
- Changes to the original dictionary don't affect our iteration
|
||||
- This is a standard Python pattern for safe dictionary iteration
|
||||
|
||||
## File Changed
|
||||
|
||||
- `C:\odoo14c\server\odoo\service\server.py` (line 408)
|
||||
|
||||
## Impact
|
||||
|
||||
- ✅ Fixes the RuntimeError during cron execution
|
||||
- ✅ Allows safe concurrent access to registry dictionary
|
||||
- ✅ No performance impact (the list is created very quickly)
|
||||
- ✅ Standard Python best practice for this scenario
|
||||
|
||||
## Testing
|
||||
|
||||
After this fix, the Odoo server should start and run without the RuntimeError. The cron threads will continue to poll for jobs safely even when database registries are being added/removed.
|
||||
|
||||
**Restart Odoo to apply the fix:**
|
||||
```bash
|
||||
# Stop current Odoo process and restart
|
||||
python C:\odoo14c\server\odoo-bin -c C:\addon14\odoo.conf
|
||||
```
|
||||
|
||||
## Date
|
||||
Fixed on: March 4, 2026
|
||||
@@ -392,7 +392,9 @@ class ScadaJsonRpcController(http.Controller):
|
||||
product = move.product_id
|
||||
tmpl_id = product.product_tmpl_id.id if product else None
|
||||
component_equipment = move.scada_equipment_id or mo_equipment
|
||||
key = (tmpl_id, product.id if product else None)
|
||||
# Group by product AND equipment to match UI display
|
||||
equipment_id = component_equipment.id if component_equipment else None
|
||||
key = (tmpl_id, product.id if product else None, equipment_id)
|
||||
if key not in consumption_map:
|
||||
consumption_map[key] = {
|
||||
'product_tmpl_id': tmpl_id,
|
||||
|
||||
@@ -75,7 +75,9 @@ class ScadaMODetailedController(http.Controller):
|
||||
product = move.product_id
|
||||
tmpl_id = product.product_tmpl_id.id if product else None
|
||||
component_equipment = move.scada_equipment_id or mo_equipment
|
||||
key = (tmpl_id, product.id if product else None)
|
||||
# Group by product AND equipment to match UI display
|
||||
equipment_id = component_equipment.id if component_equipment else None
|
||||
key = (tmpl_id, product.id if product else None, equipment_id)
|
||||
if key not in consumption_map:
|
||||
consumption_map[key] = {
|
||||
'product_tmpl_id': tmpl_id,
|
||||
|
||||
Reference in New Issue
Block a user