1
0
forked from test/crm

fix: parse call log from backend to get receiver and caller

This commit is contained in:
Shariq Ansari 2025-01-18 18:09:14 +05:30
parent dbadd1bf0b
commit 7dd53d88e5
8 changed files with 144 additions and 114 deletions

View File

@ -6,6 +6,8 @@ from frappe import _
from frappe.desk.form.load import get_docinfo from frappe.desk.form.load import get_docinfo
from frappe.query_builder import JoinType from frappe.query_builder import JoinType
from crm.fcrm.doctype.crm_call_log.crm_call_log import parse_call_log
@frappe.whitelist() @frappe.whitelist()
def get_activities(name): def get_activities(name):
@ -439,6 +441,8 @@ def get_linked_calls(name):
], ],
) )
calls = [parse_call_log(call) for call in calls] if calls else []
return {"calls": calls, "notes": notes, "tasks": tasks} return {"calls": calls, "notes": notes, "tasks": tasks}

View File

@ -306,6 +306,7 @@ def get_data(
) )
or [] or []
) )
data = parse_list_data(data, doctype)
if view_type == "kanban": if view_type == "kanban":
if not rows: if not rows:
@ -479,6 +480,13 @@ def get_data(
} }
def parse_list_data(data, doctype):
_list = get_controller(doctype)
if hasattr(_list, "parse_list_data"):
data = _list.parse_list_data(data)
return data
def convert_filter_to_tuple(doctype, filters): def convert_filter_to_tuple(doctype, filters):
if isinstance(filters, dict): if isinstance(filters, dict):
filters_items = filters.items() filters_items = filters.items()

View File

@ -4,6 +4,9 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from crm.integrations.api import get_contact_by_phone_number
from crm.utils import seconds_to_duration
class CRMCallLog(Document): class CRMCallLog(Document):
@staticmethod @staticmethod
@ -77,6 +80,9 @@ class CRMCallLog(Document):
] ]
return {"columns": columns, "rows": rows} return {"columns": columns, "rows": rows}
def parse_list_data(calls):
return [parse_call_log(call) for call in calls] if calls else []
def has_link(self, doctype, name): def has_link(self, doctype, name):
for link in self.links: for link in self.links:
if link.link_doctype == doctype and link.link_name == name: if link.link_doctype == doctype and link.link_name == name:
@ -89,6 +95,69 @@ class CRMCallLog(Document):
self.append("links", {"link_doctype": reference_doctype, "link_name": reference_name}) self.append("links", {"link_doctype": reference_doctype, "link_name": reference_name})
def parse_call_log(call):
call["show_recording"] = False
call["duration"] = seconds_to_duration(call.get("duration"))
if call.get("type") == "Incoming":
call["activity_type"] = "incoming_call"
contact = get_contact_by_phone_number(call.get("from"))
receiver = (
frappe.db.get_values("User", call.get("receiver"), ["full_name", "user_image"])[0]
if call.get("receiver")
else [None, None]
)
call["caller"] = {
"label": contact.get("full_name", "Unknown"),
"image": contact.get("image"),
}
call["receiver"] = {
"label": receiver[0],
"image": receiver[1],
}
elif call.get("type") == "Outgoing":
call["activity_type"] = "outgoing_call"
contact = get_contact_by_phone_number(call.get("to"))
caller = (
frappe.db.get_values("User", call.get("caller"), ["full_name", "user_image"])[0]
if call.get("caller")
else [None, None]
)
call["caller"] = {
"label": caller[0],
"image": caller[1],
}
call["receiver"] = {
"label": contact.get("full_name", "Unknown"),
"image": contact.get("image"),
}
return call
@frappe.whitelist()
def get_call_log(name):
call = frappe.get_cached_doc(
"CRM Call Log",
name,
fields=[
"name",
"caller",
"receiver",
"duration",
"type",
"status",
"from",
"to",
"note",
"recording_url",
"reference_doctype",
"reference_docname",
"creation",
],
).as_dict()
return parse_call_log(call)
@frappe.whitelist() @frappe.whitelist()
def create_lead_from_call_log(call_log): def create_lead_from_call_log(call_log):
lead = frappe.new_doc("CRM Lead") lead = frappe.new_doc("CRM Lead")

