diff --git a/crm/api/comment.py b/crm/api/comment.py
index 8f2df3f2..378a01b8 100644
--- a/crm/api/comment.py
+++ b/crm/api/comment.py
@@ -1,4 +1,5 @@
import frappe
+from frappe import _
from bs4 import BeautifulSoup
def on_update(self, method):
@@ -15,13 +16,26 @@ def notify_mentions(doc):
return
mentions = extract_mentions(content)
for mention in mentions:
+ owner = frappe.get_cached_value("User", doc.owner, "full_name")
+ doctype = doc.reference_doctype
+ if doctype.startswith("CRM "):
+ doctype = doctype[4:].lower()
+ notification_text = f"""
+
+ { owner }
+ { _('mentioned you in {0}').format(doctype) }
+ { doc.reference_name }
+
+ """
values = frappe._dict(
doctype="CRM Notification",
from_user=doc.owner,
to_user=mention.email,
type="Mention",
message=doc.content,
- comment=doc.name,
+ notification_text=notification_text,
+ notification_type_doctype="Comment",
+ notification_type_doc=doc.name,
reference_doctype=doc.reference_doctype,
reference_name=doc.reference_name,
)
diff --git a/crm/api/notifications.py b/crm/api/notifications.py
index 3dde662c..972fa58e 100644
--- a/crm/api/notifications.py
+++ b/crm/api/notifications.py
@@ -28,13 +28,16 @@ def get_notifications():
"to_user": notification.to_user,
"read": notification.read,
"comment": notification.comment,
- "reference_doctype": "deal"
- if notification.reference_doctype == "CRM Deal"
- else "lead",
+ "notification_text": notification.notification_text,
+ "notification_type_doctype": notification.notification_type_doctype,
+ "notification_type_doc": notification.notification_type_doc,
+ "reference_doctype": (
+ "deal" if notification.reference_doctype == "CRM Deal" else "lead"
+ ),
"reference_name": notification.reference_name,
- "route_name": "Deal"
- if notification.reference_doctype == "CRM Deal"
- else "Lead",
+ "route_name": (
+ "Deal" if notification.reference_doctype == "CRM Deal" else "Lead"
+ ),
}
)
@@ -42,12 +45,15 @@ def get_notifications():
@frappe.whitelist()
-def mark_as_read(user=None, comment=None):
+def mark_as_read(user=None, doc=None):
user = user or frappe.session.user
filters = {"to_user": user, "read": False}
- if comment:
- filters["comment"] = comment
- for n in frappe.get_all("CRM Notification", filters=filters):
+ if doc:
+ or_filters = [
+ {"comment": doc},
+ {"notification_type_doc": doc},
+ ]
+ for n in frappe.get_all("CRM Notification", filters=filters, or_filters=or_filters):
d = frappe.get_doc("CRM Notification", n.name)
d.read = True
d.save()
diff --git a/crm/api/whatsapp.py b/crm/api/whatsapp.py
new file mode 100644
index 00000000..d871fc90
--- /dev/null
+++ b/crm/api/whatsapp.py
@@ -0,0 +1,314 @@
+import frappe
+import json
+from frappe import _
+from crm.api.doc import get_assigned_users
+
+
+def validate(doc, method):
+ if doc.type == "Incoming" and doc.get("from"):
+ name, doctype = get_lead_or_deal_from_number(doc.get("from"))
+ doc.reference_doctype = doctype
+ doc.reference_name = name
+
+
+def on_update(doc, method):
+ frappe.publish_realtime(
+ "whatsapp_message",
+ {
+ "reference_doctype": doc.reference_doctype,
+ "reference_name": doc.reference_name,
+ },
+ )
+
+ notify_agent(doc)
+
+
+def notify_agent(doc):
+ if doc.type == "Incoming":
+ doctype = doc.reference_doctype
+ if doctype.startswith("CRM "):
+ doctype = doctype[4:].lower()
+ notification_text = f"""
+
+ { _('You') }
+ { _('received a whatsapp message in {0}').format(doctype) }
+ { doc.reference_name }
+
+ """
+ assigned_users = get_assigned_users(doc.reference_doctype, doc.reference_name)
+ for user in assigned_users:
+ values = frappe._dict(
+ doctype="CRM Notification",
+ from_user=doc.owner,
+ to_user=user,
+ type="WhatsApp",
+ message=doc.message,
+ notification_text=notification_text,
+ notification_type_doctype="WhatsApp Message",
+ notification_type_doc=doc.name,
+ reference_doctype=doc.reference_doctype,
+ reference_name=doc.reference_name,
+ )
+
+ if frappe.db.exists("CRM Notification", values):
+ return
+ frappe.get_doc(values).insert(ignore_permissions=True)
+
+
+def get_lead_or_deal_from_number(number):
+ """Get lead/deal from the given number."""
+
+ def find_record(doctype, mobile_no, where=""):
+ mobile_no = parse_mobile_no(mobile_no)
+
+ query = f"""
+ SELECT name, mobile_no
+ FROM `tab{doctype}`
+ WHERE CONCAT('+', REGEXP_REPLACE(mobile_no, '[^0-9]', '')) = {mobile_no}
+ """
+
+ data = frappe.db.sql(query + where, as_dict=True)
+ return data[0].name if data else None
+
+ doctype = "CRM Deal"
+
+ doc = find_record(doctype, number) or None
+ if not doc:
+ doctype = "CRM Lead"
+ doc = find_record(doctype, number, "AND converted is not True")
+ if not doc:
+ doc = find_record(doctype, number)
+
+ return doc, doctype
+
+
+def parse_mobile_no(mobile_no: str):
+ """Parse mobile number to remove spaces, brackets, etc.
+ >>> parse_mobile_no('+91 (766) 667 6666')
+ ... '+917666676666'
+ """
+ return "".join([c for c in mobile_no if c.isdigit() or c == "+"])
+
+
+@frappe.whitelist()
+def is_whatsapp_enabled():
+ if not frappe.db.exists("DocType", "WhatsApp Settings"):
+ return False
+ return frappe.get_cached_value("WhatsApp Settings", "WhatsApp Settings", "enabled")
+
+
+@frappe.whitelist()
+def get_whatsapp_messages(reference_doctype, reference_name):
+ if not frappe.db.exists("DocType", "WhatsApp Message"):
+ return []
+ messages = frappe.get_all(
+ "WhatsApp Message",
+ filters={
+ "reference_doctype": reference_doctype,
+ "reference_name": reference_name,
+ },
+ fields=[
+ "name",
+ "type",
+ "to",
+ "from",
+ "content_type",
+ "message_type",
+ "attach",
+ "template",
+ "use_template",
+ "message_id",
+ "is_reply",
+ "reply_to_message_id",
+ "creation",
+ "message",
+ "status",
+ "reference_doctype",
+ "reference_name",
+ "template_parameters",
+ "template_header_parameters",
+ ],
+ )
+
+ # Filter messages to get only Template messages
+ template_messages = [
+ message for message in messages if message["message_type"] == "Template"
+ ]
+
+ # Iterate through template messages
+ for template_message in template_messages:
+ # Find the template that this message is using
+ template = frappe.get_doc("WhatsApp Templates", template_message["template"])
+
+ # If the template is found, add the template details to the template message
+ if template:
+ template_message["template_name"] = template.template_name
+ if template_message["template_parameters"]:
+ parameters = json.loads(template_message["template_parameters"])
+ template.template = parse_template_parameters(
+ template.template, parameters
+ )
+
+ template_message["template"] = template.template
+ if template_message["template_header_parameters"]:
+ header_parameters = json.loads(
+ template_message["template_header_parameters"]
+ )
+ template.header = parse_template_parameters(
+ template.header, header_parameters
+ )
+ template_message["header"] = template.header
+ template_message["footer"] = template.footer
+
+ # Filter messages to get only reaction messages
+ reaction_messages = [
+ message for message in messages if message["content_type"] == "reaction"
+ ]
+
+ # Iterate through reaction messages
+ for reaction_message in reaction_messages:
+ # Find the message that this reaction is reacting to
+ reacted_message = next(
+ (
+ m
+ for m in messages
+ if m["message_id"] == reaction_message["reply_to_message_id"]
+ ),
+ None,
+ )
+
+ # If the reacted message is found, add the reaction to it
+ if reacted_message:
+ reacted_message["reaction"] = reaction_message["message"]
+
+ for message in messages:
+ from_name = get_from_name(message) if message["from"] else _("You")
+ message["from_name"] = from_name
+ # Filter messages to get only replies
+ reply_messages = [message for message in messages if message["is_reply"]]
+
+ # Iterate through reply messages
+ for reply_message in reply_messages:
+ # Find the message that this message is replying to
+ replied_message = next(
+ (
+ m
+ for m in messages
+ if m["message_id"] == reply_message["reply_to_message_id"]
+ ),
+ None,
+ )
+
+ # If the replied message is found, add the reply details to the reply message
+ from_name = (
+ get_from_name(reply_message) if replied_message["from"] else _("You")
+ )
+ if replied_message:
+ message = replied_message["message"]
+ if replied_message["message_type"] == "Template":
+ message = replied_message["template"]
+ reply_message["reply_message"] = message
+ reply_message["header"] = replied_message.get("header") or ""
+ reply_message["footer"] = replied_message.get("footer") or ""
+ reply_message["reply_to"] = replied_message["name"]
+ reply_message["reply_to_type"] = replied_message["type"]
+ reply_message["reply_to_from"] = from_name
+
+ return [message for message in messages if message["content_type"] != "reaction"]
+
+
+@frappe.whitelist()
+def create_whatsapp_message(
+ reference_doctype,
+ reference_name,
+ message,
+ to,
+ attach,
+ reply_to,
+ content_type="text",
+):
+ doc = frappe.new_doc("WhatsApp Message")
+
+ if reply_to:
+ reply_doc = frappe.get_doc("WhatsApp Message", reply_to)
+ doc.update(
+ {
+ "is_reply": True,
+ "reply_to_message_id": reply_doc.message_id,
+ }
+ )
+
+ doc.update(
+ {
+ "reference_doctype": reference_doctype,
+ "reference_name": reference_name,
+ "message": message or attach,
+ "to": to,
+ "attach": attach,
+ "content_type": content_type,
+ }
+ )
+ doc.insert(ignore_permissions=True)
+ return doc.name
+
+
+@frappe.whitelist()
+def send_whatsapp_template(reference_doctype, reference_name, template, to):
+ doc = frappe.new_doc("WhatsApp Message")
+ doc.update(
+ {
+ "reference_doctype": reference_doctype,
+ "reference_name": reference_name,
+ "message_type": "Template",
+ "message": "Template message",
+ "content_type": "text",
+ "use_template": True,
+ "template": template,
+ "to": to,
+ }
+ )
+ doc.insert(ignore_permissions=True)
+ return doc.name
+
+
+@frappe.whitelist()
+def react_on_whatsapp_message(emoji, reply_to_name):
+ reply_to_doc = frappe.get_doc("WhatsApp Message", reply_to_name)
+ to = reply_to_doc.type == "Incoming" and reply_to_doc.get("from") or reply_to_doc.to
+ doc = frappe.new_doc("WhatsApp Message")
+ doc.update(
+ {
+ "reference_doctype": reply_to_doc.reference_doctype,
+ "reference_name": reply_to_doc.reference_name,
+ "message": emoji,
+ "to": to,
+ "reply_to_message_id": reply_to_doc.message_id,
+ "content_type": "reaction",
+ }
+ )
+ doc.insert(ignore_permissions=True)
+ return doc.name
+
+
+def parse_template_parameters(string, parameters):
+ for i, parameter in enumerate(parameters, start=1):
+ placeholder = "{{" + str(i) + "}}"
+ string = string.replace(placeholder, parameter)
+
+ return string
+
+
+def get_from_name(message):
+ doc = frappe.get_doc(message["reference_doctype"], message["reference_name"])
+ from_name = ""
+ if message["reference_doctype"] == "CRM Deal":
+ if doc.get("contacts"):
+ for c in doc.get("contacts"):
+ if c.is_primary:
+ from_name = c.full_name or c.mobile_no
+ break
+ else:
+ from_name = doc.get("lead_name")
+ else:
+ from_name = doc.get("first_name") + " " + doc.get("last_name")
+ return from_name
diff --git a/crm/fcrm/doctype/crm_notification/crm_notification.json b/crm/fcrm/doctype/crm_notification/crm_notification.json
index 7b9cee2e..be7ac1f6 100644
--- a/crm/fcrm/doctype/crm_notification/crm_notification.json
+++ b/crm/fcrm/doctype/crm_notification/crm_notification.json
@@ -5,14 +5,20 @@
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
+ "notification_text",
+ "section_break_hace",
"from_user",
"type",
- "reference_doctype",
- "reference_name",
"column_break_dduu",
"to_user",
- "comment",
"read",
+ "section_break_pbvx",
+ "reference_doctype",
+ "reference_name",
+ "column_break_eant",
+ "notification_type_doctype",
+ "notification_type_doc",
+ "comment",
"section_break_vpwa",
"message"
],
@@ -28,7 +34,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
- "options": "Mention",
+ "options": "Mention\nWhatsApp",
"reqd": 1
},
{
@@ -46,8 +52,8 @@
{
"fieldname": "comment",
"fieldtype": "Link",
+ "hidden": 1,
"label": "Comment",
- "link_filters": "[[{\"fieldname\":\"comment\",\"field_option\":\"Comment\"},\"comment_type\",\"=\",\"Comment\"]]",
"options": "Comment"
},
{
@@ -63,6 +69,7 @@
{
"fieldname": "message",
"fieldtype": "HTML Editor",
+ "in_list_view": 1,
"label": "Message"
},
{
@@ -76,11 +83,40 @@
"fieldtype": "Link",
"label": "Reference Doctype",
"options": "DocType"
+ },
+ {
+ "fieldname": "section_break_pbvx",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_eant",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "notification_type_doctype",
+ "fieldtype": "Link",
+ "label": "Notification Type Doctype",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "notification_type_doc",
+ "fieldtype": "Dynamic Link",
+ "label": "Notification Type Doc",
+ "options": "notification_type_doctype"
+ },
+ {
+ "fieldname": "notification_text",
+ "fieldtype": "Text",
+ "label": "Notification Text"
+ },
+ {
+ "fieldname": "section_break_hace",
+ "fieldtype": "Section Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2024-01-30 01:04:27.946030",
+ "modified": "2024-04-25 16:26:07.484857",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Notification",
diff --git a/crm/fcrm/doctype/crm_notification/crm_notification.py b/crm/fcrm/doctype/crm_notification/crm_notification.py
index 9470101f..28f4ab80 100644
--- a/crm/fcrm/doctype/crm_notification/crm_notification.py
+++ b/crm/fcrm/doctype/crm_notification/crm_notification.py
@@ -1,9 +1,10 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-# import frappe
+import frappe
from frappe.model.document import Document
class CRMNotification(Document):
- pass
+ def on_update(self):
+ frappe.publish_realtime("crm_notification")
diff --git a/crm/hooks.py b/crm/hooks.py
index b25039c3..740c21b2 100644
--- a/crm/hooks.py
+++ b/crm/hooks.py
@@ -135,7 +135,11 @@ doc_events = {
},
"Comment": {
"on_update": ["crm.api.comment.on_update"],
- }
+ },
+ "WhatsApp Message": {
+ "validate": ["crm.api.whatsapp.validate"],
+ "on_update": ["crm.api.whatsapp.on_update"],
+ },
}
# Scheduled Tasks
diff --git a/crm/www/crm.py b/crm/www/crm.py
index 05e4014b..44347cf3 100644
--- a/crm/www/crm.py
+++ b/crm/www/crm.py
@@ -8,9 +8,34 @@ from frappe.utils.telemetry import capture
no_cache = 1
-def get_context(context):
+def get_context():
csrf_token = frappe.sessions.get_csrf_token()
frappe.db.commit()
+ context = frappe._dict()
+ context.boot = get_boot()
+ context.boot.csrf_token = csrf_token
if frappe.session.user != "Guest":
capture("active_site", "crm")
- context.csrf_token = csrf_token
+ return context
+
+
+@frappe.whitelist(methods=["POST"], allow_guest=True)
+def get_context_for_dev():
+ if not frappe.conf.developer_mode:
+ frappe.throw("This method is only meant for developer mode")
+ return get_boot()
+
+
+def get_boot():
+ return frappe._dict(
+ {
+ "frappe_version": frappe.__version__,
+ "default_route": get_default_route(),
+ "site_name": frappe.local.site,
+ "read_only_mode": frappe.flags.read_only,
+ }
+ )
+
+
+def get_default_route():
+ return "/crm"
diff --git a/frappe-ui b/frappe-ui
index 05a8eca5..063cf8d3 160000
--- a/frappe-ui
+++ b/frappe-ui
@@ -1 +1 @@
-Subproject commit 05a8eca589d23d44f55cfe82ae157fd5de997abb
+Subproject commit 063cf8d3563776aabe83a93ff5dcea5c435ae1f6
diff --git a/frontend/index.html b/frontend/index.html
index 31a720b7..a8d04a14 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -16,7 +16,11 @@
-
+