[UPD] update logic fdd tdd

This commit is contained in:
Madanil
2026-10-21 16:54:17 +07:00
parent dd44452fa1
commit 24c7ea9447
20 changed files with 126 additions and 27 deletions

View File

@@ -20,6 +20,7 @@ Modul untuk melacak audit trail tiket dan memisahkan durasi penanganan:
'mail', 'mail',
], ],
'data': [ 'data': [
'security/gmn_freshservice_security_v01.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'data/gmn_demo_data_v01.xml', 'data/gmn_demo_data_v01.xml',
'views/gmn_res_config_settings_views_v01.xml', 'views/gmn_res_config_settings_views_v01.xml',

View File

@@ -314,7 +314,7 @@ class FreshserviceTicketStageLog(models.Model):
delay_cause = fields.Selection([ delay_cause = fields.Selection([
('it', 'Agent'), ('user', 'User'), ('none', 'None'), ('it', 'Agent'), ('user', 'User'), ('none', 'None'),
], string='Delay Cause', default='none') ], string='Delay Cause', default='none')
automatic_responsibility = fields.Selection(related='responsibility', readonly=True) automatic_responsibility = fields.Selection(related='responsibility', string='Tanggung Jawab Otomatis', readonly=True)
override_responsibility = fields.Selection([ override_responsibility = fields.Selection([
('internal', 'Agent Active Work'), ('it_hold', 'Agent Hold & Discussion'), ('customer', 'Waiting on Requester'), ('internal', 'Agent Active Work'), ('it_hold', 'Agent Hold & Discussion'), ('customer', 'Waiting on Requester'),
('user_after_auto_resolve', 'Waiting on User After Auto-Resolve'), ('user_after_auto_resolve', 'Waiting on User After Auto-Resolve'),

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================
MODULE CATEGORY: Freshservice
Di Odoo 19, ir.module.category menjadi section header tersendiri
di tab Access Rights form User (seperti SALES, SERVICES, dll).
============================================================ -->
<record id="module_category_freshservice" model="ir.module.category">
<field name="name">Freshservice</field>
<field name="description">Modul Analisis Bottleneck &amp; Audit Trail Tiket Freshservice</field>
<field name="sequence">25</field>
</record>
<!-- ============================================================
PRIVILEGE: Freshservice (WAJIB DI ODOO 19)
Di Odoo 19, form User TIDAK LAGI membaca category_id di res.groups.
Odoo 19 mewajibkan model res.groups.privilege.
Category HANYA akan dirender jika memiliki privilege_ids
yang terhubung ke res.groups via privilege_id.
============================================================ -->
<record id="res_groups_privilege_freshservice" model="res.groups.privilege">
<field name="name">Role</field>
<field name="sequence">10</field>
<field name="category_id" ref="module_category_freshservice"/>
</record>
<!-- ============================================================
GROUP 1: Viewer / Agent IT (Read-Only)
- Menggunakan privilege_id (bukan category_id) agar muncul di Odoo 19
- sequence 10: tampil pertama di dropdown
============================================================ -->
<record id="group_freshservice_user" model="res.groups">
<field name="name">Viewer / Agent IT</field>
<field name="sequence">10</field>
<field name="privilege_id" ref="res_groups_privilege_freshservice"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
<field name="comment">Hak baca (Read) pada seluruh tiket, audit trail, percakapan, dan hari libur. Dapat mengakses wizard bottleneck report untuk generate laporan Excel.</field>
</record>
<!-- ============================================================
GROUP 2: Supervisor / IT Manager (Full CRUD)
- Menggunakan privilege_id (bukan category_id) agar muncul di Odoo 19
- sequence 20: tampil kedua di dropdown
- implied_ids mewarisi group_freshservice_user
- user_ids otomatis menambahkan administrator
============================================================ -->
<record id="group_freshservice_manager" model="res.groups">
<field name="name">Supervisor / IT Manager</field>
<field name="sequence">20</field>
<field name="privilege_id" ref="res_groups_privilege_freshservice"/>
<field name="implied_ids" eval="[(4, ref('group_freshservice_user'))]"/>
<field name="user_ids" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
<field name="comment">Hak penuh (CRUD) serta hak mengeksekusi sinkronisasi API, manual override, impor hari libur, dan ekspor laporan bottleneck.</field>
</record>
</odoo>

View File

@@ -1,7 +1,12 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_freshservice_ticket_user,freshservice.ticket.user,model_freshservice_ticket,base.group_user,1,1,1,1 access_freshservice_ticket_user,freshservice.ticket.user,model_freshservice_ticket,gmn_freshservice.group_freshservice_user,1,0,0,0
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_manager,freshservice.ticket.manager,model_freshservice_ticket,gmn_freshservice.group_freshservice_manager,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_stage_log_user,freshservice.ticket.stage.log.user,model_freshservice_ticket_stage_log,gmn_freshservice.group_freshservice_user,1,0,0,0
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_ticket_stage_log_manager,freshservice.ticket.stage.log.manager,model_freshservice_ticket_stage_log,gmn_freshservice.group_freshservice_manager,1,1,1,1
access_freshservice_holiday_user,freshservice.holiday.user,model_freshservice_holiday,base.group_user,1,1,1,1 access_freshservice_ticket_conversation_user,freshservice.ticket.conversation.user,model_freshservice_ticket_conversation,gmn_freshservice.group_freshservice_user,1,0,0,0
access_freshservice_holiday_import_wizard_user,freshservice.holiday.import.wizard.user,model_freshservice_holiday_import_wizard,base.group_user,1,1,1,1 access_freshservice_ticket_conversation_manager,freshservice.ticket.conversation.manager,model_freshservice_ticket_conversation,gmn_freshservice.group_freshservice_manager,1,0,0,0
access_freshservice_holiday_user,freshservice.holiday.user,model_freshservice_holiday,gmn_freshservice.group_freshservice_user,1,0,0,0
access_freshservice_holiday_manager,freshservice.holiday.manager,model_freshservice_holiday,gmn_freshservice.group_freshservice_manager,1,1,1,1
access_freshservice_holiday_import_wizard_manager,freshservice.holiday.import.wizard.manager,model_freshservice_holiday_import_wizard,gmn_freshservice.group_freshservice_manager,1,1,1,1
access_freshservice_ticket_bottleneck_wizard_user,freshservice.ticket.bottleneck.wizard.user,model_freshservice_ticket_bottleneck_wizard,gmn_freshservice.group_freshservice_user,1,1,1,1
access_freshservice_ticket_bottleneck_wizard_manager,freshservice.ticket.bottleneck.wizard.manager,model_freshservice_ticket_bottleneck_wizard,gmn_freshservice.group_freshservice_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_freshservice_ticket_user freshservice.ticket.user model_freshservice_ticket base.group_user gmn_freshservice.group_freshservice_user 1 1 0 1 0 1 0
3 access_freshservice_ticket_stage_log_user access_freshservice_ticket_manager freshservice.ticket.stage.log.user freshservice.ticket.manager model_freshservice_ticket_stage_log model_freshservice_ticket base.group_user gmn_freshservice.group_freshservice_manager 1 1 1 1
4 access_freshservice_ticket_conversation_user access_freshservice_ticket_stage_log_user freshservice.ticket.conversation.user freshservice.ticket.stage.log.user model_freshservice_ticket_conversation model_freshservice_ticket_stage_log base.group_user gmn_freshservice.group_freshservice_user 1 1 0 1 0 1 0
5 access_freshservice_ticket_bottleneck_wizard_user access_freshservice_ticket_stage_log_manager freshservice.ticket.bottleneck.wizard.user freshservice.ticket.stage.log.manager model_freshservice_ticket_bottleneck_wizard model_freshservice_ticket_stage_log base.group_user gmn_freshservice.group_freshservice_manager 1 1 1 1
6 access_freshservice_holiday_user access_freshservice_ticket_conversation_user freshservice.holiday.user freshservice.ticket.conversation.user model_freshservice_holiday model_freshservice_ticket_conversation base.group_user gmn_freshservice.group_freshservice_user 1 1 0 1 0 1 0
7 access_freshservice_holiday_import_wizard_user access_freshservice_ticket_conversation_manager freshservice.holiday.import.wizard.user freshservice.ticket.conversation.manager model_freshservice_holiday_import_wizard model_freshservice_ticket_conversation base.group_user gmn_freshservice.group_freshservice_manager 1 1 0 1 0 1 0
8 access_freshservice_holiday_user freshservice.holiday.user model_freshservice_holiday gmn_freshservice.group_freshservice_user 1 0 0 0
9 access_freshservice_holiday_manager freshservice.holiday.manager model_freshservice_holiday gmn_freshservice.group_freshservice_manager 1 1 1 1
10 access_freshservice_holiday_import_wizard_manager freshservice.holiday.import.wizard.manager model_freshservice_holiday_import_wizard gmn_freshservice.group_freshservice_manager 1 1 1 1
11 access_freshservice_ticket_bottleneck_wizard_user freshservice.ticket.bottleneck.wizard.user model_freshservice_ticket_bottleneck_wizard gmn_freshservice.group_freshservice_user 1 1 1 1
12 access_freshservice_ticket_bottleneck_wizard_manager freshservice.ticket.bottleneck.wizard.manager model_freshservice_ticket_bottleneck_wizard gmn_freshservice.group_freshservice_manager 1 1 1 1

View File

@@ -12,19 +12,22 @@
type="object" type="object"
class="btn-primary" class="btn-primary"
icon="fa-upload" icon="fa-upload"
display="always"/> display="always"
groups="gmn_freshservice.group_freshservice_manager"/>
<button name="action_download_template" <button name="action_download_template"
string="Download Template Excel" string="Download Template Excel"
type="object" type="object"
class="btn-secondary" class="btn-secondary"
icon="fa-download" icon="fa-download"
display="always"/> display="always"
groups="gmn_freshservice.group_freshservice_manager"/>
<button name="action_populate_default_holidays" <button name="action_populate_default_holidays"
string="Muat Libur Baku (2024-2026)" string="Muat Libur Baku (2024-2026)"
type="object" type="object"
class="btn-outline-secondary" class="btn-outline-secondary"
icon="fa-calendar-check-o" icon="fa-calendar-check-o"
display="always"/> display="always"
groups="gmn_freshservice.group_freshservice_manager"/>
</header> </header>
<field name="date" string="Tanggal Libur"/> <field name="date" string="Tanggal Libur"/>
<field name="name" string="Nama Hari Libur / Deskripsi"/> <field name="name" string="Nama Hari Libur / Deskripsi"/>

View File

@@ -61,7 +61,7 @@
<field name="needs_verification"/> <field name="needs_verification"/>
<field name="classification_basis"/> <field name="classification_basis"/>
</group> </group>
<group> <group groups="gmn_freshservice.group_freshservice_manager">
<field name="override_responsibility"/> <field name="override_responsibility"/>
<field name="override_source"/> <field name="override_source"/>
<field name="override_reviewer"/> <field name="override_reviewer"/>

View File

@@ -11,7 +11,8 @@
type="object" type="object"
string="Sync Freshservice Data" string="Sync Freshservice Data"
class="btn-primary" class="btn-primary"
icon="fa-refresh"/> icon="fa-refresh"
groups="gmn_freshservice.group_freshservice_manager"/>
<button name="action_open_bottleneck_wizard" <button name="action_open_bottleneck_wizard"
type="object" type="object"
string="Bottleneck Report (Excel)" string="Bottleneck Report (Excel)"

View File

@@ -35,27 +35,31 @@ action = model.action_open_and_fetch_tickets()
action="action_freshservice_ticket_bottleneck_wizard" action="action_freshservice_ticket_bottleneck_wizard"
sequence="30"/> sequence="30"/>
<!-- CONFIGURATION MENU --> <!-- CONFIGURATION MENU (Manager Only) -->
<menuitem id="menu_freshservice_config_parent" <menuitem id="menu_freshservice_config_parent"
name="Konfigurasi" name="Konfigurasi"
parent="menu_freshservice_root" parent="menu_freshservice_root"
groups="gmn_freshservice.group_freshservice_manager"
sequence="90"/> sequence="90"/>
<menuitem id="menu_freshservice_config_settings" <menuitem id="menu_freshservice_config_settings"
name="Pengaturan Integrasi" name="Pengaturan Integrasi"
parent="menu_freshservice_config_parent" parent="menu_freshservice_config_parent"
action="action_freshservice_config_settings" action="action_freshservice_config_settings"
groups="gmn_freshservice.group_freshservice_manager"
sequence="10"/> sequence="10"/>
<menuitem id="menu_freshservice_holiday" <menuitem id="menu_freshservice_holiday"
name="Hari Libur &amp; Cuti Bersama" name="Hari Libur &amp; Cuti Bersama"
parent="menu_freshservice_config_parent" parent="menu_freshservice_config_parent"
action="action_freshservice_holiday" action="action_freshservice_holiday"
groups="gmn_freshservice.group_freshservice_manager"
sequence="20"/> sequence="20"/>
<menuitem id="menu_freshservice_holiday_import_wizard" <menuitem id="menu_freshservice_holiday_import_wizard"
name="Impor Hari Libur (Excel)" name="Impor Hari Libur (Excel)"
parent="menu_freshservice_config_parent" parent="menu_freshservice_config_parent"
action="action_freshservice_holiday_import_wizard" action="action_freshservice_holiday_import_wizard"
groups="gmn_freshservice.group_freshservice_manager"
sequence="25"/> sequence="25"/>
</odoo> </odoo>

View File

@@ -54,9 +54,10 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
date_from = fields.Date(string='Dari Tanggal Dibuat') date_from = fields.Date(string='Dari Tanggal Dibuat')
date_to = fields.Date(string='Sampai Tanggal Dibuat') date_to = fields.Date(string='Sampai Tanggal Dibuat')
as_of = fields.Datetime( as_of = fields.Datetime(
string='Cut-off As-Of (UTC)', string='Batas Waktu Cut-Off (As-Of)',
help='Jika diisi, semua durasi stage log dipotong sampai timestamp ini. ' help='Batas waktu simulasi retrospektif (opsional). '
'Timestamp laporan ditampilkan dalam WIB (UTC+7).', 'Jika diisi, perhitungan durasi seluruh stage log dipotong sampai timestamp ini. '
'Kosongkan untuk perhitungan real-time hingga saat ini.',
) )
status_filter = fields.Selection([ status_filter = fields.Selection([
@@ -82,6 +83,19 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
file_name = fields.Char(string='Nama File', readonly=True) file_name = fields.Char(string='Nama File', readonly=True)
bottleneck_summary_html = fields.Html(string='Ringkasan Analisis Bottleneck', readonly=True) bottleneck_summary_html = fields.Html(string='Ringkasan Analisis Bottleneck', readonly=True)
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
active_model = self.env.context.get('active_model')
active_id = self.env.context.get('active_id')
if active_model == 'freshservice.ticket' and active_id:
ticket = self.env['freshservice.ticket'].browse(active_id)
if ticket.exists():
res['ticket_id'] = ticket.id
if ticket.external_ticket_id:
res['external_ticket_id'] = ticket.external_ticket_id
return res
@api.onchange('ticket_id') @api.onchange('ticket_id')
def _onchange_ticket_id(self): def _onchange_ticket_id(self):
"""Kalau user pilih tiket dari dropdown, otomatis isi nomor ID Freshservice-nya.""" """Kalau user pilih tiket dari dropdown, otomatis isi nomor ID Freshservice-nya."""
@@ -376,9 +390,14 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
# Header Title # Header Title
ws1.merge_range('B2:H2', f'TICKET ANALYSIS REPORT #{t.external_ticket_id}', title_fmt) ws1.merge_range('B2:H2', f'TICKET ANALYSIS REPORT #{t.external_ticket_id}', title_fmt)
cutoff_info = ""
if self.as_of:
from datetime import timedelta as _td
as_of_wib = (self.as_of + _td(hours=7)).strftime('%d-%m-%Y %H:%M')
cutoff_info = f" | Cut-off As-Of: {as_of_wib} WIB"
ws1.write( ws1.write(
'B3', 'B3',
f'Dicetak: {datetime.now().strftime("%d-%m-%Y %H:%M:%S")} | Dicetak oleh: {self.env.user.name}', f'Dicetak: {datetime.now().strftime("%d-%m-%Y %H:%M:%S")} | Dicetak oleh: {self.env.user.name}{cutoff_info}',
subtitle_fmt subtitle_fmt
) )
@@ -718,7 +737,12 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
return str(dt) return str(dt)
row2 = 4 row2 = 4
for idx2, log in enumerate(t.stage_log_ids.sorted('start_date'), start=1): current_idx2 = 0
for log in t.stage_log_ids.sorted('start_date'):
start, end = self._clipped_stage_interval(log, report_as_of)
if not start:
continue
current_idx2 += 1
resp = stage_resp_map.get(log.id, self._effective_responsibility(log)) resp = stage_resp_map.get(log.id, self._effective_responsibility(log))
if resp == 'internal': if resp == 'internal':
resp_label = 'Agent Active Work' resp_label = 'Agent Active Work'
@@ -758,9 +782,10 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
cause_label = dict(log._fields['delay_cause'].selection).get(log.delay_cause, log.delay_cause or 'None') cause_label = dict(log._fields['delay_cause'].selection).get(log.delay_cause, log.delay_cause or 'None')
verif_str = 'Yes' if (log.needs_verification or resp == 'needs_verification') else 'No' verif_str = 'Yes' if (log.needs_verification or resp == 'needs_verification') else 'No'
ws2.write(row2, 1, idx2, td_center_fmt) effective_end = end if (self.as_of and end) else log.end_date
ws2.write(row2, 2, _to_wib_str(log.start_date), td_center_fmt) ws2.write(row2, 1, current_idx2, td_center_fmt)
ws2.write(row2, 3, _to_wib_str(log.end_date), td_center_fmt) ws2.write(row2, 2, _to_wib_str(start), td_center_fmt)
ws2.write(row2, 3, _to_wib_str(effective_end), td_center_fmt)
ws2.write(row2, 4, log.stage_name or '-', td_fmt) ws2.write(row2, 4, log.stage_name or '-', td_fmt)
ws2.write(row2, 5, log.effective_status or log.stage_name or '-', td_fmt) ws2.write(row2, 5, log.effective_status or log.stage_name or '-', td_fmt)
ws2.write(row2, 6, resp_label, resp_fmt) ws2.write(row2, 6, resp_label, resp_fmt)
@@ -796,10 +821,14 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
ws3.write(3, col_idx, h, th_fmt) ws3.write(3, col_idx, h, th_fmt)
row3 = 4 row3 = 4
for idx3, conv in enumerate(t.conversation_ids.sorted('created_at'), start=1): current_idx3 = 0
for conv in t.conversation_ids.sorted('created_at'):
if report_as_of and conv.created_at and conv.created_at > report_as_of:
continue
current_idx3 += 1
sender_type_label = 'Requester (User)' if (conv.sender_type == 'customer' or conv.incoming) else 'Agent (IT)' sender_type_label = 'Requester (User)' if (conv.sender_type == 'customer' or conv.incoming) else 'Agent (IT)'
conv_wib_str = _to_wib_str(conv.created_at) conv_wib_str = _to_wib_str(conv.created_at)
ws3.write(row3, 1, idx3, td_center_fmt) ws3.write(row3, 1, current_idx3, td_center_fmt)
ws3.write(row3, 2, conv_wib_str, td_center_fmt) ws3.write(row3, 2, conv_wib_str, td_center_fmt)
ws3.write(row3, 3, sender_type_label, normal_badge_fmt if conv.incoming else bn_badge_fmt) ws3.write(row3, 3, sender_type_label, normal_badge_fmt if conv.incoming else bn_badge_fmt)
ws3.write(row3, 4, conv.user_name or '-', td_fmt) ws3.write(row3, 4, conv.user_name or '-', td_fmt)

View File

@@ -25,14 +25,14 @@
</group> </group>
<!-- MODE SINGLE TICKET --> <!-- MODE SINGLE TICKET -->
<!-- <group string="Parameter Tiket Tunggal" invisible="report_type != 'single'"> -->
<group> <group>
<group> <group string="Identitas Tiket">
<field name="external_ticket_id" placeholder="Contoh: 992"/> <field name="external_ticket_id" placeholder="Contoh: 992"/>
<field name="ticket_id" options="{'no_create': True}" placeholder="Atau cari judul tiket di Odoo..."/> <field name="ticket_id" options="{'no_create': True}" placeholder="Atau cari judul tiket di Odoo..."/>
</group> </group>
<group invisible="True"> <group string="Simulasi Cut-Off &amp; Opsi">
<field name="auto_sync_detail"/> <field name="as_of" placeholder="Kosongkan jika real-time s/d saat ini"/>
<field invisible="True" name="auto_sync_detail"/>
</group> </group>
</group> </group>
@@ -44,6 +44,7 @@
</group> </group>
<group> <group>
<field name="status_filter"/> <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"/> <field name="auto_sync_detail"/>
</group> </group>
<group colspan="2"> <group colspan="2">