diff --git a/crm/api/dashboard.py b/crm/api/dashboard.py index 4bed4218..0b51b6a2 100644 --- a/crm/api/dashboard.py +++ b/crm/api/dashboard.py @@ -1008,7 +1008,7 @@ def get_deals_by_territory(from_date="", to_date="", user=""): WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s {deal_conds} GROUP BY d.territory - ORDER BY value DESC + ORDER BY deals DESC, value DESC """, {"from": from_date, "to": to_date}, as_dict=True, @@ -1065,7 +1065,7 @@ def get_deals_by_salesperson(from_date="", to_date="", user=""): WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s {deal_conds} GROUP BY d.deal_owner - ORDER BY value DESC + ORDER BY deals DESC, value DESC """, {"from": from_date, "to": to_date}, as_dict=True, 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 84351670..fde6fb72 100644 --- a/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.py +++ b/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.py @@ -135,7 +135,7 @@ def get_quotation_url(crm_deal, organization): "party_name": crm_deal, "company": erpnext_crm_settings.erpnext_company, "contact_person": contact, - "customer_address": address + "customer_address": address, } else: site_url = erpnext_crm_settings.get("erpnext_site_url") @@ -147,14 +147,11 @@ def get_quotation_url(crm_deal, organization): "party_name": prospect, "company": erpnext_crm_settings.erpnext_company, "contact_person": contact, - "customer_address": address + "customer_address": address, } - + # Filter out None values and build query string - query_string = "&".join( - f"{key}={value}" for key, value in params.items() - if value is not None - ) + query_string = "&".join(f"{key}={value}" for key, value in params.items() if value is not None) return f"{base_url}?{query_string}" diff --git a/crm/fcrm/doctype/helpdesk_crm_settings/__init__.py b/crm/fcrm/doctype/helpdesk_crm_settings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/crm/fcrm/doctype/helpdesk_crm_settings/helpdesk_crm_settings.js b/crm/fcrm/doctype/helpdesk_crm_settings/helpdesk_crm_settings.js new file mode 100644 index 00000000..b31a6f24 --- /dev/null +++ b/crm/fcrm/doctype/helpdesk_crm_settings/helpdesk_crm_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Helpdesk CRM Settings", { +// refresh(frm) { + +// }, +// }); diff --git a/crm/fcrm/doctype/helpdesk_crm_settings/helpdesk_crm_settings.json b/crm/fcrm/doctype/helpdesk_crm_settings/helpdesk_crm_settings.json new file mode 100644 index 00000000..220666ab --- /dev/null +++ b/crm/fcrm/doctype/helpdesk_crm_settings/helpdesk_crm_settings.json @@ -0,0 +1,102 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-08-18 17:25:49.638398", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "enabled", + "column_break_idaw", + "is_helpdesk_in_different_site", + "helpdesk_site_url", + "helpdesk_site_apis_section", + "api_key", + "column_break_tqsm", + "api_secret" + ], + "fields": [ + { + "default": "0", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + }, + { + "fieldname": "column_break_idaw", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "enabled", + "fieldname": "is_helpdesk_in_different_site", + "fieldtype": "Check", + "label": "Is Helpdesk installed on a different site?" + }, + { + "depends_on": "eval:doc.enabled && doc.is_helpdesk_in_different_site", + "fieldname": "helpdesk_site_url", + "fieldtype": "Data", + "label": "Helpdesk Site URL", + "mandatory_depends_on": "is_helpdesk_in_different_site" + }, + { + "depends_on": "enabled", + "fieldname": "helpdesk_site_apis_section", + "fieldtype": "Section Break", + "label": "Helpdesk Site API's" + }, + { + "depends_on": "eval:doc.enabled && doc.is_helpdesk_in_different_site", + "fieldname": "api_key", + "fieldtype": "Data", + "label": "API Key", + "mandatory_depends_on": "is_helpdesk_in_different_site" + }, + { + "fieldname": "column_break_tqsm", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.enabled && doc.is_helpdesk_in_different_site", + "fieldname": "api_secret", + "fieldtype": "Password", + "label": "API Secret", + "mandatory_depends_on": "is_helpdesk_in_different_site" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2025-08-18 17:33:38.616328", + "modified_by": "Administrator", + "module": "FCRM", + "name": "Helpdesk CRM Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "Sales Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/crm/fcrm/doctype/helpdesk_crm_settings/helpdesk_crm_settings.py b/crm/fcrm/doctype/helpdesk_crm_settings/helpdesk_crm_settings.py new file mode 100644 index 00000000..efc6b999 --- /dev/null +++ b/crm/fcrm/doctype/helpdesk_crm_settings/helpdesk_crm_settings.py @@ -0,0 +1,178 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document + + +class HelpdeskCRMSettings(Document): + def validate(self): + if self.enabled: + self.validate_if_helpdesk_installed() + self.create_helpdesk_script() + + def validate_if_helpdesk_installed(self): + if not self.is_helpdesk_in_different_site: + if "helpdesk" not in frappe.get_installed_apps(): + frappe.throw(_("Helpdesk is not installed in the current site")) + + def create_helpdesk_script(self): + if not frappe.db.exists("CRM Form Script", "Helpdesk Integration Script"): + script = get_helpdesk_script() + frappe.get_doc( + { + "doctype": "CRM Form Script", + "name": "Helpdesk Integration Script", + "dt": "CRM Deal", + "view": "Form", + "script": script, + "enabled": 1, + "is_standard": 1, + } + ).insert() + + +@frappe.whitelist() +def create_customer_in_helpdesk(name, email): + helpdesk_crm_settings = frappe.get_single("Helpdesk CRM Settings") + if not helpdesk_crm_settings.enabled: + frappe.throw(_("Helpdesk is not integrated with the CRM")) + + if not helpdesk_crm_settings.is_helpdesk_in_different_site: + # from helpdesk.integrations.crm.api import create_customer + return create_customer(name, email) + + +def get_helpdesk_script(): + return """class CRMDeal { + onLoad() { + this.actions.push( + { + group: "Helpdesk", + hideLabel: true, + items: [ + { + label: "Create customer in Helpdesk", + onClick: () => { + call('crm.fcrm.doctype.helpdesk_crm_settings.helpdesk_crm_settings.create_customer_in_helpdesk', { + name: this.doc.organization, + email: this.doc.email + }).then((a) => { + toast.success("Customer created successfully, " + a.customer) + }) + } + } + ] + } + ) + } +}""" + +# Helpdesk methods TODO: move to helpdesk.integrations.crm.api +def create_customer(name, email): + customer = frappe.db.exists("HD Customer", name) + if not customer: + customer = frappe.get_doc( + { + "doctype": "HD Customer", + "customer_name": name, + } + ) + customer.insert(ignore_permissions=True, ignore_if_duplicate=True) + else: + customer = frappe.get_doc("HD Customer", customer) + + contact = frappe.db.exists("Contact", {"email_id": email}) + if contact: + contact = frappe.get_doc("Contact", contact) + contact.append( + "links", {"link_doctype": "HD Customer", "link_name": customer.name} + ) + contact.save(ignore_permissions=True) + else: + contact = frappe.get_doc( + { + "doctype": "Contact", + "first_name": email.split("@")[0], + "email_ids": [{"email_id": email, "is_primary": 1}], + "links": [{"link_doctype": "HD Customer", "link_name": customer.name}], + } + ) + contact.insert(ignore_permissions=True) + + if not frappe.db.exists("User", contact.email_id): + invite_user(contact.name) + else: + base_url = frappe.utils.get_url() + "/helpdesk" + frappe.sendmail( + recipients=[contact.email_id], + subject="Welcome existing user to Helpdesk", + message=f""" +

