Skip to content

Commit d4bf6ec

Browse files
authored
Merge pull request #148 from frappe/develop
chore: Merge develop to main
2 parents 38abad4 + 1013d9b commit d4bf6ec

31 files changed

+1507
-145
lines changed

crm/api/comment.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import frappe
2+
from frappe import _
23
from bs4 import BeautifulSoup
34

45
def on_update(self, method):
@@ -15,13 +16,26 @@ def notify_mentions(doc):
1516
return
1617
mentions = extract_mentions(content)
1718
for mention in mentions:
19+
owner = frappe.get_cached_value("User", doc.owner, "full_name")
20+
doctype = doc.reference_doctype
21+
if doctype.startswith("CRM "):
22+
doctype = doctype[4:].lower()
23+
notification_text = f"""
24+
<div class="mb-2 leading-5 text-gray-600">
25+
<span class="font-medium text-gray-900">{ owner }</span>
26+
<span>{ _('mentioned you in {0}').format(doctype) }</span>
27+
<span class="font-medium text-gray-900">{ doc.reference_name }</span>
28+
</div>
29+
"""
1830
values = frappe._dict(
1931
doctype="CRM Notification",
2032
from_user=doc.owner,
2133
to_user=mention.email,
2234
type="Mention",
2335
message=doc.content,
24-
comment=doc.name,
36+
notification_text=notification_text,
37+
notification_type_doctype="Comment",
38+
notification_type_doc=doc.name,
2539
reference_doctype=doc.reference_doctype,
2640
reference_name=doc.reference_name,
2741
)

crm/api/notifications.py

+16-10
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,32 @@ def get_notifications():
2828
"to_user": notification.to_user,
2929
"read": notification.read,
3030
"comment": notification.comment,
31-
"reference_doctype": "deal"
32-
if notification.reference_doctype == "CRM Deal"
33-
else "lead",
31+
"notification_text": notification.notification_text,
32+
"notification_type_doctype": notification.notification_type_doctype,
33+
"notification_type_doc": notification.notification_type_doc,
34+
"reference_doctype": (
35+
"deal" if notification.reference_doctype == "CRM Deal" else "lead"
36+
),
3437
"reference_name": notification.reference_name,
35-
"route_name": "Deal"
36-
if notification.reference_doctype == "CRM Deal"
37-
else "Lead",
38+
"route_name": (
39+
"Deal" if notification.reference_doctype == "CRM Deal" else "Lead"
40+
),
3841
}
3942
)
4043

4144
return _notifications
4245

4346

4447
@frappe.whitelist()
45-
def mark_as_read(user=None, comment=None):
48+
def mark_as_read(user=None, doc=None):
4649
user = user or frappe.session.user
4750
filters = {"to_user": user, "read": False}
48-
if comment:
49-
filters["comment"] = comment
50-
for n in frappe.get_all("CRM Notification", filters=filters):
51+
if doc:
52+
or_filters = [
53+
{"comment": doc},
54+
{"notification_type_doc": doc},
55+
]
56+
for n in frappe.get_all("CRM Notification", filters=filters, or_filters=or_filters):
5157
d = frappe.get_doc("CRM Notification", n.name)
5258
d.read = True
5359
d.save()

crm/api/whatsapp.py

