diff --git a/crm/api/doc.py b/crm/api/doc.py index 0c535e99..0159c663 100644 --- a/crm/api/doc.py +++ b/crm/api/doc.py @@ -66,12 +66,12 @@ def get_filterable_fields(doctype: str): # append DocFields DocField = frappe.qb.DocType("DocField") - doc_fields = get_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fields) + doc_fields = get_doctype_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fields) res.extend(doc_fields) # append Custom Fields CustomField = frappe.qb.DocType("Custom Field") - custom_fields = get_fields_meta(CustomField, doctype, allowed_fieldtypes, restricted_fields) + custom_fields = get_doctype_fields_meta(CustomField, doctype, allowed_fieldtypes, restricted_fields) res.extend(custom_fields) # append standard fields (getting error when using frappe.model.std_fields) @@ -157,44 +157,7 @@ def get_group_by_fields(doctype: str): return fields -@frappe.whitelist() -def get_quick_entry_fields(doctype: str): - sections = [] - if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": "Quick Entry"}): - layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": "Quick Entry"}) - else: - return [] - - if layout.layout: - sections = json.loads(layout.layout) - - allowed_fields = [] - for section in sections: - allowed_fields.extend(section.get("fields")) - - fields = frappe.get_meta(doctype).fields - fields = [field for field in fields if field.fieldname in allowed_fields] - - for section in sections: - for field in section.get("fields"): - field = next((f for f in fields if f.fieldname == field), None) - if field: - if field.fieldtype == "Select": - field.options = field.options.split("\n") - field.options = [{"label": _(option), "value": option} for option in field.options] - field.options.insert(0, {"label": "", "value": ""}) - field = { - "label": _(field.label), - "name": field.fieldname, - "type": field.fieldtype, - "options": field.options, - "mandatory": field.reqd, - } - section["fields"][section.get("fields").index(field["name"])] = field - - return sections or [] - -def get_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fields): +def get_doctype_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fields): parent = "parent" if DocField._table_name == "tabDocField" else "dt" return ( frappe.qb.from_(DocField) @@ -412,9 +375,43 @@ def get_list_data( "list_script": get_form_script(doctype, "List"), } - -def get_doctype_fields(doctype, name): +@frappe.whitelist() +def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False): not_allowed_fieldtypes = [ + "Tab Break", + "Section Break", + "Column Break", + ] + + if restricted_fieldtypes: + restricted_fieldtypes = frappe.parse_json(restricted_fieldtypes) + not_allowed_fieldtypes += restricted_fieldtypes + + fields = frappe.get_meta(doctype).fields + fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes] + + if as_array: + return fields + + fields_meta = {} + for field in fields: + fields_meta[field.fieldname] = field + + 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", ] @@ -422,56 +419,31 @@ def get_doctype_fields(doctype, name): fields = frappe.get_meta(doctype).fields fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes] - sections = {} - section_fields = [] - last_section = None 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 field in fields: - if field.fieldtype == "Tab Break" and last_section: - sections[last_section]["fields"] = section_fields - last_section = None - if field.read_only: - section_fields = [] - continue - if field.fieldtype == "Tab Break": - if field.read_only: - section_fields = [] - continue - section_fields = [] - last_section = field.fieldname - sections[field.fieldname] = { - "label": field.label, - "name": field.fieldname, - "opened": True, - "fields": [], - } - else: - if field.permlevel > 0: - field_has_write_access = field.permlevel in has_write_access_to_permlevels - field_has_read_access = field.permlevel in has_read_access_to_permlevels - 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 - section_fields.append(get_field_obj(field)) + 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) - section_fields = [] - for section in sections: - section_fields.append(sections[section]) - - fields = [field for field in fields if field.fieldtype not in "Tab Break"] fields_meta = {} for field in fields: fields_meta[field.fieldname] = field - return section_fields, fields_meta - + return layout def get_field_obj(field): obj = { diff --git a/crm/fcrm/doctype/crm_deal/api.py b/crm/fcrm/doctype/crm_deal/api.py index cb757b59..76a764ae 100644 --- a/crm/fcrm/doctype/crm_deal/api.py +++ b/crm/fcrm/doctype/crm_deal/api.py @@ -1,7 +1,7 @@ import frappe from frappe import _ -from crm.api.doc import get_doctype_fields, get_assigned_users +from crm.api.doc import get_fields_meta, get_assigned_users from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script @frappe.whitelist() @@ -27,8 +27,8 @@ def get_deal(name): fields=["contact", "is_primary"], ) - deal["doctype_fields"], deal["all_fields"] = get_doctype_fields("CRM Deal", name) deal["doctype"] = "CRM Deal" + deal["fields_meta"] = get_fields_meta("CRM Deal") deal["_form_script"] = get_form_script('CRM Deal') deal["_assign"] = get_assigned_users("CRM Deal", deal.name, deal.owner) return deal 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 c780557a..00012770 100644 --- a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.json +++ b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.json @@ -19,8 +19,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Document Type", - "options": "DocType", - "unique": 1 + "options": "DocType" }, { "fieldname": "type", @@ -47,7 +46,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-06-07 17:01:20.250697", + "modified": "2024-06-13 15:10:01.612851", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Fields Layout", 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 36d8bb72..3129c478 100644 --- a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py +++ b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py @@ -1,9 +1,67 @@ # Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -# import frappe +import json +import frappe +from frappe import _ from frappe.model.document import Document class CRMFieldsLayout(Document): pass + +@frappe.whitelist() +def get_fields_layout(doctype: str, type: str): + sections = [] + if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": type}): + layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": type}) + else: + return [] + + if layout.layout: + sections = json.loads(layout.layout) + + allowed_fields = [] + for section in sections: + if not section.get("fields"): + continue + allowed_fields.extend(section.get("fields")) + + fields = frappe.get_meta(doctype).fields + fields = [field for field in fields if field.fieldname in allowed_fields] + + for section in 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: + if field.fieldtype == "Select": + field.options = field.options.split("\n") + field.options = [{"label": _(option), "value": option} for option in field.options] + field.options.insert(0, {"label": "", "value": ""}) + field = { + "label": _(field.label), + "name": field.fieldname, + "type": field.fieldtype, + "options": field.options, + "mandatory": field.reqd, + } + section["fields"][section.get("fields").index(field["name"])] = field + + return sections or [] + + +@frappe.whitelist() +def save_fields_layout(doctype: str, type: str, layout: str): + if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": type}): + doc = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": type}) + else: + doc = frappe.new_doc("CRM Fields Layout") + + doc.update({ + "dt": doctype, + "type": type, + "layout": layout, + }) + doc.save(ignore_permissions=True) + + return doc.layout diff --git a/crm/fcrm/doctype/crm_lead/api.py b/crm/fcrm/doctype/crm_lead/api.py index b4dca9f7..e1bb4a4f 100644 --- a/crm/fcrm/doctype/crm_lead/api.py +++ b/crm/fcrm/doctype/crm_lead/api.py @@ -1,7 +1,7 @@ import frappe from frappe import _ -from crm.api.doc import get_doctype_fields, get_assigned_users +from crm.api.doc import get_fields_meta, get_assigned_users from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script @frappe.whitelist() @@ -15,8 +15,8 @@ def get_lead(name): frappe.throw(_("Lead not found"), frappe.DoesNotExistError) lead = lead.pop() - lead["doctype_fields"], lead["all_fields"] = get_doctype_fields("CRM Lead", name) lead["doctype"] = "CRM Lead" + lead["fields_meta"] = get_fields_meta("CRM Lead") lead["_form_script"] = get_form_script('CRM Lead') lead["_assign"] = get_assigned_users("CRM Lead", lead.name, lead.owner) return lead diff --git a/crm/patches.txt b/crm/patches.txt index c0204ec5..32c4485a 100644 --- a/crm/patches.txt +++ b/crm/patches.txt @@ -6,4 +6,5 @@ crm.patches.v1_0.move_crm_note_data_to_fcrm_note [post_model_sync] # Patches added in this section will be executed after doctypes are migrated crm.patches.v1_0.create_email_template_custom_fields -crm.patches.v1_0.create_default_fields_layout \ No newline at end of file +crm.patches.v1_0.create_default_fields_layout +crm.patches.v1_0.create_default_sidebar_fields_layout \ No newline at end of file diff --git a/crm/patches/v1_0/create_default_sidebar_fields_layout.py b/crm/patches/v1_0/create_default_sidebar_fields_layout.py new file mode 100644 index 00000000..04b65fee --- /dev/null +++ b/crm/patches/v1_0/create_default_sidebar_fields_layout.py @@ -0,0 +1,63 @@ +import json +import frappe + +def execute(): + if not frappe.db.exists("CRM Fields Layout", {"dt": "CRM Lead", "type": "Side Panel"}): + create_doctype_fields_layout("CRM Lead") + + if not frappe.db.exists("CRM Fields Layout", {"dt": "CRM Deal", "type": "Side Panel"}): + create_doctype_fields_layout("CRM Deal") + +def create_doctype_fields_layout(doctype): + not_allowed_fieldtypes = [ + "Section Break", + "Column Break", + ] + + fields = frappe.get_meta(doctype).fields + fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes] + + sections = {} + section_fields = [] + last_section = None + + for field in fields: + if field.fieldtype == "Tab Break" and last_section: + sections[last_section]["fields"] = section_fields + last_section = None + if field.read_only: + section_fields = [] + continue + if field.fieldtype == "Tab Break": + if field.read_only: + section_fields = [] + continue + section_fields = [] + last_section = field.fieldname + sections[field.fieldname] = { + "label": field.label, + "name": field.fieldname, + "opened": True, + "fields": [], + } + if field.fieldname == "contacts_tab": + sections[field.fieldname]["editable"] = False + sections[field.fieldname]["contacts"] = [] + else: + section_fields.append(field.fieldname) + + section_fields = [] + for section in sections: + if section == "contacts_tab": + sections[section]["name"] = "contacts_section" + sections[section].pop("fields", None) + section_fields.append(sections[section]) + + frappe.get_doc({ + "doctype": "CRM Fields Layout", + "dt": doctype, + "type": "Side Panel", + "layout": json.dumps(section_fields), + }).insert(ignore_permissions=True) + + return section_fields \ No newline at end of file diff --git a/frontend/src/components/Modals/ContactModal.vue b/frontend/src/components/Modals/ContactModal.vue index 7ec75161..c8e855ca 100644 --- a/frontend/src/components/Modals/ContactModal.vue +++ b/frontend/src/components/Modals/ContactModal.vue @@ -235,9 +235,9 @@ const detailFields = computed(() => { }) const sections = createResource({ - url: 'crm.api.doc.get_quick_entry_fields', + url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout', cache: ['quickEntryFields', 'Contact'], - params: { doctype: 'Contact' }, + params: { doctype: 'Contact', type: 'Quick Entry'}, auto: true, }) diff --git a/frontend/src/components/Modals/DealModal.vue b/frontend/src/components/Modals/DealModal.vue index bf465ddc..08260ef0 100644 --- a/frontend/src/components/Modals/DealModal.vue +++ b/frontend/src/components/Modals/DealModal.vue @@ -77,9 +77,9 @@ const chooseExistingContact = ref(false) const chooseExistingOrganization = ref(false) const sections = createResource({ - url: 'crm.api.doc.get_quick_entry_fields', + url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout', cache: ['quickEntryFields', 'CRM Deal'], - params: { doctype: 'CRM Deal' }, + params: { doctype: 'CRM Deal', type: 'Quick Entry'}, auto: true, transform: (data) => { return data.forEach((section) => { diff --git a/frontend/src/components/Modals/LeadModal.vue b/frontend/src/components/Modals/LeadModal.vue index 54e7566e..d288196d 100644 --- a/frontend/src/components/Modals/LeadModal.vue +++ b/frontend/src/components/Modals/LeadModal.vue @@ -40,9 +40,9 @@ const error = ref(null) const isLeadCreating = ref(false) const sections = createResource({ - url: 'crm.api.doc.get_quick_entry_fields', + url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout', cache: ['quickEntryFields', 'CRM Lead'], - params: { doctype: 'CRM Lead' }, + params: { doctype: 'CRM Lead', type: 'Quick Entry' }, auto: true, transform: (data) => { return data.forEach((section) => { diff --git a/frontend/src/components/Modals/OrganizationModal.vue b/frontend/src/components/Modals/OrganizationModal.vue index 08a7e91a..a051166c 100644 --- a/frontend/src/components/Modals/OrganizationModal.vue +++ b/frontend/src/components/Modals/OrganizationModal.vue @@ -225,9 +225,9 @@ const fields = computed(() => { }) const sections = createResource({ - url: 'crm.api.doc.get_quick_entry_fields', + url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout', cache: ['quickEntryFields', 'CRM Organization'], - params: { doctype: 'CRM Organization' }, + params: { doctype: 'CRM Organization', type: 'Quick Entry'}, auto: true, }) diff --git a/frontend/src/components/Resizer.vue b/frontend/src/components/Resizer.vue index e81df9e7..ebe0029b 100644 --- a/frontend/src/components/Resizer.vue +++ b/frontend/src/components/Resizer.vue @@ -28,6 +28,10 @@ const props = defineProps({ type: String, default: 'left', }, + parent: { + type: Object, + default: null, + }, }) const sidebarResizing = ref(false) @@ -58,6 +62,9 @@ function resize(e) { sidebarWidth.value = props.side == 'left' ? e.clientX : window.innerWidth - e.clientX + let gap = props.parent ? distance() : 0 + sidebarWidth.value = sidebarWidth.value - gap + // snap to props.defaultWidth let range = [props.defaultWidth - 10, props.defaultWidth + 10] if (sidebarWidth.value > range[0] && sidebarWidth.value < range[1]) { @@ -71,4 +78,9 @@ function resize(e) { sidebarWidth.value = props.maxWidth } } +function distance() { + if (!props.parent) return 0 + const rect = props.parent.getBoundingClientRect() + return window.innerWidth - rect[props.side] +} diff --git a/frontend/src/components/SectionFields.vue b/frontend/src/components/SectionFields.vue index 63d43e00..d80868b5 100644 --- a/frontend/src/components/SectionFields.vue +++ b/frontend/src/components/SectionFields.vue @@ -136,7 +136,7 @@ const _fields = computed(() => { let all_fields = [] props.fields?.forEach((field) => { let df = field.all_properties - if (df.depends_on) evaluate_depends_on(df.depends_on, field) + if (df?.depends_on) evaluate_depends_on(df.depends_on, field) all_fields.push(field) }) return all_fields diff --git a/frontend/src/components/Settings/FieldsLayout.vue b/frontend/src/components/Settings/FieldsLayout.vue new file mode 100644 index 00000000..9f6879c7 --- /dev/null +++ b/frontend/src/components/Settings/FieldsLayout.vue @@ -0,0 +1,94 @@ + + diff --git a/frontend/src/components/Settings/ProfileSettings.vue b/frontend/src/components/Settings/ProfileSettings.vue index a2ef0974..34b92f11 100644 --- a/frontend/src/components/Settings/ProfileSettings.vue +++ b/frontend/src/components/Settings/ProfileSettings.vue @@ -1,5 +1,5 @@