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

View File

View File

@@ -0,0 +1,5 @@
from crm.install import add_default_lead_sources
def execute():
add_default_lead_sources()

View File

@@ -0,0 +1,78 @@
"""Expose the Domain Enrichment fields on existing sites.
The enrichment fields (company_description, linkedin, twitter, facebook) were added
to the default Lead/Deal/Organization layouts in ``crm/install.py``, but that seeder
is skip-if-exists -- so sites installed before this feature never get them and users
can't see what enrichment writes. This patch injects any missing enrichment field
into the relevant Side Panel / Data Fields / Quick Entry layouts. Idempotent: a
field already present anywhere in the layout (or absent from the doctype) is left
untouched.
"""
import json
import frappe
ENRICHMENT_FIELDS = ["company_description", "linkedin", "twitter", "facebook"]
TARGET_LAYOUTS = [
"CRM Lead-Side Panel",
"CRM Deal-Side Panel",
"CRM Organization-Side Panel",
"CRM Lead-Data Fields",
"CRM Deal-Data Fields",
# Quick Entry = the create-modal layout; enrichment fields must be visible there.
"CRM Lead-Quick Entry",
"CRM Deal-Quick Entry",
"CRM Organization-Quick Entry",
]
def _iter_columns(layout):
"""Yield every column dict in a layout tree (handles tabbed layouts that nest
sections under a top-level section's ``sections`` key)."""
for section in layout:
yield from section.get("columns", [])
for nested in section.get("sections", []):
yield from nested.get("columns", [])
def _visible_columns(layout):
"""Columns of the first non-hidden section (handles tabbed layouts). Skipping
hidden sections matters for Quick Entry, whose first section (organization) is
hidden -- fields must land in a section the user can actually see."""
for section in layout:
if section.get("hidden"):
continue
if section.get("columns"):
return section["columns"]
for nested in section.get("sections", []):
if not nested.get("hidden") and nested.get("columns"):
return nested["columns"]
return None
def execute():
for name in TARGET_LAYOUTS:
if not frappe.db.exists("CRM Fields Layout", name):
continue
doc = frappe.get_doc("CRM Fields Layout", name)
try:
layout = json.loads(doc.layout or "[]")
except (ValueError, TypeError):
continue
present = {f for col in _iter_columns(layout) for f in col.get("fields", [])}
meta = frappe.get_meta(doc.dt)
missing = [f for f in ENRICHMENT_FIELDS if f not in present and meta.has_field(f)]
if not missing:
continue
target = _visible_columns(layout)
if not target:
continue
target[0].setdefault("fields", []).extend(missing)
doc.layout = json.dumps(layout)
doc.save()

View File

@@ -0,0 +1,5 @@
import frappe
def execute():
frappe.get_doc({"doctype": "CRM Lead Source", "source_name": "Facebook"}).insert(ignore_if_duplicate=True)

View File

@@ -0,0 +1,9 @@
from crm.install import (
add_assignment_rule_property_setters,
create_assignment_rule_custom_fields,
)
def execute():
create_assignment_rule_custom_fields()
add_assignment_rule_property_setters()

View File

@@ -0,0 +1,9 @@
import frappe
def execute():
if not frappe.db.get_single_value("FCRM Settings", "enable_forecasting"):
return
settings = frappe.get_doc("FCRM Settings")
settings.add_forecasting_section("Quick Entry")

View File

@@ -0,0 +1,18 @@
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
def execute():
enabled = frappe.db.get_single_value("ERPNext CRM Settings", "enabled")
if enabled:
custom_fields = {
"CRM Deal": [
{
"fieldname": "erpnext_customer",
"fieldtype": "Data",
"label": "Customer in ERPNext",
"insert_after": "lead_name",
}
]
}
create_custom_fields(custom_fields, ignore_validate=True)

View File

