From dd44452fa1ed6acb7b47cf31f0c4b78bb89d7699 Mon Sep 17 00:00:00 2001 From: Madanil Date: Tue, 20 Oct 2026 14:39:32 +0700 Subject: [PATCH] [UPD] update flow input hlidays ind --- .gitignore | 2 + README.md | 126 ++++++ gmn_freshservice/__manifest__.py | 2 + .../__pycache__/__init__.cpython-312.pyc | Bin 257 -> 359 bytes .../gmn_technical_design_document_v01.md | 360 ------------------ gmn_freshservice/models/__init__.py | 1 + .../__pycache__/__init__.cpython-312.pyc | Bin 393 -> 606 bytes ...n_freshservice_holiday_v01.cpython-312.pyc | Bin 0 -> 12576 bytes ...mn_res_config_settings_v01.cpython-312.pyc | Bin 3318 -> 3693 bytes .../models/gmn_freshservice_holiday_v01.py | 276 ++++++++++++++ .../gmn_freshservice_ticket_stage_log_v01.py | 59 ++- .../models/gmn_freshservice_ticket_v01.py | 6 +- gmn_freshservice/security/ir.model.access.csv | 2 + .../gmn_freshservice_holiday_views_v01.xml | 131 +++++++ gmn_freshservice/views/gmn_menu_views_v01.xml | 12 + gmn_freshservice/wizard/__init__.py | 3 +- ..._holiday_import_wizard_v01.cpython-312.pyc | Bin 0 -> 9651 bytes ..._freshservice_holiday_import_wizard_v01.py | 216 +++++++++++ ...ervice_holiday_import_wizard_views_v01.xml | 56 +++ 19 files changed, 875 insertions(+), 377 deletions(-) create mode 100644 .gitignore create mode 100644 README.md delete mode 100644 gmn_freshservice/gmn_technical_design_document_v01.md create mode 100644 gmn_freshservice/models/__pycache__/gmn_freshservice_holiday_v01.cpython-312.pyc create mode 100644 gmn_freshservice/models/gmn_freshservice_holiday_v01.py create mode 100644 gmn_freshservice/views/gmn_freshservice_holiday_views_v01.xml create mode 100644 gmn_freshservice/wizard/__pycache__/gmn_freshservice_holiday_import_wizard_v01.cpython-312.pyc create mode 100644 gmn_freshservice/wizard/gmn_freshservice_holiday_import_wizard_v01.py create mode 100644 gmn_freshservice/wizard/gmn_freshservice_holiday_import_wizard_views_v01.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cdeedaf --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +docs +gmn_freshservice/tests \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..03a07b2 --- /dev/null +++ b/README.md @@ -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)
[`docs/02-kebutuhan/SRS.md`](docs/02-kebutuhan/SRS.md)
[`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)
[`docs/03-desain/TDD.md`](docs/03-desain/TDD.md)
[`docs/03-desain/Arsitektur.md`](docs/03-desain/Arsitektur.md)
[`docs/03-desain/Kamus-Data-dan-DFD.md`](docs/03-desain/Kamus-Data-dan-DFD.md)
[`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)
[`docs/04-pengujian/Test-Case.md`](docs/04-pengujian/Test-Case.md)
[`docs/04-pengujian/Laporan-Validasi.md`](docs/04-pengujian/Laporan-Validasi.md)
[`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)
[`docs/05-operasional/SOP-Status-Tiket.md`](docs/05-operasional/SOP-Status-Tiket.md)
[`docs/05-operasional/Runbook.md`](docs/05-operasional/Runbook.md)
[`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)
[`docs/06-manajemen/Change-Request-Perbaikan-Logic.md`](docs/06-manajemen/Change-Request-Perbaikan-Logic.md)
[`docs/06-manajemen/Keamanan-dan-Privasi.md`](docs/06-manajemen/Keamanan-dan-Privasi.md)
[`docs/06-manajemen/Traceability-Matrix.md`](docs/06-manajemen/Traceability-Matrix.md)
[`docs/06-manajemen/Roadmap.md`](docs/06-manajemen/Roadmap.md)
[`docs/06-manajemen/Glosarium.md`](docs/06-manajemen/Glosarium.md)
[`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. diff --git a/gmn_freshservice/__manifest__.py b/gmn_freshservice/__manifest__.py index 17d3b1b..ae40847 100644 --- a/gmn_freshservice/__manifest__.py +++ b/gmn_freshservice/__manifest__.py @@ -24,6 +24,8 @@ Modul untuk melacak audit trail tiket dan memisahkan durasi penanganan: 'data/gmn_demo_data_v01.xml', 'views/gmn_res_config_settings_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', 'views/gmn_freshservice_ticket_views_v01.xml', 'views/gmn_menu_views_v01.xml', diff --git a/gmn_freshservice/__pycache__/__init__.cpython-312.pyc b/gmn_freshservice/__pycache__/__init__.cpython-312.pyc index 2202b1252c1dcd26148654f92524642308b8ff98..518bc7d0d18ca1f4c5dd7e85fc970c74e7d587bb 100644 GIT binary patch literal 359 zcmYk0zfQw25XSBNqXc!M%w8jvNU#whMi4{G#t=oaOl(RZjw3rMY6o6}XW#{R2-q1S z!NSA_s#ut~Bvj&t?|$d|eEOb;LmxriJ-u8%LVs_Eby;OBlgVDkbyyf{s@j54U#UvvqlPNjY!dw}l=UFO3|5TPrSHuWeGh*tC-IAp@ zY$hTi6&JY~!>Y3aaaNGqY{@ha{SORL0^E4L4QQ}7wy(Wy8o12{rCgMLVsy zAP;B-ihd#m$Xy1pdki8)96%ue DDyA%Z diff --git a/gmn_freshservice/gmn_technical_design_document_v01.md b/gmn_freshservice/gmn_technical_design_document_v01.md deleted file mode 100644 index 4426be5..0000000 --- a/gmn_freshservice/gmn_technical_design_document_v01.md +++ /dev/null @@ -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}`
`Accept: application/json` | `order_by=created_at`
`order_type=desc`
`per_page=100`
`page={1..N}`
`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. - diff --git a/gmn_freshservice/models/__init__.py b/gmn_freshservice/models/__init__.py index 77be9ac..15c54ad 100644 --- a/gmn_freshservice/models/__init__.py +++ b/gmn_freshservice/models/__init__.py @@ -3,6 +3,7 @@ Urutan import penting karena ticket bergantung pada stage_log dan conversation. """ 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_conversation_v01 from . import gmn_freshservice_ticket_v01 diff --git a/gmn_freshservice/models/__pycache__/__init__.cpython-312.pyc b/gmn_freshservice/models/__pycache__/__init__.cpython-312.pyc index 4752d27621fbc22a5cc5a688468caa62e14a9570..02ac545477345f4f54b366dbc949a10fc7564db1 100644 GIT binary patch delta 372 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 delta 479 zcmXwxK}#D^6ov17Z)P${W+u_7jnKsuBASI1+=voHtJu9_7Z189y{~qXOae1e8eFK5 zy~rTyZ^(d@`~zJE1Q((xh3;Gxx@f!Zn?dj5aL)O-=hy3hlg`k#bD&P!zpWqTkDYSu zgG#4BK-dHy1^P@dp9_vj$C{e23qAF`X$XVCdmya`q)V(j(Hp&F^JEV|m|b2qB96=D zOBGK+fH0&}~RJl) 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), + } diff --git a/gmn_freshservice/models/gmn_freshservice_ticket_stage_log_v01.py b/gmn_freshservice/models/gmn_freshservice_ticket_stage_log_v01.py index e505910..b5a4d52 100644 --- a/gmn_freshservice/models/gmn_freshservice_ticket_stage_log_v01.py +++ b/gmn_freshservice/models/gmn_freshservice_ticket_stage_log_v01.py @@ -73,20 +73,51 @@ _INDONESIA_HOLIDAYS = { def _holiday_dates(env, start_date, end_date): - """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__'): - return res - 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 public leave dates combining Indonesian public holidays with Odoo's calendar leaves. + + Prioritas Pengambilan Data: + 1. Model Master Data `freshservice.holiday` di database (hasil input manual atau impor Excel). + 2. Fallback ke konstanta `_INDONESIA_HOLIDAYS` jika tabel master data di DB masih kosong + atau jika env Odoo tidak tersedia (misal unit test standalone). + 3. Menggabungkan dengan cuti dinamis dari `resource.calendar.leaves` jika tersedia. + """ + res = set() + used_db = False + + if env and hasattr(env, '__getitem__'): + try: + 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 diff --git a/gmn_freshservice/models/gmn_freshservice_ticket_v01.py b/gmn_freshservice/models/gmn_freshservice_ticket_v01.py index 9e15e5f..3b9b525 100644 --- a/gmn_freshservice/models/gmn_freshservice_ticket_v01.py +++ b/gmn_freshservice/models/gmn_freshservice_ticket_v01.py @@ -1057,16 +1057,18 @@ class FreshserviceTicket(models.Model): return action @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, 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): ticket_ids = re.findall(r'\d+', name.strip()) for ticket_id in ticket_ids: if not self.search_count([('external_ticket_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 def web_search_read(self, domain, specification, offset=0, limit=None, order=None, count_limit=None): diff --git a/gmn_freshservice/security/ir.model.access.csv b/gmn_freshservice/security/ir.model.access.csv index 3657941..7dba08a 100644 --- a/gmn_freshservice/security/ir.model.access.csv +++ b/gmn_freshservice/security/ir.model.access.csv @@ -3,3 +3,5 @@ access_freshservice_ticket_user,freshservice.ticket.user,model_freshservice_tick 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_conversation_user,freshservice.ticket.conversation.user,model_freshservice_ticket_conversation,base.group_user,1,1,1,1 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_holiday_user,freshservice.holiday.user,model_freshservice_holiday,base.group_user,1,1,1,1 +access_freshservice_holiday_import_wizard_user,freshservice.holiday.import.wizard.user,model_freshservice_holiday_import_wizard,base.group_user,1,1,1,1 diff --git a/gmn_freshservice/views/gmn_freshservice_holiday_views_v01.xml b/gmn_freshservice/views/gmn_freshservice_holiday_views_v01.xml new file mode 100644 index 0000000..cd3968e --- /dev/null +++ b/gmn_freshservice/views/gmn_freshservice_holiday_views_v01.xml @@ -0,0 +1,131 @@ + + + + + freshservice.holiday.list + freshservice.holiday + + +
+
+ + + + + + +
+
+
+ + + + freshservice.holiday.form + freshservice.holiday + +
+
+
+ +
+
+ + + + + + + + + + + +
+
+
+
+ + + + freshservice.holiday.search + freshservice.holiday + + + + + + + + + + + + + + + + + + + + + + + + Hari Libur & Cuti Bersama + freshservice.holiday + list,form + + {'search_default_filter_active': 1, 'search_default_group_by_year': 1} + +

+ Belum ada data hari libur atau cuti bersama yang terdaftar. +

+

+ Klik tombol Muat Libur Baku (2024-2026) untuk langsung memuat data hari libur nasional Indonesia, + atau klik Impor Excel untuk mengunggah jadwal hari libur custom perusahaan Anda. +

+
+
+
diff --git a/gmn_freshservice/views/gmn_menu_views_v01.xml b/gmn_freshservice/views/gmn_menu_views_v01.xml index cfc8055..0c20454 100644 --- a/gmn_freshservice/views/gmn_menu_views_v01.xml +++ b/gmn_freshservice/views/gmn_menu_views_v01.xml @@ -46,4 +46,16 @@ action = model.action_open_and_fetch_tickets() parent="menu_freshservice_config_parent" action="action_freshservice_config_settings" sequence="10"/> + + + + diff --git a/gmn_freshservice/wizard/__init__.py b/gmn_freshservice/wizard/__init__.py index 69f42ca..fc92186 100644 --- a/gmn_freshservice/wizard/__init__.py +++ b/gmn_freshservice/wizard/__init__.py @@ -1,3 +1,4 @@ # -*- 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_holiday_import_wizard_v01 diff --git a/gmn_freshservice/wizard/__pycache__/gmn_freshservice_holiday_import_wizard_v01.cpython-312.pyc b/gmn_freshservice/wizard/__pycache__/gmn_freshservice_holiday_import_wizard_v01.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2f05bb2c128c2037851294aab7cdf2946177b733 GIT binary patch literal 9651 zcmb_iTW}jkcAdeCc#!}BJ^+G`;hV5Tkks3HS|TM;mPE^@Y)iDf*db;J4ln@dnE@Zz zftGgp15Q{GxZaBBT2;(c?FSVpW!A21OCR~*uJ=P*`2hr02#uAjs^X9MkFuPqjW-{; z-GhNBn%1ULnIgJ-x^LgU-S_tG+h_RCm6cWk9=3WT;^`oWe@8FcV=NV({sRVLoO--OSaY|rmMx10>sYEwN$HNrECF9{RJxNWmkSF8xZYsvcrfHsI zU^w6{(Ub&)VVd7#VKUaPb!u-fzN3{NC+Wy zt<v{?vxC0%+5GJB*rS)DyI5F;!dgKn1gXat#i)puTfgg zY+0ybb)B&CCDjsAua_)>i%YCPMm0|U%s)>g=Y&F|8gG~gR0XC69 zs%=N*;LcKD1{+|i?S~8zbHj{MuJ7C3{OE+XN8G`5ThkZwN1TZ5+#9) zhZVEHzL(?#mQf6oEI*a9j3ARnT>YeAhvfoGG0{_ColL6pG7SbjiA=ALy2J|Ow2OyFzf`K zY=(g7rgiLaTtotSTH{>X& zCw1Z?HbVv$7@L^H#$fd&%^iVp$GDQgA}7O%9EkpGzJc#*b}~j#qn?dVD;_!|f!hl* ziJ3T`pqZe?-mg~-BFj%G23iP%(}>QXTtv>4qNwjeLx=lbV-g8!;N`vqo9d*ezTQuEcPK=^${qUk>E0wvu9HHzdMDV0?% z6H-}ucSK8$qecQ8>@kNm7ZhK@xbI|Fn^Cx$lKl$SM%V5;oRwTE`fMhRhtdsQ`-xo<1f)yJj3?N%OAvv{y zI|xBf5?7&ihM3h~C1y~oXM5|y8LicCOr1vCH4&eN=z*i+98YuF0S|r#&T`mOr2qk3 zPK0JSDlr~mqsZUk<2rx}ff24hNf2B%ND{!)+_0m zZVhzC0-aa=m5LP{L0kkM8e#>sLIN^2MGuFZVw`}TE-B^&_=~BzSza+hyu*u%bwWtQ zf-H)16bm|8rjTn;^l+9aMs%P|DQ3u^7NQF4TQr|!anJyQT?0)jgljuJ^pMwCB@l*Y z^S+Y>-{6Cq!A&FKxlC?a31`ipzO5xZt=|x2HTBTfvZ;ss&)+%;PaCT8J_I45%KM$T z3#R{D-%0b}e&Tl>B&1(dwhtL}U+lLGk^0{gBuWjIA*b>84ieJm4k+Xa@By_19tHWe zNZF@BOX<<}8j0G=eokuz1=L7oKPPE{BEM>8*Z45HyJbI+HpqsZZWU>hY?|MBUKJW2E&qU&(D{NN*%WEj z%Fl8b(X3HxdAdTb_<@xx2f-Tc2P9)ko8f1HpA~*K_}L|7{rHi~GJ!ArCxW;JT1r=@ ztK_O7;)Wxn3&XkaZZFh1WXJp6lCRvae0PeV!*ScA={?fDtpQ%Owm8XH7j+AL1|scD zJ0!nG$=E(5Wam(sPMe89F1!x1SHsT*KR5hpw#>t+(ZM`Um*!EMu9NFTa>2sb<+^D? zG%%GY7mRZCyk4$NS1;5tRize3sl|m`Tnp`t6StU|>Jo*kMB&C1?uC7f8@E`P8k`%M zTAY&$C*_*=$%P>pp=RN%fq>D-h0kOcq`#6Y(`LCkZINARtL#qO@WL zE?eh~vW00(!B`*}5!tPdzcpo=kR<7PXbo(wSIbOu6wXqiMXrwwe7`kZog7|^On$nFh`yvJhn?#e`01|`GNjEH9 zkgYG|Z7uU|mYW6dmOa+2@qFPvvTpI5l-48N!nCF-+4lahZ2Q5{55vrCXmnx!mQhP} z8mAZS?{q8ECb#09)sAvO&vc}zcoox$Yt2koskIxo>X_~{#aPpAvi&e$8^y?Q)acB2#i3qhnsWWlVR-?Se;c zj~scXU#%o}Xye0~<}K;infe7Q9uikNCK>c5CJ%5CLu&IEa|6I zqbY}mKu{_sFmju? zty6S?euWH)C=%5!6PBZ4HqOpY3CB}s{?a9)gb|cb27JW{9OV-snir1;%6#4h?jj1X z{)70_Jdtya{5HH+ojtruAA!^ta*v!Z(`_YU)U(q6FZoH!MRO zKrb*9R*b@hN#qn`gpG5eVvfRXC!7#C#URa1!Tl-9BDfI&O-e-w78|%I#&|+vMFGw7 zYZPAl4PT?!{A*l+miTLQpAx`3KocetPNIraC>h*VJ2Ij+x=|y#(mrK1mt*?~l4RSXoy-i0ykSYfJ)mF$D_A!o^g$25PC&~mbf5%{ zzEz>Z0jT)@QsvJPa})`ws%*OW2lv{^C3<<^68E6_#NE9s@7+1@+0ola?;OAPMt;|c zLi33~xJx?S9UxQIVRTo=9|jcDRC1i>LW&^-*JMy<0^Ol)I19pMcbWxoY%(b(;Q|d% zWm2RkX{^%(@=0?Fc?>q2k>_HZX5 zj3B5V0#x6idK7vJKum_ih7GhJH1;akF$Abf;0gg_jZ@JCpNN&&fZ+i;K$S4#tMK$* zyx2Q56iD@-!vHq`%9HSYf^VgUp24f&c$nQ#q2aOUJ&-y<^>i{+$uR(~JVOH_9Hj*| z4qrx8P`b1~O3noQd6MS(R4?NXq^dcg1mTH+5YMr&xHSwX0>(KPgzdmhaIiZ7he)|q z*;Jngm@~LUPN@)+p%5#IXHvCW_`os(lo$t4GiBj9QQD0pQ4FZ47}Ro%=Hj4_qiESJ z0HPxB65$@x96N){iV+n7`IG?oWI=a|QQ{vhB%!fBWn2bvBF37Cb2 zjVEK^ZzL9MF);%t3EG434G1h$&$Dp>MFEPfpA%&H%*f}sSjn8B5@Y6)!-T(V#fiHmwftp^?gx^2K$iNp-XovagyWSqh15{Q7y zh>=#t-q%*DNA6ZJ!eBuTpevT=0so$rl#0@KBBfHD8v_HvHzB1mIfX#0@~ar-;8TY7 zY82E72=t6Px(%aUB~i?%8Vge1_~=czgoHQa2_n?2G(l7}v$J5Z=rfK1w!Ek~q=ZEC zL5NC_#ltrpeM}6CD3}I-?SJ#AAe?)L!!Wh?|8RmHhmpmX;G6=0-E6<%_{gzt_7)xP z8*_g(_sHeDbu!n?6&fQ2S2SmjZW@f1pOTNfEgN2c-s@lK_{@LXU-0e&T&})lqrNv^ z-+Oobv&ikpy@oY;mlyx{qWLgq3&}2(&+tx)#}xzpNmTaA5Y(u7v+zyZ`AL~*Y8@d?_E2UuRoK! zG@3b^tGoP_!<9#oLcuk%?ieXLJ&(OjnOBR?5*YUUR~@}8;eY8kkatp9V|MOgJyon} zEY>tFd-F9ftxVjr+OYuNLbXmo51^|H?b}&gFYg-#?r?|8A)P^{9Jk zB?1kB)A#Fgqu281uI0x0+_+StXj|s;b$zQZuUYbY&)t7JcZGg%ks~y+ zSxa;t+-xA64S7fB%1il|UMaSAWUGq4&a8P$(%7A~JapD>9wWRxo2N*^-jK7kE?>!Y z?k_eqXGR`5T-kj~1*K+aEVBS3jt3 zd%pGY;aAJFE8Jt7J7@D1>syvv7v;?GV^`hHp2eP=Zy@j5lc@l;*_|6UU*6_hntNdD zDcapPMm`!@+EcJMZ`eEY_Re*C_hXMYQ&}u0-M1lF&YXYfZZ0~TmaFs5wru4K`aRrt^w!>`*1s*lFEbEug{e4xTv2UYsf4*^lq48kW0-Zc9H?J>VUooxfSKrBX zpUicgy5E|;UZ}qSdhxUuJsm}AchTDhtMbHVs;SBhJ*gs`9$1H)Rf|%lTb-X( zK6cjcP`i1RB*I+iDv60645>6M@6nobm(J78vB z*Att)zUoPZ(`nr#oEGb!z8y3WPVYB_4*4*RaQ&)%3n0l;JTi(U;p!_5q0|SZ+eLOH%YM>eCDrCTEOXl(YxpM3R8}-yl&9vEC()~ z(C1DVp*-TRR?I<5MvR=RbV34FQR1%*)fzmUpc?0`tQ~S6jAg!_}5|wH3`r;BV8Y?=XSd9KPk@RcTX4R`TSx z7Na{x&Y%CLN@pA+H_e2}^+e|});!fiX%mY5RXik=-!0sNOqf7P2oj|Ng-URc zZut^sKwqr|bjiYNB_LErjmR9RB~T8kL^W8$L74>t!3#L*5) zbl~y2Y+EqJd{q55eq6W>UC^CMd=(P-nnRLL40_V=)J%}pFNw-8iK;&m?SCY?3q<$7 u5}jWXj>lGCrhCKc%UgX*!v$;Se8sn>vwG6HJh9PrB;R%9KMC}PXa7H17b2km literal 0 HcmV?d00001 diff --git a/gmn_freshservice/wizard/gmn_freshservice_holiday_import_wizard_v01.py b/gmn_freshservice/wizard/gmn_freshservice_holiday_import_wizard_v01.py new file mode 100644 index 0000000..a9a7f9b --- /dev/null +++ b/gmn_freshservice/wizard/gmn_freshservice_holiday_import_wizard_v01.py @@ -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', + }, + } + } diff --git a/gmn_freshservice/wizard/gmn_freshservice_holiday_import_wizard_views_v01.xml b/gmn_freshservice/wizard/gmn_freshservice_holiday_import_wizard_views_v01.xml new file mode 100644 index 0000000..79c23d4 --- /dev/null +++ b/gmn_freshservice/wizard/gmn_freshservice_holiday_import_wizard_views_v01.xml @@ -0,0 +1,56 @@ + + + + + freshservice.holiday.import.wizard.form + freshservice.holiday.import.wizard + +
+
+

+ Petunjuk Impor Spreadsheet Excel: +

+
    +
  • Gunakan file berformat .xlsx (Microsoft Excel / Google Sheets).
  • +
  • Pastikan kolom Tanggal Libur terisi dengan format standar YYYY-MM-DD (contoh: 2026-08-17).
  • +
  • Kolom Jenis Libur dapat diisi: Hari Libur Nasional, Cuti Bersama, atau Libur Khusus Perusahaan.
  • +
+
+
+
+ + + + + + + + + +
+
+ +
+
+ + + + Impor Hari Libur dari Excel + freshservice.holiday.import.wizard + form + new + +