diff --git a/crm/fcrm/doctype/crm_deal/crm_deal.json b/crm/fcrm/doctype/crm_deal/crm_deal.json index 67f441c3..10e56be4 100644 --- a/crm/fcrm/doctype/crm_deal/crm_deal.json +++ b/crm/fcrm/doctype/crm_deal/crm_deal.json @@ -11,11 +11,14 @@ "naming_series", "organization", "next_step", - "probability", "column_break_ijan", "status", - "close_date", "deal_owner", + "section_break_jgpm", + "probability", + "deal_value", + "column_break_kpxa", + "close_date", "contacts_tab", "contacts", "contact", @@ -91,7 +94,7 @@ { "fieldname": "close_date", "fieldtype": "Date", - "label": "Close Date" + "label": "Expected Closure Date" }, { "fieldname": "next_step", @@ -374,12 +377,26 @@ "label": "Net Total", "options": "currency", "read_only": 1 + }, + { + "fieldname": "section_break_jgpm", + "fieldtype": "Section Break" + }, + { + "fieldname": "deal_value", + "fieldtype": "Currency", + "label": "Deal Value", + "options": "currency" + }, + { + "fieldname": "column_break_kpxa", + "fieldtype": "Column Break" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2025-05-12 12:30:55.415282", + "modified": "2025-06-11 12:58:22.439045", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Deal", diff --git a/crm/fcrm/doctype/crm_deal/crm_deal.py b/crm/fcrm/doctype/crm_deal/crm_deal.py index b650301e..b7e4ec83 100644 --- a/crm/fcrm/doctype/crm_deal/crm_deal.py +++ b/crm/fcrm/doctype/crm_deal/crm_deal.py @@ -24,6 +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() def after_insert(self): if self.deal_owner: @@ -133,6 +134,13 @@ class CRMDeal(Document): if sla: sla.apply(self) + def update_close_date(self): + """ + Update the close date based on the "Won" status. + """ + if self.status == "Won" and not self.close_date: + self.close_date = frappe.utils.nowdate() + @staticmethod def default_list_data(): columns = [ 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 b0374ca1..ae026c74 100644 --- a/crm/fcrm/doctype/crm_deal_status/crm_deal_status.json +++ b/crm/fcrm/doctype/crm_deal_status/crm_deal_status.json @@ -8,7 +8,8 @@ "field_order": [ "deal_status", "color", - "position" + "position", + "probability" ], "fields": [ { @@ -32,11 +33,17 @@ "fieldtype": "Int", "in_list_view": 1, "label": "Position" + }, + { + "fieldname": "probability", + "fieldtype": "Percent", + "label": "Probability" } ], + "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2024-01-19 21:56:44.552134", + "modified": "2025-06-11 13:00:34.518808", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Deal Status", @@ -68,7 +75,8 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json index 250c8c29..de27e245 100644 --- a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json +++ b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json @@ -7,6 +7,7 @@ "field_order": [ "defaults_tab", "restore_defaults", + "enable_forecasting", "branding_tab", "brand_name", "brand_logo", @@ -28,7 +29,7 @@ { "fieldname": "defaults_tab", "fieldtype": "Tab Break", - "label": "Defaults" + "label": "Settings" }, { "fieldname": "branding_tab", @@ -56,12 +57,19 @@ "fieldname": "favicon", "fieldtype": "Attach", "label": "Favicon" + }, + { + "default": "0", + "description": "It will make deal's \"Expected Closure Date\" mandatory to get accurate forecasting insights", + "fieldname": "enable_forecasting", + "fieldtype": "Check", + "label": "Enable Forecasting" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-02-20 12:38:38.088477", + "modified": "2025-06-11 19:12:16.762499", "modified_by": "Administrator", "module": "FCRM", "name": "FCRM Settings", @@ -95,7 +103,8 @@ "share": 1 } ], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.py b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.py index 53c8c77c..f22114f1 100644 --- a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.py +++ b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.py @@ -3,6 +3,7 @@ import frappe from frappe import _ +from frappe.custom.doctype.property_setter.property_setter import delete_property_setter, make_property_setter from frappe.model.document import Document from crm.install import after_install @@ -15,6 +16,7 @@ class FCRMSettings(Document): def validate(self): self.do_not_allow_to_delete_if_standard() + self.setup_forecasting() def do_not_allow_to_delete_if_standard(self): if not self.has_value_changed("dropdown_items"): @@ -29,6 +31,23 @@ class FCRMSettings(Document): return frappe.throw(_("Cannot delete standard items {0}").format(", ".join(deleted_standard_items))) + def setup_forecasting(self): + if self.has_value_changed("enable_forecasting"): + if not self.enable_forecasting: + delete_property_setter( + "CRM Deal", + "reqd", + "close_date", + ) + else: + make_property_setter( + "CRM Deal", + "close_date", + "reqd", + 1 if self.enable_forecasting else 0, + "Check", + ) + def get_standard_dropdown_items(): return [item.get("name1") for item in frappe.get_hooks("standard_dropdown_items")] @@ -57,3 +76,36 @@ def sync_table(key, hook): crm_settings.set(key, items) crm_settings.save() + + +def create_forecasting_script(): + if not frappe.db.exists("CRM Form Script", "Forecasting Script"): + script = get_forecasting_script() + frappe.get_doc( + { + "doctype": "CRM Form Script", + "name": "Forecasting Script", + "dt": "CRM Deal", + "view": "Form", + "script": script, + "enabled": 1, + "is_standard": 1, + } + ).insert() + + +def get_forecasting_script(): + return """class CRMDeal { + async status() { + await this.doc.trigger('updateProbability') + } + async updateProbability() { + let status = await call("frappe.client.get_value", { + doctype: "CRM Deal Status", + fieldname: "probability", + filters: { name: this.doc.status }, + }) + + this.doc.probability = status.probability + } +}""" diff --git a/crm/install.py b/crm/install.py index 5f25e7d0..5ffa5a67 100644 --- a/crm/install.py +++ b/crm/install.py @@ -359,5 +359,8 @@ def add_standard_dropdown_items(): def add_default_scripts(): + from crm.fcrm.doctype.fcrm_settings.fcrm_settings import create_forecasting_script + for doctype in ["CRM Lead", "CRM Deal"]: create_product_details_script(doctype) + create_forecasting_script() diff --git a/crm/patches.txt b/crm/patches.txt index 59f37bfb..ee56be84 100644 --- a/crm/patches.txt +++ b/crm/patches.txt @@ -12,4 +12,4 @@ 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 \ No newline at end of file +crm.patches.v1_0.create_default_scripts # 13-06-2025 \ No newline at end of file diff --git a/frontend/src/components/Controls/Grid.vue b/frontend/src/components/Controls/Grid.vue index 54bc29dc..19240a86 100644 --- a/frontend/src/components/Controls/Grid.vue +++ b/frontend/src/components/Controls/Grid.vue @@ -509,8 +509,7 @@ const deleteRows = () => { } function fieldChange(value, field, row) { - row[field.fieldname] = value - triggerOnChange(field.fieldname, row) + triggerOnChange(field.fieldname, value, row) } function getDefaultValue(defaultValue, fieldtype) { diff --git a/frontend/src/components/FieldLayout/Field.vue b/frontend/src/components/FieldLayout/Field.vue index b517d973..07f530cd 100644 --- a/frontend/src/components/FieldLayout/Field.vue +++ b/frontend/src/components/FieldLayout/Field.vue @@ -332,12 +332,10 @@ const getPlaceholder = (field) => { } function fieldChange(value, df) { - data.value[df.fieldname] = value - if (isGridRow) { - triggerOnChange(df.fieldname, data.value) + triggerOnChange(df.fieldname, value, data.value) } else { - triggerOnChange(df.fieldname) + triggerOnChange(df.fieldname, value) } } diff --git a/frontend/src/components/Modals/DealModal.vue b/frontend/src/components/Modals/DealModal.vue index 1c516407..e4ce7419 100644 --- a/frontend/src/components/Modals/DealModal.vue +++ b/frontend/src/components/Modals/DealModal.vue @@ -94,7 +94,7 @@ const show = defineModel() const router = useRouter() const error = ref(null) -const { document: deal } = useDocument('CRM Deal') +const { document: deal, triggerOnChange } = useDocument('CRM Deal') const hasOrganizationSections = ref(true) const hasContactSections = ref(true) @@ -164,7 +164,7 @@ const tabs = createResource({ }) const dealStatuses = computed(() => { - let statuses = statusOptions('deal') + let statuses = statusOptions('deal', null, [], triggerOnChange) if (!deal.doc.status) { deal.doc.status = statuses[0].value } diff --git a/frontend/src/components/Modals/LeadModal.vue b/frontend/src/components/Modals/LeadModal.vue index bf1461d1..1976ad3c 100644 --- a/frontend/src/components/Modals/LeadModal.vue +++ b/frontend/src/components/Modals/LeadModal.vue @@ -70,10 +70,10 @@ const router = useRouter() const error = ref(null) const isLeadCreating = ref(false) -const { document: lead } = useDocument('CRM Lead') +const { document: lead, triggerOnChange } = useDocument('CRM Lead') const leadStatuses = computed(() => { - let statuses = statusOptions('lead') + let statuses = statusOptions('lead', null, [], triggerOnChange) if (!lead.doc.status) { lead.doc.status = statuses?.[0]?.value } diff --git a/frontend/src/components/SidePanelLayout.vue b/frontend/src/components/SidePanelLayout.vue index 3fe8fd1a..e99d9725 100644 --- a/frontend/src/components/SidePanelLayout.vue +++ b/frontend/src/components/SidePanelLayout.vue @@ -44,18 +44,21 @@ >
- {{ __(field.label) }} - + {{ __(field.label) }} +
+
* + * +
@@ -489,9 +492,7 @@ function parsedField(field) { async function fieldChange(value, df) { if (props.preview) return - document.doc[df.fieldname] = value - - await triggerOnChange(df.fieldname) + await triggerOnChange(df.fieldname, value) document.save.submit(null, { onSuccess: () => { diff --git a/frontend/src/data/document.js b/frontend/src/data/document.js index 7d456676..76e35a9e 100644 --- a/frontend/src/data/document.js +++ b/frontend/src/data/document.js @@ -117,20 +117,26 @@ export function useDocument(doctype, docname) { await trigger(handler) } - async function triggerOnChange(fieldname, row) { + async function triggerOnChange(fieldname, value, row) { + const oldValue = documentsCache[doctype][docname || ''].doc[fieldname] + documentsCache[doctype][docname || ''].doc[fieldname] = value + const handler = async function () { + this.value = value + this.oldValue = oldValue if (row) { this.currentRowIdx = row.idx - this.value = row[fieldname] - this.oldValue = getOldValue(fieldname, row) - } else { - this.value = documentsCache[doctype][docname || ''].doc[fieldname] - this.oldValue = getOldValue(fieldname) } await this[fieldname]?.() } - await trigger(handler, row) + try { + await trigger(handler, row) + } catch (error) { + documentsCache[doctype][docname || ''].doc[fieldname] = oldValue + console.error(handler) + throw error + } } async function triggerOnRowAdd(row) { diff --git a/frontend/src/data/script.js b/frontend/src/data/script.js index b809f744..47185bab 100644 --- a/frontend/src/data/script.js +++ b/frontend/src/data/script.js @@ -46,6 +46,11 @@ export function getScript(doctype, view = 'Form') { helpers.router = router helpers.call = call + helpers.throwError = (message) => { + toast.error(message || __('An error occurred')) + throw new Error(message || __('An error occurred')) + } + helpers.crm = { makePhoneCall: makeCall, } diff --git a/frontend/src/pages/Deal.vue b/frontend/src/pages/Deal.vue index e3d09fa7..34e06141 100644 --- a/frontend/src/pages/Deal.vue +++ b/frontend/src/pages/Deal.vue @@ -23,7 +23,14 @@ />