@@ -0,0 +1,35 @@
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
def execute():
if not frappe.db.get_single_value("ERPNext CRM Settings", "enabled"):
return
custom_fields = {
"CRM Product": [
{
"fieldname": "erpnext_item_code",
"fieldtype": "Data",
"label": "Item Code in ERPNext",
"read_only": 1,
"insert_after": "product_code",
}
]
}
if frappe.db.exists("DocType", "Item"):
custom_fields["Item"] = [
{
"fieldname": "crm_product_code",
"fieldtype": "Data",
"label": "CRM Product",
"read_only": 1,
"no_copy": 1,
"insert_after": "item_code",
}
]
create_custom_fields(custom_fields, ignore_validate=True)
# Latest ERPNext removed the create_custom_fields_for_frappe_crm API and creates its own
# fields automatically so we no longer call into ERPNext here.
frappe.get_single("ERPNext CRM Settings").create_custom_fields_in_frappe_crm()

View File

@@ -0,0 +1,5 @@
from crm.install import add_default_fields_layout
def execute():
add_default_fields_layout()

View File

@@ -0,0 +1,5 @@
from crm.install import add_default_lost_reasons
def execute():
add_default_lost_reasons()

View File

@@ -0,0 +1,5 @@
from crm.install import add_default_quick_filters
def execute():
add_default_quick_filters()

View File

@@ -0,0 +1,5 @@
from crm.install import add_default_scripts
def execute():
add_default_scripts()

View File

@@ -0,0 +1,68 @@
import json
import frappe
def execute():
if not frappe.db.exists("CRM Fields Layout", {"dt": "CRM Lead", "type": "Side Panel"}):
create_doctype_fields_layout("CRM Lead")
if not frappe.db.exists("CRM Fields Layout", {"dt": "CRM Deal", "type": "Side Panel"}):
create_doctype_fields_layout("CRM Deal")
def create_doctype_fields_layout(doctype):
not_allowed_fieldtypes = [
"Section Break",
"Column Break",
]
fields = frappe.get_meta(doctype).fields
fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes]
sections = {}
section_fields = []
last_section = None
for field in fields:
if field.fieldtype == "Tab Break" and last_section:
sections[last_section]["fields"] = section_fields
last_section = None
if field.read_only:
section_fields = []
continue
if field.fieldtype == "Tab Break":
if field.read_only:
section_fields = []
continue
section_fields = []
last_section = field.fieldname
sections[field.fieldname] = {
"label": field.label,
"name": field.fieldname,
"opened": True,
"fields": [],
}
if field.fieldname == "contacts_tab":
sections[field.fieldname]["editable"] = False
sections[field.fieldname]["contacts"] = []
else:
section_fields.append(field.fieldname)
section_fields = []
for section in sections:
if section == "contacts_tab":
sections[section]["name"] = "contacts_section"
sections[section].pop("fields", None)
section_fields.append(sections[section])
frappe.get_doc(
{
"doctype": "CRM Fields Layout",
"dt": doctype,
"type": "Side Panel",
"layout": json.dumps(section_fields),
}
).insert(ignore_permissions=True)
return section_fields

View File

@@ -0,0 +1,5 @@
from crm.install import add_email_account_custom_field
def execute():
add_email_account_custom_field()

View File

@@ -0,0 +1,5 @@
from crm.install import add_email_template_custom_fields
def execute():
add_email_template_custom_fields()

View File

@@ -0,0 +1,32 @@
import frappe
from frappe.model.rename_doc import rename_doc
def execute():
if not frappe.db.exists("DocType", "FCRM Note"):
frappe.flags.ignore_route_conflict_validation = True
rename_doc("DocType", "CRM Note", "FCRM Note")
frappe.flags.ignore_route_conflict_validation = False
frappe.reload_doctype("FCRM Note", force=True)
if frappe.db.exists("DocType", "FCRM Note") and frappe.db.count("FCRM Note") > 0:
return
notes = frappe.db.sql("SELECT * FROM `tabCRM Note`", as_dict=True)
if notes:
for note in notes:
doc = frappe.get_doc(
{
"doctype": "FCRM Note",
"creation": note.get("creation"),
"modified": note.get("modified"),
"modified_by": note.get("modified_by"),
"owner": note.get("owner"),
"title": note.get("title"),
"content": note.get("content"),
"reference_doctype": note.get("reference_doctype"),
"reference_docname": note.get("reference_docname"),
}
)
doc.db_insert()