View File

@ -1,4 +1,5 @@
import phonenumbers import phonenumbers
from frappe.utils import floor
from phonenumbers import NumberParseException from phonenumbers import NumberParseException
from phonenumbers import PhoneNumberFormat as PNF from phonenumbers import PhoneNumberFormat as PNF
@ -58,3 +59,37 @@ def are_same_phone_number(number1, number2, default_region="IN", validate=True):
except phonenumbers.NumberParseException: except phonenumbers.NumberParseException:
return False return False
def seconds_to_duration(seconds):
if not seconds:
return "0s"
hours = floor(seconds // 3600)
minutes = floor((seconds % 3600) // 60)
seconds = floor((seconds % 3600) % 60)
# 1h 0m 0s -> 1h
# 0h 1m 0s -> 1m
# 0h 0m 1s -> 1s
# 1h 1m 0s -> 1h 1m
# 1h 0m 1s -> 1h 1s
# 0h 1m 1s -> 1m 1s
# 1h 1m 1s -> 1h 1m 1s
if hours and minutes and seconds:
return f"{hours}h {minutes}m {seconds}s"
elif hours and minutes:
return f"{hours}h {minutes}m"
elif hours and seconds:
return f"{hours}h {seconds}s"
elif minutes and seconds:
return f"{minutes}m {seconds}s"
elif hours:
return f"{hours}h"
elif minutes:
return f"{minutes}m"
elif seconds:
return f"{seconds}s"
else:
return "0s"

View File

@ -484,7 +484,7 @@ import CommunicationArea from '@/components/CommunicationArea.vue'
import WhatsappTemplateSelectorModal from '@/components/Modals/WhatsappTemplateSelectorModal.vue' import WhatsappTemplateSelectorModal from '@/components/Modals/WhatsappTemplateSelectorModal.vue'
import AllModals from '@/components/Activities/AllModals.vue' import AllModals from '@/components/Activities/AllModals.vue'
import FilesUploader from '@/components/FilesUploader/FilesUploader.vue' import FilesUploader from '@/components/FilesUploader/FilesUploader.vue'
import { timeAgo, formatDate, secondsToDuration, startCase } from '@/utils' import { timeAgo, formatDate, startCase } from '@/utils'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { contactsStore } from '@/stores/contacts' import { contactsStore } from '@/stores/contacts'
@ -544,40 +544,6 @@ const all_activities = createResource({
cache: ['activity', doc.value.data.name], cache: ['activity', doc.value.data.name],
auto: true, auto: true,
transform: ([versions, calls, notes, tasks, attachments]) => { transform: ([versions, calls, notes, tasks, attachments]) => {
if (calls?.length) {
calls.forEach((doc) => {
doc.show_recording = false
doc.activity_type =
doc.type === 'Incoming' ? 'incoming_call' : 'outgoing_call'
doc.duration = secondsToDuration(doc.duration)
if (doc.type === 'Incoming') {
doc.caller = {
label:
getContact(doc.from)?.full_name ||
getLeadContact(doc.from)?.full_name ||
'Unknown',
image:
getContact(doc.from)?.image || getLeadContact(doc.from)?.image,
}
doc.receiver = {
label: getUser(doc.receiver).full_name,
image: getUser(doc.receiver).user_image,
}
} else {
doc.caller = {
label: getUser(doc.caller).full_name,
image: getUser(doc.caller).user_image,
}
doc.receiver = {
label:
getContact(doc.to)?.full_name ||
getLeadContact(doc.to)?.full_name ||
'Unknown',
image: getContact(doc.to)?.image || getLeadContact(doc.to)?.image,
}
}
})
}
return { versions, calls, notes, tasks, attachments } return { versions, calls, notes, tasks, attachments }
}, },
}) })

