Merge branch 'develop' into onboarding

This commit is contained in:
Shariq Ansari 2025-03-15 16:29:22 +05:30 committed by GitHub
commit 6ebcd0e887
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 98 additions and 255 deletions

View File

@ -30,59 +30,6 @@ def get_users():
return users return users
@frappe.whitelist()
def get_contacts():
contacts = frappe.get_all(
"Contact",
fields=[
"name",
"salutation",
"first_name",
"last_name",
"full_name",
"gender",
"address",
"designation",
"image",
"email_id",
"mobile_no",
"phone",
"company_name",
"modified",
],
order_by="first_name asc",
distinct=True,
)
for contact in contacts:
contact["email_ids"] = frappe.get_all(
"Contact Email",
filters={"parenttype": "Contact", "parent": contact.name},
fields=["name", "email_id", "is_primary"],
)
contact["phone_nos"] = frappe.get_all(
"Contact Phone",
filters={"parenttype": "Contact", "parent": contact.name},
fields=["name", "phone", "is_primary_phone", "is_primary_mobile_no"],
)
return contacts
@frappe.whitelist()
def get_lead_contacts():
lead_contacts = frappe.get_all(
"CRM Lead",
fields=["name", "lead_name", "mobile_no", "phone", "image", "modified"],
filters={"converted": 0},
order_by="lead_name asc",
distinct=True,
)
return lead_contacts
@frappe.whitelist() @frappe.whitelist()
def get_organizations(): def get_organizations():
organizations = frappe.qb.get_query( organizations = frappe.qb.get_query(

View File

@ -20,9 +20,13 @@ def get_deal_contacts(name):
"CRM Contacts", "CRM Contacts",
filters={"parenttype": "CRM Deal", "parent": name}, filters={"parenttype": "CRM Deal", "parent": name},
fields=["contact", "is_primary"], fields=["contact", "is_primary"],
distinct=True,
) )
deal_contacts = [] deal_contacts = []
for contact in contacts: for contact in contacts:
if not contact.contact:
continue
is_primary = contact.is_primary is_primary = contact.is_primary
contact = frappe.get_doc("Contact", contact.contact).as_dict() contact = frappe.get_doc("Contact", contact.contact).as_dict()

View File

@ -115,13 +115,14 @@ class CRMLead(Document):
elif user != agent: elif user != agent:
frappe.share.remove(self.doctype, self.name, user) frappe.share.remove(self.doctype, self.name, user)
def create_contact(self, throw=True): def create_contact(self, existing_contact=None, throw=True):
if not self.lead_name: if not self.lead_name:
self.set_full_name() self.set_full_name()
self.set_lead_name() self.set_lead_name()
existing_contact = self.contact_exists(throw) existing_contact = existing_contact or self.contact_exists(throw)
if existing_contact: if existing_contact:
self.update_lead_contact(existing_contact)
return existing_contact return existing_contact
contact = frappe.new_doc("Contact") contact = frappe.new_doc("Contact")
@ -151,12 +152,15 @@ class CRMLead(Document):
return contact.name return contact.name
def create_organization(self): def create_organization(self, existing_organization=None):
if not self.organization: if not self.organization and not existing_organization:
return return
existing_organization = frappe.db.exists("CRM Organization", {"organization_name": self.organization}) existing_organization = existing_organization or frappe.db.exists(
"CRM Organization", {"organization_name": self.organization}
)
if existing_organization: if existing_organization:
self.db_set("organization", existing_organization)
return existing_organization return existing_organization
organization = frappe.new_doc("CRM Organization") organization = frappe.new_doc("CRM Organization")
@ -172,6 +176,20 @@ class CRMLead(Document):
organization.insert(ignore_permissions=True) organization.insert(ignore_permissions=True)
return organization.name return organization.name
def update_lead_contact(self, contact):
contact = frappe.get_cached_doc("Contact", contact)
frappe.db.set_value(
"CRM Lead",
self.name,
{
"salutation": contact.salutation,
"first_name": contact.first_name,
"last_name": contact.last_name,
"email": contact.email_id,
"mobile_no": contact.mobile_no,
},
)
def contact_exists(self, throw=True): def contact_exists(self, throw=True):
email_exist = frappe.db.exists("Contact Email", {"email_id": self.email}) email_exist = frappe.db.exists("Contact Email", {"email_id": self.email})
phone_exist = frappe.db.exists("Contact Phone", {"phone": self.phone}) phone_exist = frappe.db.exists("Contact Phone", {"phone": self.phone})
@ -383,7 +401,7 @@ class CRMLead(Document):
@frappe.whitelist() @frappe.whitelist()
def convert_to_deal(lead, doc=None, deal=None): def convert_to_deal(lead, doc=None, deal=None, existing_contact=None, existing_organization=None):
if not (doc and doc.flags.get("ignore_permissions")) and not frappe.has_permission( if not (doc and doc.flags.get("ignore_permissions")) and not frappe.has_permission(
"CRM Lead", "write", lead "CRM Lead", "write", lead
): ):
@ -395,7 +413,7 @@ def convert_to_deal(lead, doc=None, deal=None):
lead.db_set("converted", 1) lead.db_set("converted", 1)
if lead.sla and frappe.db.exists("CRM Communication Status", "Replied"): if lead.sla and frappe.db.exists("CRM Communication Status", "Replied"):
lead.db_set("communication_status", "Replied") lead.db_set("communication_status", "Replied")
contact = lead.create_contact(False) contact = lead.create_contact(existing_contact, False)
organization = lead.create_organization() organization = lead.create_organization(existing_organization)
_deal = lead.create_deal(contact, organization, deal) _deal = lead.create_deal(contact, organization, deal)
return _deal return _deal

