Sudah berhasil update PLC satu cycle
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
# Perbaikan API Odoo - Ringkasan
|
||||
|
||||
## Masalah Yang Ditemukan
|
||||
|
||||
1. **Port salah**: API menggunakan port 8069, padahal Odoo berjalan di port 8070
|
||||
2. **Missing attachment files**: Beberapa file attachment di database tidak ada di filestore
|
||||
|
||||
## Error di Log
|
||||
|
||||
```
|
||||
FileNotFoundError: [Errno 2] No such file or directory:
|
||||
'C:\\Users\\sapta\\AppData\\Local\\OpenERP S.A\\Odoo\\filestore\\manukanjabung\\c5/c54d3d5e2b1320083bf5378b7c195b0985fa04c1'
|
||||
```
|
||||
|
||||
Error ini terjadi karena record `ir.attachment` di database mereferensikan file yang tidak ada.
|
||||
|
||||
## Perbaikan Yang Dilakukan
|
||||
|
||||
### 1. Fix Port Configuration
|
||||
- Mengubah semua script API dari port `8069` → `8070`
|
||||
- Lokasi: `ODOO_URL = "http://localhost:8070"`
|
||||
|
||||
### 2. Fix Missing Attachments
|
||||
- Menjalankan `fix_missing_attachments.py`
|
||||
- Menghapus 1 attachment rusak (ID 27: res.company.scss)
|
||||
- Hasil: Semua attachment sekarang bersih ✅
|
||||
|
||||
## Script Yang Tersedia
|
||||
|
||||
### 1. test_api_connection.py
|
||||
**Fungsi**: Test koneksi API dan diagnosa masalah
|
||||
**Cara pakai**:
|
||||
```bash
|
||||
python test_api_connection.py
|
||||
```
|
||||
**Output**: Test 6 aspek koneksi API (connection, auth, model access, attachment, read, write)
|
||||
|
||||
### 2. fix_missing_attachments.py
|
||||
**Fungsi**: Mencari dan memperbaiki attachment yang filenya hilang
|
||||
**Cara pakai**:
|
||||
```bash
|
||||
python fix_missing_attachments.py
|
||||
```
|
||||
**Options**:
|
||||
- `1` = DELETE - Hapus record attachment rusak (RECOMMENDED)
|
||||
- `2` = RESET - Reset ke database storage
|
||||
- `3` = CANCEL
|
||||
|
||||
### 3. test_update_api.py
|
||||
**Fungsi**: Contoh lengkap CRUD operations via API
|
||||
**Cara pakai**:
|
||||
```bash
|
||||
python test_update_api.py
|
||||
```
|
||||
**Demonstrasi**:
|
||||
- ✅ CREATE - Buat partner baru
|
||||
- ✅ READ - Baca data partner
|
||||
- ✅ UPDATE - Update data partner
|
||||
- ✅ DELETE - Hapus partner
|
||||
- ✅ SEARCH - Cari dengan filter
|
||||
|
||||
## Hasil Test API
|
||||
|
||||
```
|
||||
✅ SEMUA TEST API BERHASIL!
|
||||
|
||||
KESIMPULAN:
|
||||
- API connection: ✅ OK
|
||||
- CREATE operation: ✅ OK
|
||||
- READ operation: ✅ OK
|
||||
- UPDATE operation: ✅ OK
|
||||
- DELETE operation: ✅ OK
|
||||
- SEARCH/FILTER: ✅ OK
|
||||
```
|
||||
|
||||
## Konfigurasi API
|
||||
|
||||
```python
|
||||
ODOO_URL = "http://localhost:8070" # PORT 8070!
|
||||
ODOO_DB = "manukanjabung"
|
||||
ODOO_USERNAME = "admin"
|
||||
ODOO_PASSWORD = "admin"
|
||||
```
|
||||
|
||||
## Contoh Penggunaan API
|
||||
|
||||
### Connect to Odoo
|
||||
```python
|
||||
import xmlrpc.client
|
||||
|
||||
url = "http://localhost:8070"
|
||||
db = "manukanjabung"
|
||||
username = "admin"
|
||||
password = "admin"
|
||||
|
||||
# Authenticate
|
||||
common = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/common')
|
||||
uid = common.authenticate(db, username, password, {})
|
||||
|
||||
# Get models proxy
|
||||
models = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object')
|
||||
```
|
||||
|
||||
### Create Record
|
||||
```python
|
||||
partner_id = models.execute_kw(
|
||||
db, uid, password,
|
||||
'res.partner', 'create',
|
||||
[{
|
||||
'name': 'Partner Baru',
|
||||
'email': 'test@example.com',
|
||||
'phone': '0812-3456-7890'
|
||||
}]
|
||||
)
|
||||
```
|
||||
|
||||
### Read Record
|
||||
```python
|
||||
partner = models.execute_kw(
|
||||
db, uid, password,
|
||||
'res.partner', 'read',
|
||||
[[partner_id]],
|
||||
{'fields': ['name', 'email', 'phone']}
|
||||
)[0]
|
||||
```
|
||||
|
||||
### Update Record
|
||||
```python
|
||||
result = models.execute_kw(
|
||||
db, uid, password,
|
||||
'res.partner', 'write',
|
||||
[[partner_id], {
|
||||
'phone': '0812-9999-8888'
|
||||
}]
|
||||
)
|
||||
```
|
||||
|
||||
### Delete Record
|
||||
```python
|
||||
result = models.execute_kw(
|
||||
db, uid, password,
|
||||
'res.partner', 'unlink',
|
||||
[[partner_id]]
|
||||
)
|
||||
```
|
||||
|
||||
### Search with Filter
|
||||
```python
|
||||
partner_ids = models.execute_kw(
|
||||
db, uid, password,
|
||||
'res.partner', 'search',
|
||||
[[
|
||||
['customer_rank', '>', 0],
|
||||
['city', 'ilike', 'jakarta']
|
||||
]],
|
||||
{'limit': 10}
|
||||
)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "No connection could be made"
|
||||
- ✅ FIX: Pastikan Odoo berjalan di port 8070
|
||||
- Check dengan: `netstat -ano | findstr "8070"`
|
||||
|
||||
### Error: "FileNotFoundError" untuk attachment
|
||||
- ✅ FIX: Jalankan `python fix_missing_attachments.py`
|
||||
|
||||
### Error: "Authentication failed"
|
||||
- Check username/password
|
||||
- Pastikan user memiliki access rights yang cukup
|
||||
|
||||
### Error: "Access Denied" saat update
|
||||
- Check access rights dan record rules untuk model tersebut
|
||||
- Gunakan user dengan permission yang cukup
|
||||
|
||||
## Status Akhir
|
||||
|
||||
✅ **API update via XML-RPC sudah berfungsi dengan baik!**
|
||||
|
||||
Semua operasi CRUD (Create, Read, Update, Delete) sudah berhasil ditest dan berjalan normal.
|
||||
|
||||
---
|
||||
|
||||
**Dibuat**: 15 Februari 2026
|
||||
**Status**: ✅ RESOLVED
|
||||
@@ -0,0 +1,217 @@
|
||||
# Endpoint Update Consumption - State Support
|
||||
|
||||
## ✅ State Yang Didukung
|
||||
|
||||
Endpoint `/api/scada/mo/update-with-consumptions` **BISA** update MO dengan state:
|
||||
|
||||
### 1. **CONFIRMED** ✅
|
||||
- MO baru yang sudah di-confirm
|
||||
- Belum ada consumption
|
||||
- **Setelah update pertama** → state otomatis berubah ke **PROGRESS**
|
||||
|
||||
### 2. **PROGRESS** ✅
|
||||
- MO yang sudah dimulai produksi
|
||||
- Sudah ada consumption sebelumnya
|
||||
- **Bisa di-update berkali-kali** tanpa batasan
|
||||
- State tetap PROGRESS
|
||||
|
||||
## 🧪 Test Results
|
||||
|
||||
### Test 1: MO dengan state 'confirmed'
|
||||
```json
|
||||
Request:
|
||||
{
|
||||
"mo_id": "WH/MO/00004",
|
||||
"silo101": 25.0,
|
||||
"silo103": 30.0
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"status": "success",
|
||||
"mo_id": "WH/MO/00004",
|
||||
"mo_state": "progress", ← Berubah dari 'confirmed' ke 'progress'
|
||||
"consumed_items": [...]
|
||||
}
|
||||
```
|
||||
|
||||
### Test 2: MO dengan state 'progress'
|
||||
```json
|
||||
Request:
|
||||
{
|
||||
"mo_id": "WH/MO/00001",
|
||||
"silo101": 15.0,
|
||||
"silo102": 20.0
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"status": "success",
|
||||
"mo_id": "WH/MO/00001",
|
||||
"mo_state": "progress", ← Tetap 'progress'
|
||||
"consumed_items": [...]
|
||||
}
|
||||
```
|
||||
|
||||
## 📋 Behavior Summary
|
||||
|
||||
| MO State | Bisa Update? | State Setelah Update |
|
||||
|----------|--------------|---------------------|
|
||||
| **draft** | ❓ Belum dicoba | Kemungkinan bisa |
|
||||
| **confirmed** | ✅ YA | → **progress** (auto) |
|
||||
| **progress** | ✅ YA | → **progress** (tetap) |
|
||||
| **done** | ❓ Belum dicoba | Kemungkinan tidak bisa |
|
||||
| **cancel** | ❌ TIDAK | N/A |
|
||||
|
||||
## 🔍 Cara Kerja
|
||||
|
||||
1. **Tidak ada validasi state** di kode
|
||||
2. Selama MO ditemukan, akan di-update
|
||||
3. Odoo secara otomatis mengelola state transition:
|
||||
- `confirmed` → `progress` saat ada consumption pertama
|
||||
- `progress` tetap `progress` saat update consumption
|
||||
|
||||
## 💡 Use Case
|
||||
|
||||
### Sequential Updates (SCADA Realtime)
|
||||
```python
|
||||
# Update 1 - MO masih confirmed
|
||||
POST /api/scada/mo/update-with-consumptions
|
||||
{
|
||||
"mo_id": "WH/MO/00005",
|
||||
"silo101": 100
|
||||
}
|
||||
# → State berubah ke 'progress'
|
||||
|
||||
# Update 2 - MO sudah progress (5 menit kemudian)
|
||||
POST /api/scada/mo/update-with-consumptions
|
||||
{
|
||||
"mo_id": "WH/MO/00005",
|
||||
"silo101": 150 # Tambah 50 kg lagi
|
||||
}
|
||||
# → State tetap 'progress'
|
||||
|
||||
# Update 3 - Update equipment lain
|
||||
POST /api/scada/mo/update-with-consumptions
|
||||
{
|
||||
"mo_id": "WH/MO/00005",
|
||||
"silo101": 200, # Total sekarang 200
|
||||
"silo102": 80, # Tambah equipment baru
|
||||
"silo103": 60
|
||||
}
|
||||
# → State tetap 'progress'
|
||||
```
|
||||
|
||||
### Batch Update Multiple Equipment
|
||||
```python
|
||||
# Update sekaligus banyak equipment
|
||||
POST /api/scada/mo/update-with-consumptions
|
||||
{
|
||||
"mo_id": "WH/MO/00002",
|
||||
"silo101": 825, # SILO A
|
||||
"silo102": 600, # SILO B
|
||||
"silo103": 375, # SILO C
|
||||
"silo104": 50, # SILO D
|
||||
"silo105": 381.25 # SILO E
|
||||
}
|
||||
```
|
||||
|
||||
## ⚠️ Catatan Penting
|
||||
|
||||
### Replace Mode (Default)
|
||||
Update consumption menggunakan **REPLACE mode**, bukan APPEND:
|
||||
|
||||
```python
|
||||
# Update pertama
|
||||
{"silo101": 100} # Move quantity_done = 100
|
||||
|
||||
# Update kedua
|
||||
{"silo101": 150} # Move quantity_done = 150 (BUKAN 100+150=250)
|
||||
```
|
||||
|
||||
Setiap update **mengganti nilai lama**, bukan menambah.
|
||||
|
||||
### State Transition
|
||||
State otomatis berubah dari `confirmed` → `progress` karena:
|
||||
- **Odoo behavior**: Saat ada consumption (quantity_done > 0) di raw materials
|
||||
- **Tidak bisa dicegah**: Ini adalah fitur bawaan Odoo MRP
|
||||
|
||||
### Equipment Code Required
|
||||
```python
|
||||
# ✅ Berhasil - Equipment ditemukan
|
||||
{"silo101": 100}
|
||||
|
||||
# ❌ Gagal - Equipment tidak ada
|
||||
{"silo999": 100}
|
||||
→ Error: "silo999: Equipment not found"
|
||||
|
||||
# ⚠️ Skip - Equipment code tidak valid
|
||||
{"": 100}
|
||||
→ Diabaikan, tidak error
|
||||
```
|
||||
|
||||
## 🔧 Test Scripts
|
||||
|
||||
### Test Update Berbagai State
|
||||
```bash
|
||||
python test_mo_states.py
|
||||
```
|
||||
|
||||
### Test Update Lengkap
|
||||
```bash
|
||||
python test_mo_consumption_api.py
|
||||
```
|
||||
|
||||
### Check MO Status
|
||||
```bash
|
||||
python check_mo_status.py
|
||||
```
|
||||
|
||||
## 📚 Endpoint Documentation
|
||||
|
||||
**URL**: `POST /api/scada/mo/update-with-consumptions`
|
||||
**Auth**: Session Cookie (JSON-RPC)
|
||||
**Content-Type**: `application/json`
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"params": {
|
||||
"mo_id": "WH/MO/00001",
|
||||
"quantity": 2500, // Optional: Update MO qty
|
||||
"silo101": 825, // Equipment consumption
|
||||
"silo102": 600,
|
||||
// ... more equipment codes
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": null,
|
||||
"result": {
|
||||
"status": "success",
|
||||
"message": "MO updated successfully",
|
||||
"mo_id": "WH/MO/00001",
|
||||
"mo_state": "progress",
|
||||
"consumed_items": [
|
||||
{
|
||||
"equipment_code": "silo101",
|
||||
"equipment_name": "SILO A",
|
||||
"applied_qty": 825.0,
|
||||
"move_ids": [45],
|
||||
"products": ["Pollard Angsa"]
|
||||
}
|
||||
],
|
||||
"errors": [] // Optional: list of errors
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Dibuat**: 15 Februari 2026
|
||||
**Status**: ✅ TESTED & VERIFIED
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Check MO status dan buat MO test jika perlu
|
||||
"""
|
||||
|
||||
import xmlrpc.client
|
||||
|
||||
ODOO_URL = "http://localhost:8070"
|
||||
ODOO_DB = "manukanjabung"
|
||||
ODOO_USERNAME = "admin"
|
||||
ODOO_PASSWORD = "admin"
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print("CHECK MO STATUS")
|
||||
print("=" * 70)
|
||||
|
||||
try:
|
||||
common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common')
|
||||
uid = common.authenticate(ODOO_DB, ODOO_USERNAME, ODOO_PASSWORD, {})
|
||||
|
||||
if not uid:
|
||||
print("❌ Gagal autentikasi")
|
||||
return
|
||||
|
||||
models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object')
|
||||
print(f"✅ Terkoneksi sebagai User ID: {uid}\n")
|
||||
|
||||
# Check all MO states
|
||||
print("📊 MO by State:")
|
||||
print("-" * 70)
|
||||
|
||||
states = ['draft', 'confirmed', 'progress', 'done', 'cancel']
|
||||
|
||||
for state in states:
|
||||
count = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'mrp.production', 'search_count',
|
||||
[[('state', '=', state)]]
|
||||
)
|
||||
|
||||
icon = "✅" if count > 0 else " "
|
||||
print(f" {icon} {state.upper()}: {count} MO")
|
||||
|
||||
if count > 0 and count <= 5:
|
||||
# Show some examples
|
||||
mo_ids = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'mrp.production', 'search',
|
||||
[[('state', '=', state)]],
|
||||
{'limit': 5}
|
||||
)
|
||||
|
||||
mos = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'mrp.production', 'read',
|
||||
[mo_ids],
|
||||
{'fields': ['name', 'product_id', 'product_qty', 'state']}
|
||||
)
|
||||
|
||||
for mo in mos:
|
||||
prod_name = mo.get('product_id', [False, 'N/A'])[1]
|
||||
print(f" - {mo['name']}: {prod_name} ({mo['product_qty']})")
|
||||
|
||||
print()
|
||||
|
||||
# Check total MO
|
||||
total_count = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'mrp.production', 'search_count',
|
||||
[[]]
|
||||
)
|
||||
|
||||
print(f" TOTAL: {total_count} MO")
|
||||
|
||||
# Suggest action
|
||||
print("\n" + "=" * 70)
|
||||
print("REKOMENDASI:")
|
||||
print("=" * 70)
|
||||
|
||||
confirmed_count = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'mrp.production', 'search_count',
|
||||
[[('state', '=', 'confirmed')]]
|
||||
)
|
||||
|
||||
if confirmed_count == 0:
|
||||
print("\n⚠️ Tidak ada MO dengan state 'confirmed'")
|
||||
print("\nUntuk test API update-with-consumptions, Anda perlu:")
|
||||
print("1. Buat MO baru di Odoo")
|
||||
print("2. Confirm MO tersebut (button 'Confirm')")
|
||||
print("3. Pastikan MO punya raw materials dengan equipment code")
|
||||
print("\nATAU")
|
||||
print("\nUpdate MO yang sudah ada ke state 'confirmed'")
|
||||
|
||||
# Check if there are draft MOs
|
||||
draft_count = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'mrp.production', 'search_count',
|
||||
[[('state', '=', 'draft')]]
|
||||
)
|
||||
|
||||
if draft_count > 0:
|
||||
print(f"\n💡 Ada {draft_count} MO dalam state 'draft'")
|
||||
print(" Anda bisa confirm salah satunya untuk test")
|
||||
|
||||
confirm = input("\nConfirm MO pertama? (y/n): ").strip().lower()
|
||||
|
||||
if confirm == 'y':
|
||||
mo_ids = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'mrp.production', 'search',
|
||||
[[('state', '=', 'draft')]],
|
||||
{'limit': 1}
|
||||
)
|
||||
|
||||
if mo_ids:
|
||||
mo_id = mo_ids[0]
|
||||
|
||||
# Confirm MO
|
||||
result = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'mrp.production', 'action_confirm',
|
||||
[[mo_id]]
|
||||
)
|
||||
|
||||
mo = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'mrp.production', 'read',
|
||||
[[mo_id]],
|
||||
{'fields': ['name', 'state']}
|
||||
)[0]
|
||||
|
||||
print(f"\n✅ MO {mo['name']} berhasil di-confirm!")
|
||||
print(f" State: {mo['state']}")
|
||||
print("\nSekarang bisa test API dengan:")
|
||||
print(" python test_mo_consumption_api.py")
|
||||
else:
|
||||
print(f"\n✅ Ada {confirmed_count} MO dalam state 'confirmed'")
|
||||
print("\nAnda bisa langsung test API dengan:")
|
||||
print(" python test_mo_consumption_api.py")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Check status module grt_scada dan module terkait
|
||||
"""
|
||||
|
||||
import xmlrpc.client
|
||||
|
||||
# Konfigurasi Odoo
|
||||
ODOO_URL = "http://localhost:8070"
|
||||
ODOO_DB = "manukanjabung"
|
||||
ODOO_USERNAME = "admin"
|
||||
ODOO_PASSWORD = "admin"
|
||||
|
||||
def check_module(models, uid, module_name):
|
||||
"""Check status specific module"""
|
||||
try:
|
||||
module = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'ir.module.module', 'search_read',
|
||||
[[('name', '=', module_name)]],
|
||||
{'fields': ['name', 'state', 'summary', 'latest_version']}
|
||||
)
|
||||
|
||||
if module:
|
||||
return module[0]
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print("CHECK MODULE GRT_SCADA")
|
||||
print("=" * 70)
|
||||
|
||||
try:
|
||||
# Connect
|
||||
common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common')
|
||||
uid = common.authenticate(ODOO_DB, ODOO_USERNAME, ODOO_PASSWORD, {})
|
||||
|
||||
if not uid:
|
||||
print("❌ Gagal autentikasi")
|
||||
return
|
||||
|
||||
models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object')
|
||||
print(f"✅ Terkoneksi sebagai User ID: {uid}\n")
|
||||
|
||||
# Check modules
|
||||
modules_to_check = [
|
||||
'grt_scada',
|
||||
'grt_scada_failure_report',
|
||||
'mrp',
|
||||
'stock',
|
||||
]
|
||||
|
||||
for module_name in modules_to_check:
|
||||
print(f"Checking module: {module_name}")
|
||||
module = check_module(models, uid, module_name)
|
||||
|
||||
if module:
|
||||
if 'error' in module:
|
||||
print(f" ❌ Error: {module['error']}\n")
|
||||
else:
|
||||
state = module.get('state', 'unknown')
|
||||
if state == 'installed':
|
||||
icon = "✅"
|
||||
elif state == 'uninstalled':
|
||||
icon = "⬜"
|
||||
elif state == 'to upgrade':
|
||||
icon = "⚠️ "
|
||||
elif state == 'to install':
|
||||
icon = "🔄"
|
||||
else:
|
||||
icon = "❓"
|
||||
|
||||
print(f" {icon} State: {state}")
|
||||
print(f" Version: {module.get('latest_version', 'N/A')}")
|
||||
if module.get('summary'):
|
||||
print(f" Summary: {module.get('summary')}")
|
||||
print()
|
||||
else:
|
||||
print(f" ❌ Module not found\n")
|
||||
|
||||
# Check if grt_scada routes are registered
|
||||
print("\n" + "=" * 70)
|
||||
print("CHECK ROUTES")
|
||||
print("=" * 70)
|
||||
|
||||
# Try to get list of routes (if possible)
|
||||
try:
|
||||
routes = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'ir.http', 'search_read',
|
||||
[[]],
|
||||
{'limit': 5}
|
||||
)
|
||||
print(f"Found {len(routes)} routes registered")
|
||||
except Exception as e:
|
||||
print(f"Cannot check routes: {e}")
|
||||
|
||||
# Check if scada.equipment model exists
|
||||
print("\n" + "=" * 70)
|
||||
print("CHECK MODELS")
|
||||
print("=" * 70)
|
||||
|
||||
models_to_check = [
|
||||
'scada.equipment',
|
||||
'scada.equipment.material.log',
|
||||
'mrp.production',
|
||||
'stock.move',
|
||||
]
|
||||
|
||||
for model_name in models_to_check:
|
||||
try:
|
||||
count = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
model_name, 'search_count',
|
||||
[[]]
|
||||
)
|
||||
print(f" ✅ {model_name}: {count} records")
|
||||
except Exception as e:
|
||||
print(f" ❌ {model_name}: {str(e)[:100]}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Script to fix missing attachment files in Odoo filestore
|
||||
Handles FileNotFoundError for ir.attachment records
|
||||
"""
|
||||
|
||||
import xmlrpc.client
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Konfigurasi Odoo
|
||||
ODOO_URL = "http://localhost:8070"
|
||||
ODOO_DB = "manukanjabung"
|
||||
ODOO_USERNAME = "admin"
|
||||
ODOO_PASSWORD = "admin"
|
||||
|
||||
# Lokasi filestore
|
||||
FILESTORE_PATH = r"C:\Users\sapta\AppData\Local\OpenERP S.A\Odoo\filestore\manukanjabung"
|
||||
|
||||
def connect_odoo():
|
||||
"""Koneksi ke Odoo via XML-RPC"""
|
||||
try:
|
||||
common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common')
|
||||
uid = common.authenticate(ODOO_DB, ODOO_USERNAME, ODOO_PASSWORD, {})
|
||||
|
||||
if not uid:
|
||||
print("❌ Gagal autentikasi. Periksa username/password")
|
||||
return None, None, None
|
||||
|
||||
models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object')
|
||||
print(f"✅ Terkoneksi sebagai user ID: {uid}")
|
||||
return common, uid, models
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error koneksi: {e}")
|
||||
return None, None, None
|
||||
|
||||
def check_attachment_file_exists(store_fname):
|
||||
"""Check if physical file exists in filestore"""
|
||||
if not store_fname:
|
||||
return True # File disimpan di database, bukan filestore
|
||||
|
||||
# Replace forward slash with backslash for Windows
|
||||
file_path = os.path.join(FILESTORE_PATH, store_fname.replace('/', '\\'))
|
||||
return os.path.exists(file_path)
|
||||
|
||||
def find_broken_attachments(models, uid):
|
||||
"""Find all attachments with missing physical files"""
|
||||
print("\n🔍 Mencari attachment dengan file yang hilang...")
|
||||
|
||||
try:
|
||||
# Get all attachments that should have files in filestore
|
||||
attachment_ids = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'ir.attachment', 'search',
|
||||
[[('store_fname', '!=', False)]]
|
||||
)
|
||||
|
||||
print(f"📊 Total attachment di filestore: {len(attachment_ids)}")
|
||||
|
||||
if not attachment_ids:
|
||||
print("✅ Tidak ada attachment di filestore")
|
||||
return []
|
||||
|
||||
# Read attachment data
|
||||
attachments = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'ir.attachment', 'read',
|
||||
[attachment_ids],
|
||||
{'fields': ['id', 'name', 'store_fname', 'res_model', 'res_id', 'type']}
|
||||
)
|
||||
|
||||
broken_attachments = []
|
||||
|
||||
for att in attachments:
|
||||
if not check_attachment_file_exists(att.get('store_fname')):
|
||||
broken_attachments.append(att)
|
||||
print(f" ❌ ID {att['id']}: {att['name']} - File hilang: {att.get('store_fname')}")
|
||||
|
||||
print(f"\n📊 Total attachment rusak: {len(broken_attachments)}")
|
||||
return broken_attachments
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error saat mencari attachment: {e}")
|
||||
return []
|
||||
|
||||
def fix_broken_attachments(models, uid, broken_attachments, method='delete'):
|
||||
"""
|
||||
Fix broken attachments
|
||||
method: 'delete' or 'reset'
|
||||
- delete: Hapus record attachment yang rusak
|
||||
- reset: Reset ke database storage (datas menjadi False)
|
||||
"""
|
||||
|
||||
if not broken_attachments:
|
||||
print("✅ Tidak ada attachment yang perlu diperbaiki")
|
||||
return
|
||||
|
||||
print(f"\n🔧 Memperbaiki {len(broken_attachments)} attachment dengan metode: {method}")
|
||||
|
||||
fixed_count = 0
|
||||
failed_count = 0
|
||||
|
||||
for att in broken_attachments:
|
||||
try:
|
||||
if method == 'delete':
|
||||
# Delete the broken attachment record
|
||||
result = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'ir.attachment', 'unlink',
|
||||
[[att['id']]]
|
||||
)
|
||||
|
||||
if result:
|
||||
print(f" ✅ Deleted ID {att['id']}: {att['name']}")
|
||||
fixed_count += 1
|
||||
else:
|
||||
print(f" ❌ Failed to delete ID {att['id']}")
|
||||
failed_count += 1
|
||||
|
||||
elif method == 'reset':
|
||||
# Reset to database storage (clear store_fname and datas)
|
||||
result = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'ir.attachment', 'write',
|
||||
[[att['id']], {
|
||||
'store_fname': False,
|
||||
'datas': False,
|
||||
'db_datas': False
|
||||
}]
|
||||
)
|
||||
|
||||
if result:
|
||||
print(f" ✅ Reset ID {att['id']}: {att['name']}")
|
||||
fixed_count += 1
|
||||
else:
|
||||
print(f" ❌ Failed to reset ID {att['id']}")
|
||||
failed_count += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error fixing ID {att['id']}: {e}")
|
||||
failed_count += 1
|
||||
|
||||
print(f"\n📊 Hasil perbaikan:")
|
||||
print(f" ✅ Berhasil: {fixed_count}")
|
||||
print(f" ❌ Gagal: {failed_count}")
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("ODOO ATTACHMENT FILESTORE REPAIR TOOL")
|
||||
print("=" * 60)
|
||||
|
||||
# Connect to Odoo
|
||||
common, uid, models = connect_odoo()
|
||||
|
||||
if not uid:
|
||||
return
|
||||
|
||||
# Find broken attachments
|
||||
broken_attachments = find_broken_attachments(models, uid)
|
||||
|
||||
if not broken_attachments:
|
||||
print("\n✅ Semua attachment dalam kondisi baik!")
|
||||
return
|
||||
|
||||
# Show summary
|
||||
print("\n" + "=" * 60)
|
||||
print("RINGKASAN ATTACHMENT RUSAK")
|
||||
print("=" * 60)
|
||||
|
||||
model_count = {}
|
||||
for att in broken_attachments:
|
||||
model = att.get('res_model', 'unknown')
|
||||
model_count[model] = model_count.get(model, 0) + 1
|
||||
|
||||
for model, count in sorted(model_count.items(), key=lambda x: -x[1]):
|
||||
print(f" {model}: {count} attachment")
|
||||
|
||||
# Ask for action
|
||||
print("\n" + "=" * 60)
|
||||
print("PILIHAN PERBAIKAN:")
|
||||
print("=" * 60)
|
||||
print("1. DELETE - Hapus record attachment yang rusak (RECOMMENDED)")
|
||||
print("2. RESET - Reset ke database storage (data akan hilang)")
|
||||
print("3. CANCEL - Batal, tidak melakukan perubahan")
|
||||
print()
|
||||
|
||||
choice = input("Pilih metode perbaikan (1/2/3): ").strip()
|
||||
|
||||
if choice == '1':
|
||||
confirm = input(f"\n⚠️ Yakin hapus {len(broken_attachments)} attachment? (yes/no): ").strip().lower()
|
||||
if confirm == 'yes':
|
||||
fix_broken_attachments(models, uid, broken_attachments, method='delete')
|
||||
else:
|
||||
print("❌ Dibatalkan")
|
||||
|
||||
elif choice == '2':
|
||||
confirm = input(f"\n⚠️ Yakin reset {len(broken_attachments)} attachment? (yes/no): ").strip().lower()
|
||||
if confirm == 'yes':
|
||||
fix_broken_attachments(models, uid, broken_attachments, method='reset')
|
||||
else:
|
||||
print("❌ Dibatalkan")
|
||||
|
||||
else:
|
||||
print("❌ Dibatalkan")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("SELESAI")
|
||||
print("=" * 60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1269,6 +1269,165 @@ curl -X GET http://localhost:8069/api/scada/equipment/PLC01 \
|
||||
|
||||
---
|
||||
|
||||
### 19A. Create Equipment Failure Report (Protected)
|
||||
|
||||
**Create failure report for SCADA equipment**
|
||||
|
||||
```http
|
||||
POST /api/scada/equipment-failure
|
||||
Auth: Session cookie
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Request Body (REST format)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"equipment_code": "PLC01",
|
||||
"description": "Motor overload saat proses mixing",
|
||||
"date": "2026-02-15 08:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
**JSON-RPC Body**:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "call",
|
||||
"params": {
|
||||
"equipment_code": "PLC01",
|
||||
"description": "Motor overload saat proses mixing",
|
||||
"date": "2026-02-15 08:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Equipment failure report created",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"equipment_id": 1,
|
||||
"equipment_code": "PLC01",
|
||||
"equipment_name": "Main PLC - Injection Machine 01",
|
||||
"description": "Motor overload saat proses mixing",
|
||||
"date": "2026-02-15T08:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**cURL Example (REST)**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8069/api/scada/equipment-failure \
|
||||
-H "Content-Type: application/json" \
|
||||
-b cookies.txt \
|
||||
-d '{
|
||||
"equipment_code": "PLC01",
|
||||
"description": "Motor overload saat proses mixing",
|
||||
"date": "2026-02-15 08:30:00"
|
||||
}'
|
||||
```
|
||||
|
||||
**cURL Example (JSON-RPC)**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8069/api/scada/equipment-failure \
|
||||
-H "Content-Type: application/json" \
|
||||
-b cookies.txt \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "call",
|
||||
"params": {
|
||||
"equipment_code": "PLC01",
|
||||
"description": "Motor overload saat proses mixing",
|
||||
"date": "2026-02-15 08:30:00"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### 19B. Get Equipment Failure Reports (Protected)
|
||||
|
||||
**Get list of equipment failure reports**
|
||||
|
||||
```http
|
||||
GET /api/scada/equipment-failure?equipment_code=PLC01&limit=50&offset=0
|
||||
Auth: Session cookie
|
||||
```
|
||||
|
||||
**JSON-RPC (POST)**:
|
||||
|
||||
```http
|
||||
POST /api/scada/equipment-failure
|
||||
Auth: Session cookie
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**JSON-RPC Body**:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "call",
|
||||
"params": {
|
||||
"equipment_code": "PLC01",
|
||||
"limit": 50,
|
||||
"offset": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Query Parameters**:
|
||||
- `equipment_code` (optional): filter by SCADA equipment code
|
||||
- `limit` (optional): max records (default 50)
|
||||
- `offset` (optional): pagination offset (default 0)
|
||||
|
||||
**Response**:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"count": 1,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"equipment_id": 1,
|
||||
"equipment_code": "PLC01",
|
||||
"equipment_name": "Main PLC - Injection Machine 01",
|
||||
"description": "Motor overload saat proses mixing",
|
||||
"date": "2026-02-15T08:30:00",
|
||||
"reported_by": "Administrator"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**cURL Example (REST)**:
|
||||
```bash
|
||||
curl -X GET "http://localhost:8069/api/scada/equipment-failure?equipment_code=PLC01&limit=50" \
|
||||
-b cookies.txt
|
||||
```
|
||||
|
||||
**cURL Example (JSON-RPC)**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8069/api/scada/equipment-failure \
|
||||
-H "Content-Type: application/json" \
|
||||
-b cookies.txt \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "call",
|
||||
"params": {
|
||||
"equipment_code": "PLC01",
|
||||
"limit": 50,
|
||||
"offset": 0
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 20. Get Product List (Protected)
|
||||
|
||||
**Retrieve Product List**
|
||||
@@ -1485,6 +1644,75 @@ curl -X POST http://localhost:8069/api/scada/boms \
|
||||
|
||||
---
|
||||
|
||||
### 23. Create Failure Report (Extension Module, Protected)
|
||||
|
||||
**Create SCADA equipment failure report**
|
||||
|
||||
Available only when module `grt_scada_failure_report` is installed.
|
||||
|
||||
```http
|
||||
POST /api/scada/failure-report
|
||||
Auth: Session cookie
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Request Body**:
|
||||
|
||||
```json
|
||||
{
|
||||
"equipment_code": "PLC01",
|
||||
"description": "Motor overload saat proses mixing",
|
||||
"date": "2026-02-15 08:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
**Field notes**:
|
||||
- `equipment_code` (required): nilai `equipment_code` pada model `scada.equipment`
|
||||
- `description` (required): deskripsi failure
|
||||
- `date` (optional): format `YYYY-MM-DD HH:MM:SS` atau `YYYY-MM-DDTHH:MM`; jika kosong akan pakai waktu server saat create
|
||||
|
||||
**Response (Success)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Failure report created",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"equipment_code": "PLC01",
|
||||
"description": "Motor overload saat proses mixing",
|
||||
"date": "2026-02-15 08:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response (Error)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Equipment with code \"PLC01\" not found"
|
||||
}
|
||||
```
|
||||
|
||||
**cURL Example**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8069/api/scada/failure-report \
|
||||
-H "Content-Type: application/json" \
|
||||
-b cookies.txt \
|
||||
-d '{
|
||||
"equipment_code": "PLC01",
|
||||
"description": "Motor overload saat proses mixing",
|
||||
"date": "2026-02-15 08:30:00"
|
||||
}'
|
||||
```
|
||||
|
||||
**Form Routes**:
|
||||
- `GET /scada/failure-report/input`
|
||||
- `POST /scada/failure-report/submit`
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Error Codes & Messages
|
||||
|
||||
@@ -13,6 +13,7 @@ This module provides a comprehensive SCADA solution that integrates Odoo Manufac
|
||||
- **Material Consumption via MO** - Middleware updates MO component consumption directly
|
||||
- **Manufacturing Order Sync** - Synchronize manufacturing orders between Odoo and equipment/middleware
|
||||
- **Sensor Data Collection** - Collect and monitor real-time sensor readings
|
||||
- **Equipment Failure Report** - Pencatatan dan analisis failure per equipment (list, pivot, graph)
|
||||
- **Middleware API** - REST API endpoints for middleware communication
|
||||
- **API Logging** - Comprehensive logging of all API calls for audit trail
|
||||
|
||||
@@ -108,6 +109,24 @@ Example:
|
||||
GET /api/scada/mo-list?equipment_id=PLC01&status=planned&limit=10
|
||||
```
|
||||
|
||||
### Failure Report API (Extension Module)
|
||||
|
||||
Jika module `grt_scada_failure_report` di-install, tersedia endpoint tambahan:
|
||||
|
||||
- `POST /api/scada/failure-report`
|
||||
- `GET /scada/failure-report/input`
|
||||
- `POST /scada/failure-report/submit`
|
||||
|
||||
Body minimal untuk endpoint API:
|
||||
|
||||
```json
|
||||
{
|
||||
"equipment_code": "PLC01",
|
||||
"description": "Motor overload",
|
||||
"date": "2026-02-15 08:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
1. Place module in Odoo addons directory
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
'name': 'SCADA for Odoo - Manufacturing Integration',
|
||||
'version': '4.0.31',
|
||||
'version': '4.0.38',
|
||||
'category': 'manufacturing',
|
||||
'license': 'LGPL-3',
|
||||
'author': 'PT. Gagak Rimang Teknologi',
|
||||
@@ -32,6 +32,7 @@
|
||||
'views/scada_api_log_view.xml',
|
||||
'views/scada_quality_control_view.xml',
|
||||
'views/scada_equipment_oee_view.xml',
|
||||
'views/scada_equipment_failure_view.xml',
|
||||
'views/menu.xml',
|
||||
'data/demo_data.xml',
|
||||
'data/ir_cron.xml',
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -537,6 +537,96 @@ class ScadaJsonRpcController(http.Controller):
|
||||
_logger.error(f'Error getting equipment status: {str(e)}')
|
||||
return {'status': 'error', 'message': str(e)}
|
||||
|
||||
@http.route('/api/scada/equipment-failure', type='json', auth='user', methods=['POST'])
|
||||
def create_equipment_failure(self, **kwargs):
|
||||
"""Create equipment failure report."""
|
||||
try:
|
||||
data = self._get_json_payload()
|
||||
equipment_code = data.get('equipment_code')
|
||||
description = data.get('description')
|
||||
date_value = data.get('date')
|
||||
|
||||
if not equipment_code:
|
||||
return {'status': 'error', 'message': 'equipment_code is required'}
|
||||
if not description:
|
||||
return {'status': 'error', 'message': 'description is required'}
|
||||
|
||||
equipment = request.env['scada.equipment'].search([
|
||||
('equipment_code', '=', equipment_code),
|
||||
], limit=1)
|
||||
if not equipment:
|
||||
return {'status': 'error', 'message': f'Equipment "{equipment_code}" not found'}
|
||||
|
||||
failure_date = datetime.now()
|
||||
if date_value:
|
||||
try:
|
||||
cleaned = str(date_value).strip().replace('T', ' ')
|
||||
if len(cleaned) == 16:
|
||||
cleaned = f'{cleaned}:00'
|
||||
failure_date = datetime.fromisoformat(cleaned)
|
||||
except Exception:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Invalid date format. Use YYYY-MM-DD HH:MM:SS or YYYY-MM-DDTHH:MM',
|
||||
}
|
||||
|
||||
failure = request.env['scada.equipment.failure'].create({
|
||||
'equipment_id': equipment.id,
|
||||
'description': description,
|
||||
'date': failure_date,
|
||||
})
|
||||
return {
|
||||
'status': 'success',
|
||||
'message': 'Equipment failure report created',
|
||||
'data': {
|
||||
'id': failure.id,
|
||||
'equipment_id': equipment.id,
|
||||
'equipment_code': equipment.equipment_code,
|
||||
'equipment_name': equipment.name,
|
||||
'description': failure.description,
|
||||
'date': failure.date.isoformat() if failure.date else None,
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
_logger.error(f'Error creating equipment failure report: {str(e)}')
|
||||
return {'status': 'error', 'message': str(e)}
|
||||
|
||||
@http.route('/api/scada/equipment-failure', type='json', auth='user', methods=['GET'])
|
||||
def get_equipment_failures(self, **kwargs):
|
||||
"""Get equipment failure report list."""
|
||||
try:
|
||||
equipment_code = request.httprequest.args.get('equipment_code')
|
||||
limit = int(request.httprequest.args.get('limit', 50))
|
||||
offset = int(request.httprequest.args.get('offset', 0))
|
||||
|
||||
domain = []
|
||||
if equipment_code:
|
||||
domain.append(('equipment_code', '=', equipment_code))
|
||||
|
||||
failures = request.env['scada.equipment.failure'].search(
|
||||
domain,
|
||||
order='date desc, id desc',
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
data = []
|
||||
for failure in failures:
|
||||
data.append({
|
||||
'id': failure.id,
|
||||
'equipment_id': failure.equipment_id.id if failure.equipment_id else None,
|
||||
'equipment_code': failure.equipment_code,
|
||||
'equipment_name': failure.equipment_id.name if failure.equipment_id else None,
|
||||
'description': failure.description,
|
||||
'date': failure.date.isoformat() if failure.date else None,
|
||||
'reported_by': failure.reported_by.name if failure.reported_by else None,
|
||||
})
|
||||
|
||||
return {'status': 'success', 'count': len(data), 'data': data}
|
||||
except Exception as e:
|
||||
_logger.error(f'Error getting equipment failure reports: {str(e)}')
|
||||
return {'status': 'error', 'message': str(e)}
|
||||
|
||||
# ===== PRODUCTS =====
|
||||
|
||||
@http.route('/api/scada/products', type='http', auth='user', methods=['GET'])
|
||||
|
||||
@@ -9,6 +9,7 @@ from . import scada_api_log
|
||||
from . import scada_health
|
||||
from . import scada_module
|
||||
from . import scada_quality_control
|
||||
from . import scada_equipment_failure
|
||||
from . import mrp_bom
|
||||
from . import mrp_production
|
||||
from . import mrp_bom_line
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -138,6 +138,12 @@ class ScadaEquipment(models.Model):
|
||||
string='Equipment Material Consumption',
|
||||
help='Mapping/consumption untuk OEE dan analitik'
|
||||
)
|
||||
failure_report_ids = fields.One2many(
|
||||
'scada.equipment.failure',
|
||||
'equipment_id',
|
||||
string='Failure Reports',
|
||||
help='Riwayat laporan failure untuk equipment ini'
|
||||
)
|
||||
sensor_reading_ids = fields.One2many(
|
||||
'scada.sensor.reading',
|
||||
'equipment_id',
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ScadaEquipmentFailure(models.Model):
|
||||
_name = 'scada.equipment.failure'
|
||||
_description = 'SCADA Equipment Failure'
|
||||
_order = 'date desc, id desc'
|
||||
|
||||
equipment_id = fields.Many2one(
|
||||
'scada.equipment',
|
||||
string='Equipment',
|
||||
required=True,
|
||||
ondelete='restrict',
|
||||
)
|
||||
equipment_code = fields.Char(
|
||||
string='Equipment Code',
|
||||
related='equipment_id.equipment_code',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
description = fields.Text(string='Description', required=True)
|
||||
date = fields.Datetime(
|
||||
string='Date',
|
||||
required=True,
|
||||
default=fields.Datetime.now,
|
||||
)
|
||||
reported_by = fields.Many2one(
|
||||
'res.users',
|
||||
string='Reported By',
|
||||
default=lambda self: self.env.user,
|
||||
readonly=True,
|
||||
)
|
||||
@@ -156,6 +156,42 @@ class ScadaMaterialConsumption(models.Model):
|
||||
)
|
||||
record.status = 'validated'
|
||||
|
||||
def update_consumed(self, new_quantity):
|
||||
"""
|
||||
Update nilai consumed untuk record yang masih dalam status draft/recorded.
|
||||
Method ini memungkinkan update quantity tanpa akumulasi sebelum di-mark done.
|
||||
|
||||
Args:
|
||||
new_quantity: Nilai kuantitas baru yang ingin di-set
|
||||
|
||||
Returns:
|
||||
Bool - True jika berhasil
|
||||
"""
|
||||
for record in self:
|
||||
# Hanya boleh update jika belum di-post ke stock atau di-cancel
|
||||
if record.status not in ['draft', 'recorded', 'validated']:
|
||||
raise ValidationError(
|
||||
f"Tidak bisa update consumed pada status '{record.status}'. "
|
||||
f"Hanya bisa di-update pada status draft/recorded/validated."
|
||||
)
|
||||
|
||||
if new_quantity <= 0:
|
||||
raise ValidationError("Quantity harus lebih dari 0")
|
||||
|
||||
# Update quantity value (not accumulate)
|
||||
record.quantity = new_quantity
|
||||
|
||||
# Re-apply consumption to moves dengan nilai baru
|
||||
if record.move_id:
|
||||
moves = self._find_raw_moves_for_material(
|
||||
record.manufacturing_order_id,
|
||||
record.material_id
|
||||
)
|
||||
if moves:
|
||||
self._apply_consumption_to_moves(record, moves)
|
||||
|
||||
return True
|
||||
|
||||
def action_post_to_stock(self):
|
||||
"""Post consumption ke stock module"""
|
||||
# TODO: Create stock move untuk recording di inventory
|
||||
@@ -374,18 +410,41 @@ class ScadaMaterialConsumption(models.Model):
|
||||
)
|
||||
|
||||
def _apply_consumption_to_moves(self, record, moves):
|
||||
qty_remaining = record.quantity
|
||||
"""
|
||||
Apply consumption quantity to stock moves.
|
||||
REPLACE the quantity_done value, tidak accumulate/tambah.
|
||||
"""
|
||||
qty_to_consume = record.quantity
|
||||
planned_total = 0.0
|
||||
used_qty = 0.0
|
||||
move_id = False
|
||||
|
||||
for move in moves:
|
||||
if used_qty >= qty_to_consume:
|
||||
break
|
||||
|
||||
planned = move.product_uom_qty or 0.0
|
||||
planned_total += planned
|
||||
|
||||
# Distribute consumption across moves proportionally
|
||||
qty_remaining = qty_to_consume
|
||||
|
||||
for move in moves:
|
||||
if qty_remaining <= 0:
|
||||
break
|
||||
|
||||
planned = move.product_uom_qty or 0.0
|
||||
done = move.quantity_done or 0.0
|
||||
remaining = max(planned - done, 0.0)
|
||||
add_qty = qty_remaining if remaining == 0.0 else min(qty_remaining, remaining)
|
||||
move.quantity_done = done + add_qty
|
||||
qty_remaining -= add_qty
|
||||
|
||||
# Calculate how much to allocate to this move
|
||||
if planned_total > 0:
|
||||
allocation = (planned / planned_total) * qty_to_consume
|
||||
else:
|
||||
allocation = qty_to_consume
|
||||
|
||||
# SET quantity_done to the allocated amount (replace, not add)
|
||||
move.quantity_done = allocation
|
||||
qty_remaining -= allocation
|
||||
|
||||
if not move_id:
|
||||
move_id = move.id
|
||||
|
||||
|
||||
@@ -19,3 +19,6 @@ access_scada_quality_control_technician,scada.quality.control Technician,model_s
|
||||
access_scada_equipment_oee_manager,scada.equipment.oee Manager,model_scada_equipment_oee,group_scada_manager,1,1,1,1
|
||||
access_scada_equipment_oee_operator,scada.equipment.oee Operator,model_scada_equipment_oee,group_scada_operator,1,1,1,0
|
||||
access_scada_equipment_oee_technician,scada.equipment.oee Technician,model_scada_equipment_oee,group_scada_technician,1,1,1,0
|
||||
access_scada_equipment_failure_manager,scada.equipment.failure Manager,model_scada_equipment_failure,group_scada_manager,1,1,1,1
|
||||
access_scada_equipment_failure_operator,scada.equipment.failure Operator,model_scada_equipment_failure,group_scada_operator,1,1,1,0
|
||||
access_scada_equipment_failure_technician,scada.equipment.failure Technician,model_scada_equipment_failure,group_scada_technician,1,1,1,0
|
||||
|
||||
|
Binary file not shown.
Binary file not shown.
@@ -483,17 +483,27 @@ class MiddlewareService:
|
||||
qty_remaining = quantity
|
||||
move_ids = []
|
||||
|
||||
# Clear existing done qty so replace mode does not accumulate
|
||||
for move in moves:
|
||||
move.quantity_done = 0.0
|
||||
|
||||
move_count = len(moves)
|
||||
for idx, move in enumerate(moves):
|
||||
if qty_remaining <= 0:
|
||||
break
|
||||
|
||||
planned = move.product_uom_qty or 0.0
|
||||
|
||||
# REPLACE mode: jangan gunakan existing done, langsung set ke value baru
|
||||
# REPLACE mode: jangan gunakan existing done, langsung set ke value baru.
|
||||
# Jika overconsume diizinkan, move terakhir bisa menampung sisa qty
|
||||
# agar input middleware tidak terpotong ke planned qty.
|
||||
if planned == 0.0 and not allow_overconsume:
|
||||
add_qty = 0.0
|
||||
else:
|
||||
add_qty = qty_remaining if planned == 0.0 else min(qty_remaining, planned)
|
||||
if allow_overconsume and idx == (move_count - 1):
|
||||
add_qty = qty_remaining
|
||||
else:
|
||||
add_qty = qty_remaining if planned == 0.0 else min(qty_remaining, planned)
|
||||
|
||||
if add_qty <= 0.0:
|
||||
continue
|
||||
@@ -682,6 +692,8 @@ class MiddlewareService:
|
||||
consumed_items = []
|
||||
errors = []
|
||||
|
||||
_logger.info(f'Processing consumption for MO {mo_name}: {list(update_data.items())}')
|
||||
|
||||
for key, value in update_data.items():
|
||||
# Skip non-equipment keys
|
||||
if key in ['mo_id', 'quantity']:
|
||||
@@ -697,35 +709,54 @@ class MiddlewareService:
|
||||
continue
|
||||
|
||||
if consumption_qty <= 0:
|
||||
_logger.debug(f'Skipping {equipment_code}: qty <= 0')
|
||||
continue
|
||||
|
||||
_logger.info(f'Processing {equipment_code}: {consumption_qty} kg')
|
||||
|
||||
# Cari equipment berdasarkan code
|
||||
equipment = self.env['scada.equipment'].search([
|
||||
('equipment_code', '=', equipment_code)
|
||||
], limit=1)
|
||||
|
||||
if not equipment:
|
||||
errors.append(f'{equipment_code}: Equipment not found')
|
||||
msg = f'{equipment_code}: Equipment not found'
|
||||
_logger.warning(msg)
|
||||
errors.append(msg)
|
||||
continue
|
||||
|
||||
_logger.info(f'Found equipment: {equipment.name} (ID: {equipment.id})')
|
||||
|
||||
# Cari raw material move yang berelasi dengan equipment ini
|
||||
matching_moves = mo_record.move_raw_ids.filtered(
|
||||
lambda m: m.scada_equipment_id.id == equipment.id
|
||||
and m.state not in ['done', 'cancel']
|
||||
all_moves = mo_record.move_raw_ids
|
||||
_logger.info(f'Total raw moves in MO: {len(all_moves)}')
|
||||
|
||||
matching_moves = all_moves.filtered(
|
||||
lambda m: m.scada_equipment_id.id == equipment.id
|
||||
and m.state != 'cancel'
|
||||
)
|
||||
|
||||
_logger.info(f'Matching moves for {equipment_code}: {len(matching_moves)} (equipment_id={equipment.id})')
|
||||
for m in matching_moves:
|
||||
_logger.info(f' - Move {m.id}: {m.product_id.name} (qty_done={m.quantity_done}, state={m.state})')
|
||||
|
||||
if not matching_moves:
|
||||
errors.append(f'{equipment_code}: No raw material move found for this equipment')
|
||||
msg = f'{equipment_code}: No raw material move found for this equipment'
|
||||
_logger.warning(msg)
|
||||
errors.append(msg)
|
||||
continue
|
||||
|
||||
# Apply consumption
|
||||
applied_qty, move_ids = self._apply_consumption_to_moves(
|
||||
# Apply consumption (REPLACE mode: setiap update mengganti nilai lama)
|
||||
_logger.info(f'Applying consumption {consumption_qty} to {len(matching_moves)} moves')
|
||||
applied_qty, move_ids = self._apply_consumption_to_moves_replace(
|
||||
matching_moves, consumption_qty, allow_overconsume=True
|
||||
)
|
||||
_logger.info(f'Applied {applied_qty} to moves {move_ids}')
|
||||
|
||||
# Log consumption untuk setiap material yang di-consume
|
||||
for move in matching_moves:
|
||||
if move.id in move_ids:
|
||||
_logger.info(f'Logging consumption for move {move.id}: {move.quantity_done}')
|
||||
self._log_equipment_material_consumption(
|
||||
equipment=equipment,
|
||||
material=move.product_id,
|
||||
|
||||
@@ -64,6 +64,20 @@
|
||||
name="OEE Results"
|
||||
sequence="1"/>
|
||||
|
||||
<!-- Reports Menu -->
|
||||
<menuitem
|
||||
id="menu_scada_reports"
|
||||
parent="menu_scada_root"
|
||||
name="Reports"
|
||||
sequence="45"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_scada_equipment_failure_report"
|
||||
parent="menu_scada_reports"
|
||||
action="action_scada_equipment_failure"
|
||||
name="Equipment Failure"
|
||||
sequence="1"/>
|
||||
|
||||
<!-- API Logs Menu -->
|
||||
<menuitem
|
||||
id="menu_scada_logs"
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_scada_equipment_failure_tree" model="ir.ui.view">
|
||||
<field name="name">scada.equipment.failure.tree</field>
|
||||
<field name="model">scada.equipment.failure</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Equipment Failure Reports">
|
||||
<field name="date"/>
|
||||
<field name="equipment_id"/>
|
||||
<field name="equipment_code"/>
|
||||
<field name="description"/>
|
||||
<field name="reported_by"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_scada_equipment_failure_form" model="ir.ui.view">
|
||||
<field name="name">scada.equipment.failure.form</field>
|
||||
<field name="model">scada.equipment.failure</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Equipment Failure Report">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="date"/>
|
||||
<field name="equipment_id"/>
|
||||
<field name="equipment_code" readonly="1"/>
|
||||
<field name="reported_by" readonly="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="description"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_scada_equipment_failure_search" model="ir.ui.view">
|
||||
<field name="name">scada.equipment.failure.search</field>
|
||||
<field name="model">scada.equipment.failure</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Equipment Failure">
|
||||
<field name="equipment_id"/>
|
||||
<field name="equipment_code"/>
|
||||
<field name="date"/>
|
||||
<filter name="group_by_equipment" string="Equipment" context="{'group_by': 'equipment_id'}"/>
|
||||
<filter name="group_by_date" string="Date" context="{'group_by': 'date:day'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_scada_equipment_failure_pivot" model="ir.ui.view">
|
||||
<field name="name">scada.equipment.failure.pivot</field>
|
||||
<field name="model">scada.equipment.failure</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Equipment Failure Analysis">
|
||||
<field name="equipment_id" type="row"/>
|
||||
<field name="date" type="col" interval="day"/>
|
||||
<field name="id" type="measure" string="Total Failure"/>
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_scada_equipment_failure_graph" model="ir.ui.view">
|
||||
<field name="name">scada.equipment.failure.graph</field>
|
||||
<field name="model">scada.equipment.failure</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Equipment Failure Graph" type="bar">
|
||||
<field name="equipment_id" type="row"/>
|
||||
<field name="id" type="measure" string="Total Failure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_scada_equipment_failure" model="ir.actions.act_window">
|
||||
<field name="name">Equipment Failure</field>
|
||||
<field name="res_model">scada.equipment.failure</field>
|
||||
<field name="view_mode">tree,form,pivot,graph</field>
|
||||
<field name="search_view_id" ref="view_scada_equipment_failure_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Belum ada data failure equipment.
|
||||
</p>
|
||||
<p>
|
||||
Tambahkan data dari menu ini atau kirim dari API SCADA endpoint.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,65 @@
|
||||
# SCADA Failure Reporting
|
||||
|
||||
Custom module untuk pencatatan laporan failure equipment SCADA.
|
||||
|
||||
## Fitur
|
||||
|
||||
- Model `scada.failure.report`
|
||||
- Relasi `equipment_code` ke `scada.equipment`
|
||||
- Field laporan: `equipment_code`, `description`, `date`
|
||||
- Menu backend: `SCADA > Failure Reporting > Failure Reports`
|
||||
- API input data (JSON)
|
||||
- Form input data (HTTP)
|
||||
|
||||
## Struktur Data
|
||||
|
||||
Model: `scada.failure.report`
|
||||
|
||||
- `equipment_code` (`many2one` ke `scada.equipment`, required)
|
||||
- `description` (`text`, required)
|
||||
- `date` (`datetime`, required, default waktu saat create)
|
||||
|
||||
## HTTP Routes
|
||||
|
||||
### 1. API JSON
|
||||
|
||||
`POST /api/scada/failure-report`
|
||||
|
||||
Auth: user session (`auth='user'`)
|
||||
|
||||
Body:
|
||||
|
||||
```json
|
||||
{
|
||||
"equipment_code": "PLC01",
|
||||
"description": "Motor overload saat proses mixing",
|
||||
"date": "2026-02-15 08:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
Response success:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Failure report created",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"equipment_code": "PLC01",
|
||||
"description": "Motor overload saat proses mixing",
|
||||
"date": "2026-02-15 08:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Form Input
|
||||
|
||||
- `GET /scada/failure-report/input` untuk menampilkan form input
|
||||
- `POST /scada/failure-report/submit` untuk submit form
|
||||
|
||||
## Instalasi
|
||||
|
||||
1. Update Apps List.
|
||||
2. Install module `SCADA Failure Reporting`.
|
||||
3. Pastikan module `grt_scada` sudah ter-install.
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import controllers
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
'name': 'SCADA Failure Reporting',
|
||||
'version': '14.0.1.0.0',
|
||||
'category': 'manufacturing',
|
||||
'license': 'LGPL-3',
|
||||
'author': 'Custom',
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'depends': [
|
||||
'base',
|
||||
'web',
|
||||
'grt_scada',
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/scada_failure_report_views.xml',
|
||||
'views/scada_failure_report_menu.xml',
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
from . import main
|
||||
Binary file not shown.
@@ -0,0 +1,134 @@
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import fields, http
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class ScadaFailureReportController(http.Controller):
|
||||
|
||||
def _extract_payload(self):
|
||||
payload = request.jsonrequest or {}
|
||||
if isinstance(payload, dict) and isinstance(payload.get('params'), dict):
|
||||
return payload['params']
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
|
||||
def _normalize_datetime(self, value):
|
||||
if not value:
|
||||
return fields.Datetime.now()
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
|
||||
cleaned = str(value).strip().replace('T', ' ')
|
||||
# Support format from HTML datetime-local without seconds.
|
||||
if len(cleaned) == 16:
|
||||
cleaned = f"{cleaned}:00"
|
||||
return fields.Datetime.to_datetime(cleaned)
|
||||
|
||||
def _create_failure_report(self, equipment_code_value, description, date_value):
|
||||
if not equipment_code_value:
|
||||
return {'status': 'error', 'message': 'equipment_code is required'}
|
||||
if not description:
|
||||
return {'status': 'error', 'message': 'description is required'}
|
||||
|
||||
equipment = request.env['scada.equipment'].search([
|
||||
('equipment_code', '=', equipment_code_value),
|
||||
], limit=1)
|
||||
if not equipment:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'Equipment with code "{equipment_code_value}" not found',
|
||||
}
|
||||
|
||||
try:
|
||||
report_date = self._normalize_datetime(date_value)
|
||||
except Exception:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Invalid date format. Use YYYY-MM-DD HH:MM:SS or YYYY-MM-DDTHH:MM',
|
||||
}
|
||||
|
||||
report = request.env['scada.failure.report'].create({
|
||||
'equipment_code': equipment.id,
|
||||
'description': description,
|
||||
'date': fields.Datetime.to_string(report_date),
|
||||
})
|
||||
return {
|
||||
'status': 'success',
|
||||
'message': 'Failure report created',
|
||||
'data': {
|
||||
'id': report.id,
|
||||
'equipment_code': equipment.equipment_code,
|
||||
'description': report.description,
|
||||
'date': fields.Datetime.to_string(report.date),
|
||||
},
|
||||
}
|
||||
|
||||
@http.route('/api/scada/failure-report', type='json', auth='user', methods=['POST'])
|
||||
def create_failure_report(self, **kwargs):
|
||||
payload = self._extract_payload()
|
||||
return self._create_failure_report(
|
||||
payload.get('equipment_code'),
|
||||
payload.get('description'),
|
||||
payload.get('date'),
|
||||
)
|
||||
|
||||
@http.route('/scada/failure-report/input', type='http', auth='user', methods=['GET'])
|
||||
def failure_report_input_form(self, **kwargs):
|
||||
equipment_list = request.env['scada.equipment'].search([], order='name asc')
|
||||
options_html = ''.join(
|
||||
f'<option value="{eq.equipment_code}">{eq.equipment_code} - {eq.name}</option>'
|
||||
for eq in equipment_list
|
||||
)
|
||||
html = f"""
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=\"utf-8\" />
|
||||
<title>SCADA Failure Report Input</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Input SCADA Failure Report</h2>
|
||||
<form action=\"/scada/failure-report/submit\" method=\"post\">
|
||||
<input type=\"hidden\" name=\"csrf_token\" value=\"{request.csrf_token()}\" />
|
||||
<label>Equipment Code</label><br/>
|
||||
<select name=\"equipment_code\" required>
|
||||
<option value=\"\">-- Select Equipment --</option>
|
||||
{options_html}
|
||||
</select><br/><br/>
|
||||
|
||||
<label>Description</label><br/>
|
||||
<textarea name=\"description\" rows=\"5\" cols=\"60\" required></textarea><br/><br/>
|
||||
|
||||
<label>Date</label><br/>
|
||||
<input type=\"datetime-local\" name=\"date\" /><br/><br/>
|
||||
|
||||
<button type=\"submit\">Submit</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return request.make_response(html)
|
||||
|
||||
@http.route('/scada/failure-report/submit', type='http', auth='user', methods=['POST'])
|
||||
def submit_failure_report_form(self, **post):
|
||||
result = self._create_failure_report(
|
||||
post.get('equipment_code'),
|
||||
post.get('description'),
|
||||
post.get('date'),
|
||||
)
|
||||
|
||||
if result.get('status') == 'success':
|
||||
body = (
|
||||
'<h3>Failure report berhasil disimpan.</h3>'
|
||||
'<p><a href="/scada/failure-report/input">Input lagi</a></p>'
|
||||
)
|
||||
else:
|
||||
body = (
|
||||
f"<h3>Gagal menyimpan: {result.get('message')}</h3>"
|
||||
'<p><a href="/scada/failure-report/input">Kembali ke form</a></p>'
|
||||
)
|
||||
|
||||
return request.make_response(
|
||||
f"<!doctype html><html><body>{body}</body></html>",
|
||||
headers=[('Content-Type', 'text/html; charset=utf-8')],
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
from . import scada_failure_report
|
||||
Binary file not shown.
@@ -0,0 +1,16 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ScadaFailureReport(models.Model):
|
||||
_name = 'scada.failure.report'
|
||||
_description = 'SCADA Failure Report'
|
||||
_order = 'date desc, id desc'
|
||||
|
||||
equipment_code = fields.Many2one(
|
||||
'scada.equipment',
|
||||
string='Equipment Code',
|
||||
required=True,
|
||||
ondelete='restrict',
|
||||
)
|
||||
description = fields.Text(string='Description', required=True)
|
||||
date = fields.Datetime(string='Date', required=True, default=fields.Datetime.now)
|
||||
@@ -0,0 +1,2 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_scada_failure_report_user,scada.failure.report user,model_scada_failure_report,base.group_user,1,1,1,1
|
||||
|
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<menuitem
|
||||
id="menu_scada_failure_report"
|
||||
parent="grt_scada.menu_scada_root"
|
||||
name="Failure Reporting"
|
||||
sequence="45"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_scada_failure_report_list"
|
||||
parent="menu_scada_failure_report"
|
||||
action="action_scada_failure_report"
|
||||
name="Failure Reports"
|
||||
sequence="1"/>
|
||||
</odoo>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_scada_failure_report_tree" model="ir.ui.view">
|
||||
<field name="name">scada.failure.report.tree</field>
|
||||
<field name="model">scada.failure.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Failure Reports">
|
||||
<field name="equipment_code"/>
|
||||
<field name="description"/>
|
||||
<field name="date"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_scada_failure_report_form" model="ir.ui.view">
|
||||
<field name="name">scada.failure.report.form</field>
|
||||
<field name="model">scada.failure.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Failure Report">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="equipment_code"/>
|
||||
<field name="date"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="description"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_scada_failure_report" model="ir.actions.act_window">
|
||||
<field name="name">Failure Reports</field>
|
||||
<field name="res_model">scada.failure.report</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
</odoo>
|
||||
+15016
-36180
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
@echo off
|
||||
echo ================================================================
|
||||
echo RESTART ODOO WITH MODULE UPGRADE
|
||||
echo ================================================================
|
||||
echo.
|
||||
echo Langkah yang akan dilakukan:
|
||||
echo 1. Kill semua proses Odoo yang berjalan
|
||||
echo 2. Start Odoo dengan upgrade module grt_scada
|
||||
echo.
|
||||
pause
|
||||
|
||||
echo.
|
||||
echo [1/2] Stopping Odoo processes...
|
||||
taskkill /F /IM python.exe /FI "WINDOWTITLE eq Odoo*" 2>nul
|
||||
taskkill /F /IM python.exe /FI "COMMANDLINE eq *odoo-bin*" 2>nul
|
||||
timeout /t 3 /nobreak >nul
|
||||
|
||||
echo.
|
||||
echo [2/2] Starting Odoo with module upgrade...
|
||||
echo.
|
||||
cd C:\odoo14c\server
|
||||
start "Odoo Server" c:\odoo14c\python\python.exe odoo-bin -c C:\addon14\odoo.conf -d manukanjabung -u grt_scada --without-demo=all
|
||||
|
||||
echo.
|
||||
echo ================================================================
|
||||
echo Odoo sedang starting dengan upgrade module grt_scada...
|
||||
echo Tunggu sekitar 30-60 detik hingga Odoo fully loaded
|
||||
echo ================================================================
|
||||
echo.
|
||||
echo Setelah selesai, test endpoint dengan:
|
||||
echo python test_mo_consumption_api.py
|
||||
echo.
|
||||
pause
|
||||
@@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Test Odoo API connection and diagnose issues
|
||||
"""
|
||||
|
||||
import xmlrpc.client
|
||||
import sys
|
||||
|
||||
# Konfigurasi Odoo
|
||||
ODOO_URL = "http://localhost:8070"
|
||||
ODOO_DB = "manukanjabung"
|
||||
ODOO_USERNAME = "admin"
|
||||
ODOO_PASSWORD = "admin"
|
||||
|
||||
def test_connection():
|
||||
"""Test basic connection"""
|
||||
print("=" * 60)
|
||||
print("TEST 1: Koneksi Dasar")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common')
|
||||
version = common.version()
|
||||
print(f"✅ Server terhubung")
|
||||
print(f" Version: {version.get('server_version')}")
|
||||
print(f" Series: {version.get('server_serie')}")
|
||||
return common
|
||||
except Exception as e:
|
||||
print(f"❌ Gagal koneksi: {e}")
|
||||
return None
|
||||
|
||||
def test_authentication(common):
|
||||
"""Test authentication"""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST 2: Autentikasi")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
uid = common.authenticate(ODOO_DB, ODOO_USERNAME, ODOO_PASSWORD, {})
|
||||
|
||||
if uid:
|
||||
print(f"✅ Autentikasi berhasil")
|
||||
print(f" User ID: {uid}")
|
||||
print(f" Database: {ODOO_DB}")
|
||||
return uid
|
||||
else:
|
||||
print(f"❌ Autentikasi gagal")
|
||||
print(f" Periksa username/password")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"❌ Error autentikasi: {e}")
|
||||
return None
|
||||
|
||||
def test_model_access(uid):
|
||||
"""Test model access"""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST 3: Akses Model")
|
||||
print("=" * 60)
|
||||
|
||||
models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object')
|
||||
|
||||
test_models = [
|
||||
'res.partner',
|
||||
'res.users',
|
||||
'ir.attachment',
|
||||
'account.move',
|
||||
'account.account'
|
||||
]
|
||||
|
||||
for model in test_models:
|
||||
try:
|
||||
# Test search access
|
||||
result = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
model, 'search',
|
||||
[[]], {'limit': 1}
|
||||
)
|
||||
print(f" ✅ {model}: Access OK")
|
||||
except Exception as e:
|
||||
print(f" ❌ {model}: {str(e)[:50]}")
|
||||
|
||||
return models
|
||||
|
||||
def test_attachment_operations(models, uid):
|
||||
"""Test attachment operations specifically"""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST 4: Operasi Attachment")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
# Count attachments
|
||||
count = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'ir.attachment', 'search_count',
|
||||
[[]]
|
||||
)
|
||||
print(f" ✅ Total attachment: {count}")
|
||||
|
||||
# Get sample attachments
|
||||
att_ids = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'ir.attachment', 'search',
|
||||
[[]], {'limit': 5}
|
||||
)
|
||||
|
||||
if att_ids:
|
||||
attachments = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'ir.attachment', 'read',
|
||||
[att_ids],
|
||||
{'fields': ['id', 'name', 'type', 'store_fname']}
|
||||
)
|
||||
|
||||
print(f"\n Sample 5 attachment:")
|
||||
for att in attachments:
|
||||
storage = "filestore" if att.get('store_fname') else "database"
|
||||
print(f" - ID {att['id']}: {att['name']} [{storage}]")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
return False
|
||||
|
||||
def test_read_operations(models, uid):
|
||||
"""Test reading user data (common failing operation)"""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST 5: Operasi Read User/Partner")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
# Read current user
|
||||
user = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'res.users', 'read',
|
||||
[[uid]],
|
||||
{'fields': ['id', 'name', 'login', 'partner_id']}
|
||||
)[0]
|
||||
|
||||
print(f" ✅ User read OK: {user['name']}")
|
||||
|
||||
# Try to read user image (often fails with missing files)
|
||||
try:
|
||||
user_with_image = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'res.users', 'read',
|
||||
[[uid]],
|
||||
{'fields': ['id', 'name', 'image_128']}
|
||||
)[0]
|
||||
|
||||
if user_with_image.get('image_128'):
|
||||
print(f" ✅ User image read OK")
|
||||
else:
|
||||
print(f" ⚠️ User tidak punya image")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ User image read gagal: {str(e)[:100]}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
return False
|
||||
|
||||
def test_write_operations(models, uid):
|
||||
"""Test write operations"""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST 6: Operasi Write (Update)")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
# Try to update user's own data (safe test)
|
||||
# Just update something harmless like notification_type
|
||||
result = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'res.users', 'write',
|
||||
[[uid], {'notification_type': 'inbox'}] # Just set to inbox
|
||||
)
|
||||
|
||||
if result:
|
||||
print(f" ✅ Write operation berhasil")
|
||||
else:
|
||||
print(f" ❌ Write operation gagal")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error write: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("ODOO API CONNECTION TEST & DIAGNOSTIC")
|
||||
print("=" * 60)
|
||||
print(f"\nKonfigurasi:")
|
||||
print(f" URL: {ODOO_URL}")
|
||||
print(f" Database: {ODOO_DB}")
|
||||
print(f" Username: {ODOO_USERNAME}")
|
||||
print()
|
||||
|
||||
# Test 1: Connection
|
||||
common = test_connection()
|
||||
if not common:
|
||||
return
|
||||
|
||||
# Test 2: Authentication
|
||||
uid = test_authentication(common)
|
||||
if not uid:
|
||||
return
|
||||
|
||||
# Test 3: Model Access
|
||||
models = test_model_access(uid)
|
||||
|
||||
# Test 4: Attachment Operations
|
||||
test_attachment_operations(models, uid)
|
||||
|
||||
# Test 5: Read Operations
|
||||
test_read_operations(models, uid)
|
||||
|
||||
# Test 6: Write Operations
|
||||
test_write_operations(models, uid)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("KESIMPULAN")
|
||||
print("=" * 60)
|
||||
print("""
|
||||
Jika ada error pada TEST 4 atau TEST 5 terkait attachment/image,
|
||||
jalankan script: python fix_missing_attachments.py
|
||||
|
||||
Jika ada error pada TEST 6 (write operations),
|
||||
periksa permission dan access rights untuk user.
|
||||
""")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,371 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Test endpoint /api/scada/mo/update-with-consumptions
|
||||
Untuk diagnosa masalah update consumption MO
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# Konfigurasi
|
||||
ODOO_URL = "http://localhost:8070"
|
||||
DATABASE = "manukanjabung"
|
||||
USERNAME = "admin"
|
||||
PASSWORD = "admin"
|
||||
|
||||
def login_odoo():
|
||||
"""Login ke Odoo dan dapatkan session cookie"""
|
||||
print("=" * 70)
|
||||
print("STEP 1: LOGIN KE ODOO")
|
||||
print("=" * 70)
|
||||
|
||||
session = requests.Session()
|
||||
|
||||
# Login endpoint
|
||||
login_url = f"{ODOO_URL}/web/session/authenticate"
|
||||
|
||||
login_payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"params": {
|
||||
"db": DATABASE,
|
||||
"login": USERNAME,
|
||||
"password": PASSWORD
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
response = session.post(
|
||||
login_url,
|
||||
json=login_payload,
|
||||
headers={'Content-Type': 'application/json'}
|
||||
)
|
||||
|
||||
result = response.json()
|
||||
|
||||
if result.get('result') and result['result'].get('uid'):
|
||||
print(f"✅ Login berhasil")
|
||||
print(f" User ID: {result['result']['uid']}")
|
||||
print(f" Session ID: {result['result'].get('session_id', 'N/A')[:20]}...")
|
||||
return session, result['result']
|
||||
else:
|
||||
print(f"❌ Login gagal: {result}")
|
||||
return None, None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error login: {e}")
|
||||
return None, None
|
||||
|
||||
def get_active_mo(session):
|
||||
"""Ambil MO aktif untuk test"""
|
||||
print("\n" + "=" * 70)
|
||||
print("STEP 2: AMBIL MO AKTIF UNTUK TEST")
|
||||
print("=" * 70)
|
||||
|
||||
url = f"{ODOO_URL}/api/scada/mo-list-confirmed"
|
||||
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"params": {
|
||||
"limit": 5
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
response = session.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={'Content-Type': 'application/json'}
|
||||
)
|
||||
|
||||
print(f"\n📥 Response Status: {response.status_code}")
|
||||
result = response.json()
|
||||
|
||||
if result.get('result'):
|
||||
res = result['result']
|
||||
|
||||
# Response structure: {status: ..., count: ..., data: [...]}
|
||||
if res.get('status') == 'success' and res.get('data'):
|
||||
mos = res['data']
|
||||
|
||||
if isinstance(mos, list) and len(mos) > 0:
|
||||
mo = mos[0]
|
||||
print(f"✅ Found {len(mos)} MO untuk test")
|
||||
print(f" Using first MO: {mo.get('mo_id')}")
|
||||
print(f" Product: {mo.get('product')}")
|
||||
print(f" Qty: {mo.get('quantity', 'N/A')}")
|
||||
|
||||
# Return with name key for compatibility
|
||||
return {'name': mo.get('mo_id'), '_raw': mo}
|
||||
else:
|
||||
print("❌ Tidak ada MO confirmed")
|
||||
return None
|
||||
else:
|
||||
print(f"❌ Error: {res}")
|
||||
return None
|
||||
else:
|
||||
print(f"❌ Error: {result}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def get_equipment_list(session):
|
||||
"""Get equipment codes from MO raw materials"""
|
||||
print("\n" + "=" * 70)
|
||||
print("STEP 3: AMBIL EQUIPMENT CODES DARI MO")
|
||||
print("=" * 70)
|
||||
|
||||
# Equipment codes akan diambil dari raw materials MO yang punya equipment_code
|
||||
# Tidak perlu endpoint terpisah
|
||||
print("✅ Will use equipment codes from MO raw materials")
|
||||
|
||||
return []
|
||||
|
||||
def test_update_consumption(session, mo_id, equipment_codes):
|
||||
"""Test update consumption"""
|
||||
print("\n" + "=" * 70)
|
||||
print("STEP 4: TEST UPDATE CONSUMPTION")
|
||||
print("=" * 70)
|
||||
|
||||
url = f"{ODOO_URL}/api/scada/mo/update-with-consumptions"
|
||||
|
||||
# Buat payload test
|
||||
test_data = {
|
||||
"mo_id": mo_id,
|
||||
"quantity": None, # Tidak update quantity, hanya consumption
|
||||
}
|
||||
|
||||
# Tambahkan consumption untuk beberapa equipment (nilai kecil untuk test)
|
||||
for i, eq_code in enumerate(equipment_codes[:3]): # Test 3 equipment pertama
|
||||
test_data[eq_code] = 10.0 + i # 10, 11, 12 kg
|
||||
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"params": test_data
|
||||
}
|
||||
|
||||
print(f"\n📤 Sending request:")
|
||||
print(f" URL: {url}")
|
||||
print(f" Payload: {json.dumps(test_data, indent=2)}")
|
||||
|
||||
try:
|
||||
response = session.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={'Content-Type': 'application/json'}
|
||||
)
|
||||
|
||||
print(f"\n📥 Response Status: {response.status_code}")
|
||||
|
||||
result = response.json()
|
||||
print(f"\n📥 Response Body:")
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
# Parse result
|
||||
if result.get('result'):
|
||||
res = result['result']
|
||||
|
||||
if res.get('status') == 'success':
|
||||
print(f"\n✅ UPDATE BERHASIL!")
|
||||
print(f" MO: {res.get('mo_id')}")
|
||||
print(f" State: {res.get('mo_state')}")
|
||||
|
||||
consumed_items = res.get('consumed_items', [])
|
||||
print(f"\n Consumed items ({len(consumed_items)}):")
|
||||
for item in consumed_items:
|
||||
print(f" - {item.get('equipment_code')}: {item.get('applied_qty')} kg")
|
||||
print(f" Products: {', '.join(item.get('products', []))}")
|
||||
print(f" Move IDs: {item.get('move_ids')}")
|
||||
|
||||
if res.get('errors'):
|
||||
print(f"\n⚠️ Errors:")
|
||||
for err in res['errors']:
|
||||
print(f" - {err}")
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f"\n❌ UPDATE GAGAL!")
|
||||
print(f" Message: {res.get('message')}")
|
||||
|
||||
if res.get('errors'):
|
||||
print(f"\n Errors:")
|
||||
for err in res['errors']:
|
||||
print(f" - {err}")
|
||||
|
||||
return False
|
||||
else:
|
||||
print(f"\n❌ Response tidak valid")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def diagnose_mo_structure(mo_id):
|
||||
"""Diagnosa struktur MO untuk debugging via XML-RPC"""
|
||||
print("\n" + "=" * 70)
|
||||
print("DIAGNOSA: CEK STRUKTUR MO")
|
||||
print("=" * 70)
|
||||
|
||||
try:
|
||||
import xmlrpc.client
|
||||
|
||||
common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common')
|
||||
uid = common.authenticate(DATABASE, USERNAME, PASSWORD, {})
|
||||
|
||||
if not uid:
|
||||
print("❌ Gagal autentikasi")
|
||||
return False
|
||||
|
||||
models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object')
|
||||
|
||||
# Find MO
|
||||
mo_ids = models.execute_kw(
|
||||
DATABASE, uid, PASSWORD,
|
||||
'mrp.production', 'search',
|
||||
[[('name', '=', mo_id)]]
|
||||
)
|
||||
|
||||
if not mo_ids:
|
||||
print(f"❌ MO {mo_id} tidak ditemukan")
|
||||
return False
|
||||
|
||||
# Read MO with raw materials
|
||||
mo = models.execute_kw(
|
||||
DATABASE, uid, PASSWORD,
|
||||
'mrp.production', 'read',
|
||||
[mo_ids],
|
||||
{'fields': ['id', 'name', 'state', 'product_id', 'product_qty', 'move_raw_ids']}
|
||||
)[0]
|
||||
|
||||
print(f"\n📊 MO Detail:")
|
||||
print(f" ID: {mo.get('id')}")
|
||||
print(f" Name: {mo.get('name')}")
|
||||
print(f" State: {mo.get('state')}")
|
||||
print(f" Product: {mo.get('product_id', [False, 'N/A'])[1]}")
|
||||
print(f" Qty: {mo.get('product_qty')}")
|
||||
|
||||
# Read raw materials (stock.move)
|
||||
move_ids = mo.get('move_raw_ids', [])
|
||||
|
||||
if move_ids:
|
||||
moves = models.execute_kw(
|
||||
DATABASE, uid, PASSWORD,
|
||||
'stock.move', 'read',
|
||||
[move_ids],
|
||||
{'fields': ['id', 'product_id', 'product_uom_qty', 'quantity_done', 'state', 'scada_equipment_id']}
|
||||
)
|
||||
|
||||
print(f"\n📦 Raw Materials ({len(moves)}):")
|
||||
|
||||
# Group by equipment
|
||||
by_equipment = {}
|
||||
no_equipment = []
|
||||
|
||||
for move in moves:
|
||||
eq_id = move.get('scada_equipment_id')
|
||||
if eq_id and eq_id[0]: # [id, name]
|
||||
eq_code = eq_id[1] # Use name as code for display
|
||||
if eq_code not in by_equipment:
|
||||
by_equipment[eq_code] = []
|
||||
by_equipment[eq_code].append(move)
|
||||
else:
|
||||
no_equipment.append(move)
|
||||
|
||||
if by_equipment:
|
||||
print(f"\n Materials WITH equipment code ({len(by_equipment)} equipment):")
|
||||
for eq_code, materials in by_equipment.items():
|
||||
print(f"\n Equipment: {eq_code}")
|
||||
for mat in materials:
|
||||
print(f" - {mat.get('product_id', [False, 'N/A'])[1]}")
|
||||
print(f" Move ID: {mat.get('id')}")
|
||||
print(f" Planned: {mat.get('product_uom_qty')}, Done: {mat.get('quantity_done', 0)}")
|
||||
print(f" State: {mat.get('state')}")
|
||||
|
||||
if no_equipment:
|
||||
print(f"\n Materials WITHOUT equipment code ({len(no_equipment)}):")
|
||||
for mat in no_equipment:
|
||||
prod_name = mat.get('product_id', [False, 'N/A'])[1]
|
||||
print(f" - {prod_name}: {mat.get('product_uom_qty')}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print("TEST ENDPOINT: /api/scada/mo/update-with-consumptions")
|
||||
print("=" * 70)
|
||||
print(f"\nKonfigurasi:")
|
||||
print(f" URL: {ODOO_URL}")
|
||||
print(f" Database: {DATABASE}")
|
||||
print(f" Username: {USERNAME}")
|
||||
print()
|
||||
|
||||
# Step 1: Login
|
||||
session, user_info = login_odoo()
|
||||
if not session:
|
||||
print("\n❌ GAGAL LOGIN - Test dibatalkan")
|
||||
return
|
||||
|
||||
# Step 2: Get active MO
|
||||
mo = get_active_mo(session)
|
||||
if not mo:
|
||||
print("\n❌ TIDAK ADA MO - Test dibatalkan")
|
||||
return
|
||||
|
||||
mo_id = mo.get('name')
|
||||
|
||||
# Step 3: Get equipment codes from MO raw materials
|
||||
get_equipment_list(session)
|
||||
|
||||
# Diagnosa struktur MO untuk mendapat equipment codes
|
||||
print("\n" + "=" * 70)
|
||||
print("STEP 3B: DIAGNOSA MO STRUCTURE")
|
||||
print("=" * 70)
|
||||
|
||||
equipment_codes = []
|
||||
diagnose_success = diagnose_mo_structure(mo_id)
|
||||
|
||||
if not diagnose_success:
|
||||
print("⚠️ Gagal diagnosa MO, akan coba dengan equipment code manual")
|
||||
# Fallback: coba dengan equipment code yang sering dipakai
|
||||
equipment_codes = ['silo101', 'silo102', 'silo103']
|
||||
print(f" Using test equipment codes: {equipment_codes}")
|
||||
|
||||
# Step 4: Test update
|
||||
if equipment_codes or True: # Always try even without equipment codes
|
||||
# Use hardcoded equipment codes for testing
|
||||
if not equipment_codes:
|
||||
equipment_codes = ['silo101', 'silo102']
|
||||
|
||||
success = test_update_consumption(session, mo_id, equipment_codes)
|
||||
|
||||
if success:
|
||||
print("\n" + "=" * 70)
|
||||
print("✅ TEST BERHASIL - API BERFUNGSI DENGAN BAIK")
|
||||
print("=" * 70)
|
||||
else:
|
||||
print("\n" + "=" * 70)
|
||||
print("❌ TEST GAGAL - PERIKSA ERROR DI ATAS")
|
||||
print("=" * 70)
|
||||
else:
|
||||
print("\n⚠️ Tidak bisa test update: tidak ada equipment code")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("TEST SELESAI")
|
||||
print("=" * 70)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Test update consumption untuk MO dengan state 'progress'
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
|
||||
ODOO_URL = "http://localhost:8070"
|
||||
DATABASE = "manukanjabung"
|
||||
USERNAME = "admin"
|
||||
PASSWORD = "admin"
|
||||
|
||||
def login_odoo():
|
||||
"""Login ke Odoo"""
|
||||
session = requests.Session()
|
||||
|
||||
login_url = f"{ODOO_URL}/web/session/authenticate"
|
||||
|
||||
login_payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"params": {
|
||||
"db": DATABASE,
|
||||
"login": USERNAME,
|
||||
"password": PASSWORD
|
||||
}
|
||||
}
|
||||
|
||||
response = session.post(
|
||||
login_url,
|
||||
json=login_payload,
|
||||
headers={'Content-Type': 'application/json'}
|
||||
)
|
||||
|
||||
result = response.json()
|
||||
|
||||
if result.get('result') and result['result'].get('uid'):
|
||||
print(f"✅ Login berhasil (User ID: {result['result']['uid']})")
|
||||
return session
|
||||
else:
|
||||
print(f"❌ Login gagal")
|
||||
return None
|
||||
|
||||
def test_update_progress_mo(session):
|
||||
"""Test update MO dengan state 'progress'"""
|
||||
print("\n" + "=" * 70)
|
||||
print("TEST: UPDATE MO DENGAN STATE 'PROGRESS'")
|
||||
print("=" * 70)
|
||||
|
||||
# MO WH/MO/00001 sudah dalam state 'progress'
|
||||
mo_id = "WH/MO/00001"
|
||||
|
||||
url = f"{ODOO_URL}/api/scada/mo/update-with-consumptions"
|
||||
|
||||
test_data = {
|
||||
"mo_id": mo_id,
|
||||
"silo101": 15.0, # Test update 15 kg
|
||||
"silo102": 20.0, # Test update 20 kg
|
||||
}
|
||||
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"params": test_data
|
||||
}
|
||||
|
||||
print(f"\n📤 Updating MO: {mo_id} (state: progress)")
|
||||
print(f" Payload: {json.dumps(test_data, indent=2)}")
|
||||
|
||||
response = session.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={'Content-Type': 'application/json'}
|
||||
)
|
||||
|
||||
result = response.json()
|
||||
|
||||
if result.get('result'):
|
||||
res = result['result']
|
||||
|
||||
if res.get('status') == 'success':
|
||||
print(f"\n✅ UPDATE BERHASIL untuk MO state 'progress'!")
|
||||
print(f" MO: {res.get('mo_id')}")
|
||||
print(f" State: {res.get('mo_state')}")
|
||||
|
||||
consumed_items = res.get('consumed_items', [])
|
||||
print(f"\n Consumed items ({len(consumed_items)}):")
|
||||
for item in consumed_items:
|
||||
print(f" - {item.get('equipment_code')}: {item.get('applied_qty')} kg")
|
||||
print(f" Products: {', '.join(item.get('products', []))}")
|
||||
|
||||
if res.get('errors'):
|
||||
print(f"\n⚠️ Errors:")
|
||||
for err in res['errors']:
|
||||
print(f" - {err}")
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f"\n❌ UPDATE GAGAL!")
|
||||
print(f" Message: {res.get('message')}")
|
||||
return False
|
||||
else:
|
||||
print(f"\n❌ Response error: {result}")
|
||||
return False
|
||||
|
||||
def test_update_confirmed_again(session):
|
||||
"""Test update MO confirmed lagi (untuk perbandingan)"""
|
||||
print("\n" + "=" * 70)
|
||||
print("TEST: UPDATE MO DENGAN STATE 'CONFIRMED'")
|
||||
print("=" * 70)
|
||||
|
||||
# MO WH/MO/00004 masih confirmed
|
||||
mo_id = "WH/MO/00004"
|
||||
|
||||
url = f"{ODOO_URL}/api/scada/mo/update-with-consumptions"
|
||||
|
||||
test_data = {
|
||||
"mo_id": mo_id,
|
||||
"silo101": 25.0,
|
||||
"silo103": 30.0,
|
||||
}
|
||||
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"params": test_data
|
||||
}
|
||||
|
||||
print(f"\n📤 Updating MO: {mo_id} (state: confirmed)")
|
||||
print(f" Payload: {json.dumps(test_data, indent=2)}")
|
||||
|
||||
response = session.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={'Content-Type': 'application/json'}
|
||||
)
|
||||
|
||||
result = response.json()
|
||||
|
||||
if result.get('result'):
|
||||
res = result['result']
|
||||
|
||||
if res.get('status') == 'success':
|
||||
print(f"\n✅ UPDATE BERHASIL untuk MO state 'confirmed'!")
|
||||
print(f" MO: {res.get('mo_id')}")
|
||||
print(f" State: {res.get('mo_state')}")
|
||||
|
||||
consumed_items = res.get('consumed_items', [])
|
||||
print(f"\n Consumed items ({len(consumed_items)}):")
|
||||
for item in consumed_items:
|
||||
print(f" - {item.get('equipment_code')}: {item.get('applied_qty')} kg")
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f"\n❌ UPDATE GAGAL!")
|
||||
print(f" Message: {res.get('message')}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print("TEST: UPDATE CONSUMPTION - CONFIRMED vs PROGRESS")
|
||||
print("=" * 70)
|
||||
|
||||
session = login_odoo()
|
||||
if not session:
|
||||
return
|
||||
|
||||
# Test 1: Update MO yang sudah 'progress'
|
||||
success1 = test_update_progress_mo(session)
|
||||
|
||||
# Test 2: Update MO yang masih 'confirmed'
|
||||
success2 = test_update_confirmed_again(session)
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("KESIMPULAN")
|
||||
print("=" * 70)
|
||||
|
||||
if success1 and success2:
|
||||
print("\n✅ Endpoint bisa update MO dengan state:")
|
||||
print(" - 'confirmed' ✅")
|
||||
print(" - 'progress' ✅")
|
||||
print("\n📝 Catatan:")
|
||||
print(" - Ketika MO 'confirmed' di-update, state otomatis menjadi 'progress'")
|
||||
print(" - MO 'progress' bisa terus di-update berkali-kali")
|
||||
print(" - Tidak ada pembatasan state di endpoint ini")
|
||||
elif success1:
|
||||
print("\n✅ MO 'progress' bisa di-update")
|
||||
print("❌ MO 'confirmed' gagal di-update")
|
||||
elif success2:
|
||||
print("\n❌ MO 'progress' gagal di-update")
|
||||
print("✅ MO 'confirmed' bisa di-update")
|
||||
else:
|
||||
print("\n❌ Kedua test gagal")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Contoh script untuk update data via Odoo API
|
||||
Demonstrasi CRUD operations
|
||||
"""
|
||||
|
||||
import xmlrpc.client
|
||||
from datetime import datetime
|
||||
|
||||
# Konfigurasi Odoo
|
||||
ODOO_URL = "http://localhost:8070"
|
||||
ODOO_DB = "manukanjabung"
|
||||
ODOO_USERNAME = "admin"
|
||||
ODOO_PASSWORD = "admin"
|
||||
|
||||
def connect_odoo():
|
||||
"""Koneksi ke Odoo"""
|
||||
common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common')
|
||||
uid = common.authenticate(ODOO_DB, ODOO_USERNAME, ODOO_PASSWORD, {})
|
||||
models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object')
|
||||
return uid, models
|
||||
|
||||
def contoh_update_partner(models, uid):
|
||||
"""Contoh 1: Update data partner"""
|
||||
print("\n" + "=" * 60)
|
||||
print("CONTOH 1: UPDATE PARTNER")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
# Cari partner pertama
|
||||
partner_ids = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'res.partner', 'search',
|
||||
[[('customer_rank', '>', 0)]], {'limit': 1}
|
||||
)
|
||||
|
||||
if not partner_ids:
|
||||
print("❌ Tidak ada partner customer")
|
||||
return
|
||||
|
||||
partner_id = partner_ids[0]
|
||||
|
||||
# Baca data sebelum update
|
||||
partner_before = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'res.partner', 'read',
|
||||
[[partner_id]],
|
||||
{'fields': ['name', 'phone', 'email']}
|
||||
)[0]
|
||||
|
||||
print(f"\n📊 Partner ID {partner_id} SEBELUM update:")
|
||||
print(f" Name: {partner_before['name']}")
|
||||
print(f" Phone: {partner_before.get('phone', '-')}")
|
||||
print(f" Email: {partner_before.get('email', '-')}")
|
||||
|
||||
# Update data
|
||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||
result = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'res.partner', 'write',
|
||||
[[partner_id], {
|
||||
'phone': f'0812-3456-7890 (updated {timestamp})',
|
||||
'comment': f'Updated via API at {timestamp}'
|
||||
}]
|
||||
)
|
||||
|
||||
if result:
|
||||
# Baca data setelah update
|
||||
partner_after = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'res.partner', 'read',
|
||||
[[partner_id]],
|
||||
{'fields': ['name', 'phone', 'email', 'comment']}
|
||||
)[0]
|
||||
|
||||
print(f"\n✅ UPDATE BERHASIL!")
|
||||
print(f"\n📊 Partner ID {partner_id} SESUDAH update:")
|
||||
print(f" Name: {partner_after['name']}")
|
||||
print(f" Phone: {partner_after.get('phone', '-')}")
|
||||
print(f" Email: {partner_after.get('email', '-')}")
|
||||
print(f" Comment: {partner_after.get('comment', '-')}")
|
||||
else:
|
||||
print("❌ Update gagal")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
|
||||
def contoh_create_partner(models, uid):
|
||||
"""Contoh 2: Create partner baru"""
|
||||
print("\n" + "=" * 60)
|
||||
print("CONTOH 2: CREATE PARTNER BARU")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
new_partner = {
|
||||
'name': f'Test Partner API {timestamp}',
|
||||
'email': 'test@example.com',
|
||||
'phone': '0812-1234-5678',
|
||||
'street': 'Jl. Test No. 123',
|
||||
'city': 'Jakarta',
|
||||
'country_id': 100, # Indonesia
|
||||
'is_company': False,
|
||||
'customer_rank': 1,
|
||||
}
|
||||
|
||||
partner_id = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'res.partner', 'create',
|
||||
[new_partner]
|
||||
)
|
||||
|
||||
print(f"\n✅ CREATE BERHASIL!")
|
||||
print(f" New Partner ID: {partner_id}")
|
||||
|
||||
# Baca data yang baru dibuat
|
||||
partner = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'res.partner', 'read',
|
||||
[[partner_id]],
|
||||
{'fields': ['name', 'email', 'phone', 'city']}
|
||||
)[0]
|
||||
|
||||
print(f"\n📊 Data Partner Baru:")
|
||||
print(f" Name: {partner['name']}")
|
||||
print(f" Email: {partner.get('email', '-')}")
|
||||
print(f" Phone: {partner.get('phone', '-')}")
|
||||
print(f" City: {partner.get('city', '-')}")
|
||||
|
||||
return partner_id
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return None
|
||||
|
||||
def contoh_search_filter(models, uid):
|
||||
"""Contoh 3: Search dengan filter"""
|
||||
print("\n" + "=" * 60)
|
||||
print("CONTOH 3: SEARCH DENGAN FILTER")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
# Search partner dengan filter
|
||||
domain = [
|
||||
['customer_rank', '>', 0], # Adalah customer
|
||||
'|', # OR condition
|
||||
['city', 'ilike', 'jakarta'],
|
||||
['city', 'ilike', 'bandung']
|
||||
]
|
||||
|
||||
partner_ids = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'res.partner', 'search',
|
||||
[domain],
|
||||
{'limit': 5}
|
||||
)
|
||||
|
||||
print(f"\n✅ Ditemukan {len(partner_ids)} partner")
|
||||
|
||||
if partner_ids:
|
||||
partners = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'res.partner', 'read',
|
||||
[partner_ids],
|
||||
{'fields': ['name', 'city', 'phone']}
|
||||
)
|
||||
|
||||
print("\n📊 Hasil pencarian:")
|
||||
for p in partners:
|
||||
print(f" - {p['name']} ({p.get('city', '-')}): {p.get('phone', '-')}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
|
||||
def contoh_delete_partner(models, uid, partner_id):
|
||||
"""Contoh 4: Delete partner"""
|
||||
print("\n" + "=" * 60)
|
||||
print("CONTOH 4: DELETE PARTNER")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
if not partner_id:
|
||||
print("❌ Tidak ada partner ID untuk dihapus")
|
||||
return
|
||||
|
||||
# Verifikasi partner exists
|
||||
exists = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'res.partner', 'search_count',
|
||||
[[('id', '=', partner_id)]]
|
||||
)
|
||||
|
||||
if not exists:
|
||||
print(f"❌ Partner ID {partner_id} tidak ditemukan")
|
||||
return
|
||||
|
||||
# Delete
|
||||
result = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'res.partner', 'unlink',
|
||||
[[partner_id]]
|
||||
)
|
||||
|
||||
if result:
|
||||
print(f"✅ DELETE BERHASIL!")
|
||||
print(f" Partner ID {partner_id} telah dihapus")
|
||||
else:
|
||||
print(f"❌ Delete gagal")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("ODOO API UPDATE OPERATIONS TEST")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
# Connect
|
||||
uid, models = connect_odoo()
|
||||
print(f"✅ Terkoneksi sebagai User ID: {uid}")
|
||||
|
||||
# Contoh 1: Update partner existing
|
||||
contoh_update_partner(models, uid)
|
||||
|
||||
# Contoh 2: Create partner baru
|
||||
new_partner_id = contoh_create_partner(models, uid)
|
||||
|
||||
# Contoh 3: Search dengan filter
|
||||
contoh_search_filter(models, uid)
|
||||
|
||||
# Contoh 4: Delete partner yang baru dibuat (cleanup)
|
||||
if new_partner_id:
|
||||
print("\n⚠️ Membersihkan data test...")
|
||||
contoh_delete_partner(models, uid, new_partner_id)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ SEMUA TEST API BERHASIL!")
|
||||
print("=" * 60)
|
||||
print("""
|
||||
KESIMPULAN:
|
||||
- API connection: ✅ OK
|
||||
- CREATE operation: ✅ OK
|
||||
- READ operation: ✅ OK
|
||||
- UPDATE operation: ✅ OK
|
||||
- DELETE operation: ✅ OK
|
||||
- SEARCH/FILTER: ✅ OK
|
||||
|
||||
API Anda sekarang berfungsi dengan baik!
|
||||
Gunakan script ini sebagai template untuk operasi API Anda.
|
||||
""")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,279 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Test update consumption untuk WH/MO/00001 (progress) dengan berbagai skenario
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import xmlrpc.client
|
||||
|
||||
ODOO_URL = "http://localhost:8070"
|
||||
DATABASE = "manukanjabung"
|
||||
USERNAME = "admin"
|
||||
PASSWORD = "admin"
|
||||
|
||||
def login_odoo():
|
||||
"""Login ke Odoo"""
|
||||
session = requests.Session()
|
||||
|
||||
login_url = f"{ODOO_URL}/web/session/authenticate"
|
||||
|
||||
login_payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"params": {
|
||||
"db": DATABASE,
|
||||
"login": USERNAME,
|
||||
"password": PASSWORD
|
||||
}
|
||||
}
|
||||
|
||||
response = session.post(
|
||||
login_url,
|
||||
json=login_payload,
|
||||
headers={'Content-Type': 'application/json'}
|
||||
)
|
||||
|
||||
result = response.json()
|
||||
|
||||
if result.get('result') and result['result'].get('uid'):
|
||||
print(f"✅ Login berhasil (User ID: {result['result']['uid']})")
|
||||
return session
|
||||
else:
|
||||
print(f"❌ Login gagal")
|
||||
return None
|
||||
|
||||
def check_mo_current_state(mo_id):
|
||||
"""Cek state dan consumption MO saat ini via XML-RPC"""
|
||||
print("\n" + "=" * 70)
|
||||
print(f"CEK STATE CURRENT: {mo_id}")
|
||||
print("=" * 70)
|
||||
|
||||
try:
|
||||
common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common')
|
||||
uid = common.authenticate(DATABASE, USERNAME, PASSWORD, {})
|
||||
|
||||
models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object')
|
||||
|
||||
# Find MO
|
||||
mo_ids = models.execute_kw(
|
||||
DATABASE, uid, PASSWORD,
|
||||
'mrp.production', 'search',
|
||||
[[('name', '=', mo_id)]]
|
||||
)
|
||||
|
||||
if not mo_ids:
|
||||
print(f"❌ MO {mo_id} tidak ditemukan")
|
||||
return None
|
||||
|
||||
# Read MO
|
||||
mo = models.execute_kw(
|
||||
DATABASE, uid, PASSWORD,
|
||||
'mrp.production', 'read',
|
||||
[mo_ids],
|
||||
{'fields': ['name', 'state', 'product_id', 'product_qty', 'move_raw_ids']}
|
||||
)[0]
|
||||
|
||||
print(f"\n📊 MO Current State:")
|
||||
print(f" Name: {mo['name']}")
|
||||
print(f" Product: {mo['product_id'][1]}")
|
||||
print(f" Qty: {mo['product_qty']}")
|
||||
print(f" State: {mo['state']}")
|
||||
|
||||
# Read raw materials
|
||||
move_ids = mo.get('move_raw_ids', [])
|
||||
|
||||
if move_ids:
|
||||
moves = models.execute_kw(
|
||||
DATABASE, uid, PASSWORD,
|
||||
'stock.move', 'read',
|
||||
[move_ids],
|
||||
{'fields': ['id', 'product_id', 'product_uom_qty', 'quantity_done', 'scada_equipment_id']}
|
||||
)
|
||||
|
||||
print(f"\n📦 Current Consumption:")
|
||||
|
||||
# Filter moves with equipment only
|
||||
moves_with_eq = [m for m in moves if m.get('scada_equipment_id')]
|
||||
|
||||
if moves_with_eq:
|
||||
for move in moves_with_eq[:10]: # Show first 10
|
||||
eq_name = move['scada_equipment_id'][1] if move['scada_equipment_id'] else 'No Equipment'
|
||||
prod_name = move['product_id'][1]
|
||||
planned = move['product_uom_qty']
|
||||
done = move['quantity_done']
|
||||
|
||||
if done > 0:
|
||||
print(f" - {eq_name}: {prod_name}")
|
||||
print(f" Planned: {planned} kg, Done: {done} kg")
|
||||
else:
|
||||
print(" Belum ada consumption")
|
||||
|
||||
return mo
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return None
|
||||
|
||||
def test_update_consumption(session, mo_id, consumption_data, test_name):
|
||||
"""Test update consumption dengan data tertentu"""
|
||||
print("\n" + "=" * 70)
|
||||
print(f"TEST: {test_name}")
|
||||
print("=" * 70)
|
||||
|
||||
url = f"{ODOO_URL}/api/scada/mo/update-with-consumptions"
|
||||
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"params": {
|
||||
"mo_id": mo_id,
|
||||
**consumption_data
|
||||
}
|
||||
}
|
||||
|
||||
print(f"\n📤 Request:")
|
||||
print(f" MO: {mo_id}")
|
||||
print(f" Consumption:")
|
||||
for key, value in consumption_data.items():
|
||||
if key != 'mo_id':
|
||||
print(f" {key}: {value} kg")
|
||||
|
||||
response = session.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={'Content-Type': 'application/json'}
|
||||
)
|
||||
|
||||
result = response.json()
|
||||
|
||||
print(f"\n📥 Response Status: {response.status_code}")
|
||||
|
||||
if result.get('result'):
|
||||
res = result['result']
|
||||
|
||||
if res.get('status') == 'success':
|
||||
print(f"\n✅ UPDATE BERHASIL!")
|
||||
print(f" MO: {res.get('mo_id')}")
|
||||
print(f" State: {res.get('mo_state')}")
|
||||
|
||||
consumed_items = res.get('consumed_items', [])
|
||||
print(f"\n Updated Items ({len(consumed_items)}):")
|
||||
for item in consumed_items:
|
||||
print(f" - {item.get('equipment_code')} ({item.get('equipment_name')})")
|
||||
print(f" Applied: {item.get('applied_qty')} kg")
|
||||
print(f" Products: {', '.join(item.get('products', []))}")
|
||||
|
||||
if res.get('errors'):
|
||||
print(f"\n⚠️ Errors:")
|
||||
for err in res['errors']:
|
||||
print(f" - {err}")
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f"\n❌ UPDATE GAGAL!")
|
||||
print(f" Message: {res.get('message')}")
|
||||
|
||||
if res.get('errors'):
|
||||
print(f" Errors:")
|
||||
for err in res['errors']:
|
||||
print(f" - {err}")
|
||||
|
||||
return False
|
||||
else:
|
||||
print(f"\n❌ Response error:")
|
||||
print(json.dumps(result, indent=2))
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print("TEST UPDATE CONSUMPTION: WH/MO/00001")
|
||||
print("=" * 70)
|
||||
|
||||
mo_id = "WH/MO/00001"
|
||||
|
||||
# Step 1: Check current state
|
||||
mo = check_mo_current_state(mo_id)
|
||||
if not mo:
|
||||
print("\n❌ Tidak bisa melanjutkan test")
|
||||
return
|
||||
|
||||
# Step 2: Login
|
||||
session = login_odoo()
|
||||
if not session:
|
||||
return
|
||||
|
||||
# Step 3: Test Update 1 - Update beberapa silo dengan nilai kecil
|
||||
test_update_consumption(
|
||||
session,
|
||||
mo_id,
|
||||
{
|
||||
"silo101": 50.0, # SILO A
|
||||
"silo102": 45.0, # SILO B
|
||||
"silo103": 30.0, # SILO C
|
||||
},
|
||||
"Update 1 - Set consumption awal (3 silo)"
|
||||
)
|
||||
|
||||
# Step 4: Check state after update 1
|
||||
print("\n" + "-" * 70)
|
||||
input("Press Enter untuk check state dan lanjut ke update 2...")
|
||||
check_mo_current_state(mo_id)
|
||||
|
||||
# Step 5: Test Update 2 - Update dengan nilai lebih besar (replace mode)
|
||||
test_update_consumption(
|
||||
session,
|
||||
mo_id,
|
||||
{
|
||||
"silo101": 100.0, # Naik dari 50 → 100
|
||||
"silo102": 80.0, # Naik dari 45 → 80
|
||||
"silo104": 25.0, # SILO D (baru)
|
||||
},
|
||||
"Update 2 - Replace dengan nilai baru (3 silo, 1 silo baru)"
|
||||
)
|
||||
|
||||
# Step 6: Check final state
|
||||
print("\n" + "-" * 70)
|
||||
input("Press Enter untuk check final state...")
|
||||
check_mo_current_state(mo_id)
|
||||
|
||||
# Step 7: Test Update 3 - Update banyak silo sekaligus
|
||||
test_update_consumption(
|
||||
session,
|
||||
mo_id,
|
||||
{
|
||||
"silo101": 150.0,
|
||||
"silo102": 120.0,
|
||||
"silo103": 60.0,
|
||||
"silo104": 40.0,
|
||||
"silo105": 75.0, # SILO E (baru)
|
||||
"silo106": 50.0, # SILO F (baru)
|
||||
},
|
||||
"Update 3 - Batch update 6 silo sekaligus"
|
||||
)
|
||||
|
||||
# Final state
|
||||
print("\n" + "-" * 70)
|
||||
input("Press Enter untuk check final state setelah batch update...")
|
||||
final_mo = check_mo_current_state(mo_id)
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("KESIMPULAN")
|
||||
print("=" * 70)
|
||||
print(f"""
|
||||
✅ MO {mo_id} berhasil di-update 3 kali
|
||||
|
||||
📝 Behavior yang terlihat:
|
||||
1. State tetap 'progress' sepanjang update
|
||||
2. Replace mode: Nilai lama diganti dengan nilai baru
|
||||
3. Bisa update silo yang sama berkali-kali
|
||||
4. Bisa tambah silo baru kapan saja
|
||||
5. Tidak ada limit jumlah update
|
||||
|
||||
💡 Ini cocok untuk SCADA realtime monitoring dimana:
|
||||
- Data consumption terus berubah
|
||||
- Tidak perlu hitung delta, langsung kirim nilai total
|
||||
- Backend Odoo yang handle state management
|
||||
""")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Upgrade module grt_scada setelah Odoo restart
|
||||
"""
|
||||
|
||||
import xmlrpc.client
|
||||
import time
|
||||
|
||||
ODOO_URL = "http://localhost:8070"
|
||||
ODOO_DB = "manukanjabung"
|
||||
ODOO_USERNAME = "admin"
|
||||
ODOO_PASSWORD = "admin"
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print("UPGRADE MODULE GRT_SCADA (After Restart)")
|
||||
print("=" * 70)
|
||||
|
||||
print("\n⏳ Menunggu Odoo siap...")
|
||||
|
||||
# Wait for Odoo to be ready
|
||||
max_retries = 10
|
||||
for i in range(max_retries):
|
||||
try:
|
||||
common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common')
|
||||
version = common.version()
|
||||
print(f"✅ Odoo ready! Version: {version.get('server_version')}")
|
||||
break
|
||||
except Exception as e:
|
||||
if i < max_retries - 1:
|
||||
print(f" Retry {i+1}/{max_retries}... ({e})")
|
||||
time.sleep(3)
|
||||
else:
|
||||
print(f"❌ Odoo tidak ready setelah {max_retries} retries")
|
||||
print(f" Pastikan Odoo sudah berjalan di {ODOO_URL}")
|
||||
return
|
||||
|
||||
# Authenticate
|
||||
try:
|
||||
uid = common.authenticate(ODOO_DB, ODOO_USERNAME, ODOO_PASSWORD, {})
|
||||
if not uid:
|
||||
print("❌ Gagal autentikasi")
|
||||
return
|
||||
|
||||
print(f"✅ Authenticated as User ID: {uid}")
|
||||
|
||||
models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object')
|
||||
|
||||
# Find module
|
||||
module_ids = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'ir.module.module', 'search',
|
||||
[[('name', '=', 'grt_scada')]]
|
||||
)
|
||||
|
||||
if not module_ids:
|
||||
print("❌ Module grt_scada tidak ditemukan")
|
||||
return
|
||||
|
||||
# Upgrade
|
||||
print("\n🔄 Upgrading module grt_scada...")
|
||||
|
||||
result = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'ir.module.module', 'button_immediate_upgrade',
|
||||
[module_ids]
|
||||
)
|
||||
|
||||
print("✅ Upgrade command sent")
|
||||
print("⏳ Waiting for upgrade to complete...")
|
||||
time.sleep(10)
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("✅ UPGRADE SELESAI")
|
||||
print("=" * 70)
|
||||
print("\nTest endpoint dengan:")
|
||||
print(" python test_mo_consumption_api.py")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Upgrade module grt_scada untuk refresh routes
|
||||
"""
|
||||
|
||||
import xmlrpc.client
|
||||
import time
|
||||
|
||||
# Konfigurasi Odoo
|
||||
ODOO_URL = "http://localhost:8070"
|
||||
ODOO_DB = "manukanjabung"
|
||||
ODOO_USERNAME = "admin"
|
||||
ODOO_PASSWORD = "admin"
|
||||
|
||||
def upgrade_module(module_name):
|
||||
"""Upgrade specific module"""
|
||||
print("=" * 70)
|
||||
print(f"UPGRADE MODULE: {module_name}")
|
||||
print("=" * 70)
|
||||
|
||||
try:
|
||||
# Connect
|
||||
common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common')
|
||||
uid = common.authenticate(ODOO_DB, ODOO_USERNAME, ODOO_PASSWORD, {})
|
||||
|
||||
if not uid:
|
||||
print("❌ Gagal autentikasi")
|
||||
return False
|
||||
|
||||
models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object')
|
||||
print(f"✅ Terkoneksi sebagai User ID: {uid}\n")
|
||||
|
||||
# Find module
|
||||
module_ids = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'ir.module.module', 'search',
|
||||
[[('name', '=', module_name)]]
|
||||
)
|
||||
|
||||
if not module_ids:
|
||||
print(f"❌ Module {module_name} tidak ditemukan")
|
||||
return False
|
||||
|
||||
module_id = module_ids[0]
|
||||
|
||||
# Check current state
|
||||
module = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'ir.module.module', 'read',
|
||||
[[module_id]],
|
||||
{'fields': ['name', 'state', 'latest_version']}
|
||||
)[0]
|
||||
|
||||
print(f"📦 Module: {module['name']}")
|
||||
print(f" State: {module['state']}")
|
||||
print(f" Version: {module.get('latest_version', 'N/A')}")
|
||||
|
||||
if module['state'] != 'installed':
|
||||
print(f"\n❌ Module tidak dalam state 'installed', tidak bisa upgrade")
|
||||
return False
|
||||
|
||||
# Upgrade module
|
||||
print(f"\n🔄 Upgrading module...")
|
||||
|
||||
result = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'ir.module.module', 'button_immediate_upgrade',
|
||||
[[module_id]]
|
||||
)
|
||||
|
||||
print(f"✅ Upgrade command sent")
|
||||
print(f" Result: {result}")
|
||||
|
||||
# Wait a bit
|
||||
print(f"\n⏳ Waiting 5 seconds for upgrade to complete...")
|
||||
time.sleep(5)
|
||||
|
||||
# Check new state
|
||||
module_after = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_PASSWORD,
|
||||
'ir.module.module', 'read',
|
||||
[[module_id]],
|
||||
{'fields': ['name', 'state', 'latest_version']}
|
||||
)[0]
|
||||
|
||||
print(f"\n📦 Module after upgrade:")
|
||||
print(f" State: {module_after['state']}")
|
||||
print(f" Version: {module_after.get('latest_version', 'N/A')}")
|
||||
|
||||
if module_after['state'] == 'installed':
|
||||
print(f"\n✅ MODULE BERHASIL DI-UPGRADE")
|
||||
return True
|
||||
else:
|
||||
print(f"\n⚠️ Module state: {module_after['state']}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print("UPGRADE MODULE GRT_SCADA")
|
||||
print("=" * 70)
|
||||
print()
|
||||
print("Module ini akan di-upgrade untuk:")
|
||||
print("- Refresh routes dan controllers")
|
||||
print("- Update model definitions")
|
||||
print("- Re-load configurations")
|
||||
print()
|
||||
|
||||
confirm = input("Lanjutkan upgrade? (y/n): ").strip().lower()
|
||||
|
||||
if confirm != 'y':
|
||||
print("❌ Dibatalkan")
|
||||
return
|
||||
|
||||
success = upgrade_module('grt_scada')
|
||||
|
||||
if success:
|
||||
print("\n" + "=" * 70)
|
||||
print("✅ UPGRADE SELESAI")
|
||||
print("=" * 70)
|
||||
print()
|
||||
print("Silakan test endpoint lagi dengan:")
|
||||
print(" python test_mo_consumption_api.py")
|
||||
else:
|
||||
print("\n" + "=" * 70)
|
||||
print("❌ UPGRADE GAGAL")
|
||||
print("=" * 70)
|
||||
print()
|
||||
print("Alternatif: Restart Odoo secara manual")
|
||||
print("1. Stop Odoo (Ctrl+C di terminal Odoo)")
|
||||
print("2. Start kembali dengan F5 di VS Code Debug")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user