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

This commit is contained in:
upstream-mirror
2026-09-18 01:36:01 +05:00
commit db69de1c11
905 changed files with 102713 additions and 0 deletions

0
crm/demo/__init__.py Normal file
View File

191
crm/demo/activities.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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