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) 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"}); 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 } 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', { 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 } 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" > 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) { diff --git a/frontend/src/pages/Deal.vue b/frontend/src/pages/Deal.vue index 40ce8bf3..cd83158a 100644 --- a/frontend/src/pages/Deal.vue +++ b/frontend/src/pages/Deal.vue @@ -125,102 +125,136 @@ :sections="sections.data" :addContact="addContact" doctype="CRM Deal" - v-slot="{ section }" @update="updateField" @reload="sections.reload" > -
-
- - {{ __('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' @@ -571,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 6972d0f5..b4fc37e5 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' @@ -504,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,