View File

@@ -0,0 +1,27 @@
import frappe
def execute():
if not frappe.db.exists("DocType", "CRM Telephony Agent"):
frappe.reload_doctype("CRM Telephony Agent", force=True)
if frappe.db.exists("DocType", "Twilio Agents") and frappe.db.count("Twilio Agents") == 0:
return
agents = frappe.db.sql("SELECT * FROM `tabTwilio Agents`", as_dict=True)
if agents:
for agent in agents:
doc = frappe.get_doc(
{
"doctype": "CRM Telephony Agent",
"creation": agent.get("creation"),
"modified": agent.get("modified"),
"modified_by": agent.get("modified_by"),
"owner": agent.get("owner"),
"user": agent.get("user"),
"twilio_number": agent.get("twilio_number"),
"user_name": agent.get("user_name"),
"twilio": True,
}
)
doc.db_insert()

View File

@@ -0,0 +1,20 @@
import frappe
from frappe.model.rename_doc import rename_doc
def execute():
if frappe.db.exists("DocType", "Twilio Settings"):
frappe.flags.ignore_route_conflict_validation = True
rename_doc("DocType", "Twilio Settings", "CRM Twilio Settings")
frappe.flags.ignore_route_conflict_validation = False
frappe.reload_doctype("CRM Twilio Settings", force=True)
if frappe.db.exists("__Auth", {"doctype": "Twilio Settings"}):
Auth = frappe.qb.DocType("__Auth")
result = frappe.qb.from_(Auth).select("*").where(Auth.doctype == "Twilio Settings").run(as_dict=True)
for row in result:
frappe.qb.into(Auth).insert(
"CRM Twilio Settings", "CRM Twilio Settings", row.fieldname, row.password, row.encrypted
).run()

View File

@@ -0,0 +1,59 @@
import json
import frappe
OLD_FIELDS = [
"address_title",
"address_type",
"address_line1",
"address_line2",
"city",
"state",
"country",
"pincode",
]
NEW_LAYOUT = [
{
"name": "details_section",
"columns": [
{
"name": "column_uSSG",
"fields": ["address_title", "address_type", "address_line1", "address_line2"],
}
],
},
{
"name": "location_section",
"hideBorder": True,
"columns": [
{"name": "column_TCoZ", "fields": ["country", "city"]},
{"name": "column_PqrK", "fields": ["state", "pincode"]},
],
},
]
def execute():
if not frappe.db.exists("CRM Fields Layout", "Address-Quick Entry"):
return
layout = frappe.db.get_value("CRM Fields Layout", "Address-Quick Entry", "layout")
if not layout:
return
parsed = json.loads(layout)
# Only replace the layout if it's still the untouched default (single
# section/column with the original field order) — a site that has already
# customized this Quick Entry should keep its own arrangement.
is_default = (
isinstance(parsed, list)
and len(parsed) == 1
and len(parsed[0].get("columns", [])) == 1
and parsed[0]["columns"][0].get("fields") == OLD_FIELDS
)
if not is_default:
return
frappe.db.set_value("CRM Fields Layout", "Address-Quick Entry", "layout", json.dumps(NEW_LAYOUT))

View File

@@ -0,0 +1,19 @@
import frappe
from crm.fcrm.doctype.erpnext_crm_settings.erpnext_crm_settings import get_crm_form_script
def execute():
reset_erpnext_form_script()
def reset_erpnext_form_script():
try:
if frappe.db.exists("CRM Form Script", "Create Quotation from CRM Deal"):
script = get_crm_form_script()
frappe.db.set_value("CRM Form Script", "Create Quotation from CRM Deal", "script", script)
return True
return False
except Exception:
frappe.log_error(frappe.get_traceback(), "Error while resetting form script")
return False

View File

@@ -0,0 +1,8 @@
import frappe
def execute():
# Existing sites already have data by upgrade time; skip onboarding for them.
# Fresh installs are still empty here (demo data is created post setup wizard).
if frappe.db.count("CRM Lead") or frappe.db.count("CRM Deal"):
frappe.db.set_single_value("FCRM Settings", "persona_captured", 1)

