[UPD] update logic tiket
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -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([
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
Reference in New Issue
Block a user