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
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
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
This commit is contained in:
857
crm/api/doc.py
Normal file
857
crm/api/doc.py
Normal file
@@ -0,0 +1,857 @@
|
||||
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"
|
||||
Reference in New Issue
Block a user