View File

@@ -0,0 +1,46 @@
import json
import frappe
from frappe.query_builder import DocType
from crm.api.session import CRM_ALLOWED_ROLES
DOCTYPES = ("CRM Deal", "CRM Lead")
def execute():
"""Backfill `_seen` for CRM Deals/Leads that predate track_seen being enabled.
Without this, every record created before track_seen was turned on would
show up as unvisited (bold) to everyone, since there's no real view
history to reconstruct. Default them to already seen by every enabled
CRM user instead, so only records created or updated from now on show
as unvisited.
"""
users = get_crm_users()
if not users:
return
seen = json.dumps(users)
for doctype in DOCTYPES:
table = DocType(doctype)
frappe.qb.update(table).set(table._seen, seen).where(
table._seen.isnull() | table._seen.isin(["", "[]"])
).run()
def get_crm_users():
users_with_crm_role = frappe.get_all(
"Has Role",
filters={"parenttype": "User", "role": ["in", CRM_ALLOWED_ROLES]},
pluck="parent",
distinct=True,
)
users_with_crm_role.append("Administrator")
return frappe.get_all(
"User",
filters={"name": ["in", users_with_crm_role], "enabled": 1},
pluck="name",
)

View File

@@ -0,0 +1,22 @@
import json
import frappe
def execute():
if not frappe.db.exists("CRM Fields Layout", "CRM Deal-Quick Entry"):
return
deal = frappe.db.get_value("CRM Fields Layout", "CRM Deal-Quick Entry", "layout")
layout = json.loads(deal)
for section in layout:
if section.get("label") in [
"Select Organization",
"Organization Details",
"Select Contact",
"Contact Details",
]:
section["editable"] = False
frappe.db.set_value("CRM Fields Layout", "CRM Deal-Quick Entry", "layout", json.dumps(layout))

View File

@@ -0,0 +1,24 @@
import frappe
def execute():
deal_statuses = frappe.get_all("CRM Deal Status", fields=["name", "probability", "deal_status"])
for status in deal_statuses:
if status.probability is None or status.probability == 0:
if status.deal_status == "Qualification":
probability = 10
elif status.deal_status == "Demo/Making":
probability = 25
elif status.deal_status == "Proposal/Quotation":
probability = 50
elif status.deal_status == "Negotiation":
probability = 70
elif status.deal_status == "Ready to Close":
probability = 90
elif status.deal_status == "Won":
probability = 100
else:
probability = 0
frappe.db.set_value("CRM Deal Status", status.name, "probability", probability)

View File

@@ -0,0 +1,44 @@
import frappe
def execute():
deal_statuses = frappe.get_all("CRM Deal Status", fields=["name", "type", "deal_status"])
openStatuses = ["New", "Open", "Unassigned", "Qualification"]
ongoingStatuses = [
"Demo/Making",
"Proposal/Quotation",
"Negotiation",
"Ready to Close",
"Demo Scheduled",
"Follow Up",
]
onHoldStatuses = ["On Hold", "Paused", "Stalled", "Awaiting Reply"]
wonStatuses = ["Won", "Closed Won", "Successful", "Completed"]
lostStatuses = [
"Lost",
"Closed",
"Closed Lost",
"Junk",
"Unqualified",
"Disqualified",
"Cancelled",
"No Response",
]
for status in deal_statuses:
if not status.type or status.type is None or status.type == "Open":
if status.deal_status in openStatuses:
type = "Open"
elif status.deal_status in ongoingStatuses:
type = "Ongoing"
elif status.deal_status in onHoldStatuses:
type = "On Hold"
elif status.deal_status in wonStatuses:
type = "Won"
elif status.deal_status in lostStatuses:
type = "Lost"
else:
type = "Ongoing"
frappe.db.set_value("CRM Deal Status", status.name, "type", type)

View File

