From 1450163379c58a57d60aa12de98fa04f50c16166 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 23 Dec 2024 16:04:18 +0530 Subject: [PATCH 01/24] feat: added grid (child table) control --- frontend/src/components/Controls/Grid.vue | 268 ++++++++++++++++++++++ frontend/src/types/controls.ts | 49 ++++ frontend/src/utils/index.js | 11 + 3 files changed, 328 insertions(+) create mode 100644 frontend/src/components/Controls/Grid.vue create mode 100644 frontend/src/types/controls.ts diff --git a/frontend/src/components/Controls/Grid.vue b/frontend/src/components/Controls/Grid.vue new file mode 100644 index 00000000..c697b3be --- /dev/null +++ b/frontend/src/components/Controls/Grid.vue @@ -0,0 +1,268 @@ + + + + + diff --git a/frontend/src/types/controls.ts b/frontend/src/types/controls.ts new file mode 100644 index 00000000..9d27299e --- /dev/null +++ b/frontend/src/types/controls.ts @@ -0,0 +1,49 @@ +export type FieldTypes = + | 'Data' + | 'Int' + | 'Float' + | 'Currency' + | 'Check' + | 'Text' + | 'Small Text' + | 'Long Text' + | 'Code' + | 'Text Editor' + | 'Date' + | 'Datetime' + | 'Time' + | 'HTML' + | 'Image' + | 'Attach' + | 'Select' + | 'Read Only' + | 'Section Break' + | 'Column Break' + | 'Table' + | 'Button' + | 'Link' + | 'Dynamic Link' + | 'Password' + | 'Signature' + | 'Color' + | 'Barcode' + | 'Geolocation' + | 'Duration' + | 'Percent' + | 'Rating' + | 'Icon' + +// Grid / Child Table +export interface GridColumn { + label: string + fieldname: string + fieldtype: FieldTypes + options?: string | string[] + width?: number + onChange?: (value: string, index: number) => void +} + +export interface GridRow { + name: string + [fieldname: string]: string | number | boolean +} diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js index 557e8588..c0306e4b 100644 --- a/frontend/src/utils/index.js +++ b/frontend/src/utils/index.js @@ -305,3 +305,14 @@ export function isImage(extention) { extention.toLowerCase(), ) } + +export function getRandom(len) { + let text = '' + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + + Array.from({ length: len }).forEach(() => { + text += possible.charAt(Math.floor(Math.random() * possible.length)) + }) + + return text +} From ed11fb48cb5433c046b1c3278dcdacae19f7a0aa Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 23 Dec 2024 16:10:25 +0530 Subject: [PATCH 02/24] fix: get child table meta --- crm/api/doc.py | 3 + crm/fcrm/doctype/crm_deal/crm_deal.py | 109 ++++++++++++--------- crm/fcrm/doctype/crm_lead/crm_lead.py | 131 +++++++++++++++++--------- 3 files changed, 157 insertions(+), 86 deletions(-) diff --git a/crm/api/doc.py b/crm/api/doc.py index b7e3e463..266db1cc 100644 --- a/crm/api/doc.py +++ b/crm/api/doc.py @@ -557,6 +557,9 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False): fields_meta = {} for field in fields: fields_meta[field.get("fieldname")] = field + if field.get("fieldtype") == "Table": + _fields = frappe.get_meta(field.get("options")).fields + fields_meta[field.get("fieldname")] = {"df": field, "fields": _fields} return fields_meta diff --git a/crm/fcrm/doctype/crm_deal/crm_deal.py b/crm/fcrm/doctype/crm_deal/crm_deal.py index 04bf74fd..beb63cbd 100644 --- a/crm/fcrm/doctype/crm_deal/crm_deal.py +++ b/crm/fcrm/doctype/crm_deal/crm_deal.py @@ -1,6 +1,5 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -import json import frappe from frappe import _ @@ -8,7 +7,9 @@ from frappe.desk.form.assign_to import add as assign from frappe.model.document import Document from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla -from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import add_status_change_log +from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import ( + add_status_change_log, +) class CRMDeal(Document): @@ -94,19 +95,26 @@ class CRMDeal(Document): shared_with = [d.user for d in docshares] + [agent] for user in shared_with: - if user == agent and not frappe.db.exists("DocShare", {"user": agent, "share_name": self.name, "share_doctype": self.doctype}): + if user == agent and not frappe.db.exists( + "DocShare", + {"user": agent, "share_name": self.name, "share_doctype": self.doctype}, + ): frappe.share.add_docshare( - self.doctype, self.name, agent, write=1, flags={"ignore_share_permission": True} + self.doctype, + self.name, + agent, + write=1, + flags={"ignore_share_permission": True}, ) elif user != agent: frappe.share.remove(self.doctype, self.name, user) - def set_sla(self): """ Find an SLA to apply to the deal. """ - if self.sla: return + if self.sla: + return sla = get_sla(self) if not sla: @@ -129,48 +137,48 @@ class CRMDeal(Document): def default_list_data(): columns = [ { - 'label': 'Organization', - 'type': 'Link', - 'key': 'organization', - 'options': 'CRM Organization', - 'width': '11rem', + "label": "Organization", + "type": "Link", + "key": "organization", + "options": "CRM Organization", + "width": "11rem", }, { - 'label': 'Amount', - 'type': 'Currency', - 'key': 'annual_revenue', - 'align': 'right', - 'width': '9rem', + "label": "Annual Revenue", + "type": "Currency", + "key": "annual_revenue", + "align": "right", + "width": "9rem", }, { - 'label': 'Status', - 'type': 'Select', - 'key': 'status', - 'width': '10rem', + "label": "Status", + "type": "Select", + "key": "status", + "width": "10rem", }, { - 'label': 'Email', - 'type': 'Data', - 'key': 'email', - 'width': '12rem', + "label": "Email", + "type": "Data", + "key": "email", + "width": "12rem", }, { - 'label': 'Mobile No', - 'type': 'Data', - 'key': 'mobile_no', - 'width': '11rem', + "label": "Mobile No", + "type": "Data", + "key": "mobile_no", + "width": "11rem", }, { - 'label': 'Assigned To', - 'type': 'Text', - 'key': '_assign', - 'width': '10rem', + "label": "Assigned To", + "type": "Text", + "key": "_assign", + "width": "10rem", }, { - 'label': 'Last Modified', - 'type': 'Datetime', - 'key': 'modified', - 'width': '8rem', + "label": "Last Modified", + "type": "Datetime", + "key": "modified", + "width": "8rem", }, ] rows = [ @@ -189,16 +197,17 @@ class CRMDeal(Document): "modified", "_assign", ] - return {'columns': columns, 'rows': rows} + 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"]' + "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): @@ -209,6 +218,7 @@ def add_contact(deal, contact): deal.save() return True + @frappe.whitelist() def remove_contact(deal, contact): if not frappe.has_permission("CRM Deal", "write", deal): @@ -219,6 +229,7 @@ def remove_contact(deal, contact): deal.save() return True + @frappe.whitelist() def set_primary_contact(deal, contact): if not frappe.has_permission("CRM Deal", "write", deal): @@ -229,11 +240,14 @@ def set_primary_contact(deal, contact): deal.save() return True + def create_organization(doc): if not doc.get("organization_name"): return - existing_organization = frappe.db.exists("CRM Organization", {"organization_name": doc.get("organization_name")}) + existing_organization = frappe.db.exists( + "CRM Organization", {"organization_name": doc.get("organization_name")} + ) if existing_organization: return existing_organization @@ -250,6 +264,7 @@ def create_organization(doc): organization.insert(ignore_permissions=True) return organization.name + def contact_exists(doc): email_exist = frappe.db.exists("Contact Email", {"email_id": doc.get("email")}) mobile_exist = frappe.db.exists("Contact Phone", {"phone": doc.get("mobile_no")}) @@ -262,6 +277,7 @@ def contact_exists(doc): return False + def create_contact(doc): existing_contact = contact_exists(doc) if existing_contact: @@ -288,18 +304,23 @@ def create_contact(doc): return contact.name + @frappe.whitelist() def create_deal(args): deal = frappe.new_doc("CRM Deal") contact = args.get("contact") - if not contact and (args.get("first_name") or args.get("last_name") or args.get("email") or args.get("mobile_no")): + if not contact and ( + args.get("first_name") or args.get("last_name") or args.get("email") or args.get("mobile_no") + ): contact = create_contact(args) - deal.update({ - "organization": args.get("organization") or create_organization(args), - "contacts": [{"contact": contact, "is_primary": 1}] if contact else [], - }) + deal.update( + { + "organization": args.get("organization") or create_organization(args), + "contacts": [{"contact": contact, "is_primary": 1}] if contact else [], + } + ) args.pop("organization", None) diff --git a/crm/fcrm/doctype/crm_lead/crm_lead.py b/crm/fcrm/doctype/crm_lead/crm_lead.py index 632080a9..e81cc566 100644 --- a/crm/fcrm/doctype/crm_lead/crm_lead.py +++ b/crm/fcrm/doctype/crm_lead/crm_lead.py @@ -1,15 +1,16 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -import json import frappe from frappe import _ from frappe.desk.form.assign_to import add as assign from frappe.model.document import Document - from frappe.utils import has_gravatar, validate_email_address + from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla -from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import add_status_change_log +from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import ( + add_status_change_log, +) class CRMLead(Document): @@ -37,7 +38,15 @@ class CRMLead(Document): def set_full_name(self): if self.first_name: self.lead_name = " ".join( - filter(None, [self.salutation, self.first_name, self.middle_name, self.last_name]) + filter( + None, + [ + self.salutation, + self.first_name, + self.middle_name, + self.last_name, + ], + ) ) def set_lead_name(self): @@ -92,9 +101,16 @@ class CRMLead(Document): shared_with = [d.user for d in docshares] + [agent] for user in shared_with: - if user == agent and not frappe.db.exists("DocShare", {"user": agent, "share_name": self.name, "share_doctype": self.doctype}): + if user == agent and not frappe.db.exists( + "DocShare", + {"user": agent, "share_name": self.name, "share_doctype": self.doctype}, + ): frappe.share.add_docshare( - self.doctype, self.name, agent, write=1, flags={"ignore_share_permission": True} + self.doctype, + self.name, + agent, + write=1, + flags={"ignore_share_permission": True}, ) elif user != agent: frappe.share.remove(self.doctype, self.name, user) @@ -188,8 +204,36 @@ class CRMLead(Document): "lead_owner": "deal_owner", } - restricted_fieldtypes = ["Tab Break", "Section Break", "Column Break", "HTML", "Button", "Attach", "Table"] - restricted_map_fields = ["name", "naming_series", "creation", "owner", "modified", "modified_by", "idx", "docstatus", "status", "email", "mobile_no", "phone", "sla", "sla_status", "response_by", "first_response_time", "first_responded_on", "communication_status", "sla_creation"] + restricted_fieldtypes = [ + "Tab Break", + "Section Break", + "Column Break", + "HTML", + "Button", + "Attach", + "Table", + ] + restricted_map_fields = [ + "name", + "naming_series", + "creation", + "owner", + "modified", + "modified_by", + "idx", + "docstatus", + "status", + "email", + "mobile_no", + "phone", + "sla", + "sla_status", + "response_by", + "first_response_time", + "first_responded_on", + "communication_status", + "sla_creation", + ] for field in self.meta.fields: if field.fieldtype in restricted_fieldtypes: @@ -222,7 +266,7 @@ class CRMLead(Document): "sla_status": self.sla_status, "communication_status": self.communication_status, "first_response_time": self.first_response_time, - "first_responded_on": self.first_responded_on + "first_responded_on": self.first_responded_on, } ) @@ -233,7 +277,8 @@ class CRMLead(Document): """ Find an SLA to apply to the lead. """ - if self.sla: return + if self.sla: + return sla = get_sla(self) if not sla: @@ -263,47 +308,47 @@ class CRMLead(Document): def default_list_data(): columns = [ { - 'label': 'Name', - 'type': 'Data', - 'key': 'lead_name', - 'width': '12rem', + "label": "Name", + "type": "Data", + "key": "lead_name", + "width": "12rem", }, { - 'label': 'Organization', - 'type': 'Link', - 'key': 'organization', - 'options': 'CRM Organization', - 'width': '10rem', + "label": "Organization", + "type": "Link", + "key": "organization", + "options": "CRM Organization", + "width": "10rem", }, { - 'label': 'Status', - 'type': 'Select', - 'key': 'status', - 'width': '8rem', + "label": "Status", + "type": "Select", + "key": "status", + "width": "8rem", }, { - 'label': 'Email', - 'type': 'Data', - 'key': 'email', - 'width': '12rem', + "label": "Email", + "type": "Data", + "key": "email", + "width": "12rem", }, { - 'label': 'Mobile No', - 'type': 'Data', - 'key': 'mobile_no', - 'width': '11rem', + "label": "Mobile No", + "type": "Data", + "key": "mobile_no", + "width": "11rem", }, { - 'label': 'Assigned To', - 'type': 'Text', - 'key': '_assign', - 'width': '10rem', + "label": "Assigned To", + "type": "Text", + "key": "_assign", + "width": "10rem", }, { - 'label': 'Last Modified', - 'type': 'Datetime', - 'key': 'modified', - 'width': '8rem', + "label": "Last Modified", + "type": "Datetime", + "key": "modified", + "width": "8rem", }, ] rows = [ @@ -323,20 +368,22 @@ class CRMLead(Document): "_assign", "image", ] - return {'columns': columns, 'rows': rows} + 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"]' + "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): + if not (doc and doc.flags.get("ignore_permissions")) and not frappe.has_permission( + "CRM Lead", "write", lead + ): frappe.throw(_("Not allowed to convert Lead to Deal"), frappe.PermissionError) lead = frappe.get_cached_doc("CRM Lead", lead) From 7b86773b7343bca8b755d324b300e499b42bf9bf Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 23 Dec 2024 17:21:05 +0530 Subject: [PATCH 03/24] fix: show grid fields, update onchange of grid cell --- .../src/components/Activities/Activities.vue | 6 ++- .../src/components/Activities/DataFields.vue | 39 +++++++++++++++++++ frontend/src/components/FieldLayout.vue | 9 ++++- 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Activities/Activities.vue b/frontend/src/components/Activities/Activities.vue index baf28de0..169ef24d 100644 --- a/frontend/src/components/Activities/Activities.vue +++ b/frontend/src/components/Activities/Activities.vue @@ -365,7 +365,11 @@
- +
parseTabs(_tabs), }) +function parseTabs(_tabs) { + _tabs.forEach((tab) => { + tab.sections.forEach((section) => { + section.fields.forEach((field) => { + if (field.type === 'Table') { + let name = props.meta[field.name].df.fieldname + let fields = props.meta[field.name].fields + field.fields = fields.map((field) => { + return { + ...getFieldObj(field), + onChange: (value, index) => { + data.doc[name][index][field.fieldname] = value + }, + } + }) + field.gridFields = field.fields.filter((field) => field.in_list_view) + } + }) + }) + }) + + return _tabs +} + +function getFieldObj(field) { + return { + label: field.label, + fieldname: field.fieldname, + fieldtype: field.fieldtype, + options: field.options, + in_list_view: field.in_list_view, + } +} + function saveChanges() { data.save.submit() } diff --git a/frontend/src/components/FieldLayout.vue b/frontend/src/components/FieldLayout.vue index 0585e42c..0b342126 100644 --- a/frontend/src/components/FieldLayout.vue +++ b/frontend/src/components/FieldLayout.vue @@ -76,6 +76,12 @@ v-model="data[field.name]" :disabled="true" /> + Date: Mon, 23 Dec 2024 19:25:40 +0530 Subject: [PATCH 04/24] fix: render more fieldtypes and show all field in modal --- .../src/components/Activities/DataFields.vue | 23 +++- frontend/src/components/Controls/Grid.vue | 121 +++++++++++++++--- frontend/src/components/FieldLayout.vue | 4 +- 3 files changed, 121 insertions(+), 27 deletions(-) diff --git a/frontend/src/components/Activities/DataFields.vue b/frontend/src/components/Activities/DataFields.vue index 6edc989c..b4b58ffb 100644 --- a/frontend/src/components/Activities/DataFields.vue +++ b/frontend/src/components/Activities/DataFields.vue @@ -119,7 +119,7 @@ function parseTabs(_tabs) { if (field.type === 'Table') { let name = props.meta[field.name].df.fieldname let fields = props.meta[field.name].fields - field.fields = fields.map((field) => { + let _fields = fields.map((field) => { return { ...getFieldObj(field), onChange: (value, index) => { @@ -127,7 +127,14 @@ function parseTabs(_tabs) { }, } }) - field.gridFields = field.fields.filter((field) => field.in_list_view) + + field.fields = [ + { + no_tabs: true, + sections: [{ columns: 3, hideLabel: true, fields: _fields }], + }, + ] + field.gridFields = _fields.filter((field) => field.in_list_view) } }) }) @@ -137,10 +144,18 @@ function parseTabs(_tabs) { } function getFieldObj(field) { + if (field.fieldtype === 'Select' && typeof field.options === 'string') { + field.options = field.options.split('\n').map((option) => { + return { + label: option, + value: option, + } + }) + } return { label: field.label, - fieldname: field.fieldname, - fieldtype: field.fieldtype, + name: field.fieldname, + type: field.fieldtype, options: field.options, in_list_view: field.in_list_view, } diff --git a/frontend/src/components/Controls/Grid.vue b/frontend/src/components/Controls/Grid.vue index c697b3be..04e0e5cf 100644 --- a/frontend/src/components/Controls/Grid.vue +++ b/frontend/src/components/Controls/Grid.vue @@ -21,7 +21,7 @@
{{ field.label }}
@@ -52,26 +52,26 @@
+ + + + +
@@ -130,28 +202,35 @@ v-else class="flex flex-col items-center rounded p-5 text-sm text-gray-600" > - No Data + __("No Data")
diff --git a/frontend/src/components/Controls/GridRowModal.vue b/frontend/src/components/Controls/GridRowModal.vue new file mode 100644 index 00000000..69351f22 --- /dev/null +++ b/frontend/src/components/Controls/GridRowModal.vue @@ -0,0 +1,62 @@ + + + diff --git a/frontend/src/components/FieldLayout.vue b/frontend/src/components/FieldLayout.vue index 61f4630d..95408634 100644 --- a/frontend/src/components/FieldLayout.vue +++ b/frontend/src/components/FieldLayout.vue @@ -81,6 +81,7 @@ v-model="data[field.name]" :fields="field.fields" :gridFields="field.gridFields" + :doctype="field.options" /> Date: Sun, 29 Dec 2024 13:43:54 +0530 Subject: [PATCH 18/24] refactor: change event is not needed in grid row fields --- .../src/components/Activities/DataFields.vue | 23 +---- frontend/src/components/Controls/Grid.vue | 89 ++++--------------- frontend/src/components/FieldLayout.vue | 5 +- 3 files changed, 24 insertions(+), 93 deletions(-) diff --git a/frontend/src/components/Activities/DataFields.vue b/frontend/src/components/Activities/DataFields.vue index b4b58ffb..cca10e04 100644 --- a/frontend/src/components/Activities/DataFields.vue +++ b/frontend/src/components/Activities/DataFields.vue @@ -90,7 +90,7 @@ const data = createDocumentResource({ createToast({ title: 'Data Updated', icon: 'check', - iconClasses: 'text-green-600', + iconClasses: 'text-ink-green-3', }) }, onError: (err) => { @@ -117,24 +117,9 @@ function parseTabs(_tabs) { tab.sections.forEach((section) => { section.fields.forEach((field) => { if (field.type === 'Table') { - let name = props.meta[field.name].df.fieldname - let fields = props.meta[field.name].fields - let _fields = fields.map((field) => { - return { - ...getFieldObj(field), - onChange: (value, index) => { - data.doc[name][index][field.fieldname] = value - }, - } - }) - - field.fields = [ - { - no_tabs: true, - sections: [{ columns: 3, hideLabel: true, fields: _fields }], - }, - ] - field.gridFields = _fields.filter((field) => field.in_list_view) + field.fields = props.meta[field.name].fields + .filter((field) => field.in_list_view) + .map((field) => getFieldObj(field)) } }) }) diff --git a/frontend/src/components/Controls/Grid.vue b/frontend/src/components/Controls/Grid.vue index 0033d5b1..caf3e3e3 100644 --- a/frontend/src/components/Controls/Grid.vue +++ b/frontend/src/components/Controls/Grid.vue @@ -28,9 +28,9 @@ :style="{ gridTemplateColumns: gridTemplateColumns }" >
@@ -65,21 +65,17 @@ :style="{ gridTemplateColumns: gridTemplateColumns }" >
@@ -218,7 +166,7 @@ -
+
- +
parseTabs(_tabs), }) -function parseTabs(_tabs) { - _tabs.forEach((tab) => { - tab.sections.forEach((section) => { - section.fields.forEach((field) => { - if (field.type === 'Table') { - field.fields = props.meta[field.name].fields - .filter((field) => field.in_list_view) - .map((field) => getFieldObj(field)) - } - }) - }) - }) - - return _tabs -} - -function getFieldObj(field) { - if (field.fieldtype === 'Select' && typeof field.options === 'string') { - field.options = field.options.split('\n').map((option) => { - return { - label: option, - value: option, - } - }) - } - return { - label: field.label, - name: field.fieldname, - type: field.fieldtype, - options: field.options, - in_list_view: field.in_list_view, - } -} - function saveChanges() { data.save.submit() } diff --git a/frontend/src/components/Controls/Grid.vue b/frontend/src/components/Controls/Grid.vue index caf3e3e3..0d602d2d 100644 --- a/frontend/src/components/Controls/Grid.vue +++ b/frontend/src/components/Controls/Grid.vue @@ -108,6 +108,7 @@ field.type, ) " + rows="1" type="textarea" variant="outline" v-model="row[field.name]" @@ -138,7 +139,7 @@