From 62e545276f861676e63645a5c130cf47713febeb Mon Sep 17 00:00:00 2001 From: Madanil Date: Mon, 19 Oct 2026 16:46:27 +0700 Subject: [PATCH] [UPD] update logic tiket --- ...rvice_ticket_stage_log_v01.cpython-312.pyc | Bin 20085 -> 21118 bytes ...mn_freshservice_ticket_v01.cpython-312.pyc | Bin 48309 -> 52215 bytes .../gmn_freshservice_ticket_stage_log_v01.py | 39 +- .../models/gmn_freshservice_ticket_v01.py | 308 +++++++---- ...reshservice_ticket_stage_log_views_v01.xml | 34 +- .../gmn_freshservice_ticket_views_v01.xml | 16 +- ...cket_bottleneck_wizard_v01.cpython-312.pyc | Bin 48086 -> 52150 bytes ...eshservice_ticket_bottleneck_wizard_v01.py | 515 ++++++++++-------- 8 files changed, 574 insertions(+), 338 deletions(-) diff --git a/gmn_freshservice/models/__pycache__/gmn_freshservice_ticket_stage_log_v01.cpython-312.pyc b/gmn_freshservice/models/__pycache__/gmn_freshservice_ticket_stage_log_v01.cpython-312.pyc index 090e26fbdbd0e58b0784174883611fd9d243bba9..fd0a2f87bbb8d8113f094d1b72e05f48af0c563e 100644 GIT binary patch delta 2199 zcmaJ>ZA@EL81C&#A=CnGp&zs>myf|Z7-cXvJ~mzXFqPgr_4%m zt1Hj4VqK<`xt7X+4OSVfM6W}}GxqeCHjeAq(#sTE#@bgL%eyjcOPZ}%N^PA^vWF|T zhxIvi%1IqJ*)!XZ4f-*AG+mUc9qv{;^dGD=do`E?ut0?-<9yCuG})_?tC!X2@&$t0kJcGegkD?w0KI z*@N%smNP>?<=bXdwz4hkq3qe%!y4AUZB_+gWj$ltG@SbBc3(%TT=}k}TzTAjCvN&o zpDN&Z31Oaz$d`0@>$(pXEdpI!^6-7?qp#)h< z!dZmn(vu7)ar`(f@boZxD~d!?n=l+v5#b;cCSO4&DhWZ1L?IH7A-PJUc#V+g0FdYa zA?+UqQVbEjl}CsbgE54;Fc)GZvajsfsT|XwL}L*{6NrsQ5F0#;5OgJ%k7QHyt0bk| zJDe9EnP4zteMDfACOFInrx+|v%XUrmQOMInkS8>=+Ix(Y<=6<4i!)uaw4Xjf53d zPmH8;0=aOM5hv-vb3DTMBSmQQ#!Gs`VU(Wih{Q-y(Wl2Bk{gDXNRUgrBMfOI>n+{I zh@9x7REAux&PD64I$I8G+eJ>K=cH7gdOJwX0a7ky00lt|yuhQBVL;&HNH+DL2s2H0 zGjJDf5?Z!{uRu~nT%8hlkvq*r$Wg#WO!yLFQw10?tiVU6eFgY3;o>V0K7nu*!sifH zAY6lRodnsoZ2&z;uFVkUSTZYE!ipXdqBw+POD-{tVjMb0uCO_m>LI(!MMI>E$tW2v zc^3kP1LGTH2jyDZQQP;hm1&flLUfvjZzzTBUmAaaK5i*Lwr^2u%FT{u>eT8V9givF z9mwBR9vvw&{tVI<@)Bzl`e-o~Tx~jP*=70_2=H%=e^dI8O=ShQj@1}%gP=hblmrT> zh~f(TOEoC2uJdvHEx}SHC_CYxs7^Rl8hU1`!P79+RDPErL=eMc_=8EASeQyM1XD>l z;ccEJ&npBER5Om0L&!L{6DSs9&1p}AqUveNqM@ce#r^6|xjZ8&OI-)XhDjEWK!^jL z4^Lp6)Jnov-pdhzk??(he}`}nLIT1%gaQ&$`7D}3JS*ZgNP(sJwsNT3tN&WBjCGI3 z&2`4ja$T|e(&+V$wb3oN+1^LZ3~YLJ&g$9N{Drv-iG4@bdXkP42}9|IyLREw#m>df zL}O3V-J38tH(YfK_KV&{Z^GY~boD0;_6=**Z0!SU)w;FniB4Z*d|2eZG`5wex9y+l z+cfIvriIvp3jcbAKhbtPF>>;aWX0&b=aHyEG+03)MvV5wL>FY_UfRK7rrw#V%v<2g zbN4ncxp{dT+0%NlK7EJ??wlA&IJDK@U(mR?T#m_Cu?&EGLm$%n%8tTR|Rh%Qb**;`ka^l z{!>hDC{xsWB+FG94d3j1wIar$~7b`JWZnRL}x72Od`d| ztq>|Gw}VoSLXNadw&qs2*)Uf-!y&xH(^T?=W29|~r@54s*DlLO3KiD@4>P%aqQcWM zx)-L1u3y{?*>Fk96$R2sTvEg~lJz9M=p{;-DppcOg=%q6q`gejHoi&O%)55JpYSW5 zyk9eH$_!O^*7Un496zCIwq~TTY2cqN1ysXX%QnX-bD9$FX^Dz5&{demP}M5j*lm>c4FQX4))ZJf|gnPL~)`bAoC0!h_KrLtn%1L}G8 zG`k_*xUzy>5r6EefP4j^BWhN+*>w>)fumGc&t&m77{O!;Dkv4NSnSH46g6yqwyV4K zK1i#QT884Hp20;@JjJs2_}^V~z6XL~)BKk`3uuaGx;s$s?9J|vDDnvS#{%-6^Jxoi zD8`tDfSu!0`*xv$*$?}I-m0g-z=z15ac?5yEBg3&VjY^{R}wqW3H~OL@WDrQ?=Q@ApQ^F zIlv2mmjHeNu&E3+h{dLWngf{O7yDW~pAo;-mu#-w=$o&TYFh8sPdl&b7v##uL3HuR ze4|tsoqcogFN7ZQk^bMB@?Z=Dz(eQ#d&W$-Lwni(v)+Lbf5SLJo31t9iCm9-lHgp> FeghHFXU_lt diff --git a/gmn_freshservice/models/__pycache__/gmn_freshservice_ticket_v01.cpython-312.pyc b/gmn_freshservice/models/__pycache__/gmn_freshservice_ticket_v01.cpython-312.pyc index 199746e635594a67b79103da737732564d3bcef7..9967e7dbe0c7d2a94057e0f0a0ed5ea55893f445 100644 GIT binary patch delta 14417 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 W7qdBx4LrK+k;I1*zhtnj@ctjn!U_oh diff --git a/gmn_freshservice/models/gmn_freshservice_ticket_stage_log_v01.py b/gmn_freshservice/models/gmn_freshservice_ticket_stage_log_v01.py index 1bc77b9..34f5c13 100644 --- a/gmn_freshservice/models/gmn_freshservice_ticket_stage_log_v01.py +++ b/gmn_freshservice/models/gmn_freshservice_ticket_stage_log_v01.py @@ -255,15 +255,36 @@ class FreshserviceTicketStageLog(models.Model): stage_name = fields.Char(string='Status / Stage', required=True) responsibility = fields.Selection([ - ('internal', 'Internal (Waiting on Us)'), - ('customer', 'Customer (Waiting on User)'), - ('on_hold', 'On Hold (SLA is Paused)'), - ('third_party', 'Waiting on Third Party (SLA is Paused)'), - ('internal_discussion', 'Waiting on Internal Discussion (SLA is Paused)'), - ('scheduled_meeting', 'Scheduled Meeting (Waiting Together - SLA is Paused)'), - ('external_activity', 'External Activity (SLA is Paused)'), - ('resolved_idle', 'Resolved (Ticket Closed/Idle)'), - ], string='Tanggung Jawab', required=True, default='internal', index=True) + ('internal', 'Agent Active Work'), + ('it_hold', 'Agent Hold & Discussion'), + ('customer', 'Waiting on Requester'), + ('user_after_auto_resolve', 'Waiting on User After Auto-Resolve'), + ('needs_verification', 'Waiting on User Verification'), + ('scheduled_meeting', 'Scheduled Meeting (Shared)'), + ('third_party', 'Waiting on Third Party'), + ('resolved_idle', 'Resolved / Idle'), + # Legacy values are retained for historical audit snapshots. + ('on_hold', 'On Hold (Legacy)'), + ('internal_discussion', 'Internal Discussion (Legacy)'), + ('external_activity', 'External Activity (Legacy)'), + ], string='Task Owner', required=True, default='internal', index=True) + + effective_status = fields.Char(string='Effective Status', index=True) + classification_basis = fields.Char(string='Classification Basis') + needs_verification = fields.Boolean(string='User Verification Required') + delay_cause = fields.Selection([ + ('it', 'Agent'), ('user', 'User'), ('none', 'None'), + ], string='Delay Cause', default='none') + automatic_responsibility = fields.Selection(related='responsibility', readonly=True) + override_responsibility = fields.Selection([ + ('internal', 'Agent Active Work'), ('it_hold', 'Agent Hold & Discussion'), ('customer', 'Waiting on Requester'), + ('user_after_auto_resolve', 'Waiting on User After Auto-Resolve'), + ('needs_verification', 'Waiting on User Verification'), ('scheduled_meeting', 'Scheduled Meeting (Shared)'), + ('third_party', 'Waiting on Third Party'), ('resolved_idle', 'Resolved / Idle'), + ], string='Task Owner Override') + override_source = fields.Char(string='Override Source') + override_reviewer = fields.Char(string='Override Reviewer') + override_at = fields.Datetime(string='Override Date') actor_name = fields.Char(string='Pelaku / Actor') actor_type = fields.Selection([ diff --git a/gmn_freshservice/models/gmn_freshservice_ticket_v01.py b/gmn_freshservice/models/gmn_freshservice_ticket_v01.py index 4cdf098..5b4e76a 100644 --- a/gmn_freshservice/models/gmn_freshservice_ticket_v01.py +++ b/gmn_freshservice/models/gmn_freshservice_ticket_v01.py @@ -36,7 +36,7 @@ class FreshserviceTicket(models.Model): requester_name = fields.Char(string='Pemohon (User)', tracking=True) requester_email = fields.Char(string='Email Pemohon') - responder_name = fields.Char(string='Agen Penanggung Jawab (IT)', tracking=True) + responder_name = fields.Char(string='Agent Penanggung Jawab (IT)', tracking=True) department_name = fields.Char(string='Departemen') # ========================================== @@ -62,6 +62,10 @@ class FreshserviceTicket(models.Model): ticket_created_at = fields.Datetime(string='Waktu Dibuat', index=True) due_by = fields.Datetime(string='Batas Waktu (Due Date)') first_responded_at = fields.Datetime(string='Waktu First Response') + first_ack_at = fields.Datetime(string='Waktu First Acknowledgement', readonly=True) + first_meaningful_response_at = fields.Datetime( + string='Waktu First Meaningful Response', readonly=True, + ) resolved_at = fields.Datetime(string='Waktu Selesai (Resolved)') is_mock_data = fields.Boolean(string='Menggunakan Data Mock', default=False, readonly=True) @@ -112,24 +116,24 @@ class FreshserviceTicket(models.Model): digits=(12, 2) ) total_agent_time_display = fields.Char( - string='Agen (Waiting on Us)', + string='Agent Responsibility', compute='_compute_time_metrics', store=True ) total_customer_time_days = fields.Integer( - string='Total Waktu di User (Hari)', + string='Requester Time (Days)', compute='_compute_time_metrics', store=True ) total_customer_time_hours = fields.Float( - string='Total Waktu di User (Jam)', + string='Requester Time (Hours)', compute='_compute_time_metrics', store=True, digits=(12, 2) ) total_customer_time_display = fields.Char( - string='User (Waiting on User)', + string='Requester Responsibility', compute='_compute_time_metrics', store=True ) @@ -180,16 +184,16 @@ class FreshserviceTicket(models.Model): digits=(12, 2) ) resolution_time_display = fields.Char( - string='Total Waktu Selesai', + string='Total Resolution Time', compute='_compute_time_metrics', store=True ) current_responsibility = fields.Selection([ - ('internal', 'Waiting on Us (Internal)'), - ('customer', 'Waiting on User (Requester)'), - ('closed', 'Tiket Selesai'), - ], string='Status Tanggung Jawab Saat Ini', compute='_compute_time_metrics', store=True) + ('internal', 'Agent Active Work'), + ('customer', 'Waiting on Requester'), + ('closed', 'Completed'), + ], string='Current Responsibility', compute='_compute_time_metrics', store=True) # Relasi stage_log_ids = fields.One2many( @@ -224,11 +228,16 @@ class FreshserviceTicket(models.Model): rec.first_response_time_display = "Belum Ada Respon" # 2. Hitung total SLA berdasarkan jam kerja efektif, bukan durasi kalender. - # 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') - 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') + # Mengikuti Aturan Fair Responsibility: + # - IT Aktif / IT Hold / On Hold / Internal Discussion: Agen (Waiting on Us) + # - User / User Setelah Auto Resolve: Customer (Waiting on User) + # - Perlu Verifikasi: Dicatat tersendiri / dapat masuk rentang estimasi + # - Scheduled Meeting: Bersama + # - Third Party: Pihak Ketiga + agent_hours = sum(l.working_hours for l in rec.stage_log_ids if (l.override_responsibility or l.responsibility) in ('internal', 'it_hold', 'on_hold', 'internal_discussion')) + cust_hours = sum(l.working_hours for l in rec.stage_log_ids if (l.override_responsibility or l.responsibility) in ('customer', 'user_after_auto_resolve', 'needs_verification')) + shared_wait_hours = sum(l.working_hours for l in rec.stage_log_ids if (l.override_responsibility or l.responsibility) == 'scheduled_meeting') + third_party_hours = sum(l.working_hours for l in rec.stage_log_ids if (l.override_responsibility or l.responsibility) in ('third_party', 'external_activity')) agent_days = round(agent_hours / 8.0, 2) cust_days = round(cust_hours / 8.0, 2) shared_wait_days = round(shared_wait_hours / 8.0, 2) @@ -236,7 +245,7 @@ class FreshserviceTicket(models.Model): # 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) - resolved_idle_hours = sum(l.working_hours for l in rec.stage_log_ids if l.responsibility == 'resolved_idle') + resolved_idle_hours = sum(l.working_hours for l in rec.stage_log_ids if (l.override_responsibility or l.responsibility) == 'resolved_idle') rec.total_agent_time_hours = round(agent_hours, 2) rec.total_agent_time_days = round(agent_days) @@ -316,6 +325,28 @@ class FreshserviceTicket(models.Model): } } + def _ball_in_court_check(self, effective_status, last_msg, last_actor_type): + """ + Aturan B: Ball-in-court analysis. + Mengevaluasi apakah stage hold/waiting memiliki potensi misklasifikasi + karena Agent sedang menunggu konfirmasi user atau sebaliknya. + Mengembalikan (is_needs_verification, basis_snippet). + """ + if not last_msg: + return False, "" + msg_lower = last_msg.lower() + confirmation_keywords = [ + 'mohon konfirmasi', 'konfirmasi', 'mohon dicek', 'silakan dicek', + 'apakah sudah', 'apakah bisa', 'menunggu konfirmasi', 'apakah kendala', + 'mohon info', 'tolong konfirmasi', 'konfirmasinya' + ] + has_req = any(kw in msg_lower for kw in confirmation_keywords) or ('?' in last_msg and last_actor_type == 'Agent') + if has_req and (effective_status in ('Waiting on Internal Discussion', 'On Hold') or 'hold' in str(effective_status).lower()): + words = last_msg.strip().split() + snippet = ' '.join(words[:10]) + ('...' if len(words) > 10 else '') + return True, f"Agent meminta konfirmasi: \"{snippet}\"" + return False, "" + def action_sync_from_freshservice(self): """ Sinkronisasi data dari Freshservice (Mock Engine atau Live API sesuai konfigurasi). @@ -436,7 +467,13 @@ class FreshserviceTicket(models.Model): conv_by_time[c_time[:16]] = c # toleransi pencocokan hingga menit (YYYY-MM-DDTHH:MM) log_vals_list = [] + # Hanya perubahan status eksplisit yang mengubah state berikutnya. + effective_status = 'Open' current_state = 'internal' + last_public_message = False + last_message_actor_type = False + first_ack_dt = False + first_meaningful_dt = False meeting_wait_until = False meeting_start_dt = False meeting_waiting_for_followup = False @@ -551,59 +588,128 @@ class FreshserviceTicket(models.Model): else: stage_name = 'Aktivitas' - # 4. Tentukan Tanggung Jawab (Responsibility) dengan Sticky State Machine. - # Hanya perubahan Status eksplisit atau balasan Requester yang boleh mengubah state. + # 4. Tentukan Tanggung Jawab (Responsibility) — Fair Responsibility Classification + # ============================================================================== + # Aturan A: Hanya event "set Status as X" yang mengubah effective_status. + # Event non-status (Timesheet, Note, Assignment, Reply) MEWARISI status sebelumnya. + # Aturan B: Ball-in-court check untuk IT_HOLD / Waiting on End User. + # Deteksi 2 segmen PERLU_VERIFIKASI (Sep 18 09:38-Sep 21 14:05 & Oct 6 09:28-Oct 9 10:44). + # Aturan C: Resolved oleh sistem/auto-rule → USER_SETELAH_AUTO_RESOLVE. + # Aturan D: Scheduled Meeting → BERSAMA dengan penanda delay_cause. + # Aturan E: First response substantif terpisah dari auto-ack. + # ============================================================================== combined_text = f"{stage_name} {content_main} {' '.join(str(s) for s in sub_contents)}".lower() - is_timesheet = any(k in combined_text for k in ('timesheet', 'time spent', 'create timesheet')) + status_text = ' '.join(filter(None, (extracted_status or '', stage_name, content_main, *[str(s) for s in sub_contents]))).lower() + explicit_status = bool(re.search(r'\bset\s+status\s+as\b', status_text, re.I)) + vendor_external = bool(re.search(r'vendor\s+escalation\s+as\s+external|set\s+vendor\s+escalation\s+as\s+external', status_text)) + is_agent_assignment = bool(re.search(r'\bset\s+agent\s+as\b', status_text, re.I)) - if is_timesheet: - stage_name = 'Create Timesheet' - responsibility = 'external_activity' - else: - status_text = ' '.join(filter(None, (extracted_status or '', stage_name, content_main, *[str(s) for s in sub_contents]))).lower() - explicit_status = 'set status as' in status_text - is_requester_reopen_workflow = ( - actor_type == 'System' - and 'reopen tickets when the requester responds' in status_text - ) - requester_email = (self.requester_email or '').strip().lower() - is_agent_reply_to_requester = ( - actor_type.strip().lower() == 'agent' - and bool(re.search(r'replied\s+to', status_text, re.I)) - ) - is_agent_assignment = bool(re.search(r'\bset\s+agent\s+as\b', status_text, re.I)) - vendor_external = bool(re.search(r'vendor\s+escalation\s+as\s+external|set\s+vendor\s+escalation\s+as\s+external', status_text)) - confirmation_to_user = bool(re.search(r'(mohon|please|harap|kindly).{0,100}(konfirmasi|confirmation|keputusan|decision|approve|persetujuan)', status_text, re.I)) - if is_agent_assignment: - # Penugasan/perubahan agent mengalihkan tanggung jawab kembali ke internal (Waiting on Us). - current_state = 'internal' - elif vendor_external or (explicit_status and 'waiting on third party' in status_text): - # Vendor Escalation=External adalah trigger resmi meski bukan field Status. - current_state = 'third_party' - elif explicit_status and 'waiting on internal discussion' in status_text: - current_state = 'internal_discussion' - elif explicit_status and any(x in status_text for x in ('waiting on end user', 'waiting on user', 'set status as pending')): - current_state = 'customer' - elif explicit_status and any(x in status_text for x in ('on hold', 'on-hold')): - current_state = 'on_hold' - elif explicit_status and any(x in status_text for x in ('set status as resolved', 'set status as closed')): + # Update pesan terakhir jika ada balasan/catatan publik + if matched_msg: + last_public_message = matched_msg + last_message_actor_type = actor_type + elif 'replied' in content_main.lower() or 'reply' in content_main.lower(): + last_public_message = content_main + last_message_actor_type = actor_type + + # Rekam First Response (Aturan E) + if actor_type == 'Agent' and ('replied' in content_main.lower() or 'reply' in content_main.lower()): + is_auto_ack = any(k in combined_text for k in ('mohon menunggu', 'auto reply', 'automated response', 'thank you for contacting')) + if not first_ack_dt: + first_ack_dt = start_dt_obj + if not is_auto_ack and not first_meaningful_dt: + first_meaningful_dt = start_dt_obj + + classification_basis = "Status warisan: %s" % effective_status + needs_verification_flag = False + delay_cause = 'none' + + # Update effective_status hanya jika ada perubahan eksplisit + if vendor_external or (explicit_status and 'waiting on third party' in status_text): + effective_status = 'Waiting on Third Party' + current_state = 'third_party' + classification_basis = 'Status Freshservice: Waiting on Third Party' + elif explicit_status and 'waiting on internal discussion' in status_text: + effective_status = 'Waiting on Internal Discussion' + current_state = 'it_hold' + classification_basis = 'Status Freshservice: Waiting on Internal Discussion' + elif explicit_status and any(x in status_text for x in ('waiting on end user', 'waiting on user', 'set status as pending')): + effective_status = 'Waiting on End User' + current_state = 'customer' + classification_basis = 'Status Freshservice: Waiting on End User' + elif explicit_status and any(x in status_text for x in ('on hold', 'on-hold')): + effective_status = 'On Hold' + current_state = 'it_hold' + classification_basis = 'Status Freshservice: On Hold' + elif explicit_status and any(x in status_text for x in ('set status as resolved', 'set status as closed')): + is_auto_resolve = actor_type == 'System' or 'auto-resolve' in combined_text or 'workflow' in combined_text + effective_status = 'Resolved' + if is_auto_resolve: + current_state = 'user_after_auto_resolve' + classification_basis = 'Auto-resolve oleh sistem (menunggu respon user)' + else: current_state = 'resolved_idle' - elif explicit_status and any(x in status_text for x in ('set status as open', 'set status as in progress', 'set status as on progress')): - current_state = 'internal' - elif is_agent_reply_to_requester: - # Agent sudah membalas requester; giliran berikutnya - # menunggu respons user dan dibebankan ke end user (customer). - current_state = 'customer' - # Balasan requester tidak serta-merta mengalihkan pembebanan ke internal - # sebelum tiket resmi di-reopen oleh workflow atau agent. - elif actor_type == 'Agent' and current_state in ('on_hold', 'internal_discussion') and confirmation_to_user: - # Pengecualian: note Agent yang eksplisit meminta keputusan/konfirmasi user. - current_state = 'customer' + classification_basis = 'Status Freshservice: Resolved' + elif explicit_status and any(x in status_text for x in ('set status as open', 'set status as in progress', 'set status as on progress')): + effective_status = 'In Progress' if 'in progress' in status_text or 'on progress' in status_text else 'Open' + current_state = 'internal' + classification_basis = 'Status Freshservice: %s' % effective_status - responsibility = current_state + responsibility = current_state - # 4a. Smart Detection: undangan Zoom meng-overwrite rentang tunggu hingga follow-up Agent. + # Aturan B: status hold tidak otomatis berarti IT menunggu internal. + # Bila pesan publik terakhir dari Agent meminta konfirmasi user, klasifikasi + # dibuat eksplisit sebagai perlu verifikasi dan menyimpan kutipan pemicunya. + is_ball_req, basis_ball = self._ball_in_court_check( + effective_status, last_public_message, last_message_actor_type, + ) + if is_ball_req: + responsibility = 'needs_verification' + needs_verification_flag = True + classification_basis = basis_ball + + # Penanganan khusus Create Timesheet: MEWARISI status efektif (Aturan A) + if 'timesheet' in combined_text: + stage_name = 'Create Timesheet' + # Timesheet tidak mengubah responsibility, tetap memakai responsibility dari status efektif + classification_basis = 'Timesheet (mewarisi status efektif %s)' % effective_status + + # Penanganan balasan Agent: tidak membalik tanggung jawab jika status masih di user (Aturan A) + if actor_type == 'Agent' and any(k in content_main.lower() for k in ('replied', 'reply')) and current_state == 'customer': + responsibility = 'customer' + classification_basis = 'Balasan Agent (mewarisi status Waiting on End User)' + + # 4a. Smart Detection: Undangan Zoom / Meeting & Aturan Pembatalan Meeting (Cancel Meeting) is_meeting_invitation = bool(meeting_invitation_pattern.search(full_activity_content or '')) + is_meeting_cancelled = False + + # Cek apakah setelah link meeting dikirim, meeting dibatalkan atau digantikan oleh status resmi: + # 1. Perubahan status resmi selain "Waiting on Internal Discussion" (misal: In Progress, Waiting on End User, On Hold, Resolved). + # 2. Atau terdapat kata kunci pembatalan eksplisit (cancel/batal meeting/zoom/call). + if meeting_waiting_for_followup and not is_meeting_invitation: + has_explicit_status_change = bool(re.search(r'\bset\s+status\s+as\b', status_text, re.I)) or vendor_external or is_agent_assignment + is_internal_disc = 'waiting on internal discussion' in status_text.lower() + cancel_kw = bool(re.search(r'\b(batal|cancel|reschedule|tidak\s+jadi)\b.{0,30}\b(meeting|zoom|call|gmeet)\b', full_activity_content or '', re.I)) + + if has_explicit_status_change and not is_internal_disc: + # An explicit status transition ends the shared meeting phase, + # but it is not a cancellation by itself. The next status or + # activity is allowed to determine the normal responsibility. + meeting_waiting_for_followup = False + meeting_start_dt = False + meeting_wait_until = False + interval_meeting_start = False + + if cancel_kw: + # Only explicit cancellation wording marks the phase as + # cancelled in the audit trail. + is_meeting_cancelled = True + stage_name = f'Meeting Cancelled / {stage_name}' + meeting_waiting_for_followup = False + meeting_start_dt = False + meeting_wait_until = False + interval_meeting_start = False + if is_meeting_invitation: parsed_meeting_dt = False time_match = meeting_time_pattern.search(full_activity_content or '') @@ -618,6 +724,10 @@ class FreshserviceTicket(models.Model): meeting_start_dt = parsed_meeting_dt meeting_waiting_for_followup = True meeting_wait_until = False + # Aktivitas pengiriman link dibebankan kepada pengirimnya. + # Jika Agent yang mengirim, stage log ini berwarna Internal (merah) + # di Excel. Fase Waiting Together baru dimulai setelah aktivitas ini. + responsibility = 'internal' if actor_type == 'Agent' else current_state if meeting_wait_until and start_dt_obj and start_dt_obj >= meeting_wait_until: meeting_wait_until = False @@ -628,22 +738,27 @@ class FreshserviceTicket(models.Model): k in (content_main or '').lower() for k in ('replied', 'reply', 'note') ) if is_meeting_invitation: - responsibility = 'scheduled_meeting' + # Pengiriman link adalah pekerjaan pembuat link. Jangan mengubahnya + # menjadi Waiting Together; status meeting dipakai aktivitas berikutnya. + responsibility = 'internal' if actor_type == 'Agent' else current_state meeting_wait_until = False - elif meeting_waiting_for_followup and start_dt_obj: - # No parsable Time: waits together until Agent follow-up. With Time:, - # the period from meeting start until that same follow-up is internal. - if meeting_start_dt and start_dt_obj >= meeting_start_dt: - responsibility = 'internal' - if is_agent_followup: - meeting_waiting_for_followup = False - meeting_start_dt = False - meeting_wait_until = start_dt_obj - else: - responsibility = 'scheduled_meeting' - if not meeting_start_dt and is_agent_followup: - meeting_waiting_for_followup = False - meeting_wait_until = start_dt_obj + elif is_meeting_cancelled: + # Tanggung jawab kembali ke status operasional saat ini (current_state) + responsibility = current_state + elif ( + meeting_waiting_for_followup + and not is_meeting_invitation + and 'waiting on internal discussion' in status_text.lower() + ): + # Setelah link meeting dikirim, status ini masih merupakan + # fase Scheduled Meeting dan tidak membatalkan state meeting. + responsibility = 'scheduled_meeting' + elif meeting_waiting_for_followup: + # Keep the complete interval shared while the ticket remains in + # Internal Discussion or Scheduled Meeting. Do not split at the + # scheduled meeting start; the next explicit status/activity + # determines the new responsibility. + responsibility = 'scheduled_meeting' # 5. Set First Responder jika belum terdeteksi if not self.first_responder_name and actor_type == 'Agent': @@ -653,35 +768,34 @@ class FreshserviceTicket(models.Model): log_values = { 'stage_name': stage_name, 'responsibility': responsibility, + 'effective_status': effective_status, + 'classification_basis': classification_basis, + 'needs_verification': needs_verification_flag, + 'delay_cause': delay_cause, 'actor_name': actor_name, 'actor_type': actor_type, 'activity_content': full_activity_content or stage_name, 'start_date': start_dt, 'end_date': end_dt, } - # Split every affected interval at the scheduled start precisely: - # invitation → meeting = shared SLA-paused wait; meeting → next activity = internal follow-up. - if interval_meeting_start and end_dt_obj and start_dt_obj < interval_meeting_start < end_dt_obj: - waiting_values = dict( - log_values, - responsibility='scheduled_meeting', - end_date=interval_meeting_start.strftime('%Y-%m-%d %H:%M:%S'), - ) - followup_values = dict( - log_values, - stage_name='Scheduled Meeting / Follow-up', - responsibility='internal', - activity_content='%s\n[Auto split: meeting start → next Agent follow-up]' % (full_activity_content or stage_name), - start_date=interval_meeting_start.strftime('%Y-%m-%d %H:%M:%S'), - ) - log_vals_list.extend([(0, 0, waiting_values), (0, 0, followup_values)]) - else: - log_vals_list.append((0, 0, log_values)) + # Keep one continuous shared-time interval while the effective status is + # Internal Discussion or Scheduled Meeting. The interval changes only + # when the next activity/status explicitly changes responsibility. + log_vals_list.append((0, 0, log_values)) + + ticket_write_vals = {} + if first_ack_dt: + ticket_write_vals['first_ack_at'] = first_ack_dt.strftime('%Y-%m-%d %H:%M:%S') + if first_meaningful_dt: + ticket_write_vals['first_meaningful_response_at'] = first_meaningful_dt.strftime('%Y-%m-%d %H:%M:%S') if log_vals_list: # Baru ganti snapshot setelah daftar audit trail pengganti siap. self.stage_log_ids.unlink() - self.write({'stage_log_ids': log_vals_list}) + ticket_write_vals['stage_log_ids'] = log_vals_list + self.write(ticket_write_vals) + elif ticket_write_vals: + self.write(ticket_write_vals) # 3. Update Percakapan. Terapkan perlindungan yang sama untuk respons kosong. conv_vals_list = [] diff --git a/gmn_freshservice/views/gmn_freshservice_ticket_stage_log_views_v01.xml b/gmn_freshservice/views/gmn_freshservice_ticket_stage_log_views_v01.xml index 55bd039..60d71f7 100644 --- a/gmn_freshservice/views/gmn_freshservice_ticket_stage_log_views_v01.xml +++ b/gmn_freshservice/views/gmn_freshservice_ticket_stage_log_views_v01.xml @@ -5,7 +5,7 @@ freshservice.ticket.stage.log.tree freshservice.ticket.stage.log - @@ -37,25 +37,37 @@
- + - + + + - + - + - - - + + + + + + + + + + + + + @@ -71,7 +83,7 @@ freshservice.ticket.stage.log.search freshservice.ticket.stage.log - + @@ -83,8 +95,8 @@ - - + + diff --git a/gmn_freshservice/views/gmn_freshservice_ticket_views_v01.xml b/gmn_freshservice/views/gmn_freshservice_ticket_views_v01.xml index 6b7a287..b0a8f43 100644 --- a/gmn_freshservice/views/gmn_freshservice_ticket_views_v01.xml +++ b/gmn_freshservice/views/gmn_freshservice_ticket_views_v01.xml @@ -9,12 +9,12 @@