Merge pull request #24 from shariquerik/create-deal-doctype

refactor: Create deal doctype & fix code accordingly
This commit is contained in:
Shariq Ansari 2023-11-07 16:20:48 +05:30 committed by GitHub
commit 168a3b37d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 672 additions and 345 deletions

226
crm/api/activities.py Normal file
View File

@ -0,0 +1,226 @@
import json
import frappe
from frappe import _
from frappe.desk.form.load import get_docinfo
@frappe.whitelist()
def get_activities(name):
if frappe.db.exists("CRM Deal", name):
return get_deal_activities(name)
elif frappe.db.exists("CRM Lead", name):
return get_lead_activities(name)
else:
frappe.throw(_("Document not found"), frappe.DoesNotExistError)
def get_deal_activities(name):
get_docinfo('', "CRM Deal", name)
docinfo = frappe.response["docinfo"]
deal_fields_meta = frappe.get_meta("CRM Deal").fields
doc = frappe.db.get_values("CRM Deal", name, ["creation", "owner", "lead"])[0]
lead = doc[2]
activities = []
creation_text = "created this deal"
if lead:
activities = get_lead_activities(lead)
creation_text = "converted the lead to this deal"
activities.append({
"activity_type": "creation",
"creation": doc[0],
"owner": doc[1],
"data": creation_text,
"is_lead": False,
})
docinfo.versions.reverse()
for version in docinfo.versions:
data = json.loads(version.data)
if not data.get("changed"):
continue
field_option = None
if change := data.get("changed")[0]:
field_label, field_option = next(((f.label, f.options) for f in deal_fields_meta if f.fieldname == change[0]), None)
if field_label == "Lead" or (not change[1] and not change[2]):
continue
activity_type = "changed"
data = {
"field": change[0],
"field_label": field_label,
"old_value": change[1],
"value": change[2],
}
if not change[1] and change[2]:
activity_type = "added"
data = {
"field": change[0],
"field_label": field_label,
"value": change[2],
}
elif change[1] and not change[2]:
activity_type = "removed"
data = {
"field": change[0],
"field_label": field_label,
"value": change[1],
}
activity = {
"activity_type": activity_type,
"creation": version.creation,
"owner": version.owner,
"data": data,
"is_lead": False,
"options": field_option,
}
activities.append(activity)
for communication in docinfo.communications:
activity = {
"activity_type": "communication",
"creation": communication.creation,
"data": {
"subject": communication.subject,
"content": communication.content,
"sender_full_name": communication.sender_full_name,
"sender": communication.sender,
"recipients": communication.recipients,
"cc": communication.cc,
"bcc": communication.bcc,
"read_by_recipient": communication.read_by_recipient,
},
"is_lead": False,
}
activities.append(activity)
activities.sort(key=lambda x: x["creation"], reverse=True)
activities = handle_multiple_versions(activities)
return activities
def get_lead_activities(name):
get_docinfo('', "CRM Lead", name)
docinfo = frappe.response["docinfo"]
lead_fields_meta = frappe.get_meta("CRM Lead").fields
doc = frappe.db.get_values("CRM Lead", name, ["creation", "owner"])[0]
activities = [{
"activity_type": "creation",
"creation": doc[0],
"owner": doc[1],
"data": "created this lead",
"is_lead": True,
}]
docinfo.versions.reverse()
for version in docinfo.versions:
data = json.loads(version.data)
if not data.get("changed"):
continue
field_option = None
if change := data.get("changed")[0]:
field_label, field_option = next(((f.label, f.options) for f in lead_fields_meta if f.fieldname == change[0]), None)
if field_label == "Converted" or (not change[1] and not change[2]):
continue
activity_type = "changed"
data = {
"field": change[0],
"field_label": field_label,
"old_value": change[1],
"value": change[2],
}
if not change[1] and change[2]:
activity_type = "added"
data = {
"field": change[0],
"field_label": field_label,
"value": change[2],
}
elif change[1] and not change[2]:
activity_type = "removed"
data = {
"field": change[0],
"field_label": field_label,
"value": change[1],
}
activity = {
"activity_type": activity_type,
"creation": version.creation,
"owner": version.owner,
"data": data,
"is_lead": True,
"options": field_option,
}
activities.append(activity)
for communication in docinfo.communications:
activity = {
"activity_type": "communication",
"creation": communication.creation,
"data": {
"subject": communication.subject,
"content": communication.content,
"sender_full_name": communication.sender_full_name,
"sender": communication.sender,
"recipients": communication.recipients,
"cc": communication.cc,
"bcc": communication.bcc,
"read_by_recipient": communication.read_by_recipient,
},
"is_lead": True,
}
activities.append(activity)
activities.sort(key=lambda x: x["creation"], reverse=True)
activities = handle_multiple_versions(activities)
return activities
def handle_multiple_versions(versions):
activities = []
grouped_versions = []
old_version = None
for version in versions:
is_version = version["activity_type"] in ["changed", "added", "removed"]
if not is_version:
activities.append(version)
if not old_version:
old_version = version
if is_version: grouped_versions.append(version)
continue
if is_version and old_version.get("owner") and version["owner"] == old_version["owner"]:
grouped_versions.append(version)
else:
if grouped_versions:
activities.append(parse_grouped_versions(grouped_versions))
grouped_versions = []
if is_version: grouped_versions.append(version)
old_version = version
if version == versions[-1] and grouped_versions:
activities.append(parse_grouped_versions(grouped_versions))
return activities
def parse_grouped_versions(versions):
version = versions[0]
if len(versions) == 1:
return version
other_versions = versions[1:]
version["other_versions"] = other_versions
return version

