From 94915397739593442191f541d083674428560d23 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 3 Feb 2025 13:06:12 +0530 Subject: [PATCH 01/11] fix: Cannot add annual revenue --- frontend/src/components/Modals/DealModal.vue | 5 +++-- frontend/src/components/Modals/LeadModal.vue | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/Modals/DealModal.vue b/frontend/src/components/Modals/DealModal.vue index a869816e..9c38f1ef 100644 --- a/frontend/src/components/Modals/DealModal.vue +++ b/frontend/src/components/Modals/DealModal.vue @@ -197,8 +197,9 @@ function createDeal() { validate() { error.value = null if (deal.annual_revenue) { - deal.annual_revenue = deal.annual_revenue.replace(/,/g, '') - if (isNaN(deal.annual_revenue)) { + if (typeof deal.annual_revenue === 'string') { + deal.annual_revenue = deal.annual_revenue.replace(/,/g, '') + } else if (isNaN(deal.annual_revenue)) { error.value = __('Annual Revenue should be a number') return error.value } diff --git a/frontend/src/components/Modals/LeadModal.vue b/frontend/src/components/Modals/LeadModal.vue index 81b44903..dbe21ba6 100644 --- a/frontend/src/components/Modals/LeadModal.vue +++ b/frontend/src/components/Modals/LeadModal.vue @@ -140,8 +140,9 @@ function createNewLead() { return error.value } if (lead.annual_revenue) { - lead.annual_revenue = lead.annual_revenue.replace(/,/g, '') - if (isNaN(lead.annual_revenue)) { + if (typeof lead.annual_revenue === 'string') { + lead.annual_revenue = lead.annual_revenue.replace(/,/g, '') + } else if (isNaN(lead.annual_revenue)) { error.value = __('Annual Revenue should be a number') return error.value } From de422f68e1b5890f3bed28bfff810da75e2fcc7b Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 3 Feb 2025 13:37:21 +0530 Subject: [PATCH 02/11] fix: mobile no is not added when created new contact --- frontend/src/components/Modals/ContactModal.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Modals/ContactModal.vue b/frontend/src/components/Modals/ContactModal.vue index 1cf3c8a2..840fa14f 100644 --- a/frontend/src/components/Modals/ContactModal.vue +++ b/frontend/src/components/Modals/ContactModal.vue @@ -85,9 +85,9 @@ async function createContact() { delete _contact.value.email_id } - if (_contact.value.actual_mobile_no) { - _contact.value.phone_nos = [{ phone: _contact.value.actual_mobile_no }] - delete _contact.value.actual_mobile_no + if (_contact.value.mobile_no) { + _contact.value.phone_nos = [{ phone: _contact.value.mobile_no }] + delete _contact.value.mobile_no } const doc = await call('frappe.client.insert', { From 74d516192c3a04d410ccfd427c33bb4cef6a1083 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 3 Feb 2025 14:16:55 +0530 Subject: [PATCH 03/11] fix: show dial whom number for incoming call and to number for outgoing call --- frontend/src/components/Telephony/ExotelCallUI.vue | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Telephony/ExotelCallUI.vue b/frontend/src/components/Telephony/ExotelCallUI.vue index 3408d11c..11e7c6ba 100644 --- a/frontend/src/components/Telephony/ExotelCallUI.vue +++ b/frontend/src/components/Telephony/ExotelCallUI.vue @@ -434,10 +434,9 @@ function setup() { if ( !showCallPopup.value && !showSmallCallPopup.value && - data.AgentEmail && - data.AgentEmail == (user || user.value) + (!data.AgentEmail || data.AgentEmail == (user || user.value)) ) { - phoneNumber.value = data.CallTo || data.To + phoneNumber.value = data.DialWhomNumber || data.To showCallPopup.value = true } }) From 29b4ca566895692ec433cb03f71ecf0aaae410c6 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 3 Feb 2025 16:18:14 +0530 Subject: [PATCH 04/11] fix: set org address and primary contact while creating quotation from deal --- .../erpnext_crm_settings.py | 104 +++++++++++------- 1 file changed, 65 insertions(+), 39 deletions(-) diff --git a/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.py b/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.py index 2e15a011..1d63807e 100644 --- a/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.py +++ b/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.py @@ -1,22 +1,24 @@ # Copyright (c) 2024, Frappe and contributors # For license information, please see license.txt +import json + import frappe from frappe import _ from frappe.custom.doctype.property_setter.property_setter import make_property_setter -from frappe.model.document import Document from frappe.frappeclient import FrappeClient +from frappe.model.document import Document from frappe.utils import get_url_to_form, get_url_to_list -import json + class ERPNextCRMSettings(Document): def validate(self): if self.enabled: self.validate_if_erpnext_installed() self.add_quotation_to_option() - self.create_custom_fields() + self.create_custom_fields() self.create_crm_form_script() - + def validate_if_erpnext_installed(self): if not self.is_erpnext_in_different_site: if "erpnext" not in frappe.get_installed_apps(): @@ -37,6 +39,7 @@ class ERPNextCRMSettings(Document): def create_custom_fields(self): if not self.is_erpnext_in_different_site: from erpnext.crm.frappe_crm_api import create_custom_fields_for_frappe_crm + create_custom_fields_for_frappe_crm() else: self.create_custom_fields_in_remote_site() @@ -48,22 +51,24 @@ class ERPNextCRMSettings(Document): except Exception: frappe.log_error( frappe.get_traceback(), - f"Error while creating custom field in the remote erpnext site: {self.erpnext_site_url}" + f"Error while creating custom field in the remote erpnext site: {self.erpnext_site_url}", ) frappe.throw("Error while creating custom field in ERPNext, check error log for more details") def create_crm_form_script(self): if not frappe.db.exists("CRM Form Script", "Create Quotation from CRM Deal"): script = get_crm_form_script() - frappe.get_doc({ - "doctype": "CRM Form Script", - "name": "Create Quotation from CRM Deal", - "dt": "CRM Deal", - "view": "Form", - "script": script, - "enabled": 1, - "is_standard": 1 - }).insert() + frappe.get_doc( + { + "doctype": "CRM Form Script", + "name": "Create Quotation from CRM Deal", + "dt": "CRM Deal", + "view": "Form", + "script": script, + "enabled": 1, + "is_standard": 1, + } + ).insert() @frappe.whitelist() def reset_erpnext_form_script(self): @@ -77,14 +82,14 @@ class ERPNextCRMSettings(Document): frappe.log_error(frappe.get_traceback(), "Error while resetting form script") return False + def get_erpnext_site_client(erpnext_crm_settings): site_url = erpnext_crm_settings.erpnext_site_url api_key = erpnext_crm_settings.api_key api_secret = erpnext_crm_settings.get_password("api_secret", raise_exception=False) - return FrappeClient( - site_url, api_key=api_key, api_secret=api_secret - ) + return FrappeClient(site_url, api_key=api_key, api_secret=api_secret) + @frappe.whitelist() def get_customer_link(crm_deal): @@ -107,7 +112,7 @@ def get_customer_link(crm_deal): except Exception: frappe.log_error( frappe.get_traceback(), - f"Error while fetching customer in remote site: {erpnext_crm_settings.erpnext_site_url}" + f"Error while fetching customer in remote site: {erpnext_crm_settings.erpnext_site_url}", ) frappe.throw(_("Error while fetching customer in ERPNext, check error log for more details")) @@ -118,23 +123,28 @@ def get_quotation_url(crm_deal, organization): if not erpnext_crm_settings.enabled: frappe.throw(_("ERPNext is not integrated with the CRM")) + contact = get_contact(crm_deal) + address = get_organization_address(organization).get("name") if organization else None + if not erpnext_crm_settings.is_erpnext_in_different_site: quotation_url = get_url_to_list("Quotation") - return f"{quotation_url}/new?quotation_to=CRM Deal&crm_deal={crm_deal}&party_name={crm_deal}&company={erpnext_crm_settings.erpnext_company}" + return f"{quotation_url}/new?quotation_to=CRM Deal&crm_deal={crm_deal}&party_name={crm_deal}&company={erpnext_crm_settings.erpnext_company}&contact_person={contact}&customer_address={address}" else: site_url = erpnext_crm_settings.get("erpnext_site_url") quotation_url = f"{site_url}/app/quotation" prospect = create_prospect_in_remote_site(crm_deal, erpnext_crm_settings) - return f"{quotation_url}/new?quotation_to=Prospect&crm_deal={crm_deal}&party_name={prospect}&company={erpnext_crm_settings.erpnext_company}" + return f"{quotation_url}/new?quotation_to=Prospect&crm_deal={crm_deal}&party_name={prospect}&company={erpnext_crm_settings.erpnext_company}&contact_person={contact}&customer_address={address}" + def create_prospect_in_remote_site(crm_deal, erpnext_crm_settings): try: client = get_erpnext_site_client(erpnext_crm_settings) - doc = frappe.get_doc("CRM Deal", crm_deal) + doc = frappe.get_cached_doc("CRM Deal", crm_deal) contacts = get_contacts(doc) address = get_organization_address(doc.organization) - return client.post_api("erpnext.crm.frappe_crm_api.create_prospect_against_crm_deal", + return client.post_api( + "erpnext.crm.frappe_crm_api.create_prospect_against_crm_deal", { "organization": doc.organization, "lead_name": doc.lead_name, @@ -147,32 +157,47 @@ def create_prospect_in_remote_site(crm_deal, erpnext_crm_settings): "annual_revenue": doc.annual_revenue, "contacts": json.dumps(contacts), "erpnext_company": erpnext_crm_settings.erpnext_company, - "address": address.as_dict() if address else None + "address": address.as_dict() if address else None, }, ) except Exception: frappe.log_error( frappe.get_traceback(), - f"Error while creating prospect in remote site: {erpnext_crm_settings.erpnext_site_url}" + f"Error while creating prospect in remote site: {erpnext_crm_settings.erpnext_site_url}", ) frappe.throw(_("Error while creating prospect in ERPNext, check error log for more details")) + +def get_contact(crm_deal): + doc = frappe.get_cached_doc("CRM Deal", crm_deal) + contact = None + for c in doc.contacts: + if c.is_primary: + contact = c.contact + break + + return contact + + def get_contacts(doc): contacts = [] for c in doc.contacts: - contacts.append({ - "contact": c.contact, - "full_name": c.full_name, - "email": c.email, - "mobile_no": c.mobile_no, - "gender": c.gender, - "is_primary": c.is_primary, - }) + contacts.append( + { + "contact": c.contact, + "full_name": c.full_name, + "email": c.email, + "mobile_no": c.mobile_no, + "gender": c.gender, + "is_primary": c.is_primary, + } + ) return contacts + def get_organization_address(organization): address = frappe.db.get_value("CRM Organization", organization, "address") - address = frappe.get_doc("Address", address) if address else None + address = frappe.get_cached_doc("Address", address) if address else None if not address: return None return { @@ -188,6 +213,7 @@ def get_organization_address(organization): "pincode": address.pincode, } + def create_customer_in_erpnext(doc, method): erpnext_crm_settings = frappe.get_single("ERPNext CRM Settings") if ( @@ -196,7 +222,7 @@ def create_customer_in_erpnext(doc, method): or doc.status != erpnext_crm_settings.deal_status ): return - + contacts = get_contacts(doc) address = get_organization_address(doc.organization) customer = { @@ -213,26 +239,26 @@ def create_customer_in_erpnext(doc, method): } if not erpnext_crm_settings.is_erpnext_in_different_site: from erpnext.crm.frappe_crm_api import create_customer + create_customer(customer) else: create_customer_in_remote_site(customer, erpnext_crm_settings) frappe.publish_realtime("crm_customer_created") + def create_customer_in_remote_site(customer, erpnext_crm_settings): client = get_erpnext_site_client(erpnext_crm_settings) try: client.post_api("erpnext.crm.frappe_crm_api.create_customer", customer) except Exception: - frappe.log_error( - frappe.get_traceback(), - "Error while creating customer in remote site" - ) + frappe.log_error(frappe.get_traceback(), "Error while creating customer in remote site") frappe.throw(_("Error while creating customer in ERPNext, check error log for more details")) + @frappe.whitelist() def get_crm_form_script(): - return """ + return """ async function setupForm({ doc, call, $dialog, updateField, createToast }) { let actions = []; let is_erpnext_integration_enabled = await call("frappe.client.get_single_value", {doctype: "ERPNext CRM Settings", field: "enabled"}); From ce07060bd89825419e3e06934655b05e1a18c1bc Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 3 Feb 2025 16:18:37 +0530 Subject: [PATCH 05/11] fix: fileuploader erroring out --- frontend/src/components/FilesUploader/FilesUploaderArea.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/FilesUploader/FilesUploaderArea.vue b/frontend/src/components/FilesUploader/FilesUploaderArea.vue index 85689909..cf69c2fb 100644 --- a/frontend/src/components/FilesUploader/FilesUploaderArea.vue +++ b/frontend/src/components/FilesUploader/FilesUploaderArea.vue @@ -216,7 +216,7 @@ async function startCamera() { } function stopStream() { - stream.value.getTracks().forEach((track) => track.stop()) + stream.value?.getTracks()?.forEach((track) => track.stop()) showCamera.value = false cameraImage.value = null } From 6a16ecac0c018fd1a830923dfce71a4aa9abfa65 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 3 Feb 2025 19:28:27 +0530 Subject: [PATCH 06/11] fix: update phone number in Call UI --- .../src/components/Telephony/ExotelCallUI.vue | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/Telephony/ExotelCallUI.vue b/frontend/src/components/Telephony/ExotelCallUI.vue index 11e7c6ba..f34c1f4d 100644 --- a/frontend/src/components/Telephony/ExotelCallUI.vue +++ b/frontend/src/components/Telephony/ExotelCallUI.vue @@ -431,12 +431,15 @@ function setup() { callStatus.value = updateStatus(data) const { user } = sessionStore() - if ( - !showCallPopup.value && - !showSmallCallPopup.value && - (!data.AgentEmail || data.AgentEmail == (user || user.value)) - ) { - phoneNumber.value = data.DialWhomNumber || data.To + if (!showCallPopup.value && !showSmallCallPopup.value) { + if (data.AgentEmail && data.AgentEmail == (user || user.value)) { + // Incoming call + phoneNumber.value = data.CallFrom || data.From + } else { + // Outgoing call + phoneNumber.value = data.To + } + showCallPopup.value = true } }) From f5144f429ca950956efa384b1e6cced15000acde Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 3 Feb 2025 20:17:47 +0530 Subject: [PATCH 07/11] fix: hide column from listview if field is hidden --- crm/api/doc.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crm/api/doc.py b/crm/api/doc.py index ad2cf99c..eba670c7 100644 --- a/crm/api/doc.py +++ b/crm/api/doc.py @@ -255,6 +255,8 @@ def get_data( if hasattr(_list, "default_list_data"): default_rows = _list.default_list_data().get("rows") + meta = frappe.get_meta(doctype) + if view_type != "kanban": if columns or rows: custom_view = True @@ -296,6 +298,11 @@ def get_data( if column.get("key") == "_liked_by" and column.get("width") == "10rem": column["width"] = "50px" + # remove column if column.hidden is True + column_meta = meta.get_field(column.get("key")) + if column_meta and column_meta.get("hidden"): + columns.remove(column) + # check if rows has group_by_field if not add it if group_by_field and group_by_field not in rows: rows.append(group_by_field) From 1eb63de3ce53bb5304e7b0189e660bc39491f9fc Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 3 Feb 2025 20:44:47 +0530 Subject: [PATCH 08/11] fix: export rows with default filters applied --- frontend/src/components/ViewControls.vue | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/ViewControls.vue b/frontend/src/components/ViewControls.vue index 03ed5662..12f021ca 100644 --- a/frontend/src/components/ViewControls.vue +++ b/frontend/src/components/ViewControls.vue @@ -461,7 +461,12 @@ const export_all = ref(false) async function exportRows() { let fields = JSON.stringify(list.value.data.columns.map((f) => f.key)) - let filters = JSON.stringify(list.value.params.filters) + + let filters = JSON.stringify({ + ...props.filters, + ...list.value.params.filters, + }) + let order_by = list.value.params.order_by let page_length = list.value.params.page_length if (export_all.value) { From 69f14159cfbcfac8a0cbc3b1a076015490eac33c Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 4 Feb 2025 12:47:35 +0530 Subject: [PATCH 09/11] fix: cannot add/create contact from deal --- frontend/src/components/SidePanelLayout.vue | 42 +--- frontend/src/pages/Deal.vue | 209 ++++++++++++-------- 2 files changed, 131 insertions(+), 120 deletions(-) diff --git a/frontend/src/components/SidePanelLayout.vue b/frontend/src/components/SidePanelLayout.vue index 4b47125c..553b7d0e 100644 --- a/frontend/src/components/SidePanelLayout.vue +++ b/frontend/src/components/SidePanelLayout.vue @@ -15,40 +15,16 @@ :opened="section.opened" > -
-
- - {{ __('Loading...') }} + +
@@ -278,6 +312,7 @@ import OrganizationModal from '@/components/Modals/OrganizationModal.vue' import AssignTo from '@/components/AssignTo.vue' import FilesUploader from '@/components/FilesUploader/FilesUploader.vue' import ContactModal from '@/components/Modals/ContactModal.vue' +import Link from '@/components/Controls/Link.vue' import Section from '@/components/Section.vue' import SidePanelLayout from '@/components/SidePanelLayout.vue' import SLASection from '@/components/SLASection.vue' From 82a728e041f5b2591a10001d2ecbe37746ed1061 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 4 Feb 2025 12:59:31 +0530 Subject: [PATCH 10/11] fix: also fixed for mobile view --- frontend/src/pages/MobileDeal.vue | 220 +++++++++++++++++------------- 1 file changed, 125 insertions(+), 95 deletions(-) diff --git a/frontend/src/pages/MobileDeal.vue b/frontend/src/pages/MobileDeal.vue index 6972d0f5..0277bcd2 100644 --- a/frontend/src/pages/MobileDeal.vue +++ b/frontend/src/pages/MobileDeal.vue @@ -65,112 +65,141 @@ v-model="deal.data" :sections="sections.data" doctype="CRM Deal" - v-slot="{ section }" @update="updateField" @reload="sections.reload" > -
-
- - {{ __('Loading...') }} + +
@@ -224,6 +253,7 @@ import OrganizationModal from '@/components/Modals/OrganizationModal.vue' import AssignTo from '@/components/AssignTo.vue' import ContactModal from '@/components/Modals/ContactModal.vue' import Section from '@/components/Section.vue' +import Link from '@/components/Controls/Link.vue' import SidePanelLayout from '@/components/SidePanelLayout.vue' import SLASection from '@/components/SLASection.vue' import CustomActions from '@/components/CustomActions.vue' From ccb1636efd42c0930301f90f1e3be4802a80c31c Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 4 Feb 2025 13:11:55 +0530 Subject: [PATCH 11/11] fix: same contact can be added multiple time --- frontend/src/pages/Deal.vue | 9 +++++++++ frontend/src/pages/MobileDeal.vue | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/frontend/src/pages/Deal.vue b/frontend/src/pages/Deal.vue index 2c352fb8..cd83158a 100644 --- a/frontend/src/pages/Deal.vue +++ b/frontend/src/pages/Deal.vue @@ -606,6 +606,15 @@ function contactOptions(contact) { } async function addContact(contact) { + if (dealContacts.data?.find((c) => c.name === contact)) { + createToast({ + title: __('Contact already added'), + icon: 'x', + iconClasses: 'text-ink-red-3', + }) + return + } + let d = await call('crm.fcrm.doctype.crm_deal.crm_deal.add_contact', { deal: props.dealId, contact, diff --git a/frontend/src/pages/MobileDeal.vue b/frontend/src/pages/MobileDeal.vue index 0277bcd2..b4fc37e5 100644 --- a/frontend/src/pages/MobileDeal.vue +++ b/frontend/src/pages/MobileDeal.vue @@ -534,6 +534,15 @@ function contactOptions(contact) { } async function addContact(contact) { + if (dealContacts.data?.find((c) => c.name === contact)) { + createToast({ + title: __('Contact already added'), + icon: 'x', + iconClasses: 'text-ink-red-3', + }) + return + } + let d = await call('crm.fcrm.doctype.crm_deal.crm_deal.add_contact', { deal: props.dealId, contact,