+314
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
import frappe
2+
import json
3+
from frappe import _
4+
from crm.api.doc import get_assigned_users
5+
6+
7+
def validate(doc, method):
8+
if doc.type == "Incoming" and doc.get("from"):
9+
name, doctype = get_lead_or_deal_from_number(doc.get("from"))
10+
doc.reference_doctype = doctype
11+
doc.reference_name = name
12+
13+
14+
def on_update(doc, method):
15+
frappe.publish_realtime(
16+
"whatsapp_message",
17+
{
18+
"reference_doctype": doc.reference_doctype,
19+
"reference_name": doc.reference_name,
20+
},
21+
)
22+
23+
notify_agent(doc)
24+
25+
26+
def notify_agent(doc):
27+
if doc.type == "Incoming":
28+
doctype = doc.reference_doctype
29+
if doctype.startswith("CRM "):
30+
doctype = doctype[4:].lower()
31+
notification_text = f"""
32+
<div class="mb-2 leading-5 text-gray-600">
33+
<span class="font-medium text-gray-900">{ _('You') }</span>
34+
<span>{ _('received a whatsapp message in {0}').format(doctype) }</span>
35+
<span class="font-medium text-gray-900">{ doc.reference_name }</span>
36+
</div>
37+
"""
38+
assigned_users = get_assigned_users(doc.reference_doctype, doc.reference_name)
39+
for user in assigned_users:
40+
values = frappe._dict(
41+
doctype="CRM Notification",
42+
from_user=doc.owner,
43+
to_user=user,
44+
type="WhatsApp",
45+
message=doc.message,
46+
notification_text=notification_text,
47+
notification_type_doctype="WhatsApp Message",
48+
notification_type_doc=doc.name,
49+
reference_doctype=doc.reference_doctype,
50+
reference_name=doc.reference_name,
51+
)
52+
53+
if frappe.db.exists("CRM Notification", values):
54+
return
55+
frappe.get_doc(values).insert(ignore_permissions=True)
56+
57+
58+
def get_lead_or_deal_from_number(number):
59+
"""Get lead/deal from the given number."""
60+
61+
def find_record(doctype, mobile_no, where=""):
62+
mobile_no = parse_mobile_no(mobile_no)
63+
64+
query = f"""
65+
SELECT name, mobile_no
66+
FROM `tab{doctype}`
67+
WHERE CONCAT('+', REGEXP_REPLACE(mobile_no, '[^0-9]', '')) = {mobile_no}
68+
"""
69+
70+
data = frappe.db.sql(query + where, as_dict=True)
71+
return data[0].name if data else None
72+
73+
doctype = "CRM Deal"
74+
75+
doc = find_record(doctype, number) or None
76+
if not doc:
77+
doctype = "CRM Lead"
78+
doc = find_record(doctype, number, "AND converted is not True")
79+
if not doc:
80+
doc = find_record(doctype, number)
81+
82+
return doc, doctype
83+
84+
85+
def parse_mobile_no(mobile_no: str):
86+
"""Parse mobile number to remove spaces, brackets, etc.
87+
>>> parse_mobile_no('+91 (766) 667 6666')
88+
... '+917666676666'
89+
"""
90+
return "".join([c for c in mobile_no if c.isdigit() or c == "+"])
91+
92+
93+
@frappe.whitelist()
94+
def is_whatsapp_enabled():
95+
if not frappe.db.exists("DocType", "WhatsApp Settings"):
96+
return False
97+
return frappe.get_cached_value("WhatsApp Settings", "WhatsApp Settings", "enabled")
98+
99+
100+
@frappe.whitelist()
101+
def get_whatsapp_messages(reference_doctype, reference_name):
102+
if not frappe.db.exists("DocType", "WhatsApp Message"):
103+
return []
104+
messages = frappe.get_all(
105+
"WhatsApp Message",
106+
filters={
107+
"reference_doctype": reference_doctype,
108+
"reference_name": reference_name,
109+
},
110+
fields=[
111+
"name",
112+
"type",
113+
"to",
114+
"from",
115+
"content_type",
116+
"message_type",
117+
"attach",
118+
"template",
119+
"use_template",
120+
"message_id",
121+
"is_reply",
122+
"reply_to_message_id",
123+
"creation",
124+
"message",
125+
"status",
126+
"reference_doctype",
127+
"reference_name",
128+
"template_parameters",
129+
"template_header_parameters",
130+
],
131+
)
132+
133+
# Filter messages to get only Template messages
134+
template_messages = [
135+
message for message in messages if message["message_type"] == "Template"
136+
]
137+
138+
# Iterate through template messages
139+
for template_message in template_messages:
140+
# Find the template that this message is using
141+
template = frappe.get_doc("WhatsApp Templates", template_message["template"])
142+
143+
# If the template is found, add the template details to the template message
144+
if template:
145+
template_message["template_name"] = template.template_name
146+
if template_message["template_parameters"]:
147+
parameters = json.loads(template_message["template_parameters"])
148+
template.template = parse_template_parameters(
149+
template.template, parameters
150+
)
151+
152+
template_message["template"] = template.template
153+
if template_message["template_header_parameters"]:
154+
header_parameters = json.loads(
155+
template_message["template_header_parameters"]
156+
)
157+
template.header = parse_template_parameters(
158+
template.header, header_parameters
159+
)
160+
template_message["header"] = template.header
161+
template_message["footer"] = template.footer
162+
163+
# Filter messages to get only reaction messages
164+
reaction_messages = [
165+
message for message in messages if message["content_type"] == "reaction"
166+
]
167+
168+
# Iterate through reaction messages
169+
for reaction_message in reaction_messages:
170+
# Find the message that this reaction is reacting to
171+
reacted_message = next(
172+
(
173+
m
174+
for m in messages
175+
if m["message_id"] == reaction_message["reply_to_message_id"]
176+
),
177+
None,
178+
)
179+
180+
# If the reacted message is found, add the reaction to it
181+
if reacted_message:
182+
reacted_message["reaction"] = reaction_message["message"]
183+
184+
for message in messages:
185+
from_name = get_from_name(message) if message["from"] else _("You")
186+
message["from_name"] = from_name
187+
# Filter messages to get only replies
188+
reply_messages = [message for message in messages if message["is_reply"]]
189+
190+
# Iterate through reply messages
191+
for reply_message in reply_messages:
192+
# Find the message that this message is replying to
193+
replied_message = next(
194+
(
195+
m
196+
for m in messages
197+
if m["message_id"] == reply_message["reply_to_message_id"]
198+
),
199+
None,
200+
)
201+
202+
# If the replied message is found, add the reply details to the reply message
203+
from_name = (
204+
get_from_name(reply_message) if replied_message["from"] else _("You")
205+
)
206+
if replied_message:
207+
message = replied_message["message"]
208+
if replied_message["message_type"] == "Template":
209+
message = replied_message["template"]
210+
reply_message["reply_message"] = message
211+
reply_message["header"] = replied_message.get("header") or ""
212+
reply_message["footer"] = replied_message.get("footer") or ""
213+
reply_message["reply_to"] = replied_message["name"]
214+
reply_message["reply_to_type"] = replied_message["type"]
215+
reply_message["reply_to_from"] = from_name
216+
217+
return [message for message in messages if message["content_type"] != "reaction"]
218+
219+
220+
@frappe.whitelist()
221+
def create_whatsapp_message(
222+
reference_doctype,
223+
reference_name,
224+
message,
225+
to,
226+
attach,
227+
reply_to,
228+
content_type="text",
229+
):
230+
doc = frappe.new_doc("WhatsApp Message")
231+
232+
if reply_to:
233+
reply_doc = frappe.get_doc("WhatsApp Message", reply_to)
234+
doc.update(
235+
{
236+
"is_reply": True,
237+
"reply_to_message_id": reply_doc.message_id,
238+
}
239+
)
240+
241+
doc.update(
242+
{
243+
"reference_doctype": reference_doctype,
244+
"reference_name": reference_name,
245+
"message": message or attach,
246+
"to": to,
247+
"attach": attach,
248+
"content_type": content_type,
249+
}
250+
)
251+
doc.insert(ignore_permissions=True)
252+
return doc.name
253+
254+
255+
@frappe.whitelist()
256+
def send_whatsapp_template(reference_doctype, reference_name, template, to):
257+
doc = frappe.new_doc("WhatsApp Message")
258+
doc.update(
259+
{
260+
"reference_doctype": reference_doctype,
261+
"reference_name": reference_name,
262+
"message_type": "Template",
263+
"message": "Template message",
264+
"content_type": "text",
265+
"use_template": True,
266+
"template": template,
267+
"to": to,
268+
}
269+
)
270+
doc.insert(ignore_permissions=True)
271+
return doc.name
272+
273+
274+
@frappe.whitelist()
275+
def react_on_whatsapp_message(emoji, reply_to_name):
276+
reply_to_doc = frappe.get_doc("WhatsApp Message", reply_to_name)
277+
to = reply_to_doc.type == "Incoming" and reply_to_doc.get("from") or reply_to_doc.to
278+
doc = frappe.new_doc("WhatsApp Message")
279+
doc.update(
280+
{
281+
"reference_doctype": reply_to_doc.reference_doctype,
282+
"reference_name": reply_to_doc.reference_name,
283+
"message": emoji,
284+
"to": to,
285+
"reply_to_message_id": reply_to_doc.message_id,
286+
"content_type": "reaction",
287+
}
288+
)
289+
doc.insert(ignore_permissions=True)
290+
return doc.name
291+
292+
293+
def parse_template_parameters(string, parameters):
294+
for i, parameter in enumerate(parameters, start=1):
295+
placeholder = "{{" + str(i) + "}}"
296+
string = string.replace(placeholder, parameter)
297+
298+
return string
299+
300+
301+
def get_from_name(message):
302+
doc = frappe.get_doc(message["reference_doctype"], message["reference_name"])
303+
from_name = ""
304+
if message["reference_doctype"] == "CRM Deal":
305+
if doc.get("contacts"):
306+
for c in doc.get("contacts"):
307+
if c.is_primary:
308+
from_name = c.full_name or c.mobile_no
309+
break
310+
else:
311+
from_name = doc.get("lead_name")
312+
else:
313+
from_name = doc.get("first_name") + " " + doc.get("last_name")
314+
return from_name

0 commit comments

Comments
 (0)