From 29132f6f23213cf433a5b400a7292420993516ba Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 1 Jul 2025 12:07:44 +0530 Subject: [PATCH 1/9] fix: add default probabilities in deal status (cherry picked from commit 6d3268a61ecd96710f2c7d1cede33a5cc6f939d6) --- .../crm_deal_status/crm_deal_status.json | 3 ++- crm/install.py | 7 ++++++ crm/patches.txt | 3 ++- .../v1_0/update_deal_status_probabilities.py | 24 +++++++++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 crm/patches/v1_0/update_deal_status_probabilities.py diff --git a/crm/fcrm/doctype/crm_deal_status/crm_deal_status.json b/crm/fcrm/doctype/crm_deal_status/crm_deal_status.json index ae026c74..d9b5f203 100644 --- a/crm/fcrm/doctype/crm_deal_status/crm_deal_status.json +++ b/crm/fcrm/doctype/crm_deal_status/crm_deal_status.json @@ -37,13 +37,14 @@ { "fieldname": "probability", "fieldtype": "Percent", + "in_list_view": 1, "label": "Probability" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2025-06-11 13:00:34.518808", + "modified": "2025-07-01 12:06:42.937440", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Deal Status", diff --git a/crm/install.py b/crm/install.py index 5ffa5a67..ffa84e90 100644 --- a/crm/install.py +++ b/crm/install.py @@ -68,26 +68,32 @@ def add_default_deal_statuses(): statuses = { "Qualification": { "color": "gray", + "probability": 10, "position": 1, }, "Demo/Making": { "color": "orange", + "probability": 25, "position": 2, }, "Proposal/Quotation": { "color": "blue", + "probability": 50, "position": 3, }, "Negotiation": { "color": "yellow", + "probability": 70, "position": 4, }, "Ready to Close": { "color": "purple", + "probability": 90, "position": 5, }, "Won": { "color": "green", + "probability": 100, "position": 6, }, "Lost": { @@ -103,6 +109,7 @@ def add_default_deal_statuses(): doc = frappe.new_doc("CRM Deal Status") doc.deal_status = status doc.color = statuses[status]["color"] + doc.probability = statuses[status]["probability"] doc.position = statuses[status]["position"] doc.insert() diff --git a/crm/patches.txt b/crm/patches.txt index ee56be84..484c73dc 100644 --- a/crm/patches.txt +++ b/crm/patches.txt @@ -12,4 +12,5 @@ crm.patches.v1_0.create_default_sidebar_fields_layout crm.patches.v1_0.update_deal_quick_entry_layout crm.patches.v1_0.update_layouts_to_new_format crm.patches.v1_0.move_twilio_agent_to_telephony_agent -crm.patches.v1_0.create_default_scripts # 13-06-2025 \ No newline at end of file +crm.patches.v1_0.create_default_scripts # 13-06-2025 +crm.patches.v1_0.update_deal_status_probabilities \ No newline at end of file diff --git a/crm/patches/v1_0/update_deal_status_probabilities.py b/crm/patches/v1_0/update_deal_status_probabilities.py new file mode 100644 index 00000000..3460784e --- /dev/null +++ b/crm/patches/v1_0/update_deal_status_probabilities.py @@ -0,0 +1,24 @@ +import frappe + + +def execute(): + deal_statuses = frappe.get_all("CRM Deal Status", fields=["name", "probability", "deal_status"]) + + for status in deal_statuses: + if status.probability is None or status.probability == 0: + if status.deal_status == "Qualification": + probability = 10 + elif status.deal_status == "Demo/Making": + probability = 25 + elif status.deal_status == "Proposal/Quotation": + probability = 50 + elif status.deal_status == "Negotiation": + probability = 70 + elif status.deal_status == "Ready to Close": + probability = 90 + elif status.deal_status == "Won": + probability = 100 + else: + probability = 0 + + frappe.db.set_value("CRM Deal Status", status.name, "probability", probability) From 336ac2ad3465c1503af07d62a9d6ab23de4d5e39 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 1 Jul 2025 12:58:57 +0530 Subject: [PATCH 2/9] fix: prettyDate is not accurate (cherry picked from commit 611f4cde70d7d4d14f823b1875c57bc75fa66c0f) --- frontend/src/utils/index.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js index c337907e..375d5f51 100644 --- a/frontend/src/utils/index.js +++ b/frontend/src/utils/index.js @@ -85,7 +85,7 @@ export function prettyDate(date, mini = false) { let nowDatetime = dayjs().tz(localTimezone || systemTimezone) let diff = nowDatetime.diff(date, 'seconds') - let dayDiff = Math.floor(diff / 86400) + let dayDiff = diff / 86400 if (isNaN(dayDiff)) return '' @@ -93,15 +93,15 @@ export function prettyDate(date, mini = false) { // Return short format of time difference if (dayDiff < 0) { if (Math.abs(dayDiff) < 1) { - if (diff < 60) { + if (Math.abs(diff) < 60) { return __('now') - } else if (diff < 3600) { - return __('in {0} m', [Math.floor(diff / 60)]) - } else if (diff < 86400) { - return __('in {0} h', [Math.floor(diff / 3600)]) + } else if (Math.abs(diff) < 3600) { + return __('in {0} m', [Math.floor(Math.abs(diff) / 60)]) + } else if (Math.abs(diff) < 86400) { + return __('in {0} h', [Math.floor(Math.abs(diff) / 3600)]) } } - if (Math.abs(dayDiff) == 1) { + if (Math.abs(dayDiff) >= 1 && Math.abs(dayDiff) < 1.5) { return __('tomorrow') } else if (Math.abs(dayDiff) < 7) { return __('in {0} d', [Math.abs(dayDiff)]) @@ -112,7 +112,7 @@ export function prettyDate(date, mini = false) { } else { return __('in {0} y', [Math.floor(Math.abs(dayDiff) / 365)]) } - } else if (dayDiff == 0) { + } else if (dayDiff >= 0 && dayDiff < 1) { if (diff < 60) { return __('now') } else if (diff < 3600) { @@ -135,19 +135,19 @@ export function prettyDate(date, mini = false) { // Return long format of time difference if (dayDiff < 0) { if (Math.abs(dayDiff) < 1) { - if (diff < 60) { + if (Math.abs(diff) < 60) { return __('just now') - } else if (diff < 120) { + } else if (Math.abs(diff) < 120) { return __('in 1 minute') - } else if (diff < 3600) { - return __('in {0} minutes', [Math.floor(diff / 60)]) - } else if (diff < 7200) { + } else if (Math.abs(diff) < 3600) { + return __('in {0} minutes', [Math.floor(Math.abs(diff) / 60)]) + } else if (Math.abs(diff) < 7200) { return __('in 1 hour') - } else if (diff < 86400) { - return __('in {0} hours', [Math.floor(diff / 3600)]) + } else if (Math.abs(diff) < 86400) { + return __('in {0} hours', [Math.floor(Math.abs(diff) / 3600)]) } } - if (Math.abs(dayDiff) == 1) { + if (Math.abs(dayDiff) >= 1 && Math.abs(dayDiff) < 1.5) { return __('tomorrow') } else if (Math.abs(dayDiff) < 7) { return __('in {0} days', [Math.abs(dayDiff)]) @@ -160,7 +160,7 @@ export function prettyDate(date, mini = false) { } else { return __('in {0} years', [Math.floor(Math.abs(dayDiff) / 365)]) } - } else if (dayDiff == 0) { + } else if (dayDiff >= 0 && dayDiff < 1) { if (diff < 60) { return __('just now') } else if (diff < 120) { From 2cd08bebb9695982d018e75e8cebe828f2844ea7 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 1 Jul 2025 12:59:53 +0530 Subject: [PATCH 3/9] refactor: moved convert to deal modal into separate component (cherry picked from commit 6320e580ae556df683d50b5145e39039968ad9ad) --- frontend/components.d.ts | 1 + .../components/Modals/ConvertToDealModal.vue | 227 ++++++++++++++++ frontend/src/data/document.js | 3 +- frontend/src/pages/Lead.vue | 243 +----------------- 4 files changed, 243 insertions(+), 231 deletions(-) create mode 100644 frontend/src/components/Modals/ConvertToDealModal.vue diff --git a/frontend/components.d.ts b/frontend/components.d.ts index d4d287e4..984d8e3a 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -56,6 +56,7 @@ declare module 'vue' { ContactsIcon: typeof import('./src/components/Icons/ContactsIcon.vue')['default'] ContactsListView: typeof import('./src/components/ListViews/ContactsListView.vue')['default'] ConvertIcon: typeof import('./src/components/Icons/ConvertIcon.vue')['default'] + ConvertToDealModal: typeof import('./src/components/Modals/ConvertToDealModal.vue')['default'] CountUpTimer: typeof import('./src/components/CountUpTimer.vue')['default'] CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default'] CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default'] diff --git a/frontend/src/components/Modals/ConvertToDealModal.vue b/frontend/src/components/Modals/ConvertToDealModal.vue new file mode 100644 index 00000000..a4a29ac0 --- /dev/null +++ b/frontend/src/components/Modals/ConvertToDealModal.vue @@ -0,0 +1,227 @@ + + diff --git a/frontend/src/data/document.js b/frontend/src/data/document.js index a77174cd..9c15e1d0 100644 --- a/frontend/src/data/document.js +++ b/frontend/src/data/document.js @@ -193,8 +193,7 @@ export function useDocument(doctype, docname) { async function triggerConvertToDeal() { const args = Array.from(arguments) const handler = async function () { - await (this.convertToDeal?.(...args) || - this.on_convert_to_deal?.(...args)) + await (this.convertToDeal?.(...args) || this.convert_to_deal?.(...args)) } await trigger(handler) } diff --git a/frontend/src/pages/Lead.vue b/frontend/src/pages/Lead.vue index 52811b41..1b830319 100644 --- a/frontend/src/pages/Lead.vue +++ b/frontend/src/pages/Lead.vue @@ -216,108 +216,11 @@ :errorTitle="errorTitle" :errorMessage="errorMessage" /> - - - - + :lead="lead.data" + /> (showConvertToDealModal.value = false), - ) - - let _deal = await call('crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', { - lead: lead.data.name, - deal, - existing_contact: existingContact.value, - existing_organization: existingOrganization.value, - }).catch((err) => { - toast.error(__('Error converting to deal: {0}', [err.messages?.[0]])) - }) - if (_deal) { - showConvertToDealModal.value = false - existingContactChecked.value = false - existingOrganizationChecked.value = false - existingContact.value = '' - existingOrganization.value = '' - updateOnboardingStep('convert_lead_to_deal', true, false, () => { - localStorage.setItem('firstDeal' + user, _deal) - }) - capture('convert_lead_to_deal') - router.push({ name: 'Deal', params: { dealId: _deal } }) - } -} - const activities = ref(null) function openEmailBox() { @@ -698,54 +531,6 @@ function openEmailBox() { nextTick(() => (activities.value.emailBox.show = true)) } -const deal = reactive({}) - -const dealStatuses = computed(() => { - let statuses = statusOptions('deal') - if (!deal.status) { - deal.status = statuses[0].value - } - return statuses -}) - -const dealTabs = createResource({ - url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout', - cache: ['RequiredFields', 'CRM Deal'], - params: { doctype: 'CRM Deal', type: 'Required Fields' }, - auto: true, - transform: (_tabs) => { - let hasFields = false - let parsedTabs = _tabs?.forEach((tab) => { - tab.sections?.forEach((section) => { - section.columns?.forEach((column) => { - column.fields?.forEach((field) => { - hasFields = true - if (field.fieldname == 'status') { - field.fieldtype = 'Select' - field.options = dealStatuses.value - field.prefix = getDealStatus(deal.status).color - } - - if (field.fieldtype === 'Table') { - deal[field.fieldname] = [] - } - }) - }) - }) - }) - return hasFields ? parsedTabs : [] - }, -}) - -function openQuickEntryModal() { - showQuickEntryModal.value = true - quickEntryProps.value = { - doctype: 'CRM Deal', - onlyRequired: true, - } - showConvertToDealModal.value = false -} - function reloadAssignees(data) { if (data?.hasOwnProperty('lead_owner')) { assignees.reload() From e3510b0e9cc9f317e8935db2790f0e9a137cad6f Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 1 Jul 2025 13:16:40 +0530 Subject: [PATCH 4/9] fix: added default probability to Lost status (cherry picked from commit 4d3fe722e86a1e91e056418a173ad30f1a1b6c63) --- crm/install.py | 1 + 1 file changed, 1 insertion(+) diff --git a/crm/install.py b/crm/install.py index ffa84e90..9f8e617d 100644 --- a/crm/install.py +++ b/crm/install.py @@ -98,6 +98,7 @@ def add_default_deal_statuses(): }, "Lost": { "color": "red", + "probability": 0, "position": 7, }, } From 47002deed6c2781b4f935fe7f57c2112641131c2 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 1 Jul 2025 15:05:37 +0530 Subject: [PATCH 5/9] fix: made deal value mandatory if forecasting is enabled (cherry picked from commit 4f58aa110a39b431e58d1fd86c2dfe8e41d0034c) --- crm/fcrm/doctype/crm_deal/crm_deal.py | 10 +++++++++- crm/fcrm/doctype/fcrm_settings/fcrm_settings.json | 4 ++-- crm/fcrm/doctype/fcrm_settings/fcrm_settings.py | 12 ++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/crm/fcrm/doctype/crm_deal/crm_deal.py b/crm/fcrm/doctype/crm_deal/crm_deal.py index b7e4ec83..9afab159 100644 --- a/crm/fcrm/doctype/crm_deal/crm_deal.py +++ b/crm/fcrm/doctype/crm_deal/crm_deal.py @@ -24,7 +24,7 @@ class CRMDeal(Document): self.assign_agent(self.deal_owner) if self.has_value_changed("status"): add_status_change_log(self) - self.update_close_date() + self.validate_forcasting_fields() def after_insert(self): if self.deal_owner: @@ -141,6 +141,14 @@ class CRMDeal(Document): if self.status == "Won" and not self.close_date: self.close_date = frappe.utils.nowdate() + def validate_forcasting_fields(self): + self.update_close_date() + if frappe.db.get_single_value("FCRM Settings", "enable_forecasting"): + if not self.deal_value or self.deal_value == 0: + frappe.throw(_("Deal Value is required."), frappe.MandatoryError) + if not self.close_date: + frappe.throw(_("Close Date is required."), frappe.MandatoryError) + @staticmethod def default_list_data(): columns = [ diff --git a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json index de27e245..e679b023 100644 --- a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json +++ b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json @@ -60,7 +60,7 @@ }, { "default": "0", - "description": "It will make deal's \"Expected Closure Date\" mandatory to get accurate forecasting insights", + "description": "It will make deal's \"Close Date\" & \"Deal Value\" mandatory to get accurate forecasting insights", "fieldname": "enable_forecasting", "fieldtype": "Check", "label": "Enable Forecasting" @@ -69,7 +69,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-06-11 19:12:16.762499", + "modified": "2025-07-01 13:20:48.757603", "modified_by": "Administrator", "module": "FCRM", "name": "FCRM Settings", diff --git a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.py b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.py index f22114f1..1460c265 100644 --- a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.py +++ b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.py @@ -39,6 +39,11 @@ class FCRMSettings(Document): "reqd", "close_date", ) + delete_property_setter( + "CRM Deal", + "reqd", + "deal_value", + ) else: make_property_setter( "CRM Deal", @@ -47,6 +52,13 @@ class FCRMSettings(Document): 1 if self.enable_forecasting else 0, "Check", ) + make_property_setter( + "CRM Deal", + "deal_value", + "reqd", + 1 if self.enable_forecasting else 0, + "Check", + ) def get_standard_dropdown_items(): From 08243530e7ff6b82c7d9f6e836a36b39bf03b9bf Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 1 Jul 2025 15:06:02 +0530 Subject: [PATCH 6/9] fix: mandatory error (cherry picked from commit 4c70b1a06bff81aac6baacffe34d624c7e3c4b86) --- frontend/src/components/Modals/AddressModal.vue | 5 ++++- frontend/src/components/Modals/CallLogModal.vue | 5 ++++- frontend/src/data/document.js | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Modals/AddressModal.vue b/frontend/src/components/Modals/AddressModal.vue index 444490c4..c1e9ba20 100644 --- a/frontend/src/components/Modals/AddressModal.vue +++ b/frontend/src/components/Modals/AddressModal.vue @@ -121,7 +121,10 @@ const callBacks = { loading.value = false if (err.exc_type == 'MandatoryError') { const errorMessage = err.messages - .map((msg) => msg.split(': ')[2].trim()) + .map((msg) => { + let arr = msg.split(': ') + return arr[arr.length - 1].trim() + }) .join(', ') error.value = __('These fields are required: {0}', [errorMessage]) return diff --git a/frontend/src/components/Modals/CallLogModal.vue b/frontend/src/components/Modals/CallLogModal.vue index 1c90541f..dab2374c 100644 --- a/frontend/src/components/Modals/CallLogModal.vue +++ b/frontend/src/components/Modals/CallLogModal.vue @@ -124,7 +124,10 @@ const callBacks = { loading.value = false if (err.exc_type == 'MandatoryError') { const errorMessage = err.messages - .map((msg) => msg.split(': ')[2].trim()) + .map((msg) => { + let arr = msg.split(': ') + return arr[arr.length - 1].trim() + }) .join(', ') error.value = __('These fields are required: {0}', [errorMessage]) return diff --git a/frontend/src/data/document.js b/frontend/src/data/document.js index 9c15e1d0..d5893552 100644 --- a/frontend/src/data/document.js +++ b/frontend/src/data/document.js @@ -26,7 +26,10 @@ export function useDocument(doctype, docname) { let errorMessage = __('Error updating document') if (err.exc_type == 'MandatoryError') { const fieldName = err.messages - .map((msg) => msg.split(': ')[2].trim()) + .map((msg) => { + let arr = msg.split(': ') + return arr[arr.length - 1].trim() + }) .join(', ') errorMessage = __('Mandatory field error: {0}', [fieldName]) } From 57fb8b3abec13fe593ced96b3950f6f2f4684915 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 1 Jul 2025 16:04:40 +0530 Subject: [PATCH 7/9] fix: show error message on convert to deal modal (cherry picked from commit adc22efcb1e20eda3c0c3b897e5c5456ccf67b98) --- .../components/Modals/ConvertToDealModal.vue | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/Modals/ConvertToDealModal.vue b/frontend/src/components/Modals/ConvertToDealModal.vue index a4a29ac0..427d688f 100644 --- a/frontend/src/components/Modals/ConvertToDealModal.vue +++ b/frontend/src/components/Modals/ConvertToDealModal.vue @@ -90,6 +90,7 @@ :data="deal.doc" doctype="CRM Deal" /> + @@ -107,7 +108,7 @@ import { showQuickEntryModal, quickEntryProps } from '@/composables/modals' import { isMobileView } from '@/composables/settings' import { capture } from '@/telemetry' import { useOnboarding } from 'frappe-ui/frappe' -import { Switch, Dialog, toast, createResource, call } from 'frappe-ui' +import { Switch, Dialog, createResource, call } from 'frappe-ui' import { ref, computed } from 'vue' import { useRouter } from 'vue-router' @@ -132,18 +133,21 @@ const existingOrganizationChecked = ref(false) const existingContact = ref('') const existingOrganization = ref('') +const error = ref('') const { triggerConvertToDeal } = useDocument('CRM Lead', props.lead.name) const { document: deal } = useDocument('CRM Deal') async function convertToDeal() { + error.value = '' + if (existingContactChecked.value && !existingContact.value) { - toast.error(__('Please select an existing contact')) + error.value = __('Please select an existing contact') return } if (existingOrganizationChecked.value && !existingOrganization.value) { - toast.error(__('Please select an existing organization')) + error.value = __('Please select an existing organization') return } @@ -163,7 +167,22 @@ async function convertToDeal() { existing_contact: existingContact.value, existing_organization: existingOrganization.value, }).catch((err) => { - toast.error(__('Error converting to deal: {0}', [err.messages?.[0]])) + if (err.exc_type == 'MandatoryError') { + const errorMessage = err.messages + .map((msg) => { + let arr = msg.split(': ') + return arr[arr.length - 1].trim() + }) + .join(', ') + + if (errorMessage.toLowerCase().includes('required')) { + error.value = __(errorMessage) + } else { + error.value = __('{0} is required', [errorMessage]) + } + return + } + error.value = __('Error converting to deal: {0}', [err.messages?.[0]]) }) if (_deal) { show.value = false @@ -171,6 +190,7 @@ async function convertToDeal() { existingOrganizationChecked.value = false existingContact.value = '' existingOrganization.value = '' + error.value = '' updateOnboardingStep('convert_lead_to_deal', true, false, () => { localStorage.setItem('firstDeal' + user, _deal) }) From b1ea3edcebd8dbabf96076f1304b1a38dcc24603 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 1 Jul 2025 16:05:08 +0530 Subject: [PATCH 8/9] fix: add mandatory fields in convert to deal modal if not added (cherry picked from commit 17fdbb05cedad890631465d1ad64fbc502dd00c9) --- .../crm_fields_layout/crm_fields_layout.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) 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 1fd85824..9274e857 100644 --- a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py +++ b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py @@ -47,6 +47,13 @@ def get_fields_layout(doctype: str, type: str, parent_doctype: str | None = None fields = frappe.get_meta(doctype).fields fields = [field for field in fields if field.fieldname in allowed_fields] + required_fields = [] + + if type == "Required Fields": + required_fields = [ + field for field in frappe.get_meta(doctype, False).fields if field.reqd and not field.default + ] + for tab in tabs: for section in tab.get("sections"): if section.get("columns"): @@ -60,6 +67,32 @@ def get_fields_layout(doctype: str, type: str, parent_doctype: str | None = None handle_perm_level_restrictions(field, doctype, parent_doctype) column["fields"][column.get("fields").index(field["fieldname"])] = field + # remove field from required_fields if it is already present + if ( + type == "Required Fields" + and field.reqd + and any(f.get("fieldname") == field.get("fieldname") for f in required_fields) + ): + required_fields = [ + f for f in required_fields if f.get("fieldname") != field.get("fieldname") + ] + + if type == "Required Fields" and required_fields and tabs: + tabs[-1].get("sections").append( + { + "label": "Required Fields", + "name": "required_fields_section_" + str(random_string(4)), + "opened": True, + "hideLabel": True, + "columns": [ + { + "name": "required_fields_column_" + str(random_string(4)), + "fields": [field.as_dict() for field in required_fields], + } + ], + } + ) + return tabs or [] From 4cfe106144790d72cb40b6802787c390482e1fab Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 1 Jul 2025 16:37:04 +0530 Subject: [PATCH 9/9] fix: show forcasted sales section in sidepanel if forecasting is enabled (cherry picked from commit 485360f29194a95749b1a6fd27108bec03b37b50) --- .../crm_fields_layout/crm_fields_layout.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) 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 9274e857..34a361e9 100644 --- a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py +++ b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py @@ -116,6 +116,8 @@ def get_sidepanel_sections(doctype): fields = frappe.get_meta(doctype).fields fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes] + add_forecasting_section(layout, doctype) + for section in layout: section["name"] = section.get("name") or section.get("label") for column in section.get("columns") if section.get("columns") else []: @@ -133,6 +135,38 @@ def get_sidepanel_sections(doctype): return layout +def add_forecasting_section(layout, doctype): + if ( + doctype == "CRM Deal" + and frappe.db.get_single_value("FCRM Settings", "enable_forecasting") + and not any(section.get("name") == "forecasted_sales_section" for section in layout) + ): + contacts_section_index = next( + ( + i + for i, section in enumerate(layout) + if section.get("name") == "contacts_section" or section.get("label") == "Contacts" + ), + None, + ) + + if contacts_section_index is not None: + layout.insert( + contacts_section_index + 1, + { + "name": "forecasted_sales_section", + "label": "Forecasted Sales", + "opened": True, + "columns": [ + { + "name": "column_" + str(random_string(4)), + "fields": ["close_date", "probability", "deal_value"], + } + ], + }, + ) + + def handle_perm_level_restrictions(field, doctype, parent_doctype=None): if field.permlevel == 0: return