View File

@ -14,7 +14,8 @@
"duration",
"medium",
"start_time",
"lead",
"reference_doctype",
"reference_docname",
"column_break_ufnp",
"to",
"type",
@ -89,12 +90,6 @@
"fieldtype": "Datetime",
"label": "End Time"
},
{
"fieldname": "lead",
"fieldtype": "Link",
"label": "Lead/Deal",
"options": "CRM Lead"
},
{
"fieldname": "note",
"fieldtype": "Link",
@ -114,11 +109,24 @@
"fieldtype": "Link",
"label": "Caller",
"options": "User"
},
{
"default": "CRM Lead",
"fieldname": "reference_doctype",
"fieldtype": "Link",
"label": "Reference Document Type",
"options": "DocType"
},
{
"fieldname": "reference_docname",
"fieldtype": "Dynamic Link",
"label": "Reference Name",
"options": "reference_doctype"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-08-30 15:39:46.613734",
"modified": "2023-11-07 13:52:40.504747",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Call Log",

View File

View File

@ -0,0 +1,25 @@
import json
import frappe
from frappe import _
from frappe.desk.form.load import get_docinfo
from crm.fcrm.doctype.crm_lead.api import get_activities as get_lead_activities
@frappe.whitelist()
def get_deal(name):
Deal = frappe.qb.DocType("CRM Deal")
query = (
frappe.qb.from_(Deal)
.select("*")
.where(Deal.name == name)
.limit(1)
)
deal = query.run(as_dict=True)
if not len(deal):
frappe.throw(_("Deal not found"), frappe.DoesNotExistError)
deal = deal.pop()
return deal

View File

@ -0,0 +1,8 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("CRM Deal", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,145 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "naming_series:",
"creation": "2023-11-06 17:56:25.210449",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"naming_series",
"organization",
"website",
"annual_revenue",
"column_break_afce",
"deal_owner",
"close_date",
"status",
"probability",
"next_step",
"section_break_eepu",
"lead",
"column_break_bqvs",
"contacts_tab",
"email",
"mobile_no"
],
"fields": [
{
"fieldname": "organization",
"fieldtype": "Link",
"label": "Organization",
"options": "CRM Organization"
},
{
"fieldname": "probability",
"fieldtype": "Percent",
"label": "Probability"
},
{
"fetch_from": "organization.annual_revenue",
"fieldname": "annual_revenue",
"fieldtype": "Int",
"label": "Annual Revenue"
},
{
"fieldname": "column_break_afce",
"fieldtype": "Column Break"
},
{
"fetch_from": "organization.website",
"fieldname": "website",
"fieldtype": "Data",
"label": "Website",
"options": "URL"
},
{
"fieldname": "close_date",
"fieldtype": "Date",
"label": "Close Date"
},
{
"fieldname": "next_step",
"fieldtype": "Data",
"label": "Next Step"
},
{
"fieldname": "lead",
"fieldtype": "Link",
"label": "Lead",
"options": "CRM Lead"
},
{
"fieldname": "section_break_eepu",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_bqvs",
"fieldtype": "Column Break"
},
{
"fieldname": "deal_owner",
"fieldtype": "Link",
"label": "Deal Owner",
"options": "User"
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "CRM-DEAL-.YYYY.-"
},
{
"fieldname": "contacts_tab",
"fieldtype": "Tab Break",
"label": "Contacts"
},
{
"fieldname": "email",
"fieldtype": "Data",
"label": "Email",
"options": "Email"
},
{
"fieldname": "mobile_no",
"fieldtype": "Data",
"label": "Mobile No",
"options": "Phone"
},
{
"default": "Qualification",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Qualification\nDemo/Making\nProposal/Quotation\nNegotiation\nReady to Close\nWon\nLost",
"reqd": 1,
"search_index": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-11-06 21:53:50.442404",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Deal",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -0,0 +1,19 @@
# 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 CRMDeal(Document):
@staticmethod
def sort_options():
return [
{ "label": 'Created', "value": 'creation' },
{ "label": 'Modified', "value": 'modified' },
{ "label": 'Status', "value": 'status' },
{ "label": 'Deal owner', "value": 'deal_owner' },
{ "label": 'Organization', "value": 'organization' },
{ "label": 'Email', "value": 'email' },
{ "label": 'Mobile no', "value": 'mobile_no' },
]

View File

@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestCRMDeal(FrappeTestCase):
pass

View File

@ -22,126 +22,3 @@ def get_lead(name):
lead = lead.pop()
return lead
@frappe.whitelist()
def get_activities(name):
get_docinfo('', "CRM Lead", name)
docinfo = frappe.response["docinfo"]
lead_fields_meta = frappe.get_meta("CRM Lead").fields
doc = frappe.db.get_values("CRM Lead", name, ["creation", "owner", "created_as_deal"])[0]
created_as_deal = doc[2]
is_lead = False if created_as_deal else True
activities = [{
"activity_type": "creation",
"creation": doc[0],
"owner": doc[1],
"data": "created this " + ("deal" if created_as_deal else "lead"),
"is_lead": is_lead,
}]
docinfo.versions.reverse()
for version in docinfo.versions:
data = json.loads(version.data)
if not data.get("changed"):
continue
field_option = None
if change := data.get("changed")[0]:
field_label, field_option = next(((f.label, f.options) for f in lead_fields_meta if f.fieldname == change[0]), None)
activity_type = "changed"
if field_label == "Lead Owner" and (created_as_deal or not is_lead):
field_label = "Deal Owner"
data = {
"field": change[0],
"field_label": field_label,
"old_value": change[1],
"value": change[2],
}
if not change[1] and not change[2]:
continue
if not change[1] and change[2]:
activity_type = "added"
data = {
"field": change[0],
"field_label": field_label,
"value": change[2],
}
if field_label == "Is Deal" and change[2] and is_lead:
activity_type = "deal"
is_lead = False
elif change[1] and not change[2]:
activity_type = "removed"
data = {
"field": change[0],
"field_label": field_label,
"value": change[1],
}
activity = {
"activity_type": activity_type,
"creation": version.creation,
"owner": version.owner,
"data": data,
"is_lead": is_lead,
"options": field_option,
}
activities.append(activity)
for communication in docinfo.communications:
activity = {
"activity_type": "communication",
"creation": communication.creation,
"data": {
"subject": communication.subject,
"content": communication.content,
"sender_full_name": communication.sender_full_name,
"sender": communication.sender,
"recipients": communication.recipients,
"cc": communication.cc,
"bcc": communication.bcc,
"read_by_recipient": communication.read_by_recipient,
},
"is_lead": is_lead,
}
activities.append(activity)
activities.sort(key=lambda x: x["creation"], reverse=True)
activities = handle_multiple_versions(activities)
return activities
def handle_multiple_versions(versions):
activities = []
grouped_versions = []
old_version = None
for version in versions:
is_version = version["activity_type"] in ["changed", "added", "removed"]
if not is_version:
activities.append(version)
if not old_version:
old_version = version
if is_version: grouped_versions.append(version)
continue
if is_version and old_version.get("owner") and version["owner"] == old_version["owner"]:
grouped_versions.append(version)
else:
if grouped_versions:
activities.append(parse_grouped_versions(grouped_versions))
grouped_versions = []
if is_version: grouped_versions.append(version)
old_version = version
if version == versions[-1] and grouped_versions:
activities.append(parse_grouped_versions(grouped_versions))
return activities
def parse_grouped_versions(versions):
version = versions[0]
if len(versions) == 1:
return version
other_versions = versions[1:]
version["other_versions"] = other_versions
return version

View File

@ -13,20 +13,15 @@
"first_name",
"middle_name",
"last_name",
"is_deal",
"created_as_deal",
"column_break_izjs",
"lead_name",
"gender",
"lead_owner",
"status",
"image",
"column_break_lcuv",
"lead_owner",
"status",
"source",
"deal_status",
"close_date",
"probability",
"next_step",
"converted",
"organization_tab",
"section_break_uixv",
"organization",
@ -93,7 +88,7 @@
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Lead Status",
"label": "Status",
"options": "Open\nContacted\nNurture\nQualified\nUnqualified\nJunk",
"reqd": 1,
"search_index": 1
@ -123,8 +118,9 @@
},
{
"fieldname": "mobile_no",
"fieldtype": "Phone",
"label": "Mobile No"
"fieldtype": "Data",
"label": "Mobile No",
"options": "Phone"
},
{
"fieldname": "column_break_sjtw",
@ -132,8 +128,9 @@
},
{
"fieldname": "phone",
"fieldtype": "Phone",
"label": "Phone"
"fieldtype": "Data",
"label": "Phone",
"options": "Phone"
},
{
"fieldname": "section_break_uixv",
@ -189,25 +186,6 @@
"label": "Full Name",
"search_index": 1
},
{
"default": "Qualification",
"fieldname": "deal_status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Deal Status",
"options": "Qualification\nDemo/Making\nProposal/Quotation\nNegotiation\nReady to Close\nWon\nLost",
"reqd": 1,
"search_index": 1
},
{
"default": "0",
"fieldname": "is_deal",
"fieldtype": "Check",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Is Deal",
"search_index": 1
},
{
"fetch_from": "organization.job_title",
"fieldname": "job_title",
@ -225,21 +203,6 @@
"fieldtype": "Tab Break",
"label": "Contact"
},
{
"fieldname": "close_date",
"fieldtype": "Date",
"label": "Close Date"
},
{
"fieldname": "probability",
"fieldtype": "Data",
"label": "Probability"
},
{
"fieldname": "next_step",
"fieldtype": "Data",
"label": "Next Step"
},
{
"fieldname": "contacts",
"fieldtype": "Table",
@ -250,24 +213,25 @@
"fieldname": "section_break_jyxr",
"fieldtype": "Section Break"
},
{
"default": "0",
"fieldname": "created_as_deal",
"fieldtype": "Check",
"hidden": 1,
"label": "Created as Deal"
},
{
"fieldname": "organization",
"fieldtype": "Link",
"label": "Organization",
"options": "CRM Organization"
},
{
"default": "0",
"fieldname": "converted",
"fieldtype": "Check",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Converted"
}
],
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-11-06 15:29:56.868755",
"modified": "2023-11-06 21:53:32.542503",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Lead",

View File

@ -9,7 +9,8 @@
"field_order": [
"title",
"content",
"lead"
"reference_doctype",
"reference_docname"
],
"fields": [
{
@ -27,10 +28,17 @@
"label": "Content"
},
{
"fieldname": "lead",
"default": "CRM Lead",
"fieldname": "reference_doctype",
"fieldtype": "Link",
"label": "Lead",
"options": "CRM Lead"
"label": "Reference Document Type",
"options": "DocType"
},
{
"fieldname": "reference_docname",
"fieldtype": "Dynamic Link",
"label": "Reference Doc",
"options": "reference_doctype"
}
],
"index_web_pages_for_search": 1,
@ -40,7 +48,7 @@
"link_fieldname": "note"
}
],
"modified": "2023-08-28 11:48:42.100802",
"modified": "2023-11-07 13:41:11.249515",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Note",

