Merge pull request #1144 from shariquerik/helpdesk-integration

This commit is contained in:
Shariq Ansari 2025-08-18 22:41:21 +05:30 committed by GitHub
commit d173d5584a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 389 additions and 28 deletions

View File

@ -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,

View File

@ -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}"

View File

@ -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) {
// },
// });

View File

@ -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": []
}

View File

@ -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"""
<h1>Hello,</h1>
<button>{base_url}</button>
""",
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,
)

View File

@ -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

View File

@ -0,0 +1,24 @@
<p>
{{_("Hello")}} {{ first_name }}{% if last_name %} {{ last_name}}{% endif %},
</p>
{% set site_link = "<a href='" + site_url + "'>" + site_url + "</a>" %}
<p>{{_("A new account has been created for you at {0}").format(site_link)}}.</p>
<p>{{_("Your login id is")}}: <b>{{ user }}</b>
<p>{{_("Click on the link below to complete your registration and set a new password")}}.</p>
<p style="margin: 15px 0px;">
<a href="{{ link }}" rel="nofollow" class="btn btn-primary">{{ _("Complete Registration") }}</a>
</p>
{% if created_by != "Administrator" %}
<br>
<p style="margin-top: 15px">
{{_("Thanks")}},<br>
{{ created_by }}
</p>
{% endif %}
<br>
<p>
{{_("You can also copy-paste following link in your browser")}}<br>
<a href="{{ link }}">{{ link }}</a>
</p>

View File

@ -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']

View File

@ -1,20 +1,15 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1 5C1 2.79086 2.79086 1 5 1H13C15.2091 1 17 2.79086 17 5V13C17 15.2091 15.2091 17 13 17H5C2.79086 17 1 15.2091 1 13V5Z"
stroke="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.7819 6.27142H11.5136H8.02453H6.28001V4.84002H11.7819V6.27142ZM8.02451 9.62623V11.5944H11.8267V13.0258H6.27999V8.19484H8.02451H11.5135V9.62623H8.02451Z"
fill="currentColor"
/>
</svg>
</template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.6611 8.2289V9.77773H7.88672V11.9066H11.999V13.4545H6V8.2289H11.6611ZM11.9512 4.6V6.14883H6V4.6H11.9512Z"
fill="currentColor"
/>
<rect x="1.5" y="1.5" width="15" height="15" rx="3.5" stroke="currentColor" />
</svg>
</template>

View File

@ -0,0 +1,15 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="1.5" y="1.5" width="15" height="15" rx="3.5" stroke="currentColor" />
<path
d="M13.7928 8.0619V5H4.29999V6.39494H12.3621V7.72014C11.787 7.88056 11.37 8.39669 11.37 9.00349C11.37 9.61029 11.787 10.1194 12.3621 10.2799V11.6051L5.79999 11.6051V7.96425H4.29999V13H13.8V9.9381L12.9444 9.34525V8.66173L13.8 8.06888L13.7928 8.0619Z"
fill="currentColor"
/>
</svg>
</template>

View File

@ -0,0 +1,11 @@
<template>
<SettingsPage
doctype="Helpdesk CRM Settings"
:title="__('Helpdesk settings')"
:successMessage="__('Helpdesk settings updated')"
class="p-8"
/>
</template>
<script setup>
import SettingsPage from '@/components/Settings/SettingsPage.vue'
</script>

View File

@ -43,6 +43,7 @@
<script setup>
import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
import ERPNextIcon from '@/components/Icons/ERPNextIcon.vue'
import HelpdeskIcon from '@/components/Icons/HelpdeskIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import Email2Icon from '@/components/Icons/Email2Icon.vue'
import EmailTemplateIcon from '@/components/Icons/EmailTemplateIcon.vue'
@ -52,6 +53,7 @@ import InviteUserPage from '@/components/Settings/InviteUserPage.vue'
import ProfileSettings from '@/components/Settings/ProfileSettings.vue'
import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.vue'
import ERPNextSettings from '@/components/Settings/ERPNextSettings.vue'
import HelpdeskSettings from '@/components/Settings/HelpdeskSettings.vue'
import EmailTemplatePage from '@/components/Settings/EmailTemplate/EmailTemplatePage.vue'
import TelephonySettings from '@/components/Settings/TelephonySettings.vue'
import EmailConfig from '@/components/Settings/EmailConfig.vue'
@ -137,6 +139,12 @@ const tabs = computed(() => {
component: markRaw(ERPNextSettings),
condition: () => isManager(),
},
{
label: __('Helpdesk'),
icon: HelpdeskIcon,
component: markRaw(HelpdeskSettings),
condition: () => isManager(),
},
],
condition: () => isManager() || isTelephonyAgent(),
},