Hello,

+ + """, + now=True, + ) + + return {"customer": customer.name, "contact": contact.name} + + +def invite_user(contact: str): + contact = frappe.get_doc("Contact", contact) + contact.check_permission() + + if not contact.email_id: + frappe.throw(_("Please set Email Address")) + + user = frappe.get_doc( + { + "doctype": "User", + "first_name": contact.first_name, + "last_name": contact.last_name, + "email": contact.email_id, + "user_type": "Website User", + "send_welcome_email": 0 + } + ).insert() + + contact.user = user.name + contact.save(ignore_permissions=True) + + send_welcome_mail_to_user(user) + return user.name + + +def send_welcome_mail_to_user(user): + from frappe.utils import get_url + from frappe.utils.user import get_user_fullname + + link = user.reset_password() + + frappe.cache.hset("redirect_after_login", user.name, "/helpdesk") + + site_url = get_url() + subject = _("Welcome to Helpdesk") + + created_by = get_user_fullname(frappe.session["user"]) + if created_by == "Guest": + created_by = "Administrator" + + args = { + "first_name": user.first_name or user.last_name or "user", + "last_name": user.last_name, + "user": user.name, + "title": subject, + "login_url": get_url(), + "created_by": created_by, + "site_url": site_url, + "link": link + } + + frappe.sendmail( + recipients=[user.email], + subject=subject, + template="helpdesk_invitation", + args=args, + now=True, + ) diff --git a/crm/fcrm/doctype/helpdesk_crm_settings/test_helpdesk_crm_settings.py b/crm/fcrm/doctype/helpdesk_crm_settings/test_helpdesk_crm_settings.py new file mode 100644 index 00000000..c60d55dc --- /dev/null +++ b/crm/fcrm/doctype/helpdesk_crm_settings/test_helpdesk_crm_settings.py @@ -0,0 +1,21 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + + +class IntegrationTestHelpdeskCRMSettings(IntegrationTestCase): + """ + Integration tests for HelpdeskCRMSettings. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/crm/templates/emails/helpdesk_invitation.html b/crm/templates/emails/helpdesk_invitation.html new file mode 100644 index 00000000..77fb852a --- /dev/null +++ b/crm/templates/emails/helpdesk_invitation.html @@ -0,0 +1,24 @@ +