View File

@ -487,7 +487,6 @@ import FilesUploader from '@/components/FilesUploader/FilesUploader.vue'
import { timeAgo, formatDate, 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 { whatsappEnabled } from '@/composables/settings' import { whatsappEnabled } from '@/composables/settings'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { Button, Tooltip, createResource } from 'frappe-ui' import { Button, Tooltip, createResource } from 'frappe-ui'
@ -506,7 +505,6 @@ import { useRoute } from 'vue-router'
const { makeCall, $socket } = globalStore() const { makeCall, $socket } = globalStore()
const { getUser } = usersStore() const { getUser } = usersStore()
const { getContact, getLeadContact } = contactsStore()
const props = defineProps({ const props = defineProps({
doctype: { doctype: {

View File

@ -203,7 +203,9 @@
label: __('Export'), label: __('Export'),
icon: () => h(ExportIcon, { class: 'h-4 w-4' }), icon: () => h(ExportIcon, { class: 'h-4 w-4' }),
onClick: () => (showExportDialog = true), onClick: () => (showExportDialog = true),
condition: () => !options.hideColumnsButton && route.params.viewType !== 'kanban', condition: () =>
!options.hideColumnsButton &&
route.params.viewType !== 'kanban',
}, },
{ {
label: __('Customize quick filters'), label: __('Customize quick filters'),
@ -535,6 +537,7 @@ onMounted(() => useDebounceFn(reload, 100)())
const isLoading = computed(() => list.value?.loading) const isLoading = computed(() => list.value?.loading)
function reload() { function reload() {
if (isLoading.value) return
list.value.params = getParams() list.value.params = getParams()
list.value.reload() list.value.reload()
} }
@ -803,12 +806,13 @@ const quickFilters = createResource({
url: 'crm.api.doc.get_quick_filters', url: 'crm.api.doc.get_quick_filters',
params: { doctype: props.doctype }, params: { doctype: props.doctype },
cache: ['Quick Filters', props.doctype], cache: ['Quick Filters', props.doctype],
auto: true,
onSuccess(filters) { onSuccess(filters) {
setupNewQuickFilters(filters) setupNewQuickFilters(filters)
}, },
}) })
if (!quickFilters.data) quickFilters.fetch()
function setupNewQuickFilters(filters) { function setupNewQuickFilters(filters) {
newQuickFilters.value = filters.map((f) => ({ newQuickFilters.value = filters.map((f) => ({
label: f.label, label: f.label,

View File

@ -571,10 +571,11 @@ const sections = createResource({
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_sidepanel_sections', url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_sidepanel_sections',
cache: ['sidePanelSections', 'CRM Deal'], cache: ['sidePanelSections', 'CRM Deal'],
params: { doctype: 'CRM Deal' }, params: { doctype: 'CRM Deal' },
auto: true,
transform: (data) => getParsedSections(data), transform: (data) => getParsedSections(data),
}) })
if (!sections.data) sections.fetch()
function getParsedSections(_sections) { function getParsedSections(_sections) {
_sections.forEach((section) => { _sections.forEach((section) => {
if (section.name == 'contacts_section') return if (section.name == 'contacts_section') return
@ -677,7 +678,6 @@ const dealContacts = createResource({
url: 'crm.fcrm.doctype.crm_deal.api.get_deal_contacts', url: 'crm.fcrm.doctype.crm_deal.api.get_deal_contacts',
params: { name: props.dealId }, params: { name: props.dealId },
cache: ['deal_contacts', props.dealId], cache: ['deal_contacts', props.dealId],
auto: true,
transform: (data) => { transform: (data) => {
data.forEach((contact) => { data.forEach((contact) => {
contact.opened = false contact.opened = false
@ -686,6 +686,8 @@ const dealContacts = createResource({
}, },
}) })
if (!dealContacts.data) dealContacts.fetch()
function triggerCall() { function triggerCall() {
let primaryContact = dealContacts.data?.find((c) => c.is_primary) let primaryContact = dealContacts.data?.find((c) => c.is_primary)
let mobile_no = primaryContact.mobile_no || null let mobile_no = primaryContact.mobile_no || null

View File

@ -341,7 +341,6 @@ import { getView } from '@/utils/view'
import { getSettings } from '@/stores/settings' import { getSettings } from '@/stores/settings'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { contactsStore } from '@/stores/contacts'
import { statusesStore } from '@/stores/statuses' import { statusesStore } from '@/stores/statuses'
import { getMeta } from '@/stores/meta' import { getMeta } from '@/stores/meta'
import { import {
@ -370,7 +369,6 @@ import { useActiveTabManager } from '@/composables/useActiveTabManager'
const { brand } = getSettings() const { brand } = getSettings()
const { isManager } = usersStore() const { isManager } = usersStore()
const { $dialog, $socket, makeCall } = globalStore() const { $dialog, $socket, makeCall } = globalStore()
const { getContactByName, contacts } = contactsStore()
const { statusOptions, getLeadStatus, getDealStatus } = statusesStore() const { statusOptions, getLeadStatus, getDealStatus } = statusesStore()
const { doctypeMeta } = getMeta('CRM Lead') const { doctypeMeta } = getMeta('CRM Lead')
@ -603,9 +601,7 @@ const existingOrganizationChecked = ref(false)
const existingContact = ref('') const existingContact = ref('')
const existingOrganization = ref('') const existingOrganization = ref('')
async function convertToDeal(updated) { async function convertToDeal() {
let valueUpdated = false
if (existingContactChecked.value && !existingContact.value) { if (existingContactChecked.value && !existingContact.value) {
createToast({ createToast({
title: __('Error'), title: __('Error'),
@ -626,56 +622,36 @@ async function convertToDeal(updated) {
return return
} }
if (existingContactChecked.value && existingContact.value) { if (!existingContactChecked.value && existingContact.value) {
lead.data.salutation = getContactByName(existingContact.value).salutation existingContact.value = ''
lead.data.first_name = getContactByName(existingContact.value).first_name
lead.data.last_name = getContactByName(existingContact.value).last_name
lead.data.email_id = getContactByName(existingContact.value).email_id
lead.data.mobile_no = getContactByName(existingContact.value).mobile_no
existingContactChecked.value = false
valueUpdated = true
} }
if (existingOrganizationChecked.value && existingOrganization.value) { if (!existingOrganizationChecked.value && existingOrganization.value) {
lead.data.organization = existingOrganization.value existingOrganization.value = ''
existingOrganizationChecked.value = false
valueUpdated = true
} }
if (valueUpdated) { let _deal = await call('crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', {
updateLead( lead: lead.data.name,
{ deal,
salutation: lead.data.salutation, existing_contact: existingContact.value,
first_name: lead.data.first_name, existing_organization: existingOrganization.value,
last_name: lead.data.last_name, }).catch((err) => {
email_id: lead.data.email_id, createToast({
mobile_no: lead.data.mobile_no, title: __('Error converting to deal'),
organization: lead.data.organization, text: __(err.messages?.[0]),
}, icon: 'x',
'', iconClasses: 'text-ink-red-4',
() => convertToDeal(true),
)
showConvertToDealModal.value = false
} else {
let _deal = await call(
'crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal',
{ lead: lead.data.name, deal },
).catch((err) => {
createToast({
title: __('Error converting to deal'),
text: __(err.messages?.[0]),
icon: 'x',
iconClasses: 'text-ink-red-4',
})
}) })
if (_deal) { })
updateOnboardingStep('convert_lead_to_deal') if (_deal) {
capture('convert_lead_to_deal') showConvertToDealModal.value = false
if (updated) { existingContactChecked.value = false
await contacts.reload() existingOrganizationChecked.value = false
} existingContact.value = ''
router.push({ name: 'Deal', params: { dealId: _deal } }) existingOrganization.value = ''
} updateOnboardingStep('convert_lead_to_deal')
capture('convert_lead_to_deal')
router.push({ name: 'Deal', params: { dealId: _deal } })
} }
} }

View File

@ -178,7 +178,6 @@ import { createToast, setupAssignees, setupCustomizations } from '@/utils'
import { getView } from '@/utils/view' import { getView } from '@/utils/view'
import { getSettings } from '@/stores/settings' import { getSettings } from '@/stores/settings'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { contactsStore } from '@/stores/contacts'
import { statusesStore } from '@/stores/statuses' import { statusesStore } from '@/stores/statuses'
import { getMeta } from '@/stores/meta' import { getMeta } from '@/stores/meta'
import { import {
@ -186,6 +185,7 @@ import {
callEnabled, callEnabled,
isMobileView, isMobileView,
} from '@/composables/settings' } from '@/composables/settings'
import { capture } from '@/telemetry'
import { useActiveTabManager } from '@/composables/useActiveTabManager' import { useActiveTabManager } from '@/composables/useActiveTabManager'
import { import {
createResource, createResource,
@ -203,7 +203,6 @@ import { useRouter, useRoute } from 'vue-router'
const { brand } = getSettings() const { brand } = getSettings()
const { $dialog, $socket } = globalStore() const { $dialog, $socket } = globalStore()
const { getContactByName, contacts } = contactsStore()
const { statusOptions, getLeadStatus } = statusesStore() const { statusOptions, getLeadStatus } = statusesStore()
const { doctypeMeta } = getMeta('CRM Lead') const { doctypeMeta } = getMeta('CRM Lead')
const route = useRoute() const route = useRoute()
@ -433,9 +432,7 @@ const existingOrganizationChecked = ref(false)
const existingContact = ref('') const existingContact = ref('')
const existingOrganization = ref('') const existingOrganization = ref('')
async function convertToDeal(updated) { async function convertToDeal() {
let valueUpdated = false
if (existingContactChecked.value && !existingContact.value) { if (existingContactChecked.value && !existingContact.value) {
createToast({ createToast({
title: __('Error'), title: __('Error'),
@ -456,49 +453,28 @@ async function convertToDeal(updated) {
return return
} }
if (existingContactChecked.value && existingContact.value) { if (!existingContactChecked.value && existingContact.value) {
lead.data.salutation = getContactByName(existingContact.value).salutation existingContact.value = ''
lead.data.first_name = getContactByName(existingContact.value).first_name
lead.data.last_name = getContactByName(existingContact.value).last_name
lead.data.email_id = getContactByName(existingContact.value).email_id
lead.data.mobile_no = getContactByName(existingContact.value).mobile_no
existingContactChecked.value = false
valueUpdated = true
} }
if (existingOrganizationChecked.value && existingOrganization.value) { if (!existingOrganizationChecked.value && existingOrganization.value) {
lead.data.organization = existingOrganization.value existingOrganization.value = ''
existingOrganizationChecked.value = false
valueUpdated = true
} }
if (valueUpdated) { let deal = await call('crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', {
updateLead( lead: lead.data.name,
{ deal: {},
salutation: lead.data.salutation, existing_contact: existingContact.value,
first_name: lead.data.first_name, existing_organization: existingOrganization.value,
last_name: lead.data.last_name, })
email_id: lead.data.email_id, if (deal) {
mobile_no: lead.data.mobile_no,
organization: lead.data.organization,
},
'',
() => convertToDeal(true),
)
showConvertToDealModal.value = false showConvertToDealModal.value = false
} else { existingContactChecked.value = false
let deal = await call( existingOrganizationChecked.value = false
'crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', existingContact.value = ''
{ existingOrganization.value = ''
lead: lead.data.name, capture('convert_lead_to_deal')
}, router.push({ name: 'Deal', params: { dealId: deal } })
)
if (deal) {
if (updated) {
await contacts.reload()
}
router.push({ name: 'Deal', params: { dealId: deal } })
}
} }
} }
</script> </script>

View File

@ -1,80 +0,0 @@
import { defineStore } from 'pinia'
import { createResource } from 'frappe-ui'
import { reactive } from 'vue'
export const contactsStore = defineStore('crm-contacts', () => {
let contactsByPhone = reactive({})
let contactsByName = reactive({})
let leadContactsByPhone = reactive({})
let allContacts = reactive([])
const contacts = createResource({
url: 'crm.api.session.get_contacts',
cache: 'contacts',
initialData: [],
auto: true,
transform(contacts) {
for (let contact of contacts) {
// remove special characters from phone number to make it easier to search
// also remove spaces but keep + sign at the start
contact.actual_mobile_no = contact.mobile_no
contact.mobile_no = contact.mobile_no?.replace(/[^0-9+]/g, '')
contactsByPhone[contact.mobile_no] = contact
contactsByName[contact.name] = contact
}
allContacts = [...contacts]
return contacts
},
onError(error) {
if (error && error.exc_type === 'AuthenticationError') {
router.push('/login')
}
},
})
const leadContacts = createResource({
url: 'crm.api.session.get_lead_contacts',
cache: 'lead_contacts',
initialData: [],
auto: true,
transform(lead_contacts) {
for (let lead_contact of lead_contacts) {
// remove special characters from phone number to make it easier to search
// also remove spaces but keep + sign at the start
lead_contact.mobile_no = lead_contact.mobile_no?.replace(/[^0-9+]/g, '')
lead_contact.full_name = lead_contact.lead_name
leadContactsByPhone[lead_contact.mobile_no] = lead_contact
}
return lead_contacts
},
onError(error) {
if (error && error.exc_type === 'AuthenticationError') {
router.push('/login')
}
},
})
function getContact(mobile_no) {
mobile_no = mobile_no?.replace(/[^0-9+]/g, '')
return contactsByPhone[mobile_no]
}
function getContactByName(name) {
return contactsByName[name]
}
function getLeadContact(mobile_no) {
mobile_no = mobile_no?.replace(/[^0-9+]/g, '')
return leadContactsByPhone[mobile_no]
}
function getContacts() {
return allContacts || contacts?.data || []
}
return {
contacts,
getContacts,
getContact,
getContactByName,
getLeadContact,
}
})

View File

@ -24,7 +24,7 @@ export function getMeta(doctype) {
}, },
}) })
if (!doctypeMeta[doctype]) { if (!doctypeMeta[doctype] && !meta.loading) {
meta.fetch() meta.fetch()
} }

View File

@ -4,17 +4,17 @@ import { reactive, ref } from 'vue'
const settings = ref({}) const settings = ref({})
const brand = reactive({}) const brand = reactive({})
export function getSettings() { const _settings = createDocumentResource({
const _settings = createDocumentResource({ doctype: 'FCRM Settings',
doctype: 'FCRM Settings', name: 'FCRM Settings',
name: 'FCRM Settings', onSuccess: (data) => {
onSuccess: (data) => { settings.value = data
settings.value = data getSettings().setupBrand()
setupBrand() return data
return data },
}, })
})
export function getSettings() {
function setupBrand() { function setupBrand() {
brand.name = settings.value?.brand_name brand.name = settings.value?.brand_name
brand.logo = settings.value?.brand_logo brand.logo = settings.value?.brand_logo

View File

@ -75,11 +75,9 @@ export function timeAgo(date) {
return useTimeAgo(date).value return useTimeAgo(date).value
} }
const taskMeta = getMeta('CRM Task')
export function taskStatusOptions(action, data) { export function taskStatusOptions(action, data) {
let options = ['Backlog', 'Todo', 'In Progress', 'Done', 'Canceled'] let options = ['Backlog', 'Todo', 'In Progress', 'Done', 'Canceled']
let statusMeta = taskMeta let statusMeta = getMeta('CRM Task')
.getFields() .getFields()
?.find((field) => field.fieldname == 'status') ?.find((field) => field.fieldname == 'status')
if (statusMeta) { if (statusMeta) {
@ -98,7 +96,7 @@ export function taskStatusOptions(action, data) {
export function taskPriorityOptions(action, data) { export function taskPriorityOptions(action, data) {
let options = ['Low', 'Medium', 'High'] let options = ['Low', 'Medium', 'High']
let priorityMeta = taskMeta let priorityMeta = getMeta('CRM Task')
.getFields() .getFields()
?.find((field) => field.fieldname == 'priority') ?.find((field) => field.fieldname == 'priority')
if (priorityMeta) { if (priorityMeta) {