[UPD] update colot template and logic UI
This commit is contained in:
Binary file not shown.
@@ -207,6 +207,11 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
||||
'bg_color': '#FCF3CF', 'font_color': '#7D6608', 'border': 1,
|
||||
'border_color': '#F9E79F', 'align': 'center', 'valign': 'vcenter'
|
||||
})
|
||||
scheduled_meeting_fmt = workbook.add_format({
|
||||
'bold': True, 'font_size': 9, 'font_name': 'Calibri',
|
||||
'bg_color': '#D6EAF8', 'font_color': '#21618C', 'border': 1,
|
||||
'border_color': '#85C1E9', 'align': 'center', 'valign': 'vcenter'
|
||||
})
|
||||
kpi_box_label = workbook.add_format({
|
||||
'font_size': 9, 'font_name': 'Calibri', 'bold': True,
|
||||
'bg_color': '#EBF5FB', 'font_color': '#2874A6', 'border': 1, 'border_color': '#AED6F1'
|
||||
@@ -255,13 +260,15 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
||||
summary_html = self._build_single_ticket_sheets(
|
||||
workbook, t, title_fmt, subtitle_fmt, section_fmt, th_fmt,
|
||||
td_fmt, td_wrap_fmt, td_center_fmt, td_num_fmt, td_dec_fmt, td_pct_fmt, td_date_fmt,
|
||||
bn_badge_fmt, normal_badge_fmt, kpi_box_label, kpi_box_val, status_formats, sla_paused_fmt
|
||||
bn_badge_fmt, normal_badge_fmt, kpi_box_label, kpi_box_val, status_formats, sla_paused_fmt,
|
||||
scheduled_meeting_fmt
|
||||
)
|
||||
else:
|
||||
summary_html = self._build_ranking_sheets(
|
||||
workbook, tickets, title_fmt, subtitle_fmt, section_fmt, th_fmt,
|
||||
td_fmt, td_wrap_fmt, td_center_fmt, td_num_fmt, td_dec_fmt, td_pct_fmt, td_date_fmt,
|
||||
bn_badge_fmt, normal_badge_fmt, kpi_box_label, kpi_box_val, status_formats, sla_paused_fmt
|
||||
bn_badge_fmt, normal_badge_fmt, kpi_box_label, kpi_box_val, status_formats, sla_paused_fmt,
|
||||
scheduled_meeting_fmt
|
||||
)
|
||||
|
||||
workbook.close()
|
||||
@@ -309,7 +316,8 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
||||
def _build_single_ticket_sheets(
|
||||
self, workbook, t, title_fmt, subtitle_fmt, section_fmt, th_fmt,
|
||||
td_fmt, td_wrap_fmt, td_center_fmt, td_num_fmt, td_dec_fmt, td_pct_fmt, td_date_fmt,
|
||||
bn_badge_fmt, normal_badge_fmt, kpi_box_label, kpi_box_val, status_formats, sla_paused_fmt=None
|
||||
bn_badge_fmt, normal_badge_fmt, kpi_box_label, kpi_box_val, status_formats, sla_paused_fmt,
|
||||
scheduled_meeting_fmt
|
||||
):
|
||||
"""Membuat sheet untuk analisis mendalam bottleneck tiket tunggal."""
|
||||
ws1 = workbook.add_worksheet('Ringkasan & Bottleneck KPI')
|
||||
@@ -404,12 +412,50 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
||||
'bg_color': '#FFFFFF', 'font_color': '#145A32', 'border': 1, 'border_color': '#A9DFBF',
|
||||
'align': 'center', 'valign': 'vcenter'
|
||||
})
|
||||
# Warna KPI mengikuti responsibility: internal merah, customer orange,
|
||||
# scheduled meeting biru, dan third party abu-abu.
|
||||
kpi_internal_label = workbook.add_format({
|
||||
'font_size': 9, 'font_name': 'Calibri', 'bold': True,
|
||||
'bg_color': '#FADBD8', 'font_color': '#922B21', 'border': 1, 'border_color': '#E6B0AA'
|
||||
})
|
||||
kpi_customer_label = workbook.add_format({
|
||||
'font_size': 9, 'font_name': 'Calibri', 'bold': True,
|
||||
'bg_color': '#FCF3CF', 'font_color': '#7D6608', 'border': 1, 'border_color': '#F9E79F'
|
||||
})
|
||||
kpi_meeting_label = workbook.add_format({
|
||||
'font_size': 9, 'font_name': 'Calibri', 'bold': True,
|
||||
'bg_color': '#D6EAF8', 'font_color': '#21618C', 'border': 1, 'border_color': '#85C1E9'
|
||||
})
|
||||
kpi_third_party_label = workbook.add_format({
|
||||
'font_size': 9, 'font_name': 'Calibri', 'bold': True,
|
||||
'bg_color': '#E5E7E9', 'font_color': '#566573', 'border': 1, 'border_color': '#BDC3C7'
|
||||
})
|
||||
kpi_internal_val = workbook.add_format({
|
||||
'font_size': 11, 'font_name': 'Calibri', 'bold': True,
|
||||
'bg_color': '#FDEDEC', 'font_color': '#922B21', 'border': 1, 'border_color': '#E6B0AA',
|
||||
'align': 'center', 'valign': 'vcenter'
|
||||
})
|
||||
kpi_customer_val = workbook.add_format({
|
||||
'font_size': 11, 'font_name': 'Calibri', 'bold': True,
|
||||
'bg_color': '#FEFDE8', 'font_color': '#7D6608', 'border': 1, 'border_color': '#F9E79F',
|
||||
'align': 'center', 'valign': 'vcenter'
|
||||
})
|
||||
kpi_meeting_val = workbook.add_format({
|
||||
'font_size': 11, 'font_name': 'Calibri', 'bold': True,
|
||||
'bg_color': '#EBF5FB', 'font_color': '#21618C', 'border': 1, 'border_color': '#85C1E9',
|
||||
'align': 'center', 'valign': 'vcenter'
|
||||
})
|
||||
kpi_third_party_val = workbook.add_format({
|
||||
'font_size': 11, 'font_name': 'Calibri', 'bold': True,
|
||||
'bg_color': '#F2F3F4', 'font_color': '#566573', 'border': 1, 'border_color': '#BDC3C7',
|
||||
'align': 'center', 'valign': 'vcenter'
|
||||
})
|
||||
ws1.write('B13', 'First Response', kpi_wh_label)
|
||||
ws1.write('C13', 'Total Waktu', kpi_wh_label)
|
||||
ws1.write('D13', 'Waiting on Us (IT/Agent)', kpi_wh_label)
|
||||
ws1.write('E13', 'Waiting on User (Pengguna)', kpi_wh_label)
|
||||
ws1.write('F13', 'Total SLA Paused', kpi_wh_label)
|
||||
ws1.write('G13', 'Resolved / Idle', kpi_wh_label)
|
||||
ws1.write('D13', 'Waiting on Us (IT/Agent)', kpi_internal_label)
|
||||
ws1.write('E13', 'Waiting on User (Requester)', kpi_customer_label)
|
||||
ws1.write('F13', 'Scheduled Meeting', kpi_meeting_label)
|
||||
ws1.write('G13', 'Third Party', kpi_third_party_label)
|
||||
ws1.write('H13', 'Catatan Rumus', kpi_wh_label)
|
||||
|
||||
# Hitung jam kerja (UTC+7, 08:30-17:30, break 12:30-13:30, Senin-Jumat)
|
||||
@@ -450,10 +496,10 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
||||
wh_total = sum(stage_wh_map.values())
|
||||
ws1.write('B14', format_working_duration(wh_fr), kpi_box_val)
|
||||
ws1.write('C14', format_working_duration(wh_total), kpi_box_val)
|
||||
ws1.write('D14', format_working_duration(wh_agent), kpi_box_val)
|
||||
ws1.write('E14', format_working_duration(wh_cust), kpi_box_val)
|
||||
ws1.write('F14', format_working_duration(wh_paused), kpi_box_val)
|
||||
ws1.write('G14', format_working_duration(wh_resolved_idle), kpi_box_val)
|
||||
ws1.write('D14', format_working_duration(wh_agent), kpi_internal_val)
|
||||
ws1.write('E14', format_working_duration(wh_cust), kpi_customer_val)
|
||||
ws1.write('F14', format_working_duration(wh_meeting), kpi_meeting_val)
|
||||
ws1.write('G14', format_working_duration(wh_vendor), kpi_third_party_val)
|
||||
ws1.write('H14', 'Senin-Jumat, 08:30-17:30 WIB\nBreak 12:30-13:30 (8 Jam/Hari)\nDB UTC → +7 WIB', kpi_box_val)
|
||||
|
||||
# REKAPITULASI BEBAN TANGGUNG JAWAB — harus identik dengan total Sheet 2.
|
||||
@@ -466,15 +512,17 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
||||
recap_rows = [
|
||||
('Waiting on Us (IT - Pengerjaan Aktif)', 'Waktu penanganan langsung oleh Tim IT (Open / In Progress).', wh_it_direct),
|
||||
('Waiting on Us (IT - Internal Hold & Discussion)', 'Tiket ditahan/dianalisa internal IT (Aturan 9: default Beban IT).', wh_internal_hold),
|
||||
('Waiting on User (Customer)', 'Menunggu balasan, konfirmasi, atau keputusan dari pemohon / user.', wh_cust),
|
||||
('Tunggu Bersama (Scheduled Meeting)', 'Masa tunggu bersama menjelang jadwal meeting / Zoom (SLA Paused).', wh_meeting),
|
||||
('Waiting on User (Requester)', 'Menunggu balasan, konfirmasi, atau keputusan dari pemohon / user.', wh_cust),
|
||||
('Scheduled Meeting (Waiting Together)', 'Masa tunggu bersama menjelang jadwal meeting / Zoom (SLA Paused).', wh_meeting),
|
||||
('Waiting on Third Party (Vendor)', 'Eskalasi vendor eksternal atau pengerjaan pihak ketiga (SLA Paused).', wh_vendor),
|
||||
('SLA Paused Lainnya (Admin / External)', 'Aktivitas jeda administratif sistemik lainnya.', wh_other_paused),
|
||||
('Resolved / Idle Time (Sebelum Reopen)', 'Tiket telah di-resolve atau idle otomatis, tidak membebani pemohon.', wh_resolved_idle),
|
||||
('Grand Total Umur Tiket', 'Jumlah seluruh tahapan; harus 100% sama dengan total Sheet 2.', wh_total),
|
||||
]
|
||||
for recap_row, (label, description, hours) in enumerate(recap_rows, start=18):
|
||||
label_fmt = bn_badge_fmt if label.startswith('Waiting on Us') else (normal_badge_fmt if label.startswith('Waiting on User') else (section_fmt if label == 'Grand Total Umur Tiket' else (sla_paused_fmt or td_center_fmt)))
|
||||
# xlsxwriter memakai indeks baris 0-based: start=17 berarti Excel row 18.
|
||||
# Ini menghindari satu baris kosong sebelum rekap pertama.
|
||||
for recap_row, (label, description, hours) in enumerate(recap_rows, start=17):
|
||||
label_fmt = bn_badge_fmt if label.startswith('Waiting on Us') else (normal_badge_fmt if label.startswith('Waiting on User') else (scheduled_meeting_fmt if label.startswith('Scheduled Meeting') else (section_fmt if label == 'Grand Total Umur Tiket' else (sla_paused_fmt or td_center_fmt))))
|
||||
ws1.write(recap_row, 1, label, label_fmt)
|
||||
ws1.merge_range(recap_row, 2, recap_row, 5, description, td_fmt)
|
||||
ws1.write(recap_row, 6, hours, td_dec_fmt)
|
||||
@@ -482,18 +530,19 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
||||
ws1.write(recap_row, 8, (hours / wh_total) if wh_total else 0.0, td_pct_fmt)
|
||||
|
||||
# TABEL RANKING BOTTLENECK PER STAGE
|
||||
# Rekapitulasi berakhir di baris Excel 26 (grand total), sehingga
|
||||
# ranking dimulai di baris 28 agar tidak overlap dengan merge C25:F25.
|
||||
ws1.merge_range('B28:I28', 'RANKING BOTTLENECK PER STAGE (Diurutkan dari Tahapan Paling Lama)', section_fmt)
|
||||
# Rekap berakhir di Excel row 25. Row 26 adalah pemisah.
|
||||
# Title di Excel row 27 (index 26), header di Excel row 28 (index 27),
|
||||
# dan data mulai di Excel row 29 (index 28) secara berurutan tanpa baris kosong.
|
||||
ws1.merge_range('B27:I27', 'RANKING BOTTLENECK PER STAGE (Diurutkan dari Tahapan Paling Lama)', section_fmt)
|
||||
|
||||
headers_bn = ['Rank', 'Tahapan / Status', 'Tanggung Jawab', 'Pengguna', 'Hari Kerja Efektif', 'Jam Kerja', '% Kontribusi', 'Status']
|
||||
for col_idx, h in enumerate(headers_bn, start=1):
|
||||
ws1.write(29, col_idx, h, th_fmt)
|
||||
ws1.write(27, col_idx, h, th_fmt)
|
||||
|
||||
sorted_stages = t.stage_log_ids.sorted(key=lambda s: stage_wh_map.get(s.id, 0.0), reverse=True)
|
||||
total_wh_ticket = max(0.01, sum(stage_wh_map.values()) or 0.01)
|
||||
|
||||
row = 30
|
||||
row = 28
|
||||
primary_bn_text = "Tidak terdeteksi delay signifikan."
|
||||
primary_bn_actor = "-"
|
||||
primary_bn_stage = "-"
|
||||
@@ -508,6 +557,7 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
||||
resp_fmt = bn_badge_fmt
|
||||
elif stage.responsibility == 'customer':
|
||||
resp_label = 'Waiting on User (Requester)'
|
||||
# Customer selalu orange/kuning, bukan merah.
|
||||
resp_fmt = normal_badge_fmt
|
||||
elif stage.responsibility == 'on_hold':
|
||||
resp_label = 'On Hold (Internal Hold / Waiting on Us)'
|
||||
@@ -519,8 +569,8 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
||||
resp_label = 'Waiting on Internal Discussion (Internal Hold / Waiting on Us)'
|
||||
resp_fmt = bn_badge_fmt
|
||||
elif stage.responsibility == 'scheduled_meeting':
|
||||
resp_label = 'Tunggu Bersama (Scheduled Meeting - SLA is Paused)'
|
||||
resp_fmt = sla_paused_fmt or td_center_fmt
|
||||
resp_label = 'Scheduled Meeting (Waiting Together - SLA is Paused)'
|
||||
resp_fmt = scheduled_meeting_fmt
|
||||
elif stage.responsibility == 'external_activity':
|
||||
resp_label = 'External Activity (SLA is Paused)'
|
||||
resp_fmt = sla_paused_fmt or td_center_fmt
|
||||
@@ -529,7 +579,8 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
||||
resp_fmt = sla_paused_fmt or td_center_fmt
|
||||
is_primary = (rank_idx == 1 and stage_wh > 0.0)
|
||||
|
||||
ws1.write(row, 1, f"#{rank_idx}", bn_badge_fmt if is_primary else td_center_fmt)
|
||||
# Nomor ranking utama mengikuti warna tanggung jawab bottleneck.
|
||||
ws1.write(row, 1, f"#{rank_idx}", resp_fmt if is_primary else td_center_fmt)
|
||||
ws1.write(row, 2, stage.stage_name or '-', td_fmt)
|
||||
ws1.write(row, 3, resp_label, resp_fmt)
|
||||
ws1.write(row, 4, stage.actor_name or '-', td_fmt)
|
||||
@@ -579,7 +630,7 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
||||
if log.responsibility == 'internal':
|
||||
resp_label = 'Waiting on Us (Internal)'
|
||||
elif log.responsibility == 'customer':
|
||||
resp_label = 'Waiting on User (Customer)'
|
||||
resp_label = 'Waiting on User (Requester)'
|
||||
elif log.responsibility == 'on_hold':
|
||||
resp_label = 'On Hold (Internal Hold / Waiting on Us)'
|
||||
elif log.responsibility == 'third_party':
|
||||
@@ -587,9 +638,9 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
||||
elif log.responsibility == 'internal_discussion':
|
||||
resp_label = 'Waiting on Internal Discussion (Internal Hold / Waiting on Us)'
|
||||
elif log.responsibility == 'scheduled_meeting':
|
||||
resp_label = 'Tunggu Bersama (Scheduled Meeting - SLA is Paused)'
|
||||
resp_label = 'Scheduled Meeting (Waiting Together - SLA is Paused)'
|
||||
elif log.responsibility == 'external_activity':
|
||||
resp_label = 'External Activity (SLA is Paused)'
|
||||
resp_label = 'External Activity (SLA is Paused)'
|
||||
else:
|
||||
resp_label = 'Resolved (Ticket Closed/Idle)'
|
||||
msg_body = self._get_stage_conversation_body(log, t)
|
||||
@@ -615,7 +666,8 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
||||
ws2.write(row2, 2, _to_wib_str(log.start_date), td_center_fmt)
|
||||
ws2.write(row2, 3, _to_wib_str(log.end_date), td_center_fmt)
|
||||
ws2.write(row2, 4, log.stage_name or '-', td_fmt)
|
||||
ws2.write(row2, 5, resp_label, bn_badge_fmt if log.responsibility == 'internal' else (normal_badge_fmt if log.responsibility == 'customer' else (sla_paused_fmt or td_center_fmt)))
|
||||
resp_fmt = bn_badge_fmt if log.responsibility in ('internal', 'on_hold', 'internal_discussion') else (normal_badge_fmt if log.responsibility == 'customer' else (scheduled_meeting_fmt if log.responsibility == 'scheduled_meeting' else (sla_paused_fmt or td_center_fmt)))
|
||||
ws2.write(row2, 5, resp_label, resp_fmt)
|
||||
ws2.write(row2, 6, log.actor_type or '-', td_center_fmt)
|
||||
ws2.write(row2, 7, log.actor_name or '-', td_fmt)
|
||||
ws2.write(row2, 8, wd_eff, td_dec_fmt)
|
||||
@@ -832,8 +884,10 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
||||
resp_label = 'On Hold (SLA is Paused)'
|
||||
elif log.responsibility == 'third_party':
|
||||
resp_label = 'Waiting on Third Party (SLA is Paused)'
|
||||
elif log.responsibility == 'scheduled_meeting':
|
||||
resp_label = 'Scheduled Meeting (Waiting Together - SLA is Paused)'
|
||||
else:
|
||||
resp_label = 'Waiting on Internal Discussion (SLA is Paused)'
|
||||
resp_label = 'Waiting on Internal Discussion (SLA is Paused)'
|
||||
msg_body = self._get_stage_conversation_body(log, t)
|
||||
raw_act_main = log.activity_content or ''
|
||||
if 'Isi Pesan:' in raw_act_main:
|
||||
@@ -856,7 +910,9 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
||||
ws2.write(row2, 2, _wib(log.start_date), td_center_fmt)
|
||||
ws2.write(row2, 3, _wib(log.end_date), td_center_fmt)
|
||||
ws2.write(row2, 4, log.stage_name or '-', td_fmt)
|
||||
ws2.write(row2, 5, resp_label, bn_badge_fmt if log.responsibility == 'internal' else (normal_badge_fmt if log.responsibility == 'customer' else (sla_paused_fmt or td_center_fmt)))
|
||||
resp_fmt = bn_badge_fmt if log.responsibility in ('internal', 'on_hold', 'internal_discussion') else (normal_badge_fmt if log.responsibility == 'customer' else (scheduled_meeting_fmt if log.responsibility == 'scheduled_meeting' and scheduled_meeting_fmt else (sla_paused_fmt or td_center_fmt)))
|
||||
# Waiting on User memakai normal_badge_fmt orange/kuning.
|
||||
ws2.write(row2, 5, resp_label, resp_fmt)
|
||||
ws2.write(row2, 6, log.actor_name or '-', td_fmt)
|
||||
ws2.write(row2, 7, calc_log_days, td_num_fmt)
|
||||
ws2.write(row2, 8, log.duration_hours, td_dec_fmt)
|
||||
|
||||
Reference in New Issue
Block a user