View File

@ -85,7 +85,8 @@
</template> </template>
<template <template
v-if=" v-if="
callLog.doc?.type.label == 'Incoming' && !callLog.doc?.reference_docname callLog.data?.type.label == 'Incoming' &&
!callLog.data?.reference_docname
" "
#actions #actions
> >
@ -117,6 +118,7 @@ import {
Tooltip, Tooltip,
createDocumentResource, createDocumentResource,
call, call,
createResource,
} from 'frappe-ui' } from 'frappe-ui'
import { getCallLogDetail } from '@/utils/callLog' import { getCallLogDetail } from '@/utils/callLog'
import { ref, computed, h, watch } from 'vue' import { ref, computed, h, watch } from 'vue'
@ -136,63 +138,63 @@ const callNoteDoc = ref(null)
const callLog = ref({}) const callLog = ref({})
const detailFields = computed(() => { const detailFields = computed(() => {
if (!callLog.value.doc) return [] if (!callLog.value.data) return []
let details = [ let details = [
{ {
icon: h(FeatherIcon, { icon: h(FeatherIcon, {
name: callLog.value.doc.type.icon, name: callLog.value.data.type.icon,
class: 'h-3.5 w-3.5', class: 'h-3.5 w-3.5',
}), }),
name: 'type', name: 'type',
value: callLog.value.doc.type.label + ' Call', value: callLog.value.data.type.label + ' Call',
}, },
{ {
icon: ContactsIcon, icon: ContactsIcon,
name: 'receiver', name: 'receiver',
value: { value: {
receiver: callLog.value.doc.receiver, receiver: callLog.value.data.receiver,
caller: callLog.value.doc.caller, caller: callLog.value.data.caller,
}, },
}, },
{ {
icon: icon:
callLog.value.doc.reference_doctype == 'CRM Lead' callLog.value.data.reference_doctype == 'CRM Lead'
? LeadsIcon ? LeadsIcon
: Dealsicon, : Dealsicon,
name: 'reference_doctype', name: 'reference_doctype',
value: value:
callLog.value.doc.reference_doctype == 'CRM Lead' ? 'Lead' : 'Deal', callLog.value.data.reference_doctype == 'CRM Lead' ? 'Lead' : 'Deal',
link: () => { link: () => {
if (callLog.value.doc.reference_doctype == 'CRM Lead') { if (callLog.value.data.reference_doctype == 'CRM Lead') {
router.push({ router.push({
name: 'Lead', name: 'Lead',
params: { leadId: callLog.value.doc.reference_docname }, params: { leadId: callLog.value.data.reference_docname },
}) })
} else { } else {
router.push({ router.push({
name: 'Deal', name: 'Deal',
params: { dealId: callLog.value.doc.reference_docname }, params: { dealId: callLog.value.data.reference_docname },
}) })
} }
}, },
condition: () => callLog.value.doc.reference_docname, condition: () => callLog.value.data.reference_docname,
}, },
{ {
icon: CalendarIcon, icon: CalendarIcon,
name: 'creation', name: 'creation',
value: callLog.value.doc.creation.label, value: callLog.value.data.creation.label,
tooltip: callLog.value.doc.creation.label, tooltip: callLog.value.data.creation.label,
}, },
{ {
icon: DurationIcon, icon: DurationIcon,
name: 'duration', name: 'duration',
value: callLog.value.doc.duration.label, value: callLog.value.data.duration.label,
}, },
{ {
icon: CheckCircleIcon, icon: CheckCircleIcon,
name: 'status', name: 'status',
value: callLog.value.doc.status.label, value: callLog.value.data.status.label,
color: callLog.value.doc.status.color, color: callLog.value.data.status.color,
}, },
{ {
icon: h(FeatherIcon, { icon: h(FeatherIcon, {
@ -200,7 +202,7 @@ const detailFields = computed(() => {
class: 'h-4 w-4 mt-2', class: 'h-4 w-4 mt-2',
}), }),
name: 'recording_url', name: 'recording_url',
value: callLog.value.doc.recording_url, value: callLog.value.data.recording_url,
}, },
{ {
icon: NoteIcon, icon: NoteIcon,
@ -216,7 +218,7 @@ const detailFields = computed(() => {
function createLead() { function createLead() {
call('crm.fcrm.doctype.crm_call_log.crm_call_log.create_lead_from_call_log', { call('crm.fcrm.doctype.crm_call_log.crm_call_log.create_lead_from_call_log', {
call_log: callLog.value.doc, call_log: callLog.value.data,
}).then((d) => { }).then((d) => {
if (d) { if (d) {
router.push({ name: 'Lead', params: { leadId: d } }) router.push({ name: 'Lead', params: { leadId: d } })
@ -226,24 +228,9 @@ function createLead() {
watch(show, (val) => { watch(show, (val) => {
if (val) { if (val) {
callLog.value = createDocumentResource({ callLog.value = createResource({
doctype: 'CRM Call Log', url: 'crm.fcrm.doctype.crm_call_log.crm_call_log.get_call_log',
name: props.name, params: { name: props.name },
fields: [
'name',
'caller',
'receiver',
'duration',
'type',
'status',
'from',
'to',
'note',
'recording_url',
'reference_doctype',
'reference_docname',
'creation',
],
cache: ['call_log', props.name], cache: ['call_log', props.name],
auto: true, auto: true,
transform: (doc) => { transform: (doc) => {

View File

@ -1,41 +1,15 @@
import { secondsToDuration, formatDate, timeAgo } from '@/utils' import { formatDate, timeAgo } from '@/utils'
import { getMeta } from '@/stores/meta' import { getMeta } from '@/stores/meta'
import { usersStore } from '@/stores/users'
import { contactsStore } from '@/stores/contacts'
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } = const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
getMeta('CRM Call Log') getMeta('CRM Call Log')
const { getUser } = usersStore()
const { getContact, getLeadContact } = contactsStore()
export function getCallLogDetail(row, log, columns = []) { export function getCallLogDetail(row, log, columns = []) {
let incoming = log.type === 'Incoming' let incoming = log.type === 'Incoming'
if (row === 'caller') { if (row === 'duration') {
return { return {
label: incoming label: log.duration,
? getContact(log.from)?.full_name ||
getLeadContact(log.from)?.full_name ||
'Unknown'
: log.caller && getUser(log.caller).full_name,
image: incoming
? getContact(log.from)?.image || getLeadContact(log.from)?.image
: log.caller && getUser(log.caller).user_image,
}
} else if (row === 'receiver') {
return {
label: incoming
? log.receiver && getUser(log.receiver).full_name
: getContact(log.to)?.full_name ||
getLeadContact(log.to)?.full_name ||
'Unknown',
image: incoming
? log.receiver && getUser(log.receiver).user_image
: getContact(log.to)?.image || getLeadContact(log.to)?.image,
}
} else if (row === 'duration') {
return {
label: secondsToDuration(log.duration),
icon: 'clock', icon: 'clock',
} }
} else if (row === 'type') { } else if (row === 'type') {

View File

@ -113,19 +113,6 @@ export function htmlToText(html) {
return div.textContent || div.innerText || '' return div.textContent || div.innerText || ''
} }
export function secondsToDuration(seconds) {
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const _seconds = Math.floor((seconds % 3600) % 60)
if (hours == 0 && minutes == 0) {
return `${_seconds}s`
} else if (hours == 0) {
return `${minutes}m ${_seconds}s`
}
return `${hours}h ${minutes}m ${_seconds}s`
}
export function startCase(str) { export function startCase(str) {
return str.charAt(0).toUpperCase() + str.slice(1) return str.charAt(0).toUpperCase() + str.slice(1)
} }