snapshot of https://github.com/frappe/crm.git (develop, 2026-09-18; blobs > 131072B omitted)
Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 1s
Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 1s
This commit is contained in:
0
crm/demo/__init__.py
Normal file
0
crm/demo/__init__.py
Normal file
191
crm/demo/activities.py
Normal file
191
crm/demo/activities.py
Normal file
@@ -0,0 +1,191 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import frappe
|
||||
|
||||
from crm.demo.utils import (
|
||||
build_full_names,
|
||||
insert_comment,
|
||||
insert_communication,
|
||||
insert_version,
|
||||
resolve_owners,
|
||||
)
|
||||
|
||||
|
||||
def create_demo_activities(lead_names, demo_users):
|
||||
"""Create comments, email communications, and field-change versions for demo leads."""
|
||||
session_user, owner_1, owner_2, _ = resolve_owners(demo_users)
|
||||
_full_names = build_full_names(session_user)
|
||||
|
||||
now = datetime.now()
|
||||
|
||||
comment_names = _create_comments(lead_names, session_user, owner_1, owner_2, _full_names, now)
|
||||
communication_names = _create_communications(lead_names, session_user, owner_1, _full_names, now)
|
||||
_create_versions(lead_names, session_user, owner_1, owner_2, now)
|
||||
|
||||
return {"comments": comment_names, "communications": communication_names}
|
||||
|
||||
|
||||
def _create_comments(lead_names, session_user, owner_1, owner_2, full_names, now):
|
||||
comments_data = [
|
||||
{
|
||||
"lead": lead_names[0],
|
||||
"owner": session_user,
|
||||
"content": (
|
||||
"<p>Initial discovery call was very productive. Alice's team of ~300 engineers is actively "
|
||||
"evaluating CRM solutions. She mentioned budget has already been approved — just need "
|
||||
"to get sign-off from the VP of Sales.</p>"
|
||||
),
|
||||
"days_ago": 56, # lead[0] deal created at 50 days ago — keep before that
|
||||
},
|
||||
{
|
||||
"lead": lead_names[1],
|
||||
"owner": owner_1,
|
||||
"content": (
|
||||
"<p>Bob mentioned they had a poor experience with their last CRM. He emphasized that "
|
||||
"onboarding support and data migration are top priorities. Need to loop in the "
|
||||
"customer success team before the next call.</p>"
|
||||
),
|
||||
"days_ago": 9,
|
||||
},
|
||||
{
|
||||
"lead": lead_names[2],
|
||||
"owner": owner_2,
|
||||
"content": (
|
||||
"<p>Carol is temporarily unavailable — on leave until end of month. Her colleague "
|
||||
"Mark is the interim contact. Revisit in 3 weeks with updated pricing deck.</p>"
|
||||
),
|
||||
"days_ago": 11,
|
||||
},
|
||||
{
|
||||
"lead": lead_names[3],
|
||||
"owner": owner_1,
|
||||
"content": (
|
||||
"<p>Technical demo completed successfully. David's engineering team was impressed with "
|
||||
"the API and custom field options. He's requesting pricing for 15 seats — send "
|
||||
"the proposal by Friday.</p>"
|
||||
),
|
||||
"days_ago": 41, # lead[3] deal created at 37 days ago — keep before that
|
||||
},
|
||||
]
|
||||
|
||||
return [
|
||||
insert_comment(
|
||||
"CRM Lead",
|
||||
data["lead"],
|
||||
data["owner"],
|
||||
data["content"],
|
||||
full_names,
|
||||
now - timedelta(days=data["days_ago"]),
|
||||
)
|
||||
for data in comments_data
|
||||
]
|
||||
|
||||
|
||||
def _create_communications(lead_names, session_user, owner_1, full_names, now):
|
||||
# Lead emails used for recipient/sender fields
|
||||
lead_emails = {
|
||||
lead_names[0]: "alice.johnson@example.com",
|
||||
lead_names[1]: "bob.martinez@example.com",
|
||||
lead_names[3]: "david.lee@example.com",
|
||||
}
|
||||
|
||||
comms_data = [
|
||||
{
|
||||
"lead": lead_names[0],
|
||||
"owner": session_user,
|
||||
"sent_or_received": "Sent",
|
||||
"subject": "Following up on our discovery call — Acme Corp",
|
||||
"content": (
|
||||
"<p>Hi Alice,</p>"
|
||||
"<p>It was great speaking with you earlier today. As discussed, I'm sharing our "
|
||||
"product overview and a link to schedule a full demo with your team.</p>"
|
||||
"<p>Please let me know if you have any questions before we meet.</p>"
|
||||
"<p>Best regards</p>"
|
||||
),
|
||||
"sender": session_user,
|
||||
"recipients": lead_emails[lead_names[0]],
|
||||
"days_ago": 54, # lead[0] deal at 50 days ago — keep before that
|
||||
},
|
||||
{
|
||||
"lead": lead_names[1],
|
||||
"owner": owner_1,
|
||||
"sent_or_received": "Received",
|
||||
"subject": "Re: CRM Demo Request — Globex Industries",
|
||||
"content": (
|
||||
"<p>Hi,</p>"
|
||||
"<p>Thanks for reaching out. We'd definitely like to see a live demo, specifically "
|
||||
"around pipeline management and email integration. Can we schedule something for "
|
||||
"next Tuesday at 2 PM?</p>"
|
||||
"<p>Best,<br>Bob Martinez<br>CEO, Globex Industries</p>"
|
||||
),
|
||||
"sender": lead_emails[lead_names[1]],
|
||||
"recipients": owner_1,
|
||||
"days_ago": 8,
|
||||
},
|
||||
{
|
||||
"lead": lead_names[3],
|
||||
"owner": owner_1,
|
||||
"sent_or_received": "Sent",
|
||||
"subject": "CRM Pricing Proposal — TechStart Inc (15 seats)",
|
||||
"content": (
|
||||
"<p>Hi David,</p>"
|
||||
"<p>Great connecting with your team today — really glad the API walkthrough was "
|
||||
"useful. Attached is our pricing proposal for 15 seats including onboarding and "
|
||||
"priority support.</p>"
|
||||
"<p>Happy to jump on a call if you have questions.</p>"
|
||||
"<p>Cheers</p>"
|
||||
),
|
||||
"sender": owner_1,
|
||||
"recipients": lead_emails[lead_names[3]],
|
||||
"days_ago": 39, # lead[3] deal at 37 days ago — keep before that
|
||||
},
|
||||
]
|
||||
|
||||
return [
|
||||
insert_communication(
|
||||
"CRM Lead", data["lead"], data, full_names, now - timedelta(days=data["days_ago"])
|
||||
)
|
||||
for data in comms_data
|
||||
]
|
||||
|
||||
|
||||
def _create_versions(lead_names, session_user, owner_1, owner_2, now):
|
||||
versions_data = [
|
||||
{
|
||||
"lead": lead_names[0],
|
||||
"owner": session_user,
|
||||
"changed": [["no_of_employees", "201-500", "501-1000"]],
|
||||
"days_ago": 52, # lead[0] deal at 50 days ago — keep before that
|
||||
},
|
||||
{
|
||||
"lead": lead_names[1],
|
||||
"owner": owner_1,
|
||||
"changed": [["annual_revenue", None, "10000000"]],
|
||||
"days_ago": 10,
|
||||
},
|
||||
{
|
||||
"lead": lead_names[2],
|
||||
"owner": owner_2,
|
||||
"changed": [["source", "Email", "Referral"]],
|
||||
"days_ago": 12,
|
||||
},
|
||||
{
|
||||
"lead": lead_names[3],
|
||||
"owner": owner_1,
|
||||
"changed": [["status", "Contacted", "Qualified"]],
|
||||
"days_ago": 38, # lead[3] deal at 37 days ago — keep before that
|
||||
},
|
||||
]
|
||||
|
||||
for data in versions_data:
|
||||
insert_version(
|
||||
"CRM Lead", data["lead"], data["owner"], data["changed"], now - timedelta(days=data["days_ago"])
|
||||
)
|
||||
|
||||
|
||||
def delete_demo_activities(activity_data):
|
||||
"""Delete communications (comments and versions are cascade-deleted with their lead)."""
|
||||
communication_names = activity_data.get("communications", [])
|
||||
for name in communication_names:
|
||||
if frappe.db.exists("Communication", name):
|
||||
frappe.delete_doc("Communication", name, ignore_permissions=True, force=True)
|
||||
91
crm/demo/api.py
Normal file
91
crm/demo/api.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.utils.telemetry import capture
|
||||
|
||||
DEMO_STATE_KEY = "crm_demo_data_created"
|
||||
DEMO_LEADS_KEY = "crm_demo_leads"
|
||||
DEMO_NOTES_KEY = "crm_demo_notes"
|
||||
DEMO_TASKS_KEY = "crm_demo_tasks"
|
||||
DEMO_CALL_LOGS_KEY = "crm_demo_call_logs"
|
||||
DEMO_ACTIVITIES_KEY = "crm_demo_activities"
|
||||
DEMO_DEALS_KEY = "crm_demo_deals"
|
||||
|
||||
|
||||
def create_demo_data(_args: dict | None = None):
|
||||
if frappe.db.get_default(DEMO_STATE_KEY):
|
||||
return
|
||||
|
||||
from crm.demo.activities import create_demo_activities
|
||||
from crm.demo.call_logs import create_demo_call_logs
|
||||
from crm.demo.deals import create_demo_deals
|
||||
from crm.demo.leads import create_demo_leads
|
||||
from crm.demo.notes import create_demo_notes
|
||||
from crm.demo.tasks import create_demo_tasks
|
||||
from crm.demo.users import create_demo_users
|
||||
|
||||
demo_users = create_demo_users()
|
||||
lead_names = create_demo_leads(demo_users)
|
||||
note_names = create_demo_notes(lead_names, demo_users)
|
||||
task_names = create_demo_tasks(lead_names, demo_users)
|
||||
call_log_names = create_demo_call_logs(lead_names, demo_users)
|
||||
activity_data = create_demo_activities(lead_names, demo_users)
|
||||
|
||||
from crm.demo.leads import rebackdate_demo_leads
|
||||
|
||||
rebackdate_demo_leads(lead_names, demo_users)
|
||||
|
||||
deal_data = create_demo_deals(lead_names, demo_users)
|
||||
frappe.db.set_default(DEMO_LEADS_KEY, json.dumps(lead_names))
|
||||
frappe.db.set_default(DEMO_NOTES_KEY, json.dumps(note_names))
|
||||
frappe.db.set_default(DEMO_TASKS_KEY, json.dumps(task_names))
|
||||
frappe.db.set_default(DEMO_CALL_LOGS_KEY, json.dumps(call_log_names))
|
||||
frappe.db.set_default(DEMO_ACTIVITIES_KEY, json.dumps(activity_data))
|
||||
frappe.db.set_default(DEMO_DEALS_KEY, json.dumps(deal_data))
|
||||
frappe.db.set_default(DEMO_STATE_KEY, "1")
|
||||
|
||||
capture("demo_data_created", "crm")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def clear_demo_data():
|
||||
frappe.only_for(["Sales Manager", "System Manager"], True)
|
||||
|
||||
if not frappe.db.get_default(DEMO_STATE_KEY):
|
||||
return
|
||||
|
||||
from crm.demo.activities import delete_demo_activities
|
||||
from crm.demo.call_logs import delete_demo_call_logs
|
||||
from crm.demo.deals import delete_demo_deals
|
||||
from crm.demo.leads import delete_demo_leads
|
||||
from crm.demo.notes import delete_demo_notes
|
||||
from crm.demo.tasks import delete_demo_tasks
|
||||
from crm.demo.users import DEMO_USER_EMAILS, delete_demo_users
|
||||
|
||||
lead_names = json.loads(frappe.db.get_default(DEMO_LEADS_KEY) or "[]")
|
||||
note_names = json.loads(frappe.db.get_default(DEMO_NOTES_KEY) or "[]")
|
||||
task_names = json.loads(frappe.db.get_default(DEMO_TASKS_KEY) or "[]")
|
||||
call_log_names = json.loads(frappe.db.get_default(DEMO_CALL_LOGS_KEY) or "[]")
|
||||
activity_data = json.loads(frappe.db.get_default(DEMO_ACTIVITIES_KEY) or "{}")
|
||||
deal_data = json.loads(frappe.db.get_default(DEMO_DEALS_KEY) or "{}")
|
||||
delete_demo_deals(deal_data, lead_names)
|
||||
delete_demo_activities(activity_data)
|
||||
delete_demo_notes(note_names)
|
||||
delete_demo_tasks(task_names)
|
||||
delete_demo_call_logs(call_log_names)
|
||||
delete_demo_leads(lead_names)
|
||||
delete_demo_users(DEMO_USER_EMAILS)
|
||||
frappe.db.set_default(DEMO_LEADS_KEY, None)
|
||||
frappe.db.set_default(DEMO_NOTES_KEY, None)
|
||||
frappe.db.set_default(DEMO_TASKS_KEY, None)
|
||||
frappe.db.set_default(DEMO_CALL_LOGS_KEY, None)
|
||||
frappe.db.set_default(DEMO_ACTIVITIES_KEY, None)
|
||||
frappe.db.set_default(DEMO_DEALS_KEY, None)
|
||||
frappe.db.set_default(DEMO_STATE_KEY, None)
|
||||
|
||||
capture("demo_data_cleared", "crm")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_demo_state():
|
||||
return {"demo_data_created": bool(frappe.db.get_default(DEMO_STATE_KEY))}
|
||||
122
crm/demo/call_logs.py
Normal file
122
crm/demo/call_logs.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import frappe
|
||||
|
||||
from crm.demo.users import DEMO_USERS
|
||||
from crm.demo.utils import resolve_owners
|
||||
|
||||
|
||||
def create_demo_call_logs(lead_names, demo_users):
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
now = datetime.now()
|
||||
session_user, owner_1, owner_2, _ = resolve_owners(demo_users)
|
||||
|
||||
# Build rep phone number map from DEMO_USERS mobile numbers
|
||||
_user_mobile = {u["email"]: u["mobile_no"] for u in DEMO_USERS}
|
||||
rep_numbers = {
|
||||
session_user: _user_mobile.get(session_user, "+1 555 100 0001"),
|
||||
owner_1: _user_mobile.get(owner_1, "+1 555 100 0002"),
|
||||
owner_2: _user_mobile.get(owner_2, "+1 555 100 0003"),
|
||||
}
|
||||
|
||||
call_logs_data = [
|
||||
{
|
||||
"type": "Outgoing",
|
||||
"status": "Completed",
|
||||
"telephony_medium": "Manual",
|
||||
"duration": 720,
|
||||
"from": rep_numbers[session_user],
|
||||
"to": "+1 555 000 1234",
|
||||
"caller": session_user,
|
||||
"start_time": now - timedelta(days=5, hours=2),
|
||||
"end_time": now - timedelta(days=5, hours=2) + timedelta(seconds=720),
|
||||
"reference_doctype": "CRM Lead",
|
||||
"reference_docname": lead_names[0],
|
||||
},
|
||||
{
|
||||
"type": "Incoming",
|
||||
"status": "Completed",
|
||||
"telephony_medium": "Manual",
|
||||
"duration": 300,
|
||||
"from": "+1 555 000 1234",
|
||||
"to": rep_numbers[session_user],
|
||||
"receiver": session_user,
|
||||
"start_time": now - timedelta(days=2, hours=1),
|
||||
"end_time": now - timedelta(days=2, hours=1) + timedelta(seconds=300),
|
||||
"reference_doctype": "CRM Lead",
|
||||
"reference_docname": lead_names[0],
|
||||
},
|
||||
{
|
||||
"type": "Outgoing",
|
||||
"status": "Completed",
|
||||
"telephony_medium": "Manual",
|
||||
"duration": 540,
|
||||
"from": rep_numbers[owner_1],
|
||||
"to": "+1 555 000 5678",
|
||||
"caller": owner_1,
|
||||
"start_time": now - timedelta(days=4),
|
||||
"end_time": now - timedelta(days=4) + timedelta(seconds=540),
|
||||
"reference_doctype": "CRM Lead",
|
||||
"reference_docname": lead_names[1],
|
||||
},
|
||||
{
|
||||
"type": "Outgoing",
|
||||
"status": "No Answer",
|
||||
"telephony_medium": "Manual",
|
||||
"duration": 0,
|
||||
"from": rep_numbers[owner_1],
|
||||
"to": "+1 555 000 5678",
|
||||
"caller": owner_1,
|
||||
"start_time": now - timedelta(days=1),
|
||||
"end_time": now - timedelta(days=1),
|
||||
"reference_doctype": "CRM Lead",
|
||||
"reference_docname": lead_names[1],
|
||||
},
|
||||
{
|
||||
"type": "Outgoing",
|
||||
"status": "Completed",
|
||||
"telephony_medium": "Manual",
|
||||
"duration": 420,
|
||||
"from": rep_numbers[owner_2],
|
||||
"to": "+1 555 000 9012",
|
||||
"caller": owner_2,
|
||||
"start_time": now - timedelta(days=3),
|
||||
"end_time": now - timedelta(days=3) + timedelta(seconds=420),
|
||||
"reference_doctype": "CRM Lead",
|
||||
"reference_docname": lead_names[2],
|
||||
},
|
||||
{
|
||||
"type": "Incoming",
|
||||
"status": "Completed",
|
||||
"telephony_medium": "Manual",
|
||||
"duration": 600,
|
||||
"from": "+1 555 000 3456",
|
||||
"to": rep_numbers[owner_1],
|
||||
"receiver": owner_1,
|
||||
"start_time": now - timedelta(days=6),
|
||||
"end_time": now - timedelta(days=6) + timedelta(seconds=600),
|
||||
"reference_doctype": "CRM Lead",
|
||||
"reference_docname": lead_names[3],
|
||||
},
|
||||
]
|
||||
|
||||
created = []
|
||||
for data in call_logs_data:
|
||||
log_owner = data.get("caller") or data.get("receiver") or frappe.session.user
|
||||
log_ts = data.get("start_time")
|
||||
log = frappe.get_doc(
|
||||
{"doctype": "CRM Call Log", "id": frappe.generate_hash(length=10), **data}
|
||||
).insert(ignore_permissions=True)
|
||||
patch = {"owner": log_owner, "modified_by": log_owner}
|
||||
if log_ts:
|
||||
patch["creation"] = log_ts
|
||||
patch["modified"] = log_ts
|
||||
frappe.db.set_value("CRM Call Log", log.name, patch, update_modified=False)
|
||||
created.append(log.name)
|
||||
|
||||
return created
|
||||
|
||||
|
||||
def delete_demo_call_logs(call_log_names):
|
||||
for name in call_log_names:
|
||||
if frappe.db.exists("CRM Call Log", name):
|
||||
frappe.delete_doc("CRM Call Log", name, ignore_permissions=True, force=True)
|
||||
348
crm/demo/deals.py
Normal file
348
crm/demo/deals.py
Normal file
@@ -0,0 +1,348 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder import DocType
|
||||
|
||||
from crm.demo.utils import (
|
||||
backdate,
|
||||
build_full_names,
|
||||
fix_auto_records,
|
||||
insert_comment,
|
||||
insert_communication,
|
||||
insert_version,
|
||||
resolve_owners,
|
||||
)
|
||||
|
||||
|
||||
def create_demo_deals(lead_names, demo_users):
|
||||
"""Convert seven leads into deals and add deal-specific activity."""
|
||||
from crm.fcrm.doctype.crm_lead.crm_lead import convert_to_deal
|
||||
|
||||
session_user, owner_1, owner_2, _ = resolve_owners(demo_users)
|
||||
_full_names = build_full_names(session_user)
|
||||
|
||||
# leads[0] Alice, [3] David, [7] Henry, [8] Iris, [9] Jack → 5 active/won deals
|
||||
# leads[10] Karen, [11] Leo → 2 lost deals
|
||||
d_alice = convert_to_deal(
|
||||
lead=lead_names[0],
|
||||
deal={"status": "Demo/Making", "deal_value": 120000, "probability": 50, "deal_owner": session_user},
|
||||
)
|
||||
d_david = convert_to_deal(
|
||||
lead=lead_names[3],
|
||||
deal={"status": "Proposal/Quotation", "deal_value": 45000, "probability": 70, "deal_owner": owner_1},
|
||||
)
|
||||
d_henry = convert_to_deal(
|
||||
lead=lead_names[7],
|
||||
deal={"status": "Negotiation", "deal_value": 85000, "probability": 60, "deal_owner": owner_2},
|
||||
)
|
||||
d_iris = convert_to_deal(
|
||||
lead=lead_names[8],
|
||||
deal={"status": "Qualification", "deal_value": 60000, "probability": 35, "deal_owner": session_user},
|
||||
)
|
||||
d_jack = convert_to_deal(
|
||||
lead=lead_names[9],
|
||||
deal={"status": "Won", "deal_value": 175000, "probability": 100, "deal_owner": owner_1},
|
||||
)
|
||||
d_karen = convert_to_deal(
|
||||
lead=lead_names[10],
|
||||
deal={
|
||||
"status": "Lost",
|
||||
"deal_value": 95000,
|
||||
"probability": 0,
|
||||
"deal_owner": owner_2,
|
||||
"lost_reason": "Competition",
|
||||
"lost_notes": "Prospect chose a competitor offering deeper BI integrations out of the box. Price was not the issue — feature parity was.",
|
||||
},
|
||||
)
|
||||
d_leo = convert_to_deal(
|
||||
lead=lead_names[11],
|
||||
deal={
|
||||
"status": "Lost",
|
||||
"deal_value": 55000,
|
||||
"probability": 0,
|
||||
"deal_owner": session_user,
|
||||
"lost_reason": "Budget constraints",
|
||||
"lost_notes": "Q2 budget was cut. Leo said they would revisit in Q4 once headcount hiring is complete. Added to nurture sequence.",
|
||||
},
|
||||
)
|
||||
|
||||
deal_names_list = [d_alice, d_david, d_henry, d_iris, d_jack, d_karen, d_leo]
|
||||
# Converted lead indices — must match deal_names_list order
|
||||
_converted_lead_indices = [0, 3, 7, 8, 9, 10, 11]
|
||||
# Days ago each deal was created (always after its lead)
|
||||
_deal_days = [50, 37, 18, 11, 24, 9, 5]
|
||||
_deal_owners = [session_user, owner_1, owner_2, session_user, owner_1, owner_2, session_user]
|
||||
# Organization logos keyed by org name (index order: alice, david, henry, iris, jack, karen, leo)
|
||||
_org_logos = {
|
||||
"Acme Corp": "/assets/crm/images/demo/acme-corp.png",
|
||||
"TechStart Inc": "/assets/crm/images/demo/techstart-inc.png",
|
||||
"PivotTech Solutions": "/assets/crm/images/demo/pivottech-solutions.png",
|
||||
"ScaleUp Labs": "/assets/crm/images/demo/scaleup-labs.png",
|
||||
"Meridian Systems": "/assets/crm/images/demo/meridian-systems.png",
|
||||
"Vertex Analytics": "/assets/crm/images/demo/vertex-analytics.png",
|
||||
"Forge Digital": "/assets/crm/images/demo/forge-digital.png",
|
||||
}
|
||||
now = datetime.now()
|
||||
|
||||
for d_name, days, d_owner, li in zip(
|
||||
deal_names_list, _deal_days, _deal_owners, _converted_lead_indices, strict=False
|
||||
):
|
||||
ts = now - timedelta(days=days)
|
||||
backdate("CRM Deal", d_name, d_owner, ts)
|
||||
org = frappe.db.get_value("CRM Deal", d_name, "organization")
|
||||
if org:
|
||||
logo = _org_logos.get(org)
|
||||
if logo:
|
||||
frappe.db.set_value("CRM Organization", org, "organization_logo", logo, update_modified=False)
|
||||
backdate("CRM Organization", org, d_owner, ts)
|
||||
contacts = frappe.get_all(
|
||||
"CRM Contacts", filters={"parent": d_name, "parenttype": "CRM Deal"}, pluck="contact"
|
||||
)
|
||||
for contact in contacts:
|
||||
if contact:
|
||||
backdate("Contact", contact, d_owner, ts)
|
||||
backdate("CRM Lead", lead_names[li], d_owner, ts, set_creation=False)
|
||||
fix_auto_records("CRM Deal", d_name, d_owner, ts)
|
||||
fix_auto_records("CRM Lead", lead_names[li], d_owner, ts)
|
||||
|
||||
comment_names = _create_deal_comments(deal_names_list, session_user, owner_1, owner_2, _full_names, now)
|
||||
communication_names = _create_deal_communications(
|
||||
deal_names_list, session_user, owner_1, _full_names, now
|
||||
)
|
||||
_create_deal_versions(deal_names_list, session_user, owner_1, owner_2, now)
|
||||
|
||||
# Re-backdate modified to last-activity date so active deals sort to the top of the list.
|
||||
# Index order: alice, david, henry, iris, jack, karen, leo
|
||||
_deal_last_touched_days = [2, 4, 7, 10, 6, 8, 12]
|
||||
for d_name, days, d_owner in zip(deal_names_list, _deal_last_touched_days, _deal_owners, strict=False):
|
||||
backdate("CRM Deal", d_name, d_owner, now - timedelta(days=days), set_creation=False)
|
||||
|
||||
return {
|
||||
"deals": deal_names_list,
|
||||
"comments": comment_names,
|
||||
"communications": communication_names,
|
||||
}
|
||||
|
||||
|
||||
def _create_deal_comments(deal_names, session_user, owner_1, owner_2, full_names, now):
|
||||
comments_data = [
|
||||
{
|
||||
"deal": deal_names[0], # Alice / Acme Corp
|
||||
"owner": session_user,
|
||||
"content": (
|
||||
"<p>Live demo went well — Alice's product team joined and had great questions "
|
||||
"about workflow automation and bulk import. They want a custom sandbox environment "
|
||||
"to evaluate with their own data before signing. Following up to arrange access.</p>"
|
||||
),
|
||||
"days_ago": 3,
|
||||
},
|
||||
{
|
||||
"deal": deal_names[1], # David / TechStart Inc
|
||||
"owner": owner_1,
|
||||
"content": (
|
||||
"<p>David reviewed the proposal with his co-founder. They're happy with the pricing "
|
||||
"but want a 3-month pilot before committing to annual. Checking with management on "
|
||||
"whether we can offer a pilot discount.</p>"
|
||||
),
|
||||
"days_ago": 5,
|
||||
},
|
||||
{
|
||||
"deal": deal_names[2], # Henry / PivotTech Solutions
|
||||
"owner": owner_2,
|
||||
"content": (
|
||||
"<p>Negotiation call with Henry went long but productive. He’s pushing for a 15% "
|
||||
"discount on the annual plan citing budget constraints. Legal team is now reviewing "
|
||||
"the contract — decision expected by end of week.</p>"
|
||||
),
|
||||
"days_ago": 7,
|
||||
},
|
||||
{
|
||||
"deal": deal_names[3], # Iris / ScaleUp Labs
|
||||
"owner": session_user,
|
||||
"content": (
|
||||
"<p>Qualification call completed with Iris and her co-founder. Small but fast-moving "
|
||||
"team of 8 engineers. Main concerns are API depth and self-serve onboarding. "
|
||||
"Sending over the developer docs and scheduling a technical deep-dive.</p>"
|
||||
),
|
||||
"days_ago": 10,
|
||||
},
|
||||
{
|
||||
"deal": deal_names[4], # Jack / Meridian Systems
|
||||
"owner": owner_1,
|
||||
"content": (
|
||||
"<p>Deal closed — Jack signed the contract this morning. Final value $175k annual. "
|
||||
"Onboarding is scheduled for next Monday. Handoff notes sent to the customer "
|
||||
"success team. Great result for the quarter!</p>"
|
||||
),
|
||||
"days_ago": 2,
|
||||
},
|
||||
{
|
||||
"deal": deal_names[5], # Karen / Vertex Analytics
|
||||
"owner": owner_2,
|
||||
"content": (
|
||||
"<p>Karen's team went with a competitor — they had deeper BI integrations that we "
|
||||
"currently don't support. Not a pricing issue. Flagged to product team for roadmap "
|
||||
"consideration. Karen asked us to follow up in 6 months.</p>"
|
||||
),
|
||||
"days_ago": 8,
|
||||
},
|
||||
{
|
||||
"deal": deal_names[6], # Leo / Forge Digital
|
||||
"owner": session_user,
|
||||
"content": (
|
||||
"<p>Budget was cut for the rest of the year — Leo was very apologetic and said the "
|
||||
"product was exactly what they needed. Re-added to nurture sequence for Q4. "
|
||||
"Good candidate for a comeback deal.</p>"
|
||||
),
|
||||
"days_ago": 12,
|
||||
},
|
||||
]
|
||||
|
||||
return [
|
||||
insert_comment(
|
||||
"CRM Deal",
|
||||
data["deal"],
|
||||
data["owner"],
|
||||
data["content"],
|
||||
full_names,
|
||||
now - timedelta(days=data["days_ago"]),
|
||||
)
|
||||
for data in comments_data
|
||||
]
|
||||
|
||||
|
||||
def _create_deal_communications(deal_names, session_user, owner_1, full_names, now):
|
||||
# Use a placeholder email for recipients where we don't have a real one handy
|
||||
comms_data = [
|
||||
{
|
||||
"deal": deal_names[0],
|
||||
"owner": session_user,
|
||||
"sent_or_received": "Sent",
|
||||
"subject": "Sandbox access details — Acme Corp CRM Evaluation",
|
||||
"content": (
|
||||
"<p>Hi Alice,</p>"
|
||||
"<p>Thanks for joining yesterday's demo — really great session. I've created a "
|
||||
"dedicated sandbox environment pre-loaded with sample data. Login details and "
|
||||
"setup instructions are attached.</p>"
|
||||
"<p>Our onboarding specialist Sarah will also be available for a walkthrough "
|
||||
"session this week — just let us know a convenient time.</p>"
|
||||
"<p>Best regards</p>"
|
||||
),
|
||||
"sender": session_user,
|
||||
"recipients": "alice.johnson@example.com",
|
||||
"days_ago": 2,
|
||||
},
|
||||
{
|
||||
"deal": deal_names[1],
|
||||
"owner": owner_1,
|
||||
"sent_or_received": "Received",
|
||||
"subject": "Re: CRM Proposal — Pilot terms query",
|
||||
"content": (
|
||||
"<p>Hi,</p>"
|
||||
"<p>We've reviewed the proposal and the pricing looks good. Before we proceed, "
|
||||
"could you clarify what's included in the onboarding package? Specifically, we "
|
||||
"need help migrating ~4,000 contacts from our existing system.</p>"
|
||||
"<p>Also, is there flexibility on the contract start date?</p>"
|
||||
"<p>Thanks,<br>David Lee<br>CTO, TechStart Inc</p>"
|
||||
),
|
||||
"sender": "david.lee@example.com",
|
||||
"recipients": owner_1,
|
||||
"days_ago": 4,
|
||||
},
|
||||
]
|
||||
|
||||
return [
|
||||
insert_communication(
|
||||
"CRM Deal", data["deal"], data, full_names, now - timedelta(days=data["days_ago"])
|
||||
)
|
||||
for data in comms_data
|
||||
]
|
||||
|
||||
|
||||
def _create_deal_versions(deal_names, session_user, owner_1, owner_2, now):
|
||||
versions_data = [
|
||||
{
|
||||
"deal": deal_names[0], # Alice
|
||||
"owner": session_user,
|
||||
"changed": [["probability", "30", "50"]],
|
||||
"days_ago": 4,
|
||||
},
|
||||
{
|
||||
"deal": deal_names[1], # David
|
||||
"owner": owner_1,
|
||||
"changed": [["deal_value", "40000", "45000"]],
|
||||
"days_ago": 6,
|
||||
},
|
||||
{
|
||||
"deal": deal_names[2], # Henry
|
||||
"owner": owner_2,
|
||||
"changed": [["probability", "50", "60"]],
|
||||
"days_ago": 8,
|
||||
},
|
||||
{
|
||||
"deal": deal_names[3], # Iris
|
||||
"owner": session_user,
|
||||
"changed": [["probability", "25", "35"]],
|
||||
"days_ago": 11,
|
||||
},
|
||||
{
|
||||
"deal": deal_names[4], # Jack
|
||||
"owner": owner_1,
|
||||
"changed": [["status", "Ready to Close", "Won"]],
|
||||
"days_ago": 3,
|
||||
},
|
||||
{
|
||||
"deal": deal_names[5], # Karen
|
||||
"owner": owner_2,
|
||||
"changed": [["status", "Proposal/Quotation", "Lost"]],
|
||||
"days_ago": 9,
|
||||
},
|
||||
{
|
||||
"deal": deal_names[6], # Leo
|
||||
"owner": session_user,
|
||||
"changed": [["status", "Demo/Making", "Lost"]],
|
||||
"days_ago": 13,
|
||||
},
|
||||
]
|
||||
|
||||
for data in versions_data:
|
||||
insert_version(
|
||||
"CRM Deal", data["deal"], data["owner"], data["changed"], now - timedelta(days=data["days_ago"])
|
||||
)
|
||||
|
||||
|
||||
def delete_demo_deals(deal_data, lead_names):
|
||||
"""
|
||||
Delete deals, their linked contacts, organizations, and deal-specific communications.
|
||||
Comments and Versions are cascade-deleted when the deal is deleted.
|
||||
"""
|
||||
communication_names = deal_data.get("communications", [])
|
||||
deal_names = set(deal_data.get("deals", []))
|
||||
|
||||
# Find all deals converted from the provided demo leads
|
||||
if lead_names:
|
||||
demo_lead_deals = frappe.get_all("CRM Deal", filters={"lead": ["in", lead_names]}, pluck="name")
|
||||
deal_names.update(demo_lead_deals)
|
||||
|
||||
for name in communication_names:
|
||||
if frappe.db.exists("Communication", name):
|
||||
frappe.delete_doc("Communication", name, ignore_permissions=True, force=True)
|
||||
|
||||
for name in deal_names:
|
||||
if not frappe.db.exists("CRM Deal", name):
|
||||
continue
|
||||
# Collect linked contacts and organization before deleting deal
|
||||
contact_names = frappe.get_all(
|
||||
"CRM Contacts", filters={"parent": name, "parenttype": "CRM Deal"}, pluck="contact"
|
||||
)
|
||||
org = frappe.db.get_value("CRM Deal", name, "organization")
|
||||
frappe.delete_doc("CRM Deal", name, ignore_permissions=True, force=True)
|
||||
for contact in contact_names:
|
||||
if contact and frappe.db.exists("Contact", contact):
|
||||
for child_doctype in ("Contact Email", "Contact Phone", "Dynamic Link"):
|
||||
Child = DocType(child_doctype)
|
||||
frappe.qb.from_(Child).delete().where(Child.parent == contact).run()
|
||||
Contact = DocType("Contact")
|
||||
frappe.qb.from_(Contact).delete().where(Contact.name == contact).run()
|
||||
if org and frappe.db.exists("CRM Organization", org):
|
||||
frappe.delete_doc("CRM Organization", org, ignore_permissions=True, force=True)
|
||||
253
crm/demo/leads.py
Normal file
253
crm/demo/leads.py
Normal file
@@ -0,0 +1,253 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import frappe
|
||||
|
||||
from crm.demo.utils import backdate, fix_auto_records, resolve_owners
|
||||
|
||||
# Days ago each lead was created — index matches leads_data order
|
||||
_LEAD_DAYS = [58, 52, 47, 43, 38, 34, 28, 22, 17, 30, 13, 8]
|
||||
|
||||
# Last-activity date per lead (used for modified so active leads sort to top).
|
||||
# Converted leads will be overwritten by create_demo_deals anyway.
|
||||
# Index: [0]alice [1]bob [2]carol [3]david [4]eve [5]frank [6]grace [7]henry [8]iris [9]jack [10]karen [11]leo
|
||||
_LEAD_LAST_TOUCHED_DAYS = [50, 8, 11, 37, 38, 34, 28, 18, 11, 24, 9, 5]
|
||||
|
||||
|
||||
def create_demo_leads(demo_users):
|
||||
session_user, owner_1, owner_2, owner_3 = resolve_owners(demo_users)
|
||||
|
||||
leads_data = [
|
||||
# [0] Alice — will be converted to a deal
|
||||
{
|
||||
"first_name": "Alice",
|
||||
"last_name": "Johnson",
|
||||
"email": "alice.johnson@example.com",
|
||||
"mobile_no": "+1 555 000 1234",
|
||||
"organization": "Acme Corp",
|
||||
"website": "https://acmecorp.example.com",
|
||||
"industry": "Technology",
|
||||
"job_title": "VP of Engineering",
|
||||
"status": "Qualified",
|
||||
"source": "Cold Calling",
|
||||
"no_of_employees": "201-500",
|
||||
"annual_revenue": 5000000,
|
||||
"lead_owner": session_user,
|
||||
},
|
||||
# [1] Bob — stays as a lead
|
||||
{
|
||||
"first_name": "Bob",
|
||||
"last_name": "Martinez",
|
||||
"email": "bob.martinez@example.com",
|
||||
"mobile_no": "+1 555 000 5678",
|
||||
"organization": "Globex Industries",
|
||||
"website": "https://globexindustries.example.com",
|
||||
"industry": "Manufacturing",
|
||||
"job_title": "CEO",
|
||||
"status": "Contacted",
|
||||
"source": "Advertisement",
|
||||
"no_of_employees": "51-200",
|
||||
"annual_revenue": 8000000,
|
||||
"lead_owner": owner_1,
|
||||
},
|
||||
# [2] Carol — stays as a lead
|
||||
{
|
||||
"first_name": "Carol",
|
||||
"last_name": "Smith",
|
||||
"email": "carol.smith@example.com",
|
||||
"mobile_no": "+1 555 000 9012",
|
||||
"organization": "BrightPath Ltd",
|
||||
"website": "https://brightpath.example.com",
|
||||
"industry": "Consulting",
|
||||
"job_title": "Head of Operations",
|
||||
"status": "Nurture",
|
||||
"source": "Email",
|
||||
"no_of_employees": "11-50",
|
||||
"annual_revenue": 1500000,
|
||||
"lead_owner": owner_2,
|
||||
},
|
||||
# [3] David — will be converted to a deal
|
||||
{
|
||||
"first_name": "David",
|
||||
"last_name": "Lee",
|
||||
"email": "david.lee@example.com",
|
||||
"mobile_no": "+1 555 000 3456",
|
||||
"organization": "TechStart Inc",
|
||||
"website": "https://techstart.example.com",
|
||||
"industry": "Technology",
|
||||
"job_title": "CTO",
|
||||
"status": "Qualified",
|
||||
"source": "Reference",
|
||||
"no_of_employees": "1-10",
|
||||
"annual_revenue": 500000,
|
||||
"lead_owner": owner_1,
|
||||
},
|
||||
# [4] Emma — stays as a lead
|
||||
{
|
||||
"first_name": "Emma",
|
||||
"last_name": "Williams",
|
||||
"email": "emma.williams@example.com",
|
||||
"mobile_no": "+1 555 000 2345",
|
||||
"organization": "CloudBase LLC",
|
||||
"website": "https://cloudbase.example.com",
|
||||
"industry": "Technology",
|
||||
"job_title": "Procurement Manager",
|
||||
"status": "New",
|
||||
"source": "Website",
|
||||
"no_of_employees": "51-200",
|
||||
"annual_revenue": 3000000,
|
||||
"lead_owner": owner_1,
|
||||
},
|
||||
# [5] Frank — stays as a lead
|
||||
{
|
||||
"first_name": "Frank",
|
||||
"last_name": "Turner",
|
||||
"email": "frank.turner@example.com",
|
||||
"mobile_no": "+1 555 000 6789",
|
||||
"organization": "DataSync Co",
|
||||
"website": "https://datasync.example.com",
|
||||
"industry": "Technology",
|
||||
"job_title": "Head of Sales",
|
||||
"status": "Nurture",
|
||||
"source": "Email",
|
||||
"no_of_employees": "11-50",
|
||||
"annual_revenue": 2000000,
|
||||
"lead_owner": owner_2,
|
||||
},
|
||||
# [6] Grace — stays as a lead
|
||||
{
|
||||
"first_name": "Grace",
|
||||
"last_name": "Park",
|
||||
"email": "grace.park@example.com",
|
||||
"mobile_no": "+1 555 000 0123",
|
||||
"organization": "NextWave Corp",
|
||||
"website": "https://nextwavecorp.example.com",
|
||||
"industry": "Telecommunications",
|
||||
"job_title": "COO",
|
||||
"status": "Contacted",
|
||||
"source": "Reference",
|
||||
"no_of_employees": "201-500",
|
||||
"annual_revenue": 12000000,
|
||||
"lead_owner": session_user,
|
||||
},
|
||||
# [7] Henry — will be converted to a deal
|
||||
{
|
||||
"first_name": "Henry",
|
||||
"last_name": "Adams",
|
||||
"email": "henry.adams@example.com",
|
||||
"mobile_no": "+1 555 000 4567",
|
||||
"organization": "PivotTech Solutions",
|
||||
"website": "https://pivottech.example.com",
|
||||
"industry": "Technology",
|
||||
"job_title": "CFO",
|
||||
"status": "Qualified",
|
||||
"source": "Advertisement",
|
||||
"no_of_employees": "51-200",
|
||||
"annual_revenue": 6000000,
|
||||
"lead_owner": owner_3,
|
||||
},
|
||||
# [8] Iris — will be converted to a deal
|
||||
{
|
||||
"first_name": "Iris",
|
||||
"last_name": "Chen",
|
||||
"email": "iris.chen@example.com",
|
||||
"mobile_no": "+1 555 000 7890",
|
||||
"organization": "ScaleUp Labs",
|
||||
"website": "https://scaleuplabs.example.com",
|
||||
"industry": "Technology",
|
||||
"job_title": "VP Engineering",
|
||||
"status": "Qualified",
|
||||
"source": "Cold Calling",
|
||||
"no_of_employees": "1-10",
|
||||
"annual_revenue": 750000,
|
||||
"lead_owner": session_user,
|
||||
},
|
||||
# [9] Jack — will be converted to a deal
|
||||
{
|
||||
"first_name": "Jack",
|
||||
"last_name": "Morrison",
|
||||
"email": "jack.morrison@example.com",
|
||||
"mobile_no": "+1 555 000 8901",
|
||||
"organization": "Meridian Systems",
|
||||
"website": "https://meridiansystems.example.com",
|
||||
"industry": "Defense",
|
||||
"job_title": "CEO",
|
||||
"status": "Qualified",
|
||||
"source": "Reference",
|
||||
"no_of_employees": "501-1000",
|
||||
"annual_revenue": 25000000,
|
||||
"lead_owner": owner_1,
|
||||
},
|
||||
# [10] Karen — will be converted to a Lost deal
|
||||
{
|
||||
"first_name": "Karen",
|
||||
"last_name": "White",
|
||||
"email": "karen.white@example.com",
|
||||
"mobile_no": "+1 555 000 2222",
|
||||
"organization": "Vertex Analytics",
|
||||
"website": "https://vertexanalytics.example.com",
|
||||
"industry": "Financial Services",
|
||||
"job_title": "Director of Strategy",
|
||||
"status": "Qualified",
|
||||
"source": "Advertisement",
|
||||
"no_of_employees": "201-500",
|
||||
"annual_revenue": 9000000,
|
||||
"lead_owner": owner_2,
|
||||
},
|
||||
# [11] Leo — will be converted to a Lost deal
|
||||
{
|
||||
"first_name": "Leo",
|
||||
"last_name": "Brown",
|
||||
"email": "leo.brown@example.com",
|
||||
"mobile_no": "+1 555 000 3333",
|
||||
"organization": "Forge Digital",
|
||||
"website": "https://forgedigital.example.com",
|
||||
"industry": "Advertising",
|
||||
"job_title": "Head of Product",
|
||||
"status": "Qualified",
|
||||
"source": "Cold Calling",
|
||||
"no_of_employees": "51-200",
|
||||
"annual_revenue": 4000000,
|
||||
"lead_owner": session_user,
|
||||
},
|
||||
]
|
||||
|
||||
now = datetime.now()
|
||||
created = []
|
||||
for data, days in zip(leads_data, _LEAD_DAYS, strict=False):
|
||||
ts = now - timedelta(days=days)
|
||||
owner = data["lead_owner"]
|
||||
lead = frappe.get_doc({"doctype": "CRM Lead", **data}).insert(ignore_permissions=True)
|
||||
backdate("CRM Lead", lead.name, owner, ts)
|
||||
fix_auto_records("CRM Lead", lead.name, owner, ts)
|
||||
created.append(lead.name)
|
||||
|
||||
return created
|
||||
|
||||
|
||||
def rebackdate_demo_leads(lead_names, demo_users):
|
||||
"""Re-apply modified timestamps after activity creation resets them."""
|
||||
session_user, owner_1, owner_2, owner_3 = resolve_owners(demo_users)
|
||||
# Must match the lead_owner assignments in create_demo_leads (same index order)
|
||||
_owners = [
|
||||
session_user,
|
||||
owner_1,
|
||||
owner_2,
|
||||
owner_1,
|
||||
owner_1,
|
||||
owner_2,
|
||||
session_user,
|
||||
owner_3,
|
||||
session_user,
|
||||
owner_1,
|
||||
owner_2,
|
||||
session_user,
|
||||
]
|
||||
now = datetime.now()
|
||||
for name, days, owner in zip(lead_names, _LEAD_LAST_TOUCHED_DAYS, _owners, strict=False):
|
||||
backdate("CRM Lead", name, owner, now - timedelta(days=days), set_creation=False)
|
||||
|
||||
|
||||
def delete_demo_leads(lead_names):
|
||||
for name in lead_names:
|
||||
if frappe.db.exists("CRM Lead", name):
|
||||
frappe.delete_doc("CRM Lead", name, ignore_permissions=True, force=True)
|
||||
86
crm/demo/notes.py
Normal file
86
crm/demo/notes.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import frappe
|
||||
|
||||
from crm.demo.utils import backdate, resolve_owners
|
||||
|
||||
|
||||
def create_demo_notes(lead_names, demo_users):
|
||||
session_user, owner_1, owner_2, _ = resolve_owners(demo_users)
|
||||
now = datetime.now()
|
||||
|
||||
# Two notes per lead — a call summary and a follow-up observation
|
||||
notes_data = [
|
||||
{
|
||||
"title": "Initial Discovery Call",
|
||||
"content": "<p>Had a productive intro call. They're evaluating solutions for Q3. Key stakeholders include the CTO and VP of Sales. Budget is pre-approved. Follow up with a tailored proposal.</p>",
|
||||
"reference_doctype": "CRM Lead",
|
||||
"reference_docname": lead_names[0],
|
||||
"owner": session_user,
|
||||
"days_ago": 56,
|
||||
},
|
||||
{
|
||||
"title": "Pain Points Identified",
|
||||
"content": "<p>Current system lacks reporting and integrations. Team size is growing fast — 40% headcount increase this year. Decision timeline is 6–8 weeks.</p>",
|
||||
"reference_doctype": "CRM Lead",
|
||||
"reference_docname": lead_names[0],
|
||||
"owner": session_user,
|
||||
"days_ago": 51,
|
||||
},
|
||||
{
|
||||
"title": "Demo Feedback",
|
||||
"content": "<p>Very positive response to the pipeline view. Asked about custom fields and API access. Needs sign-off from CFO before moving forward.</p>",
|
||||
"reference_doctype": "CRM Lead",
|
||||
"reference_docname": lead_names[1],
|
||||
"owner": owner_1,
|
||||
"days_ago": 46,
|
||||
},
|
||||
{
|
||||
"title": "Competitor Mentioned",
|
||||
"content": "<p>Currently trialing a competitor. Our differentiators: better UX, open source, and lower TCO. Send a comparison doc before next call.</p>",
|
||||
"reference_doctype": "CRM Lead",
|
||||
"reference_docname": lead_names[1],
|
||||
"owner": owner_1,
|
||||
"days_ago": 39,
|
||||
},
|
||||
{
|
||||
"title": "Requirements Gathered",
|
||||
"content": "<p>Needs CRM + email integration + team collaboration. Currently using spreadsheets. Strong motivation to move to a structured system this quarter.</p>",
|
||||
"reference_doctype": "CRM Lead",
|
||||
"reference_docname": lead_names[2],
|
||||
"owner": owner_2,
|
||||
"days_ago": 44,
|
||||
},
|
||||
{
|
||||
"title": "Pricing Discussion",
|
||||
"content": "<p>Walked through pricing tiers. They prefer an annual plan. Requested a 2-week trial extension. Champion is very engaged — internal buy-in looks strong.</p>",
|
||||
"reference_doctype": "CRM Lead",
|
||||
"reference_docname": lead_names[3],
|
||||
"owner": owner_1,
|
||||
"days_ago": 40,
|
||||
},
|
||||
]
|
||||
|
||||
created = []
|
||||
for data in notes_data:
|
||||
ts = now - timedelta(days=data["days_ago"])
|
||||
owner = data["owner"]
|
||||
note = frappe.get_doc(
|
||||
{
|
||||
"doctype": "FCRM Note",
|
||||
"title": data["title"],
|
||||
"content": data["content"],
|
||||
"reference_doctype": data["reference_doctype"],
|
||||
"reference_docname": data["reference_docname"],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
backdate("FCRM Note", note.name, owner, ts)
|
||||
created.append(note.name)
|
||||
|
||||
return created
|
||||
|
||||
|
||||
def delete_demo_notes(note_names):
|
||||
for name in note_names:
|
||||
if frappe.db.exists("FCRM Note", name):
|
||||
frappe.delete_doc("FCRM Note", name, ignore_permissions=True, force=True)
|
||||
96
crm/demo/tasks.py
Normal file
96
crm/demo/tasks.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import frappe
|
||||
|
||||
from crm.demo.utils import backdate, resolve_owners
|
||||
|
||||
|
||||
def create_demo_tasks(lead_names, demo_users):
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
today = date.today()
|
||||
now = datetime.now()
|
||||
session_user, owner_1, owner_2, _ = resolve_owners(demo_users)
|
||||
|
||||
tasks_data = [
|
||||
{
|
||||
"title": "Send proposal document",
|
||||
"priority": "High",
|
||||
"status": "Todo",
|
||||
"assigned_to": session_user,
|
||||
"due_date": today + timedelta(days=2),
|
||||
"reference_doctype": "CRM Lead",
|
||||
"reference_docname": lead_names[0],
|
||||
"description": "Prepare and send a tailored proposal covering pricing, integrations, and onboarding timeline.",
|
||||
"days_ago": 1,
|
||||
},
|
||||
{
|
||||
"title": "Schedule technical demo",
|
||||
"priority": "Medium",
|
||||
"status": "In Progress",
|
||||
"assigned_to": owner_1,
|
||||
"due_date": today + timedelta(days=5),
|
||||
"reference_doctype": "CRM Lead",
|
||||
"reference_docname": lead_names[0],
|
||||
"description": "Coordinate with the CTO to set up a 1-hour technical deep-dive session.",
|
||||
"days_ago": 3,
|
||||
},
|
||||
{
|
||||
"title": "Follow up on trial access",
|
||||
"priority": "High",
|
||||
"status": "Todo",
|
||||
"assigned_to": owner_1,
|
||||
"due_date": today + timedelta(days=1),
|
||||
"reference_doctype": "CRM Lead",
|
||||
"reference_docname": lead_names[1],
|
||||
"description": "Check in on trial progress and address any blockers before the next call.",
|
||||
"days_ago": 2,
|
||||
},
|
||||
{
|
||||
"title": "Send competitor comparison doc",
|
||||
"priority": "Medium",
|
||||
"status": "Done",
|
||||
"assigned_to": owner_2,
|
||||
"due_date": today - timedelta(days=2),
|
||||
"reference_doctype": "CRM Lead",
|
||||
"reference_docname": lead_names[1],
|
||||
"description": "Share the feature comparison sheet highlighting our advantages over the competitor they're trialing.",
|
||||
"days_ago": 6,
|
||||
},
|
||||
{
|
||||
"title": "Set up trial environment",
|
||||
"priority": "High",
|
||||
"status": "In Progress",
|
||||
"assigned_to": owner_2,
|
||||
"due_date": today + timedelta(days=3),
|
||||
"reference_doctype": "CRM Lead",
|
||||
"reference_docname": lead_names[2],
|
||||
"description": "Provision a trial instance with sample data pre-loaded for their team to evaluate.",
|
||||
"days_ago": 3,
|
||||
},
|
||||
{
|
||||
"title": "Confirm annual plan details",
|
||||
"priority": "Low",
|
||||
"status": "Backlog",
|
||||
"assigned_to": session_user,
|
||||
"due_date": today + timedelta(days=10),
|
||||
"reference_doctype": "CRM Lead",
|
||||
"reference_docname": lead_names[3],
|
||||
"description": "Finalize seat count and send updated quote for annual subscription.",
|
||||
"days_ago": 1,
|
||||
},
|
||||
]
|
||||
|
||||
created = []
|
||||
for data in tasks_data:
|
||||
owner = data["assigned_to"]
|
||||
ts = now - timedelta(days=data.pop("days_ago"))
|
||||
task = frappe.get_doc({"doctype": "CRM Task", **data}).insert(ignore_permissions=True)
|
||||
backdate("CRM Task", task.name, owner, ts)
|
||||
created.append(task.name)
|
||||
|
||||
return created
|
||||
|
||||
|
||||
def delete_demo_tasks(task_names):
|
||||
for name in task_names:
|
||||
if frappe.db.exists("CRM Task", name):
|
||||
frappe.delete_doc("CRM Task", name, ignore_permissions=True, force=True)
|
||||
89
crm/demo/users.py
Normal file
89
crm/demo/users.py
Normal file
@@ -0,0 +1,89 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder import DocType
|
||||
|
||||
DEMO_USERS = [
|
||||
{
|
||||
"email": "sarah.demo@example.com",
|
||||
"first_name": "Sarah",
|
||||
"last_name": "Connor",
|
||||
"mobile_no": "+1 555 100 0002",
|
||||
"roles": ["Sales Manager", "Sales User"],
|
||||
"avatar": "/assets/crm/images/demo/sarah-connor.png",
|
||||
},
|
||||
{
|
||||
"email": "john.demo@example.com",
|
||||
"first_name": "John",
|
||||
"last_name": "Parker",
|
||||
"mobile_no": "+1 555 100 0003",
|
||||
"roles": ["Sales User"],
|
||||
"avatar": "/assets/crm/images/demo/john-parker.png",
|
||||
},
|
||||
{
|
||||
"email": "emily.demo@example.com",
|
||||
"first_name": "Emily",
|
||||
"last_name": "Chen",
|
||||
"mobile_no": "+1 555 100 0004",
|
||||
"roles": ["Sales User"],
|
||||
"avatar": "/assets/crm/images/demo/emily-chen.png",
|
||||
},
|
||||
]
|
||||
|
||||
DEMO_USER_EMAILS = [u["email"] for u in DEMO_USERS]
|
||||
|
||||
|
||||
def create_demo_users():
|
||||
for user_data in DEMO_USERS:
|
||||
if not frappe.db.exists("User", user_data["email"]):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "User",
|
||||
"email": user_data["email"],
|
||||
"first_name": user_data["first_name"],
|
||||
"last_name": user_data["last_name"],
|
||||
"send_welcome_email": 0,
|
||||
"user_type": "System User",
|
||||
"mobile_no": user_data["mobile_no"],
|
||||
"user_image": user_data["avatar"],
|
||||
"roles": [{"role": r} for r in user_data["roles"]],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
# Backdate auto-created contacts and set their image
|
||||
_ts = datetime.now() - timedelta(days=70)
|
||||
_avatar_by_email = {u["email"]: u["avatar"] for u in DEMO_USERS}
|
||||
contact_names = frappe.get_all(
|
||||
"Contact", filters={"user": ["in", DEMO_USER_EMAILS]}, fields=["name", "user"]
|
||||
)
|
||||
for row in contact_names:
|
||||
frappe.db.set_value(
|
||||
"Contact",
|
||||
row.name,
|
||||
{"image": _avatar_by_email.get(row.user, ""), "creation": _ts, "modified": _ts},
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
return DEMO_USER_EMAILS
|
||||
|
||||
|
||||
def delete_demo_users(demo_user_emails):
|
||||
# delete notifications first — they link to users and block deletion
|
||||
Notification = DocType("CRM Notification")
|
||||
frappe.qb.from_(Notification).delete().where(
|
||||
(Notification.from_user.isin(demo_user_emails)) | (Notification.to_user.isin(demo_user_emails))
|
||||
).run()
|
||||
|
||||
# collect contact names before deleting users — User.on_trash NULLs contact.user
|
||||
contact_names = frappe.get_all("Contact", filters={"user": ["in", demo_user_emails]}, pluck="name")
|
||||
|
||||
for email in demo_user_emails:
|
||||
if frappe.db.exists("User", email):
|
||||
frappe.delete_doc("User", email, ignore_permissions=True, force=True)
|
||||
|
||||
if contact_names:
|
||||
for child_doctype in ("Contact Email", "Contact Phone", "Dynamic Link"):
|
||||
Child = DocType(child_doctype)
|
||||
frappe.qb.from_(Child).delete().where(Child.parent.isin(contact_names)).run()
|
||||
Contact = DocType("Contact")
|
||||
frappe.qb.from_(Contact).delete().where(Contact.name.isin(contact_names)).run()
|
||||
117
crm/demo/utils.py
Normal file
117
crm/demo/utils.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder import DocType
|
||||
|
||||
|
||||
def resolve_owners(demo_users):
|
||||
"""Return (session_user, owner_1, owner_2, owner_3) each falling back to session_user."""
|
||||
s = frappe.session.user
|
||||
if s in ("Administrator", "Guest"):
|
||||
s = demo_users[0] if demo_users else s
|
||||
owners = [demo_users[i] if i < len(demo_users) else s for i in range(3)]
|
||||
return (s, *owners)
|
||||
|
||||
|
||||
def build_full_names(session_user):
|
||||
"""Return {email: full_name} mapping for all demo users plus session_user."""
|
||||
from crm.demo.users import DEMO_USERS
|
||||
|
||||
names = {u["email"]: f"{u['first_name']} {u['last_name']}" for u in DEMO_USERS}
|
||||
names[session_user] = frappe.utils.get_fullname(session_user)
|
||||
return names
|
||||
|
||||
|
||||
def backdate(doctype, name, owner, ts, *, set_creation=True):
|
||||
"""Set owner, modified_by, modified (and optionally creation) without triggering hooks."""
|
||||
fields = {"owner": owner, "modified_by": owner, "modified": ts}
|
||||
if set_creation:
|
||||
fields["creation"] = ts
|
||||
frappe.db.set_value(doctype, name, fields, update_modified=False)
|
||||
|
||||
|
||||
def fix_auto_records(doctype, docname, owner, ts):
|
||||
"""Backdate the Version and assignment Comment auto-created with owner='Administrator' on insert."""
|
||||
Version = DocType("Version")
|
||||
(
|
||||
frappe.qb.update(Version)
|
||||
.set(Version.owner, owner)
|
||||
.set(Version.modified_by, owner)
|
||||
.set(Version.creation, ts)
|
||||
.set(Version.modified, ts)
|
||||
.where(
|
||||
(Version.ref_doctype == doctype)
|
||||
& (Version.docname == docname)
|
||||
& (Version.owner == "Administrator")
|
||||
)
|
||||
.run()
|
||||
)
|
||||
Comment = DocType("Comment")
|
||||
(
|
||||
frappe.qb.update(Comment)
|
||||
.set(Comment.owner, owner)
|
||||
.set(Comment.modified_by, owner)
|
||||
.set(Comment.comment_email, owner)
|
||||
.set(Comment.creation, ts)
|
||||
.set(Comment.modified, ts)
|
||||
.where(
|
||||
(Comment.reference_doctype == doctype)
|
||||
& (Comment.reference_name == docname)
|
||||
& (Comment.owner == "Administrator")
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
|
||||
def insert_comment(reference_doctype, reference_name, owner, content, full_names, ts):
|
||||
"""Insert a Comment, backdate it, and return its name."""
|
||||
comment = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Comment",
|
||||
"comment_type": "Comment",
|
||||
"reference_doctype": reference_doctype,
|
||||
"reference_name": reference_name,
|
||||
"content": content,
|
||||
"comment_email": owner,
|
||||
"comment_by": full_names.get(owner, owner),
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
backdate("Comment", comment.name, owner, ts)
|
||||
return comment.name
|
||||
|
||||
|
||||
def insert_communication(reference_doctype, reference_name, data, full_names, ts):
|
||||
"""Insert an email Communication, backdate it, and return its name."""
|
||||
comm = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"communication_medium": "Email",
|
||||
"status": "Linked",
|
||||
"sent_or_received": data["sent_or_received"],
|
||||
"reference_doctype": reference_doctype,
|
||||
"reference_name": reference_name,
|
||||
"subject": data["subject"],
|
||||
"content": data["content"],
|
||||
"sender": data["sender"],
|
||||
"sender_full_name": full_names.get(data["sender"], data["sender"]),
|
||||
"recipients": data["recipients"],
|
||||
"communication_date": ts,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
backdate("Communication", comm.name, data["owner"], ts)
|
||||
return comm.name
|
||||
|
||||
|
||||
def insert_version(ref_doctype, docname, owner, changed, ts):
|
||||
"""Insert a Version record, backdate it, and return its name."""
|
||||
version = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Version",
|
||||
"ref_doctype": ref_doctype,
|
||||
"docname": docname,
|
||||
"data": json.dumps({"changed": changed}),
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
backdate("Version", version.name, owner, ts)
|
||||
return version.name
|
||||
Reference in New Issue
Block a user