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 @@
>