+ {{_("Hello")}} {{ first_name }}{% if last_name %} {{ last_name}}{% endif %}, +

+{% set site_link = "" + site_url + "" %} +

{{_("A new account has been created for you at {0}").format(site_link)}}.

+

{{_("Your login id is")}}: {{ user }} +

{{_("Click on the link below to complete your registration and set a new password")}}.

+ +

+ {{ _("Complete Registration") }} +

+ +{% if created_by != "Administrator" %} +
+

+ {{_("Thanks")}},
+ {{ created_by }} +

+{% endif %} +
+

+ {{_("You can also copy-paste following link in your browser")}}
+ {{ link }} +

diff --git a/frontend/components.d.ts b/frontend/components.d.ts index d5b0b176..094bac87 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -142,6 +142,8 @@ declare module 'vue' { GroupBy: typeof import('./src/components/GroupBy.vue')['default'] GroupByIcon: typeof import('./src/components/Icons/GroupByIcon.vue')['default'] HeartIcon: typeof import('./src/components/Icons/HeartIcon.vue')['default'] + HelpdeskIcon: typeof import('./src/components/Icons/HelpdeskIcon.vue')['default'] + HelpdeskSettings: typeof import('./src/components/Settings/HelpdeskSettings.vue')['default'] HelpIcon: typeof import('./src/components/Icons/HelpIcon.vue')['default'] HomeActions: typeof import('./src/components/Settings/General/HomeActions.vue')['default'] Icon: typeof import('./src/components/Icon.vue')['default'] diff --git a/frontend/src/components/Icons/ERPNextIcon.vue b/frontend/src/components/Icons/ERPNextIcon.vue index e512b92d..5c5ade8a 100644 --- a/frontend/src/components/Icons/ERPNextIcon.vue +++ b/frontend/src/components/Icons/ERPNextIcon.vue @@ -1,20 +1,15 @@ \ No newline at end of file + + + + + diff --git a/frontend/src/components/Icons/HelpdeskIcon.vue b/frontend/src/components/Icons/HelpdeskIcon.vue new file mode 100644 index 00000000..1e4507d3 --- /dev/null +++ b/frontend/src/components/Icons/HelpdeskIcon.vue @@ -0,0 +1,15 @@ + diff --git a/frontend/src/components/Settings/HelpdeskSettings.vue b/frontend/src/components/Settings/HelpdeskSettings.vue new file mode 100644 index 00000000..d617388e --- /dev/null +++ b/frontend/src/components/Settings/HelpdeskSettings.vue @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/frontend/src/components/Settings/Settings.vue b/frontend/src/components/Settings/Settings.vue index ed5d4a15..4b1f9b90 100644 --- a/frontend/src/components/Settings/Settings.vue +++ b/frontend/src/components/Settings/Settings.vue @@ -43,6 +43,7 @@