diff --git a/crm/api/doc.py b/crm/api/doc.py index 64d14f6f..11ddcf5b 100644 --- a/crm/api/doc.py +++ b/crm/api/doc.py @@ -565,95 +565,6 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False): return fields_meta -@frappe.whitelist() -def get_sidebar_fields(doctype, name): - if not frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}): - return [] - layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}).layout - - if not layout: - return [] - - layout = json.loads(layout) - - not_allowed_fieldtypes = [ - "Tab Break", - "Section Break", - "Column Break", - ] - - fields = frappe.get_meta(doctype).fields - fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes] - - doc = frappe.get_cached_doc(doctype, name) - has_high_permlevel_fields = any(df.permlevel > 0 for df in fields) - if has_high_permlevel_fields: - has_read_access_to_permlevels = doc.get_permlevel_access("read") - has_write_access_to_permlevels = doc.get_permlevel_access("write") - - for section in layout: - section["name"] = section.get("name") or section.get("label") - for field in section.get("fields") if section.get("fields") else []: - field_obj = next((f for f in fields if f.fieldname == field), None) - if field_obj: - if field_obj.permlevel > 0: - field_has_write_access = field_obj.permlevel in has_write_access_to_permlevels - field_has_read_access = field_obj.permlevel in has_read_access_to_permlevels - if not field_has_write_access and field_has_read_access: - field_obj.read_only = 1 - if not field_has_read_access and not field_has_write_access: - field_obj.hidden = 1 - section["fields"][section.get("fields").index(field)] = get_field_obj(field_obj) - - fields_meta = {} - for field in fields: - fields_meta[field.fieldname] = field - - return layout - - -def get_field_obj(field): - obj = { - "label": field.label, - "type": get_type(field), - "name": field.fieldname, - "hidden": field.hidden, - "reqd": field.reqd, - "read_only": field.read_only, - "all_properties": field, - } - - obj["placeholder"] = field.get("placeholder") or "Add " + field.label + "..." - - if field.fieldtype == "Link": - obj["placeholder"] = field.get("placeholder") or "Select " + field.label + "..." - obj["doctype"] = field.options - elif field.fieldtype == "Select" and field.options: - obj["placeholder"] = field.get("placeholder") or "Select " + field.label + "..." - obj["options"] = [{"label": option, "value": option} for option in field.options.split("\n")] - - if field.read_only: - obj["tooltip"] = "This field is read only and cannot be edited." - - return obj - - -def get_type(field): - if field.fieldtype == "Data" and field.options == "Phone": - return "phone" - elif field.fieldtype == "Data" and field.options == "Email": - return "email" - elif field.fieldtype == "Check": - return "checkbox" - elif field.fieldtype == "Int": - return "number" - elif field.fieldtype in ["Small Text", "Text", "Long Text"]: - return "textarea" - elif field.read_only: - return "read_only" - return field.fieldtype.lower() - - def get_assigned_users(doctype, name, default_assigned_to=None): assigned_users = frappe.get_all( "ToDo", @@ -685,22 +596,7 @@ def get_fields(doctype: str, allow_all_fieldtypes: bool = False): for field in fields: if field.fieldtype not in not_allowed_fieldtypes and field.fieldname: - _fields.append( - { - "label": field.label, - "type": field.fieldtype, - "value": field.fieldname, - "options": field.options, - "mandatory": field.reqd, - "read_only": field.read_only, - "hidden": field.hidden, - "depends_on": field.depends_on, - "mandatory_depends_on": field.mandatory_depends_on, - "read_only_depends_on": field.read_only_depends_on, - "link_filters": field.get("link_filters"), - "placeholder": field.get("placeholder"), - } - ) + _fields.append(field) return _fields diff --git a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.json b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.json index 7094e58d..43019c5c 100644 --- a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.json +++ b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.json @@ -37,7 +37,7 @@ "fieldname": "layout", "fieldtype": "Code", "label": "Layout", - "options": "JS" + "options": "JSON" }, { "fieldname": "column_break_post", @@ -46,7 +46,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-12-29 12:58:54.280569", + "modified": "2025-01-02 22:12:51.663011", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Fields Layout", @@ -64,6 +64,15 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 } ], "sort_field": "creation", diff --git a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py index 23552e5c..271a5da9 100644 --- a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py +++ b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py @@ -6,6 +6,7 @@ import json import frappe from frappe import _ from frappe.model.document import Document +from frappe.utils import random_string class CRMFieldsLayout(Document): @@ -13,7 +14,7 @@ class CRMFieldsLayout(Document): @frappe.whitelist() -def get_fields_layout(doctype: str, type: str): +def get_fields_layout(doctype: str, type: str, parent_doctype: str | None = None): tabs = [] layout = None @@ -29,38 +30,116 @@ def get_fields_layout(doctype: str, type: str): has_tabs = tabs[0].get("sections") if tabs and tabs[0] else False if not has_tabs: - tabs = [{"no_tabs": True, "sections": tabs}] + tabs = [{"name": "first_tab", "sections": tabs}] allowed_fields = [] for tab in tabs: for section in tab.get("sections"): - if not section.get("fields"): + if "columns" not in section: continue - allowed_fields.extend(section.get("fields")) + for column in section.get("columns"): + if not column.get("fields"): + continue + allowed_fields.extend(column.get("fields")) fields = frappe.get_meta(doctype).fields fields = [field for field in fields if field.fieldname in allowed_fields] for tab in tabs: for section in tab.get("sections"): - for field in section.get("fields") if section.get("fields") else []: - field = next((f for f in fields if f.fieldname == field), None) - if field: - field = { - "label": _(field.label), - "name": field.fieldname, - "type": field.fieldtype, - "options": getOptions(field), - "mandatory": field.reqd, - "read_only": field.read_only, - "placeholder": field.get("placeholder"), - "filters": field.get("link_filters"), - } - section["fields"][section.get("fields").index(field["name"])] = field + for column in section.get("columns") if section.get("columns") else []: + for field in column.get("fields") if column.get("fields") else []: + field = next((f for f in fields if f.fieldname == field), None) + if field: + field = field.as_dict() + handle_perm_level_restrictions(field, doctype, parent_doctype) + column["fields"][column.get("fields").index(field["fieldname"])] = field return tabs or [] +@frappe.whitelist() +def get_sidepanel_sections(doctype): + if not frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}): + return [] + layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}).layout + + if not layout: + return [] + + layout = json.loads(layout) + + not_allowed_fieldtypes = [ + "Tab Break", + "Section Break", + "Column Break", + ] + + fields = frappe.get_meta(doctype).fields + fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes] + + for section in layout: + section["name"] = section.get("name") or section.get("label") + for column in section.get("columns") if section.get("columns") else []: + for field in column.get("fields") if column.get("fields") else []: + field_obj = next((f for f in fields if f.fieldname == field), None) + if field_obj: + field_obj = field_obj.as_dict() + handle_perm_level_restrictions(field_obj, doctype) + column["fields"][column.get("fields").index(field)] = get_field_obj(field_obj) + + fields_meta = {} + for field in fields: + fields_meta[field.fieldname] = field + + return layout + + +def handle_perm_level_restrictions(field, doctype, parent_doctype=None): + if field.permlevel == 0: + return + field_has_write_access = field.permlevel in get_permlevel_access("write", doctype, parent_doctype) + field_has_read_access = field.permlevel in get_permlevel_access("read", doctype, parent_doctype) + + if not field_has_write_access and field_has_read_access: + field.read_only = 1 + if not field_has_read_access and not field_has_write_access: + field.hidden = 1 + + +def get_permlevel_access(permission_type="write", doctype=None, parent_doctype=None): + allowed_permlevels = [] + roles = frappe.get_roles() + + meta = frappe.get_meta(doctype) + + if meta.istable and parent_doctype: + meta = frappe.get_meta(parent_doctype) + elif meta.istable and not parent_doctype: + return [1, 0] + + for perm in meta.permissions: + if perm.role in roles and perm.get(permission_type) and perm.permlevel not in allowed_permlevels: + allowed_permlevels.append(perm.permlevel) + + return allowed_permlevels + + +def get_field_obj(field): + field["placeholder"] = field.get("placeholder") or "Add " + field.label + "..." + + if field.fieldtype == "Link": + field["placeholder"] = field.get("placeholder") or "Select " + field.label + "..." + elif field.fieldtype == "Select" and field.options: + field["placeholder"] = field.get("placeholder") or "Select " + field.label + "..." + field["options"] = [{"label": option, "value": option} for option in field.options.split("\n")] + + if field.read_only: + field["tooltip"] = "This field is read only and cannot be edited." + + return field + + @frappe.whitelist() def save_fields_layout(doctype: str, type: str, layout: str): if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": type}): @@ -82,18 +161,45 @@ def save_fields_layout(doctype: str, type: str, layout: str): def get_default_layout(doctype: str): fields = frappe.get_meta(doctype).fields - fields = [ - field.fieldname - for field in fields - if field.fieldtype not in ["Tab Break", "Section Break", "Column Break"] - ] - return [{"no_tabs": True, "sections": [{"hideLabel": True, "fields": fields}]}] + tabs = [] + if fields and fields[0].fieldtype != "Tab Break": + sections = [] + if fields and fields[0].fieldtype != "Section Break": + sections.append( + { + "name": "section_" + str(random_string(4)), + "columns": [{"name": "column_" + str(random_string(4)), "fields": []}], + } + ) + tabs.append({"name": "tab_" + str(random_string(4)), "sections": sections}) -def getOptions(field): - if field.fieldtype == "Select" and field.options: - field.options = field.options.split("\n") - field.options = [{"label": _(option), "value": option} for option in field.options] - field.options.insert(0, {"label": "", "value": ""}) - return field.options + for field in fields: + if field.fieldtype == "Tab Break": + tabs.append( + { + "name": "tab_" + str(random_string(4)), + "sections": [ + { + "name": "section_" + str(random_string(4)), + "columns": [{"name": "column_" + str(random_string(4)), "fields": []}], + } + ], + } + ) + elif field.fieldtype == "Section Break": + tabs[-1]["sections"].append( + { + "name": "section_" + str(random_string(4)), + "columns": [{"name": "column_" + str(random_string(4)), "fields": []}], + } + ) + elif field.fieldtype == "Column Break": + tabs[-1]["sections"][-1]["columns"].append( + {"name": "column_" + str(random_string(4)), "fields": []} + ) + else: + tabs[-1]["sections"][-1]["columns"][-1]["fields"].append(field.fieldname) + + return tabs diff --git a/crm/fcrm/doctype/crm_industry/crm_industry.json b/crm/fcrm/doctype/crm_industry/crm_industry.json index d3d15a71..5128d8be 100644 --- a/crm/fcrm/doctype/crm_industry/crm_industry.json +++ b/crm/fcrm/doctype/crm_industry/crm_industry.json @@ -21,7 +21,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-01-19 21:57:02.025918", + "modified": "2025-01-02 22:14:28.686821", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Industry", @@ -51,6 +51,15 @@ "role": "Sales Manager", "share": 1, "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 } ], "quick_entry": 1, diff --git a/crm/fcrm/doctype/crm_lead/crm_lead.json b/crm/fcrm/doctype/crm_lead/crm_lead.json index 786c03a5..e39af407 100644 --- a/crm/fcrm/doctype/crm_lead/crm_lead.json +++ b/crm/fcrm/doctype/crm_lead/crm_lead.json @@ -290,7 +290,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2024-09-17 18:36:57.289897", + "modified": "2025-01-02 22:14:01.991054", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Lead", @@ -320,6 +320,15 @@ "role": "Sales Manager", "share": 1, "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 } ], "sender_field": "email", diff --git a/crm/fcrm/doctype/crm_lead_source/crm_lead_source.json b/crm/fcrm/doctype/crm_lead_source/crm_lead_source.json index 23aea6f6..416ccd7e 100644 --- a/crm/fcrm/doctype/crm_lead_source/crm_lead_source.json +++ b/crm/fcrm/doctype/crm_lead_source/crm_lead_source.json @@ -29,7 +29,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-01-19 21:56:04.702254", + "modified": "2025-01-02 22:13:30.498404", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Lead Source", @@ -59,6 +59,15 @@ "role": "Sales Manager", "share": 1, "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 } ], "quick_entry": 1, diff --git a/crm/fcrm/doctype/crm_lead_status/crm_lead_status.json b/crm/fcrm/doctype/crm_lead_status/crm_lead_status.json index 587f6b61..21ca8b61 100644 --- a/crm/fcrm/doctype/crm_lead_status/crm_lead_status.json +++ b/crm/fcrm/doctype/crm_lead_status/crm_lead_status.json @@ -37,7 +37,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-01-19 21:56:16.872924", + "modified": "2025-01-02 22:13:43.038656", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Lead Status", @@ -67,6 +67,15 @@ "role": "Sales Manager", "share": 1, "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 } ], "sort_field": "modified", diff --git a/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.json b/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.json index e51a1b4f..408075c5 100644 --- a/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.json +++ b/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.json @@ -6,13 +6,13 @@ "engine": "InnoDB", "field_order": [ "enabled", - "is_erpnext_in_different_site", - "column_break_vfru", "erpnext_company", - "section_break_oubd", + "column_break_vfru", + "is_erpnext_in_different_site", "erpnext_site_url", - "column_break_fllx", + "section_break_oubd", "api_key", + "column_break_fllx", "api_secret", "section_break_jnbn", "create_customer_on_status_change", @@ -37,7 +37,8 @@ { "depends_on": "enabled", "fieldname": "section_break_oubd", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "ERPNext Site API's" }, { "fieldname": "column_break_fllx", @@ -101,7 +102,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-12-30 19:22:07.929304", + "modified": "2024-12-31 23:24:29.636820", "modified_by": "Administrator", "module": "FCRM", "name": "ERPNext CRM Settings", diff --git a/crm/install.py b/crm/install.py index 6dc50be9..b0201d6d 100644 --- a/crm/install.py +++ b/crm/install.py @@ -120,53 +120,53 @@ def add_default_fields_layout(force=False): quick_entry_layouts = { "CRM Lead-Quick Entry": { "doctype": "CRM Lead", - "layout": '[{"label":"Person","fields":["salutation","first_name","last_name","email","mobile_no", "gender"],"hideLabel":true},{"label":"Organization","fields":["organization","website","no_of_employees","territory","annual_revenue","industry"],"hideLabel":true,"hideBorder":false},{"label":"Other","columns":2,"fields":["status","lead_owner"],"hideLabel":true,"hideBorder":false}]', + "layout": '[{"name": "person_section", "columns": [{"name": "column_5jrk", "fields": ["salutation", "email"]}, {"name": "column_5CPV", "fields": ["first_name", "mobile_no"]}, {"name": "column_gXOy", "fields": ["last_name", "gender"]}]}, {"name": "organization_section", "columns": [{"name": "column_GHfX", "fields": ["organization", "territory"]}, {"name": "column_hXjS", "fields": ["website", "annual_revenue"]}, {"name": "column_RDNA", "fields": ["no_of_employees", "industry"]}]}, {"name": "lead_section", "columns": [{"name": "column_EO1H", "fields": ["status"]}, {"name": "column_RWBe", "fields": ["lead_owner"]}]}]', }, "CRM Deal-Quick Entry": { "doctype": "CRM Deal", - "layout": '[{"label": "Select Organization", "fields": ["organization"], "hideLabel": true, "editable": true}, {"label": "Organization Details", "fields": ["organization_name", "website", "no_of_employees", "territory", "annual_revenue", "industry"], "hideLabel": true, "editable": true}, {"label": "Select Contact", "fields": ["contact"], "hideLabel": true, "editable": true}, {"label": "Contact Details", "fields": ["salutation", "first_name", "last_name", "email", "mobile_no", "gender"], "hideLabel": true, "editable": true}, {"label": "Other", "columns": 2, "fields": ["status", "deal_owner"], "hideLabel": true}]', + "layout": '[{"name": "organization_section", "hidden": true, "editable": false, "columns": [{"name": "column_GpMP", "fields": ["organization"]}, {"name": "column_FPTn", "fields": []}]}, {"name": "organization_details_section", "editable": false, "columns": [{"name": "column_S3tQ", "fields": ["organization_name", "territory"]}, {"name": "column_KqV1", "fields": ["website", "annual_revenue"]}, {"name": "column_1r67", "fields": ["no_of_employees", "industry"]}]}, {"name": "contact_section", "hidden": true, "editable": false, "columns": [{"name": "column_CeXr", "fields": ["contact"]}, {"name": "column_yHbk", "fields": []}]}, {"name": "contact_details_section", "editable": false, "columns": [{"name": "column_ZTWr", "fields": ["salutation", "email"]}, {"name": "column_tabr", "fields": ["first_name", "mobile_no"]}, {"name": "column_Qjdx", "fields": ["last_name", "gender"]}]}, {"name": "deal_section", "columns": [{"name": "column_mdps", "fields": ["status"]}, {"name": "column_H40H", "fields": ["deal_owner"]}]}]', }, "Contact-Quick Entry": { "doctype": "Contact", - "layout": '[{"label":"Salutation","columns":1,"fields":["salutation"],"hideLabel":true},{"label":"Full Name","columns":2,"hideBorder":true,"fields":["first_name","last_name"],"hideLabel":true},{"label":"Email","columns":1,"hideBorder":true,"fields":["email_id"],"hideLabel":true},{"label":"Mobile No. & Gender","columns":2,"hideBorder":true,"fields":["mobile_no","gender"],"hideLabel":true},{"label":"Organization","columns":1,"hideBorder":true,"fields":["company_name"],"hideLabel":true},{"label":"Designation","columns":1,"hideBorder":true,"fields":["designation"],"hideLabel":true},{"label":"Address","columns":1,"hideBorder":true,"fields":["address"],"hideLabel":true}]', + "layout": '[{"name": "salutation_section", "columns": [{"name": "column_eXks", "fields": ["salutation"]}]}, {"name": "full_name_section", "hideBorder": true, "columns": [{"name": "column_cSxf", "fields": ["first_name"]}, {"name": "column_yBc7", "fields": ["last_name"]}]}, {"name": "email_section", "hideBorder": true, "columns": [{"name": "column_tH3L", "fields": ["email_id"]}]}, {"name": "mobile_gender_section", "hideBorder": true, "columns": [{"name": "column_lrfI", "fields": ["mobile_no"]}, {"name": "column_Tx3n", "fields": ["gender"]}]}, {"name": "organization_section", "hideBorder": true, "columns": [{"name": "column_S0J8", "fields": ["company_name"]}]}, {"name": "designation_section", "hideBorder": true, "columns": [{"name": "column_bsO8", "fields": ["designation"]}]}, {"name": "address_section", "hideBorder": true, "columns": [{"name": "column_W3VY", "fields": ["address"]}]}]', }, "CRM Organization-Quick Entry": { "doctype": "CRM Organization", - "layout": '[{"label":"Organization Name","columns":1,"fields":["organization_name"],"hideLabel":true},{"label":"Website & Revenue","columns":2,"hideBorder":true,"fields":["website","annual_revenue"],"hideLabel":true},{"label":"Territory","columns":1,"hideBorder":true,"fields":["territory"],"hideLabel":true},{"label":"No of Employees & Industry","columns":2,"hideBorder":true,"fields":["no_of_employees","industry"],"hideLabel":true},{"label":"Address","columns":1,"hideBorder":true,"fields":["address"],"hideLabel":true}]', + "layout": '[{"name": "organization_section", "columns": [{"name": "column_zOuv", "fields": ["organization_name"]}]}, {"name": "website_revenue_section", "hideBorder": true, "columns": [{"name": "column_I5Dy", "fields": ["website"]}, {"name": "column_Rgss", "fields": ["annual_revenue"]}]}, {"name": "territory_section", "hideBorder": true, "columns": [{"name": "column_w6ap", "fields": ["territory"]}]}, {"name": "employee_industry_section", "hideBorder": true, "columns": [{"name": "column_u5tZ", "fields": ["no_of_employees"]}, {"name": "column_FFrT", "fields": ["industry"]}]}, {"name": "address_section", "hideBorder": true, "columns": [{"name": "column_O2dk", "fields": ["address"]}]}]', }, "Address-Quick Entry": { "doctype": "Address", - "layout": '[{"label":"Address","columns":1,"fields":["address_title","address_type","address_line1","address_line2","city","state","country","pincode"],"hideLabel":true}]', + "layout": '[{"name": "details_section", "columns": [{"name": "column_uSSG", "fields": ["address_title", "address_type", "address_line1", "address_line2", "city", "state", "country", "pincode"]}]}]', }, } sidebar_fields_layouts = { "CRM Lead-Side Panel": { "doctype": "CRM Lead", - "layout": '[{"label": "Details", "name": "details", "opened": true, "fields": ["organization", "website", "territory", "industry", "job_title", "source", "lead_owner"]}, {"label": "Person", "name": "person_tab", "opened": true, "fields": ["salutation", "first_name", "last_name", "email", "mobile_no"]}]', + "layout": '[{"label": "Details", "name": "details_section", "opened": true, "columns": [{"name": "column_kl92", "fields": ["organization", "website", "territory", "industry", "job_title", "source", "lead_owner"]}]}, {"label": "Person", "name": "person_section", "opened": true, "columns": [{"name": "column_XmW2", "fields": ["salutation", "first_name", "last_name", "email", "mobile_no"]}]}]', }, "CRM Deal-Side Panel": { "doctype": "CRM Deal", - "layout": '[{"label":"Contacts","name":"contacts_section","opened":true,"editable":false,"contacts":[]},{"label":"Organization Details","name":"organization_tab","opened":true,"fields":["organization","website","territory","annual_revenue","close_date","probability","next_step","deal_owner"]}]', + "layout": '[{"label": "Contacts", "name": "contacts_section", "opened": true, "editable": false, "contacts": []}, {"label": "Organization Details", "name": "organization_section", "opened": true, "columns": [{"name": "column_na2Q", "fields": ["organization", "website", "territory", "annual_revenue", "close_date", "probability", "next_step", "deal_owner"]}]}]', }, "Contact-Side Panel": { "doctype": "Contact", - "layout": '[{"label":"Details","name":"details","opened":true,"fields":["salutation","first_name","last_name","email_id","mobile_no","gender","company_name","designation","address"]}]', + "layout": '[{"label": "Details", "name": "details_section", "opened": true, "columns": [{"name": "column_eIWl", "fields": ["salutation", "first_name", "last_name", "email_id", "mobile_no", "gender", "company_name", "designation", "address"]}]}]', }, "CRM Organization-Side Panel": { "doctype": "CRM Organization", - "layout": '[{"label":"Details","name":"details","opened":true,"fields":["organization_name","website","territory","industry","no_of_employees","address"]}]', + "layout": '[{"label": "Details", "name": "details_section", "opened": true, "columns": [{"name": "column_IJOV", "fields": ["organization_name", "website", "territory", "industry", "no_of_employees", "address"]}]}]', }, } data_fields_layouts = { "CRM Lead-Data Fields": { "doctype": "CRM Lead", - "layout": '[{"no_tabs":true,"sections":[{"label": "Details", "name": "details", "opened": true, "fields": ["organization", "website", "territory", "industry", "job_title", "source", "lead_owner"]}, {"label": "Person", "name": "person_tab", "opened": true, "fields": ["salutation", "first_name", "last_name", "email", "mobile_no"]}]}]', + "layout": '[{"label": "Details", "name": "details_section", "opened": true, "columns": [{"name": "column_ZgLG", "fields": ["organization", "industry", "lead_owner"]}, {"name": "column_TbYq", "fields": ["website", "job_title"]}, {"name": "column_OKSX", "fields": ["territory", "source"]}]}, {"label": "Person", "name": "person_section", "opened": true, "columns": [{"name": "column_6c5g", "fields": ["salutation", "email"]}, {"name": "column_1n7Q", "fields": ["first_name", "mobile_no"]}, {"name": "column_cT6C", "fields": ["last_name"]}]}]', }, "CRM Deal-Data Fields": { "doctype": "CRM Deal", - "layout": '[{"no_tabs":true,"sections":[{"label":"Organization Details","name":"organization_tab","opened":true,"fields":["organization","website","territory","annual_revenue","close_date","probability","next_step","deal_owner"]}]}]', + "layout": '[{"label": "Details", "name": "details_section", "opened": true, "columns": [{"name": "column_z9XL", "fields": ["organization", "annual_revenue", "next_step"]}, {"name": "column_gM4w", "fields": ["website", "close_date", "deal_owner"]}, {"name": "column_gWmE", "fields": ["territory", "probability"]}]}]', }, } diff --git a/crm/patches.txt b/crm/patches.txt index a0e91a3d..e30c693a 100644 --- a/crm/patches.txt +++ b/crm/patches.txt @@ -8,4 +8,5 @@ crm.patches.v1_0.move_crm_note_data_to_fcrm_note crm.patches.v1_0.create_email_template_custom_fields crm.patches.v1_0.create_default_fields_layout #10/12/2024 crm.patches.v1_0.create_default_sidebar_fields_layout -crm.patches.v1_0.update_deal_quick_entry_layout \ No newline at end of file +crm.patches.v1_0.update_deal_quick_entry_layout +crm.patches.v1_0.update_fields_layout_to_new_format \ No newline at end of file diff --git a/crm/patches/v1_0/update_fields_layout_to_new_format.py b/crm/patches/v1_0/update_fields_layout_to_new_format.py new file mode 100644 index 00000000..a720339d --- /dev/null +++ b/crm/patches/v1_0/update_fields_layout_to_new_format.py @@ -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 old_layout[0]: + new_layout = new_layout[0].get("sections") + + return json.dumps(new_layout) diff --git a/frontend/src/components/Controls/Grid.vue b/frontend/src/components/Controls/Grid.vue index e866bab1..0515015e 100644 --- a/frontend/src/components/Controls/Grid.vue +++ b/frontend/src/components/Controls/Grid.vue @@ -33,7 +33,7 @@
{{ __(field.label) }} @@ -84,36 +84,71 @@
+ + + + + +
@@ -170,6 +205,7 @@ :index="index" :data="row" :doctype="doctype" + :parentDoctype="parentDoctype" />
@@ -198,6 +234,7 @@ v-if="showGridRowFieldsModal" v-model="showGridRowFieldsModal" :doctype="doctype" + :parentDoctype="parentDoctype" /> { function getFieldObj(field) { return { - label: field.label, - name: field.fieldname, - type: field.fieldtype, - options: field.options, - in_list_view: field.in_list_view, + ...field, + filters: field.link_filters && JSON.parse(field.link_filters), + placeholder: field.placeholder || field.label, } } @@ -317,8 +356,8 @@ const toggleSelectRow = (row) => { const addRow = () => { const newRow = {} fields.value?.forEach((field) => { - if (field.type === 'Check') newRow[field.name] = false - else newRow[field.name] = '' + if (field.fieldtype === 'Check') newRow[field.fieldname] = false + else newRow[field.fieldname] = '' }) newRow.name = getRandom(10) showRowList.value.push(false) diff --git a/frontend/src/components/Controls/GridRowFieldsModal.vue b/frontend/src/components/Controls/GridRowFieldsModal.vue index 4c18383f..fb0227b2 100644 --- a/frontend/src/components/Controls/GridRowFieldsModal.vue +++ b/frontend/src/components/Controls/GridRowFieldsModal.vue @@ -1,5 +1,5 @@ - import Resizer from '@/components/Resizer.vue' -import Section from '@/components/Section.vue' import SidePanelLayout from '@/components/SidePanelLayout.vue' -import SidePanelModal from '@/components/Modals/SidePanelModal.vue' import Icon from '@/components/Icon.vue' import LayoutHeader from '@/components/LayoutHeader.vue' import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue' @@ -208,7 +178,6 @@ import AddressModal from '@/components/Modals/AddressModal.vue' import DealsListView from '@/components/ListViews/DealsListView.vue' import ContactsListView from '@/components/ListViews/ContactsListView.vue' import WebsiteIcon from '@/components/Icons/WebsiteIcon.vue' -import EditIcon from '@/components/Icons/EditIcon.vue' import CameraIcon from '@/components/Icons/CameraIcon.vue' import DealsIcon from '@/components/Icons/DealsIcon.vue' import ContactsIcon from '@/components/Icons/ContactsIcon.vue' @@ -243,10 +212,9 @@ const props = defineProps({ }) const { brand } = getSettings() -const { getUser, isManager } = usersStore() +const { getUser } = usersStore() const { $dialog } = globalStore() const { getDealStatus } = statusesStore() -const showSidePanelModal = ref(false) const showQuickEntryModal = ref(false) const route = useRoute() @@ -367,43 +335,42 @@ const showAddressModal = ref(false) const _organization = ref({}) const _address = ref({}) -const fieldsLayout = createResource({ - url: 'crm.api.doc.get_sidebar_fields', - cache: ['fieldsLayout', props.organizationId], - params: { doctype: 'CRM Organization', name: props.organizationId }, +const sections = createResource({ + url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_sidepanel_sections', + cache: ['sidePanelSections', 'CRM Organization'], + params: { doctype: 'CRM Organization' }, auto: true, - transform: (data) => getParsedFields(data), + transform: (data) => getParsedSections(data), }) -function getParsedFields(data) { - return data.map((section) => { - return { - ...section, - fields: computed(() => - section.fields.map((field) => { - if (field.name === 'address') { - return { - ...field, - create: (value, close) => { - _organization.value.address = value - _address.value = {} - showAddressModal.value = true - close() - }, - edit: async (addr) => { - _address.value = await call('frappe.client.get', { - doctype: 'Address', - name: addr, - }) - showAddressModal.value = true - }, - } - } else { - return field +function getParsedSections(_sections) { + return _sections.map((section) => { + section.columns = section.columns.map((column) => { + column.fields = column.fields.map((field) => { + if (field.fieldname === 'address') { + return { + ...field, + create: (value, close) => { + _organization.value.address = value + _address.value = {} + showAddressModal.value = true + close() + }, + edit: async (addr) => { + _address.value = await call('frappe.client.get', { + doctype: 'Address', + name: addr, + }) + showAddressModal.value = true + }, } - }), - ), - } + } else { + return field + } + }) + return column + }) + return section }) } diff --git a/frontend/src/pages/Organizations.vue b/frontend/src/pages/Organizations.vue index f58bb3f8..282411b2 100644 --- a/frontend/src/pages/Organizations.vue +++ b/frontend/src/pages/Organizations.vue @@ -60,15 +60,16 @@ + diff --git a/frontend/src/stores/meta.js b/frontend/src/stores/meta.js index 07b4d149..092c948a 100644 --- a/frontend/src/stores/meta.js +++ b/frontend/src/stores/meta.js @@ -75,6 +75,16 @@ export function getMeta(doctype) { value: option, } }) + + if (f.options[0]?.value !== '') { + f.options.unshift({ + label: '', + value: '', + }) + } + } + if (f.fieldtype === 'Link' && f.options == 'User') { + f.fieldtype = 'User' } return f }) diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js index c0306e4b..899b2607 100644 --- a/frontend/src/utils/index.js +++ b/frontend/src/utils/index.js @@ -306,7 +306,7 @@ export function isImage(extention) { ) } -export function getRandom(len) { +export function getRandom(len=4) { let text = '' const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' diff --git a/frontend/src/utils/numberFormat.js b/frontend/src/utils/numberFormat.js index 7ee2128f..b0fb3845 100644 --- a/frontend/src/utils/numberFormat.js +++ b/frontend/src/utils/numberFormat.js @@ -169,18 +169,18 @@ export function formatCurrency(value, format, currency = 'USD', precision = 2) { } // If you change anything below, it's going to hurt a company in UAE, a bit. - if (precision > 2) { - let parts = cstr(value).split('.') // should be minimum 2, comes from the DB - let decimals = parts.length > 1 ? parts[1] : '' // parts.length == 2 ??? + // if (precision > 2) { + // let parts = cstr(value).split('.') // should be minimum 2, comes from the DB + // let decimals = parts.length > 1 ? parts[1] : '' // parts.length == 2 ??? - if (decimals.length < 3 || decimals.length < precision) { - const fraction = 100 + // if (decimals.length < 3 || decimals.length < precision) { + // const fraction = 100 - if (decimals.length < cstr(fraction).length) { - precision = cstr(fraction).length - 1 - } - } - } + // if (decimals.length < cstr(fraction).length) { + // precision = cstr(fraction).length - 1 + // } + // } + // } format = getNumberFormat(format) let symbol = getCurrencySymbol(currency)