[UPD] initial commit
This commit is contained in:
4
gmn_freshservice/wizard/__init__.py
Normal file
4
gmn_freshservice/wizard/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""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
|
||||
BIN
gmn_freshservice/wizard/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
gmn_freshservice/wizard/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,216 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Wizard Impor Hari Libur Nasional & Cuti Bersama dari File Excel (.xlsx).
|
||||
Membaca file spreadsheet Excel yang diunggah pengguna, memvalidasi data tanggal,
|
||||
dan memasukkan data hari libur ke dalam model freshservice.holiday.
|
||||
"""
|
||||
import io
|
||||
import base64
|
||||
import logging
|
||||
from datetime import datetime, date
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
import openpyxl
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FreshserviceHolidayImportWizard(models.TransientModel):
|
||||
"""Wizard interaktif untuk mengunggah dan mengimpor data hari libur dari file Excel."""
|
||||
_name = 'freshservice.holiday.import.wizard'
|
||||
_description = 'Wizard Impor Hari Libur dari Excel'
|
||||
|
||||
file = fields.Binary(string='File Excel (.xlsx)', help='Pilih file Excel berformat .xlsx')
|
||||
filename = fields.Char(string='Nama File')
|
||||
import_mode = fields.Selection([
|
||||
('append', 'Tambahkan / Perbarui (Upsert Tanggal)'),
|
||||
('replace', 'Ganti Seluruh Data (Hapus Data Lama)'),
|
||||
], string='Mode Impor', default='append', required=True,
|
||||
help='Append: jika tanggal sudah ada maka datanya diperbarui, jika belum ada maka ditambahkan. '
|
||||
'Replace: menghapus semua hari libur yang ada saat ini dan menggantinya dengan data dari file.')
|
||||
|
||||
def action_download_template(self, *args, **kwargs):
|
||||
"""Mengunduh file template Excel resmi untuk pengisian hari libur."""
|
||||
return self.env['freshservice.holiday'].action_download_template(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _parse_excel_date(val):
|
||||
"""Mengonversi nilai tanggal dari sel Excel menjadi objek datetime.date."""
|
||||
if not val:
|
||||
return None
|
||||
if isinstance(val, datetime):
|
||||
return val.date()
|
||||
if isinstance(val, date):
|
||||
return val
|
||||
if isinstance(val, (int, float)):
|
||||
# Excel serial number untuk rentang tahun 2000-2099 berada di antara 36526 dan 73050
|
||||
if 30000 <= val <= 90000:
|
||||
try:
|
||||
dt = openpyxl.utils.datetime.from_excel(val)
|
||||
return dt.date() if isinstance(dt, datetime) else dt
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
if isinstance(val, str):
|
||||
clean_str = val.strip()
|
||||
for fmt in ('%Y-%m-%d', '%d/%m/%Y', '%d-%m-%Y', '%Y/%m/%d', '%d.%m.%Y'):
|
||||
try:
|
||||
return datetime.strptime(clean_str, fmt).date()
|
||||
except ValueError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def action_import_excel(self, *args, **kwargs):
|
||||
"""Memproses file Excel yang diunggah dan menyimpannya ke master data."""
|
||||
self.ensure_one()
|
||||
if not self.file:
|
||||
raise UserError(_('Silakan pilih file Excel (.xlsx) terlebih dahulu sebelum memproses impor.'))
|
||||
|
||||
try:
|
||||
file_bytes = base64.b64decode(self.file)
|
||||
workbook = openpyxl.load_workbook(io.BytesIO(file_bytes), data_only=True)
|
||||
except Exception as e:
|
||||
_logger.exception("Gagal membaca file Excel hari libur: %s", e)
|
||||
raise UserError(_('File yang diunggah bukan file spreadsheet Excel yang valid (.xlsx). Pesan error: %s') % str(e))
|
||||
|
||||
sheet = workbook.active
|
||||
if sheet is None:
|
||||
raise UserError(_('File Excel tidak memiliki lembar kerja (worksheet) yang aktif.'))
|
||||
|
||||
rows = list(sheet.iter_rows(values_only=True))
|
||||
if not rows:
|
||||
raise UserError(_('File Excel kosong atau tidak memiliki baris data.'))
|
||||
|
||||
# Deteksi baris header dan pemetaan indeks kolom secara cerdas
|
||||
header_row_idx = None
|
||||
date_col = None
|
||||
name_col = None
|
||||
type_col = None
|
||||
notes_col = None
|
||||
|
||||
for idx, row in enumerate(rows):
|
||||
row_str = [str(c).lower().strip() if c is not None else '' for c in row]
|
||||
# Lewati baris banner / petunjuk teks panjang
|
||||
if any('petunjuk' in c or 'template import' in c for c in row_str):
|
||||
continue
|
||||
|
||||
found_date = None
|
||||
found_name = None
|
||||
found_type = None
|
||||
found_notes = None
|
||||
|
||||
for col_i, cell_text in enumerate(row_str):
|
||||
if not cell_text:
|
||||
continue
|
||||
if ('tanggal' in cell_text or 'date' in cell_text) and found_date is None:
|
||||
found_date = col_i
|
||||
elif ('nama' in cell_text or 'deskripsi' in cell_text) and found_name is None:
|
||||
found_name = col_i
|
||||
elif ('jenis' in cell_text or 'kategori' in cell_text or 'type' in cell_text) and found_type is None:
|
||||
found_type = col_i
|
||||
elif ('keterangan' in cell_text or 'catatan' in cell_text or 'notes' in cell_text) and found_notes is None:
|
||||
found_notes = col_i
|
||||
|
||||
# Baris header yang sah harus mengandung kolom tanggal dan nama
|
||||
if found_date is not None and found_name is not None:
|
||||
header_row_idx = idx
|
||||
date_col = found_date
|
||||
name_col = found_name
|
||||
type_col = found_type
|
||||
notes_col = found_notes
|
||||
break
|
||||
|
||||
# Fallback indeks kolom jika tidak terdeteksi dari teks header
|
||||
if header_row_idx is None or date_col is None:
|
||||
header_row_idx = 0
|
||||
if len(rows[0]) >= 5:
|
||||
date_col, name_col, type_col, notes_col = 1, 2, 3, 4
|
||||
elif len(rows[0]) >= 4:
|
||||
date_col, name_col, type_col, notes_col = 0, 1, 2, 3
|
||||
else:
|
||||
date_col, name_col, type_col, notes_col = 0, 1, None, None
|
||||
|
||||
if name_col is None:
|
||||
name_col = date_col + 1
|
||||
|
||||
Holiday = self.env['freshservice.holiday']
|
||||
|
||||
# Jika mode 'replace', hapus data yang ada sebelumnya
|
||||
if self.import_mode == 'replace':
|
||||
Holiday.search([]).unlink()
|
||||
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
# Iterasi data mulai dari baris setelah header
|
||||
for r_idx in range(header_row_idx + 1, len(rows)):
|
||||
row = rows[r_idx]
|
||||
if not row or all(c is None or str(c).strip() == '' for c in row):
|
||||
continue
|
||||
|
||||
# Parsing tanggal
|
||||
raw_date = row[date_col] if date_col < len(row) else None
|
||||
parsed_date = self._parse_excel_date(raw_date)
|
||||
if not parsed_date:
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Parsing nama
|
||||
raw_name = row[name_col] if name_col < len(row) else None
|
||||
holiday_name = str(raw_name).strip() if raw_name else _('Hari Libur')
|
||||
|
||||
# Parsing jenis libur
|
||||
holiday_type = 'public'
|
||||
if type_col is not None and type_col < len(row) and row[type_col]:
|
||||
type_str = str(row[type_col]).lower()
|
||||
if 'cuti' in type_str or 'joint' in type_str:
|
||||
holiday_type = 'joint_leave'
|
||||
elif 'khusus' in type_str or 'perusahaan' in type_str or 'company' in type_str:
|
||||
holiday_type = 'company'
|
||||
|
||||
# Parsing catatan
|
||||
notes_str = ''
|
||||
if notes_col is not None and notes_col < len(row) and row[notes_col]:
|
||||
notes_str = str(row[notes_col]).strip()
|
||||
|
||||
# Simpan / perbarui ke database (upsert)
|
||||
existing = Holiday.search([('date', '=', parsed_date)], limit=1)
|
||||
vals = {
|
||||
'date': parsed_date,
|
||||
'name': holiday_name,
|
||||
'holiday_type': holiday_type,
|
||||
'notes': notes_str,
|
||||
'active': True,
|
||||
}
|
||||
if existing:
|
||||
existing.write(vals)
|
||||
updated_count += 1
|
||||
else:
|
||||
Holiday.create(vals)
|
||||
created_count += 1
|
||||
|
||||
total_processed = created_count + updated_count
|
||||
if total_processed == 0:
|
||||
raise UserError(_('Tidak ada data tanggal libur valid yang dapat diimpor dari file Excel. Pastikan kolom tanggal terisi format YYYY-MM-DD.'))
|
||||
|
||||
msg = _('Impor berhasil! Ditambahkan: %d, Diperbarui: %d.') % (created_count, updated_count)
|
||||
if skipped_count > 0:
|
||||
msg += _(' (%d baris dilewati karena format tanggal tidak sesuai/kosong).') % skipped_count
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Impor Excel Selesai'),
|
||||
'message': msg,
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
'next': {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'freshservice.holiday',
|
||||
'view_mode': 'list,form',
|
||||
'views': [[False, 'list'], [False, 'form']],
|
||||
'target': 'main',
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- FORM VIEW WIZARD IMPOR HARI LIBUR DARI EXCEL -->
|
||||
<record id="view_freshservice_holiday_import_wizard_form" model="ir.ui.view">
|
||||
<field name="name">freshservice.holiday.import.wizard.form</field>
|
||||
<field name="model">freshservice.holiday.import.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Impor Hari Libur dari Excel">
|
||||
<div class="alert alert-info" role="status">
|
||||
<p class="mb-1 font-weight-bold">
|
||||
<i class="fa fa-info-circle me-1"/> Petunjuk Impor Spreadsheet Excel:
|
||||
</p>
|
||||
<ul class="mb-0 ps-3">
|
||||
<li>Gunakan file berformat <b>.xlsx</b> (Microsoft Excel / Google Sheets).</li>
|
||||
<li>Pastikan kolom <b>Tanggal Libur</b> terisi dengan format standar <code>YYYY-MM-DD</code> (contoh: <code>2026-08-17</code>).</li>
|
||||
<li>Kolom <b>Jenis Libur</b> dapat diisi: <i>Hari Libur Nasional</i>, <i>Cuti Bersama</i>, atau <i>Libur Khusus Perusahaan</i>.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<button name="action_download_template"
|
||||
type="object"
|
||||
string="Download Template Excel (.xlsx)"
|
||||
class="btn btn-outline-primary"
|
||||
icon="fa-download"/>
|
||||
</div>
|
||||
<group>
|
||||
<group string="Unggah Dokumen">
|
||||
<field name="file" filename="filename" string="File Excel (.xlsx)"/>
|
||||
<field name="filename" invisible="1"/>
|
||||
</group>
|
||||
<group string="Metode Penanganan Data">
|
||||
<field name="import_mode" widget="radio"/>
|
||||
</group>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_import_excel"
|
||||
string="Mulai Impor"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
icon="fa-upload"/>
|
||||
<button string="Batal"
|
||||
class="btn-secondary"
|
||||
special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ACTION WIZARD IMPOR -->
|
||||
<record id="action_freshservice_holiday_import_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Impor Hari Libur dari Excel</field>
|
||||
<field name="res_model">freshservice.holiday.import.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- FORM VIEW WIZARD BOTTLENECK -->
|
||||
<record id="view_freshservice_ticket_bottleneck_wizard_form" model="ir.ui.view">
|
||||
<field name="name">freshservice.ticket.bottleneck.wizard.form</field>
|
||||
<field name="model">freshservice.ticket.bottleneck.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Laporan Analisis Bottleneck Tiket (Excel)">
|
||||
<field name="state" invisible="1"/>
|
||||
|
||||
<!-- STATE 1: PILIH PARAMETER -->
|
||||
<div invisible="state != 'choose'">
|
||||
<div class="alert alert-primary mb-3 d-flex align-items-center gap-2" role="status" invisible="True">
|
||||
<i class="fa fa-info-circle fa-2x" title="Informasi"/>
|
||||
<div>
|
||||
<strong>Ekspor Laporan Analisis Bottleneck ke Excel (.xlsx)</strong><br/>
|
||||
Laporan ini dirancang khusus sebagai <em>bahan meeting</em> untuk mengidentifikasi tahapan, penanggung jawab, dan faktor yang menyebabkan tiket tertahan lama (Waiting on Us vs Waiting on User).
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<group invisible="True">
|
||||
<group string="Pilihan Laporan">
|
||||
<field name="report_type" widget="radio"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- MODE SINGLE TICKET -->
|
||||
<group>
|
||||
<group string="Identitas Tiket">
|
||||
<field name="external_ticket_id" placeholder="Contoh: 992"/>
|
||||
<field name="ticket_id" options="{'no_create': True}" placeholder="Atau cari judul tiket di Odoo..."/>
|
||||
</group>
|
||||
<group string="Simulasi Cut-Off & Opsi">
|
||||
<field name="as_of" placeholder="Kosongkan jika real-time s/d saat ini"/>
|
||||
<field invisible="True" name="auto_sync_detail"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- MODE RANKING MULTI-TIKET -->
|
||||
<group string="Filter Multi-Tiket (Ranking)" invisible="report_type != 'ranking'">
|
||||
<group>
|
||||
<field name="date_from"/>
|
||||
<field name="date_to"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="status_filter"/>
|
||||
<field name="as_of" string="Batas Cut-Off (As-Of)" placeholder="Kosongkan jika real-time s/d saat ini"/>
|
||||
<field name="auto_sync_detail"/>
|
||||
</group>
|
||||
<group colspan="2">
|
||||
<field name="ticket_ids" widget="many2many_tags" placeholder="Kosongkan untuk menyertakan seluruh tiket sesuai filter di atas"/>
|
||||
</group>
|
||||
</group>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- FOOTER BUTTONS -->
|
||||
<footer>
|
||||
<div invisible="state != 'choose'" class="d-flex flex-row gap-2 justify-content-end">
|
||||
<button name="action_generate_excel"
|
||||
string="Export"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
icon="fa-file-excel-o"/>
|
||||
<button string="Batal"
|
||||
class="btn-secondary"
|
||||
special="cancel"/>
|
||||
</div>
|
||||
|
||||
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ACTION WINDOW WIZARD (POP-UP MODAL) -->
|
||||
<record id="action_freshservice_ticket_bottleneck_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Laporan Analisis Bottleneck Excel</field>
|
||||
<field name="res_model">freshservice.ticket.bottleneck.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="binding_model_id" ref="model_freshservice_ticket"/>
|
||||
<field name="binding_view_types">form,list</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user