Skip to content

Commit

Permalink
Merge pull request #327 from frappe/develop
Browse files Browse the repository at this point in the history
chore: Merge develop to main
  • Loading branch information
shariquerik authored Sep 3, 2024
2 parents 768c35f + 1040c7e commit 46daa6f
Show file tree
Hide file tree
Showing 43 changed files with 1,142 additions and 334 deletions.
56 changes: 54 additions & 2 deletions crm/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from bs4 import BeautifulSoup
import frappe
from frappe.translate import get_all_translations
from frappe.utils import cstr
from frappe.utils import validate_email_address, split_emails, cstr
from frappe.utils.telemetry import POSTHOG_HOST_FIELD, POSTHOG_PROJECT_FIELD


Expand Down Expand Up @@ -47,11 +47,63 @@ def get_user_signature():
content = f'<br><p class="signature">{signature}</p>'
return content


@frappe.whitelist()
def get_posthog_settings():
return {
"posthog_project_id": frappe.conf.get(POSTHOG_PROJECT_FIELD),
"posthog_host": frappe.conf.get(POSTHOG_HOST_FIELD),
"enable_telemetry": frappe.get_system_settings("enable_telemetry"),
"telemetry_site_age": frappe.utils.telemetry.site_age(),
}
}


def check_app_permission():
if frappe.session.user == "Administrator":
return True

roles = frappe.get_roles()
if any(role in ["System Manager", "Sales User", "Sales Manager", "Sales Master Manager"] for role in roles):
return True

return False


@frappe.whitelist(allow_guest=True)
def accept_invitation(key: str = None):
if not key:
frappe.throw("Invalid or expired key")

result = frappe.db.get_all("CRM Invitation", filters={"key": key}, pluck="name")
if not result:
frappe.throw("Invalid or expired key")

invitation = frappe.get_doc("CRM Invitation", result[0])
invitation.accept()
invitation.reload()

if invitation.status == "Accepted":
frappe.local.login_manager.login_as(invitation.email)
frappe.local.response["type"] = "redirect"
frappe.local.response["location"] = "/crm"


@frappe.whitelist()
def invite_by_email(emails: str, role: str):
if not emails:
return
email_string = validate_email_address(emails, throw=False)
email_list = split_emails(email_string)
if not email_list:
return
existing_members = frappe.db.get_all("User", filters={"email": ["in", email_list]}, pluck="email")
existing_invites = frappe.db.get_all(
"CRM Invitation",
filters={"email": ["in", email_list], "role": ["in", ["Sales Manager", "Sales User"]]},
pluck="email",
)

to_invite = list(set(email_list) - set(existing_members) - set(existing_invites))

for email in to_invite:
frappe.get_doc(doctype="CRM Invitation", email=email, role=role).insert(ignore_permissions=True)
Empty file.
12 changes: 12 additions & 0 deletions crm/fcrm/doctype/crm_invitation/crm_invitation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt

frappe.ui.form.on("CRM Invitation", {
refresh(frm) {
if (frm.doc.status != "Accepted") {
frm.add_custom_button(__("Accept Invitation"), () => {
return frm.call("accept_invitation");
});
}
},
});
112 changes: 112 additions & 0 deletions crm/fcrm/doctype/crm_invitation/crm_invitation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2024-09-03 12:19:18.933810",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"email",
"role",
"key",
"invited_by",
"column_break_dsuz",
"status",
"email_sent_at",
"accepted_at"
],
"fields": [
{
"fieldname": "email",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Email",
"reqd": 1
},
{
"fieldname": "role",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Role",
"options": "\nSales User\nSales Manager",
"reqd": 1
},
{
"fieldname": "key",
"fieldtype": "Data",
"label": "Key"
},
{
"fieldname": "invited_by",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Invited By",
"options": "User"
},
{
"fieldname": "column_break_dsuz",
"fieldtype": "Column Break"
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "\nPending\nAccepted\nExpired"
},
{
"fieldname": "email_sent_at",
"fieldtype": "Datetime",
"label": "Email Sent At"
},
{
"fieldname": "accepted_at",
"fieldtype": "Datetime",
"label": "Accepted At"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-09-03 14:59:29.450018",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Invitation",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"share": 1
}
],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
79 changes: 79 additions & 0 deletions crm/fcrm/doctype/crm_invitation/crm_invitation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt

import frappe
from frappe.model.document import Document


class CRMInvitation(Document):
def before_insert(self):
frappe.utils.validate_email_address(self.email, True)

self.key = frappe.generate_hash(length=12)
self.invited_by = frappe.session.user
self.status = "Pending"

def after_insert(self):
self.invite_via_email()

def invite_via_email(self):
invite_link = frappe.utils.get_url(f"/api/method/crm.api.accept_invitation?key={self.key}")
if frappe.local.dev_server:
print(f"Invite link for {self.email}: {invite_link}")

title = f"Frappe CRM"
template = "crm_invitation"

frappe.sendmail(
recipients=self.email,
subject=f"You have been invited to join {title}",
template=template,
args={"title": title, "invite_link": invite_link},
now=True,
)
self.db_set("email_sent_at", frappe.utils.now())

@frappe.whitelist()
def accept_invitation(self):
frappe.only_for("System Manager")
self.accept()

def accept(self):
if self.status == "Expired":
frappe.throw("Invalid or expired key")

user = self.create_user_if_not_exists()
user.append_roles(self.role)
user.save(ignore_permissions=True)

self.status = "Accepted"
self.accepted_at = frappe.utils.now()
self.save(ignore_permissions=True)

def create_user_if_not_exists(self):
if not frappe.db.exists("User", self.email):
first_name = self.email.split("@")[0].title()
user = frappe.get_doc(
doctype="User",
user_type="System User",
email=self.email,
send_welcome_email=0,
first_name=first_name,
).insert(ignore_permissions=True)
else:
user = frappe.get_doc("User", self.email)
return user


def expire_invitations():
"""expire invitations after 3 days"""
from frappe.utils import add_days, now

days = 3
invitations_to_expire = frappe.db.get_all(
"CRM Invitation", filters={"status": "Pending", "creation": ["<", add_days(now(), -days)]}
)
for invitation in invitations_to_expire:
invitation = frappe.get_doc("CRM Invitation", invitation.name)
invitation.status = "Expired"
invitation.save(ignore_permissions=True)
9 changes: 9 additions & 0 deletions crm/fcrm/doctype/crm_invitation/test_crm_invitation.py
Original file line number Diff line number Diff line change
@@ -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 TestCRMInvitation(FrappeTestCase):
pass
2 changes: 1 addition & 1 deletion crm/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"logo": "/assets/crm/manifest/apple-icon-180.png",
"title": "CRM",
"route": "/crm",
# "has_permission": "crm.api.permission.has_app_permission"
"has_permission": "crm.api.check_app_permission",
}
]

Expand Down
4 changes: 4 additions & 0 deletions crm/templates/emails/crm_invitation.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<h2>You have been invited to join Frappe CRM</h2>
<p>
<a class="btn btn-primary" href="{{ invite_link }}">Accept Invitation</a>
</p>
Loading

0 comments on commit 46daa6f

Please sign in to comment.