fix: set/apply sla and first response logic
This commit is contained in:
parent
338322774e
commit
da53a9eed5
@ -220,9 +220,9 @@
|
|||||||
"options": "CRM Service Level Agreement"
|
"options": "CRM Service Level Agreement"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "response_by",
|
"fieldname": "sla_creation",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Datetime",
|
||||||
"label": "Response By"
|
"label": "SLA Creation"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_ffnp",
|
"fieldname": "column_break_ffnp",
|
||||||
@ -234,16 +234,16 @@
|
|||||||
"label": "SLA Status",
|
"label": "SLA Status",
|
||||||
"options": "\nFirst Response Due\nFailed\nFulfilled"
|
"options": "\nFirst Response Due\nFailed\nFulfilled"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "sla_creation",
|
|
||||||
"fieldtype": "Datetime",
|
|
||||||
"label": "SLA Creation"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "response_details_section",
|
"fieldname": "response_details_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Response Details"
|
"label": "Response Details"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "response_by",
|
||||||
|
"fieldtype": "Datetime",
|
||||||
|
"label": "Response By"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_pweh",
|
"fieldname": "column_break_pweh",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
@ -262,7 +262,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-12-04 15:25:07.155734",
|
"modified": "2023-12-10 13:54:53.630114",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Lead",
|
"name": "CRM Lead",
|
||||||
|
|||||||
@ -9,12 +9,18 @@ from frappe.utils import has_gravatar, validate_email_address
|
|||||||
|
|
||||||
|
|
||||||
class CRMLead(Document):
|
class CRMLead(Document):
|
||||||
|
def before_validate(self):
|
||||||
|
self.set_sla()
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.set_full_name()
|
self.set_full_name()
|
||||||
self.set_lead_name()
|
self.set_lead_name()
|
||||||
self.set_title()
|
self.set_title()
|
||||||
self.validate_email()
|
self.validate_email()
|
||||||
|
|
||||||
|
def before_save(self):
|
||||||
|
self.apply_sla()
|
||||||
|
|
||||||
def set_full_name(self):
|
def set_full_name(self):
|
||||||
if self.first_name:
|
if self.first_name:
|
||||||
self.lead_name = " ".join(
|
self.lead_name = " ".join(
|
||||||
@ -121,6 +127,22 @@ class CRMLead(Document):
|
|||||||
deal.insert(ignore_permissions=True)
|
deal.insert(ignore_permissions=True)
|
||||||
return deal.name
|
return deal.name
|
||||||
|
|
||||||
|
def set_sla(self):
|
||||||
|
"""
|
||||||
|
Find an SLA to apply to the lead.
|
||||||
|
"""
|
||||||
|
if sla := get_sla("CRM Lead"):
|
||||||
|
if not sla:
|
||||||
|
return
|
||||||
|
self.sla = sla.name
|
||||||
|
|
||||||
|
def apply_sla(self):
|
||||||
|
"""
|
||||||
|
Apply SLA if set.
|
||||||
|
"""
|
||||||
|
if sla := frappe.get_last_doc("CRM Service Level Agreement", {"name": self.sla}):
|
||||||
|
sla.apply(self)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def sort_options():
|
def sort_options():
|
||||||
return [
|
return [
|
||||||
@ -209,4 +231,10 @@ def convert_to_deal(lead):
|
|||||||
contact = lead.create_contact(False)
|
contact = lead.create_contact(False)
|
||||||
deal = lead.create_deal(contact)
|
deal = lead.create_deal(contact)
|
||||||
lead.save()
|
lead.save()
|
||||||
return deal
|
return deal
|
||||||
|
|
||||||
|
def get_sla(doctype):
|
||||||
|
sla = frappe.db.exists("CRM Service Level Agreement", {"apply_on": doctype, "enabled": 1})
|
||||||
|
if not sla:
|
||||||
|
return None
|
||||||
|
return frappe.get_cached_doc("CRM Service Level Agreement", sla)
|
||||||
0
crm/fcrm/doctype/crm_service_day/__init__.py
Normal file
0
crm/fcrm/doctype/crm_service_day/__init__.py
Normal file
59
crm/fcrm/doctype/crm_service_day/crm_service_day.json
Normal file
59
crm/fcrm/doctype/crm_service_day/crm_service_day.json
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2023-12-04 16:07:20.400084",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"workday",
|
||||||
|
"section_break_uegc",
|
||||||
|
"start_time",
|
||||||
|
"column_break_maie",
|
||||||
|
"end_time"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "workday",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Workday",
|
||||||
|
"options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_uegc",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "start_time",
|
||||||
|
"fieldtype": "Time",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Start Time",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_maie",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "end_time",
|
||||||
|
"fieldtype": "Time",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "End Time",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2023-12-04 16:09:22.928308",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "FCRM",
|
||||||
|
"name": "CRM Service Day",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
9
crm/fcrm/doctype/crm_service_day/crm_service_day.py
Normal file
9
crm/fcrm/doctype/crm_service_day/crm_service_day.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class CRMServiceDay(Document):
|
||||||
|
pass
|
||||||
@ -13,7 +13,9 @@
|
|||||||
"column_break_uxua",
|
"column_break_uxua",
|
||||||
"condition",
|
"condition",
|
||||||
"section_break_ufaf",
|
"section_break_ufaf",
|
||||||
"priorities"
|
"priorities",
|
||||||
|
"section_break_rmgo",
|
||||||
|
"working_hours"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -66,11 +68,22 @@
|
|||||||
"label": "Priorities",
|
"label": "Priorities",
|
||||||
"options": "CRM Service Level Priority",
|
"options": "CRM Service Level Priority",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_rmgo",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "working_hours",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Working Hours",
|
||||||
|
"options": "CRM Service Day",
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-12-04 13:49:09.547523",
|
"modified": "2023-12-04 16:13:24.638239",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Service Level Agreement",
|
"name": "CRM Service Level Agreement",
|
||||||
|
|||||||
@ -1,9 +1,159 @@
|
|||||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.utils import (
|
||||||
|
add_to_date,
|
||||||
|
get_datetime,
|
||||||
|
get_weekdays,
|
||||||
|
getdate,
|
||||||
|
now_datetime,
|
||||||
|
time_diff_in_seconds,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CRMServiceLevelAgreement(Document):
|
class CRMServiceLevelAgreement(Document):
|
||||||
pass
|
def apply(self, doc: Document):
|
||||||
|
self.handle_new(doc)
|
||||||
|
self.handle_status(doc)
|
||||||
|
self.handle_targets(doc)
|
||||||
|
self.handle_sla_status(doc)
|
||||||
|
|
||||||
|
def handle_new(self, doc: Document):
|
||||||
|
if not doc.is_new():
|
||||||
|
return
|
||||||
|
creation = doc.sla_creation or now_datetime()
|
||||||
|
doc.sla_creation = creation
|
||||||
|
|
||||||
|
def handle_status(self, doc: Document):
|
||||||
|
if doc.is_new() or not doc.has_value_changed("status"):
|
||||||
|
return
|
||||||
|
self.set_first_response_time(doc)
|
||||||
|
|
||||||
|
def set_first_response_time(self, doc: Document):
|
||||||
|
start_at = doc.sla_creation
|
||||||
|
end_at = doc.first_responded_on
|
||||||
|
if not start_at or not end_at:
|
||||||
|
return
|
||||||
|
doc.first_response_time = self.calc_elapsed_time(start_at, end_at)
|
||||||
|
|
||||||
|
def handle_targets(self, doc: Document):
|
||||||
|
self.set_response_by(doc)
|
||||||
|
|
||||||
|
def set_response_by(self, doc: Document):
|
||||||
|
start = doc.sla_creation
|
||||||
|
doc.response_by = self.calc_time(start, doc.status, "first_response_time")
|
||||||
|
|
||||||
|
def handle_sla_status(self, doc: Document):
|
||||||
|
is_failed = self.is_first_response_failed(doc)
|
||||||
|
options = {
|
||||||
|
"Fulfilled": True,
|
||||||
|
"First Response Due": not doc.first_responded_on,
|
||||||
|
"Failed": is_failed,
|
||||||
|
}
|
||||||
|
for status in options:
|
||||||
|
if options[status]:
|
||||||
|
doc.sla_status = status
|
||||||
|
|
||||||
|
def is_first_response_failed(self, doc: Document):
|
||||||
|
if not doc.first_responded_on:
|
||||||
|
return get_datetime(doc.response_by) < now_datetime()
|
||||||
|
return get_datetime(doc.response_by) < get_datetime(doc.first_responded_on)
|
||||||
|
|
||||||
|
def calc_time(
|
||||||
|
self,
|
||||||
|
start_at: str,
|
||||||
|
priority: str,
|
||||||
|
target: Literal["first_response_time"],
|
||||||
|
):
|
||||||
|
res = get_datetime(start_at)
|
||||||
|
priority = self.get_priorities()[priority]
|
||||||
|
time_needed = priority.get(target, 0)
|
||||||
|
holidays = []
|
||||||
|
weekdays = get_weekdays()
|
||||||
|
workdays = self.get_workdays()
|
||||||
|
while time_needed:
|
||||||
|
today = res
|
||||||
|
today_day = getdate(today)
|
||||||
|
today_weekday = weekdays[today.weekday()]
|
||||||
|
is_workday = today_weekday in workdays
|
||||||
|
is_holiday = today_day in holidays
|
||||||
|
if is_holiday or not is_workday:
|
||||||
|
res = add_to_date(res, days=1, as_datetime=True)
|
||||||
|
continue
|
||||||
|
today_workday = workdays[today_weekday]
|
||||||
|
now_in_seconds = time_diff_in_seconds(today, today_day)
|
||||||
|
start_time = max(today_workday.start_time.total_seconds(), now_in_seconds)
|
||||||
|
till_start_time = max(start_time - now_in_seconds, 0)
|
||||||
|
end_time = max(today_workday.end_time.total_seconds(), now_in_seconds)
|
||||||
|
time_left = max(end_time - start_time, 0)
|
||||||
|
if not time_left:
|
||||||
|
res = getdate(add_to_date(res, days=1, as_datetime=True))
|
||||||
|
continue
|
||||||
|
time_taken = min(time_needed, time_left)
|
||||||
|
time_needed -= time_taken
|
||||||
|
time_required = till_start_time + time_taken
|
||||||
|
res = add_to_date(res, seconds=time_required, as_datetime=True)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def calc_elapsed_time(self, start_at, end_at) -> float:
|
||||||
|
"""
|
||||||
|
Get took from start to end, excluding non-working hours
|
||||||
|
|
||||||
|
:param start_at: Date at which calculation starts
|
||||||
|
:param end_at: Date at which calculation ends
|
||||||
|
:return: Number of seconds
|
||||||
|
"""
|
||||||
|
start_at = getdate(start_at)
|
||||||
|
end_at = getdate(end_at)
|
||||||
|
time_took = 0
|
||||||
|
holidays = []
|
||||||
|
weekdays = get_weekdays()
|
||||||
|
workdays = self.get_workdays()
|
||||||
|
while getdate(start_at) <= getdate(end_at):
|
||||||
|
today = start_at
|
||||||
|
today_day = getdate(today)
|
||||||
|
today_weekday = weekdays[today.weekday()]
|
||||||
|
is_workday = today_weekday in workdays
|
||||||
|
is_holiday = today_day in holidays
|
||||||
|
if is_holiday or not is_workday:
|
||||||
|
start_at = getdate(add_to_date(start_at, days=1, as_datetime=True))
|
||||||
|
continue
|
||||||
|
today_workday = workdays[today_weekday]
|
||||||
|
is_today = getdate(start_at) == getdate(end_at)
|
||||||
|
if not is_today:
|
||||||
|
working_start = today_workday.start_time
|
||||||
|
working_end = today_workday.end_time
|
||||||
|
working_time = time_diff_in_seconds(working_start, working_end)
|
||||||
|
time_took += working_time
|
||||||
|
start_at = getdate(add_to_date(start_at, days=1, as_datetime=True))
|
||||||
|
continue
|
||||||
|
now_in_seconds = time_diff_in_seconds(today, today_day)
|
||||||
|
start_time = max(today_workday.start_time.total_seconds(), now_in_seconds)
|
||||||
|
end_at_seconds = time_diff_in_seconds(getdate(end_at), end_at)
|
||||||
|
end_time = max(today_workday.end_time.total_seconds(), end_at_seconds)
|
||||||
|
time_taken = end_time - start_time
|
||||||
|
time_took += time_taken
|
||||||
|
start_at = getdate(add_to_date(start_at, days=1, as_datetime=True))
|
||||||
|
return time_took
|
||||||
|
|
||||||
|
def get_priorities(self):
|
||||||
|
"""
|
||||||
|
Return priorities related info as a dict. With `priority` as key
|
||||||
|
"""
|
||||||
|
res = {}
|
||||||
|
for row in self.priorities:
|
||||||
|
res[row.priority] = row
|
||||||
|
return res
|
||||||
|
|
||||||
|
def get_workdays(self) -> dict[str, dict]:
|
||||||
|
"""
|
||||||
|
Return workdays related info as a dict. With `workday` as key
|
||||||
|
"""
|
||||||
|
res = {}
|
||||||
|
for row in self.working_hours:
|
||||||
|
res[row.workday] = row
|
||||||
|
return res
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user