View File

@ -10,7 +10,8 @@
"title",
"priority",
"start_date",
"lead",
"reference_doctype",
"reference_docname",
"column_break_cqua",
"assigned_to",
"status",
@ -70,15 +71,22 @@
"label": "Description"
},
{
"fieldname": "lead",
"default": "CRM Lead",
"fieldname": "reference_doctype",
"fieldtype": "Link",
"label": "Lead",
"options": "CRM Lead"
"label": "Reference Document Type",
"options": "DocType"
},
{
"fieldname": "reference_docname",
"fieldtype": "Dynamic Link",
"label": "Reference Doc",
"options": "reference_doctype"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-09-28 15:05:27.986420",
"modified": "2023-11-07 13:41:18.277998",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Task",

View File

@ -86,9 +86,10 @@ def update_call_log(call_sid, status=None):
call_log.duration = call_details.duration
call_log.start_time = get_datetime_from_timestamp(call_details.start_time)
call_log.end_time = get_datetime_from_timestamp(call_details.end_time)
call_log.lead = get_lead_from_number(call_log)
if call_log.note and call_log.lead:
frappe.db.set_value("CRM Note", call_log.note, "lead", call_log.lead)
call_log.reference_docname, call_log.reference_doctype = get_lead_or_deal_from_number(call_log)
if call_log.note and call_log.reference_docname:
frappe.db.set_value("CRM Note", call_log.note, "reference_doctype", call_log.reference_doctype)
frappe.db.set_value("CRM Note", call_log.note, "reference_docname", call_log.reference_docname)
call_log.flags.ignore_permissions = True
call_log.save()
frappe.db.commit()
@ -145,13 +146,20 @@ def add_note_to_call_log(call_sid, note):
frappe.db.set_value("CRM Call Log", call_details.parent_call_sid, "note", note)
frappe.db.commit()
def get_lead_from_number(call):
"""Get lead from the given number.
def get_lead_or_deal_from_number(call):
"""Get lead/deal from the given number.
"""
lead = None
doctype = "CRM Lead"
doc = None
if call.type == 'Outgoing':
lead = frappe.db.get_value("CRM Lead", { "mobile_no": call.get('to') })
doc = frappe.get_cached_value(doctype, { "mobile_no": call.get('to') })
if not doc:
doctype = "CRM Deal"
doc = frappe.get_cached_value(doctype, { "mobile_no": call.get('to') })
else:
lead = frappe.db.get_value("CRM Lead", { "mobile_no": call.get('from') })
doc = frappe.get_cached_value(doctype, { "mobile_no": call.get('from') })
if not doc:
doctype = "CRM Deal"
doc = frappe.get_cached_value(doctype, { "mobile_no": call.get('from') })
return lead
return doc, doctype

View File

@ -6,7 +6,7 @@
<Button
v-if="title == 'Calls'"
variant="solid"
@click="makeCall(lead.data.mobile_no)"
@click="makeCall(doc.data.mobile_no)"
>
<template #prefix>
<PhoneIcon class="h-4 w-4" />
@ -561,7 +561,7 @@
v-if="title == 'Calls'"
variant="solid"
label="Make a call"
@click="makeCall(lead.data.mobile_no)"
@click="makeCall(doc.data.mobile_no)"
/>
<Button
v-else-if="title == 'Notes'"
@ -585,20 +585,22 @@
<CommunicationArea
ref="emailBox"
v-if="['Emails', 'Activity'].includes(title)"
v-model="lead"
v-model="doc"
v-model:reload="reload_email"
/>
<NoteModal
v-model="showNoteModal"
v-model:reloadNotes="notes"
:note="note"
:lead="lead.data?.name"
:doctype="doctype"
:doc="doc.data?.name"
/>
<TaskModal
v-model="showTaskModal"
v-model:reloadTasks="tasks"
:task="task"
:lead="lead.data?.name"
:doctype="doctype"
:doc="doc.data?.name"
/>
</template>
<script setup>
@ -652,24 +654,28 @@ const props = defineProps({
type: String,
default: 'Activity',
},
doctype: {
type: String,
default: 'CRM Lead',
},
})
const lead = defineModel()
const doc = defineModel()
const reload = defineModel('reload')
const reload_email = ref(false)
const versions = createResource({
url: 'crm.fcrm.doctype.crm_lead.api.get_activities',
params: { name: lead.value.data.name },
cache: ['activity', lead.value.data.name],
url: 'crm.api.activities.get_activities',
params: { name: doc.value.data.name },
cache: ['activity', doc.value.data.name],
auto: true,
})
const calls = createListResource({
type: 'list',
doctype: 'CRM Call Log',
cache: ['Call Logs', lead.value.data.name],
cache: ['Call Logs', doc.value.data.name],
fields: [
'name',
'caller',
@ -685,7 +691,7 @@ const calls = createListResource({
'creation',
'note',
],
filters: { lead: lead.value.data.name },
filters: { reference_docname: doc.value.data.name },
orderBy: 'creation desc',
pageLength: 999,
auto: true,
@ -722,9 +728,9 @@ const calls = createListResource({
const notes = createListResource({
type: 'list',
doctype: 'CRM Note',
cache: ['Notes', lead.value.data.name],
cache: ['Notes', doc.value.data.name],
fields: ['name', 'title', 'content', 'owner', 'modified'],
filters: { lead: lead.value.data.name },
filters: { reference_docname: doc.value.data.name },
orderBy: 'modified desc',
pageLength: 999,
auto: true,
@ -733,7 +739,7 @@ const notes = createListResource({
const tasks = createListResource({
type: 'list',
doctype: 'CRM Task',
cache: ['Tasks', lead.value.data.name],
cache: ['Tasks', doc.value.data.name],
fields: [
'name',
'title',
@ -745,7 +751,7 @@ const tasks = createListResource({
'status',
'modified',
],
filters: { lead: lead.value.data.name },
filters: { reference_docname: doc.value.data.name },
orderBy: 'modified desc',
pageLength: 999,
auto: true,
@ -805,10 +811,6 @@ function update_activities_details(activity) {
if (activity.activity_type == 'creation') {
activity.type = activity.data
} else if (activity.activity_type == 'deal') {
activity.type = 'converted the lead to this deal'
activity.data.field_label = ''
activity.data.value = ''
} else if (activity.activity_type == 'added') {
activity.type = 'added'
activity.value = 'as'
@ -823,7 +825,7 @@ function update_activities_details(activity) {
}
const emptyText = computed(() => {
let text = 'No emails communications'
let text = 'No email communications'
if (props.title == 'Calls') {
text = 'No call logs'
} else if (props.title == 'Notes') {

View File

@ -42,7 +42,7 @@
},
}"
:editable="showCommunicationBox"
v-model="lead.data"
v-model="doc.data"
placeholder="Add a reply..."
/>
</div>
@ -56,7 +56,7 @@ import { usersStore } from '@/stores/users'
import { call } from 'frappe-ui'
import { ref, watch, computed, defineModel } from 'vue'
const lead = defineModel()
const doc = defineModel()
const reload = defineModel('reload')
const { getUser } = usersStore()
@ -84,14 +84,19 @@ const onNewEmailChange = (value) => {
}
async function sendMail() {
let doctype = 'CRM Lead'
if (doc.value.data.lead) {
doctype = 'CRM Deal'
}
await call('frappe.core.doctype.communication.email.make', {
recipients: lead.value.data.email,
recipients: doc.value.data.email,
cc: '',
bcc: '',
subject: 'Email from Agent',
content: newEmail.value,
doctype: 'CRM Lead',
name: lead.value.data.name,
doctype: doctype,
name: doc.value.data.name,
send_email: 1,
sender: getUser().name,
sender_full_name: getUser()?.full_name || undefined,

View File

@ -18,7 +18,7 @@
>
<ListRowItem :item="item">
<template #prefix>
<div v-if="column.key === 'deal_status'">
<div v-if="column.key === 'status'">
<IndicatorIcon :class="item.color" />
</div>
<div v-else-if="column.key === 'organization'">
@ -30,7 +30,7 @@
size="sm"
/>
</div>
<div v-else-if="column.key === 'lead_owner'">
<div v-else-if="column.key === 'deal_owner'">
<Avatar
v-if="item.full_name"
class="flex items-center"

View File

@ -50,7 +50,11 @@ const props = defineProps({
type: Object,
default: {},
},
lead: {
doctype: {
type: String,
default: 'CRM Lead',
},
doc: {
type: String,
default: '',
},
@ -85,7 +89,8 @@ async function updateNote(close) {
doctype: 'CRM Note',
title: _note.value.title,
content: _note.value.content,
lead: props.lead || '',
reference_doctype: props.doctype,
reference_docname: props.doc || '',
},
})
if (d.name) {

View File

@ -92,15 +92,18 @@ import {
DatePicker,
call,
} from 'frappe-ui'
import { ref, defineModel, h, watch, nextTick } from 'vue'
import { get } from '@vueuse/core'
import { ref, defineModel, watch, nextTick } from 'vue'
const props = defineProps({
task: {
type: Object,
default: {},
},
lead: {
doctype: {
type: String,
default: 'CRM Lead',
},
doc: {
type: String,
default: '',
},
@ -149,7 +152,8 @@ async function updateTask(close) {
let d = await call('frappe.client.insert', {
doc: {
doctype: 'CRM Task',
lead: props.lead || null,
reference_doctype: props.doctype,
reference_docname: props.doc || null,
..._task.value,
},
})

View File

@ -10,7 +10,7 @@
:options="field.options"
v-model="newDeal[field.name]"
>
<template v-if="field.name == 'deal_status'" #prefix>
<template v-if="field.name == 'status'" #prefix>
<IndicatorIcon :class="dealStatuses[newDeal[field.name]].color" />
</template>
</FormControl>
@ -153,7 +153,7 @@ const allFields = [
},
{
label: 'Status',
name: 'deal_status',
name: 'status',
type: 'select',
options: statusDropdownOptions(props.newDeal, 'deal'),
},

View File

@ -148,7 +148,7 @@ const allFields = [
placeholder: 'Organization',
options: getOrganizationOptions(),
change: (option) => {
newLead.organization = option.name
props.newLead.organization = option.value
},
},
{

View File

@ -174,11 +174,6 @@
</template>
</Tabs>
</div>
<ContactModal
v-model="showContactModal"
v-model:reloadContacts="contacts"
:contact="contact"
/>
</template>
<script setup>
@ -197,7 +192,6 @@ import {
createListResource,
} from 'frappe-ui'
import LayoutHeader from '@/components/LayoutHeader.vue'
import ContactModal from '@/components/ContactModal.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import CameraIcon from '@/components/Icons/CameraIcon.vue'
import LeadsIcon from '@/components/Icons/LeadsIcon.vue'
@ -223,8 +217,6 @@ const { getContactByName, contacts } = contactsStore()
const { getUser } = usersStore()
const { getOrganization, getOrganizationOptions } = organizationsStore()
const showContactModal = ref(false)
const props = defineProps({
contactId: {
type: String,
@ -314,7 +306,7 @@ const leads = createListResource({
],
filters: {
email: contact.value.email_id,
is_deal: 0,
converted: 0,
},
orderBy: 'modified desc',
pageLength: 20,
@ -329,7 +321,7 @@ const deals = createListResource({
'name',
'organization',
'annual_revenue',
'deal_status',
'status',
'email',
'mobile_no',
'lead_owner',
@ -337,7 +329,7 @@ const deals = createListResource({
],
filters: {
email: contact.value.email_id,
is_deal: 1,
converted: 1,
},
orderBy: 'modified desc',
pageLength: 20,
@ -396,9 +388,9 @@ function getDealRowObject(deal) {
logo: getOrganization(deal.organization)?.organization_logo,
},
annual_revenue: formatNumberIntoCurrency(deal.annual_revenue),
deal_status: {
label: deal.deal_status,
color: dealStatuses[deal.deal_status]?.color,
status: {
label: deal.status,
color: dealStatuses[deal.status]?.color,
},
email: deal.email,
mobile_no: deal.mobile_no,
@ -464,7 +456,7 @@ const dealColumns = [
},
{
label: 'Status',
key: 'deal_status',
key: 'status',
width: '10rem',
},
{

View File

@ -43,7 +43,7 @@
<script setup>
import LayoutHeader from '@/components/LayoutHeader.vue'
import ContactModal from '@/components/ContactModal.vue'
import ContactModal from '@/components/Modals/ContactModal.vue'
import ContactsListView from '@/components/ListViews/ContactsListView.vue'
import SortBy from '@/components/SortBy.vue'
import Filter from '@/components/Filter.vue'

View File

@ -7,12 +7,12 @@
<FormControl
type="autocomplete"
:options="activeAgents"
:value="getUser(deal.data.lead_owner).full_name"
:value="getUser(deal.data.deal_owner).full_name"
@change="(option) => updateAssignedAgent(option.email)"
placeholder="Deal owner"
>
<template #prefix>
<UserAvatar class="mr-2" :user="deal.data.lead_owner" size="sm" />
<UserAvatar class="mr-2" :user="deal.data.deal_owner" size="sm" />
</template>
<template #item-prefix="{ option }">
<UserAvatar class="mr-2" :user="option.email" size="sm" />
@ -20,17 +20,16 @@
</FormControl>
<Dropdown :options="statusDropdownOptions(deal.data, 'deal', updateDeal)">
<template #default="{ open }">
<Button :label="deal.data.deal_status">
<Button :label="deal.data.status">
<template #prefix>
<IndicatorIcon
:class="dealStatuses[deal.data.deal_status].color"
/>
<IndicatorIcon :class="dealStatuses[deal.data.status].color" />
</template>
<template #suffix
><FeatherIcon
<template #suffix>
<FeatherIcon
:name="open ? 'chevron-up' : 'chevron-down'"
class="h-4 text-gray-600"
/></template>
/>
</template>
</Button>
</template>
</Dropdown>
@ -38,7 +37,12 @@
</LayoutHeader>
<div v-if="deal.data" class="flex h-full overflow-hidden">
<Tabs v-model="tabIndex" v-slot="{ tab }" :tabs="tabs">
<Activities :title="tab.label" v-model:reload="reload" v-model="deal" />
<Activities
doctype="CRM Deal"
:title="tab.label"
v-model:reload="reload"
v-model="deal"
/>
</Tabs>
<div class="flex w-[352px] flex-col justify-between border-l">
<div
@ -254,7 +258,8 @@
:debounce="500"
class="form-control"
/>
<Tooltip :text="field.tooltip"
<Tooltip
:text="field.tooltip"
class="flex h-7 cursor-pointer items-center px-2 py-1"
v-else-if="field.type === 'read_only'"
>
@ -272,7 +277,11 @@
/>
</div>
<ExternalLinkIcon
v-if="field.type === 'link' && field.link && deal.data[field.name]"
v-if="
field.type === 'link' &&
field.link &&
deal.data[field.name]
"
class="h-4 w-4 shrink-0 cursor-pointer text-gray-600"
@click="field.link(deal.data[field.name])"
/>
@ -335,7 +344,7 @@ const props = defineProps({
})
const deal = createResource({
url: 'crm.fcrm.doctype.crm_lead.api.get_lead',
url: 'crm.fcrm.doctype.crm_deal.api.get_deal',
params: { name: props.dealId },
cache: ['deal', props.dealId],
auto: true,
@ -347,7 +356,7 @@ function updateDeal(fieldname, value) {
createResource({
url: 'frappe.client.set_value',
params: {
doctype: 'CRM Lead',
doctype: 'CRM Deal',
name: props.dealId,
fieldname,
value,
@ -377,7 +386,7 @@ function updateDeal(fieldname, value) {
const breadcrumbs = computed(() => {
let items = [{ label: 'Deals', route: { name: 'Deals' } }]
items.push({
label: organization.value.name,
label: organization.value?.name,
route: { name: 'Deal', params: { dealId: deal.data.name } },
})
return items
@ -435,12 +444,16 @@ const detailSections = computed(() => {
type: 'read_only',
name: 'website',
value: organization.value?.website,
tooltip: 'It is a read only field, value is fetched from organization',
tooltip:
'It is a read only field, value is fetched from organization',
},
{
label: 'Amount',
type: 'number',
type: 'read_only',
name: 'annual_revenue',
value: organization.value?.annual_revenue,
tooltip:
'It is a read only field, value is fetched from organization',
},
{
label: 'Close date',
@ -514,8 +527,8 @@ const organization = computed(() => {
})
function updateAssignedAgent(email) {
deal.data.lead_owner = email
updateDeal('lead_owner', email)
deal.data.deal_owner = email
updateDeal('deal_owner', email)
}
</script>

View File

@ -28,8 +28,8 @@
</Dropdown>
</div>
<div class="flex items-center gap-2">
<Filter doctype="CRM Lead" />
<SortBy doctype="CRM Lead" />
<Filter doctype="CRM Deal" />
<SortBy doctype="CRM Deal" />
<Button icon="more-horizontal" />
</div>
</div>
@ -96,23 +96,20 @@ const currentView = ref({
})
function getFilter() {
return {
...(getArgs() || {}),
is_deal: 1,
}
return getArgs() || {}
}
const leads = createListResource({
const deals = createListResource({
type: 'list',
doctype: 'CRM Lead',
doctype: 'CRM Deal',
fields: [
'name',
'organization',
'annual_revenue',
'deal_status',
'status',
'email',
'mobile_no',
'lead_owner',
'deal_owner',
'modified',
],
filters: getFilter(),
@ -125,8 +122,8 @@ watch(
() => getOrderBy(),
(value, old_value) => {
if (!value && !old_value) return
leads.orderBy = getOrderBy() || 'modified desc'
leads.reload()
deals.orderBy = getOrderBy() || 'modified desc'
deals.reload()
},
{ immediate: true }
)
@ -135,8 +132,8 @@ watch(
storage,
useDebounceFn((value, old_value) => {
if (JSON.stringify([...value]) === JSON.stringify([...old_value])) return
leads.filters = getFilter()
leads.reload()
deals.filters = getFilter()
deals.reload()
}, 300),
{ deep: true }
)
@ -154,7 +151,7 @@ const columns = [
},
{
label: 'Status',
key: 'deal_status',
key: 'status',
width: '10rem',
},
{
@ -168,8 +165,8 @@ const columns = [
width: '11rem',
},
{
label: 'Lead owner',
key: 'lead_owner',
label: 'Deal owner',
key: 'deal_owner',
width: '10rem',
},
{
@ -180,28 +177,28 @@ const columns = [
]
const rows = computed(() => {
if (!leads.data) return []
return leads.data.map((lead) => {
if (!deals.data) return []
return deals.data.map((deal) => {
return {
name: lead.name,
name: deal.name,
organization: {
label: lead.organization,
logo: getOrganization(lead.organization)?.organization_logo,
label: deal.organization,
logo: getOrganization(deal.organization)?.organization_logo,
},
annual_revenue: formatNumberIntoCurrency(lead.annual_revenue),
deal_status: {
label: lead.deal_status,
color: dealStatuses[lead.deal_status]?.color,
annual_revenue: formatNumberIntoCurrency(deal.annual_revenue),
status: {
label: deal.status,
color: dealStatuses[deal.status]?.color,
},
email: lead.email,
mobile_no: lead.mobile_no,
lead_owner: {
label: lead.lead_owner && getUser(lead.lead_owner).full_name,
...(lead.lead_owner && getUser(lead.lead_owner)),
email: deal.email,
mobile_no: deal.mobile_no,
deal_owner: {
label: deal.deal_owner && getUser(deal.deal_owner).full_name,
...(deal.deal_owner && getUser(deal.deal_owner)),
},
modified: {
label: dateFormat(lead.modified, dateTooltipFormat),
timeAgo: timeAgo(lead.modified),
label: dateFormat(deal.modified, dateTooltipFormat),
timeAgo: timeAgo(deal.modified),
},
}
})
@ -253,25 +250,19 @@ const viewsDropdownOptions = [
const showNewDialog = ref(false)
let newDeal = reactive({
salutation: '',
first_name: '',
last_name: '',
lead_name: '',
organization: '',
deal_status: 'Qualification',
status: 'Qualification',
email: '',
mobile_no: '',
lead_owner: getUser().email,
is_deal: 1,
created_as_deal: 1,
deal_owner: getUser().email,
})
const createLead = createResource({
const createDeal = createResource({
url: 'frappe.client.insert',
makeParams(values) {
return {
doc: {
doctype: 'CRM Lead',
doctype: 'CRM Deal',
...values,
},
}
@ -281,7 +272,7 @@ const createLead = createResource({
const router = useRouter()
function createNewDeal(close) {
createLead
createDeal
.submit(newDeal, {
validate() {
if (!newDeal.first_name) {

View File

@ -41,7 +41,12 @@
</LayoutHeader>
<div v-if="lead?.data" class="flex h-full overflow-hidden">
<Tabs v-model="tabIndex" v-slot="{ tab }" :tabs="tabs">
<Activities :title="tab.label" v-model:reload="reload" v-model="lead" />
<Activities
doctype="CRM Lead"
:title="tab.label"
v-model:reload="reload"
v-model="lead"
/>
</Tabs>
<div class="flex w-[352px] flex-col justify-between border-l">
<div
@ -336,6 +341,7 @@ import {
Avatar,
Tabs,
Breadcrumbs,
call,
} from 'frappe-ui'
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
@ -372,9 +378,6 @@ function updateLead(fieldname, value) {
},
auto: true,
onSuccess: () => {
if (fieldname == 'is_deal') {
router.push({ name: 'Deal', params: { dealId: lead.data.name } })
}
lead.reload()
contacts.reload()
reload.value = true
@ -565,8 +568,23 @@ const organization = computed(() => {
function convertToDeal() {
lead.data.status = 'Qualified'
lead.data.is_deal = 1
updateLead('is_deal', 1)
lead.data.converted = 1
createDeal(lead.data)
}
async function createDeal(lead) {
let d = await call('frappe.client.insert', {
doc: {
doctype: 'CRM Deal',
organization: lead.organization,
email: lead.email,
mobile_no: lead.mobile_no,
lead: lead.name,
},
})
if (d.name) {
router.push({ name: 'Deal', params: { dealId: d.name } })
}
}
function updateAssignedAgent(email) {

View File

@ -91,7 +91,7 @@ const currentView = ref({
function getFilter() {
return {
...(getArgs() || {}),
is_deal: 0,
converted: 0,
}
}

View File

@ -298,7 +298,7 @@ const leads = createListResource({
],
filters: {
organization: props.organization.name,
is_deal: 0,
converted: 0,
},
orderBy: 'modified desc',
pageLength: 20,
@ -313,7 +313,7 @@ const deals = createListResource({
'name',
'organization',
'annual_revenue',
'deal_status',
'status',
'email',
'mobile_no',
'lead_owner',
@ -321,7 +321,7 @@ const deals = createListResource({
],
filters: {
organization: props.organization.name,
is_deal: 1,
converted: 1,
},
orderBy: 'modified desc',
pageLength: 20,
@ -409,9 +409,9 @@ function getDealRowObject(deal) {
logo: props.organization?.organization_logo,
},
annual_revenue: formatNumberIntoCurrency(deal.annual_revenue),
deal_status: {
label: deal.deal_status,
color: dealStatuses[deal.deal_status]?.color,
status: {
label: deal.status,
color: dealStatuses[deal.status]?.color,
},
email: deal.email,
mobile_no: deal.mobile_no,
@ -498,7 +498,7 @@ const dealColumns = [
},
{
label: 'Status',
key: 'deal_status',
key: 'status',
width: '10rem',
},
{

View File

@ -71,23 +71,15 @@ export const dealStatuses = {
}
export function statusDropdownOptions(data, doctype, action) {
let statuses = leadStatuses
if (doctype == 'deal') {
statuses = dealStatuses
}
let statuses = doctype == 'deal' ? dealStatuses : leadStatuses
let options = []
for (const status in statuses) {
options.push({
label: statuses[status].label,
icon: () => h(IndicatorIcon, { class: statuses[status].color }),
onClick: () => {
if (doctype == 'deal') {
data.deal_status = statuses[status].label
} else {
data.status = statuses[status].label
}
let field = doctype == 'deal' ? 'deal_status' : 'status'
action && action(field, statuses[status].label)
data.status = statuses[status].label
action && action('status', statuses[status].label)
},
})
}