@@ -0,0 +1,101 @@
import json
from math import ceil
import frappe
from frappe.utils import random_string
def execute():
layouts = frappe.get_all("CRM Fields Layout", fields=["name", "layout", "type"])
for layout in layouts:
old_layout = layout.layout
new_layout = get_new_layout(old_layout, layout.type)
frappe.db.set_value("CRM Fields Layout", layout.name, "layout", new_layout)
def get_new_layout(old_layout, type):
if isinstance(old_layout, str):
old_layout = json.loads(old_layout)
new_layout = []
already_converted = False
starts_with_sections = False
if not old_layout[0].get("sections"):
starts_with_sections = True
if starts_with_sections:
old_layout = [{"sections": old_layout}]
for tab in old_layout:
new_tab = tab.copy()
if "no_tabs" in new_tab:
new_tab.pop("no_tabs")
new_tab["sections"] = []
new_tab["name"] = "tab_" + str(random_string(4))
for section in tab.get("sections"):
section["name"] = section.get("name") or "section_" + str(random_string(4))
if section.get("label") == "Select Organization":
section["name"] = "organization_section"
section["hidden"] = 1
elif section.get("label") == "Organization Details":
section["name"] = "organization_details_section"
elif section.get("label") == "Select Contact":
section["name"] = "contact_section"
section["hidden"] = 1
elif section.get("label") == "Contact Details":
section["name"] = "contact_details_section"
if "contacts" in section:
new_tab["sections"].append(section)
continue
if isinstance(section.get("columns"), list):
already_converted = True
break
column_count = section.get("columns") or 3
if type == "Side Panel":
column_count = 1
fields = section.get("fields") or []
new_section = section.copy()
if "fields" in new_section:
new_section.pop("fields")
new_section["columns"] = []
if len(fields) == 0:
new_section["columns"].append({"name": "column_" + str(random_string(4)), "fields": []})
new_tab["sections"].append(new_section)
continue
if len(fields) == 1 and column_count > 1:
new_section["columns"].append(
{"name": "column_" + str(random_string(4)), "fields": [fields[0]]}
)
new_section["columns"].append({"name": "column_" + str(random_string(4)), "fields": []})
new_tab["sections"].append(new_section)
continue
fields_per_column = ceil(len(fields) / column_count)
for i in range(column_count):
new_column = {
"name": "column_" + str(random_string(4)),
"fields": fields[i * fields_per_column : (i + 1) * fields_per_column],
}
new_section["columns"].append(new_column)
new_tab["sections"].append(new_section)
new_layout.append(new_tab)
if starts_with_sections:
new_layout = new_layout[0].get("sections")
if already_converted:
new_layout = old_layout
if type == "Side Panel" and "sections" in new_layout[0]:
new_layout = new_layout[0].get("sections")
return json.dumps(new_layout)

View File

@@ -0,0 +1,59 @@
import frappe
def execute():
if not frappe.db.exists("CRM Lead Status", "Converted"):
status_doc = frappe.get_doc(
{
"doctype": "CRM Lead Status",
"lead_status": "Converted",
"color": "teal",
"type": "Won",
"position": 5,
}
)
status_doc.insert()
lead_statuses = frappe.get_all("CRM Lead Status", fields=["name", "type", "lead_status"])
openStatuses = ["New", "Open", "Unassigned"]
ongoingStatuses = [
"Contacted",
"Nurture",
"Interested",
"Demo/Making",
"Proposal/Quotation",
"Negotiation",
"Ready to Close",
"Demo Scheduled",
"Follow Up",
]
onHoldStatuses = ["On Hold", "Paused", "Stalled", "Awaiting Reply"]
wonStatuses = ["Won", "Closed Won", "Successful", "Completed", "Qualified", "Converted"]
lostStatuses = [
"Lost",
"Closed",
"Closed Lost",
"Junk",
"Unqualified",
"Disqualified",
"Cancelled",
"No Response",
]
for status in lead_statuses:
if not status.type or status.type is None or status.type == "Open":
if status.lead_status in openStatuses:
type = "Open"
elif status.lead_status in ongoingStatuses:
type = "Ongoing"
elif status.lead_status in onHoldStatuses:
type = "On Hold"
elif status.lead_status in wonStatuses:
type = "Won"
elif status.lead_status in lostStatuses:
type = "Lost"
else:
type = "Ongoing"
frappe.db.set_value("CRM Lead Status", status.name, "type", type)