Perbaikan komplit Dashboard
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
# DEPLOYMENT & VERIFICATION GUIDE
|
||||
# SILO OEE Equipment Fix - grt_scada Module
|
||||
|
||||
## Issue Yang Diperbaiki
|
||||
- ✅ SILO equipment sekarang akan menampilkan OEE records dari `scada.equipment.oee.line` (detail level)
|
||||
- ✅ Main PLC terus menampilkan OEE records dari header level (tidak ada perubahan)
|
||||
|
||||
## Modified File
|
||||
- `grt_scada/controllers/main.py`
|
||||
- Function: `get_oee_equipment_avg()` [around line 980-990]
|
||||
- Change: Added fallback untuk count OEE line records jika header OEE count = 0
|
||||
|
||||
## Deployment Checklist
|
||||
|
||||
### ✅ Pre-Deployment
|
||||
- [ ] File `main.py` sudah di-edit lokal
|
||||
- [ ] Syntax validation sudah OK (python -m py_compile)
|
||||
- [ ] Backup original file jika perlu
|
||||
|
||||
### 🚀 DEPLOYMENT STEPS
|
||||
|
||||
#### Step 1: Upload File ke Server
|
||||
```bash
|
||||
# Option A: Via SCP (recommended)
|
||||
scp /path/to/grt_scada/controllers/main.py user@kanjabung.web.id:/opt/odoo/addons/grt_scada/controllers/
|
||||
|
||||
# Option B: Via SFTP (WinSCP, FileZilla)
|
||||
# Connect to server, navigate to /opt/odoo/addons/grt_scada/controllers/
|
||||
# Upload main.py file
|
||||
|
||||
# Option C: Via Git (if repo is synced)
|
||||
git push origin main # on local
|
||||
# Then pull on server
|
||||
```
|
||||
|
||||
#### Step 2: Restart Odoo Service
|
||||
```bash
|
||||
# Login ke server
|
||||
ssh user@kanjabung.web.id
|
||||
|
||||
# Restart Odoo (gunakan salah satu)
|
||||
sudo systemctl restart odoo14
|
||||
# atau
|
||||
sudo /etc/init.d/odoo14 restart
|
||||
# atau
|
||||
sudo supervisorctl restart odoo
|
||||
|
||||
# Tunggu 30 detik, kemudian cek status
|
||||
sudo systemctl status odoo14
|
||||
# Pastikan: Active (running)
|
||||
```
|
||||
|
||||
#### Step 3: Upgrade Module di Odoo UI
|
||||
1. Login ke Odoo: https://kanjabung.web.id
|
||||
2. Go to **Apps** menu (hamburger icon top-left)
|
||||
3. Search bar: type "grt_scada"
|
||||
4. Click on **SCADA** module
|
||||
5. Click **Upgrade** button (top-right)
|
||||
6. Tunggu sampai status berubah jadi "Installed"
|
||||
|
||||
#### Step 4: Verify Deployment
|
||||
```powershell
|
||||
# Run test script setelah restart selesai
|
||||
cd c:\addon14
|
||||
.\test_silo_oee_after_deployment.ps1
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
✓ Authentication successful
|
||||
✓ API call successful
|
||||
|
||||
Equipment with OEE Data:
|
||||
• SILO A (silo101)
|
||||
- OEE Records: XXX
|
||||
- Avg Yield: YY.YY%
|
||||
|
||||
• Main PLC - Injection Machine 01 (plc01)
|
||||
- OEE Records: 64
|
||||
- Avg Yield: 100.0%
|
||||
|
||||
SILO equipment with data: 1+
|
||||
LQ equipment with data: 1+
|
||||
PLC equipment with data: 1+
|
||||
|
||||
✓ SUCCESS: SILO equipment sekarang muncul dengan OEE records!
|
||||
```
|
||||
|
||||
## Rollback (Jika Ada Masalah)
|
||||
```bash
|
||||
# Restore original file
|
||||
# Restart Odoo
|
||||
sudo systemctl restart odoo14
|
||||
# Upgrade module di UI
|
||||
```
|
||||
|
||||
## Support Info
|
||||
- Server: https://kanjabung.web.id
|
||||
- Database: kanjabung_MRP
|
||||
- Module: grt_scada
|
||||
- Date Range: 2026-02-24 to 2026-03-13
|
||||
@@ -19,6 +19,7 @@ This enables different staging flow per business category through team pipelines
|
||||
"views/crm_activity_history_views.xml",
|
||||
"views/crm_business_category_views.xml",
|
||||
"views/crm_lead_views.xml",
|
||||
"views/crm_menu_views.xml",
|
||||
"views/crm_stage_views.xml",
|
||||
"views/crm_team_views.xml",
|
||||
"views/crm_team_business_category_views.xml",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="crm.menu_crm_lead_stage_act" model="ir.ui.menu">
|
||||
<field
|
||||
name="groups_id"
|
||||
eval="[(6, 0, [ref('sales_team.group_sale_manager'), ref('base.group_system')])]"
|
||||
/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,75 @@
|
||||
# API Report Dashboard SCADA Vue.js (One-Page)
|
||||
|
||||
Dokumen ringkas untuk handoff tim frontend Vue.js.
|
||||
Fokus: endpoint report, params minimum, output wajib, dan pola integrasi.
|
||||
|
||||
## Base URL
|
||||
|
||||
`/api/scada`
|
||||
|
||||
## Endpoint Report Inti
|
||||
|
||||
| Endpoint | Method | Params Minimum (`params`) | Output Wajib untuk Dashboard |
|
||||
|---|---|---|---|
|
||||
| `/today-reports` | `POST` JSON-RPC | `{ "date": "YYYY-MM-DD", "limit": 200 }` | `batch_status`, `total_production_today`, `oee_quality_today`, `batch_to_batch_deviation_chart.data[]` |
|
||||
| `/periodic-report` | `POST` JSON-RPC | `{ "period": "this_month", "limit": 1000 }` | `metrics_total_production`, `metrics_total_mo`, `metrics_avg_oee_quality`, `chart_daily_target_vs_actual.data[]`, `chart_raw_material_consumption.data[]`, `table_silo_consumption_stock.data[]`, `table_daily_finished_goods.data[]` |
|
||||
| `/oee-equipment-avg` | `POST` JSON-RPC | `{ "period": "this_month", "limit": 100, "offset": 0 }` | `data[].equipment`, `data[].avg_summary`, `data[].avg_consumption_detail`, `data[].oee_records_count` |
|
||||
| `/oee-detail` | `POST` JSON-RPC | `{ "mo_id": "WH/MO/00001", "limit": 50, "offset": 0 }` | `data[].mo_id`, `data[].product_name`, `data[].yield_percent`, `data[].consumption_ratio`, `data[].lines[]` |
|
||||
| `/kpi-product-report` | `POST` JSON-RPC | `{ "period": "this_month", "limit": 100, "offset": 0 }` | `summary.total_products`, `summary.total_oee_records`, `data[].product_name`, `data[].avg_kpi` |
|
||||
| `/equipment-failure-report` | `POST` JSON-RPC | `{ "period": "this_month", "limit": 100, "offset": 0 }` | `summary.total_failures`, `summary.equipment_count`, `summary.by_equipment[]`, `data[]` |
|
||||
|
||||
## Endpoint Pendukung Filter
|
||||
|
||||
| Endpoint | Method | Fungsi |
|
||||
|---|---|---|
|
||||
| `/products` | `GET` / `POST` JSON-RPC | Dropdown produk |
|
||||
| `/products-by-category` | `POST` JSON-RPC | Dropdown bahan baku per kategori |
|
||||
| `/boms` | `GET` / `POST` JSON-RPC | Komposisi BoM |
|
||||
| `/mo-list` | `GET` | Daftar MO per equipment/status |
|
||||
| `/mo-list-confirmed` | `POST` JSON-RPC | Queue MO confirmed |
|
||||
| `/mo-list-detailed` | `POST` JSON-RPC | Detail MO + komponen + consumption |
|
||||
|
||||
## Kontrak Request (POST Report)
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "call",
|
||||
"params": {}
|
||||
}
|
||||
```
|
||||
|
||||
## Template Fetch Frontend
|
||||
|
||||
```javascript
|
||||
async function callScadaReport(endpoint, params = {}) {
|
||||
const response = await fetch(`/api/scada/${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: 'call',
|
||||
params,
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = await response.json();
|
||||
if (!response.ok || payload?.status === 'error' || payload?.error) {
|
||||
throw new Error(payload?.message || payload?.error?.message || 'Request failed');
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist Integrasi Minimum
|
||||
|
||||
1. Selalu kirim `credentials: 'include'`.
|
||||
2. Selalu cek `status` sebelum render data.
|
||||
3. Prioritaskan `today-reports` dan `periodic-report` untuk chart utama.
|
||||
4. Endpoint lain umumnya data mentah dan perlu mapping chart/table di frontend.
|
||||
|
||||
## Referensi Detail Lengkap
|
||||
|
||||
Lihat dokumen utama: `API_SPEC.md` (section API report & endpoint detail lengkap).
|
||||
@@ -83,6 +83,216 @@ curl -X POST http://localhost:8069/api/scada/material-consumption \
|
||||
|
||||
## Endpoints Reference
|
||||
|
||||
## API Report Khusus Dashboard SCADA Vue.js
|
||||
|
||||
Bagian ini merangkum endpoint yang difokuskan untuk kebutuhan **reporting dashboard** di frontend Vue.js.
|
||||
|
||||
Dokumen ringkas terpisah (1 halaman): [API_REPORT_DASHBOARD_VUEJS.md](API_REPORT_DASHBOARD_VUEJS.md)
|
||||
|
||||
### Endpoint Inti Report Dashboard
|
||||
|
||||
| No | Endpoint | Method | Kegunaan Dashboard | Output Utama |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `/api/scada/today-reports` | `POST` (JSON-RPC) | Ringkasan harian (KPI, batch, quality) | `batch_status`, `total_production_today`, `oee_quality_today`, `batch_to_batch_deviation_chart` |
|
||||
| 2 | `/api/scada/periodic-report` | `POST` (JSON-RPC) | Ringkasan periodik mingguan/bulanan/tahunan | `metrics_*`, `chart_daily_target_vs_actual`, `chart_raw_material_consumption`, `table_*` |
|
||||
| 3 | `/api/scada/oee-equipment-avg` | `POST` (JSON-RPC) | Analitik rata-rata OEE per equipment | `data[].avg_summary`, `data[].avg_consumption_detail` |
|
||||
| 4 | `/api/scada/oee-detail` | `POST` (JSON-RPC) | Drill-down OEE per batch/MO/equipment | `data[]`, `data[].lines` |
|
||||
| 5 | `/api/scada/kpi-product-report` | `POST` (JSON-RPC) | KPI performa per produk | `data[].avg_kpi`, `summary` |
|
||||
| 6 | `/api/scada/equipment-failure-report` | `POST` (JSON-RPC) | Report failure & downtime equipment | `data[]`, `summary.total_failures`, `summary.by_equipment` |
|
||||
|
||||
### Endpoint Pendukung Filter Dashboard Report
|
||||
|
||||
| Endpoint | Method | Fungsi Filter |
|
||||
|---|---|---|
|
||||
| `/api/scada/products` | `GET` / `POST` (JSON-RPC) | Sumber dropdown produk |
|
||||
| `/api/scada/products-by-category` | `POST` (JSON-RPC) | Sumber dropdown bahan baku per kategori |
|
||||
| `/api/scada/boms` | `GET` / `POST` (JSON-RPC) | Detail komposisi BoM untuk analitik konsumsi |
|
||||
| `/api/scada/mo-list` | `GET` | Daftar MO per equipment/status |
|
||||
| `/api/scada/mo-list-confirmed` | `POST` (JSON-RPC) | Queue MO confirmed untuk picker/frontend |
|
||||
| `/api/scada/mo-list-detailed` | `POST` (JSON-RPC) | Detail MO + komponen + consumption |
|
||||
|
||||
### Contract Request untuk Vue.js
|
||||
|
||||
Semua endpoint report dengan method `POST` pada tabel di atas menggunakan body JSON-RPC:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "call",
|
||||
"params": {
|
||||
"period": "this_month",
|
||||
"limit": 100,
|
||||
"offset": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Catatan frontend:
|
||||
1. Gunakan `credentials: 'include'` agar session cookie Odoo ikut terkirim.
|
||||
2. Cek `status` response sebelum membaca `data`/`summary`.
|
||||
3. Untuk report chart, prioritaskan endpoint `today-reports` dan `periodic-report` karena sudah menyediakan blok chart siap map ke ApexCharts.
|
||||
|
||||
### Cheatsheet Copy-Paste Frontend (Vue.js)
|
||||
|
||||
Gunakan section ini sebagai referensi cepat implementasi dashboard/report di frontend.
|
||||
|
||||
#### 1) `POST /api/scada/today-reports`
|
||||
|
||||
**Params minimum (JSON-RPC):**
|
||||
|
||||
```json
|
||||
{
|
||||
"date": "2026-02-17",
|
||||
"limit": 200
|
||||
}
|
||||
```
|
||||
|
||||
**Field utama untuk UI:**
|
||||
- KPI: `batch_status.scheduled_total`, `batch_status.completed`, `batch_status.unfinished`
|
||||
- KPI: `total_production_today.qty_finished`, `total_production_today.completed_batch_count`
|
||||
- KPI: `oee_quality_today.avg_yield_percent`, `oee_quality_today.avg_consumption_ratio`, `oee_quality_today.total_deviation_alerts`
|
||||
- Chart: `batch_to_batch_deviation_chart.data[]`
|
||||
- Chart per equipment: `oee_quality_today.by_equipment[]`
|
||||
|
||||
#### 2) `POST /api/scada/periodic-report`
|
||||
|
||||
**Params minimum (JSON-RPC):**
|
||||
|
||||
```json
|
||||
{
|
||||
"period": "this_month",
|
||||
"limit": 1000
|
||||
}
|
||||
```
|
||||
|
||||
**Field utama untuk UI:**
|
||||
- KPI produksi: `metrics_total_production.target_qty_total`, `actual_qty_total`, `achievement_percent`
|
||||
- KPI MO: `metrics_total_mo.total_mo_planned`, `total_mo_done`, `total_mo_in_progress`
|
||||
- KPI kualitas: `metrics_avg_oee_quality.avg_yield_percent`, `avg_consumption_ratio`
|
||||
- Chart: `chart_daily_target_vs_actual.data[]`
|
||||
- Chart: `chart_raw_material_consumption.data[]`
|
||||
- Table: `table_silo_consumption_stock.data[]`, `table_daily_finished_goods.data[]`
|
||||
|
||||
#### 3) `POST /api/scada/oee-equipment-avg`
|
||||
|
||||
**Params minimum (JSON-RPC):**
|
||||
|
||||
```json
|
||||
{
|
||||
"period": "this_month",
|
||||
"limit": 100,
|
||||
"offset": 0
|
||||
}
|
||||
```
|
||||
|
||||
**Field utama untuk UI:**
|
||||
- List summary: `data[]`
|
||||
- Nilai utama: `data[].avg_summary.yield_percent`, `consumption_ratio`, `qty_planned`, `qty_finished`
|
||||
- Detail konsumsi: `data[].avg_consumption_detail.to_consume`, `actual_consumed`, `consumption_ratio`
|
||||
|
||||
#### 4) `POST /api/scada/oee-detail`
|
||||
|
||||
**Params minimum (JSON-RPC):**
|
||||
|
||||
```json
|
||||
{
|
||||
"mo_id": "WH/MO/00001",
|
||||
"limit": 50,
|
||||
"offset": 0
|
||||
}
|
||||
```
|
||||
|
||||
**Field utama untuk UI:**
|
||||
- Header detail: `data[].mo_id`, `product_name`, `equipment.name`, `date_done`
|
||||
- KPI batch: `data[].yield_percent`, `consumption_ratio`, `variance_finished`, `variance_consumption`
|
||||
- Tabel/Chart line: `data[].lines[]` (`to_consume`, `actual_consumed`, `variance`, `consumption_ratio`)
|
||||
|
||||
#### 5) `POST /api/scada/kpi-product-report`
|
||||
|
||||
**Params minimum (JSON-RPC):**
|
||||
|
||||
```json
|
||||
{
|
||||
"period": "this_month",
|
||||
"limit": 100,
|
||||
"offset": 0
|
||||
}
|
||||
```
|
||||
|
||||
**Field utama untuk UI:**
|
||||
- KPI global: `summary.total_products`, `summary.total_oee_records`
|
||||
- Ranking produk: `data[]`
|
||||
- Nilai KPI produk: `data[].avg_kpi.yield_percent`, `consumption_ratio`, `qty_planned`, `qty_finished`
|
||||
|
||||
#### 6) `POST /api/scada/equipment-failure-report`
|
||||
|
||||
**Params minimum (JSON-RPC):**
|
||||
|
||||
```json
|
||||
{
|
||||
"period": "this_month",
|
||||
"limit": 100,
|
||||
"offset": 0
|
||||
}
|
||||
```
|
||||
|
||||
**Field utama untuk UI:**
|
||||
- KPI: `summary.total_failures`, `summary.equipment_count`
|
||||
- Chart: `summary.by_equipment[]` (`failure_count`)
|
||||
- Tabel log: `data[]` (`equipment_name`, `description`, `date`, `duration_minutes`)
|
||||
|
||||
#### Template pemanggilan umum (fetch)
|
||||
|
||||
```javascript
|
||||
async function callScadaReport(endpoint, params = {}) {
|
||||
const res = await fetch(`/api/scada/${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: 'call',
|
||||
params,
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = await res.json();
|
||||
if (!res.ok || payload?.status === 'error' || payload?.error) {
|
||||
throw new Error(payload?.message || payload?.error?.message || 'Request failed');
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
```
|
||||
|
||||
### One-Page Ringkas (Endpoint + Params + Output Wajib)
|
||||
|
||||
Gunakan bagian ini untuk handoff cepat ke tim frontend.
|
||||
|
||||
| Endpoint | Params Minimum (`params`) | Output Wajib untuk Dashboard |
|
||||
|---|---|---|
|
||||
| `POST /api/scada/today-reports` | `{ "date": "YYYY-MM-DD", "limit": 200 }` | `batch_status`, `total_production_today`, `oee_quality_today`, `batch_to_batch_deviation_chart.data[]` |
|
||||
| `POST /api/scada/periodic-report` | `{ "period": "this_month", "limit": 1000 }` | `metrics_total_production`, `metrics_total_mo`, `metrics_avg_oee_quality`, `chart_daily_target_vs_actual.data[]`, `chart_raw_material_consumption.data[]`, `table_silo_consumption_stock.data[]`, `table_daily_finished_goods.data[]` |
|
||||
| `POST /api/scada/oee-equipment-avg` | `{ "period": "this_month", "limit": 100, "offset": 0 }` | `data[].equipment`, `data[].avg_summary`, `data[].avg_consumption_detail`, `data[].oee_records_count` |
|
||||
| `POST /api/scada/oee-detail` | `{ "mo_id": "WH/MO/00001", "limit": 50, "offset": 0 }` | `data[].mo_id`, `data[].product_name`, `data[].yield_percent`, `data[].consumption_ratio`, `data[].lines[]` |
|
||||
| `POST /api/scada/kpi-product-report` | `{ "period": "this_month", "limit": 100, "offset": 0 }` | `summary.total_products`, `summary.total_oee_records`, `data[].product_name`, `data[].avg_kpi` |
|
||||
| `POST /api/scada/equipment-failure-report` | `{ "period": "this_month", "limit": 100, "offset": 0 }` | `summary.total_failures`, `summary.equipment_count`, `summary.by_equipment[]`, `data[]` |
|
||||
|
||||
**Kontrak request (wajib untuk endpoint `POST` report):**
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "call",
|
||||
"params": {}
|
||||
}
|
||||
```
|
||||
|
||||
**Checklist implementasi frontend (minimum):**
|
||||
1. Selalu kirim `credentials: 'include'`.
|
||||
2. Selalu validasi `status !== 'error'` sebelum render.
|
||||
3. Untuk chart utama dashboard, prioritaskan `today-reports` dan `periodic-report`.
|
||||
4. Endpoint lain dianggap data mentah dan perlu mapping di frontend.
|
||||
|
||||
## Frontend Dashboard Quick Reference (Vue.js + ApexCharts)
|
||||
|
||||
Bagian ini sudah dipisah per kelompok bab agar implementasi frontend lebih mudah. Setiap bab berisi: endpoint sumber data, field yang ditampilkan, dan rekomendasi visual.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from . import controllers
|
||||
from . import hooks
|
||||
from . import models
|
||||
from . import wizard
|
||||
from . import wizard
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
'name': 'SCADA for Odoo - Manufacturing Integration',
|
||||
'version': '7.0.86',
|
||||
'version': '7.0.89',
|
||||
'category': 'manufacturing',
|
||||
'license': 'LGPL-3',
|
||||
'author': 'PT. Gagak Rimang Teknologi',
|
||||
@@ -13,9 +13,11 @@
|
||||
'depends': [
|
||||
'stock',
|
||||
'mrp',
|
||||
'maintenance',
|
||||
'web',
|
||||
'base',
|
||||
],
|
||||
'post_init_hook': 'post_init_hook',
|
||||
'external_dependencies': {
|
||||
'python': [
|
||||
'requests',
|
||||
@@ -38,6 +40,7 @@
|
||||
'views/scada_mo_bulk_wizard_view.xml',
|
||||
# Menus - AFTER all views (references actions from views)
|
||||
'views/menu.xml',
|
||||
'views/scada_maintenance_views.xml',
|
||||
# Data files
|
||||
'data/demo_data.xml',
|
||||
'data/ir_cron.xml',
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+365
-111
@@ -45,10 +45,33 @@ class ScadaJsonRpcController(http.Controller):
|
||||
if not value:
|
||||
return None
|
||||
cleaned = str(value).strip().replace('T', ' ')
|
||||
if len(cleaned) == 10:
|
||||
return f"{cleaned} {'23:59:59' if is_end else '00:00:00'}"
|
||||
if len(cleaned) == 16:
|
||||
return f"{cleaned}{':59' if is_end else ':00'}"
|
||||
if not cleaned:
|
||||
return None
|
||||
|
||||
parse_formats = [
|
||||
'%Y-%m-%d %H:%M:%S',
|
||||
'%Y-%m-%d %H:%M',
|
||||
'%Y-%m-%d',
|
||||
'%d/%m/%Y %H:%M:%S',
|
||||
'%d/%m/%Y %H:%M',
|
||||
'%d/%m/%Y',
|
||||
]
|
||||
for datetime_format in parse_formats:
|
||||
try:
|
||||
parsed = datetime.strptime(cleaned, datetime_format)
|
||||
if datetime_format in ('%Y-%m-%d', '%d/%m/%Y'):
|
||||
if is_end:
|
||||
parsed = parsed.replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
else:
|
||||
parsed = parsed.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
elif datetime_format in ('%Y-%m-%d %H:%M', '%d/%m/%Y %H:%M'):
|
||||
parsed = parsed.replace(second=59 if is_end else 0, microsecond=0)
|
||||
else:
|
||||
parsed = parsed.replace(microsecond=0)
|
||||
return parsed.strftime('%Y-%m-%d %H:%M:%S')
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return cleaned
|
||||
|
||||
def _get_period_datetime_range(self, period):
|
||||
@@ -147,6 +170,32 @@ class ScadaJsonRpcController(http.Controller):
|
||||
'last_connected': equipment.last_connected.isoformat() if equipment.last_connected else None,
|
||||
}
|
||||
|
||||
def _read_group_metric(self, row, field_name, aggregate, default=None):
|
||||
"""Helper: Safely read aggregated value from read_group result.
|
||||
|
||||
Odoo read_group keys can vary by version/context:
|
||||
- field_sum / field_avg / field_max / field_count
|
||||
- field (some cases)
|
||||
- __count (for grouped row count)
|
||||
"""
|
||||
if not isinstance(row, dict):
|
||||
return default
|
||||
|
||||
candidates = []
|
||||
if aggregate == 'count':
|
||||
candidates.extend([f'{field_name}_count', '__count', field_name])
|
||||
else:
|
||||
candidates.extend([
|
||||
f'{field_name}_{aggregate}',
|
||||
f'{field_name}:{aggregate}',
|
||||
field_name,
|
||||
])
|
||||
|
||||
for key in candidates:
|
||||
if key in row and row.get(key) is not None:
|
||||
return row.get(key)
|
||||
return default
|
||||
|
||||
def _authenticate_session(self, login, password, dbname=None):
|
||||
db = dbname or request.session.db or request.env.cr.dbname
|
||||
uid = request.session.authenticate(db, login, password)
|
||||
@@ -687,8 +736,9 @@ class ScadaJsonRpcController(http.Controller):
|
||||
- oee_id (optional): specific OEE record id
|
||||
- mo_id (optional): MO name or ID
|
||||
- equipment_code (optional): filter by equipment code
|
||||
- date_from (optional): YYYY-MM-DD
|
||||
- date_to (optional): YYYY-MM-DD
|
||||
- date_from (optional): YYYY-MM-DD atau DD/MM/YYYY (boleh datetime)
|
||||
- date_to (optional): YYYY-MM-DD atau DD/MM/YYYY (boleh datetime)
|
||||
- period (optional): today, yesterday, this_week, last_7_days, this_month, last_month, this_year
|
||||
- limit (optional): default 50
|
||||
- offset (optional): default 0
|
||||
"""
|
||||
@@ -736,11 +786,27 @@ class ScadaJsonRpcController(http.Controller):
|
||||
domain.append(('equipment_id', '=', equipment.id))
|
||||
|
||||
date_from = data.get('date_from')
|
||||
if date_from:
|
||||
domain.append(('date_done', '>=', f'{date_from} 00:00:00'))
|
||||
date_to = data.get('date_to')
|
||||
if date_to:
|
||||
domain.append(('date_done', '<=', f'{date_to} 23:59:59'))
|
||||
period = data.get('period')
|
||||
|
||||
period_from, period_to = self._get_period_datetime_range(period)
|
||||
if period_from is False:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': (
|
||||
'Invalid period value. '
|
||||
'Supported: today, yesterday, this_week, last_7_days, '
|
||||
'this_month, last_month, this_year'
|
||||
),
|
||||
}
|
||||
|
||||
normalized_from = self._normalize_datetime_input(date_from, is_end=False) or period_from
|
||||
normalized_to = self._normalize_datetime_input(date_to, is_end=True) or period_to
|
||||
|
||||
if normalized_from:
|
||||
domain.append(('date_done', '>=', normalized_from))
|
||||
if normalized_to:
|
||||
domain.append(('date_done', '<=', normalized_to))
|
||||
|
||||
records = oee_model.search(
|
||||
domain,
|
||||
@@ -801,8 +867,9 @@ class ScadaJsonRpcController(http.Controller):
|
||||
- equipment_code (optional)
|
||||
- equipment_type (optional)
|
||||
- is_active (optional): true/false
|
||||
- date_from (optional): YYYY-MM-DD
|
||||
- date_to (optional): YYYY-MM-DD
|
||||
- date_from (optional): YYYY-MM-DD atau DD/MM/YYYY (boleh datetime)
|
||||
- date_to (optional): YYYY-MM-DD atau DD/MM/YYYY (boleh datetime)
|
||||
- period (optional): today, yesterday, this_week, last_7_days, this_month, last_month, this_year
|
||||
- limit (optional): default 100
|
||||
- offset (optional): default 0
|
||||
"""
|
||||
@@ -846,14 +913,30 @@ class ScadaJsonRpcController(http.Controller):
|
||||
line_domain = [('equipment_id', 'in', equipment_ids)]
|
||||
|
||||
date_from = data.get('date_from')
|
||||
if date_from:
|
||||
oee_domain.append(('date_done', '>=', f'{date_from} 00:00:00'))
|
||||
line_domain.append(('oee_id.date_done', '>=', f'{date_from} 00:00:00'))
|
||||
|
||||
date_to = data.get('date_to')
|
||||
if date_to:
|
||||
oee_domain.append(('date_done', '<=', f'{date_to} 23:59:59'))
|
||||
line_domain.append(('oee_id.date_done', '<=', f'{date_to} 23:59:59'))
|
||||
period = data.get('period')
|
||||
|
||||
period_from, period_to = self._get_period_datetime_range(period)
|
||||
if period_from is False:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': (
|
||||
'Invalid period value. '
|
||||
'Supported: today, yesterday, this_week, last_7_days, '
|
||||
'this_month, last_month, this_year'
|
||||
),
|
||||
}
|
||||
|
||||
normalized_from = self._normalize_datetime_input(date_from, is_end=False) or period_from
|
||||
normalized_to = self._normalize_datetime_input(date_to, is_end=True) or period_to
|
||||
|
||||
if normalized_from:
|
||||
oee_domain.append(('date_done', '>=', normalized_from))
|
||||
line_domain.append(('oee_id.date_done', '>=', normalized_from))
|
||||
|
||||
if normalized_to:
|
||||
oee_domain.append(('date_done', '<=', normalized_to))
|
||||
line_domain.append(('oee_id.date_done', '<=', normalized_to))
|
||||
|
||||
oee_groups = request.env['scada.equipment.oee'].read_group(
|
||||
oee_domain,
|
||||
@@ -880,8 +963,10 @@ class ScadaJsonRpcController(http.Controller):
|
||||
'qty_to_consume:avg',
|
||||
'qty_consumed:avg',
|
||||
'variance_qty:avg',
|
||||
'oee_silo_percent:avg',
|
||||
'consumption_ratio:avg',
|
||||
'material_count:avg',
|
||||
'oee_id.date_done:max',
|
||||
],
|
||||
['equipment_id'],
|
||||
lazy=False
|
||||
@@ -901,27 +986,55 @@ class ScadaJsonRpcController(http.Controller):
|
||||
oee_stat = oee_map.get(equipment.id, {})
|
||||
line_stat = line_map.get(equipment.id, {})
|
||||
|
||||
# For SILO equipment: if no header OEE but has line OEE, count line records
|
||||
# For Main PLC: use header count
|
||||
oee_records_count = self._read_group_metric(oee_stat, 'id', 'count', 0)
|
||||
if oee_records_count == 0 and line_stat:
|
||||
# SILO case: count number of OEE line records for this equipment
|
||||
oee_records_count = self._read_group_metric(line_stat, 'id', 'count', 0)
|
||||
|
||||
has_header_stats = bool(oee_stat)
|
||||
|
||||
if has_header_stats:
|
||||
yield_percent = self._read_group_metric(oee_stat, 'yield_percent', 'avg', 0.0)
|
||||
consumption_ratio = self._read_group_metric(oee_stat, 'consumption_ratio', 'avg', 0.0)
|
||||
else:
|
||||
# SILO/LQ level stats are stored in line model
|
||||
yield_percent = self._read_group_metric(line_stat, 'oee_silo_percent', 'avg', 0.0)
|
||||
consumption_ratio = self._read_group_metric(line_stat, 'consumption_ratio', 'avg', 0.0)
|
||||
|
||||
last_oee_date = self._read_group_metric(oee_stat, 'date_done', 'max', None)
|
||||
if not last_oee_date:
|
||||
last_oee_date = self._read_group_metric(line_stat, 'oee_id.date_done', 'max', None)
|
||||
|
||||
# Return FLAT structure matching frontend mapper expectations (same format as today-reports by_equipment)
|
||||
result_data.append({
|
||||
'equipment_id': equipment.id,
|
||||
'equipment_name': equipment.name,
|
||||
'oee_records_count': oee_records_count,
|
||||
'avg_yield_percent': yield_percent,
|
||||
'avg_consumption_ratio': consumption_ratio,
|
||||
'avg_oee_silo_percent': yield_percent, # For compatibility
|
||||
'last_oee_date': last_oee_date,
|
||||
# Keep nested structure for backward compatibility with API docs
|
||||
'equipment': self._get_equipment_details(equipment),
|
||||
'oee_records_count': oee_stat.get('__count', 0),
|
||||
'avg_summary': {
|
||||
'qty_planned': oee_stat.get('qty_planned_avg', 0.0),
|
||||
'qty_finished': oee_stat.get('qty_finished_avg', 0.0),
|
||||
'variance_finished': oee_stat.get('variance_finished_avg', 0.0),
|
||||
'yield_percent': oee_stat.get('yield_percent_avg', 0.0),
|
||||
'qty_bom_consumption': oee_stat.get('qty_bom_consumption_avg', 0.0),
|
||||
'qty_actual_consumption': oee_stat.get('qty_actual_consumption_avg', 0.0),
|
||||
'variance_consumption': oee_stat.get('variance_consumption_avg', 0.0),
|
||||
'consumption_ratio': oee_stat.get('consumption_ratio_avg', 0.0),
|
||||
'qty_planned': self._read_group_metric(oee_stat, 'qty_planned', 'avg', 0.0) or self._read_group_metric(line_stat, 'qty_to_consume', 'avg', 0.0),
|
||||
'qty_finished': self._read_group_metric(oee_stat, 'qty_finished', 'avg', 0.0) or self._read_group_metric(line_stat, 'qty_consumed', 'avg', 0.0),
|
||||
'variance_finished': self._read_group_metric(oee_stat, 'variance_finished', 'avg', 0.0) or self._read_group_metric(line_stat, 'variance_qty', 'avg', 0.0),
|
||||
'yield_percent': yield_percent,
|
||||
'qty_bom_consumption': self._read_group_metric(oee_stat, 'qty_bom_consumption', 'avg', 0.0) or self._read_group_metric(line_stat, 'qty_to_consume', 'avg', 0.0),
|
||||
'qty_actual_consumption': self._read_group_metric(oee_stat, 'qty_actual_consumption', 'avg', 0.0) or self._read_group_metric(line_stat, 'qty_consumed', 'avg', 0.0),
|
||||
'variance_consumption': self._read_group_metric(oee_stat, 'variance_consumption', 'avg', 0.0) or self._read_group_metric(line_stat, 'variance_qty', 'avg', 0.0),
|
||||
'consumption_ratio': consumption_ratio,
|
||||
},
|
||||
'avg_consumption_detail': {
|
||||
'to_consume': line_stat.get('qty_to_consume_avg', 0.0),
|
||||
'actual_consumed': line_stat.get('qty_consumed_avg', 0.0),
|
||||
'variance': line_stat.get('variance_qty_avg', 0.0),
|
||||
'consumption_ratio': line_stat.get('consumption_ratio_avg', 0.0),
|
||||
'material_count': line_stat.get('material_count_avg', 0.0),
|
||||
'to_consume': self._read_group_metric(line_stat, 'qty_to_consume', 'avg', 0.0),
|
||||
'actual_consumed': self._read_group_metric(line_stat, 'qty_consumed', 'avg', 0.0),
|
||||
'variance': self._read_group_metric(line_stat, 'variance_qty', 'avg', 0.0),
|
||||
'consumption_ratio': self._read_group_metric(line_stat, 'consumption_ratio', 'avg', 0.0),
|
||||
'material_count': self._read_group_metric(line_stat, 'material_count', 'avg', 0.0),
|
||||
},
|
||||
'last_oee_date': oee_stat.get('date_done_max'),
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -1036,18 +1149,18 @@ class ScadaJsonRpcController(http.Controller):
|
||||
result_data.append({
|
||||
'product_id': product[0],
|
||||
'product_name': product[1],
|
||||
'oee_records_count': row.get('id_count', 0),
|
||||
'oee_records_count': self._read_group_metric(row, 'id', 'count', 0),
|
||||
'avg_kpi': {
|
||||
'qty_planned': row.get('qty_planned_avg', 0.0),
|
||||
'qty_finished': row.get('qty_finished_avg', 0.0),
|
||||
'variance_finished': row.get('variance_finished_avg', 0.0),
|
||||
'yield_percent': row.get('yield_percent_avg', 0.0),
|
||||
'qty_bom_consumption': row.get('qty_bom_consumption_avg', 0.0),
|
||||
'qty_actual_consumption': row.get('qty_actual_consumption_avg', 0.0),
|
||||
'variance_consumption': row.get('variance_consumption_avg', 0.0),
|
||||
'consumption_ratio': row.get('consumption_ratio_avg', 0.0),
|
||||
'qty_planned': self._read_group_metric(row, 'qty_planned', 'avg', 0.0),
|
||||
'qty_finished': self._read_group_metric(row, 'qty_finished', 'avg', 0.0),
|
||||
'variance_finished': self._read_group_metric(row, 'variance_finished', 'avg', 0.0),
|
||||
'yield_percent': self._read_group_metric(row, 'yield_percent', 'avg', 0.0),
|
||||
'qty_bom_consumption': self._read_group_metric(row, 'qty_bom_consumption', 'avg', 0.0),
|
||||
'qty_actual_consumption': self._read_group_metric(row, 'qty_actual_consumption', 'avg', 0.0),
|
||||
'variance_consumption': self._read_group_metric(row, 'variance_consumption', 'avg', 0.0),
|
||||
'consumption_ratio': self._read_group_metric(row, 'consumption_ratio', 'avg', 0.0),
|
||||
},
|
||||
'last_oee_date': row.get('date_done_max'),
|
||||
'last_oee_date': self._read_group_metric(row, 'date_done', 'max', None),
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -1110,8 +1223,8 @@ class ScadaJsonRpcController(http.Controller):
|
||||
lazy=False,
|
||||
)
|
||||
production_row = production_stats[0] if production_stats else {}
|
||||
total_production_qty = production_row.get('qty_finished_sum', 0.0)
|
||||
completed_batch_count = production_row.get('id_count', 0)
|
||||
total_production_qty = self._read_group_metric(production_row, 'qty_finished', 'sum', 0.0)
|
||||
completed_batch_count = self._read_group_metric(production_row, 'id', 'count', 0)
|
||||
|
||||
# 3) Kualitas OEE rata-rata hari ini per equipment
|
||||
equipment_avg_rows = oee_model.read_group(
|
||||
@@ -1128,21 +1241,88 @@ class ScadaJsonRpcController(http.Controller):
|
||||
['equipment_id'],
|
||||
lazy=False,
|
||||
)
|
||||
line_equipment_domain = [
|
||||
('oee_id.date_done', '>=', date_from),
|
||||
('oee_id.date_done', '<=', date_to),
|
||||
]
|
||||
line_equipment_avg_rows = request.env['scada.equipment.oee.line'].read_group(
|
||||
line_equipment_domain,
|
||||
[
|
||||
'equipment_id',
|
||||
'oee_silo_percent:avg',
|
||||
'consumption_ratio:avg',
|
||||
'id:count',
|
||||
],
|
||||
['equipment_id'],
|
||||
lazy=False,
|
||||
)
|
||||
line_records = request.env['scada.equipment.oee.line'].search(line_equipment_domain)
|
||||
line_kpi_map = defaultdict(lambda: {
|
||||
'abs_deviation_sum': 0.0,
|
||||
'line_count': 0,
|
||||
'deviation_alert_count': 0,
|
||||
})
|
||||
for line in line_records:
|
||||
if not line.equipment_id:
|
||||
continue
|
||||
equipment_kpis = line_kpi_map[line.equipment_id.id]
|
||||
deviation_abs = abs(line.deviation_percent or 0.0)
|
||||
equipment_kpis['abs_deviation_sum'] += deviation_abs
|
||||
equipment_kpis['line_count'] += 1
|
||||
if deviation_abs > 2.0:
|
||||
equipment_kpis['deviation_alert_count'] += 1
|
||||
|
||||
equipment_avg_map = {
|
||||
row['equipment_id'][0]: row
|
||||
for row in equipment_avg_rows if row.get('equipment_id')
|
||||
}
|
||||
line_equipment_avg_map = {
|
||||
row['equipment_id'][0]: row
|
||||
for row in line_equipment_avg_rows if row.get('equipment_id')
|
||||
}
|
||||
combined_equipment_ids = sorted(set(equipment_avg_map.keys()) | set(line_equipment_avg_map.keys()))
|
||||
equipment_avg = []
|
||||
for row in equipment_avg_rows:
|
||||
equipment = row.get('equipment_id')
|
||||
for equipment_id in combined_equipment_ids:
|
||||
row = equipment_avg_map.get(equipment_id, {})
|
||||
line_row = line_equipment_avg_map.get(equipment_id, {})
|
||||
equipment = row.get('equipment_id') or line_row.get('equipment_id')
|
||||
if not equipment:
|
||||
continue
|
||||
line_kpis = line_kpi_map.get(equipment_id, {})
|
||||
line_count = line_kpis.get('line_count', 0)
|
||||
avg_abs_deviation = (
|
||||
(line_kpis.get('abs_deviation_sum', 0.0) / line_count)
|
||||
if line_count else 0.0
|
||||
)
|
||||
equipment_avg.append({
|
||||
'equipment_id': equipment[0],
|
||||
'equipment_name': equipment[1],
|
||||
'oee_records_count': row.get('id_count', 0),
|
||||
'avg_yield_percent': row.get('yield_percent_avg', 0.0),
|
||||
'avg_consumption_ratio': row.get('consumption_ratio_avg', 0.0),
|
||||
'avg_oee_silo_percent': row.get('avg_silo_oee_percent_avg', 0.0),
|
||||
'avg_max_abs_deviation_percent': row.get('max_abs_deviation_percent_avg', 0.0),
|
||||
'total_deviation_alerts': row.get('deviation_alert_count_sum', 0),
|
||||
'oee_records_count': (
|
||||
self._read_group_metric(row, 'id', 'count', 0)
|
||||
or self._read_group_metric(line_row, 'id', 'count', 0)
|
||||
),
|
||||
'avg_yield_percent': (
|
||||
self._read_group_metric(row, 'yield_percent', 'avg', 0.0)
|
||||
or self._read_group_metric(line_row, 'oee_silo_percent', 'avg', 0.0)
|
||||
),
|
||||
'avg_consumption_ratio': (
|
||||
self._read_group_metric(row, 'consumption_ratio', 'avg', 0.0)
|
||||
or self._read_group_metric(line_row, 'consumption_ratio', 'avg', 0.0)
|
||||
),
|
||||
'avg_oee_silo_percent': (
|
||||
self._read_group_metric(row, 'avg_silo_oee_percent', 'avg', 0.0)
|
||||
or self._read_group_metric(line_row, 'oee_silo_percent', 'avg', 0.0)
|
||||
),
|
||||
'avg_max_abs_deviation_percent': (
|
||||
self._read_group_metric(row, 'max_abs_deviation_percent', 'avg', 0.0)
|
||||
or avg_abs_deviation
|
||||
),
|
||||
'total_deviation_alerts': (
|
||||
self._read_group_metric(row, 'deviation_alert_count', 'sum', 0)
|
||||
or line_kpis.get('deviation_alert_count', 0)
|
||||
),
|
||||
})
|
||||
equipment_avg.sort(key=lambda item: ((item.get('equipment_name') or '').lower(), item.get('equipment_id') or 0))
|
||||
|
||||
oee_quality_rows = oee_model.read_group(
|
||||
oee_domain,
|
||||
@@ -1167,6 +1347,18 @@ class ScadaJsonRpcController(http.Controller):
|
||||
)
|
||||
deviation_chart = []
|
||||
for rec in deviation_records:
|
||||
line_records = rec.line_ids
|
||||
line_max_abs_deviation = 0.0
|
||||
line_avg_silo_oee = 0.0
|
||||
if line_records:
|
||||
deviations = [abs(line.deviation_percent or 0.0) for line in line_records]
|
||||
oee_scores = [line.oee_silo_percent or 0.0 for line in line_records]
|
||||
line_max_abs_deviation = max(deviations) if deviations else 0.0
|
||||
line_avg_silo_oee = (sum(oee_scores) / len(oee_scores)) if oee_scores else 0.0
|
||||
|
||||
max_abs_deviation_percent = rec.max_abs_deviation_percent or line_max_abs_deviation
|
||||
avg_silo_oee_percent = rec.avg_silo_oee_percent or line_avg_silo_oee
|
||||
|
||||
deviation_chart.append({
|
||||
'oee_id': rec.id,
|
||||
'mo_id': rec.mo_name,
|
||||
@@ -1175,8 +1367,8 @@ class ScadaJsonRpcController(http.Controller):
|
||||
'date_done': rec.date_done.isoformat() if rec.date_done else None,
|
||||
'variance_finished': rec.variance_finished,
|
||||
'variance_consumption': rec.variance_consumption,
|
||||
'max_abs_deviation_percent': rec.max_abs_deviation_percent,
|
||||
'avg_silo_oee_percent': rec.avg_silo_oee_percent,
|
||||
'max_abs_deviation_percent': max_abs_deviation_percent,
|
||||
'avg_silo_oee_percent': avg_silo_oee_percent,
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -1194,12 +1386,12 @@ class ScadaJsonRpcController(http.Controller):
|
||||
'completed_batch_count': completed_batch_count,
|
||||
},
|
||||
'oee_quality_today': {
|
||||
'oee_records_count': oee_quality.get('id_count', 0),
|
||||
'avg_yield_percent': oee_quality.get('yield_percent_avg', 0.0),
|
||||
'avg_consumption_ratio': oee_quality.get('consumption_ratio_avg', 0.0),
|
||||
'avg_oee_silo_percent': oee_quality.get('avg_silo_oee_percent_avg', 0.0),
|
||||
'avg_max_abs_deviation_percent': oee_quality.get('max_abs_deviation_percent_avg', 0.0),
|
||||
'total_deviation_alerts': oee_quality.get('deviation_alert_count_sum', 0),
|
||||
'oee_records_count': self._read_group_metric(oee_quality, 'id', 'count', 0),
|
||||
'avg_yield_percent': self._read_group_metric(oee_quality, 'yield_percent', 'avg', 0.0),
|
||||
'avg_consumption_ratio': self._read_group_metric(oee_quality, 'consumption_ratio', 'avg', 0.0),
|
||||
'avg_oee_silo_percent': self._read_group_metric(oee_quality, 'avg_silo_oee_percent', 'avg', 0.0),
|
||||
'avg_max_abs_deviation_percent': self._read_group_metric(oee_quality, 'max_abs_deviation_percent', 'avg', 0.0),
|
||||
'total_deviation_alerts': self._read_group_metric(oee_quality, 'deviation_alert_count', 'sum', 0),
|
||||
'by_equipment': equipment_avg,
|
||||
},
|
||||
'batch_to_batch_deviation_chart': {
|
||||
@@ -1302,7 +1494,8 @@ class ScadaJsonRpcController(http.Controller):
|
||||
[],
|
||||
lazy=False,
|
||||
)
|
||||
target_total_qty = (mo_target_group[0] if mo_target_group else {}).get('product_qty_sum', 0.0)
|
||||
mo_target_row = mo_target_group[0] if mo_target_group else {}
|
||||
target_total_qty = self._read_group_metric(mo_target_row, 'product_qty', 'sum', 0.0)
|
||||
|
||||
oee_total_group = request.env['scada.equipment.oee'].read_group(
|
||||
oee_domain,
|
||||
@@ -1311,8 +1504,8 @@ class ScadaJsonRpcController(http.Controller):
|
||||
lazy=False,
|
||||
)
|
||||
oee_total_row = oee_total_group[0] if oee_total_group else {}
|
||||
actual_total_qty = oee_total_row.get('qty_finished_sum', 0.0)
|
||||
actual_total_target_done = oee_total_row.get('qty_planned_sum', 0.0)
|
||||
actual_total_qty = self._read_group_metric(oee_total_row, 'qty_finished', 'sum', 0.0)
|
||||
actual_total_target_done = self._read_group_metric(oee_total_row, 'qty_planned', 'sum', 0.0)
|
||||
|
||||
# 2) Metrik Total MO
|
||||
mo_done_domain = [('date_finished', '>=', date_from), ('date_finished', '<=', date_to)]
|
||||
@@ -1366,12 +1559,12 @@ class ScadaJsonRpcController(http.Controller):
|
||||
oee_quality_by_equipment.append({
|
||||
'equipment_id': equipment[0],
|
||||
'equipment_name': equipment[1],
|
||||
'oee_records_count': row.get('id_count', 0),
|
||||
'avg_yield_percent': row.get('yield_percent_avg', 0.0),
|
||||
'avg_consumption_ratio': row.get('consumption_ratio_avg', 0.0),
|
||||
'avg_oee_silo_percent': row.get('avg_silo_oee_percent_avg', 0.0),
|
||||
'avg_max_abs_deviation_percent': row.get('max_abs_deviation_percent_avg', 0.0),
|
||||
'total_deviation_alerts': row.get('deviation_alert_count_sum', 0),
|
||||
'oee_records_count': self._read_group_metric(row, 'id', 'count', 0),
|
||||
'avg_yield_percent': self._read_group_metric(row, 'yield_percent', 'avg', 0.0),
|
||||
'avg_consumption_ratio': self._read_group_metric(row, 'consumption_ratio', 'avg', 0.0),
|
||||
'avg_oee_silo_percent': self._read_group_metric(row, 'avg_silo_oee_percent', 'avg', 0.0),
|
||||
'avg_max_abs_deviation_percent': self._read_group_metric(row, 'max_abs_deviation_percent', 'avg', 0.0),
|
||||
'total_deviation_alerts': self._read_group_metric(row, 'deviation_alert_count', 'sum', 0),
|
||||
})
|
||||
|
||||
# 4) Grafik produksi harian dua bar: target vs actual
|
||||
@@ -1380,20 +1573,31 @@ class ScadaJsonRpcController(http.Controller):
|
||||
order='date_planned_start asc, id asc',
|
||||
limit=chart_limit,
|
||||
)
|
||||
oee_daily_actual = request.env['scada.equipment.oee'].search(
|
||||
oee_domain,
|
||||
order='date_done asc, id asc',
|
||||
limit=chart_limit,
|
||||
)
|
||||
target_by_day = defaultdict(float)
|
||||
actual_by_day = defaultdict(float)
|
||||
mo_day_map = {}
|
||||
for mo in mo_daily_target:
|
||||
if mo.date_planned_start:
|
||||
day_key = mo.date_planned_start.date().isoformat()
|
||||
target_by_day[day_key] += mo.product_qty or 0.0
|
||||
mo_day_map[mo.id] = day_key
|
||||
|
||||
# Align actual to the same planned day axis as target (per MO)
|
||||
oee_chart_domain = [('manufacturing_order_id', 'in', list(mo_day_map.keys()) or [0])]
|
||||
if finished_product_ids:
|
||||
oee_chart_domain.append(('product_id', 'in', finished_product_ids))
|
||||
if equipment_ids:
|
||||
oee_chart_domain.append(('equipment_id', 'in', equipment_ids))
|
||||
|
||||
oee_daily_actual = request.env['scada.equipment.oee'].search(
|
||||
oee_chart_domain,
|
||||
order='date_done asc, id asc',
|
||||
limit=chart_limit,
|
||||
)
|
||||
for rec in oee_daily_actual:
|
||||
if rec.date_done:
|
||||
day_key = rec.date_done.date().isoformat()
|
||||
mo_id = rec.manufacturing_order_id.id if rec.manufacturing_order_id else False
|
||||
day_key = mo_day_map.get(mo_id)
|
||||
if day_key:
|
||||
actual_by_day[day_key] += rec.qty_finished or 0.0
|
||||
|
||||
all_days = sorted(set(target_by_day.keys()) | set(actual_by_day.keys()))
|
||||
@@ -1407,37 +1611,65 @@ class ScadaJsonRpcController(http.Controller):
|
||||
]
|
||||
|
||||
# 5) Grafik konsumsi raw material
|
||||
material_domain = [
|
||||
('timestamp', '>=', date_from),
|
||||
('timestamp', '<=', date_to),
|
||||
('active', '=', True),
|
||||
raw_move_domain = [
|
||||
('state', '=', 'done'),
|
||||
('raw_material_production_id', '!=', False),
|
||||
('date', '>=', date_from),
|
||||
('date', '<=', date_to),
|
||||
]
|
||||
if raw_material_ids:
|
||||
material_domain.append(('product_id', 'in', raw_material_ids))
|
||||
material_equipment_ids = equipment_ids
|
||||
if material_equipment_ids:
|
||||
material_domain.append(('equipment_id', 'in', material_equipment_ids))
|
||||
raw_move_domain.append(('product_id', 'in', raw_material_ids))
|
||||
if equipment_ids:
|
||||
raw_move_domain.append(('scada_equipment_id', 'in', equipment_ids))
|
||||
if finished_product_ids:
|
||||
raw_move_domain.append(('raw_material_production_id.product_id', 'in', finished_product_ids))
|
||||
|
||||
raw_material_group = request.env['scada.equipment.material'].read_group(
|
||||
material_domain,
|
||||
['product_id', 'consumption_actual:sum', 'consumption_bom:sum', 'id:count'],
|
||||
raw_move_records = request.env['stock.move'].search(raw_move_domain)
|
||||
raw_move_line_domain = [
|
||||
('state', '=', 'done'),
|
||||
('move_id', 'in', raw_move_records.ids or [0]),
|
||||
]
|
||||
|
||||
raw_material_actual_group = request.env['stock.move.line'].read_group(
|
||||
raw_move_line_domain,
|
||||
['product_id', 'qty_done:sum', 'id:count'],
|
||||
['product_id'],
|
||||
lazy=False,
|
||||
)
|
||||
raw_material_bom_group = request.env['stock.move'].read_group(
|
||||
raw_move_domain,
|
||||
['product_id', 'product_uom_qty:sum'],
|
||||
['product_id'],
|
||||
lazy=False,
|
||||
)
|
||||
|
||||
actual_map = {
|
||||
row['product_id'][0]: row
|
||||
for row in raw_material_actual_group if row.get('product_id')
|
||||
}
|
||||
bom_map = {
|
||||
row['product_id'][0]: row
|
||||
for row in raw_material_bom_group if row.get('product_id')
|
||||
}
|
||||
|
||||
raw_material_chart = []
|
||||
for row in raw_material_group:
|
||||
product = row.get('product_id')
|
||||
material_product_ids = sorted(set(actual_map.keys()) | set(bom_map.keys()))
|
||||
for product_id in material_product_ids:
|
||||
actual_row = actual_map.get(product_id, {})
|
||||
bom_row = bom_map.get(product_id, {})
|
||||
product = actual_row.get('product_id') or bom_row.get('product_id')
|
||||
if not product:
|
||||
continue
|
||||
actual_cons = row.get('consumption_actual_sum', 0.0)
|
||||
bom_cons = row.get('consumption_bom_sum', 0.0)
|
||||
|
||||
actual_cons = self._read_group_metric(actual_row, 'qty_done', 'sum', 0.0)
|
||||
bom_cons = self._read_group_metric(bom_row, 'product_uom_qty', 'sum', 0.0)
|
||||
raw_material_chart.append({
|
||||
'raw_material_id': product[0],
|
||||
'raw_material_name': product[1],
|
||||
'consumption_actual': actual_cons,
|
||||
'consumption_bom': bom_cons,
|
||||
'variance_consumption': actual_cons - bom_cons,
|
||||
'records_count': row.get('id_count', 0),
|
||||
'records_count': self._read_group_metric(actual_row, 'id', 'count', 0),
|
||||
})
|
||||
|
||||
# 6) Table konsumsi per silo + stock awal/akhir periode
|
||||
@@ -1447,23 +1679,45 @@ class ScadaJsonRpcController(http.Controller):
|
||||
silo_equipments = request.env['scada.equipment'].search(silo_domain, order='name asc')
|
||||
|
||||
# Pre-calc consumption per equipment-product in period
|
||||
silo_material_group = request.env['scada.equipment.material'].read_group(
|
||||
material_domain + [('equipment_id', 'in', silo_equipments.ids or [0])],
|
||||
['equipment_id', 'product_id', 'consumption_actual:sum', 'id:count'],
|
||||
['equipment_id', 'product_id'],
|
||||
silo_raw_move_domain = list(raw_move_domain)
|
||||
silo_raw_move_domain.append(('scada_equipment_id', 'in', silo_equipments.ids or [0]))
|
||||
silo_move_records = request.env['stock.move'].search(silo_raw_move_domain)
|
||||
silo_raw_move_line_domain = [
|
||||
('state', '=', 'done'),
|
||||
('move_id', 'in', silo_move_records.ids or [0]),
|
||||
]
|
||||
|
||||
silo_material_group = request.env['stock.move.line'].read_group(
|
||||
silo_raw_move_line_domain,
|
||||
['move_id', 'product_id', 'qty_done:sum', 'id:count'],
|
||||
['move_id', 'product_id'],
|
||||
lazy=False,
|
||||
)
|
||||
|
||||
move_equipment_map = {
|
||||
move.id: move.scada_equipment_id.id
|
||||
for move in silo_move_records if move.scada_equipment_id
|
||||
}
|
||||
|
||||
consumption_map = {}
|
||||
for row in silo_material_group:
|
||||
eq = row.get('equipment_id')
|
||||
move_value = row.get('move_id')
|
||||
prod = row.get('product_id')
|
||||
if not eq or not prod:
|
||||
if not move_value or not prod:
|
||||
continue
|
||||
consumption_map[(eq[0], prod[0])] = {
|
||||
'consumed_qty': row.get('consumption_actual_sum', 0.0),
|
||||
'records_count': row.get('id_count', 0),
|
||||
'product_name': prod[1],
|
||||
}
|
||||
eq_id = move_equipment_map.get(move_value[0])
|
||||
if not eq_id:
|
||||
continue
|
||||
|
||||
key = (eq_id, prod[0])
|
||||
if key not in consumption_map:
|
||||
consumption_map[key] = {
|
||||
'consumed_qty': 0.0,
|
||||
'records_count': 0,
|
||||
'product_name': prod[1],
|
||||
}
|
||||
consumption_map[key]['consumed_qty'] += self._read_group_metric(row, 'qty_done', 'sum', 0.0)
|
||||
consumption_map[key]['records_count'] += self._read_group_metric(row, 'id', 'count', 0)
|
||||
|
||||
# Helpers to reverse stock at period start from current quant + period net movement.
|
||||
stock_quant_model = request.env['stock.quant']
|
||||
@@ -1611,12 +1865,12 @@ class ScadaJsonRpcController(http.Controller):
|
||||
'total_mo_in_progress': total_mo_in_progress,
|
||||
},
|
||||
'metrics_avg_oee_quality': {
|
||||
'oee_records_count': oee_quality_row.get('id_count', 0),
|
||||
'avg_yield_percent': oee_quality_row.get('yield_percent_avg', 0.0),
|
||||
'avg_consumption_ratio': oee_quality_row.get('consumption_ratio_avg', 0.0),
|
||||
'avg_oee_silo_percent': oee_quality_row.get('avg_silo_oee_percent_avg', 0.0),
|
||||
'avg_max_abs_deviation_percent': oee_quality_row.get('max_abs_deviation_percent_avg', 0.0),
|
||||
'total_deviation_alerts': oee_quality_row.get('deviation_alert_count_sum', 0),
|
||||
'oee_records_count': self._read_group_metric(oee_quality_row, 'id', 'count', 0),
|
||||
'avg_yield_percent': self._read_group_metric(oee_quality_row, 'yield_percent', 'avg', 0.0),
|
||||
'avg_consumption_ratio': self._read_group_metric(oee_quality_row, 'consumption_ratio', 'avg', 0.0),
|
||||
'avg_oee_silo_percent': self._read_group_metric(oee_quality_row, 'avg_silo_oee_percent', 'avg', 0.0),
|
||||
'avg_max_abs_deviation_percent': self._read_group_metric(oee_quality_row, 'max_abs_deviation_percent', 'avg', 0.0),
|
||||
'total_deviation_alerts': self._read_group_metric(oee_quality_row, 'deviation_alert_count', 'sum', 0),
|
||||
'by_equipment': oee_quality_by_equipment,
|
||||
},
|
||||
'chart_daily_target_vs_actual': {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from odoo import api, SUPERUSER_ID
|
||||
|
||||
|
||||
def post_init_hook(cr, registry):
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
scada_equipment_model = env['scada.equipment']
|
||||
if scada_equipment_model._maintenance_bridge_column_ready():
|
||||
scada_equipment_model.search([])._sync_to_maintenance_equipment()
|
||||
@@ -1,5 +1,6 @@
|
||||
from . import scada_base
|
||||
from . import scada_equipment
|
||||
from . import scada_maintenance_bridge
|
||||
from . import scada_material_consumption
|
||||
from . import scada_mo_weight
|
||||
from . import scada_equipment_material
|
||||
@@ -15,3 +16,5 @@ from . import mrp_production
|
||||
from . import mrp_bom_line
|
||||
from . import stock_move
|
||||
from . import scada_mo_data_deprecated
|
||||
from . import maintenance_equipment
|
||||
from . import maintenance_request
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,49 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class MaintenanceEquipment(models.Model):
|
||||
_inherit = 'maintenance.equipment'
|
||||
|
||||
scada_equipment_id = fields.Many2one(
|
||||
'scada.equipment',
|
||||
string='SCADA Equipment',
|
||||
ondelete='restrict',
|
||||
index=True,
|
||||
copy=False,
|
||||
)
|
||||
scada_equipment_code = fields.Char(
|
||||
string='SCADA Code',
|
||||
related='scada_equipment_id.equipment_code',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
scada_equipment_type = fields.Selection(
|
||||
related='scada_equipment_id.equipment_type',
|
||||
string='SCADA Type',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
scada_connection_status = fields.Selection(
|
||||
related='scada_equipment_id.connection_status',
|
||||
string='SCADA Connection',
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
'maintenance_equipment_scada_unique',
|
||||
'unique(scada_equipment_id)',
|
||||
'Each SCADA equipment can only be linked to one maintenance equipment.',
|
||||
)
|
||||
]
|
||||
|
||||
@api.onchange('scada_equipment_id')
|
||||
def _onchange_scada_equipment_id(self):
|
||||
if self.scada_equipment_id:
|
||||
self._apply_scada_equipment_values(self.scada_equipment_id)
|
||||
|
||||
def _apply_scada_equipment_values(self, scada_equipment):
|
||||
self.name = scada_equipment.name
|
||||
self.model = scada_equipment.model_number or False
|
||||
self.serial_no = scada_equipment.serial_number or False
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class MaintenanceRequest(models.Model):
|
||||
_inherit = 'maintenance.request'
|
||||
|
||||
scada_equipment_id = fields.Many2one(
|
||||
'scada.equipment',
|
||||
related='equipment_id.scada_equipment_id',
|
||||
string='SCADA Equipment',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ScadaEquipment(models.Model):
|
||||
_inherit = 'scada.equipment'
|
||||
|
||||
maintenance_equipment_ids = fields.One2many(
|
||||
'maintenance.equipment',
|
||||
'scada_equipment_id',
|
||||
string='Maintenance Equipments',
|
||||
)
|
||||
maintenance_request_ids = fields.One2many(
|
||||
'maintenance.request',
|
||||
'scada_equipment_id',
|
||||
string='Maintenance Requests',
|
||||
)
|
||||
maintenance_equipment_count = fields.Integer(
|
||||
compute='_compute_maintenance_counts',
|
||||
string='Maintenance Equipment Count',
|
||||
)
|
||||
maintenance_request_count = fields.Integer(
|
||||
compute='_compute_maintenance_counts',
|
||||
string='Maintenance Request Count',
|
||||
)
|
||||
|
||||
def _compute_maintenance_counts(self):
|
||||
for record in self:
|
||||
record.maintenance_equipment_count = len(record.maintenance_equipment_ids)
|
||||
record.maintenance_request_count = len(record.maintenance_request_ids)
|
||||
|
||||
def _register_hook(self):
|
||||
result = super()._register_hook()
|
||||
if self._maintenance_bridge_column_ready():
|
||||
self.sudo().search([])._sync_to_maintenance_equipment()
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def _maintenance_bridge_column_ready(self):
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'maintenance_equipment'
|
||||
AND column_name = 'scada_equipment_id'
|
||||
"""
|
||||
)
|
||||
return bool(self.env.cr.fetchone())
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super().create(vals_list)
|
||||
records._sync_to_maintenance_equipment()
|
||||
return records
|
||||
|
||||
def write(self, vals):
|
||||
result = super().write(vals)
|
||||
if any(
|
||||
field in vals
|
||||
for field in ('name', 'model_number', 'serial_number', 'is_active')
|
||||
):
|
||||
self._sync_to_maintenance_equipment()
|
||||
return result
|
||||
|
||||
def _prepare_maintenance_equipment_vals(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': self.name,
|
||||
'model': self.model_number or False,
|
||||
'serial_no': self.serial_number or False,
|
||||
'scada_equipment_id': self.id,
|
||||
'active': self.is_active,
|
||||
}
|
||||
|
||||
def _sync_to_maintenance_equipment(self):
|
||||
if not self._maintenance_bridge_column_ready():
|
||||
return
|
||||
maintenance_equipment_model = self.env['maintenance.equipment']
|
||||
for record in self:
|
||||
maintenance_equipment = maintenance_equipment_model.search(
|
||||
[('scada_equipment_id', '=', record.id)],
|
||||
limit=1,
|
||||
)
|
||||
vals = record._prepare_maintenance_equipment_vals()
|
||||
if maintenance_equipment:
|
||||
maintenance_equipment.write(vals)
|
||||
else:
|
||||
maintenance_equipment_model.create(vals)
|
||||
|
||||
def action_view_maintenance_equipment(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref('maintenance.hr_equipment_action').read()[0]
|
||||
action['domain'] = [('scada_equipment_id', '=', self.id)]
|
||||
action['context'] = {'default_scada_equipment_id': self.id}
|
||||
return action
|
||||
|
||||
def action_view_maintenance_requests(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref('maintenance.maintenance_request_action').read()[0]
|
||||
action['domain'] = [('scada_equipment_id', '=', self.id)]
|
||||
action['context'] = {'default_scada_equipment_id': self.id}
|
||||
return action
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,149 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_scada_equipment_form_maintenance" model="ir.ui.view">
|
||||
<field name="name">scada.equipment.form.maintenance</field>
|
||||
<field name="model">scada.equipment</field>
|
||||
<field name="inherit_id" ref="grt_scada.view_scada_equipment_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//sheet" position="inside">
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button
|
||||
name="action_view_maintenance_equipment"
|
||||
type="object"
|
||||
string="Maintenance Equipment"
|
||||
class="oe_stat_button"
|
||||
icon="fa-cogs">
|
||||
<field name="maintenance_equipment_count" widget="statinfo"/>
|
||||
</button>
|
||||
<button
|
||||
name="action_view_maintenance_requests"
|
||||
type="object"
|
||||
string="Maintenance Requests"
|
||||
class="oe_stat_button"
|
||||
icon="fa-wrench">
|
||||
<field name="maintenance_request_count" widget="statinfo"/>
|
||||
</button>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_maintenance_equipment_tree_scada" model="ir.ui.view">
|
||||
<field name="name">maintenance.equipment.tree.scada</field>
|
||||
<field name="model">maintenance.equipment</field>
|
||||
<field name="inherit_id" ref="maintenance.hr_equipment_view_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='name']" position="after">
|
||||
<field name="scada_equipment_code"/>
|
||||
<field name="scada_equipment_type"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_maintenance_equipment_form_scada" model="ir.ui.view">
|
||||
<field name="name">maintenance.equipment.form.scada</field>
|
||||
<field name="model">maintenance.equipment</field>
|
||||
<field name="inherit_id" ref="maintenance.hr_equipment_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='name']" position="after">
|
||||
<field name="scada_equipment_id" options="{'no_create': True}"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='serial_no']" position="after">
|
||||
<field name="scada_equipment_code" readonly="1"/>
|
||||
<field name="scada_equipment_type" readonly="1"/>
|
||||
<field name="scada_connection_status" readonly="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_maintenance_equipment_search_scada" model="ir.ui.view">
|
||||
<field name="name">maintenance.equipment.search.scada</field>
|
||||
<field name="model">maintenance.equipment</field>
|
||||
<field name="inherit_id" ref="maintenance.hr_equipment_view_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='name']" position="after">
|
||||
<field name="scada_equipment_code"/>
|
||||
<field name="scada_equipment_id"/>
|
||||
</xpath>
|
||||
<xpath expr="//search" position="inside">
|
||||
<filter
|
||||
name="filter_scada_equipment"
|
||||
string="SCADA Equipment"
|
||||
domain="[('scada_equipment_id', '!=', False)]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_maintenance_request_tree_scada" model="ir.ui.view">
|
||||
<field name="name">maintenance.request.tree.scada</field>
|
||||
<field name="model">maintenance.request</field>
|
||||
<field name="inherit_id" ref="maintenance.hr_equipment_request_view_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//tree" position="inside">
|
||||
<field name="scada_equipment_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_maintenance_request_form_scada" model="ir.ui.view">
|
||||
<field name="name">maintenance.request.form.scada</field>
|
||||
<field name="model">maintenance.request</field>
|
||||
<field name="inherit_id" ref="maintenance.hr_equipment_request_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='equipment_id']" position="after">
|
||||
<field name="scada_equipment_id" readonly="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_maintenance_request_search_scada" model="ir.ui.view">
|
||||
<field name="name">maintenance.request.search.scada</field>
|
||||
<field name="model">maintenance.request</field>
|
||||
<field name="inherit_id" ref="maintenance.hr_equipment_request_view_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//search" position="inside">
|
||||
<field name="scada_equipment_id"/>
|
||||
<filter
|
||||
name="filter_scada_request"
|
||||
string="SCADA Equipment"
|
||||
domain="[('scada_equipment_id', '!=', False)]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_scada_maintenance_equipment" model="ir.actions.act_window">
|
||||
<field name="name">Maintenance Equipment</field>
|
||||
<field name="res_model">maintenance.equipment</field>
|
||||
<field name="view_mode">tree,kanban,form,pivot,graph</field>
|
||||
<field name="domain">[('scada_equipment_id', '!=', False)]</field>
|
||||
<field name="context">{'search_default_filter_scada_equipment': 1}</field>
|
||||
</record>
|
||||
|
||||
<record id="action_scada_maintenance_request" model="ir.actions.act_window">
|
||||
<field name="name">Maintenance Requests</field>
|
||||
<field name="res_model">maintenance.request</field>
|
||||
<field name="view_mode">tree,kanban,form,pivot,graph</field>
|
||||
<field name="domain">[('scada_equipment_id', '!=', False)]</field>
|
||||
<field name="context">{'search_default_filter_scada_request': 1}</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
id="menu_scada_maintenance"
|
||||
parent="grt_scada.menu_scada_root"
|
||||
name="Maintenance"
|
||||
sequence="46"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_scada_maintenance_equipment"
|
||||
parent="menu_scada_maintenance"
|
||||
action="action_scada_maintenance_equipment"
|
||||
name="Equipment"
|
||||
sequence="1"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_scada_maintenance_request"
|
||||
parent="menu_scada_maintenance"
|
||||
action="action_scada_maintenance_request"
|
||||
name="Requests"
|
||||
sequence="2"/>
|
||||
</odoo>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+847
-8379
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -8,7 +8,9 @@ class KpiPeriod(models.Model):
|
||||
_name = "kpi.period"
|
||||
_description = "KPI Period"
|
||||
_order = "year desc, month desc, id desc"
|
||||
_rec_name = "name"
|
||||
|
||||
name = fields.Char(compute="_compute_name", store=True, index=True)
|
||||
year = fields.Integer(required=True, default=lambda self: fields.Date.today().year)
|
||||
month = fields.Integer(required=True, default=lambda self: fields.Date.today().month)
|
||||
date_start = fields.Date(required=True)
|
||||
@@ -34,3 +36,12 @@ class KpiPeriod(models.Model):
|
||||
rec.date_start = date(rec.year, rec.month, 1)
|
||||
rec.date_end = date(rec.year, rec.month, last_day)
|
||||
|
||||
@api.depends("year", "month")
|
||||
def _compute_name(self):
|
||||
for rec in self:
|
||||
if rec.year and rec.month and 1 <= rec.month <= 12:
|
||||
rec.name = "%s %s" % (calendar.month_name[rec.month], rec.year)
|
||||
elif rec.year and rec.month:
|
||||
rec.name = "%s/%s" % (rec.month, rec.year)
|
||||
else:
|
||||
rec.name = "-"
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# Test script untuk verify SILO equipment OEE records setelah deployment
|
||||
# Jalankan: .\test_silo_oee_after_deployment.ps1
|
||||
|
||||
$base = 'https://kanjabung.web.id'
|
||||
$db = 'kanjabung_MRP'
|
||||
$login = 'admin'
|
||||
$password = 'admin'
|
||||
|
||||
Write-Host "=== SILO OEE Equipment Verification Test ===" -ForegroundColor Cyan
|
||||
Write-Host "Server: $base" -ForegroundColor Gray
|
||||
Write-Host "Database: $db" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
|
||||
# Authenticate
|
||||
Write-Host "[1/3] Authenticating..." -ForegroundColor Yellow
|
||||
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
|
||||
$authBody = @{
|
||||
jsonrpc = '2.0'
|
||||
method = 'call'
|
||||
params = @{
|
||||
db = $db
|
||||
login = $login
|
||||
password = $password
|
||||
}
|
||||
} | ConvertTo-Json -Depth 6
|
||||
|
||||
try {
|
||||
$authResp = Invoke-RestMethod -Uri "$base/web/session/authenticate" `
|
||||
-Method Post `
|
||||
-ContentType 'application/json' `
|
||||
-Body $authBody `
|
||||
-WebSession $session `
|
||||
-TimeoutSec 60
|
||||
Write-Host "✓ Authentication successful (UID: $($authResp.result.uid))" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "✗ Authentication failed: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Test OEE equipment avg endpoint with date range
|
||||
Write-Host "[2/3] Fetching OEE equipment data (24/02/2026 - 13/03/2026)..." -ForegroundColor Yellow
|
||||
$payload = @{
|
||||
jsonrpc = '2.0'
|
||||
method = 'call'
|
||||
params = @{
|
||||
date_from = '2026-02-24'
|
||||
date_to = '2026-03-13'
|
||||
limit = 100
|
||||
offset = 0
|
||||
}
|
||||
} | ConvertTo-Json -Depth 8
|
||||
|
||||
try {
|
||||
$response = Invoke-RestMethod -Uri "$base/api/scada/oee-equipment-avg" `
|
||||
-Method Post `
|
||||
-ContentType 'application/json' `
|
||||
-Body $payload `
|
||||
-WebSession $session `
|
||||
-TimeoutSec 60
|
||||
|
||||
Write-Host "✓ API call successful" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Show summary
|
||||
$total = $response.result.count
|
||||
$equipWithData = @($response.result.data | Where-Object { $_.oee_records_count -gt 0 })
|
||||
|
||||
Write-Host "=== RESULTS ===" -ForegroundColor Cyan
|
||||
Write-Host "Total Equipment: $total"
|
||||
Write-Host "Equipment with OEE records: $($equipWithData.Count)"
|
||||
Write-Host ""
|
||||
|
||||
# Show equipment with OEE records
|
||||
Write-Host "Equipment with OEE Data:" -ForegroundColor Yellow
|
||||
$equipWithData | ForEach-Object {
|
||||
$name = $_.equipment.name
|
||||
$code = $_.equipment.code
|
||||
$count = $_.oee_records_count
|
||||
$yield = [math]::Round($_.avg_summary.yield_percent, 2)
|
||||
Write-Host " • $name ($code)" -ForegroundColor Cyan
|
||||
Write-Host " - OEE Records: $count" -ForegroundColor Gray
|
||||
Write-Host " - Avg Yield: $yield%" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== VERIFICATION ===" -ForegroundColor Cyan
|
||||
|
||||
# Check if SILO equipment have records
|
||||
$siloEquip = @($equipWithData | Where-Object { $_.equipment.code -match 'silo' })
|
||||
$lqEquip = @($equipWithData | Where-Object { $_.equipment.code -match 'lq' })
|
||||
$plcEquip = @($equipWithData | Where-Object { $_.equipment.code -match 'plc' })
|
||||
|
||||
Write-Host "SILO equipment with data: $($siloEquip.Count)" -ForegroundColor $(if($siloEquip.Count -gt 0) { 'Green' } else { 'Yellow' })
|
||||
Write-Host "LQ equipment with data: $($lqEquip.Count)" -ForegroundColor $(if($lqEquip.Count -gt 0) { 'Green' } else { 'Yellow' })
|
||||
Write-Host "PLC equipment with data: $($plcEquip.Count)" -ForegroundColor $(if($plcEquip.Count -gt 0) { 'Green' } else { 'Yellow' })
|
||||
|
||||
if ($siloEquip.Count -gt 0) {
|
||||
Write-Host ""
|
||||
Write-Host "✓ SUCCESS: SILO equipment sekarang muncul dengan OEE records!" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host ""
|
||||
Write-Host "⚠ WARNING: SILO equipment masih belum ada OEE records" -ForegroundColor Yellow
|
||||
Write-Host " Kemungkinan: deployment maybe belum selesai atau code belum di-reload" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
} catch {
|
||||
Write-Host "✗ API call failed: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[3/3] Test completed" -ForegroundColor Yellow
|
||||
Binary file not shown.
@@ -0,0 +1,378 @@
|
||||
from pathlib import Path
|
||||
|
||||
import xlsxwriter
|
||||
|
||||
|
||||
OUTPUT_PATH = Path("odoo_kpi") / "kpi_weight_sample.xlsx"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
workbook = xlsxwriter.Workbook(str(OUTPUT_PATH))
|
||||
workbook.set_properties(
|
||||
{
|
||||
"title": "KPI Weight Sample",
|
||||
"subject": "Contoh perhitungan bobot KPI employee",
|
||||
"author": "Codex",
|
||||
"comments": "Generated from odoo_kpi formula",
|
||||
}
|
||||
)
|
||||
title_fmt = workbook.add_format(
|
||||
{
|
||||
"bold": True,
|
||||
"font_size": 14,
|
||||
"bg_color": "#D9EAF7",
|
||||
"border": 1,
|
||||
"align": "center",
|
||||
"valign": "vcenter",
|
||||
}
|
||||
)
|
||||
header_fmt = workbook.add_format(
|
||||
{
|
||||
"bold": True,
|
||||
"bg_color": "#B7DEE8",
|
||||
"border": 1,
|
||||
"align": "center",
|
||||
"valign": "vcenter",
|
||||
}
|
||||
)
|
||||
text_fmt = workbook.add_format({"border": 1, "valign": "top"})
|
||||
center_fmt = workbook.add_format({"border": 1, "align": "center", "valign": "vcenter"})
|
||||
num_fmt = workbook.add_format({"border": 1, "num_format": "0.00"})
|
||||
pct_fmt = workbook.add_format({"border": 1, "num_format": "0.00%"})
|
||||
note_fmt = workbook.add_format(
|
||||
{
|
||||
"text_wrap": True,
|
||||
"valign": "top",
|
||||
"border": 1,
|
||||
}
|
||||
)
|
||||
total_label_fmt = workbook.add_format(
|
||||
{
|
||||
"bold": True,
|
||||
"bg_color": "#E2F0D9",
|
||||
"border": 1,
|
||||
}
|
||||
)
|
||||
total_num_fmt = workbook.add_format(
|
||||
{
|
||||
"bold": True,
|
||||
"bg_color": "#E2F0D9",
|
||||
"border": 1,
|
||||
"num_format": "0.00",
|
||||
}
|
||||
)
|
||||
section_fmt = workbook.add_format(
|
||||
{
|
||||
"bold": True,
|
||||
"font_size": 12,
|
||||
"bg_color": "#FCE4D6",
|
||||
"border": 1,
|
||||
}
|
||||
)
|
||||
info_fmt = workbook.add_format(
|
||||
{
|
||||
"text_wrap": True,
|
||||
"border": 1,
|
||||
"valign": "top",
|
||||
"bg_color": "#FFF2CC",
|
||||
}
|
||||
)
|
||||
input_fmt = workbook.add_format(
|
||||
{
|
||||
"border": 1,
|
||||
"bg_color": "#FFF2CC",
|
||||
"num_format": "0.00",
|
||||
}
|
||||
)
|
||||
grade_fmt = workbook.add_format(
|
||||
{
|
||||
"bold": True,
|
||||
"font_size": 12,
|
||||
"align": "center",
|
||||
"valign": "vcenter",
|
||||
"border": 1,
|
||||
"bg_color": "#E2F0D9",
|
||||
}
|
||||
)
|
||||
subtitle_fmt = workbook.add_format(
|
||||
{
|
||||
"italic": True,
|
||||
"font_color": "#555555",
|
||||
}
|
||||
)
|
||||
|
||||
sheet = workbook.add_worksheet("Sample KPI")
|
||||
sheet.set_column("A:A", 8)
|
||||
sheet.set_column("B:B", 24)
|
||||
sheet.set_column("C:F", 14)
|
||||
sheet.set_column("G:H", 18)
|
||||
sheet.set_column("I:I", 16)
|
||||
sheet.set_column("J:J", 42)
|
||||
|
||||
sheet.freeze_panes(3, 0)
|
||||
sheet.merge_range("A1:J1", "Sample Perhitungan Bobot KPI Employee", title_fmt)
|
||||
sheet.merge_range(
|
||||
"A2:J2",
|
||||
"Formula modul odoo_kpi: Total Score = SUM((Actual / Target) x Weight) untuk semua assignment employee dalam 1 periode.",
|
||||
info_fmt,
|
||||
)
|
||||
sheet.write_row(
|
||||
"A3",
|
||||
[
|
||||
"No",
|
||||
"KPI",
|
||||
"Target",
|
||||
"Actual",
|
||||
"Weight",
|
||||
"Achievement",
|
||||
"Nilai Assignment",
|
||||
"Kontribusi ke Total",
|
||||
"Status",
|
||||
"Catatan",
|
||||
],
|
||||
header_fmt,
|
||||
)
|
||||
|
||||
rows = [
|
||||
(
|
||||
1,
|
||||
"Sales",
|
||||
100,
|
||||
80,
|
||||
50,
|
||||
"=(D4/C4)",
|
||||
"=(D4/C4)*E4",
|
||||
"=G4/$G$8",
|
||||
'=IF(D4>C4,"Over target",IF(D4=C4,"On target","Below target"))',
|
||||
"Rumus Odoo: (actual / target) x weight",
|
||||
),
|
||||
(
|
||||
2,
|
||||
"Visit",
|
||||
20,
|
||||
15,
|
||||
30,
|
||||
"=(D5/C5)",
|
||||
"=(D5/C5)*E5",
|
||||
"=G5/$G$8",
|
||||
'=IF(D5>C5,"Over target",IF(D5=C5,"On target","Below target"))',
|
||||
"Kontribusi dihitung dari nilai assignment dibagi total score employee",
|
||||
),
|
||||
(
|
||||
3,
|
||||
"Collection",
|
||||
10,
|
||||
12,
|
||||
20,
|
||||
"=(D6/C6)",
|
||||
"=(D6/C6)*E6",
|
||||
"=G6/$G$8",
|
||||
'=IF(D6>C6,"Over target",IF(D6=C6,"On target","Below target"))',
|
||||
"Tidak ada cap di kode, jadi over target bisa melampaui bobot",
|
||||
),
|
||||
]
|
||||
|
||||
start_row = 3
|
||||
for row_idx, row in enumerate(rows, start=start_row):
|
||||
sheet.write_number(row_idx, 0, row[0], center_fmt)
|
||||
sheet.write_string(row_idx, 1, row[1], text_fmt)
|
||||
sheet.write_number(row_idx, 2, row[2], num_fmt)
|
||||
sheet.write_number(row_idx, 3, row[3], num_fmt)
|
||||
sheet.write_number(row_idx, 4, row[4], num_fmt)
|
||||
sheet.write_formula(row_idx, 5, row[5], pct_fmt)
|
||||
sheet.write_formula(row_idx, 6, row[6], num_fmt)
|
||||
sheet.write_formula(row_idx, 7, row[7], pct_fmt)
|
||||
sheet.write_formula(row_idx, 8, row[8], center_fmt)
|
||||
sheet.write_string(row_idx, 9, row[9], note_fmt)
|
||||
|
||||
sheet.write("F8", "Total Score", total_label_fmt)
|
||||
sheet.write_formula("G8", "=SUM(G4:G6)", total_num_fmt)
|
||||
sheet.write("F9", "Total Weight", total_label_fmt)
|
||||
sheet.write_formula("G9", "=SUM(E4:E6)", total_num_fmt)
|
||||
sheet.write("F10", "Grade", total_label_fmt)
|
||||
sheet.write_formula(
|
||||
"G10",
|
||||
'=IF(G8>=90,"A",IF(G8>=75,"B",IF(G8>=60,"C",IF(G8>=40,"D","E"))))',
|
||||
grade_fmt,
|
||||
)
|
||||
sheet.write("H8", "Interpretasi", total_label_fmt)
|
||||
sheet.write_formula(
|
||||
"I8",
|
||||
'=IF(G8>=90,"Sangat baik",IF(G8>=75,"Baik",IF(G8>=60,"Cukup",IF(G8>=40,"Kurang","Buruk"))))',
|
||||
total_label_fmt,
|
||||
)
|
||||
sheet.write("H9", "Catatan", total_label_fmt)
|
||||
sheet.merge_range(
|
||||
"I9:J10",
|
||||
"Jika total weight = 100 maka total score terasa seperti skala 0-100. Jika actual > target, nilai assignment bisa melebihi bobot karena rumus belum di-cap.",
|
||||
note_fmt,
|
||||
)
|
||||
|
||||
sheet.write("A12", "Cara pakai:", header_fmt)
|
||||
instructions = [
|
||||
"Ubah kolom Target, Actual, atau Weight untuk simulasi KPI lain.",
|
||||
"Achievement = Actual / Target.",
|
||||
"Nilai Assignment = Achievement x Weight.",
|
||||
"Kontribusi ke Total = Nilai Assignment / Total Score.",
|
||||
"Jika total weight = 100, total score akan terasa seperti nilai skala 0-100.",
|
||||
"Jika actual melebihi target, nilai assignment bisa melebihi bobot karena kode saat ini tidak membatasi hasil.",
|
||||
]
|
||||
for idx, instruction in enumerate(instructions, start=12):
|
||||
sheet.write(f"A{idx}", f"{idx - 11}.", text_fmt)
|
||||
sheet.merge_range(f"B{idx}:J{idx}", instruction, note_fmt)
|
||||
|
||||
summary_sheet = workbook.add_worksheet("Executive Summary")
|
||||
summary_sheet.set_column("A:A", 24)
|
||||
summary_sheet.set_column("B:B", 18)
|
||||
summary_sheet.set_column("C:C", 40)
|
||||
summary_sheet.freeze_panes(4, 0)
|
||||
|
||||
summary_sheet.merge_range("A1:C1", "Ringkasan Untuk HR / Manager", title_fmt)
|
||||
summary_sheet.write("A3", "Area", header_fmt)
|
||||
summary_sheet.write("B3", "Nilai", header_fmt)
|
||||
summary_sheet.write("C3", "Keterangan", header_fmt)
|
||||
summary_sheet.write("A4", "Total Score Employee", text_fmt)
|
||||
summary_sheet.write_formula("B4", "='Sample KPI'!G8", total_num_fmt)
|
||||
summary_sheet.write("C4", "Total nilai akhir employee untuk periode tersebut.", note_fmt)
|
||||
summary_sheet.write("A5", "Grade", text_fmt)
|
||||
summary_sheet.write_formula("B5", "='Sample KPI'!G10", grade_fmt)
|
||||
summary_sheet.write("C5", "Grade mengikuti mapping A/B/C/D/E dari modul.", note_fmt)
|
||||
summary_sheet.write("A6", "Total Weight", text_fmt)
|
||||
summary_sheet.write_formula("B6", "='Sample KPI'!G9", total_num_fmt)
|
||||
summary_sheet.write("C6", "Idealnya 100 jika ingin score langsung terbaca sebagai persentase performa.", note_fmt)
|
||||
summary_sheet.write("A8", "KPI Dominan", header_fmt)
|
||||
summary_sheet.write("B8", "Kontribusi", header_fmt)
|
||||
summary_sheet.write("C8", "Makna", header_fmt)
|
||||
summary_sheet.write_formula("A9", '=INDEX(\'Sample KPI\'!B4:B6, MATCH(MAX(\'Sample KPI\'!H4:H6), \'Sample KPI\'!H4:H6, 0))', text_fmt)
|
||||
summary_sheet.write_formula("B9", '=MAX(\'Sample KPI\'!H4:H6)', pct_fmt)
|
||||
summary_sheet.write("C9", "Assignment dengan sumbangan terbesar ke total nilai akhir.", note_fmt)
|
||||
summary_sheet.merge_range(
|
||||
"A11:C14",
|
||||
"Baca report ini seperti berikut: bobot menunjukkan porsi maksimum kontribusi KPI, tetapi nilai akhir tetap tergantung pencapaian actual terhadap target. KPI dengan bobot besar tetapi actual rendah tetap memberi kontribusi kecil.",
|
||||
info_fmt,
|
||||
)
|
||||
|
||||
override_sheet = workbook.add_worksheet("Override Example")
|
||||
override_sheet.set_column("A:A", 24)
|
||||
override_sheet.set_column("B:C", 16)
|
||||
override_sheet.set_column("D:D", 38)
|
||||
override_sheet.freeze_panes(3, 0)
|
||||
|
||||
override_sheet.merge_range("A1:D1", "Contoh Target Override dan Weight Override", title_fmt)
|
||||
override_sheet.write("A2", "Override dipakai lebih dulu daripada default target/weight KPI.", subtitle_fmt)
|
||||
override_sheet.write_row("A3", ["Parameter", "Nilai", "Dipakai?", "Catatan"], header_fmt)
|
||||
override_rows = [
|
||||
("Default Target KPI", 100, "Tidak", "Akan kalah prioritas jika target_override diisi"),
|
||||
("Default Weight KPI", 40, "Tidak", "Akan kalah prioritas jika weight_override diisi"),
|
||||
("Target Override", 80, "Ya", "Dipakai sebagai effective_target"),
|
||||
("Weight Override", 50, "Ya", "Dipakai sebagai effective_weight"),
|
||||
("Actual", 60, "Ya", "Total dari semua kpi.value untuk assignment ini"),
|
||||
]
|
||||
for idx, row in enumerate(override_rows, start=3):
|
||||
override_sheet.write_string(idx, 0, row[0], text_fmt)
|
||||
override_sheet.write_number(idx, 1, row[1], num_fmt)
|
||||
override_sheet.write_string(idx, 2, row[2], text_fmt)
|
||||
override_sheet.write_string(idx, 3, row[3], note_fmt)
|
||||
|
||||
override_sheet.write("A10", "Rumus", total_label_fmt)
|
||||
override_sheet.write_formula("B10", "=(B7/B5)*B6", total_num_fmt)
|
||||
override_sheet.write("C10", "Hasil", total_label_fmt)
|
||||
override_sheet.write("D10", "Nilai assignment = (60 / 80) x 50 = 37.5", note_fmt)
|
||||
|
||||
sim_sheet = workbook.add_worksheet("Simulator")
|
||||
sim_sheet.set_column("A:A", 8)
|
||||
sim_sheet.set_column("B:B", 24)
|
||||
sim_sheet.set_column("C:F", 14)
|
||||
sim_sheet.set_column("G:H", 18)
|
||||
sim_sheet.set_column("I:J", 24)
|
||||
sim_sheet.freeze_panes(5, 0)
|
||||
|
||||
sim_sheet.merge_range("A1:J1", "Simulator KPI Employee", title_fmt)
|
||||
sim_sheet.merge_range(
|
||||
"A2:J2",
|
||||
"Kolom kuning bisa diubah. Rumus akan otomatis menghitung nilai assignment, kontribusi, total score, dan grade.",
|
||||
info_fmt,
|
||||
)
|
||||
sim_sheet.write("A4", "Employee Name", header_fmt)
|
||||
sim_sheet.merge_range("B4:D4", "Nama Employee", info_fmt)
|
||||
sim_sheet.write("E4", "Periode", header_fmt)
|
||||
sim_sheet.merge_range("F4:G4", "2026-03", info_fmt)
|
||||
sim_sheet.write_row(
|
||||
"A5",
|
||||
[
|
||||
"No",
|
||||
"KPI",
|
||||
"Target",
|
||||
"Actual",
|
||||
"Weight",
|
||||
"Achievement",
|
||||
"Nilai Assignment",
|
||||
"Kontribusi",
|
||||
"Catatan",
|
||||
"Status",
|
||||
],
|
||||
header_fmt,
|
||||
)
|
||||
|
||||
sim_rows = range(6, 14)
|
||||
for excel_row, seq in zip(sim_rows, range(1, 9)):
|
||||
sim_sheet.write_number(excel_row - 1, 0, seq, center_fmt)
|
||||
sim_sheet.write_blank(excel_row - 1, 1, "", text_fmt)
|
||||
sim_sheet.write_blank(excel_row - 1, 2, "", input_fmt)
|
||||
sim_sheet.write_blank(excel_row - 1, 3, "", input_fmt)
|
||||
sim_sheet.write_blank(excel_row - 1, 4, "", input_fmt)
|
||||
sim_sheet.write_formula(
|
||||
excel_row - 1,
|
||||
5,
|
||||
f'=IFERROR(D{excel_row}/C{excel_row},0)',
|
||||
pct_fmt,
|
||||
)
|
||||
sim_sheet.write_formula(
|
||||
excel_row - 1,
|
||||
6,
|
||||
f'=IF(OR(C{excel_row}<=0,E{excel_row}<=0),0,(D{excel_row}/C{excel_row})*E{excel_row})',
|
||||
num_fmt,
|
||||
)
|
||||
sim_sheet.write_formula(
|
||||
excel_row - 1,
|
||||
7,
|
||||
f'=IFERROR(G{excel_row}/$G$16,0)',
|
||||
pct_fmt,
|
||||
)
|
||||
sim_sheet.write_blank(excel_row - 1, 8, "", note_fmt)
|
||||
sim_sheet.write_formula(
|
||||
excel_row - 1,
|
||||
9,
|
||||
f'=IF(G{excel_row}=0,"No score",IF(D{excel_row}>C{excel_row},"Over target",IF(D{excel_row}=C{excel_row},"On target","Below target")))',
|
||||
center_fmt,
|
||||
)
|
||||
|
||||
sim_sheet.write("F16", "Total Score", total_label_fmt)
|
||||
sim_sheet.write_formula("G16", "=SUM(G6:G13)", total_num_fmt)
|
||||
sim_sheet.write("F17", "Total Weight", total_label_fmt)
|
||||
sim_sheet.write_formula("G17", "=SUM(E6:E13)", total_num_fmt)
|
||||
sim_sheet.write("F18", "Grade", total_label_fmt)
|
||||
sim_sheet.write_formula(
|
||||
"G18",
|
||||
'=IF(G16>=90,"A",IF(G16>=75,"B",IF(G16>=60,"C",IF(G16>=40,"D","E"))))',
|
||||
grade_fmt,
|
||||
)
|
||||
sim_sheet.write("H16", "Interpretasi", total_label_fmt)
|
||||
sim_sheet.write_formula(
|
||||
"I16",
|
||||
'=IF(G16>=90,"Sangat baik",IF(G16>=75,"Baik",IF(G16>=60,"Cukup",IF(G16>=40,"Kurang","Buruk"))))',
|
||||
total_label_fmt,
|
||||
)
|
||||
sim_sheet.merge_range(
|
||||
"H17:J18",
|
||||
"Tips: set total bobot ke 100. Jika total bobot tidak 100, score akhir tetap valid menurut kode, tetapi lebih sulit dibaca sebagai skala 0-100.",
|
||||
note_fmt,
|
||||
)
|
||||
|
||||
workbook.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user