diff --git a/crm/api/doc.py b/crm/api/doc.py
index a1a7bb40..8505a7a8 100644
--- a/crm/api/doc.py
+++ b/crm/api/doc.py
@@ -4,6 +4,7 @@ from frappe import _
from frappe.model.document import get_controller
from frappe.model import no_value_fields
from pypika import Criterion
+from frappe.utils import make_filter_tuple
from crm.api.views import get_views
from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script
@@ -200,19 +201,27 @@ def get_quick_filters(doctype: str):
return quick_filters
@frappe.whitelist()
-def get_list_data(
+def get_data(
doctype: str,
filters: dict,
order_by: str,
page_length=20,
page_length_count=20,
- columns=None,
- rows=None,
+ column_field=None,
+ title_field=None,
+ columns=[],
+ rows=[],
+ kanban_columns=[],
+ kanban_fields=[],
view=None,
default_filters=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
@@ -235,61 +244,133 @@ def get_list_data(
filters.update(default_filters)
is_default = True
- 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_default": 1,
- "user": frappe.session.user,
- }
-
+ data = []
_list = get_controller(doctype)
-
- 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"):
- columns = _list.default_list_data().get("columns")
-
if hasattr(_list, "default_list_data"):
rows = _list.default_list_data().get("rows")
- # check if rows has all keys from columns if not add them
- for column in columns:
- if column.get("key") not in rows:
- rows.append(column.get("key"))
- column["label"] = _(column.get("label"))
+ 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 column.get("key") == "_liked_by" and column.get("width") == "10rem":
- column["width"] = "50px"
+ if not columns:
+ columns = [
+ {"label": "Name", "type": "Data", "key": "name", "width": "16rem"},
+ {"label": "Last Modified", "type": "Datetime", "key": "modified", "width": "8rem"},
+ ]
- # 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 not rows:
+ rows = ["name"]
- data = frappe.get_list(
- doctype,
- fields=rows,
- filters=filters,
- order_by=order_by,
- page_length=page_length,
- ) or []
+ default_view_filters = {
+ "dt": doctype,
+ "type": view_type or 'list',
+ "is_default": 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"):
+ columns = _list.default_list_data().get("columns")
+
+ # check if rows has all keys from columns if not add them
+ for column in columns:
+ if column.get("key") not in rows:
+ rows.append(column.get("key"))
+ column["label"] = _(column.get("label"))
+
+ if column.get("key") == "_liked_by" and column.get("width") == "10rem":
+ column["width"] = "50px"
+
+ # 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)
+
+ data = frappe.get_list(
+ doctype,
+ fields=rows,
+ filters=filters,
+ order_by=order_by,
+ page_length=page_length,
+ ) or []
+
+ if view_type == "kanban":
+ 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:
+ column_filters = { column_field: kc.get('name') }
+ if column_field in filters and filters.get(column_field) != kc.name:
+ column_data = []
+ else:
+ column_filters.update(filters.copy())
+ page_length = 20
+
+ if kc.get("page_length"):
+ page_length = kc.get("page_length")
+
+ order = kc.get("order")
+ 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=convert_filter_to_tuple(doctype, column_filters),
+ order_by=order_by,
+ page_length=page_length,
+ )
+
+ new_filters = filters.copy()
+ new_filters.update({ column_field: kc.get('name') })
+
+ all_count = len(frappe.get_list(doctype, filters=convert_filter_to_tuple(doctype, new_filters)))
+
+ kc["all_count"] = all_count
+ kc["count"] = len(column_data)
+
+ for d in column_data:
+ getCounts(d, doctype)
+
+ 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]
@@ -365,6 +446,10 @@ def get_list_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,
@@ -374,8 +459,46 @@ def get_list_data(
"row_count": len(data),
"form_script": get_form_script(doctype),
"list_script": get_form_script(doctype, "List"),
+ "view_type": view_type,
}
+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 get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False):
not_allowed_fieldtypes = [
@@ -391,12 +514,38 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False):
fields = frappe.get_meta(doctype).fields
fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes]
+ standard_fields = [
+ {"fieldname": "name", "fieldtype": "Link", "label": "ID", "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:
+ if not restricted_fieldtypes or field.get('fieldtype') not in restricted_fieldtypes:
+ fields.append(field)
+
if as_array:
return fields
fields_meta = {}
for field in fields:
- fields_meta[field.fieldname] = field
+ fields_meta[field.get('fieldname')] = field
return fields_meta
@@ -531,4 +680,13 @@ def get_fields(doctype: str, allow_all_fieldtypes: bool = False):
"mandatory": field.reqd,
})
- return _fields
\ No newline at end of file
+ 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
\ No newline at end of file
diff --git a/crm/fcrm/doctype/crm_deal/crm_deal.py b/crm/fcrm/doctype/crm_deal/crm_deal.py
index 83615ab0..4ca327ad 100644
--- a/crm/fcrm/doctype/crm_deal/crm_deal.py
+++ b/crm/fcrm/doctype/crm_deal/crm_deal.py
@@ -190,6 +190,14 @@ class CRMDeal(Document):
]
return {'columns': columns, 'rows': rows}
+ @staticmethod
+ def default_kanban_settings():
+ return {
+ "column_field": "status",
+ "title_field": "organization",
+ "kanban_fields": '["annual_revenue", "email", "mobile_no", "_assign", "modified"]'
+ }
+
@frappe.whitelist()
def add_contact(deal, contact):
if not frappe.has_permission("CRM Deal", "write", deal):
diff --git a/crm/fcrm/doctype/crm_lead/crm_lead.py b/crm/fcrm/doctype/crm_lead/crm_lead.py
index f4ebe162..c9269e8a 100644
--- a/crm/fcrm/doctype/crm_lead/crm_lead.py
+++ b/crm/fcrm/doctype/crm_lead/crm_lead.py
@@ -324,6 +324,15 @@ class CRMLead(Document):
]
return {'columns': columns, 'rows': rows}
+ @staticmethod
+ def default_kanban_settings():
+ return {
+ "column_field": "status",
+ "title_field": "lead_name",
+ "kanban_fields": '["organization", "email", "mobile_no", "_assign", "modified"]'
+ }
+
+
@frappe.whitelist()
def convert_to_deal(lead, doc=None):
if not (doc and doc.flags.get("ignore_permissions")) and not frappe.has_permission("CRM Lead", "write", lead):
diff --git a/crm/fcrm/doctype/crm_task/crm_task.py b/crm/fcrm/doctype/crm_task/crm_task.py
index 1559ff3e..cf1bc963 100644
--- a/crm/fcrm/doctype/crm_task/crm_task.py
+++ b/crm/fcrm/doctype/crm_task/crm_task.py
@@ -60,3 +60,11 @@ class CRMTask(Document):
"modified",
]
return {'columns': columns, 'rows': rows}
+
+ @staticmethod
+ def default_kanban_settings():
+ return {
+ "column_field": "status",
+ "title_field": "title",
+ "kanban_fields": '["description", "priority", "creation"]'
+ }
diff --git a/crm/fcrm/doctype/crm_view_settings/crm_view_settings.json b/crm/fcrm/doctype/crm_view_settings/crm_view_settings.json
index de1ed9e8..dde0b128 100644
--- a/crm/fcrm/doctype/crm_view_settings/crm_view_settings.json
+++ b/crm/fcrm/doctype/crm_view_settings/crm_view_settings.json
@@ -15,16 +15,23 @@
"route_name",
"pinned",
"public",
- "columns_tab",
- "load_default_columns",
- "columns",
- "rows",
"filters_tab",
"filters",
"order_by_tab",
"order_by",
+ "list_tab",
+ "list_section",
+ "load_default_columns",
+ "columns",
+ "rows",
"group_by_tab",
- "group_by_field"
+ "group_by_field",
+ "kanban_tab",
+ "kanban_section",
+ "column_field",
+ "title_field",
+ "kanban_columns",
+ "kanban_fields"
],
"fields": [
{
@@ -48,11 +55,6 @@
"fieldtype": "Code",
"label": "Filters"
},
- {
- "fieldname": "columns_tab",
- "fieldtype": "Tab Break",
- "label": "Columns"
- },
{
"fieldname": "filters_tab",
"fieldtype": "Tab Break",
@@ -126,7 +128,7 @@
"fieldname": "type",
"fieldtype": "Select",
"label": "Type",
- "options": "list\ngroup_by"
+ "options": "list\ngroup_by\nkanban"
},
{
"fieldname": "group_by_tab",
@@ -137,11 +139,50 @@
"fieldname": "group_by_field",
"fieldtype": "Data",
"label": "Group By Field"
+ },
+ {
+ "fieldname": "list_section",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "kanban_section",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_field",
+ "fieldtype": "Data",
+ "label": "Column Field"
+ },
+ {
+ "fieldname": "list_tab",
+ "fieldtype": "Tab Break",
+ "label": "List"
+ },
+ {
+ "fieldname": "kanban_tab",
+ "fieldtype": "Tab Break",
+ "label": "Kanban"
+ },
+ {
+ "fieldname": "kanban_columns",
+ "fieldtype": "Code",
+ "label": "Kanban Columns"
+ },
+ {
+ "fieldname": "kanban_fields",
+ "fieldtype": "Code",
+ "label": "Kanban Fields"
+ },
+ {
+ "default": "name",
+ "fieldname": "title_field",
+ "fieldtype": "Data",
+ "label": "Title Field"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2024-06-01 16:58:34.952945",
+ "modified": "2024-06-25 19:40:12.067788",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM View Settings",
diff --git a/crm/fcrm/doctype/crm_view_settings/crm_view_settings.py b/crm/fcrm/doctype/crm_view_settings/crm_view_settings.py
index f33f3e7f..86f1bff5 100644
--- a/crm/fcrm/doctype/crm_view_settings/crm_view_settings.py
+++ b/crm/fcrm/doctype/crm_view_settings/crm_view_settings.py
@@ -16,13 +16,17 @@ def create(view):
view.filters = parse_json(view.filters) or {}
view.columns = parse_json(view.columns or '[]')
view.rows = parse_json(view.rows or '[]')
+ view.kanban_columns = parse_json(view.kanban_columns or '[]')
+ view.kanban_fields = parse_json(view.kanban_fields or '[]')
- default_rows = sync_default_list_rows(view.doctype)
+ default_rows = sync_default_rows(view.doctype)
view.rows = view.rows + default_rows if default_rows else view.rows
view.rows = remove_duplicates(view.rows)
- if not view.columns:
- view.columns = sync_default_list_columns(view.doctype)
+ if not view.kanban_columns and view.type == "kanban":
+ view.kanban_columns = sync_default_columns(view)
+ elif not view.columns:
+ view.columns = sync_default_columns(view)
doc = frappe.new_doc("CRM View Settings")
doc.name = view.label
@@ -36,6 +40,10 @@ def create(view):
doc.filters = json.dumps(view.filters)
doc.order_by = view.order_by
doc.group_by_field = view.group_by_field
+ doc.column_field = view.column_field
+ doc.title_field = view.title_field
+ doc.kanban_columns = json.dumps(view.kanban_columns)
+ doc.kanban_fields = json.dumps(view.kanban_fields)
doc.columns = json.dumps(view.columns)
doc.rows = json.dumps(view.rows)
doc.insert()
@@ -48,8 +56,10 @@ def update(view):
filters = parse_json(view.filters) or {}
columns = parse_json(view.columns) or []
rows = parse_json(view.rows) or []
+ kanban_columns = parse_json(view.kanban_columns) or []
+ kanban_fields = parse_json(view.kanban_fields) or []
- default_rows = sync_default_list_rows(view.doctype)
+ default_rows = sync_default_rows(view.doctype)
rows = rows + default_rows if default_rows else rows
rows = remove_duplicates(rows)
@@ -62,6 +72,10 @@ def update(view):
doc.filters = json.dumps(filters)
doc.order_by = view.order_by
doc.group_by_field = view.group_by_field
+ doc.column_field = view.column_field
+ doc.title_field = view.title_field
+ doc.kanban_columns = json.dumps(kanban_columns)
+ doc.kanban_fields = json.dumps(kanban_fields)
doc.columns = json.dumps(columns)
doc.rows = json.dumps(rows)
doc.save()
@@ -91,7 +105,7 @@ def pin(name, value):
def remove_duplicates(l):
return list(dict.fromkeys(l))
-def sync_default_list_rows(doctype):
+def sync_default_rows(doctype, type="list"):
list = get_controller(doctype)
rows = []
@@ -100,11 +114,21 @@ def sync_default_list_rows(doctype):
return rows
-def sync_default_list_columns(doctype):
- list = get_controller(doctype)
+def sync_default_columns(view):
+ list = get_controller(view.doctype)
columns = []
- if hasattr(list, "default_list_data"):
+ if view.type == "kanban" and view.column_field:
+ field_meta = frappe.get_meta(view.doctype).get_field(view.column_field)
+ if field_meta.fieldtype == "Link":
+ columns = frappe.get_all(
+ field_meta.options,
+ fields=["name"],
+ order_by="modified asc",
+ )
+ elif field_meta.fieldtype == "Select":
+ columns = [{"name": option} for option in field_meta.options.split("\n")]
+ elif hasattr(list, "default_list_data"):
columns = list.default_list_data().get("columns")
return columns
@@ -117,13 +141,17 @@ def create_or_update_default_view(view):
filters = parse_json(view.filters) or {}
columns = parse_json(view.columns or '[]')
rows = parse_json(view.rows or '[]')
+ kanban_columns = parse_json(view.kanban_columns or '[]')
+ kanban_fields = parse_json(view.kanban_fields or '[]')
- default_rows = sync_default_list_rows(view.doctype)
+ default_rows = sync_default_rows(view.doctype, view.type)
rows = rows + default_rows if default_rows else rows
rows = remove_duplicates(rows)
- if not columns:
- columns = sync_default_list_columns(view.doctype)
+ if not kanban_columns and view.type == "kanban":
+ kanban_columns = sync_default_columns(view)
+ elif not columns:
+ columns = sync_default_columns(view)
doc = frappe.db.exists(
"CRM View Settings",
@@ -143,6 +171,10 @@ def create_or_update_default_view(view):
doc.filters = json.dumps(filters)
doc.order_by = view.order_by
doc.group_by_field = view.group_by_field
+ doc.column_field = view.column_field
+ doc.title_field = view.title_field
+ doc.kanban_columns = json.dumps(kanban_columns)
+ doc.kanban_fields = json.dumps(kanban_fields)
doc.columns = json.dumps(columns)
doc.rows = json.dumps(rows)
doc.save()
@@ -159,6 +191,10 @@ def create_or_update_default_view(view):
doc.filters = json.dumps(filters)
doc.order_by = view.order_by
doc.group_by_field = view.group_by_field
+ doc.column_field = view.column_field
+ doc.title_field = view.title_field
+ doc.kanban_columns = json.dumps(kanban_columns)
+ doc.kanban_fields = json.dumps(kanban_fields)
doc.columns = json.dumps(columns)
doc.rows = json.dumps(rows)
doc.is_default = True
diff --git a/frontend/src/components/Icons/KanbanIcon.vue b/frontend/src/components/Icons/KanbanIcon.vue
new file mode 100644
index 00000000..bb12b401
--- /dev/null
+++ b/frontend/src/components/Icons/KanbanIcon.vue
@@ -0,0 +1,18 @@
+
+
+
diff --git a/frontend/src/components/Icons/TaskStatusIcon.vue b/frontend/src/components/Icons/TaskStatusIcon.vue
index c7ed07b1..75e9e20a 100644
--- a/frontend/src/components/Icons/TaskStatusIcon.vue
+++ b/frontend/src/components/Icons/TaskStatusIcon.vue
@@ -3,6 +3,7 @@
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
+ viewBox="0 0 16 16"
fill="none"
class="text-gray-700"
:aria-label="status"
diff --git a/frontend/src/components/Kanban/KanbanSettings.vue b/frontend/src/components/Kanban/KanbanSettings.vue
new file mode 100644
index 00000000..e04c396a
--- /dev/null
+++ b/frontend/src/components/Kanban/KanbanSettings.vue
@@ -0,0 +1,221 @@
+
+
+
+
+
diff --git a/frontend/src/components/Kanban/KanbanView.vue b/frontend/src/components/Kanban/KanbanView.vue
new file mode 100644
index 00000000..d8136501
--- /dev/null
+++ b/frontend/src/components/Kanban/KanbanView.vue
@@ -0,0 +1,253 @@
+
+