Files
upstream-frappe-crm/crm/api/doc.py
upstream-mirror db69de1c11
Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
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
snapshot of https://github.com/frappe/crm.git (develop, 2026-09-18; blobs > 131072B omitted)
2026-09-18 01:36:01 +05:00

858 lines
23 KiB
Python

import json
import frappe
from frappe import _
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.desk.form.assign_to import set_status
from frappe.model import no_value_fields
from frappe.model.delete_doc import get_dynamic_linked_docs, get_linked_docs
from frappe.model.document import get_controller
from frappe.utils import make_filter_tuple
from pypika import Criterion
from crm.api.views import get_views
from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script
from crm.utils import is_frappe_version
COUNT_NAME = (
{"COUNT": "name", "as": "total_count"}
if is_frappe_version("16", above=True)
else "count(name) as total_count"
)
@frappe.whitelist()
def sort_options(doctype: str):
fields = frappe.get_meta(doctype).fields
fields = [field for field in fields if field.fieldtype not in no_value_fields]
fields = [
{
"label": _(field.label),
"value": field.fieldname,
"fieldname": field.fieldname,
}
for field in fields
if field.label and field.fieldname
]
standard_fields = [
{"label": "Name", "fieldname": "name"},
{"label": "Created On", "fieldname": "creation"},
{"label": "Last Modified", "fieldname": "modified"},
{"label": "Modified By", "fieldname": "modified_by"},
{"label": "Owner", "fieldname": "owner"},
]
for field in standard_fields:
field["label"] = _(field["label"])
field["value"] = field["fieldname"]
fields.append(field)
return fields
@frappe.whitelist()
def get_filterable_fields(doctype: str):
allowed_fieldtypes = [
"Check",
"Data",
"Float",
"Int",
"Currency",
"Dynamic Link",
"Link",
"Long Text",
"Select",
"Small Text",
"Text Editor",
"Text",
"Duration",
"Rating",
"Date",
"Datetime",
]
c = get_controller(doctype)
restricted_fields = []
if hasattr(c, "get_non_filterable_fields"):
restricted_fields = c.get_non_filterable_fields()
fields = []
meta = frappe.get_meta(doctype).as_dict()
# append standard fields (getting error when using frappe.model.std_fields)
standard_fields = [
{"fieldname": "name", "fieldtype": "Link", "label": "Name", "options": doctype},
{"fieldname": "owner", "fieldtype": "Link", "label": "Created By", "options": "User"},
{
"fieldname": "modified_by",
"fieldtype": "Link",
"label": "Last Updated By",
"options": "User",
},
{"fieldname": "_user_tags", "fieldtype": "Data", "label": "Tags"},
{"fieldname": "_liked_by", "fieldtype": "Data", "label": "Like"},
{"fieldname": "_comments", "fieldtype": "Text", "label": "Comments"},
{"fieldname": "_assign", "fieldtype": "Text", "label": "Assigned To"},
{"fieldname": "creation", "fieldtype": "Datetime", "label": "Created On"},
{"fieldname": "modified", "fieldtype": "Datetime", "label": "Last Updated On"},
]
for field in standard_fields + meta.get("fields", []):
if field.get("fieldname") not in restricted_fields and field.get("fieldtype") in allowed_fieldtypes:
field["name"] = field.get("fieldname")
field["label"] = _(field.get("label"))
field["value"] = field.get("fieldname")
fields.append(field)
return fields
@frappe.whitelist()
def get_group_by_fields(doctype: str):
allowed_fieldtypes = [
"Check",
"Data",
"Float",
"Int",
"Currency",
"Dynamic Link",
"Link",
"Select",
"Duration",
"Date",
"Datetime",
]
fields = frappe.get_meta(doctype).fields
fields = [
field
for field in fields
if field.fieldtype not in no_value_fields and field.fieldtype in allowed_fieldtypes
]
fields = [
{
"label": _(field.label),
"fieldname": field.fieldname,
}
for field in fields
if field.label and field.fieldname
]
standard_fields = [
{"label": "Name", "fieldname": "name"},
{"label": "Created On", "fieldname": "creation"},
{"label": "Last Modified", "fieldname": "modified"},
{"label": "Modified By", "fieldname": "modified_by"},
{"label": "Owner", "fieldname": "owner"},
{"label": "Like", "fieldname": "_liked_by"},
{"label": "Assigned To", "fieldname": "_assign"},
{"label": "Comments", "fieldname": "_comments"},
{"label": "Created On", "fieldname": "creation"},
{"label": "Modified On", "fieldname": "modified"},
]
for field in standard_fields:
field["label"] = _(field["label"])
fields.append(field)
return fields
@frappe.whitelist()
def get_quick_filters(doctype: str, cached: bool = True):
meta = frappe.get_meta(doctype, cached)
quick_filters = []
if global_settings := frappe.db.exists("CRM Global Settings", {"dt": doctype, "type": "Quick Filters"}):
_quick_filters = frappe.db.get_value("CRM Global Settings", global_settings, "json")
_quick_filters = json.loads(_quick_filters) or []
fields = []
for filter in _quick_filters:
if filter == "name":
fields.append({"label": "Name", "fieldname": "name", "fieldtype": "Data"})
else:
field = next((f for f in meta.fields if f.fieldname == filter), None)
if field:
fields.append(field)
else:
fields = [field for field in meta.fields if field.in_standard_filter]
for field in fields:
options = field.get("options")
if field.get("fieldtype") == "Select" and options and isinstance(options, str):
options = options.split("\n")
options = [{"label": option, "value": option} for option in options]
if not any([not option.get("value") for option in options]):
options.insert(0, {"label": "", "value": ""})
quick_filters.append(
{
"label": _(field.get("label")),
"fieldname": field.get("fieldname"),
"fieldtype": field.get("fieldtype"),
"options": options,
}
)
if doctype == "CRM Lead":
quick_filters = [filter for filter in quick_filters if filter.get("fieldname") != "converted"]
return quick_filters
@frappe.whitelist()
def update_quick_filters(quick_filters: str, old_filters: str, doctype: str):
quick_filters = json.loads(quick_filters)
old_filters = json.loads(old_filters)
new_filters = [filter for filter in quick_filters if filter not in old_filters]
removed_filters = [filter for filter in old_filters if filter not in quick_filters]
# update or create global quick filter settings
create_update_global_settings(doctype, quick_filters)
# remove old filters
for filter in removed_filters:
update_in_standard_filter(filter, doctype, 0)
# add new filters
for filter in new_filters:
update_in_standard_filter(filter, doctype, 1)
def create_update_global_settings(doctype, quick_filters):
if global_settings := frappe.db.exists("CRM Global Settings", {"dt": doctype, "type": "Quick Filters"}):
frappe.db.set_value("CRM Global Settings", global_settings, "json", json.dumps(quick_filters))
else:
# create CRM Global Settings doc
doc = frappe.new_doc("CRM Global Settings")
doc.dt = doctype
doc.type = "Quick Filters"
doc.json = json.dumps(quick_filters)
doc.insert()
def update_in_standard_filter(fieldname, doctype, value):
if property_name := frappe.db.exists(
"Property Setter",
{"doc_type": doctype, "field_name": fieldname, "property": "in_standard_filter"},
):
frappe.db.set_value("Property Setter", property_name, "value", value)
else:
make_property_setter(
doctype,
fieldname,
"in_standard_filter",
value,
"Check",
validate_fields_for_doctype=False,
)
@frappe.whitelist()
def get_data(
doctype: str,
filters: dict,
order_by: str,
page_length: int = 20,
page_length_count: int = 20,
column_field: str | None = None,
title_field: str | None = None,
columns: str | list | None = None,
rows: str | list | None = None,
kanban_columns: str | list | None = None,
kanban_fields: str | list | None = None,
view: str | dict | None = None,
default_filters: dict | None = None,
):
custom_view = False
filters = frappe._dict(filters)
rows = frappe.parse_json(rows or "[]")
columns = frappe.parse_json(columns or "[]")
kanban_fields = frappe.parse_json(kanban_fields or "[]")
kanban_columns = frappe.parse_json(kanban_columns or "[]")
custom_view_name = view.get("custom_view_name") if view else None
view_type = view.get("view_type") if view else None
group_by_field = view.get("group_by_field") if view else None
for key in filters:
value = filters[key]
if isinstance(value, list):
if "@me" in value:
value[value.index("@me")] = frappe.session.user
elif "%@me%" in value:
index = [i for i, v in enumerate(value) if v == "%@me%"]
for i in index:
value[i] = "%" + frappe.session.user + "%"
elif value == "@me":
filters[key] = frappe.session.user
if default_filters:
default_filters = frappe.parse_json(default_filters)
filters.update(default_filters)
is_default = True
data = []
_list = get_controller(doctype)
default_rows = []
default_column_keys = []
if hasattr(_list, "default_list_data"):
default_list_data = _list.default_list_data()
default_rows = default_list_data.get("rows")
default_column_keys = [column.get("key") for column in default_list_data.get("columns", [])]
meta = frappe.get_meta(doctype)
if view_type != "kanban":
if columns or rows:
custom_view = True
is_default = False
columns = frappe.parse_json(columns)
rows = frappe.parse_json(rows)
if not columns:
columns = [
{"label": "Name", "type": "Data", "key": "name", "width": "16rem"},
{"label": "Last Modified", "type": "Datetime", "key": "modified", "width": "8rem"},
]
if not rows:
rows = ["name"]
default_view_filters = {
"dt": doctype,
"type": view_type or "list",
"is_standard": 1,
"user": frappe.session.user,
}
if not custom_view and frappe.db.exists("CRM View Settings", default_view_filters):
list_view_settings = frappe.get_doc("CRM View Settings", default_view_filters)
columns = frappe.parse_json(list_view_settings.columns)
rows = frappe.parse_json(list_view_settings.rows)
is_default = False
elif not custom_view or (is_default and hasattr(_list, "default_list_data")):
rows = default_rows
columns = _list.default_list_data().get("columns")
visible_columns = []
for column in columns:
key = column.get("key")
# hidden fields are dropped unless the doctype lists them in its own defaults
column_meta = meta.get_field(key)
if column_meta and column_meta.get("hidden") and key not in default_column_keys:
continue
if key not in rows:
rows.append(key)
column["label"] = _(column.get("label"))
if key == "_liked_by" and column.get("width") == "10rem":
column["width"] = "50px"
visible_columns.append(column)
columns = visible_columns
# check if rows has group_by_field if not add it
if group_by_field and group_by_field not in rows:
rows.append(group_by_field)
if meta.track_seen and "_seen" not in rows:
rows.append("_seen")
data = (
frappe.get_list(
doctype,
fields=rows,
filters=filters,
order_by=order_by,
page_length=page_length,
)
or []
)
data = parse_list_data(data, doctype)
if view_type == "kanban":
if not rows:
rows = default_rows
if not kanban_columns and column_field:
field_meta = frappe.get_meta(doctype).get_field(column_field)
if field_meta.fieldtype == "Link":
kanban_columns = frappe.get_all(
field_meta.options,
fields=["name"],
order_by="modified asc",
)
elif field_meta.fieldtype == "Select":
kanban_columns = [{"name": option} for option in field_meta.options.split("\n")]
if not title_field:
title_field = "name"
if hasattr(_list, "default_kanban_settings"):
title_field = _list.default_kanban_settings().get("title_field")
if title_field not in rows:
rows.append(title_field)
if not kanban_fields:
kanban_fields = ["name"]
if hasattr(_list, "default_kanban_settings"):
kanban_fields = json.loads(_list.default_kanban_settings().get("kanban_fields"))
for field in kanban_fields:
if field not in rows:
rows.append(field)
for kc in kanban_columns:
# Start with base filters
column_filters = []
# Convert and add the main filters first
if filters:
base_filters = convert_filter_to_tuple(doctype, filters)
column_filters.extend(base_filters)
# Add the column-specific filter
if column_field and kc.get("name"):
column_filters.append([doctype, column_field, "=", kc.get("name")])
order = kc.get("order")
if kc.get("delete"):
column_data = []
else:
page_length = kc.get("page_length", 20)
if order:
column_data = get_records_based_on_order(
doctype, rows, column_filters, page_length, order
)
else:
column_data = frappe.get_list(
doctype,
fields=rows,
filters=column_filters,
order_by=order_by,
page_length=page_length,
)
all_count = frappe.get_list(
doctype,
filters=column_filters,
fields=[COUNT_NAME],
)[0].total_count
kc["all_count"] = all_count
kc["count"] = len(column_data)
if order:
column_data = sorted(
column_data,
key=lambda x: order.index(x.get("name")) if x.get("name") in order else len(order),
)
data.append({"column": kc, "fields": kanban_fields, "data": column_data})
fields = frappe.get_meta(doctype).fields
fields = [field for field in fields if field.fieldtype not in no_value_fields]
fields = [
{
"label": _(field.label),
"fieldtype": field.fieldtype,
"fieldname": field.fieldname,
"options": field.options,
}
for field in fields
if field.label and field.fieldname
]
std_fields = [
{"label": "Name", "fieldtype": "Data", "fieldname": "name"},
{"label": "Created On", "fieldtype": "Datetime", "fieldname": "creation"},
{"label": "Last Modified", "fieldtype": "Datetime", "fieldname": "modified"},
{
"label": "Modified By",
"fieldtype": "Link",
"fieldname": "modified_by",
"options": "User",
},
{"label": "Assigned To", "fieldtype": "Text", "fieldname": "_assign"},
{"label": "Owner", "fieldtype": "Link", "fieldname": "owner", "options": "User"},
{"label": "Like", "fieldtype": "Data", "fieldname": "_liked_by"},
]
for field in std_fields:
if field.get("fieldname") not in rows:
rows.append(field.get("fieldname"))
if field not in fields:
field["label"] = _(field["label"])
fields.append(field)
if not is_default and custom_view_name:
is_default = frappe.db.get_value("CRM View Settings", custom_view_name, "load_default_columns")
if group_by_field and view_type == "group_by":
def get_options(type, options):
if type == "Select":
return [option for option in options.split("\n")]
else:
has_empty_values = any([not d.get(group_by_field) for d in data])
options = list(set([d.get(group_by_field) for d in data]))
options = [u for u in options if u]
if has_empty_values:
options.append("")
if order_by and group_by_field in order_by:
order_by_fields = order_by.split(",")
order_by_fields = [
(field.split(" ")[0], field.split(" ")[1]) for field in order_by_fields
]
if (group_by_field, "asc") in order_by_fields:
options.sort()
elif (group_by_field, "desc") in order_by_fields:
options.sort(reverse=True)
else:
options.sort()
return options
for field in fields:
if field.get("fieldname") == group_by_field:
group_by_field = {
"label": field.get("label"),
"fieldname": field.get("fieldname"),
"fieldtype": field.get("fieldtype"),
"options": get_options(field.get("fieldtype"), field.get("options")),
}
return {
"data": data,
"columns": columns,
"rows": rows,
"fields": fields,
"column_field": column_field,
"title_field": title_field,
"kanban_columns": kanban_columns,
"kanban_fields": kanban_fields,
"group_by_field": group_by_field,
"page_length": page_length,
"page_length_count": page_length_count,
"is_default": is_default,
"views": get_views(doctype),
"total_count": frappe.get_list(doctype, filters=filters, fields=[COUNT_NAME])[0].total_count,
"row_count": len(data),
"form_script": get_form_script(doctype),
"list_script": get_form_script(doctype, "List"),
"view_type": view_type,
}
def parse_list_data(data, doctype):
_list = get_controller(doctype)
if hasattr(_list, "parse_list_data"):
data = _list.parse_list_data(data)
return data
def convert_filter_to_tuple(doctype, filters):
if isinstance(filters, dict):
filters_items = filters.items()
filters = []
for key, value in filters_items:
filters.append(make_filter_tuple(doctype, key, value))
return filters
def get_records_based_on_order(doctype, rows, filters, page_length, order):
records = []
filters = convert_filter_to_tuple(doctype, filters)
in_filters = filters.copy()
in_filters.append([doctype, "name", "in", order[:page_length]])
records = frappe.get_list(
doctype,
fields=rows,
filters=in_filters,
order_by="creation desc",
page_length=page_length,
)
if len(records) < page_length:
not_in_filters = filters.copy()
not_in_filters.append([doctype, "name", "not in", order])
remaining_records = frappe.get_list(
doctype,
fields=rows,
filters=not_in_filters,
order_by="creation desc",
page_length=page_length - len(records),
)
for record in remaining_records:
records.append(record)
return records
@frappe.whitelist()
def remove_assignments(doctype: str, name: str, assignees: str | list, ignore_permissions: bool = False):
assignees = frappe.parse_json(assignees)
if not assignees:
return
for assign_to in assignees:
set_status(
doctype,
name,
todo=None,
assign_to=assign_to,
status="Cancelled",
ignore_permissions=ignore_permissions,
)
@frappe.whitelist()
def add_seen(doctype: str, name: str):
doc = frappe.get_doc(doctype, name)
doc.check_permission("read")
doc.add_seen()
@frappe.whitelist()
def get_assigned_users(doctype: str, name: str | int, default_assigned_to: str | None = None):
assigned_users = frappe.get_all(
"ToDo",
fields=["allocated_to"],
filters={
"reference_type": doctype,
"reference_name": name,
"status": ("not in", ("Closed", "Cancelled")),
},
pluck="allocated_to",
)
users = list(set(assigned_users))
# if users is empty, add default_assigned_to
if not users and default_assigned_to:
users = [default_assigned_to]
return users
@frappe.whitelist()
def get_fields(doctype: str, allow_all_fieldtypes: bool = False):
not_allowed_fieldtypes = [*list(frappe.model.no_value_fields), "Read Only"]
if allow_all_fieldtypes:
not_allowed_fieldtypes = []
fields = frappe.get_meta(doctype).fields
_fields = []
for field in fields:
if field.fieldtype not in not_allowed_fieldtypes and field.fieldname:
_fields.append(field)
return _fields
def getCounts(d, doctype):
d["_email_count"] = (
frappe.db.count(
"Communication",
filters={
"reference_doctype": doctype,
"reference_name": d.get("name"),
"communication_type": "Communication",
},
)
or 0
)
d["_email_count"] = d["_email_count"] + frappe.db.count(
"Communication",
filters={
"reference_doctype": doctype,
"reference_name": d.get("name"),
"communication_type": "Automated Message",
},
)
d["_comment_count"] = frappe.db.count(
"Comment",
filters={"reference_doctype": doctype, "reference_name": d.get("name"), "comment_type": "Comment"},
)
d["_task_count"] = frappe.db.count(
"CRM Task", filters={"reference_doctype": doctype, "reference_docname": d.get("name")}
)
d["_note_count"] = frappe.db.count(
"FCRM Note", filters={"reference_doctype": doctype, "reference_docname": d.get("name")}
)
return d
@frappe.whitelist()
def get_linked_docs_of_document(doctype: str, docname: str):
try:
doc = frappe.get_doc(doctype, docname)
except frappe.DoesNotExistError:
return []
linked_docs = get_linked_docs(doc)
dynamic_linked_docs = get_dynamic_linked_docs(doc)
linked_docs.extend(dynamic_linked_docs)
linked_docs = list({doc["reference_docname"]: doc for doc in linked_docs}.values())
docs_data = []
for doc in linked_docs:
if not doc.get("reference_doctype") or not doc.get("reference_docname"):
continue
try:
data = frappe.get_doc(doc["reference_doctype"], doc["reference_docname"])
except (frappe.DoesNotExistError, frappe.ValidationError):
continue
title = data.get("title")
if data.doctype == "CRM Call Log":
title = f"Call from {data.get('from')} to {data.get('to')}"
if data.doctype == "CRM Deal":
title = data.get("organization")
if data.doctype == "CRM Notification":
title = data.get("message")
docs_data.append(
{
"doc": data.doctype,
"title": title or data.get("name"),
"reference_docname": doc["reference_docname"],
"reference_doctype": doc["reference_doctype"],
}
)
return docs_data
def remove_doc_link(doctype, docname):
if not doctype or not docname:
return
try:
linked_doc_data = frappe.get_doc(doctype, docname)
if doctype == "CRM Notification":
delete_notification_type = {
"notification_type_doctype": "",
"notification_type_doc": "",
}
delete_references = {
"reference_doctype": "",
"reference_name": "",
}
if linked_doc_data.get("notification_type_doctype") == linked_doc_data.get("reference_doctype"):
delete_references.update(delete_notification_type)
linked_doc_data.update(delete_references)
else:
linked_doc_data.update(
{
"reference_doctype": "",
"reference_docname": "",
}
)
linked_doc_data.save(ignore_permissions=True)
except (frappe.DoesNotExistError, frappe.ValidationError):
pass
def remove_contact_link(doctype, docname):
if not doctype or not docname:
return
try:
linked_doc_data = frappe.get_doc(doctype, docname)
linked_doc_data.update(
{
"contact": None,
"contacts": [],
}
)
linked_doc_data.save(ignore_permissions=True)
except (frappe.DoesNotExistError, frappe.ValidationError):
pass
@frappe.whitelist()
def remove_linked_doc_reference(items: str | list, remove_contact: bool = False, delete: bool = False):
if isinstance(items, str):
items = frappe.parse_json(items)
for item in items:
if not item.get("doctype") or not item.get("docname"):
continue
if not frappe.has_permission(item["doctype"], "write", item["docname"]):
continue
try:
if remove_contact:
remove_contact_link(item["doctype"], item["docname"])
else:
remove_doc_link(item["doctype"], item["docname"])
if delete:
frappe.delete_doc(item["doctype"], item["docname"])
except (frappe.DoesNotExistError, frappe.ValidationError):
# Skip if document doesn't exist or has validation errors
continue
return "success"
@frappe.whitelist()
def delete_bulk_docs(doctype: str, items: str | list, delete_linked: bool = False):
from frappe.desk.reportview import delete_bulk
if not doctype:
frappe.throw(_("Doctype is required"))
if not items:
frappe.throw(_("Items are required"))
items = frappe.parse_json(items)
if not isinstance(items, list):
frappe.throw(_("Items must be a list"))
for doc in items:
try:
if not frappe.db.exists(doctype, doc):
frappe.log_error(f"Document {doctype} {doc} does not exist", "Bulk Delete Error")
continue
linked_docs = get_linked_docs_of_document(doctype, doc)
for linked_doc in linked_docs:
if not linked_doc.get("reference_doctype") or not linked_doc.get("reference_docname"):
continue
remove_linked_doc_reference(
[
{
"doctype": linked_doc["reference_doctype"],
"docname": linked_doc["reference_docname"],
}
],
remove_contact=doctype == "Contact",
delete=delete_linked,
)
except Exception as e:
frappe.log_error(f"Error processing linked docs for {doctype} {doc}: {e!s}", "Bulk Delete Error")
if len(items) > 10:
frappe.enqueue("frappe.desk.reportview.delete_bulk", doctype=doctype, items=items)
else:
delete_bulk(doctype, items)
return "success"