[UPD] update flow input hlidays ind

This commit is contained in:
Madanil
2026-10-20 14:39:32 +07:00
parent c0a185478b
commit dd44452fa1
19 changed files with 875 additions and 377 deletions

View File

@@ -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

View File

@@ -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',
},
}
}

View File

@@ -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>