[UPD] update colot template and logic UI
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -25,7 +25,7 @@ class FreshserviceTicketConversation(models.Model):
|
|||||||
|
|
||||||
sender_type = fields.Selection([
|
sender_type = fields.Selection([
|
||||||
('agent', 'IT Support (Agent)'),
|
('agent', 'IT Support (Agent)'),
|
||||||
('customer', 'User (Customer)'),
|
('customer', 'User (Requester)'),
|
||||||
], string='Tipe Pengirim', compute='_compute_sender_type', store=True)
|
], string='Tipe Pengirim', compute='_compute_sender_type', store=True)
|
||||||
|
|
||||||
body_text = fields.Text(string='Isi Pesan')
|
body_text = fields.Text(string='Isi Pesan')
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ class FreshserviceTicketStageLog(models.Model):
|
|||||||
('on_hold', 'On Hold (SLA is Paused)'),
|
('on_hold', 'On Hold (SLA is Paused)'),
|
||||||
('third_party', 'Waiting on Third Party (SLA is Paused)'),
|
('third_party', 'Waiting on Third Party (SLA is Paused)'),
|
||||||
('internal_discussion', 'Waiting on Internal Discussion (SLA is Paused)'),
|
('internal_discussion', 'Waiting on Internal Discussion (SLA is Paused)'),
|
||||||
('scheduled_meeting', 'Tunggu Bersama (Scheduled Meeting - SLA is Paused)'),
|
('scheduled_meeting', 'Scheduled Meeting (Waiting Together - SLA is Paused)'),
|
||||||
('external_activity', 'External Activity (SLA is Paused)'),
|
('external_activity', 'External Activity (SLA is Paused)'),
|
||||||
('resolved_idle', 'Resolved (Ticket Closed/Idle)'),
|
('resolved_idle', 'Resolved (Ticket Closed/Idle)'),
|
||||||
], string='Tanggung Jawab', required=True, default='internal', index=True)
|
], string='Tanggung Jawab', required=True, default='internal', index=True)
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class FreshserviceTicket(models.Model):
|
|||||||
first_responded_at = fields.Datetime(string='Waktu First Response')
|
first_responded_at = fields.Datetime(string='Waktu First Response')
|
||||||
resolved_at = fields.Datetime(string='Waktu Selesai (Resolved)')
|
resolved_at = fields.Datetime(string='Waktu Selesai (Resolved)')
|
||||||
|
|
||||||
is_mock_data = fields.Boolean(string='Menggunakan Data Mock', default=True, readonly=True)
|
is_mock_data = fields.Boolean(string='Menggunakan Data Mock', default=False, readonly=True)
|
||||||
is_detail_synced = fields.Boolean(
|
is_detail_synced = fields.Boolean(
|
||||||
string='Detail Lengkap Tersinkron',
|
string='Detail Lengkap Tersinkron',
|
||||||
default=False,
|
default=False,
|
||||||
@@ -134,6 +134,40 @@ class FreshserviceTicket(models.Model):
|
|||||||
store=True
|
store=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
shared_wait_time_days = fields.Integer(
|
||||||
|
string='Scheduled Meeting (Days)',
|
||||||
|
compute='_compute_time_metrics',
|
||||||
|
store=True
|
||||||
|
)
|
||||||
|
shared_wait_time_hours = fields.Float(
|
||||||
|
string='Scheduled Meeting (Hours)',
|
||||||
|
compute='_compute_time_metrics',
|
||||||
|
store=True,
|
||||||
|
digits=(12, 2)
|
||||||
|
)
|
||||||
|
shared_wait_time_display = fields.Char(
|
||||||
|
string='Scheduled Meeting',
|
||||||
|
compute='_compute_time_metrics',
|
||||||
|
store=True
|
||||||
|
)
|
||||||
|
|
||||||
|
third_party_time_days = fields.Integer(
|
||||||
|
string='Waiting on Third Party (Hari)',
|
||||||
|
compute='_compute_time_metrics',
|
||||||
|
store=True
|
||||||
|
)
|
||||||
|
third_party_time_hours = fields.Float(
|
||||||
|
string='Waiting on Third Party (Jam)',
|
||||||
|
compute='_compute_time_metrics',
|
||||||
|
store=True,
|
||||||
|
digits=(12, 2)
|
||||||
|
)
|
||||||
|
third_party_time_display = fields.Char(
|
||||||
|
string='Waiting on Third Party',
|
||||||
|
compute='_compute_time_metrics',
|
||||||
|
store=True
|
||||||
|
)
|
||||||
|
|
||||||
resolution_time_days = fields.Integer(
|
resolution_time_days = fields.Integer(
|
||||||
string='Waktu Penyelesaian (Hari)',
|
string='Waktu Penyelesaian (Hari)',
|
||||||
compute='_compute_time_metrics',
|
compute='_compute_time_metrics',
|
||||||
@@ -153,7 +187,7 @@ class FreshserviceTicket(models.Model):
|
|||||||
|
|
||||||
current_responsibility = fields.Selection([
|
current_responsibility = fields.Selection([
|
||||||
('internal', 'Waiting on Us (Internal)'),
|
('internal', 'Waiting on Us (Internal)'),
|
||||||
('customer', 'Waiting on User (Customer)'),
|
('customer', 'Waiting on User (Requester)'),
|
||||||
('closed', 'Tiket Selesai'),
|
('closed', 'Tiket Selesai'),
|
||||||
], string='Status Tanggung Jawab Saat Ini', compute='_compute_time_metrics', store=True)
|
], string='Status Tanggung Jawab Saat Ini', compute='_compute_time_metrics', store=True)
|
||||||
|
|
||||||
@@ -171,7 +205,7 @@ class FreshserviceTicket(models.Model):
|
|||||||
|
|
||||||
@api.depends(
|
@api.depends(
|
||||||
'ticket_created_at', 'first_responded_at', 'resolved_at', 'status', 'is_detail_synced',
|
'ticket_created_at', 'first_responded_at', 'resolved_at', 'status', 'is_detail_synced',
|
||||||
'stage_log_ids.duration_days', 'stage_log_ids.duration_hours', 'stage_log_ids.is_active'
|
'stage_log_ids.duration_days', 'stage_log_ids.duration_hours', 'stage_log_ids.working_hours', 'stage_log_ids.is_active'
|
||||||
)
|
)
|
||||||
def _compute_time_metrics(self):
|
def _compute_time_metrics(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
@@ -190,10 +224,15 @@ class FreshserviceTicket(models.Model):
|
|||||||
rec.first_response_time_display = "Belum Ada Respon"
|
rec.first_response_time_display = "Belum Ada Respon"
|
||||||
|
|
||||||
# 2. Hitung total SLA berdasarkan jam kerja efektif, bukan durasi kalender.
|
# 2. Hitung total SLA berdasarkan jam kerja efektif, bukan durasi kalender.
|
||||||
agent_hours = sum(l.working_hours for l in rec.stage_log_ids if l.responsibility == 'internal')
|
# Mengikuti Aturan 9: Internal Hold & Internal Discussion dibebankan ke Agen (Waiting on Us).
|
||||||
|
agent_hours = sum(l.working_hours for l in rec.stage_log_ids if l.responsibility in ('internal', 'on_hold', 'internal_discussion'))
|
||||||
cust_hours = sum(l.working_hours for l in rec.stage_log_ids if l.responsibility == 'customer')
|
cust_hours = sum(l.working_hours for l in rec.stage_log_ids if l.responsibility == 'customer')
|
||||||
|
shared_wait_hours = sum(l.working_hours for l in rec.stage_log_ids if l.responsibility == 'scheduled_meeting')
|
||||||
|
third_party_hours = sum(l.working_hours for l in rec.stage_log_ids if l.responsibility == 'third_party')
|
||||||
agent_days = round(agent_hours / 8.0, 2)
|
agent_days = round(agent_hours / 8.0, 2)
|
||||||
cust_days = round(cust_hours / 8.0, 2)
|
cust_days = round(cust_hours / 8.0, 2)
|
||||||
|
shared_wait_days = round(shared_wait_hours / 8.0, 2)
|
||||||
|
third_party_days = round(third_party_hours / 8.0, 2)
|
||||||
|
|
||||||
# Resolved/closed dan aktivitas SLA-paused tidak dibebankan ke user/IT.
|
# Resolved/closed dan aktivitas SLA-paused tidak dibebankan ke user/IT.
|
||||||
total_working_hours = sum(l.working_hours for l in rec.stage_log_ids)
|
total_working_hours = sum(l.working_hours for l in rec.stage_log_ids)
|
||||||
@@ -207,6 +246,14 @@ class FreshserviceTicket(models.Model):
|
|||||||
rec.total_customer_time_days = round(cust_days)
|
rec.total_customer_time_days = round(cust_days)
|
||||||
rec.total_customer_time_display = format_duration_days(round(cust_days)) if (rec.is_detail_synced or rec.stage_log_ids) else "-"
|
rec.total_customer_time_display = format_duration_days(round(cust_days)) if (rec.is_detail_synced or rec.stage_log_ids) else "-"
|
||||||
|
|
||||||
|
rec.shared_wait_time_hours = round(shared_wait_hours, 2)
|
||||||
|
rec.shared_wait_time_days = round(shared_wait_days)
|
||||||
|
rec.shared_wait_time_display = format_duration_days(round(shared_wait_days)) if (rec.is_detail_synced or rec.stage_log_ids) else "-"
|
||||||
|
|
||||||
|
rec.third_party_time_hours = round(third_party_hours, 2)
|
||||||
|
rec.third_party_time_days = round(third_party_days)
|
||||||
|
rec.third_party_time_display = format_duration_days(round(third_party_days)) if (rec.is_detail_synced or rec.stage_log_ids) else "-"
|
||||||
|
|
||||||
# 3. Hitung Resolution Time
|
# 3. Hitung Resolution Time
|
||||||
if rec.ticket_created_at and rec.resolved_at:
|
if rec.ticket_created_at and rec.resolved_at:
|
||||||
delta_res = rec.resolved_at - rec.ticket_created_at
|
delta_res = rec.resolved_at - rec.ticket_created_at
|
||||||
@@ -289,9 +336,9 @@ class FreshserviceTicket(models.Model):
|
|||||||
return
|
return
|
||||||
|
|
||||||
get_param = self.env['ir.config_parameter'].sudo().get_param
|
get_param = self.env['ir.config_parameter'].sudo().get_param
|
||||||
base_url = get_param('freshservice_ticket_audit.freshservice_url', 'https://servicedesk.lyman.co.id')
|
base_url = get_param('gmn_freshservice.freshservice_url', 'https://servicedesk.lyman.co.id')
|
||||||
api_key = get_param('freshservice_ticket_audit.freshservice_api_key', '')
|
api_key = get_param('gmn_freshservice.freshservice_api_key', '')
|
||||||
use_mock = get_param('freshservice_ticket_audit.freshservice_use_mock', 'True') in ('True', '1', True)
|
use_mock = False
|
||||||
|
|
||||||
client = FreshserviceClient(base_url=base_url, api_key=api_key, use_mock=use_mock)
|
client = FreshserviceClient(base_url=base_url, api_key=api_key, use_mock=use_mock)
|
||||||
payload = client.fetch_ticket_full_data(self.external_ticket_id)
|
payload = client.fetch_ticket_full_data(self.external_ticket_id)
|
||||||
@@ -657,7 +704,7 @@ class FreshserviceTicket(models.Model):
|
|||||||
'riwayat percakapan lama dipertahankan.', self.external_ticket_id,
|
'riwayat percakapan lama dipertahankan.', self.external_ticket_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
mode_text = "MOCK ENGINE (Simulasi Format Resmi)" if use_mock else "LIVE FRESHSERVICE API"
|
mode_text = "LIVE FRESHSERVICE API"
|
||||||
return {
|
return {
|
||||||
'type': 'ir.actions.client',
|
'type': 'ir.actions.client',
|
||||||
'tag': 'display_notification',
|
'tag': 'display_notification',
|
||||||
@@ -669,25 +716,6 @@ class FreshserviceTicket(models.Model):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def action_toggle_mock_mode(self):
|
|
||||||
"""Toggle instan mode antara Mock dan Live API"""
|
|
||||||
param_obj = self.env['ir.config_parameter'].sudo()
|
|
||||||
current = param_obj.get_param('freshservice_ticket_audit.freshservice_use_mock', 'True') in ('True', '1', True)
|
|
||||||
new_val = not current
|
|
||||||
param_obj.set_param('freshservice_ticket_audit.freshservice_use_mock', str(new_val))
|
|
||||||
|
|
||||||
status_label = "MOCK DATA (Simulasi)" if new_val else "LIVE API FRESHSERVICE"
|
|
||||||
return {
|
|
||||||
'type': 'ir.actions.client',
|
|
||||||
'tag': 'display_notification',
|
|
||||||
'params': {
|
|
||||||
'title': 'Mode Berhasil Diubah',
|
|
||||||
'message': f'Sistem sekarang beralih ke: {status_label}. Klik tombol "Tarik Data Freshservice" untuk menyegarkan data.',
|
|
||||||
'type': 'info',
|
|
||||||
'sticky': False,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# AUTO-FETCH SAAT BUKA HALAMAN LIST TIKET & MULTI-USER SYNCHRONIZATION
|
# AUTO-FETCH SAAT BUKA HALAMAN LIST TIKET & MULTI-USER SYNCHRONIZATION
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -711,14 +739,14 @@ class FreshserviceTicket(models.Model):
|
|||||||
param_obj = self.env['ir.config_parameter'].sudo()
|
param_obj = self.env['ir.config_parameter'].sudo()
|
||||||
if not limit:
|
if not limit:
|
||||||
try:
|
try:
|
||||||
limit = int(param_obj.get_param('freshservice_ticket_audit.freshservice_sync_limit', 200) or 200)
|
limit = int(param_obj.get_param('gmn_freshservice.freshservice_sync_limit', 200) or 200)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
limit = 200
|
limit = 200
|
||||||
if limit <= 0:
|
if limit <= 0:
|
||||||
limit = 200
|
limit = 200
|
||||||
|
|
||||||
now_ts = fields.Datetime.now().timestamp()
|
now_ts = fields.Datetime.now().timestamp()
|
||||||
last_sync = param_obj.get_param('freshservice_ticket_audit.last_list_sync_time')
|
last_sync = param_obj.get_param('gmn_freshservice.last_list_sync_time')
|
||||||
if last_sync:
|
if last_sync:
|
||||||
try:
|
try:
|
||||||
# Throttling: Jangan panggil ulang jika baru saja di-fetch dalam 5 detik terakhir
|
# Throttling: Jangan panggil ulang jika baru saja di-fetch dalam 5 detik terakhir
|
||||||
@@ -726,14 +754,14 @@ class FreshserviceTicket(models.Model):
|
|||||||
return 0
|
return 0
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
param_obj.set_param('freshservice_ticket_audit.last_list_sync_time', str(now_ts))
|
param_obj.set_param('gmn_freshservice.last_list_sync_time', str(now_ts))
|
||||||
|
|
||||||
base_url = param_obj.get_param('freshservice_ticket_audit.freshservice_url', 'https://servicedesk.lyman.co.id')
|
base_url = param_obj.get_param('gmn_freshservice.freshservice_url', 'https://servicedesk.lyman.co.id')
|
||||||
api_key = param_obj.get_param('freshservice_ticket_audit.freshservice_api_key', '')
|
api_key = param_obj.get_param('gmn_freshservice.freshservice_api_key', '')
|
||||||
use_mock = param_obj.get_param('freshservice_ticket_audit.freshservice_use_mock', 'True') in ('True', '1', True)
|
use_mock = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
client = FreshserviceClient(base_url=base_url, api_key=api_key, use_mock=use_mock)
|
client = FreshserviceClient(base_url=base_url, api_key=api_key, use_mock=False)
|
||||||
tickets_list = client.fetch_recent_tickets(limit=limit, tiket_id=tiket_id)
|
tickets_list = client.fetch_recent_tickets(limit=limit, tiket_id=tiket_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_logger.warning("Auto-fetch Freshservice gagal: %s", e)
|
_logger.warning("Auto-fetch Freshservice gagal: %s", e)
|
||||||
@@ -884,10 +912,10 @@ class FreshserviceTicket(models.Model):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
_logger.warning("Auto fetch saat buka menu tiket: %s", e)
|
_logger.warning("Auto fetch saat buka menu tiket: %s", e)
|
||||||
|
|
||||||
action = self.env['ir.actions.act_window']._for_xml_id('freshservice_ticket_audit.action_freshservice_ticket')
|
action = self.env['ir.actions.act_window']._for_xml_id('gmn_freshservice.action_freshservice_ticket')
|
||||||
param_obj = self.env['ir.config_parameter'].sudo()
|
param_obj = self.env['ir.config_parameter'].sudo()
|
||||||
try:
|
try:
|
||||||
sync_limit = int(param_obj.get_param('freshservice_ticket_audit.freshservice_sync_limit', 200) or 200)
|
sync_limit = int(param_obj.get_param('gmn_freshservice.freshservice_sync_limit', 200) or 200)
|
||||||
if sync_limit > 0:
|
if sync_limit > 0:
|
||||||
action['limit'] = sync_limit
|
action['limit'] = sync_limit
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -8,24 +8,25 @@ class ResConfigSettings(models.TransientModel):
|
|||||||
|
|
||||||
freshservice_url = fields.Char(
|
freshservice_url = fields.Char(
|
||||||
string='Freshservice URL',
|
string='Freshservice URL',
|
||||||
config_parameter='freshservice_ticket_audit.freshservice_url',
|
config_parameter='gmn_freshservice.freshservice_url',
|
||||||
default='https://ptglobalmultimedianetwork.freshservice.com'
|
default='https://ptglobalmultimedianetwork.freshservice.com'
|
||||||
)
|
)
|
||||||
freshservice_api_key = fields.Char(
|
freshservice_api_key = fields.Char(
|
||||||
string='Freshservice API Key',
|
string='Freshservice API Key',
|
||||||
config_parameter='freshservice_ticket_audit.freshservice_api_key'
|
config_parameter='gmn_freshservice.freshservice_api_key'
|
||||||
)
|
)
|
||||||
freshservice_sync_limit = fields.Integer(
|
freshservice_sync_limit = fields.Integer(
|
||||||
string='Limit Sinkronisasi Tiket',
|
string='Limit Sinkronisasi Tiket',
|
||||||
config_parameter='freshservice_ticket_audit.freshservice_sync_limit',
|
config_parameter='gmn_freshservice.freshservice_sync_limit',
|
||||||
default=200,
|
default=200,
|
||||||
help='Batas jumlah tiket terbaru yang ditarik dari Freshservice saat membuka halaman list tiket (default 200).'
|
help='Batas jumlah tiket terbaru yang ditarik dari Freshservice saat membuka halaman list tiket (default 200).'
|
||||||
)
|
)
|
||||||
freshservice_use_mock = fields.Boolean(
|
freshservice_use_mock = fields.Boolean(
|
||||||
string='Gunakan Mock Data Engine',
|
string='Gunakan Mock Data Engine',
|
||||||
config_parameter='freshservice_ticket_audit.freshservice_use_mock',
|
config_parameter='gmn_freshservice.freshservice_use_mock',
|
||||||
# default=True,
|
default=False,
|
||||||
help='Jika dicentang, sistem menggunakan simulasi format JSON resmi Freshservice tanpa perlu API Key asli.'
|
readonly=True,
|
||||||
|
help='Mode mock dinonaktifkan. Integrasi selalu menggunakan Live Freshservice API.'
|
||||||
)
|
)
|
||||||
|
|
||||||
def action_test_freshservice_connection(self):
|
def action_test_freshservice_connection(self):
|
||||||
|
|||||||
Binary file not shown.
@@ -31,10 +31,10 @@ class FreshserviceClient:
|
|||||||
_agent_cache = {}
|
_agent_cache = {}
|
||||||
_dept_cache = {}
|
_dept_cache = {}
|
||||||
|
|
||||||
def __init__(self, base_url="https://servicedesk.lyman.co.id", api_key="", use_mock=True):
|
def __init__(self, base_url="https://servicedesk.lyman.co.id", api_key="", use_mock=False):
|
||||||
self.base_url = (base_url or "").rstrip("/")
|
self.base_url = (base_url or "").rstrip("/")
|
||||||
self.api_key = api_key or ""
|
self.api_key = api_key or ""
|
||||||
self.use_mock = use_mock
|
self.use_mock = False
|
||||||
|
|
||||||
def get_agent_name(self, agent_id):
|
def get_agent_name(self, agent_id):
|
||||||
"""Ambil nama agen IT Freshservice berdasarkan ID (dengan in-memory cache)"""
|
"""Ambil nama agen IT Freshservice berdasarkan ID (dengan in-memory cache)"""
|
||||||
|
|||||||
@@ -13,9 +13,10 @@
|
|||||||
<field name="stage_name" widget="badge"/>
|
<field name="stage_name" widget="badge"/>
|
||||||
<field name="responsibility"
|
<field name="responsibility"
|
||||||
widget="badge"
|
widget="badge"
|
||||||
decoration-danger="responsibility == 'internal'"
|
decoration-danger="responsibility in ('internal','on_hold','internal_discussion')"
|
||||||
decoration-warning="responsibility == 'customer'"
|
decoration-warning="responsibility == 'customer'"
|
||||||
decoration-muted="responsibility in ('on_hold','third_party','internal_discussion','scheduled_meeting','external_activity','resolved_idle')"/>
|
decoration-info="responsibility == 'scheduled_meeting'"
|
||||||
|
decoration-muted="responsibility in ('third_party','external_activity','resolved_idle')"/>
|
||||||
<field name="actor_name"/>
|
<field name="actor_name"/>
|
||||||
<field name="actor_type"/>
|
<field name="actor_type"/>
|
||||||
<field name="start_date" string="Waktu Mulai (UTC)"/>
|
<field name="start_date" string="Waktu Mulai (UTC)"/>
|
||||||
@@ -75,11 +76,11 @@
|
|||||||
<field name="stage_name"/>
|
<field name="stage_name"/>
|
||||||
<field name="actor_name"/>
|
<field name="actor_name"/>
|
||||||
<filter name="filter_internal" string="Waiting on Us (Internal)" domain="[('responsibility', '=', 'internal')]"/>
|
<filter name="filter_internal" string="Waiting on Us (Internal)" domain="[('responsibility', '=', 'internal')]"/>
|
||||||
<filter name="filter_customer" string="Waiting on User (Customer)" domain="[('responsibility', '=', 'customer')]"/>
|
<filter name="filter_customer" string="Waiting on User (Requester)" domain="[('responsibility', '=', 'customer')]"/>
|
||||||
<filter name="filter_on_hold" string="On Hold (SLA Paused)" domain="[('responsibility', '=', 'on_hold')]"/>
|
<filter name="filter_on_hold" string="On Hold (SLA Paused)" domain="[('responsibility', '=', 'on_hold')]"/>
|
||||||
<filter name="filter_third_party" string="Waiting on Third Party (SLA Paused)" domain="[('responsibility', '=', 'third_party')]"/>
|
<filter name="filter_third_party" string="Waiting on Third Party (SLA Paused)" domain="[('responsibility', '=', 'third_party')]"/>
|
||||||
<filter name="filter_internal_discussion" string="Internal Discussion (SLA Paused)" domain="[('responsibility', '=', 'internal_discussion')]"/>
|
<filter name="filter_internal_discussion" string="Internal Discussion (SLA Paused)" domain="[('responsibility', '=', 'internal_discussion')]"/>
|
||||||
<filter name="filter_scheduled_meeting" string="Tunggu Bersama (Scheduled Meeting)" domain="[('responsibility', '=', 'scheduled_meeting')]"/>
|
<filter name="filter_scheduled_meeting" string="Scheduled Meeting (Waiting Together)" domain="[('responsibility', '=', 'scheduled_meeting')]"/>
|
||||||
<filter name="filter_external_activity" string="External Activity (SLA Paused)" domain="[('responsibility', '=', 'external_activity')]"/>
|
<filter name="filter_external_activity" string="External Activity (SLA Paused)" domain="[('responsibility', '=', 'external_activity')]"/>
|
||||||
<filter name="filter_resolved_idle" string="Resolved (Ticket Closed/Idle)" domain="[('responsibility', '=', 'resolved_idle')]"/>
|
<filter name="filter_resolved_idle" string="Resolved (Ticket Closed/Idle)" domain="[('responsibility', '=', 'resolved_idle')]"/>
|
||||||
<filter name="filter_sla_paused" string="Semua SLA Paused" domain="[('responsibility', 'in', ['on_hold','third_party','internal_discussion','scheduled_meeting','external_activity'])]"/>
|
<filter name="filter_sla_paused" string="Semua SLA Paused" domain="[('responsibility', 'in', ['on_hold','third_party','internal_discussion','scheduled_meeting','external_activity'])]"/>
|
||||||
|
|||||||
@@ -17,21 +17,10 @@
|
|||||||
string="Laporan Bottleneck (Excel)"
|
string="Laporan Bottleneck (Excel)"
|
||||||
class="btn-outline-success"
|
class="btn-outline-success"
|
||||||
icon="fa-file-excel-o"/>
|
icon="fa-file-excel-o"/>
|
||||||
<!-- <button name="action_assign_to_me"
|
|
||||||
type="object"
|
|
||||||
string="Tugaskan ke Saya"
|
|
||||||
class="btn-outline-primary"
|
|
||||||
icon="fa-user-check"/>
|
|
||||||
<button name="action_toggle_mock_mode"
|
|
||||||
type="object"
|
|
||||||
string="Ganti Mode (Mock / Live)"
|
|
||||||
class="btn-secondary"
|
|
||||||
icon="fa-exchange"/> -->
|
|
||||||
<field name="status" widget="statusbar" statusbar_visible="open,pending,resolved,closed"/>
|
<field name="status" widget="statusbar" statusbar_visible="open,pending,resolved,closed"/>
|
||||||
</header>
|
</header>
|
||||||
<sheet>
|
<sheet>
|
||||||
<field name="is_detail_synced" invisible="1"/>
|
<field name="is_detail_synced" invisible="1"/>
|
||||||
<widget name="web_ribbon" title="SIMULASI MOCK DATA" bg_color="text-bg-info" invisible="not is_mock_data"/>
|
|
||||||
|
|
||||||
<div class="oe_title">
|
<div class="oe_title">
|
||||||
<label for="name" string="Judul / Masalah Tiket"/>
|
<label for="name" string="Judul / Masalah Tiket"/>
|
||||||
@@ -43,47 +32,53 @@
|
|||||||
<field name="current_responsibility" widget="badge"
|
<field name="current_responsibility" widget="badge"
|
||||||
decoration-danger="current_responsibility == 'internal'"
|
decoration-danger="current_responsibility == 'internal'"
|
||||||
decoration-warning="current_responsibility == 'customer'"
|
decoration-warning="current_responsibility == 'customer'"
|
||||||
|
decoration-info="current_responsibility == 'scheduled_meeting'"
|
||||||
|
decoration-muted="current_responsibility == 'third_party'"
|
||||||
decoration-success="current_responsibility == 'closed'"/>
|
decoration-success="current_responsibility == 'closed'"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- KPI METRIC CARDS SECTION -->
|
<!-- KPI METRIC CARDS SECTION -->
|
||||||
<div class="row mt-4 mb-4">
|
<div class="row mt-4 mb-4">
|
||||||
<div class="col-md-3">
|
<div class="col-md-2">
|
||||||
<div class="card text-center p-3 shadow-sm border-primary">
|
<div class="card text-center p-3 shadow-sm border-primary">
|
||||||
<small class="text-muted fw-bold text-uppercase">First Response</small>
|
<small class="text-muted fw-bold text-uppercase">First Response</small>
|
||||||
<h4 class="mb-0 text-primary mt-1">
|
<h4 class="mb-0 text-primary mt-1"><field name="first_response_time_display" readonly="1"/></h4>
|
||||||
<field name="first_response_time_display" readonly="1"/>
|
<small class="text-secondary mt-1">Oleh: <field name="first_responder_name" readonly="1"/></small>
|
||||||
</h4>
|
|
||||||
<small class="text-secondary mt-1">
|
|
||||||
Oleh: <field name="first_responder_name" readonly="1"/>
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-2">
|
||||||
<div class="card text-center p-3 shadow-sm border-danger">
|
<div class="card text-center p-3 shadow-sm border-danger">
|
||||||
<small class="text-muted fw-bold text-uppercase">Agent</small>
|
<small class="text-muted fw-bold text-uppercase">Agent</small>
|
||||||
<h4 class="mb-0 text-danger mt-1">
|
<h4 class="mb-0 text-danger mt-1"><field name="total_agent_time_display" readonly="1"/></h4>
|
||||||
<field name="total_agent_time_display" readonly="1"/>
|
|
||||||
</h4>
|
|
||||||
<small class="text-secondary mt-1">Waiting on Us</small>
|
<small class="text-secondary mt-1">Waiting on Us</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-2">
|
||||||
<div class="card text-center p-3 shadow-sm border-warning">
|
<div class="card text-center p-3 shadow-sm border-warning">
|
||||||
<small class="text-muted fw-bold text-uppercase">User</small>
|
<small class="text-muted fw-bold text-uppercase">User</small>
|
||||||
<h4 class="mb-0 text-warning mt-1">
|
<h4 class="mb-0 text-warning mt-1"><field name="total_customer_time_display" readonly="1"/></h4>
|
||||||
<field name="total_customer_time_display" readonly="1"/>
|
|
||||||
</h4>
|
|
||||||
<small class="text-secondary mt-1">Waiting on Customer</small>
|
<small class="text-secondary mt-1">Waiting on Customer</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-2">
|
||||||
|
<div class="card text-center p-3 shadow-sm border-info">
|
||||||
|
<small class="text-muted fw-bold text-uppercase">Scheduled Meeting</small>
|
||||||
|
<h4 class="mb-0 text-info mt-1"><field name="shared_wait_time_display" readonly="1"/></h4>
|
||||||
|
<small class="text-secondary mt-1">Waiting Together</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="card text-center p-3 shadow-sm border-secondary">
|
||||||
|
<small class="text-muted fw-bold text-uppercase">Third Party</small>
|
||||||
|
<h4 class="mb-0 text-secondary mt-1"><field name="third_party_time_display" readonly="1"/></h4>
|
||||||
|
<small class="text-secondary mt-1">Waiting on Vendor</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
<div class="card text-center p-3 shadow-sm border-success">
|
<div class="card text-center p-3 shadow-sm border-success">
|
||||||
<small class="text-muted fw-bold text-uppercase">Waktu Selesai</small>
|
<small class="text-muted fw-bold text-uppercase">Waktu Selesai</small>
|
||||||
<h4 class="mb-0 text-success mt-1">
|
<h4 class="mb-0 text-success mt-1"><field name="resolution_time_display" readonly="1"/></h4>
|
||||||
<field name="resolution_time_display" readonly="1"/>
|
|
||||||
</h4>
|
|
||||||
<small class="text-secondary mt-1">Total Resolution</small>
|
<small class="text-secondary mt-1">Total Resolution</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,9 +110,10 @@
|
|||||||
decoration-muted="not is_active or responsibility in ('on_hold','third_party','internal_discussion','scheduled_meeting','external_activity','resolved_idle')">
|
decoration-muted="not is_active or responsibility in ('on_hold','third_party','internal_discussion','scheduled_meeting','external_activity','resolved_idle')">
|
||||||
<field name="stage_name" widget="badge"/>
|
<field name="stage_name" widget="badge"/>
|
||||||
<field name="responsibility" widget="badge"
|
<field name="responsibility" widget="badge"
|
||||||
decoration-danger="responsibility == 'internal'"
|
decoration-danger="responsibility in ('internal','on_hold','internal_discussion')"
|
||||||
decoration-warning="responsibility == 'customer'"
|
decoration-warning="responsibility == 'customer'"
|
||||||
decoration-muted="responsibility in ('on_hold','third_party','internal_discussion','scheduled_meeting','external_activity','resolved_idle')"/>
|
decoration-info="responsibility == 'scheduled_meeting'"
|
||||||
|
decoration-muted="responsibility in ('third_party','external_activity','resolved_idle')"/>
|
||||||
<field name="actor_name"/>
|
<field name="actor_name"/>
|
||||||
<field name="actor_type"/>
|
<field name="actor_type"/>
|
||||||
<field name="start_date" string="Waktu Mulai (UTC)"/>
|
<field name="start_date" string="Waktu Mulai (UTC)"/>
|
||||||
@@ -159,7 +155,11 @@
|
|||||||
<field name="first_response_time_days" readonly="1"/>
|
<field name="first_response_time_days" readonly="1"/>
|
||||||
<field name="total_agent_time_days" readonly="1"/>
|
<field name="total_agent_time_days" readonly="1"/>
|
||||||
<field name="total_customer_time_days" readonly="1"/>
|
<field name="total_customer_time_days" readonly="1"/>
|
||||||
|
<field name="shared_wait_time_days" readonly="1"/>
|
||||||
|
<field name="third_party_time_days" readonly="1"/>
|
||||||
<field name="resolution_time_days" readonly="1"/>
|
<field name="resolution_time_days" readonly="1"/>
|
||||||
|
<field name="shared_wait_time_hours" invisible="1"/>
|
||||||
|
<field name="third_party_time_hours" invisible="1"/>
|
||||||
<!-- Field jam disembunyikan (hide) -->
|
<!-- Field jam disembunyikan (hide) -->
|
||||||
<field name="first_response_time_hours" invisible="1"/>
|
<field name="first_response_time_hours" invisible="1"/>
|
||||||
<field name="total_agent_time_hours" invisible="1"/>
|
<field name="total_agent_time_hours" invisible="1"/>
|
||||||
@@ -204,6 +204,8 @@
|
|||||||
decoration-success="current_responsibility == 'closed'"/>
|
decoration-success="current_responsibility == 'closed'"/>
|
||||||
<field name="total_agent_time_display"/>
|
<field name="total_agent_time_display"/>
|
||||||
<field name="total_customer_time_display"/>
|
<field name="total_customer_time_display"/>
|
||||||
|
<field name="shared_wait_time_display" optional="show"/>
|
||||||
|
<field name="third_party_time_display" optional="show"/>
|
||||||
<field name="first_response_time_display"/>
|
<field name="first_response_time_display"/>
|
||||||
<field name="responder_name"/>
|
<field name="responder_name"/>
|
||||||
<field name="requester_name"/>
|
<field name="requester_name"/>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//form" position="inside">
|
<xpath expr="//form" position="inside">
|
||||||
<app data-string="Freshservice Audit" string="Freshservice Audit" name="freshservice_ticket_audit">
|
<app data-string="Freshservice Audit" string="Freshservice Audit" name="gmn_freshservice">
|
||||||
<block title="Konfigurasi Integrasi Freshservice" name="freshservice_settings_block">
|
<block title="Konfigurasi Integrasi Freshservice" name="freshservice_settings_block">
|
||||||
<setting id="freshservice_url_setting" help="URL Portal Freshservice perusahaan Anda">
|
<setting id="freshservice_url_setting" help="URL Portal Freshservice perusahaan Anda">
|
||||||
<field name="freshservice_url"/>
|
<field name="freshservice_url"/>
|
||||||
@@ -21,12 +21,11 @@
|
|||||||
<field name="freshservice_sync_limit"/>
|
<field name="freshservice_sync_limit"/>
|
||||||
</setting>
|
</setting>
|
||||||
|
|
||||||
<setting id="freshservice_mock_setting" help="Gunakan mock data sesuai format resmi Freshservice API untuk testing tanpa API Key asli">
|
<setting id="freshservice_connection_setting" help="Uji koneksi ke Live Freshservice API">
|
||||||
<field name="freshservice_use_mock"/>
|
|
||||||
<div class="mt8">
|
<div class="mt8">
|
||||||
<button name="action_test_freshservice_connection"
|
<button name="action_test_freshservice_connection"
|
||||||
type="object"
|
type="object"
|
||||||
string="Tes Koneksi / Cek Status"
|
string="Tes Koneksi Live API"
|
||||||
class="btn-primary"
|
class="btn-primary"
|
||||||
icon="fa-plug"/>
|
icon="fa-plug"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,6 +41,6 @@
|
|||||||
<field name="res_model">res.config.settings</field>
|
<field name="res_model">res.config.settings</field>
|
||||||
<field name="view_mode">form</field>
|
<field name="view_mode">form</field>
|
||||||
<field name="target">main</field>
|
<field name="target">main</field>
|
||||||
<field name="context">{'module': 'freshservice_ticket_audit'}</field>
|
<field name="context">{'module': 'gmn_freshservice'}</field>
|
||||||
</record>
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
Binary file not shown.
@@ -207,6 +207,11 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
|||||||
'bg_color': '#FCF3CF', 'font_color': '#7D6608', 'border': 1,
|
'bg_color': '#FCF3CF', 'font_color': '#7D6608', 'border': 1,
|
||||||
'border_color': '#F9E79F', 'align': 'center', 'valign': 'vcenter'
|
'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({
|
kpi_box_label = workbook.add_format({
|
||||||
'font_size': 9, 'font_name': 'Calibri', 'bold': True,
|
'font_size': 9, 'font_name': 'Calibri', 'bold': True,
|
||||||
'bg_color': '#EBF5FB', 'font_color': '#2874A6', 'border': 1, 'border_color': '#AED6F1'
|
'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(
|
summary_html = self._build_single_ticket_sheets(
|
||||||
workbook, t, title_fmt, subtitle_fmt, section_fmt, th_fmt,
|
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,
|
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:
|
else:
|
||||||
summary_html = self._build_ranking_sheets(
|
summary_html = self._build_ranking_sheets(
|
||||||
workbook, tickets, title_fmt, subtitle_fmt, section_fmt, th_fmt,
|
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,
|
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()
|
workbook.close()
|
||||||
@@ -309,7 +316,8 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
|||||||
def _build_single_ticket_sheets(
|
def _build_single_ticket_sheets(
|
||||||
self, workbook, t, title_fmt, subtitle_fmt, section_fmt, th_fmt,
|
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,
|
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."""
|
"""Membuat sheet untuk analisis mendalam bottleneck tiket tunggal."""
|
||||||
ws1 = workbook.add_worksheet('Ringkasan & Bottleneck KPI')
|
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',
|
'bg_color': '#FFFFFF', 'font_color': '#145A32', 'border': 1, 'border_color': '#A9DFBF',
|
||||||
'align': 'center', 'valign': 'vcenter'
|
'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('B13', 'First Response', kpi_wh_label)
|
||||||
ws1.write('C13', 'Total Waktu', kpi_wh_label)
|
ws1.write('C13', 'Total Waktu', kpi_wh_label)
|
||||||
ws1.write('D13', 'Waiting on Us (IT/Agent)', kpi_wh_label)
|
ws1.write('D13', 'Waiting on Us (IT/Agent)', kpi_internal_label)
|
||||||
ws1.write('E13', 'Waiting on User (Pengguna)', kpi_wh_label)
|
ws1.write('E13', 'Waiting on User (Requester)', kpi_customer_label)
|
||||||
ws1.write('F13', 'Total SLA Paused', kpi_wh_label)
|
ws1.write('F13', 'Scheduled Meeting', kpi_meeting_label)
|
||||||
ws1.write('G13', 'Resolved / Idle', kpi_wh_label)
|
ws1.write('G13', 'Third Party', kpi_third_party_label)
|
||||||
ws1.write('H13', 'Catatan Rumus', kpi_wh_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)
|
# 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())
|
wh_total = sum(stage_wh_map.values())
|
||||||
ws1.write('B14', format_working_duration(wh_fr), kpi_box_val)
|
ws1.write('B14', format_working_duration(wh_fr), kpi_box_val)
|
||||||
ws1.write('C14', format_working_duration(wh_total), 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('D14', format_working_duration(wh_agent), kpi_internal_val)
|
||||||
ws1.write('E14', format_working_duration(wh_cust), kpi_box_val)
|
ws1.write('E14', format_working_duration(wh_cust), kpi_customer_val)
|
||||||
ws1.write('F14', format_working_duration(wh_paused), kpi_box_val)
|
ws1.write('F14', format_working_duration(wh_meeting), kpi_meeting_val)
|
||||||
ws1.write('G14', format_working_duration(wh_resolved_idle), kpi_box_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)
|
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.
|
# REKAPITULASI BEBAN TANGGUNG JAWAB — harus identik dengan total Sheet 2.
|
||||||
@@ -466,15 +512,17 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
|||||||
recap_rows = [
|
recap_rows = [
|
||||||
('Waiting on Us (IT - Pengerjaan Aktif)', 'Waktu penanganan langsung oleh Tim IT (Open / In Progress).', wh_it_direct),
|
('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 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),
|
('Waiting on User (Requester)', '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),
|
('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),
|
('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),
|
('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),
|
('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),
|
('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):
|
# xlsxwriter memakai indeks baris 0-based: start=17 berarti Excel row 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)))
|
# 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.write(recap_row, 1, label, label_fmt)
|
||||||
ws1.merge_range(recap_row, 2, recap_row, 5, description, td_fmt)
|
ws1.merge_range(recap_row, 2, recap_row, 5, description, td_fmt)
|
||||||
ws1.write(recap_row, 6, hours, td_dec_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)
|
ws1.write(recap_row, 8, (hours / wh_total) if wh_total else 0.0, td_pct_fmt)
|
||||||
|
|
||||||
# TABEL RANKING BOTTLENECK PER STAGE
|
# TABEL RANKING BOTTLENECK PER STAGE
|
||||||
# Rekapitulasi berakhir di baris Excel 26 (grand total), sehingga
|
# Rekap berakhir di Excel row 25. Row 26 adalah pemisah.
|
||||||
# ranking dimulai di baris 28 agar tidak overlap dengan merge C25:F25.
|
# Title di Excel row 27 (index 26), header di Excel row 28 (index 27),
|
||||||
ws1.merge_range('B28:I28', 'RANKING BOTTLENECK PER STAGE (Diurutkan dari Tahapan Paling Lama)', section_fmt)
|
# 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']
|
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):
|
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)
|
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)
|
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_text = "Tidak terdeteksi delay signifikan."
|
||||||
primary_bn_actor = "-"
|
primary_bn_actor = "-"
|
||||||
primary_bn_stage = "-"
|
primary_bn_stage = "-"
|
||||||
@@ -508,6 +557,7 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
|||||||
resp_fmt = bn_badge_fmt
|
resp_fmt = bn_badge_fmt
|
||||||
elif stage.responsibility == 'customer':
|
elif stage.responsibility == 'customer':
|
||||||
resp_label = 'Waiting on User (Requester)'
|
resp_label = 'Waiting on User (Requester)'
|
||||||
|
# Customer selalu orange/kuning, bukan merah.
|
||||||
resp_fmt = normal_badge_fmt
|
resp_fmt = normal_badge_fmt
|
||||||
elif stage.responsibility == 'on_hold':
|
elif stage.responsibility == 'on_hold':
|
||||||
resp_label = 'On Hold (Internal Hold / Waiting on Us)'
|
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_label = 'Waiting on Internal Discussion (Internal Hold / Waiting on Us)'
|
||||||
resp_fmt = bn_badge_fmt
|
resp_fmt = bn_badge_fmt
|
||||||
elif stage.responsibility == 'scheduled_meeting':
|
elif stage.responsibility == 'scheduled_meeting':
|
||||||
resp_label = 'Tunggu Bersama (Scheduled Meeting - SLA is Paused)'
|
resp_label = 'Scheduled Meeting (Waiting Together - SLA is Paused)'
|
||||||
resp_fmt = sla_paused_fmt or td_center_fmt
|
resp_fmt = scheduled_meeting_fmt
|
||||||
elif stage.responsibility == 'external_activity':
|
elif stage.responsibility == 'external_activity':
|
||||||
resp_label = 'External Activity (SLA is Paused)'
|
resp_label = 'External Activity (SLA is Paused)'
|
||||||
resp_fmt = sla_paused_fmt or td_center_fmt
|
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
|
resp_fmt = sla_paused_fmt or td_center_fmt
|
||||||
is_primary = (rank_idx == 1 and stage_wh > 0.0)
|
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, 2, stage.stage_name or '-', td_fmt)
|
||||||
ws1.write(row, 3, resp_label, resp_fmt)
|
ws1.write(row, 3, resp_label, resp_fmt)
|
||||||
ws1.write(row, 4, stage.actor_name or '-', td_fmt)
|
ws1.write(row, 4, stage.actor_name or '-', td_fmt)
|
||||||
@@ -579,7 +630,7 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
|||||||
if log.responsibility == 'internal':
|
if log.responsibility == 'internal':
|
||||||
resp_label = 'Waiting on Us (Internal)'
|
resp_label = 'Waiting on Us (Internal)'
|
||||||
elif log.responsibility == 'customer':
|
elif log.responsibility == 'customer':
|
||||||
resp_label = 'Waiting on User (Customer)'
|
resp_label = 'Waiting on User (Requester)'
|
||||||
elif log.responsibility == 'on_hold':
|
elif log.responsibility == 'on_hold':
|
||||||
resp_label = 'On Hold (Internal Hold / Waiting on Us)'
|
resp_label = 'On Hold (Internal Hold / Waiting on Us)'
|
||||||
elif log.responsibility == 'third_party':
|
elif log.responsibility == 'third_party':
|
||||||
@@ -587,9 +638,9 @@ class FreshserviceTicketBottleneckWizard(models.TransientModel):
|
|||||||
elif log.responsibility == 'internal_discussion':
|
elif log.responsibility == 'internal_discussion':
|
||||||
resp_label = 'Waiting on Internal Discussion (Internal Hold / Waiting on Us)'
|
resp_label = 'Waiting on Internal Discussion (Internal Hold / Waiting on Us)'
|
||||||
elif log.responsibility == 'scheduled_meeting':
|
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':
|
elif log.responsibility == 'external_activity':
|
||||||
resp_label = 'External Activity (SLA is Paused)'
|
resp_label = 'External Activity (SLA is Paused)'
|
||||||
else:
|
else:
|
||||||
resp_label = 'Resolved (Ticket Closed/Idle)'
|
resp_label = 'Resolved (Ticket Closed/Idle)'
|
||||||
msg_body = self._get_stage_conversation_body(log, t)
|
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, 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, 3, _to_wib_str(log.end_date), 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, 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, 6, log.actor_type or '-', td_center_fmt)
|
||||||
ws2.write(row2, 7, log.actor_name or '-', td_fmt)
|
ws2.write(row2, 7, log.actor_name or '-', td_fmt)
|
||||||
ws2.write(row2, 8, wd_eff, td_dec_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)'
|
resp_label = 'On Hold (SLA is Paused)'
|
||||||
elif log.responsibility == 'third_party':
|
elif log.responsibility == 'third_party':
|
||||||
resp_label = 'Waiting on Third Party (SLA is Paused)'
|
resp_label = 'Waiting on Third Party (SLA is Paused)'
|
||||||
|
elif log.responsibility == 'scheduled_meeting':
|
||||||
|
resp_label = 'Scheduled Meeting (Waiting Together - SLA is Paused)'
|
||||||
else:
|
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)
|
msg_body = self._get_stage_conversation_body(log, t)
|
||||||
raw_act_main = log.activity_content or ''
|
raw_act_main = log.activity_content or ''
|
||||||
if 'Isi Pesan:' in raw_act_main:
|
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, 2, _wib(log.start_date), td_center_fmt)
|
||||||
ws2.write(row2, 3, _wib(log.end_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, 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, 6, log.actor_name or '-', td_fmt)
|
||||||
ws2.write(row2, 7, calc_log_days, td_num_fmt)
|
ws2.write(row2, 7, calc_log_days, td_num_fmt)
|
||||||
ws2.write(row2, 8, log.duration_hours, td_dec_fmt)
|
ws2.write(row2, 8, log.duration_hours, td_dec_fmt)
|
||||||
|
|||||||
Reference in New Issue
Block a user