Sudah berhasil update PLC satu cycle

This commit is contained in:
2026-02-16 09:24:49 +07:00
parent d5a05971ae
commit 6c436363e0
47 changed files with 18391 additions and 36196 deletions
+186
View File
@@ -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
+217
View File
@@ -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
+150
View File
@@ -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()
+129
View File
@@ -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()
+213
View File
@@ -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()
+228
View File
@@ -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
+19
View File
@@ -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
+2 -1
View File
@@ -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',
+90
View File
@@ -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'])
+1
View File
@@ -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.
+6
View File
@@ -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,
)
+65 -6
View File
@@ -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
+3
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
19 access_scada_equipment_oee_manager scada.equipment.oee Manager model_scada_equipment_oee group_scada_manager 1 1 1 1
20 access_scada_equipment_oee_operator scada.equipment.oee Operator model_scada_equipment_oee group_scada_operator 1 1 1 0
21 access_scada_equipment_oee_technician scada.equipment.oee Technician model_scada_equipment_oee group_scada_technician 1 1 1 0
22 access_scada_equipment_failure_manager scada.equipment.failure Manager model_scada_equipment_failure group_scada_manager 1 1 1 1
23 access_scada_equipment_failure_operator scada.equipment.failure Operator model_scada_equipment_failure group_scada_operator 1 1 1 0
24 access_scada_equipment_failure_technician scada.equipment.failure Technician model_scada_equipment_failure group_scada_technician 1 1 1 0
+40 -9
View File
@@ -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,
+14
View File
@@ -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>
+65
View File
@@ -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.
+2
View File
@@ -0,0 +1,2 @@
from . import models
from . import controllers
+19
View File
@@ -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
@@ -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
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 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
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -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
+236
View File
@@ -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()
+371
View File
@@ -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()
+195
View File
@@ -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()
+259
View File
@@ -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()
+279
View File
@@ -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()
+86
View File
@@ -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()
+140
View File
@@ -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()