Compare commits
3 Commits
c0a185478b
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1c6b39444 | ||
|
|
24c7ea9447 | ||
|
|
dd44452fa1 |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
docs
|
||||||
|
gmn_freshservice/tests
|
||||||
126
README.md
Normal file
126
README.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# Generator Laporan Analisis Tiket (GMN Freshservice)
|
||||||
|
|
||||||
|
| Atribut | Nilai |
|
||||||
|
|---|---|
|
||||||
|
| **Versi** | 1.0.0 |
|
||||||
|
| **Tanggal** | 20 Oktober 2026 |
|
||||||
|
| **Status** | Disetujui (Approved) |
|
||||||
|
| **Pembaca** | Semua Tim (Manajemen, Service Desk, Developer, QA) |
|
||||||
|
| **Pembaruan Terakhir** | Saat penyesuaian aturan klasifikasi SLA |
|
||||||
|
|
||||||
|
> **Intinya dalam 3 kalimat:**
|
||||||
|
> Aplikasi ini membaca riwayat aktivitas (audit trail) tiket Freshservice dan mengubahnya menjadi laporan Excel 3 sheet yang adil dan transparan. Sistem menghitung secara otomatis berapa lama waktu pengerjaan di pihak IT, waktu tunggu di pihak pemohon/user, waktu vendor, dan waktu rapat bersama. Dengan laporan ini, evaluasi SLA tidak lagi saling menyalahkan karena setiap menit tercatat lengkap dengan bukti pesan dan statusnya.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Daftar Dokumen & Panduan Membaca
|
||||||
|
Seluruh dokumentasi teknis, bisnis, operasional, dan pengujian tersimpan rapi di dalam folder `docs/`:
|
||||||
|
|
||||||
|
| Kategori | Dokumen Kunci | Keterangan |
|
||||||
|
|---|---|---|
|
||||||
|
| **Daftar Isi & Indeks** | [`docs/00-INDEX.md`](docs/00-INDEX.md) | Peta navigasi seluruh dokumen dan urutan baca pemula |
|
||||||
|
| **Bisnis & Kebutuhan** | [`docs/01-bisnis/BRD.md`](docs/01-bisnis/BRD.md)<br>[`docs/02-kebutuhan/SRS.md`](docs/02-kebutuhan/SRS.md)<br>[`docs/02-kebutuhan/Katalog-Aturan-Bisnis.md`](docs/02-kebutuhan/Katalog-Aturan-Bisnis.md) | Alasan proyek dibuat, spesifikasi kebutuhan FR/NFR, dan katalog aturan RULE-01 s/d RULE-12 |
|
||||||
|
| **Desain Teknis & Data** | [`docs/03-desain/FDD.md`](docs/03-desain/FDD.md)<br>[`docs/03-desain/TDD.md`](docs/03-desain/TDD.md)<br>[`docs/03-desain/Arsitektur.md`](docs/03-desain/Arsitektur.md)<br>[`docs/03-desain/Kamus-Data-dan-DFD.md`](docs/03-desain/Kamus-Data-dan-DFD.md)<br>[`docs/03-desain/ADR/`](docs/03-desain/ADR/) | Desain fungsional, arsitektur modul Odoo, kamus kolom Excel, dan catatan keputusan arsitektur (ADR-01 s/d ADR-03) |
|
||||||
|
| **Pengujian & Validasi** | [`docs/04-pengujian/Test-Plan.md`](docs/04-pengujian/Test-Plan.md)<br>[`docs/04-pengujian/Test-Case.md`](docs/04-pengujian/Test-Case.md)<br>[`docs/04-pengujian/Laporan-Validasi.md`](docs/04-pengujian/Laporan-Validasi.md)<br>[`docs/04-pengujian/UAT.md`](docs/04-pengujian/UAT.md) | Rencana uji, skenario uji TC-01 s/d TC-08, bukti validasi Tiket #9658, dan lembar UAT pengguna |
|
||||||
|
| **Operasional & Panduan** | [`docs/05-operasional/Panduan-Pengguna.md`](docs/05-operasional/Panduan-Pengguna.md)<br>[`docs/05-operasional/SOP-Status-Tiket.md`](docs/05-operasional/SOP-Status-Tiket.md)<br>[`docs/05-operasional/Runbook.md`](docs/05-operasional/Runbook.md)<br>[`docs/05-operasional/Konfigurasi.md`](docs/05-operasional/Konfigurasi.md) | Panduan langkah demi langkah agen/supervisor, SOP pemilihan status, instalasi, dan parameter konfigurasi |
|
||||||
|
| **Manajemen & Tata Kelola**| [`docs/06-manajemen/Risiko-dan-Known-Issues.md`](docs/06-manajemen/Risiko-dan-Known-Issues.md)<br>[`docs/06-manajemen/Change-Request-Perbaikan-Logic.md`](docs/06-manajemen/Change-Request-Perbaikan-Logic.md)<br>[`docs/06-manajemen/Keamanan-dan-Privasi.md`](docs/06-manajemen/Keamanan-dan-Privasi.md)<br>[`docs/06-manajemen/Traceability-Matrix.md`](docs/06-manajemen/Traceability-Matrix.md)<br>[`docs/06-manajemen/Roadmap.md`](docs/06-manajemen/Roadmap.md)<br>[`docs/06-manajemen/Glosarium.md`](docs/06-manajemen/Glosarium.md)<br>[`CHANGELOG.md`](CHANGELOG.md) | Daftar risiko, analisis dampak CR-01, matriks ketelusuran BR-FR-RULE-TC, peta jalan rilis, dan glosarium istilah |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Fitur Utama
|
||||||
|
1. **Two-Tier Synchronization**: Tarik daftar tiket secara instan tanpa freeze, dan ambil rincian lengkap (aktivitas audit trail + riwayat chat) secara on-demand saat tiket dibuka.
|
||||||
|
2. **Mesin Hitung Jam Kerja Efektif**:
|
||||||
|
- Hari Kerja: Senin – Jumat (08:30 – 17:30 WIB).
|
||||||
|
- Istirahat: 12:30 – 13:30 WIB (tidak dihitung sebagai jam kerja).
|
||||||
|
- Libur Nasional: Terintegrasi dengan kalender libur Indonesia (2024–2026) dan kalender Odoo.
|
||||||
|
- Konversi 8 jam kerja efektif = 1 hari kerja.
|
||||||
|
3. **Klasifikasi Tanggung Jawab Adil (Fair Responsibility Logic)**:
|
||||||
|
- Membagi waktu tiket ke dalam 8 kategori (*bucket*).
|
||||||
|
- Deteksi otomatis balasan agen (*Ball-in-Court Analysis*) untuk menandai status ambigu (`needs_verification`).
|
||||||
|
- Deteksi undangan rapat Zoom/Teams otomatis ke bucket *Scheduled Meeting (Bersama)*.
|
||||||
|
- Pemisahan durasi *Resolved* oleh bot otomatis sistem (`user_after_auto_resolve`) agar tidak menyalahkan user secara keliru.
|
||||||
|
4. **Generator Laporan Excel 3 Sheet**:
|
||||||
|
- **Sheet 1 (Ringkasan & Bottleneck KPI)**: Kartu KPI, breakdown persentase tanggung jawab, dan tabel ranking segmen bottleneck terlama.
|
||||||
|
- **Sheet 2 (Audit Trail & Chronology)**: Rekonstruksi kronologis setiap menit aktivitas tiket dengan formula total otomatis.
|
||||||
|
- **Sheet 3 (Conversations & Notes)**: Transkrip lengkap percakapan dan catatan tiket antara agen dan pemohon.
|
||||||
|
5. **Dukungan Multi-Tiket**: Ekspor perbandingan ranking bottleneck untuk bahan rapat koordinasi mingguan tim Service Desk.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Panduan Cepat (Quick Start dalam 15 Menit)
|
||||||
|
|
||||||
|
### 3.1 Prasyarat
|
||||||
|
- Odoo 19.0 Community / Enterprise (Python 3.10+)
|
||||||
|
- Modul python `xlsxwriter` dan `requests` terpasang di environment Odoo
|
||||||
|
- Akun admin Freshservice dengan hak akses API (API Key)
|
||||||
|
|
||||||
|
### 3.2 Konfigurasi Koneksi
|
||||||
|
1. Masuk ke Odoo dengan hak akses Administrator.
|
||||||
|
2. Buka menu **Settings** > **Freshservice Configuration**.
|
||||||
|
3. Masukkan URL Freshservice (contoh: `https://servicedesk.perusahaan.co.id`) dan **API Key**.
|
||||||
|
4. Atur batas sinkronisasi (default: `200` tiket).
|
||||||
|
5. Klik tombol **Uji Koneksi** untuk memastikan status terhubung.
|
||||||
|
|
||||||
|
### 3.3 Menjalankan Sinkronisasi & Analisis
|
||||||
|
1. Buka menu **Freshservice** > **Daftar Tiket**.
|
||||||
|
2. Klik tombol **Tarik Tiket Terbaru** pada bagian atas tabel.
|
||||||
|
3. Buka salah satu tiket yang ingin dianalisis (misal: Tiket #9658).
|
||||||
|
4. Klik tombol **Analisis Bottleneck (Excel)** di pojok kanan atas formulir tiket.
|
||||||
|
5. Pilih tipe laporan: *Analisis Detail Bottleneck Tiket Tunggal*.
|
||||||
|
6. Klik **Unduh Laporan Excel**. File `.xlsx` 3 sheet akan otomatis diunduh ke komputer Anda.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Struktur Folder Modul
|
||||||
|
```text
|
||||||
|
gmn_module_v2/
|
||||||
|
├── README.md # Dokumentasi utama proyek ini
|
||||||
|
├── CHANGELOG.md # Catatan rilis dan riwayat perubahan
|
||||||
|
├── docs/ # Folder seluruh dokumen pendukung
|
||||||
|
│ ├── 00-INDEX.md # Indeks dokumen dan panduan membaca
|
||||||
|
│ ├── 01-bisnis/ # BRD (Business Requirements Document)
|
||||||
|
│ ├── 02-kebutuhan/ # SRS & Katalog Aturan Bisnis
|
||||||
|
│ ├── 03-desain/ # FDD, TDD, Arsitektur, Kamus Data, ADR
|
||||||
|
│ ├── 04-pengujian/ # Test Plan, Test Case, Laporan Validasi, UAT
|
||||||
|
│ ├── 05-operasional/ # Panduan Pengguna, SOP, Runbook, Konfigurasi
|
||||||
|
│ └── 06-manajemen/ # Risiko, Change Request, Traceability Matrix, Glosarium
|
||||||
|
└── gmn_freshservice/ # Kode sumber addon Odoo 19
|
||||||
|
├── __init__.py
|
||||||
|
├── __manifest__.py
|
||||||
|
├── models/ # Model Odoo: ticket, stage_log, conversation, settings
|
||||||
|
├── services/ # Client integrasi Freshservice REST API v2
|
||||||
|
├── wizard/ # Wizard export laporan Excel via xlsxwriter
|
||||||
|
├── views/ # XML view Odoo (form, list, settings, menu)
|
||||||
|
├── data/ # Demo data & konfigurasi default
|
||||||
|
├── security/ # Hak akses ir.model.access.csv
|
||||||
|
└── tests/ # Unit test otomatis & validasi acceptance
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Catatan Penulisan & Verifikasi Integritas (Self-Check)
|
||||||
|
|
||||||
|
1. **Daftar File Kode Sumber yang Dianalisis**:
|
||||||
|
- `gmn_freshservice/__manifest__.py`
|
||||||
|
- `gmn_freshservice/models/gmn_freshservice_ticket_v01.py`
|
||||||
|
- `gmn_freshservice/models/gmn_freshservice_ticket_stage_log_v01.py`
|
||||||
|
- `gmn_freshservice/models/gmn_freshservice_ticket_conversation_v01.py`
|
||||||
|
- `gmn_freshservice/models/gmn_res_config_settings_v01.py`
|
||||||
|
- `gmn_freshservice/services/gmn_freshservice_client_v01.py`
|
||||||
|
- `gmn_freshservice/wizard/gmn_freshservice_ticket_bottleneck_wizard_v01.py`
|
||||||
|
- `gmn_freshservice/tests/test_responsibility_rules.py`
|
||||||
|
2. **Daftar Item [PERLU KONFIRMASI]**:
|
||||||
|
- PK-01: Nama resmi departemen pemohon SLS apakah ada kepanjangan divisi spesifik di luar data Freshservice? (Asumsi saat ini: SLS merepresentasikan departemen operasional pemohon).
|
||||||
|
- PK-02: Jadwal rilis resmi ke server staging user apakah mengikuti jadwal sprint dua mingguan tim IT?
|
||||||
|
- PK-03: Kebijakan retensi data teks pesan percakapan di database Odoo lokal (apakah dipangkas setelah 1 tahun atau disimpan selamanya).
|
||||||
|
3. **Daftar Aturan yang Semula Direncanakan [PLANNED] dan Telah Berhasil [IMPLEMENTED] di Kode**:
|
||||||
|
- Aturan A (Sticky State & Event Status Eksplisit): Telah diimplementasikan di `gmn_freshservice_ticket_v01.py::action_sync_from_freshservice`.
|
||||||
|
- Aturan B (Ball-in-Court Analysis & Deteksi Ambiguitas): Telah diimplementasikan di `gmn_freshservice_ticket_v01.py::_ball_in_court_check`.
|
||||||
|
- Aturan C (User Setelah Auto-Resolve Sistem): Telah diimplementasikan di `gmn_freshservice_ticket_v01.py::action_sync_from_freshservice`.
|
||||||
|
- Aturan D (Deteksi Rapat Bersama & Pembatalan Zoom): Telah diimplementasikan di `gmn_freshservice_ticket_v01.py::action_sync_from_freshservice`.
|
||||||
|
- Aturan E (First Ack vs First Meaningful Response): Telah diimplementasikan di `gmn_freshservice_ticket_v01.py::action_sync_from_freshservice`.
|
||||||
|
- Aturan F (Override Manual & Cut-off as_of): Telah diimplementasikan di `gmn_freshservice_ticket_stage_log_v01.py` dan wizard `gmn_freshservice_ticket_bottleneck_wizard_v01.py`.
|
||||||
|
4. **Daftar Baris GAP pada Traceability Matrix**:
|
||||||
|
- GAP-01: Belum ada test otomatis khusus UI/JS web client saat tombol 'Tarik Tiket Terbaru' ditekan (ditutup via pengujian fungsional UAT manual).
|
||||||
|
5. **Konfirmasi Privasi**:
|
||||||
|
- Seluruh data contoh, transkrip pesan, dan cuplikan pengujian telah **dianonimkan 100%**. Tidak ada alamat email riil, nomor telepon, link rapat Zoom aktif, maupun passcode rapat yang disertakan dalam dokumentasi ini.
|
||||||
@@ -20,10 +20,13 @@ Modul untuk melacak audit trail tiket dan memisahkan durasi penanganan:
|
|||||||
'mail',
|
'mail',
|
||||||
],
|
],
|
||||||
'data': [
|
'data': [
|
||||||
|
'security/gmn_freshservice_security_v01.xml',
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'data/gmn_demo_data_v01.xml',
|
'data/gmn_demo_data_v01.xml',
|
||||||
'views/gmn_res_config_settings_views_v01.xml',
|
'views/gmn_res_config_settings_views_v01.xml',
|
||||||
'views/gmn_freshservice_ticket_stage_log_views_v01.xml',
|
'views/gmn_freshservice_ticket_stage_log_views_v01.xml',
|
||||||
|
'views/gmn_freshservice_holiday_views_v01.xml',
|
||||||
|
'wizard/gmn_freshservice_holiday_import_wizard_views_v01.xml',
|
||||||
'wizard/gmn_freshservice_ticket_bottleneck_wizard_views_v01.xml',
|
'wizard/gmn_freshservice_ticket_bottleneck_wizard_views_v01.xml',
|
||||||
'views/gmn_freshservice_ticket_views_v01.xml',
|
'views/gmn_freshservice_ticket_views_v01.xml',
|
||||||
'views/gmn_menu_views_v01.xml',
|
'views/gmn_menu_views_v01.xml',
|
||||||
|
|||||||
Binary file not shown.
@@ -1,360 +0,0 @@
|
|||||||
# TECHNICAL DESIGN DOCUMENT (TDD)
|
|
||||||
## Integrasi Freshservice API v2 & Audit Trail SLA pada Odoo 19
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Informasi Dokumen
|
|
||||||
* **Nama Modul**: `freshservice_ticket_audit`
|
|
||||||
* **Target Sistem**: Odoo 19.0 Community / Enterprise
|
|
||||||
* **Sistem Eksternal**: Freshservice API v2 (`https://servicedesk.lyman.co.id`)
|
|
||||||
* **Versi Dokumen**: 1.0.0
|
|
||||||
* **Status**: Approved & Implemented
|
|
||||||
* **Tanggal**: 13 Oktober 2026
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Pendahuluan & Latar Belakang
|
|
||||||
|
|
||||||
### 1.1 Tujuan Sistem
|
|
||||||
Modul `freshservice_ticket_audit` dirancang untuk mengintegrasikan data tiket dari Freshservice ke dalam Odoo 19 secara real-time dan efisien. Fokus utama dari modul ini adalah:
|
|
||||||
1. **Audit Trail Real-Time**: Merekam setiap perubahan status tiket, perpindahan penanganan antara tim IT (*Waiting on Us*) dan pengguna (*Waiting on User*), hingga tiket selesai.
|
|
||||||
2. **Perhitungan Metrik SLA Berbasis Hari Kalender**: Menghitung durasi penanganan secara adil berdasarkan pergantian tanggal kalender (bukan jam kerja murni), sehingga mempermudah rekonsiliasi laporan SLA bulanan.
|
|
||||||
3. **Pemisahan Aktor & Hak Akses**: Data tiket murni merefleksikan aktor Freshservice (Pemohon, Agen IT, Departemen) tanpa ketergantungan pada akun internal user Odoo (`res.users`).
|
|
||||||
4. **Performa Tinggi & Zero-Latency**: Menerapkan strategi **Two-Tier Synchronization** agar pembukaan daftar tiket tidak mengalami *freeze* atau *timeout*.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Arsitektur Integrasi & Strategi Sinkronisasi
|
|
||||||
|
|
||||||
### 2.1 Arsitektur Tingkat Tinggi (High-Level Architecture)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
subgraph Freshservice [Freshservice API v2 Cloud]
|
|
||||||
FS_TICKETS["GET /api/v2/tickets?include=requester,stats"]
|
|
||||||
FS_DETAIL["GET /api/v2/tickets/{id}?include=requester,stats"]
|
|
||||||
FS_ACTIVITIES["GET /api/v2/tickets/{id}/activities"]
|
|
||||||
FS_CONVERSATIONS["GET /api/v2/tickets/{id}/conversations"]
|
|
||||||
FS_AGENTS["GET /api/v2/agents/{id}"]
|
|
||||||
FS_DEPTS["GET /api/v2/departments/{id}"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Adapter [Integration Layer: FreshserviceClient]
|
|
||||||
HTTP_CLIENT[HTTP Client with Basic Auth]
|
|
||||||
CACHE_AGENT[In-Memory Cache: Agents]
|
|
||||||
CACHE_DEPT[In-Memory Cache: Departments]
|
|
||||||
DATA_PARSER[JSON Parser & Normalizer]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Odoo_Core [Odoo 19 ORM & Business Logic]
|
|
||||||
LOCK_ENGINE[Row Lock: FOR UPDATE NOWAIT]
|
|
||||||
SLA_ENGINE[Calendar Day SLA Engine]
|
|
||||||
M_TICKET[(freshservice.ticket)]
|
|
||||||
M_STAGE[(freshservice.ticket.stage.log)]
|
|
||||||
M_CONV[(freshservice.ticket.conversation)]
|
|
||||||
end
|
|
||||||
|
|
||||||
FS_TICKETS -->|Batch Fetch Max 200| HTTP_CLIENT
|
|
||||||
FS_DETAIL -->|Single Fetch on Demand| HTTP_CLIENT
|
|
||||||
FS_ACTIVITIES --> HTTP_CLIENT
|
|
||||||
FS_CONVERSATIONS --> HTTP_CLIENT
|
|
||||||
FS_AGENTS <--> CACHE_AGENT
|
|
||||||
FS_DEPTS <--> CACHE_DEPT
|
|
||||||
CACHE_AGENT <--> HTTP_CLIENT
|
|
||||||
CACHE_DEPT <--> HTTP_CLIENT
|
|
||||||
|
|
||||||
HTTP_CLIENT --> DATA_PARSER
|
|
||||||
DATA_PARSER --> LOCK_ENGINE
|
|
||||||
LOCK_ENGINE --> M_TICKET
|
|
||||||
DATA_PARSER --> SLA_ENGINE
|
|
||||||
SLA_ENGINE --> M_STAGE
|
|
||||||
DATA_PARSER --> M_CONV
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2.2 Pola Sinkronisasi: Two-Tier Synchronization
|
|
||||||
|
|
||||||
Untuk menjamin skalabilitas dan menghindari *timeout*, sinkronisasi dibagi menjadi 2 tahap:
|
|
||||||
|
|
||||||
1. **Tier 1: Lightweight Batch Sync (Daftar Tiket Terbaru)**
|
|
||||||
* Dijalankan otomatis saat user membuka menu **Tiket Audit** (`web_search_read`).
|
|
||||||
* Hanya mengambil ringkasan **200 tiket terbaru** (menggunakan paginasi 100 tiket per request).
|
|
||||||
* Parameter query: `include=requester,stats` untuk mengambil ID, Subjek, Status, Prioritas, Nama Pemohon, Email, dan Waktu Dibuat.
|
|
||||||
* **Tidak memanggil activities dan conversations** pada tier ini untuk menghemat bandwidth dan waktu eksekusi.
|
|
||||||
|
|
||||||
2. **Tier 2: On-Demand Deep Sync (Detail Lengkap Tiket)**
|
|
||||||
* Dijalankan secara otomatis (*lazy loading*) saat form tiket tertentu dibuka (`web_read`) atau saat tombol **"Sinkronkan Ulang dari Freshservice"** diklik.
|
|
||||||
* Mengambil paket lengkap:
|
|
||||||
* Status & detail terkini tiket.
|
|
||||||
* Log seluruh riwayat aktivitas (`/activities`).
|
|
||||||
* Log seluruh percakapan dan balasan email (`/conversations`).
|
|
||||||
* Resolusi nama Agen IT dan Departemen via cache.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2.3 Diagram Alur Interaksi (Sequence Diagrams)
|
|
||||||
|
|
||||||
#### A. Alur Tier 1: Membuka List Tiket (Auto-Sync Ringan)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
autonumber
|
|
||||||
actor User as Pengguna Odoo
|
|
||||||
participant Web as Odoo Web Client (JS)
|
|
||||||
participant Model as freshservice.ticket (ORM)
|
|
||||||
participant Client as FreshserviceClient
|
|
||||||
participant FS as Freshservice API v2
|
|
||||||
|
|
||||||
User->>Web: Buka Menu "Tiket Freshservice"
|
|
||||||
Web->>Model: web_search_read(domain, fields, offset, limit=200)
|
|
||||||
Model->>Model: _auto_sync_recent_tickets(limit=200)
|
|
||||||
Note over Model: Cek throttle interval (misal 60 detik)
|
|
||||||
Model->>Client: fetch_recent_tickets(limit=200)
|
|
||||||
Client->>FS: GET /api/v2/tickets?order_by=created_at&order_type=desc&per_page=100&page=1&include=requester,stats
|
|
||||||
FS-->>Client: 200 OK (Array 100 Tiket + Requester + Stats)
|
|
||||||
Client->>FS: GET /api/v2/tickets?order_by=created_at&order_type=desc&per_page=100&page=2&include=requester,stats
|
|
||||||
FS-->>Client: 200 OK (Array 100 Tiket + Requester + Stats)
|
|
||||||
Client-->>Model: Return List 200 Parsed Tickets
|
|
||||||
Model->>Model: Batch Insert/Update ke freshservice_ticket
|
|
||||||
Model-->>Web: Return Data 200 Tiket
|
|
||||||
Web-->>User: Render Tree View Tiket
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B. Alur Tier 2: Membuka Form Tiket (On-Demand Deep Sync)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
autonumber
|
|
||||||
actor User as Pengguna Odoo
|
|
||||||
participant Web as Odoo Web Client (JS)
|
|
||||||
participant Model as freshservice.ticket (ORM)
|
|
||||||
participant Client as FreshserviceClient
|
|
||||||
participant FS as Freshservice API v2
|
|
||||||
|
|
||||||
User->>Web: Klik Baris Tiket (Buka Form View)
|
|
||||||
Web->>Model: web_read(id, specification)
|
|
||||||
alt is_detail_synced == False
|
|
||||||
Model->>Model: Execute Row Lock (SELECT ... FOR UPDATE NOWAIT)
|
|
||||||
Model->>Client: fetch_ticket_full_data(external_ticket_id)
|
|
||||||
par Ambil Detail Tiket
|
|
||||||
Client->>FS: GET /api/v2/tickets/{id}?include=requester,stats
|
|
||||||
FS-->>Client: Ticket JSON
|
|
||||||
and Ambil Activities
|
|
||||||
Client->>FS: GET /api/v2/tickets/{id}/activities
|
|
||||||
FS-->>Client: Activities Array
|
|
||||||
and Ambil Percakapan
|
|
||||||
Client->>FS: GET /api/v2/tickets/{id}/conversations
|
|
||||||
FS-->>Client: Conversations Array
|
|
||||||
end
|
|
||||||
Client->>Client: Resolve Agent & Dept Names (Cache Checked)
|
|
||||||
Client-->>Model: Full Payload Package
|
|
||||||
Model->>Model: Update Header Tiket (is_detail_synced = True)
|
|
||||||
Model->>Model: Rekonstruksi freshservice.ticket.stage.log
|
|
||||||
Model->>Model: Hitung Durasi SLA Hari Kalender
|
|
||||||
Model->>Model: Simpan freshservice.ticket.conversation
|
|
||||||
end
|
|
||||||
Model-->>Web: Return Record Values (Header, Logs, Conversations)
|
|
||||||
Web-->>User: Tampilkan Form View Lengkap dengan Tab SLA & Audit Trail
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Spesifikasi Endpoint Freshservice API v2 & Konfigurasi
|
|
||||||
|
|
||||||
### 3.1 Parameter Konfigurasi Sistem (UI Settings)
|
|
||||||
|
|
||||||
| Parameter Key | Field di Settings | Default | Deskripsi |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `freshservice_ticket_audit.freshservice_url` | Freshservice URL | `https://ptglobalmultimedianetwork.freshservice.com` | Base URL portal Freshservice perusahaan. |
|
|
||||||
| `freshservice_ticket_audit.freshservice_api_key` | Freshservice API Key | - | API Key pengguna dengan hak baca tiket. |
|
|
||||||
| `freshservice_ticket_audit.freshservice_sync_limit` | Limit Sinkronisasi Tiket | `200` | **Batas jumlah tiket terbaru yang ditarik saat membuka halaman daftar tiket (dapat diubah dinamis via UI Konfigurasi).** |
|
|
||||||
| `freshservice_ticket_audit.freshservice_use_mock` | Gunakan Mock Data Engine | `False` | Switch antara mode simulasi dan Live API. |
|
|
||||||
|
|
||||||
### 3.2 Spesifikasi Endpoint Freshservice API v2
|
|
||||||
|
|
||||||
| No | Endpoint | HTTP Method | Header & Autentikasi | Parameter Wajib | Kegunaan |
|
|
||||||
|---|---|:---:|---|---|---|
|
|
||||||
| 1 | `/api/v2/tickets` | `GET` | `Authorization: Basic {API_KEY:X}`<br>`Accept: application/json` | `order_by=created_at`<br>`order_type=desc`<br>`per_page=100`<br>`page={1..N}`<br>`include=requester,stats` | Penarikan tiket terbaru sesuai limit konfigurasi (`freshservice_sync_limit`). |
|
|
||||||
| 2 | `/api/v2/tickets/{id}` | `GET` | Basic Auth | `include=requester,stats` | Pengambilan detail spesifik satu tiket. |
|
|
||||||
| 3 | `/api/v2/tickets/{id}/activities` | `GET` | Basic Auth | - | Rekonstruksi log audit trail dan tahapan status. |
|
|
||||||
| 4 | `/api/v2/tickets/{id}/conversations` | `GET` | Basic Auth | - | Pengambilan log chat/balasan email pada tiket. |
|
|
||||||
| 5 | `/api/v2/agents/{id}` | `GET` | Basic Auth | - | Resolusi ID agen menjadi nama lengkap (`first_name` + `last_name`). |
|
|
||||||
| 6 | `/api/v2/departments/{id}` | `GET` | Basic Auth | - | Resolusi ID departemen menjadi nama departemen. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Pemetaan Data (Data Field Mapping Dictionary)
|
|
||||||
|
|
||||||
### 4.1 Entitas Utama: `freshservice.ticket`
|
|
||||||
|
|
||||||
| Nama Field Odoo | Tipe Odoo | Field Asal Freshservice v2 | Tipe Asal | Logika Transformasi & Keterangan |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| `external_ticket_id` | `Char` | `ticket.id` | `Integer` | Konversi integer ke string, diindeks untuk pencarian cepat. |
|
|
||||||
| `name` | `Char` | `ticket.subject` | `String` | Subjek/judul tiket. |
|
|
||||||
| `description` | `Text` | `ticket.description_text` | `String` | Teks isi deskripsi tanpa format HTML. |
|
|
||||||
| `status` | `Selection` | `ticket.status` | `Integer` | Dipetakan via `STATUS_MAP` (lihat tabel 4.4). |
|
|
||||||
| `priority` | `Selection` | `ticket.priority` | `Integer` | Dipetakan via `PRIORITY_MAP` (lihat tabel 4.5). |
|
|
||||||
| `requester_name` | `Char` | `ticket.requester.name` | `String` | Diambil dari objek embedded `requester`. |
|
|
||||||
| `requester_email` | `Char` | `ticket.requester.email` | `String` | Email pemohon dari objek embedded `requester`. |
|
|
||||||
| `responder_name` | `Char` | `ticket.responder_id` | `Integer` | Dicari via `/api/v2/agents/{responder_id}` $\rightarrow$ nama lengkap agen IT. Di-cache. |
|
|
||||||
| `department_name` | `Char` | `ticket.department_id` | `Integer` | Dicari via `/api/v2/departments/{department_id}` $\rightarrow$ nama departemen. Di-cache. |
|
|
||||||
| `ticket_created_at` | `Datetime` | `ticket.created_at` | `ISO 8601` | Diparsing dari UTC ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`) ke Odoo Datetime. |
|
|
||||||
| `due_by` | `Datetime` | `ticket.due_by` | `ISO 8601` | Batas waktu SLA penyelesaian tiket. |
|
|
||||||
| `first_responded_at` | `Datetime` | `ticket.stats.first_responded_at` | `ISO 8601` | Diambil dari objek embedded `stats`. |
|
|
||||||
| `resolved_at` | `Datetime` | `ticket.stats.resolved_at` | `ISO 8601` | Waktu selesai dari objek embedded `stats`. |
|
|
||||||
| `first_responder_name` | `Char` | Log Activities / Responder | `String` | Nama agen yang pertama kali merespons tiket. |
|
|
||||||
| `is_mock_data` | `Boolean` | Parameter Sistem | `Boolean` | `True` jika ditarik dari Mock Engine, `False` jika Live API. |
|
|
||||||
| `is_detail_synced` | `Boolean` | Internal System Flag | `Boolean` | Penanda apakah log aktivitas & percakapan sudah ditarik. |
|
|
||||||
| `last_detail_sync` | `Datetime` | Internal System Flag | `Datetime` | Timestamp terakhir kali detail disinkronkan. |
|
|
||||||
| `current_responsibility` | `Selection` | Dihitung otomatis | - | `'customer'` jika pending; `'closed'` jika resolved/closed; `'internal'` selainnya. |
|
|
||||||
| `first_response_time_days` | `Integer` | Dihitung otomatis | - | Selisih hari kalender dari `ticket_created_at` ke `first_responded_at`. |
|
|
||||||
| `first_response_time_display` | `Char` | Dihitung otomatis | - | Format representasi: `"X Hari"` atau `"Belum Ada Respon"`. |
|
|
||||||
| `total_agent_time_days` | `Integer` | Dihitung otomatis | - | Akumulasi hari kalender tanggung jawab `internal` (Waiting on Us). |
|
|
||||||
| `total_agent_time_display` | `Char` | Dihitung otomatis | - | Format representasi: `"X Hari"`. |
|
|
||||||
| `total_customer_time_days` | `Integer` | Dihitung otomatis | - | Akumulasi hari kalender tanggung jawab `customer` (Waiting on User). |
|
|
||||||
| `total_customer_time_display` | `Char` | Dihitung otomatis | - | Format representasi: `"X Hari"`. |
|
|
||||||
| `resolution_time_days` | `Integer` | Dihitung otomatis | - | Selisih hari kalender dari `ticket_created_at` ke `resolved_at`. |
|
|
||||||
| `resolution_time_display` | `Char` | Dihitung otomatis | - | Format representasi: `"X Hari"` atau `"Masih Berjalan"`. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4.2 Entitas Audit Trail Status: `freshservice.ticket.stage.log`
|
|
||||||
|
|
||||||
Model ini dihasilkan dari rekonsiliasi data riwayat `/api/v2/tickets/{id}/activities`.
|
|
||||||
|
|
||||||
| Nama Field Odoo | Tipe Odoo | Field Asal Freshservice v2 | Tipe Asal | Logika Transformasi & Keterangan |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| `ticket_id` | `Many2one` | `ticket.id` | `Integer` | Relasi Many2one ke `freshservice.ticket` (ondelete='cascade'). |
|
|
||||||
| `stage_name` | `Char` | `activity.content` / Status | `String` | Nama tahapan status (misal: *Open*, *Pending Customer*, *Resolved*). |
|
|
||||||
| `responsibility` | `Selection` | Evaluasi Status & Aktivitas | `String` | `'customer'` jika menunggu user/pending; `'internal'` jika di pihak agen/IT. |
|
|
||||||
| `actor_name` | `Char` | `activity.actor.name` | `String` | Nama pihak yang melakukan aksi/perubahan status. |
|
|
||||||
| `actor_type` | `Selection` | `activity.actor.is_agent` / ID | - | `'Agent'` jika `is_agent=True`; `'System'` jika actor=0/workflow; `'Requester'` selainnya. |
|
|
||||||
| `activity_content` | `Text` | `activity.content` | `String` | Deskripsi detail log atau perubahan field pada Freshservice. |
|
|
||||||
| `start_date` | `Datetime` | `activity.created_at` | `ISO 8601` | Waktu dimulainya tahapan status ini. |
|
|
||||||
| `end_date` | `Datetime` | Aktivitas berikutnya (`created_at`) | `ISO 8601` | Waktu berakhirnya tahapan status ini (False jika status aktif). |
|
|
||||||
| `is_active` | `Boolean` | Evaluasi `end_date` | - | `True` jika merupakan tahapan terakhir yang sedang berlangsung. |
|
|
||||||
| `duration_days` | `Integer` | Dihitung otomatis | - | Durasi hari kalender dari `start_date` sampai `end_date` (atau saat ini). |
|
|
||||||
| `duration_hours` | `Float` | Dihitung otomatis | - | Durasi jam teknis (disimpan di database, disembunyikan di UI). |
|
|
||||||
| `duration_display` | `Char` | Dihitung otomatis | - | Contoh format tampilan: `"1 Hari"` atau `"2 Hari (Aktif)"`. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4.3 Entitas Percakapan: `freshservice.ticket.conversation`
|
|
||||||
|
|
||||||
Model ini dihasilkan dari data `/api/v2/tickets/{id}/conversations`.
|
|
||||||
|
|
||||||
| Nama Field Odoo | Tipe Odoo | Field Asal Freshservice v2 | Tipe Asal | Logika Transformasi & Keterangan |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| `ticket_id` | `Many2one` | `ticket.id` | `Integer` | Relasi Many2one ke `freshservice.ticket`. |
|
|
||||||
| `external_id` | `Char` | `conversation.id` | `Integer` | ID unik percakapan pada Freshservice. |
|
|
||||||
| `user_name` | `Char` | `conversation.user_name` / User | `String` | Nama pengirim pesan/catatan. |
|
|
||||||
| `user_id_external` | `Char` | `conversation.user_id` | `Integer` | ID pengguna pengirim pesan pada Freshservice. |
|
|
||||||
| `incoming` | `Boolean` | `conversation.incoming` | `Boolean` | `True` jika pesan dari Pemohon (Customer); `False` jika balasan dari Agen IT. |
|
|
||||||
| `sender_type` | `Selection` | Evaluasi `incoming` | - | `'customer'` jika `incoming=True`; `'agent'` jika `incoming=False`. |
|
|
||||||
| `body_text` | `Text` | `conversation.body_text` | `String` | Isi teks pesan (plain text / stripped HTML). |
|
|
||||||
| `created_at` | `Datetime` | `conversation.created_at` | `ISO 8601` | Waktu pesan dikirimkan. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4.4 Kamus Konversi Status (`STATUS_MAP`)
|
|
||||||
|
|
||||||
| Kode Freshservice | Deskripsi Freshservice | Value Odoo (`status`) | Label Odoo | Tanggung Jawab Default |
|
|
||||||
|:---:|---|---|---|:---:|
|
|
||||||
| `2` | Open | `'open'` | Open | Internal (*Waiting on Us*) |
|
|
||||||
| `3` | Pending | `'pending'` | Pending User | Customer (*Waiting on User*) |
|
|
||||||
| `4` | Resolved | `'resolved'` | Resolved | Selesai (*Closed*) |
|
|
||||||
| `5` | Closed | `'closed'` | Closed | Selesai (*Closed*) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4.5 Kamus Konversi Prioritas (`PRIORITY_MAP`)
|
|
||||||
|
|
||||||
| Kode Freshservice | Deskripsi Freshservice | Value Odoo (`priority`) | Label Odoo |
|
|
||||||
|:---:|---|---|---|
|
|
||||||
| `1` | Low | `'low'` | Low |
|
|
||||||
| `2` | Medium | `'medium'` | Medium |
|
|
||||||
| `3` | High | `'high'` | High |
|
|
||||||
| `4` | Urgent | `'urgent'` | Urgent |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Logika Perhitungan SLA Berbasis Hari Kalender
|
|
||||||
|
|
||||||
### 5.1 Definisi & Formulasi
|
|
||||||
Perhitungan SLA tidak menggunakan selisih jam murni dibagi 24, melainkan menggunakan **selisih tanggal kalender** (*Calendar Days*) berdasarkan zona waktu pengguna (default `Asia/Jakarta`, UTC+7):
|
|
||||||
|
|
||||||
$$\text{Tanggal Mulai} = \text{Date}(\text{ConvertTZ}(\text{StartDatetime}, \text{UserTZ}))$$
|
|
||||||
|
|
||||||
$$\text{Tanggal Selesai} = \text{Date}(\text{ConvertTZ}(\text{EndDatetime}, \text{UserTZ}))$$
|
|
||||||
|
|
||||||
$$\text{Durasi Hari Kalender} = \max(0, \text{Tanggal Selesai} - \text{Tanggal Mulai})$$
|
|
||||||
|
|
||||||
### 5.2 Contoh Kasus Perhitungan
|
|
||||||
* **Kasus 1 (Hari yang Sama)**:
|
|
||||||
* Tiket dibuat: `13 Okt 2026 09:00 WIB`
|
|
||||||
* First Response: `13 Okt 2026 15:30 WIB`
|
|
||||||
* Perhitungan: $13\text{ Okt} - 13\text{ Okt} = \mathbf{0\text{ Hari}}$.
|
|
||||||
* **Kasus 2 (Ganti Hari / Besoknya)**:
|
|
||||||
* Tiket dibuat: `13 Okt 2026 16:00 WIB`
|
|
||||||
* First Response: `14 Okt 2026 08:30 WIB`
|
|
||||||
* Perhitungan: $14\text{ Okt} - 13\text{ Okt} = \mathbf{1\text{ Hari}}$.
|
|
||||||
* **Kasus 3 (Pending Berhari-hari)**:
|
|
||||||
* Status diubah ke *Waiting on User*: `14 Okt 2026`
|
|
||||||
* User membalas kembali: `17 Okt 2026`
|
|
||||||
* Perhitungan: $17\text{ Okt} - 14\text{ Okt} = \mathbf{3\text{ Hari}}$.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Penanganan Masalah & Optimasi Kinerja
|
|
||||||
|
|
||||||
1. **Peniadaan Ketergantungan User Odoo**:
|
|
||||||
* Field `user_id` ("Petugas Odoo") dan `user_ids` telah dihapus dari antarmuka pengguna (Form View, List View, Search View).
|
|
||||||
* Nilai `default=lambda self: self.env.user` telah dinonaktifkan sehingga tidak ada penugasan otomatis ke user sistem (seperti `OdooBot`).
|
|
||||||
2. **In-Memory Caching untuk Master Data**:
|
|
||||||
* API Freshservice tidak menyertakan nama agen/departemen secara langsung pada objek tiket.
|
|
||||||
* `FreshserviceClient` mengimplementasikan dictionary cache `_agent_cache` dan `_dept_cache` yang memangkas ratusan panggilan HTTP redundan saat memproses 200 tiket sekaligus.
|
|
||||||
3. **Database Concurrency Lock**:
|
|
||||||
* Fungsi `action_sync_from_freshservice` menerapkan query SQL:
|
|
||||||
```sql
|
|
||||||
SELECT id FROM freshservice_ticket WHERE id = %s FOR UPDATE NOWAIT;
|
|
||||||
```
|
|
||||||
Hal ini mencegah konflik penulisan/duplikasi data jika dua pengguna membuka tiket yang sama secara bersamaan.
|
|
||||||
4. **Paginasi Cepat Freshservice API**:
|
|
||||||
* Freshservice API membatasi maksimal 100 tiket per panggilan HTTP (`per_page=100`).
|
|
||||||
* Sinkronisasi awal melakukan *looping* otomatis (`page=1` dan `page=2`) untuk memperoleh tepat 200 tiket terbaru tanpa membebani server.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Status Pengujian & Verifikasi
|
|
||||||
|
|
||||||
Modul telah diuji langsung pada lingkungan Odoo 19 dengan database PostgreSQL `odoo19`:
|
|
||||||
* **Pengujian Penarikan 200 Tiket**: Berhasil mengekstrak tiket terbaru secara cepat.
|
|
||||||
* **Pengujian Integritas Data**: Field Pemohon, Email, Agen IT, dan Departemen terverifikasi terisi lengkap.
|
|
||||||
* **Pengujian Tampilan UI**: Field durasi jam berhasil disembunyikan dan seluruh metrik SLA tampil dalam satuan Hari Kalender.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Fitur Wizard Laporan Analisis Bottleneck (Excel / .xlsx)
|
|
||||||
|
|
||||||
### 7.1 Deskripsi & Tujuan
|
|
||||||
Fitur ini menyediakan pop-up wizard (`freshservice.ticket.bottleneck.wizard`) yang mengekspor hasil analisis bottleneck tiket ke dalam file spreadsheet Excel (`.xlsx`) berstandar presentasi manajemen/meeting.
|
|
||||||
|
|
||||||
Laporan ini membedah faktor keterlambatan tiket:
|
|
||||||
1. **Pemisahan Tanggung Jawab**: Apakah tiket tertahan lama di sisi Internal IT (*Waiting on Us*) atau di sisi User (*Waiting on Customer*).
|
|
||||||
2. **Ranking Bottleneck Tahapan**: Mengurutkan tahapan/status tiket dari yang durasinya paling lama (*slowest stage*) beserta penanggung jawab dan kontribusi persentasenya.
|
|
||||||
3. **Auto-Fetch On-Demand**: Bila tiket yang dipilih (misal tiket `#992`) belum tersinkron rincian stage log dan percakapannya, wizard otomatis melakukan fetch langsung ke API Freshservice sebelum mengompilasi workbook Excel.
|
|
||||||
|
|
||||||
### 7.2 Struktur Sheet Workbook Excel
|
|
||||||
1. **Sheet 1: `Ringkasan & Bottleneck KPI`**:
|
|
||||||
- Metadata Tiket (ID, Subjek, Pemohon, Departemen, Agen IT, Status, Tanggal Dibuat, Tanggal Selesai).
|
|
||||||
- Kartu Metrik KPI (Total Hari, Hari di IT, Hari di User, First Response).
|
|
||||||
- Tabel Ranking Bottleneck per Tahapan (diurutkan dari stage terlama ke tercepat).
|
|
||||||
- Banner Kesimpulan Bottleneck Utama untuk bahan evaluasi meeting.
|
|
||||||
2. **Sheet 2: `Audit Trail & Kronologi`**:
|
|
||||||
- Rincian kronologis transisi status, waktu mulai, waktu selesai, durasi hari & jam, penanggung jawab, dan deskripsi aktivitas.
|
|
||||||
3. **Sheet 3: `Percakapan & Respon`**:
|
|
||||||
- Riwayat catatan publik (*Public Reply*) dan internal (*Private Note*), pengirim, dan cuplikan pesan.
|
|
||||||
4. **Mode Multi-Tiket / Ranking**:
|
|
||||||
- Menghasilkan tabel ranking tiket terlama dengan ringkasan *primary bottleneck stage* masing-masing tiket.
|
|
||||||
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
Urutan import penting karena ticket bergantung pada stage_log dan conversation.
|
Urutan import penting karena ticket bergantung pada stage_log dan conversation.
|
||||||
"""
|
"""
|
||||||
from . import gmn_res_config_settings_v01
|
from . import gmn_res_config_settings_v01
|
||||||
|
from . import gmn_freshservice_holiday_v01
|
||||||
from . import gmn_freshservice_ticket_stage_log_v01
|
from . import gmn_freshservice_ticket_stage_log_v01
|
||||||
from . import gmn_freshservice_ticket_conversation_v01
|
from . import gmn_freshservice_ticket_conversation_v01
|
||||||
from . import gmn_freshservice_ticket_v01
|
from . import gmn_freshservice_ticket_v01
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
276
gmn_freshservice/models/gmn_freshservice_holiday_v01.py
Normal file
276
gmn_freshservice/models/gmn_freshservice_holiday_v01.py
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Model Master Data Hari Libur Nasional & Cuti Bersama.
|
||||||
|
Menyimpan daftar tanggal merah resmi Indonesia dan cuti bersama perusahaan
|
||||||
|
yang digunakan oleh Working Hours Engine untuk memotong jam kerja secara presisi.
|
||||||
|
|
||||||
|
Mendukung:
|
||||||
|
1. Input manual melalui antarmuka Tree/Form Odoo.
|
||||||
|
2. Impor massal dari file spreadsheet Excel (.xlsx).
|
||||||
|
3. Pengunduhan template spreadsheet Excel (.xlsx) dengan contoh data.
|
||||||
|
4. Pemuatan instan data baku hari libur nasional Indonesia (2024-2026).
|
||||||
|
"""
|
||||||
|
import io
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, date
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
import xlsxwriter
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Daftar Baku Hari Libur Nasional & Cuti Bersama Indonesia (2024-2026)
|
||||||
|
DEFAULT_INDONESIA_HOLIDAYS = [
|
||||||
|
# 2024
|
||||||
|
('2024-01-01', 'Tahun Baru 2024 Masehi', 'public', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-02-08', 'Isra Mikraj Nabi Muhammad SAW', 'public', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-02-09', 'Cuti Bersama Tahun Baru Imlek 2575 Kongzili', 'joint_leave', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-02-10', 'Tahun Baru Imlek 2575 Kongzili', 'public', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-03-11', 'Hari Suci Nyepi Tahun Baru Saka 1946', 'public', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-03-12', 'Cuti Bersama Hari Suci Nyepi', 'joint_leave', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-03-29', 'Wafat Isa Al Masih', 'public', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-03-31', 'Hari Paskah', 'public', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-04-08', 'Cuti Bersama Hari Raya Idul Fitri 1445 H', 'joint_leave', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-04-09', 'Cuti Bersama Hari Raya Idul Fitri 1445 H', 'joint_leave', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-04-10', 'Hari Raya Idul Fitri 1445 Hijriah', 'public', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-04-11', 'Hari Raya Idul Fitri 1445 Hijriah', 'public', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-04-12', 'Cuti Bersama Hari Raya Idul Fitri 1445 H', 'joint_leave', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-04-15', 'Cuti Bersama Hari Raya Idul Fitri 1445 H', 'joint_leave', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-05-01', 'Hari Buruh Internasional', 'public', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-05-09', 'Kenaikan Isa Al Masih', 'public', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-05-10', 'Cuti Bersama Kenaikan Isa Al Masih', 'joint_leave', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-05-23', 'Hari Raya Waisak 2568 BE', 'public', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-05-24', 'Cuti Bersama Hari Raya Waisak', 'joint_leave', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-06-01', 'Hari Lahir Pancasila', 'public', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-06-17', 'Hari Raya Idul Adha 1445 Hijriah', 'public', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-06-18', 'Cuti Bersama Hari Raya Idul Adha', 'joint_leave', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-07-07', 'Tahun Baru Islam 1446 Hijriah', 'public', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-08-17', 'Hari Proklamasi Kemerdekaan RI ke-79', 'public', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-09-16', 'Maulid Nabi Muhammad SAW', 'public', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-12-25', 'Hari Raya Natal', 'public', 'SKB 3 Menteri 2024'),
|
||||||
|
('2024-12-26', 'Cuti Bersama Hari Raya Natal', 'joint_leave', 'SKB 3 Menteri 2024'),
|
||||||
|
# 2025
|
||||||
|
('2025-01-01', 'Tahun Baru 2025 Masehi', 'public', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-01-27', 'Isra Mikraj Nabi Muhammad SAW', 'public', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-01-28', 'Cuti Bersama Tahun Baru Imlek 2576 Kongzili', 'joint_leave', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-01-29', 'Tahun Baru Imlek 2576 Kongzili', 'public', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-03-28', 'Cuti Bersama Hari Suci Nyepi', 'joint_leave', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-03-29', 'Hari Suci Nyepi Tahun Baru Saka 1947', 'public', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-03-31', 'Hari Raya Idul Fitri 1446 Hijriah', 'public', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-04-01', 'Hari Raya Idul Fitri 1446 Hijriah', 'public', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-04-02', 'Cuti Bersama Hari Raya Idul Fitri 1446 H', 'joint_leave', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-04-03', 'Cuti Bersama Hari Raya Idul Fitri 1446 H', 'joint_leave', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-04-04', 'Cuti Bersama Hari Raya Idul Fitri 1446 H', 'joint_leave', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-04-07', 'Cuti Bersama Hari Raya Idul Fitri 1446 H', 'joint_leave', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-04-18', 'Wafat Isa Al Masih', 'public', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-04-20', 'Hari Paskah', 'public', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-05-01', 'Hari Buruh Internasional', 'public', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-05-12', 'Hari Raya Waisak 2569 BE', 'public', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-05-13', 'Cuti Bersama Hari Raya Waisak', 'joint_leave', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-05-29', 'Kenaikan Isa Al Masih', 'public', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-05-30', 'Cuti Bersama Kenaikan Isa Al Masih', 'joint_leave', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-06-01', 'Hari Lahir Pancasila', 'public', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-06-06', 'Hari Raya Idul Adha 1446 Hijriah', 'public', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-06-09', 'Cuti Bersama Hari Raya Idul Adha', 'joint_leave', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-06-27', '1 Muharam Tahun Baru Islam 1447 H', 'public', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-08-17', 'Hari Proklamasi Kemerdekaan RI ke-80', 'public', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-09-05', 'Maulid Nabi Muhammad SAW', 'public', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-12-25', 'Hari Raya Natal', 'public', 'SKB 3 Menteri 2025'),
|
||||||
|
('2025-12-26', 'Cuti Bersama Hari Raya Natal', 'joint_leave', 'SKB 3 Menteri 2025'),
|
||||||
|
# 2026
|
||||||
|
('2026-01-01', 'Tahun Baru 2026 Masehi', 'public', 'SKB 3 Menteri 2026'),
|
||||||
|
('2026-01-16', 'Isra Mikraj Nabi Muhammad SAW', 'public', 'SKB 3 Menteri 2026'),
|
||||||
|
('2026-02-17', 'Tahun Baru Imlek 2577 Kongzili', 'public', 'SKB 3 Menteri 2026'),
|
||||||
|
('2026-03-19', 'Hari Raya Idul Fitri 1447 Hijriah', 'public', 'SKB 3 Menteri 2026'),
|
||||||
|
('2026-03-20', 'Hari Raya Idul Fitri 1447 Hijriah', 'public', 'SKB 3 Menteri 2026'),
|
||||||
|
('2026-03-21', 'Hari Suci Nyepi Tahun Baru Saka 1948', 'public', 'SKB 3 Menteri 2026'),
|
||||||
|
('2026-04-03', 'Wafat Isa Al Masih', 'public', 'SKB 3 Menteri 2026'),
|
||||||
|
('2026-05-01', 'Hari Buruh Internasional', 'public', 'SKB 3 Menteri 2026'),
|
||||||
|
('2026-05-14', 'Kenaikan Isa Al Masih', 'public', 'SKB 3 Menteri 2026'),
|
||||||
|
('2026-05-31', 'Hari Raya Waisak 2570 BE', 'public', 'SKB 3 Menteri 2026'),
|
||||||
|
('2026-06-01', 'Hari Lahir Pancasila', 'public', 'SKB 3 Menteri 2026'),
|
||||||
|
('2026-06-27', 'Hari Raya Idul Adha 1447 Hijriah', 'public', 'SKB 3 Menteri 2026'),
|
||||||
|
('2026-07-16', 'Tahun Baru Islam 1448 Hijriah', 'public', 'SKB 3 Menteri 2026'),
|
||||||
|
('2026-08-17', 'Hari Kemerdekaan Republik Indonesia ke-81', 'public', 'SKB 3 Menteri 2026'),
|
||||||
|
('2026-08-25', 'Maulid Nabi Muhammad SAW', 'public', 'SKB 3 Menteri 2026'),
|
||||||
|
('2026-12-25', 'Hari Raya Natal', 'public', 'SKB 3 Menteri 2026'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class FreshserviceHoliday(models.Model):
|
||||||
|
"""Model Master Data Hari Libur Nasional & Cuti Bersama.
|
||||||
|
Menyimpan tanggal merah yang dijadikan acuan pemotongan jam kerja oleh modul.
|
||||||
|
"""
|
||||||
|
_name = 'freshservice.holiday'
|
||||||
|
_description = 'Master Data Hari Libur & Cuti Bersama Indonesia'
|
||||||
|
_order = 'date desc, id desc'
|
||||||
|
|
||||||
|
name = fields.Char(string='Nama Hari Libur / Keterangan', required=True, index=True)
|
||||||
|
date = fields.Date(string='Tanggal Libur', required=True, index=True)
|
||||||
|
holiday_type = fields.Selection([
|
||||||
|
('public', 'Hari Libur Nasional'),
|
||||||
|
('joint_leave', 'Cuti Bersama'),
|
||||||
|
('company', 'Libur Khusus Perusahaan'),
|
||||||
|
], string='Jenis Libur', default='public', required=True)
|
||||||
|
year = fields.Integer(string='Tahun', compute='_compute_year', store=True, index=True)
|
||||||
|
active = fields.Boolean(string='Aktif', default=True)
|
||||||
|
notes = fields.Text(string='Catatan Tambahan')
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
('date_unique', 'unique(date)', 'Tanggal hari libur ini sudah terdaftar dalam sistem!')
|
||||||
|
]
|
||||||
|
|
||||||
|
@api.depends('date')
|
||||||
|
def _compute_year(self):
|
||||||
|
"""Menghitung tahun secara otomatis dari tanggal libur."""
|
||||||
|
for rec in self:
|
||||||
|
rec.year = rec.date.year if rec.date else 0
|
||||||
|
|
||||||
|
def action_populate_default_holidays(self, *args, **kwargs):
|
||||||
|
"""Memuat daftar hari libur baku Indonesia (2024-2026) ke dalam database.
|
||||||
|
Hanya menambahkan data yang belum terdaftar tanpa menimpa data yang sudah ada.
|
||||||
|
"""
|
||||||
|
created_count = 0
|
||||||
|
for date_str, name, h_type, notes in DEFAULT_INDONESIA_HOLIDAYS:
|
||||||
|
existing = self.search([('date', '=', date_str)], limit=1)
|
||||||
|
if not existing:
|
||||||
|
self.create({
|
||||||
|
'date': date_str,
|
||||||
|
'name': name,
|
||||||
|
'holiday_type': h_type,
|
||||||
|
'notes': notes,
|
||||||
|
'active': True,
|
||||||
|
})
|
||||||
|
created_count += 1
|
||||||
|
|
||||||
|
message = _('Berhasil memuat %d hari libur nasional baku ke database.') % created_count if created_count > 0 else _('Seluruh hari libur nasional baku sudah terdaftar.')
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.client',
|
||||||
|
'tag': 'display_notification',
|
||||||
|
'params': {
|
||||||
|
'title': _('Pemuatan Data Baku Selesai'),
|
||||||
|
'message': message,
|
||||||
|
'type': 'success',
|
||||||
|
'sticky': False,
|
||||||
|
'next': {'type': 'ir.actions.client', 'tag': 'reload'},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_download_template(self, *args, **kwargs):
|
||||||
|
"""Membuat dan mengunduh file template Excel (.xlsx) untuk impor hari libur.
|
||||||
|
Template dilengkapi dengan format kolom yang benar dan contoh data resmi 2026.
|
||||||
|
"""
|
||||||
|
output = io.BytesIO()
|
||||||
|
workbook = xlsxwriter.Workbook(output, {'in_memory': True})
|
||||||
|
worksheet = workbook.add_worksheet('Template Hari Libur')
|
||||||
|
|
||||||
|
# Formatter Styling Excel
|
||||||
|
header_banner_fmt = workbook.add_format({
|
||||||
|
'bold': True, 'font_size': 14, 'font_name': 'Calibri',
|
||||||
|
'font_color': '#FFFFFF', 'bg_color': '#1B365D',
|
||||||
|
'align': 'left', 'valign': 'vcenter'
|
||||||
|
})
|
||||||
|
info_fmt = workbook.add_format({
|
||||||
|
'font_size': 9, 'font_name': 'Calibri', 'font_color': '#5D6D7E',
|
||||||
|
'italic': True, 'valign': 'vcenter'
|
||||||
|
})
|
||||||
|
th_fmt = workbook.add_format({
|
||||||
|
'bold': True, 'font_size': 10, 'font_name': 'Calibri',
|
||||||
|
'font_color': '#FFFFFF', 'bg_color': '#2E4053',
|
||||||
|
'align': 'center', 'valign': 'vcenter', 'border': 1, 'border_color': '#BDC3C7'
|
||||||
|
})
|
||||||
|
td_center_fmt = workbook.add_format({
|
||||||
|
'font_size': 9, 'font_name': 'Calibri', 'align': 'center',
|
||||||
|
'border': 1, 'border_color': '#D5D8DC', 'valign': 'vcenter'
|
||||||
|
})
|
||||||
|
td_date_fmt = workbook.add_format({
|
||||||
|
'font_size': 9, 'font_name': 'Calibri', 'align': 'center',
|
||||||
|
'border': 1, 'border_color': '#D5D8DC', 'valign': 'vcenter',
|
||||||
|
'num_format': 'yyyy-mm-dd'
|
||||||
|
})
|
||||||
|
td_left_fmt = workbook.add_format({
|
||||||
|
'font_size': 9, 'font_name': 'Calibri', 'align': 'left',
|
||||||
|
'border': 1, 'border_color': '#D5D8DC', 'valign': 'vcenter'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Set Lebar Kolom
|
||||||
|
worksheet.set_column('A:A', 6)
|
||||||
|
worksheet.set_column('B:B', 16)
|
||||||
|
worksheet.set_column('C:C', 40)
|
||||||
|
worksheet.set_column('D:D', 26)
|
||||||
|
worksheet.set_column('E:E', 35)
|
||||||
|
|
||||||
|
# Header Title Banner
|
||||||
|
worksheet.merge_range('A1:E1', 'TEMPLATE IMPORT HARI LIBUR NASIONAL & CUTI BERSAMA', header_banner_fmt)
|
||||||
|
worksheet.set_row(0, 30)
|
||||||
|
|
||||||
|
# Petunjuk Pengisian
|
||||||
|
worksheet.write('A2', 'PETUNJUK PENGISIAN:', info_fmt)
|
||||||
|
worksheet.write('A3', '1. Kolom Tanggal Libur (*) dan Nama Hari Libur (*) wajib diisi. Format tanggal: YYYY-MM-DD (contoh: 2026-08-17).', info_fmt)
|
||||||
|
worksheet.write('A4', '2. Kolom Jenis Libur dapat diisi: "Hari Libur Nasional", "Cuti Bersama", atau "Libur Khusus Perusahaan".', info_fmt)
|
||||||
|
worksheet.write('A5', '3. Anda dapat mengedit, menambah, atau menghapus baris contoh di bawah ini sebelum mengimpor.', info_fmt)
|
||||||
|
|
||||||
|
# Table Headers (Baris 7)
|
||||||
|
headers = ['No', 'Tanggal Libur (*)', 'Nama Hari Libur (*)', 'Jenis Libur', 'Keterangan']
|
||||||
|
for col_idx, h in enumerate(headers):
|
||||||
|
worksheet.write(6, col_idx, h, th_fmt)
|
||||||
|
worksheet.set_row(6, 24)
|
||||||
|
|
||||||
|
# Contoh Data (Hari Libur Resmi 2026 sebagai sampel lengkap)
|
||||||
|
sample_data = [
|
||||||
|
(1, '2026-01-01', 'Tahun Baru 2026 Masehi', 'Hari Libur Nasional', 'Libur Nasional Resmi'),
|
||||||
|
(2, '2026-01-16', 'Isra Mikraj Nabi Muhammad SAW', 'Hari Libur Nasional', 'SKB 3 Menteri'),
|
||||||
|
(3, '2026-02-17', 'Tahun Baru Imlek 2577 Kongzili', 'Hari Libur Nasional', 'SKB 3 Menteri'),
|
||||||
|
(4, '2026-03-19', 'Hari Raya Idul Fitri 1447 Hijriah', 'Hari Libur Nasional', 'SKB 3 Menteri'),
|
||||||
|
(5, '2026-03-20', 'Hari Raya Idul Fitri 1447 Hijriah', 'Hari Libur Nasional', 'SKB 3 Menteri'),
|
||||||
|
(6, '2026-03-21', 'Hari Suci Nyepi Tahun Baru Saka 1948', 'Hari Libur Nasional', 'SKB 3 Menteri'),
|
||||||
|
(7, '2026-04-03', 'Wafat Isa Al Masih', 'Hari Libur Nasional', 'SKB 3 Menteri'),
|
||||||
|
(8, '2026-05-01', 'Hari Buruh Internasional', 'Hari Libur Nasional', 'Hari Buruh'),
|
||||||
|
(9, '2026-05-14', 'Kenaikan Isa Al Masih', 'Hari Libur Nasional', 'SKB 3 Menteri'),
|
||||||
|
(10, '2026-05-31', 'Hari Raya Waisak 2570 BE', 'Hari Libur Nasional', 'SKB 3 Menteri'),
|
||||||
|
(11, '2026-06-01', 'Hari Lahir Pancasila', 'Hari Libur Nasional', 'Hari Lahir Pancasila'),
|
||||||
|
(12, '2026-06-27', 'Hari Raya Idul Adha 1447 Hijriah', 'Hari Libur Nasional', 'SKB 3 Menteri'),
|
||||||
|
(13, '2026-07-16', 'Tahun Baru Islam 1448 Hijriah', 'Hari Libur Nasional', 'SKB 3 Menteri'),
|
||||||
|
(14, '2026-08-17', 'Hari Kemerdekaan Republik Indonesia ke-81', 'Hari Libur Nasional', 'Proklamasi Kemerdekaan'),
|
||||||
|
(15, '2026-08-25', 'Maulid Nabi Muhammad SAW', 'Hari Libur Nasional', 'SKB 3 Menteri'),
|
||||||
|
(16, '2026-12-25', 'Hari Raya Natal', 'Hari Libur Nasional', 'Hari Natal'),
|
||||||
|
]
|
||||||
|
|
||||||
|
row = 7
|
||||||
|
for no, dt_str, name, h_type, notes in sample_data:
|
||||||
|
worksheet.write(row, 0, no, td_center_fmt)
|
||||||
|
worksheet.write(row, 1, dt_str, td_date_fmt)
|
||||||
|
worksheet.write(row, 2, name, td_left_fmt)
|
||||||
|
worksheet.write(row, 3, h_type, td_left_fmt)
|
||||||
|
worksheet.write(row, 4, notes, td_left_fmt)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
workbook.close()
|
||||||
|
output.seek(0)
|
||||||
|
file_data = output.read()
|
||||||
|
output.close()
|
||||||
|
|
||||||
|
filename = 'Template_Hari_Libur_Indonesia.xlsx'
|
||||||
|
attachment = self.env['ir.attachment'].sudo().create({
|
||||||
|
'name': filename,
|
||||||
|
'datas': base64.b64encode(file_data),
|
||||||
|
'mimetype': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'public': True,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_url',
|
||||||
|
'url': f'/web/content/{attachment.id}?download=true',
|
||||||
|
'target': 'new',
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_open_import_wizard(self, *args, **kwargs):
|
||||||
|
"""Membuka dialog popup wizard impor hari libur dari file Excel."""
|
||||||
|
return {
|
||||||
|
'name': _('Impor Hari Libur dari Excel'),
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_model': 'freshservice.holiday.import.wizard',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'target': 'new',
|
||||||
|
'context': dict(self.env.context),
|
||||||
|
}
|
||||||
@@ -73,20 +73,51 @@ _INDONESIA_HOLIDAYS = {
|
|||||||
|
|
||||||
|
|
||||||
def _holiday_dates(env, start_date, end_date):
|
def _holiday_dates(env, start_date, end_date):
|
||||||
"""Return public leave dates combining Indonesian public holidays with Odoo's calendar leaves."""
|
"""Return public leave dates combining Indonesian public holidays with Odoo's calendar leaves.
|
||||||
res = {d for d in _INDONESIA_HOLIDAYS if start_date <= d <= end_date}
|
|
||||||
if not env or not hasattr(env, '__getitem__'):
|
Prioritas Pengambilan Data:
|
||||||
return res
|
1. Model Master Data `freshservice.holiday` di database (hasil input manual atau impor Excel).
|
||||||
try:
|
2. Fallback ke konstanta `_INDONESIA_HOLIDAYS` jika tabel master data di DB masih kosong
|
||||||
leaves = env['resource.calendar.leaves'].sudo().search([
|
atau jika env Odoo tidak tersedia (misal unit test standalone).
|
||||||
('date_from', '<', datetime.combine(end_date + timedelta(days=1), time.min)),
|
3. Menggabungkan dengan cuti dinamis dari `resource.calendar.leaves` jika tersedia.
|
||||||
('date_to', '>=', datetime.combine(start_date, time.min)),
|
"""
|
||||||
])
|
res = set()
|
||||||
for leave in leaves:
|
used_db = False
|
||||||
if leave.date_from:
|
|
||||||
res.add(fields.Datetime.context_timestamp(env.user, leave.date_from).date())
|
if env and hasattr(env, '__getitem__'):
|
||||||
except Exception:
|
try:
|
||||||
pass
|
HolidayModel = env['freshservice.holiday'].sudo()
|
||||||
|
# Cek apakah ada record hari libur aktif di database
|
||||||
|
db_records = HolidayModel.search([
|
||||||
|
('date', '>=', start_date),
|
||||||
|
('date', '<=', end_date),
|
||||||
|
('active', '=', True),
|
||||||
|
])
|
||||||
|
if db_records:
|
||||||
|
res.update(rec.date for rec in db_records)
|
||||||
|
used_db = True
|
||||||
|
elif HolidayModel.search_count([('active', '=', True)]) > 0:
|
||||||
|
# Database sudah memiliki data libur, namun tidak ada libur di rentang tanggal ini
|
||||||
|
used_db = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not used_db:
|
||||||
|
# Fallback ke konstanta _INDONESIA_HOLIDAYS jika DB belum terisi / tanpa koneksi DB
|
||||||
|
res.update(d for d in _INDONESIA_HOLIDAYS if start_date <= d <= end_date)
|
||||||
|
|
||||||
|
if env and hasattr(env, '__getitem__'):
|
||||||
|
try:
|
||||||
|
leaves = env['resource.calendar.leaves'].sudo().search([
|
||||||
|
('date_from', '<', datetime.combine(end_date + timedelta(days=1), time.min)),
|
||||||
|
('date_to', '>=', datetime.combine(start_date, time.min)),
|
||||||
|
])
|
||||||
|
for leave in leaves:
|
||||||
|
if leave.date_from:
|
||||||
|
res.add(fields.Datetime.context_timestamp(env.user, leave.date_from).date())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
@@ -283,7 +314,7 @@ class FreshserviceTicketStageLog(models.Model):
|
|||||||
delay_cause = fields.Selection([
|
delay_cause = fields.Selection([
|
||||||
('it', 'Agent'), ('user', 'User'), ('none', 'None'),
|
('it', 'Agent'), ('user', 'User'), ('none', 'None'),
|
||||||
], string='Delay Cause', default='none')
|
], string='Delay Cause', default='none')
|
||||||
automatic_responsibility = fields.Selection(related='responsibility', readonly=True)
|
automatic_responsibility = fields.Selection(related='responsibility', string='Tanggung Jawab Otomatis', readonly=True)
|
||||||
override_responsibility = fields.Selection([
|
override_responsibility = fields.Selection([
|
||||||
('internal', 'Agent Active Work'), ('it_hold', 'Agent Hold & Discussion'), ('customer', 'Waiting on Requester'),
|
('internal', 'Agent Active Work'), ('it_hold', 'Agent Hold & Discussion'), ('customer', 'Waiting on Requester'),
|
||||||
('user_after_auto_resolve', 'Waiting on User After Auto-Resolve'),
|
('user_after_auto_resolve', 'Waiting on User After Auto-Resolve'),
|
||||||
|
|||||||
@@ -1057,16 +1057,18 @@ class FreshserviceTicket(models.Model):
|
|||||||
return action
|
return action
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def name_search(self, name='', args=None, operator='ilike', limit=100):
|
def name_search(self, name='', domain=None, operator='ilike', limit=100, **kwargs):
|
||||||
"""Override name_search: kalau user ketik nomor tiket yang belum ada di Odoo,
|
"""Override name_search: kalau user ketik nomor tiket yang belum ada di Odoo,
|
||||||
otomatis fetch dari Freshservice dulu baru tampilkan.
|
otomatis fetch dari Freshservice dulu baru tampilkan.
|
||||||
"""
|
"""
|
||||||
|
if domain is None and 'args' in kwargs:
|
||||||
|
domain = kwargs.pop('args')
|
||||||
if name and isinstance(name, str):
|
if name and isinstance(name, str):
|
||||||
ticket_ids = re.findall(r'\d+', name.strip())
|
ticket_ids = re.findall(r'\d+', name.strip())
|
||||||
for ticket_id in ticket_ids:
|
for ticket_id in ticket_ids:
|
||||||
if not self.search_count([('external_ticket_id', '=', ticket_id)]):
|
if not self.search_count([('external_ticket_id', '=', ticket_id)]):
|
||||||
self._fetch_and_create_ticket_by_external_id(ticket_id)
|
self._fetch_and_create_ticket_by_external_id(ticket_id)
|
||||||
return super().name_search(name=name, args=args, operator=operator, limit=limit)
|
return super().name_search(name=name, domain=domain, operator=operator, limit=limit, **kwargs)
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def web_search_read(self, domain, specification, offset=0, limit=None, order=None, count_limit=None):
|
def web_search_read(self, domain, specification, offset=0, limit=None, order=None, count_limit=None):
|
||||||
|
|||||||
55
gmn_freshservice/security/gmn_freshservice_security_v01.xml
Normal file
55
gmn_freshservice/security/gmn_freshservice_security_v01.xml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- ============================================================
|
||||||
|
MODULE CATEGORY: Freshservice
|
||||||
|
Di Odoo 19, ir.module.category menjadi section header tersendiri
|
||||||
|
di tab Access Rights form User (seperti SALES, SERVICES, dll).
|
||||||
|
============================================================ -->
|
||||||
|
<record id="module_category_freshservice" model="ir.module.category">
|
||||||
|
<field name="name">Freshservice</field>
|
||||||
|
<field name="description">Modul Analisis Bottleneck & Audit Trail Tiket Freshservice</field>
|
||||||
|
<field name="sequence">25</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ============================================================
|
||||||
|
PRIVILEGE: Freshservice (WAJIB DI ODOO 19)
|
||||||
|
Di Odoo 19, form User TIDAK LAGI membaca category_id di res.groups.
|
||||||
|
Odoo 19 mewajibkan model res.groups.privilege.
|
||||||
|
Category HANYA akan dirender jika memiliki privilege_ids
|
||||||
|
yang terhubung ke res.groups via privilege_id.
|
||||||
|
============================================================ -->
|
||||||
|
<record id="res_groups_privilege_freshservice" model="res.groups.privilege">
|
||||||
|
<field name="name">Role</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="category_id" ref="module_category_freshservice"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ============================================================
|
||||||
|
GROUP 1: Viewer / Agent IT (Read-Only)
|
||||||
|
- Menggunakan privilege_id (bukan category_id) agar muncul di Odoo 19
|
||||||
|
- sequence 10: tampil pertama di dropdown
|
||||||
|
============================================================ -->
|
||||||
|
<record id="group_freshservice_user" model="res.groups">
|
||||||
|
<field name="name">Viewer / Agent IT</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="privilege_id" ref="res_groups_privilege_freshservice"/>
|
||||||
|
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
<field name="comment">Hak baca (Read) pada seluruh tiket, audit trail, percakapan, dan hari libur. Dapat mengakses wizard bottleneck report untuk generate laporan Excel.</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ============================================================
|
||||||
|
GROUP 2: Supervisor / IT Manager (Full CRUD)
|
||||||
|
- Menggunakan privilege_id (bukan category_id) agar muncul di Odoo 19
|
||||||
|
- sequence 20: tampil kedua di dropdown
|
||||||
|
- implied_ids mewarisi group_freshservice_user
|
||||||
|
- user_ids otomatis menambahkan administrator
|
||||||
|
============================================================ -->
|
||||||
|
<record id="group_freshservice_manager" model="res.groups">
|
||||||
|
<field name="name">Supervisor / IT Manager</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="privilege_id" ref="res_groups_privilege_freshservice"/>
|
||||||
|
<field name="implied_ids" eval="[(4, ref('group_freshservice_user'))]"/>
|
||||||
|
<field name="user_ids" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
||||||
|
<field name="comment">Hak penuh (CRUD) serta hak mengeksekusi sinkronisasi API, manual override, impor hari libur, dan ekspor laporan bottleneck.</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
access_freshservice_ticket_user,freshservice.ticket.user,model_freshservice_ticket,base.group_user,1,1,1,1
|
access_freshservice_ticket_user,freshservice.ticket.user,model_freshservice_ticket,gmn_freshservice.group_freshservice_user,1,0,0,0
|
||||||
access_freshservice_ticket_stage_log_user,freshservice.ticket.stage.log.user,model_freshservice_ticket_stage_log,base.group_user,1,1,1,1
|
access_freshservice_ticket_manager,freshservice.ticket.manager,model_freshservice_ticket,gmn_freshservice.group_freshservice_manager,1,1,1,1
|
||||||
access_freshservice_ticket_conversation_user,freshservice.ticket.conversation.user,model_freshservice_ticket_conversation,base.group_user,1,1,1,1
|
access_freshservice_ticket_stage_log_user,freshservice.ticket.stage.log.user,model_freshservice_ticket_stage_log,gmn_freshservice.group_freshservice_user,1,0,0,0
|
||||||
access_freshservice_ticket_bottleneck_wizard_user,freshservice.ticket.bottleneck.wizard.user,model_freshservice_ticket_bottleneck_wizard,base.group_user,1,1,1,1
|
access_freshservice_ticket_stage_log_manager,freshservice.ticket.stage.log.manager,model_freshservice_ticket_stage_log,gmn_freshservice.group_freshservice_manager,1,1,1,1
|
||||||
|
access_freshservice_ticket_conversation_user,freshservice.ticket.conversation.user,model_freshservice_ticket_conversation,gmn_freshservice.group_freshservice_user,1,1,1,1
|
||||||
|
access_freshservice_ticket_conversation_manager,freshservice.ticket.conversation.manager,model_freshservice_ticket_conversation,gmn_freshservice.group_freshservice_manager,1,1,1,1
|
||||||
|
access_freshservice_holiday_user,freshservice.holiday.user,model_freshservice_holiday,gmn_freshservice.group_freshservice_user,1,0,0,0
|
||||||
|
access_freshservice_holiday_manager,freshservice.holiday.manager,model_freshservice_holiday,gmn_freshservice.group_freshservice_manager,1,1,1,1
|
||||||
|
access_freshservice_holiday_import_wizard_manager,freshservice.holiday.import.wizard.manager,model_freshservice_holiday_import_wizard,gmn_freshservice.group_freshservice_manager,1,1,1,1
|
||||||
|
access_freshservice_ticket_bottleneck_wizard_user,freshservice.ticket.bottleneck.wizard.user,model_freshservice_ticket_bottleneck_wizard,gmn_freshservice.group_freshservice_user,1,1,1,1
|
||||||
|
access_freshservice_ticket_bottleneck_wizard_manager,freshservice.ticket.bottleneck.wizard.manager,model_freshservice_ticket_bottleneck_wizard,gmn_freshservice.group_freshservice_manager,1,1,1,1
|
||||||
|
|||||||
|
Binary file not shown.
Binary file not shown.
134
gmn_freshservice/views/gmn_freshservice_holiday_views_v01.xml
Normal file
134
gmn_freshservice/views/gmn_freshservice_holiday_views_v01.xml
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- LIST VIEW HARI LIBUR & CUTI BERSAMA -->
|
||||||
|
<record id="view_freshservice_holiday_list" model="ir.ui.view">
|
||||||
|
<field name="name">freshservice.holiday.list</field>
|
||||||
|
<field name="model">freshservice.holiday</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Daftar Hari Libur & Cuti Bersama" default_order="date desc">
|
||||||
|
<header>
|
||||||
|
<button name="action_open_import_wizard"
|
||||||
|
string="Impor Excel"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"
|
||||||
|
icon="fa-upload"
|
||||||
|
display="always"
|
||||||
|
groups="gmn_freshservice.group_freshservice_manager"/>
|
||||||
|
<button name="action_download_template"
|
||||||
|
string="Download Template Excel"
|
||||||
|
type="object"
|
||||||
|
class="btn-secondary"
|
||||||
|
icon="fa-download"
|
||||||
|
display="always"
|
||||||
|
groups="gmn_freshservice.group_freshservice_manager"/>
|
||||||
|
<button name="action_populate_default_holidays"
|
||||||
|
string="Muat Libur Baku (2024-2026)"
|
||||||
|
type="object"
|
||||||
|
class="btn-outline-secondary"
|
||||||
|
icon="fa-calendar-check-o"
|
||||||
|
display="always"
|
||||||
|
groups="gmn_freshservice.group_freshservice_manager"/>
|
||||||
|
</header>
|
||||||
|
<field name="date" string="Tanggal Libur"/>
|
||||||
|
<field name="name" string="Nama Hari Libur / Deskripsi"/>
|
||||||
|
<field name="holiday_type"
|
||||||
|
widget="badge"
|
||||||
|
decoration-danger="holiday_type == 'public'"
|
||||||
|
decoration-info="holiday_type == 'joint_leave'"
|
||||||
|
decoration-warning="holiday_type == 'company'"/>
|
||||||
|
<field name="year" string="Tahun" optional="show"/>
|
||||||
|
<field name="notes" string="Catatan" optional="show"/>
|
||||||
|
<field name="active" widget="boolean_toggle" string="Aktif" optional="show"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- FORM VIEW HARI LIBUR & CUTI BERSAMA -->
|
||||||
|
<record id="view_freshservice_holiday_form" model="ir.ui.view">
|
||||||
|
<field name="name">freshservice.holiday.form</field>
|
||||||
|
<field name="model">freshservice.holiday</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Hari Libur & Cuti Bersama">
|
||||||
|
<header>
|
||||||
|
<button name="action_open_import_wizard"
|
||||||
|
string="Impor Excel"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"
|
||||||
|
icon="fa-upload"/>
|
||||||
|
<button name="action_download_template"
|
||||||
|
string="Download Template Excel"
|
||||||
|
type="object"
|
||||||
|
class="btn-secondary"
|
||||||
|
icon="fa-download"/>
|
||||||
|
<button name="action_populate_default_holidays"
|
||||||
|
string="Muat Libur Baku (2024-2026)"
|
||||||
|
type="object"
|
||||||
|
class="btn-outline-secondary"
|
||||||
|
icon="fa-calendar-check-o"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<label for="name" string="Nama Hari Libur"/>
|
||||||
|
<h1>
|
||||||
|
<field name="name" placeholder="misal: Hari Raya Idul Fitri 1447 H"/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group string="Waktu & Klasifikasi">
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="holiday_type" widget="radio"/>
|
||||||
|
<field name="year" readonly="1"/>
|
||||||
|
<field name="active" widget="boolean_toggle"/>
|
||||||
|
</group>
|
||||||
|
<group string="Keterangan & Legalitas">
|
||||||
|
<field name="notes" placeholder="misal: SKB 3 Menteri No. 1017 Tahun 2024..."/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- SEARCH VIEW HARI LIBUR & CUTI BERSAMA -->
|
||||||
|
<record id="view_freshservice_holiday_search" model="ir.ui.view">
|
||||||
|
<field name="name">freshservice.holiday.search</field>
|
||||||
|
<field name="model">freshservice.holiday</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Cari Hari Libur">
|
||||||
|
<field name="name" string="Nama / Deskripsi"/>
|
||||||
|
<field name="date" string="Tanggal"/>
|
||||||
|
<field name="year" string="Tahun"/>
|
||||||
|
<filter name="filter_active" string="Aktif" domain="[('active', '=', True)]"/>
|
||||||
|
<filter name="filter_public" string="Hari Libur Nasional" domain="[('holiday_type', '=', 'public')]"/>
|
||||||
|
<filter name="filter_joint" string="Cuti Bersama" domain="[('holiday_type', '=', 'joint_leave')]"/>
|
||||||
|
<filter name="filter_company" string="Libur Khusus Perusahaan" domain="[('holiday_type', '=', 'company')]"/>
|
||||||
|
<separator/>
|
||||||
|
<filter name="filter_2026" string="Tahun 2026" domain="[('year', '=', 2026)]"/>
|
||||||
|
<filter name="filter_2025" string="Tahun 2025" domain="[('year', '=', 2025)]"/>
|
||||||
|
<filter name="filter_2024" string="Tahun 2024" domain="[('year', '=', 2024)]"/>
|
||||||
|
<group>
|
||||||
|
<filter name="group_by_year" string="Tahun" context="{'group_by': 'year'}"/>
|
||||||
|
<filter name="group_by_type" string="Jenis Libur" context="{'group_by': 'holiday_type'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ACTION WINDOW HARI LIBUR -->
|
||||||
|
<record id="action_freshservice_holiday" model="ir.actions.act_window">
|
||||||
|
<field name="name">Hari Libur & Cuti Bersama</field>
|
||||||
|
<field name="res_model">freshservice.holiday</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="search_view_id" ref="view_freshservice_holiday_search"/>
|
||||||
|
<field name="context">{'search_default_filter_active': 1, 'search_default_group_by_year': 1}</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Belum ada data hari libur atau cuti bersama yang terdaftar.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Klik tombol <b>Muat Libur Baku (2024-2026)</b> untuk langsung memuat data hari libur nasional Indonesia,
|
||||||
|
atau klik <b>Impor Excel</b> untuk mengunggah jadwal hari libur custom perusahaan Anda.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
<field name="needs_verification"/>
|
<field name="needs_verification"/>
|
||||||
<field name="classification_basis"/>
|
<field name="classification_basis"/>
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group groups="gmn_freshservice.group_freshservice_manager">
|
||||||
<field name="override_responsibility"/>
|
<field name="override_responsibility"/>
|
||||||
<field name="override_source"/>
|
<field name="override_source"/>
|
||||||
<field name="override_reviewer"/>
|
<field name="override_reviewer"/>
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
type="object"
|
type="object"
|
||||||
string="Sync Freshservice Data"
|
string="Sync Freshservice Data"
|
||||||
class="btn-primary"
|
class="btn-primary"
|
||||||
icon="fa-refresh"/>
|
icon="fa-refresh"
|
||||||
|
groups="gmn_freshservice.group_freshservice_manager"/>
|
||||||
<button name="action_open_bottleneck_wizard"
|
<button name="action_open_bottleneck_wizard"
|
||||||
type="object"
|
type="object"
|
||||||
string="Bottleneck Report (Excel)"
|
string="Bottleneck Report (Excel)"
|
||||||
|
|||||||
@@ -35,15 +35,31 @@ action = model.action_open_and_fetch_tickets()
|
|||||||
action="action_freshservice_ticket_bottleneck_wizard"
|
action="action_freshservice_ticket_bottleneck_wizard"
|
||||||
sequence="30"/>
|
sequence="30"/>
|
||||||
|
|
||||||
<!-- CONFIGURATION MENU -->
|
<!-- CONFIGURATION MENU (Manager Only) -->
|
||||||
<menuitem id="menu_freshservice_config_parent"
|
<menuitem id="menu_freshservice_config_parent"
|
||||||
name="Konfigurasi"
|
name="Konfigurasi"
|
||||||
parent="menu_freshservice_root"
|
parent="menu_freshservice_root"
|
||||||
|
groups="gmn_freshservice.group_freshservice_manager"
|
||||||
sequence="90"/>
|
sequence="90"/>
|
||||||
|
|
||||||
<menuitem id="menu_freshservice_config_settings"
|
<menuitem id="menu_freshservice_config_settings"
|
||||||
name="Pengaturan Integrasi"
|
name="Pengaturan Integrasi"
|
||||||
parent="menu_freshservice_config_parent"
|
parent="menu_freshservice_config_parent"
|
||||||
action="action_freshservice_config_settings"
|
action="action_freshservice_config_settings"
|
||||||
|
groups="gmn_freshservice.group_freshservice_manager"
|
||||||
sequence="10"/>
|
sequence="10"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_freshservice_holiday"
|
||||||
|
name="Hari Libur & Cuti Bersama"
|
||||||
|
parent="menu_freshservice_config_parent"
|
||||||
|
action="action_freshservice_holiday"
|
||||||
|
groups="gmn_freshservice.group_freshservice_manager"
|
||||||
|
sequence="20"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_freshservice_holiday_import_wizard"
|
||||||
|
name="Impor Hari Libur (Excel)"
|
||||||
|
parent="menu_freshservice_config_parent"
|
||||||
|
action="action_freshservice_holiday_import_wizard"
|
||||||
|
groups="gmn_freshservice.group_freshservice_manager"
|
||||||
|
sequence="25"/>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Package wizard untuk laporan bottleneck tiket Freshservice."""
|
"""Package wizard untuk laporan bottleneck tiket dan utilitas impor Freshservice."""
|
||||||
from . import gmn_freshservice_ticket_bottleneck_wizard_v01
|
from . import gmn_freshservice_ticket_bottleneck_wizard_v01
|
||||||
|
from . import gmn_freshservice_holiday_import_wizard_v01
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,216 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Wizard Impor Hari Libur Nasional & Cuti Bersama dari File Excel (.xlsx).
|
||||||
|
Membaca file spreadsheet Excel yang diunggah pengguna, memvalidasi data tanggal,
|
||||||
|
dan memasukkan data hari libur ke dalam model freshservice.holiday.
|
||||||
|
"""
|
||||||
|
import io
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, date
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
import openpyxl
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FreshserviceHolidayImportWizard(models.TransientModel):
|
||||||
|
"""Wizard interaktif untuk mengunggah dan mengimpor data hari libur dari file Excel."""
|
||||||
|
_name = 'freshservice.holiday.import.wizard'
|
||||||
|
_description = 'Wizard Impor Hari Libur dari Excel'
|
||||||
|
|
||||||
|
file = fields.Binary(string='File Excel (.xlsx)', help='Pilih file Excel berformat .xlsx')
|
||||||
|
filename = fields.Char(string='Nama File')
|
||||||
|
import_mode = fields.Selection([
|
||||||
|
('append', 'Tambahkan / Perbarui (Upsert Tanggal)'),
|
||||||
|
('replace', 'Ganti Seluruh Data (Hapus Data Lama)'),
|
||||||
|
], string='Mode Impor', default='append', required=True,
|
||||||
|
help='Append: jika tanggal sudah ada maka datanya diperbarui, jika belum ada maka ditambahkan. '
|
||||||
|
'Replace: menghapus semua hari libur yang ada saat ini dan menggantinya dengan data dari file.')
|
||||||
|
|
||||||
|
def action_download_template(self, *args, **kwargs):
|
||||||
|
"""Mengunduh file template Excel resmi untuk pengisian hari libur."""
|
||||||
|
return self.env['freshservice.holiday'].action_download_template(*args, **kwargs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_excel_date(val):
|
||||||
|
"""Mengonversi nilai tanggal dari sel Excel menjadi objek datetime.date."""
|
||||||
|
if not val:
|
||||||
|
return None
|
||||||
|
if isinstance(val, datetime):
|
||||||
|
return val.date()
|
||||||
|
if isinstance(val, date):
|
||||||
|
return val
|
||||||
|
if isinstance(val, (int, float)):
|
||||||
|
# Excel serial number untuk rentang tahun 2000-2099 berada di antara 36526 dan 73050
|
||||||
|
if 30000 <= val <= 90000:
|
||||||
|
try:
|
||||||
|
dt = openpyxl.utils.datetime.from_excel(val)
|
||||||
|
return dt.date() if isinstance(dt, datetime) else dt
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
if isinstance(val, str):
|
||||||
|
clean_str = val.strip()
|
||||||
|
for fmt in ('%Y-%m-%d', '%d/%m/%Y', '%d-%m-%Y', '%Y/%m/%d', '%d.%m.%Y'):
|
||||||
|
try:
|
||||||
|
return datetime.strptime(clean_str, fmt).date()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
def action_import_excel(self, *args, **kwargs):
|
||||||
|
"""Memproses file Excel yang diunggah dan menyimpannya ke master data."""
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.file:
|
||||||
|
raise UserError(_('Silakan pilih file Excel (.xlsx) terlebih dahulu sebelum memproses impor.'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
file_bytes = base64.b64decode(self.file)
|
||||||
|
workbook = openpyxl.load_workbook(io.BytesIO(file_bytes), data_only=True)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.exception("Gagal membaca file Excel hari libur: %s", e)
|
||||||
|
raise UserError(_('File yang diunggah bukan file spreadsheet Excel yang valid (.xlsx). Pesan error: %s') % str(e))
|
||||||
|
|
||||||
|
sheet = workbook.active
|
||||||
|
if sheet is None:
|
||||||
|
raise UserError(_('File Excel tidak memiliki lembar kerja (worksheet) yang aktif.'))
|
||||||
|
|
||||||
|
rows = list(sheet.iter_rows(values_only=True))
|
||||||
|
if not rows:
|
||||||
|
raise UserError(_('File Excel kosong atau tidak memiliki baris data.'))
|
||||||
|
|
||||||
|
# Deteksi baris header dan pemetaan indeks kolom secara cerdas
|
||||||
|
header_row_idx = None
|
||||||
|
date_col = None
|
||||||
|
name_col = None
|
||||||
|
type_col = None
|
||||||
|
notes_col = None
|
||||||
|
|
||||||
|
for idx, row in enumerate(rows):
|
||||||
|
row_str = [str(c).lower().strip() if c is not None else '' for c in row]
|
||||||
|
# Lewati baris banner / petunjuk teks panjang
|
||||||
|
if any('petunjuk' in c or 'template import' in c for c in row_str):
|
||||||
|
continue
|
||||||
|
|
||||||
|
found_date = None
|
||||||
|
found_name = None
|
||||||
|
found_type = None
|
||||||
|
found_notes = None
|
||||||
|
|
||||||
|
for col_i, cell_text in enumerate(row_str):
|
||||||
|
if not cell_text:
|
||||||
|
continue
|
||||||
|
if ('tanggal' in cell_text or 'date' in cell_text) and found_date is None:
|
||||||
|
found_date = col_i
|
||||||
|
elif ('nama' in cell_text or 'deskripsi' in cell_text) and found_name is None:
|
||||||
|
found_name = col_i
|
||||||
|
elif ('jenis' in cell_text or 'kategori' in cell_text or 'type' in cell_text) and found_type is None:
|
||||||
|
found_type = col_i
|
||||||
|
elif ('keterangan' in cell_text or 'catatan' in cell_text or 'notes' in cell_text) and found_notes is None:
|
||||||
|
found_notes = col_i
|
||||||
|
|
||||||
|
# Baris header yang sah harus mengandung kolom tanggal dan nama
|
||||||
|
if found_date is not None and found_name is not None:
|
||||||
|
header_row_idx = idx
|
||||||
|
date_col = found_date
|
||||||
|
name_col = found_name
|
||||||
|
type_col = found_type
|
||||||
|
notes_col = found_notes
|
||||||
|
break
|
||||||
|
|
||||||
|
# Fallback indeks kolom jika tidak terdeteksi dari teks header
|
||||||
|
if header_row_idx is None or date_col is None:
|
||||||
|
header_row_idx = 0
|
||||||
|
if len(rows[0]) >= 5:
|
||||||
|
date_col, name_col, type_col, notes_col = 1, 2, 3, 4
|
||||||
|
elif len(rows[0]) >= 4:
|
||||||
|
date_col, name_col, type_col, notes_col = 0, 1, 2, 3
|
||||||
|
else:
|
||||||
|
date_col, name_col, type_col, notes_col = 0, 1, None, None
|
||||||
|
|
||||||
|
if name_col is None:
|
||||||
|
name_col = date_col + 1
|
||||||
|
|
||||||
|
Holiday = self.env['freshservice.holiday']
|
||||||
|
|
||||||
|
# Jika mode 'replace', hapus data yang ada sebelumnya
|
||||||
|
if self.import_mode == 'replace':
|
||||||
|
Holiday.search([]).unlink()
|
||||||
|
|
||||||
|
created_count = 0
|
||||||
|
updated_count = 0
|
||||||
|
skipped_count = 0
|
||||||
|
|
||||||
|
# Iterasi data mulai dari baris setelah header
|
||||||
|
for r_idx in range(header_row_idx + 1, len(rows)):
|
||||||
|
row = rows[r_idx]
|
||||||
|
if not row or all(c is None or str(c).strip() == '' for c in row):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Parsing tanggal
|
||||||
|
raw_date = row[date_col] if date_col < len(row) else None
|
||||||
|
parsed_date = self._parse_excel_date(raw_date)
|
||||||
|
if not parsed_date:
|
||||||
|
skipped_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Parsing nama
|
||||||
|
raw_name = row[name_col] if name_col < len(row) else None
|
||||||
|
holiday_name = str(raw_name).strip() if raw_name else _('Hari Libur')
|
||||||
|
|
||||||
|
# Parsing jenis libur
|
||||||
|
holiday_type = 'public'
|
||||||
|
if type_col is not None and type_col < len(row) and row[type_col]:
|
||||||
|
type_str = str(row[type_col]).lower()
|
||||||
|
if 'cuti' in type_str or 'joint' in type_str:
|
||||||
|
holiday_type = 'joint_leave'
|
||||||
|
elif 'khusus' in type_str or 'perusahaan' in type_str or 'company' in type_str:
|
||||||
|
holiday_type = 'company'
|
||||||
|
|
||||||
|
# Parsing catatan
|
||||||
|
notes_str = ''
|
||||||
|
if notes_col is not None and notes_col < len(row) and row[notes_col]:
|
||||||
|
notes_str = str(row[notes_col]).strip()
|
||||||
|
|
||||||
|
# Simpan / perbarui ke database (upsert)
|
||||||
|
existing = Holiday.search([('date', '=', parsed_date)], limit=1)
|
||||||
|
vals = {
|
||||||
|
'date': parsed_date,
|
||||||
|
'name': holiday_name,
|
||||||
|
'holiday_type': holiday_type,
|
||||||
|
'notes': notes_str,
|
||||||
|
'active': True,
|
||||||
|
}
|
||||||
|
if existing:
|
||||||
|
existing.write(vals)
|
||||||
|
updated_count += 1
|
||||||
|
else:
|
||||||
|
Holiday.create(vals)
|
||||||
|
created_count += 1
|
||||||
|
|
||||||
|
total_processed = created_count + updated_count
|
||||||
|
if total_processed == 0:
|
||||||
|
raise UserError(_('Tidak ada data tanggal libur valid yang dapat diimpor dari file Excel. Pastikan kolom tanggal terisi format YYYY-MM-DD.'))
|
||||||
|
|
||||||
|
msg = _('Impor berhasil! Ditambahkan: %d, Diperbarui: %d.') % (created_count, updated_count)
|
||||||
|
if skipped_count > 0:
|
||||||
|
msg += _(' (%d baris dilewati karena format tanggal tidak sesuai/kosong).') % skipped_count
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.client',
|
||||||
|
'tag': 'display_notification',
|
||||||
|
'params': {
|
||||||
|
'title': _('Impor Excel Selesai'),
|
||||||
|
'message': msg,
|
||||||
|
'type': 'success',
|
||||||
|
'sticky': False,
|
||||||
|
'next': {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_model': 'freshservice.holiday',
|
||||||
|
'view_mode': 'list,form',
|
||||||
|
'views': [[False, 'list'], [False, 'form']],
|
||||||
|
'target': 'main',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- FORM VIEW WIZARD IMPOR HARI LIBUR DARI EXCEL -->
|
||||||
|
<record id="view_freshservice_holiday_import_wizard_form" model="ir.ui.view">
|
||||||
|
<field name="name">freshservice.holiday.import.wizard.form</field>
|
||||||
|
<field name="model">freshservice.holiday.import.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Impor Hari Libur dari Excel">
|
||||||
|
<div class="alert alert-info" role="status">
|
||||||
|
<p class="mb-1 font-weight-bold">
|
||||||
|
<i class="fa fa-info-circle me-1"/> Petunjuk Impor Spreadsheet Excel:
|
||||||
|
</p>
|
||||||
|
<ul class="mb-0 ps-3">
|
||||||
|
<li>Gunakan file berformat <b>.xlsx</b> (Microsoft Excel / Google Sheets).</li>
|
||||||
|
<li>Pastikan kolom <b>Tanggal Libur</b> terisi dengan format standar <code>YYYY-MM-DD</code> (contoh: <code>2026-08-17</code>).</li>
|
||||||
|
<li>Kolom <b>Jenis Libur</b> dapat diisi: <i>Hari Libur Nasional</i>, <i>Cuti Bersama</i>, atau <i>Libur Khusus Perusahaan</i>.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<button name="action_download_template"
|
||||||
|
type="object"
|
||||||
|
string="Download Template Excel (.xlsx)"
|
||||||
|
class="btn btn-outline-primary"
|
||||||
|
icon="fa-download"/>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group string="Unggah Dokumen">
|
||||||
|
<field name="file" filename="filename" string="File Excel (.xlsx)"/>
|
||||||
|
<field name="filename" invisible="1"/>
|
||||||
|
</group>
|
||||||
|
<group string="Metode Penanganan Data">
|
||||||
|
<field name="import_mode" widget="radio"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<footer>
|
||||||
|
<button name="action_import_excel"
|
||||||
|
string="Mulai Impor"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"
|
||||||
|
icon="fa-upload"/>
|
||||||
|
<button string="Batal"
|
||||||
|
class="btn-secondary"
|
||||||
|
special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ACTION WIZARD IMPOR -->
|
||||||
|
<record id="action_freshservice_holiday_import_wizard" model="ir.actions.act_window">
|
||||||
|
<field name="name">Impor Hari Libur dari Excel</field>
|
||||||
|
<field name="res_model">freshservice.holiday.import.wizard</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@@ -54,9 +54,10 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
|||||||
date_from = fields.Date(string='Dari Tanggal Dibuat')
|
date_from = fields.Date(string='Dari Tanggal Dibuat')
|
||||||
date_to = fields.Date(string='Sampai Tanggal Dibuat')
|
date_to = fields.Date(string='Sampai Tanggal Dibuat')
|
||||||
as_of = fields.Datetime(
|
as_of = fields.Datetime(
|
||||||
string='Cut-off As-Of (UTC)',
|
string='Batas Waktu Cut-Off (As-Of)',
|
||||||
help='Jika diisi, semua durasi stage log dipotong sampai timestamp ini. '
|
help='Batas waktu simulasi retrospektif (opsional). '
|
||||||
'Timestamp laporan ditampilkan dalam WIB (UTC+7).',
|
'Jika diisi, perhitungan durasi seluruh stage log dipotong sampai timestamp ini. '
|
||||||
|
'Kosongkan untuk perhitungan real-time hingga saat ini.',
|
||||||
)
|
)
|
||||||
|
|
||||||
status_filter = fields.Selection([
|
status_filter = fields.Selection([
|
||||||
@@ -82,6 +83,19 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
|||||||
file_name = fields.Char(string='Nama File', readonly=True)
|
file_name = fields.Char(string='Nama File', readonly=True)
|
||||||
bottleneck_summary_html = fields.Html(string='Ringkasan Analisis Bottleneck', readonly=True)
|
bottleneck_summary_html = fields.Html(string='Ringkasan Analisis Bottleneck', readonly=True)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def default_get(self, fields_list):
|
||||||
|
res = super().default_get(fields_list)
|
||||||
|
active_model = self.env.context.get('active_model')
|
||||||
|
active_id = self.env.context.get('active_id')
|
||||||
|
if active_model == 'freshservice.ticket' and active_id:
|
||||||
|
ticket = self.env['freshservice.ticket'].browse(active_id)
|
||||||
|
if ticket.exists():
|
||||||
|
res['ticket_id'] = ticket.id
|
||||||
|
if ticket.external_ticket_id:
|
||||||
|
res['external_ticket_id'] = ticket.external_ticket_id
|
||||||
|
return res
|
||||||
|
|
||||||
@api.onchange('ticket_id')
|
@api.onchange('ticket_id')
|
||||||
def _onchange_ticket_id(self):
|
def _onchange_ticket_id(self):
|
||||||
"""Kalau user pilih tiket dari dropdown, otomatis isi nomor ID Freshservice-nya."""
|
"""Kalau user pilih tiket dari dropdown, otomatis isi nomor ID Freshservice-nya."""
|
||||||
@@ -376,9 +390,14 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
|||||||
|
|
||||||
# Header Title
|
# Header Title
|
||||||
ws1.merge_range('B2:H2', f'TICKET ANALYSIS REPORT #{t.external_ticket_id}', title_fmt)
|
ws1.merge_range('B2:H2', f'TICKET ANALYSIS REPORT #{t.external_ticket_id}', title_fmt)
|
||||||
|
cutoff_info = ""
|
||||||
|
if self.as_of:
|
||||||
|
from datetime import timedelta as _td
|
||||||
|
as_of_wib = (self.as_of + _td(hours=7)).strftime('%d-%m-%Y %H:%M')
|
||||||
|
cutoff_info = f" | Cut-off As-Of: {as_of_wib} WIB"
|
||||||
ws1.write(
|
ws1.write(
|
||||||
'B3',
|
'B3',
|
||||||
f'Dicetak: {datetime.now().strftime("%d-%m-%Y %H:%M:%S")} | Dicetak oleh: {self.env.user.name}',
|
f'Dicetak: {datetime.now().strftime("%d-%m-%Y %H:%M:%S")} | Dicetak oleh: {self.env.user.name}{cutoff_info}',
|
||||||
subtitle_fmt
|
subtitle_fmt
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -718,7 +737,12 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
|||||||
return str(dt)
|
return str(dt)
|
||||||
|
|
||||||
row2 = 4
|
row2 = 4
|
||||||
for idx2, log in enumerate(t.stage_log_ids.sorted('start_date'), start=1):
|
current_idx2 = 0
|
||||||
|
for log in t.stage_log_ids.sorted('start_date'):
|
||||||
|
start, end = self._clipped_stage_interval(log, report_as_of)
|
||||||
|
if not start:
|
||||||
|
continue
|
||||||
|
current_idx2 += 1
|
||||||
resp = stage_resp_map.get(log.id, self._effective_responsibility(log))
|
resp = stage_resp_map.get(log.id, self._effective_responsibility(log))
|
||||||
if resp == 'internal':
|
if resp == 'internal':
|
||||||
resp_label = 'Agent Active Work'
|
resp_label = 'Agent Active Work'
|
||||||
@@ -758,9 +782,10 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
|||||||
cause_label = dict(log._fields['delay_cause'].selection).get(log.delay_cause, log.delay_cause or 'None')
|
cause_label = dict(log._fields['delay_cause'].selection).get(log.delay_cause, log.delay_cause or 'None')
|
||||||
verif_str = 'Yes' if (log.needs_verification or resp == 'needs_verification') else 'No'
|
verif_str = 'Yes' if (log.needs_verification or resp == 'needs_verification') else 'No'
|
||||||
|
|
||||||
ws2.write(row2, 1, idx2, td_center_fmt)
|
effective_end = end if (self.as_of and end) else log.end_date
|
||||||
ws2.write(row2, 2, _to_wib_str(log.start_date), td_center_fmt)
|
ws2.write(row2, 1, current_idx2, td_center_fmt)
|
||||||
ws2.write(row2, 3, _to_wib_str(log.end_date), td_center_fmt)
|
ws2.write(row2, 2, _to_wib_str(start), td_center_fmt)
|
||||||
|
ws2.write(row2, 3, _to_wib_str(effective_end), td_center_fmt)
|
||||||
ws2.write(row2, 4, log.stage_name or '-', td_fmt)
|
ws2.write(row2, 4, log.stage_name or '-', td_fmt)
|
||||||
ws2.write(row2, 5, log.effective_status or log.stage_name or '-', td_fmt)
|
ws2.write(row2, 5, log.effective_status or log.stage_name or '-', td_fmt)
|
||||||
ws2.write(row2, 6, resp_label, resp_fmt)
|
ws2.write(row2, 6, resp_label, resp_fmt)
|
||||||
@@ -796,10 +821,14 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
|||||||
ws3.write(3, col_idx, h, th_fmt)
|
ws3.write(3, col_idx, h, th_fmt)
|
||||||
|
|
||||||
row3 = 4
|
row3 = 4
|
||||||
for idx3, conv in enumerate(t.conversation_ids.sorted('created_at'), start=1):
|
current_idx3 = 0
|
||||||
|
for conv in t.conversation_ids.sorted('created_at'):
|
||||||
|
if report_as_of and conv.created_at and conv.created_at > report_as_of:
|
||||||
|
continue
|
||||||
|
current_idx3 += 1
|
||||||
sender_type_label = 'Requester (User)' if (conv.sender_type == 'customer' or conv.incoming) else 'Agent (IT)'
|
sender_type_label = 'Requester (User)' if (conv.sender_type == 'customer' or conv.incoming) else 'Agent (IT)'
|
||||||
conv_wib_str = _to_wib_str(conv.created_at)
|
conv_wib_str = _to_wib_str(conv.created_at)
|
||||||
ws3.write(row3, 1, idx3, td_center_fmt)
|
ws3.write(row3, 1, current_idx3, td_center_fmt)
|
||||||
ws3.write(row3, 2, conv_wib_str, td_center_fmt)
|
ws3.write(row3, 2, conv_wib_str, td_center_fmt)
|
||||||
ws3.write(row3, 3, sender_type_label, normal_badge_fmt if conv.incoming else bn_badge_fmt)
|
ws3.write(row3, 3, sender_type_label, normal_badge_fmt if conv.incoming else bn_badge_fmt)
|
||||||
ws3.write(row3, 4, conv.user_name or '-', td_fmt)
|
ws3.write(row3, 4, conv.user_name or '-', td_fmt)
|
||||||
|
|||||||
@@ -25,14 +25,14 @@
|
|||||||
</group>
|
</group>
|
||||||
|
|
||||||
<!-- MODE SINGLE TICKET -->
|
<!-- MODE SINGLE TICKET -->
|
||||||
<!-- <group string="Parameter Tiket Tunggal" invisible="report_type != 'single'"> -->
|
|
||||||
<group>
|
<group>
|
||||||
<group>
|
<group string="Identitas Tiket">
|
||||||
<field name="external_ticket_id" placeholder="Contoh: 992"/>
|
<field name="external_ticket_id" placeholder="Contoh: 992"/>
|
||||||
<field name="ticket_id" options="{'no_create': True}" placeholder="Atau cari judul tiket di Odoo..."/>
|
<field name="ticket_id" options="{'no_create': True}" placeholder="Atau cari judul tiket di Odoo..."/>
|
||||||
</group>
|
</group>
|
||||||
<group invisible="True">
|
<group string="Simulasi Cut-Off & Opsi">
|
||||||
<field name="auto_sync_detail"/>
|
<field name="as_of" placeholder="Kosongkan jika real-time s/d saat ini"/>
|
||||||
|
<field invisible="True" name="auto_sync_detail"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
@@ -44,6 +44,7 @@
|
|||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="status_filter"/>
|
<field name="status_filter"/>
|
||||||
|
<field name="as_of" string="Batas Cut-Off (As-Of)" placeholder="Kosongkan jika real-time s/d saat ini"/>
|
||||||
<field name="auto_sync_detail"/>
|
<field name="auto_sync_detail"/>
|
||||||
</group>
|
</group>
|
||||||
<group colspan="2">
|
<group colspan="2">
|
||||||
|
|||||||
Reference in New Issue
Block a user