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 2202b12..518bc7d 100644 Binary files a/gmn_freshservice/__pycache__/__init__.cpython-312.pyc and b/gmn_freshservice/__pycache__/__init__.cpython-312.pyc differ 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 4752d27..02ac545 100644 Binary files a/gmn_freshservice/models/__pycache__/__init__.cpython-312.pyc and b/gmn_freshservice/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/gmn_freshservice/models/__pycache__/gmn_freshservice_holiday_v01.cpython-312.pyc b/gmn_freshservice/models/__pycache__/gmn_freshservice_holiday_v01.cpython-312.pyc new file mode 100644 index 0000000..bb6d1c1 Binary files /dev/null and b/gmn_freshservice/models/__pycache__/gmn_freshservice_holiday_v01.cpython-312.pyc differ diff --git a/gmn_freshservice/models/__pycache__/gmn_res_config_settings_v01.cpython-312.pyc b/gmn_freshservice/models/__pycache__/gmn_res_config_settings_v01.cpython-312.pyc index 2f5be3e..f3dc9f1 100644 Binary files a/gmn_freshservice/models/__pycache__/gmn_res_config_settings_v01.cpython-312.pyc and b/gmn_freshservice/models/__pycache__/gmn_res_config_settings_v01.cpython-312.pyc differ diff --git a/gmn_freshservice/models/gmn_freshservice_holiday_v01.py b/gmn_freshservice/models/gmn_freshservice_holiday_v01.py new file mode 100644 index 0000000..541f110 --- /dev/null +++ b/gmn_freshservice/models/gmn_freshservice_holiday_v01.py @@ -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), + } 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 0000000..2f05bb2 Binary files /dev/null and b/gmn_freshservice/wizard/__pycache__/gmn_freshservice_holiday_import_wizard_v01.cpython-312.pyc differ 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 + +