diff --git a/crm/fcrm/doctype/crm_deal/crm_deal.json b/crm/fcrm/doctype/crm_deal/crm_deal.json index e5c973d8c..34a22e35a 100644 --- a/crm/fcrm/doctype/crm_deal/crm_deal.json +++ b/crm/fcrm/doctype/crm_deal/crm_deal.json @@ -80,8 +80,7 @@ "fetch_from": ".website", "fieldname": "website", "fieldtype": "Data", - "label": "Website", - "options": "URL" + "label": "Website" }, { "fieldname": "close_date", @@ -339,7 +338,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-06-20 12:55:41.602364", + "modified": "2024-09-17 18:34:15.873610", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Deal", @@ -371,8 +370,10 @@ "write": 1 } ], + "show_title_field_in_link": 1, "sort_field": "modified", "sort_order": "DESC", "states": [], + "title_field": "organization", "track_changes": 1 } \ No newline at end of file diff --git a/crm/fcrm/doctype/crm_form_script/crm_form_script.js b/crm/fcrm/doctype/crm_form_script/crm_form_script.js index e8ba8b974..2c8c2ff1b 100644 --- a/crm/fcrm/doctype/crm_form_script/crm_form_script.js +++ b/crm/fcrm/doctype/crm_form_script/crm_form_script.js @@ -18,6 +18,10 @@ frappe.ui.form.on("CRM Form Script", { ); } + if (!frappe.boot.developer_mode) { + frm.toggle_enable("is_standard", 0); + } + frm.trigger("add_enable_button"); }, diff --git a/crm/fcrm/doctype/crm_form_script/crm_form_script.json b/crm/fcrm/doctype/crm_form_script/crm_form_script.json index 1cc14d9a3..9246913a0 100644 --- a/crm/fcrm/doctype/crm_form_script/crm_form_script.json +++ b/crm/fcrm/doctype/crm_form_script/crm_form_script.json @@ -35,6 +35,7 @@ "default": "0", "fieldname": "enabled", "fieldtype": "Check", + "hidden": 1, "label": "Enabled" }, { @@ -64,7 +65,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-09-11 12:56:09.288849", + "modified": "2024-09-16 19:40:19.340948", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Form Script", diff --git a/crm/fcrm/doctype/crm_form_script/crm_form_script.py b/crm/fcrm/doctype/crm_form_script/crm_form_script.py index 682a97fc2..bb35c851c 100644 --- a/crm/fcrm/doctype/crm_form_script/crm_form_script.py +++ b/crm/fcrm/doctype/crm_form_script/crm_form_script.py @@ -14,7 +14,7 @@ def validate(self): or frappe.flags.in_test or frappe.flags.in_fixtures ) - if in_user_env and self.is_standard and not frappe.conf.developer_mode: + if in_user_env and not self.is_new() and self.is_standard and not frappe.conf.developer_mode: # only enabled can be changed for standard form scripts if self.has_value_changed("enabled"): enabled_value = self.enabled diff --git a/crm/fcrm/doctype/crm_lead/crm_lead.json b/crm/fcrm/doctype/crm_lead/crm_lead.json index ced8e3cb4..786c03a53 100644 --- a/crm/fcrm/doctype/crm_lead/crm_lead.json +++ b/crm/fcrm/doctype/crm_lead/crm_lead.json @@ -107,8 +107,7 @@ { "fieldname": "website", "fieldtype": "Data", - "label": "Website", - "options": "URL" + "label": "Website" }, { "fieldname": "mobile_no", @@ -291,7 +290,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2024-02-05 00:58:07.321058", + "modified": "2024-09-17 18:36:57.289897", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Lead", @@ -325,6 +324,7 @@ ], "sender_field": "email", "sender_name_field": "first_name", + "show_title_field_in_link": 1, "sort_field": "modified", "sort_order": "DESC", "states": [], diff --git a/crm/fcrm/doctype/crm_organization/crm_organization.json b/crm/fcrm/doctype/crm_organization/crm_organization.json index d371ca529..34252d1c6 100644 --- a/crm/fcrm/doctype/crm_organization/crm_organization.json +++ b/crm/fcrm/doctype/crm_organization/crm_organization.json @@ -28,8 +28,7 @@ { "fieldname": "website", "fieldtype": "Data", - "label": "Website", - "options": "URL" + "label": "Website" }, { "fieldname": "organization_logo", @@ -80,7 +79,7 @@ "image_field": "organization_logo", "index_web_pages_for_search": 1, "links": [], - "modified": "2024-09-13 15:52:05.106389", + "modified": "2024-09-17 18:37:10.341062", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Organization", diff --git a/crm/fcrm/doctype/erpnext_crm_settings/__init__.py b/crm/fcrm/doctype/erpnext_crm_settings/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.js b/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.js new file mode 100644 index 000000000..535e83fbd --- /dev/null +++ b/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.js @@ -0,0 +1,21 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on("ERPNext CRM Settings", { + refresh(frm) { + if (!frm.doc.enabled) return; + frm.add_custom_button(__("Reset ERPNext Form Script"), () => { + frappe.confirm( + __( + "Are you sure you want to reset 'Create Quotation from CRM Deal' Form Script?" + ), + () => frm.trigger("reset_erpnext_form_script") + ); + }); + }, + async reset_erpnext_form_script(frm) { + let script = await frm.call("reset_erpnext_form_script"); + script.message && + frappe.msgprint(__("Form Script updated successfully")); + }, +}); diff --git a/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.json b/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.json new file mode 100644 index 000000000..9a6f0f75e --- /dev/null +++ b/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.json @@ -0,0 +1,124 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-07-02 15:23:17.022214", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "enabled", + "is_erpnext_in_different_site", + "column_break_vfru", + "erpnext_company", + "section_break_oubd", + "erpnext_site_url", + "column_break_fllx", + "api_key", + "api_secret", + "section_break_jnbn", + "create_customer_on_status_change", + "column_break_kbhw", + "deal_status" + ], + "fields": [ + { + "depends_on": "eval:doc.enabled && doc.is_erpnext_in_different_site", + "fieldname": "api_key", + "fieldtype": "Data", + "label": "API Key", + "mandatory_depends_on": "is_erpnext_in_different_site" + }, + { + "depends_on": "eval:doc.enabled && doc.is_erpnext_in_different_site", + "fieldname": "api_secret", + "fieldtype": "Password", + "label": "API Secret", + "mandatory_depends_on": "is_erpnext_in_different_site" + }, + { + "depends_on": "enabled", + "fieldname": "section_break_oubd", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_fllx", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.enabled && doc.is_erpnext_in_different_site", + "fieldname": "erpnext_site_url", + "fieldtype": "Data", + "label": "ERPNext Site URL", + "mandatory_depends_on": "is_erpnext_in_different_site" + }, + { + "depends_on": "enabled", + "fieldname": "erpnext_company", + "fieldtype": "Data", + "label": "Company in ERPNext Site", + "mandatory_depends_on": "enabled" + }, + { + "fieldname": "column_break_vfru", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + }, + { + "default": "0", + "depends_on": "enabled", + "fieldname": "is_erpnext_in_different_site", + "fieldtype": "Check", + "label": "Is ERPNext installed on a different site?" + }, + { + "fieldname": "section_break_jnbn", + "fieldtype": "Section Break" + }, + { + "default": "0", + "depends_on": "enabled", + "fieldname": "create_customer_on_status_change", + "fieldtype": "Check", + "label": "Create customer on status change" + }, + { + "fieldname": "column_break_kbhw", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.enabled && doc.create_customer_on_status_change", + "fieldname": "deal_status", + "fieldtype": "Link", + "label": "Deal Status", + "mandatory_depends_on": "create_customer_on_status_change", + "options": "CRM Deal Status" + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2024-09-17 19:21:11.060901", + "modified_by": "Administrator", + "module": "FCRM", + "name": "ERPNext CRM Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.py b/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.py new file mode 100644 index 000000000..a98f86074 --- /dev/null +++ b/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.py @@ -0,0 +1,262 @@ +# Copyright (c) 2024, Frappe and contributors +# For license information, please see license.txt + +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.utils import get_url_to_form +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_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(): + frappe.throw(_("ERPNext is not installed in the current site")) + + def add_quotation_to_option(self): + if not self.is_erpnext_in_different_site: + if not frappe.db.exists("Property Setter", {"name": "Quotation-quotation_to-link_filters"}): + make_property_setter( + doctype="Quotation", + fieldname="quotation_to", + property="link_filters", + value='[["DocType","name","in", ["Customer", "Lead", "Prospect", "Frappe CRM Deal"]]]', + property_type="JSON", + validate_fields_for_doctype=False, + ) + + 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() + + def create_custom_fields_in_remote_site(self): + client = get_erpnext_site_client(self) + try: + client.post_api("erpnext.crm.frappe_crm_api.create_custom_fields_for_frappe_crm") + except Exception: + frappe.log_error( + frappe.get_traceback(), + 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.whitelist() + def reset_erpnext_form_script(self): + try: + if frappe.db.exists("CRM Form Script", "Create Quotation from CRM Deal"): + script = get_crm_form_script() + frappe.db.set_value("CRM Form Script", "Create Quotation from CRM Deal", "script", script) + return True + return False + except Exception: + 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 + ) + +@frappe.whitelist() +def get_customer_link(crm_deal): + erpnext_crm_settings = frappe.get_single("ERPNext CRM Settings") + if not erpnext_crm_settings.enabled: + frappe.throw(_("ERPNext is not integrated with the CRM")) + + if not erpnext_crm_settings.is_erpnext_in_different_site: + customer_url = get_url_to_form("Customer") + customer = frappe.db.exists("Customer", {"crm_deal": crm_deal}) + if customer: + return f"{customer_url}/{customer}" + else: + return "" + else: + client = get_erpnext_site_client(erpnext_crm_settings) + try: + customer = client.get_list("Customer", {"crm_deal": crm_deal})[0]["name"] + if customer: + return f"{erpnext_crm_settings.erpnext_site_url}/app/customer/{customer}" + else: + return "" + except Exception: + frappe.log_error( + frappe.get_traceback(), + 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")) + + +@frappe.whitelist() +def get_quotation_url(crm_deal, organization): + erpnext_crm_settings = frappe.get_single("ERPNext CRM Settings") + if not erpnext_crm_settings.enabled: + frappe.throw(_("ERPNext is not integrated with the CRM")) + + if not erpnext_crm_settings.is_erpnext_in_different_site: + quotation_url = get_url_to_form("Quotation") + return f"{quotation_url}/new?quotation_to=CRM Deal&crm_deal={crm_deal}&party_name={crm_deal}&company={erpnext_crm_settings.erpnext_company}" + 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}" + +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) + contacts = get_contacts(doc) + address = get_organization_address(doc.organization) + return client.post_api("erpnext.crm.frappe_crm_api.create_prospect_against_crm_deal", + { + "organization": doc.organization, + "lead_name": doc.lead_name, + "no_of_employees": doc.no_of_employees, + "deal_owner": doc.deal_owner, + "crm_deal": doc.name, + "territory": doc.territory, + "industry": doc.industry, + "website": doc.website, + "annual_revenue": doc.annual_revenue, + "contacts": json.dumps(contacts), + "erpnext_company": erpnext_crm_settings.erpnext_company, + "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}" + ) + frappe.throw(_("Error while creating prospect in ERPNext, check error log for more details")) + +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, + }) + return contacts + +def get_organization_address(organization): + address = frappe.get_value("CRM Organization", organization, "address") + address = frappe.get_doc("Address", address) if address else None + return address + +def create_customer_in_erpnext(doc, method): + erpnext_crm_settings = frappe.get_single("ERPNext CRM Settings") + if ( + not erpnext_crm_settings.enabled + or not erpnext_crm_settings.create_customer_on_status_change + or doc.status != erpnext_crm_settings.deal_status + ): + return + + contacts = get_contacts(doc) + address = get_organization_address(doc.organization) + customer = { + "customer_name": doc.organization, + "customer_group": "All Customer Groups", + "customer_type": "Company", + "territory": doc.territory, + "default_currency": doc.currency, + "industry": doc.industry, + "website": doc.website, + "crm_deal": doc.name, + "contacts": json.dumps(contacts), + "address": address.as_dict() if address else None, + } + 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.throw(_("Error while creating customer in ERPNext, check error log for more details")) + +@frappe.whitelist() +def get_crm_form_script(): + 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"}); + if (!["Lost", "Won"].includes(doc?.status) && is_erpnext_integration_enabled) { + actions.push({ + label: __("Create Quotation"), + onClick: async () => { + let quotation_url = await call( + "crm.fcrm.doctype.erpnext_crm_settings.erpnext_crm_settings.get_quotation_url", + { + crm_deal: doc.name, + organization: doc.organization + } + ); + + if (quotation_url) { + window.open(quotation_url, '_blank'); + } + } + }) + } + if (is_erpnext_integration_enabled) { + let customer_url = await call("crm.fcrm.doctype.erpnext_crm_settings.erpnext_crm_settings.get_customer_link", { + crm_deal: doc.name + }); + if (customer_url) { + actions.push({ + label: __("View Customer"), + onClick: () => window.open(customer_url, '_blank') + }); + } + } + return { + actions: actions, + }; +} +""" diff --git a/crm/fcrm/doctype/erpnext_crm_settings/test_erpnext_crm_settings.py b/crm/fcrm/doctype/erpnext_crm_settings/test_erpnext_crm_settings.py new file mode 100644 index 000000000..17ae02845 --- /dev/null +++ b/crm/fcrm/doctype/erpnext_crm_settings/test_erpnext_crm_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestERPNextCRMSettings(FrappeTestCase): + pass diff --git a/crm/hooks.py b/crm/hooks.py index e53fc8aa6..fa7e606a8 100644 --- a/crm/hooks.py +++ b/crm/hooks.py @@ -152,6 +152,9 @@ "validate": ["crm.api.whatsapp.validate"], "on_update": ["crm.api.whatsapp.on_update"], }, + "CRM Deal": { + "on_update": ["crm.fcrm.doctype.erpnext_crm_settings.erpnext_crm_settings.create_customer_in_erpnext"], + }, } # Scheduled Tasks diff --git a/frontend/index.html b/frontend/index.html index 351990a31..7bb1e8791 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,7 +8,7 @@ /> Frappe CRM - + diff --git a/frontend/src/components/Fields.vue b/frontend/src/components/Fields.vue index bbf6d2e66..c2bcd3466 100644 --- a/frontend/src/components/Fields.vue +++ b/frontend/src/components/Fields.vue @@ -3,7 +3,7 @@
{{ __(field.label) }} - * + *
diff --git a/frontend/src/components/Icons/ERPNextIcon.vue b/frontend/src/components/Icons/ERPNextIcon.vue new file mode 100644 index 000000000..e512b92db --- /dev/null +++ b/frontend/src/components/Icons/ERPNextIcon.vue @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/frontend/src/components/ListBulkActions.vue b/frontend/src/components/ListBulkActions.vue index e37b36c93..7a5c969f7 100644 --- a/frontend/src/components/ListBulkActions.vue +++ b/frontend/src/components/ListBulkActions.vue @@ -19,7 +19,7 @@ \ No newline at end of file diff --git a/frontend/src/components/Settings/SettingsModal.vue b/frontend/src/components/Settings/SettingsModal.vue index 16487387b..2bbe99223 100644 --- a/frontend/src/components/Settings/SettingsModal.vue +++ b/frontend/src/components/Settings/SettingsModal.vue @@ -39,10 +39,12 @@ diff --git a/frontend/src/components/Settings/SidePanelModal.vue b/frontend/src/components/Settings/SidePanelModal.vue index 401d71cb0..0e3665ad0 100644 --- a/frontend/src/components/Settings/SidePanelModal.vue +++ b/frontend/src/components/Settings/SidePanelModal.vue @@ -43,7 +43,11 @@ :class="{ 'border-b': i !== sections.data.length - 1 }" >
- +
diff --git a/frontend/src/pages/Deal.vue b/frontend/src/pages/Deal.vue index 27a8f3c57..c9ef4fbb5 100644 --- a/frontend/src/pages/Deal.vue +++ b/frontend/src/pages/Deal.vue @@ -8,19 +8,14 @@