diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 3cc28a3dc8..4e7a653e39 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -149,22 +149,6 @@ frappe.ui.form.on("Journal Entry", {
}
});
}
- else if(frm.doc.voucher_type=="Opening Entry") {
- return frappe.call({
- type:"GET",
- method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_opening_accounts",
- args: {
- "company": frm.doc.company
- },
- callback: function(r) {
- frappe.model.clear_table(frm.doc, "accounts");
- if(r.message) {
- update_jv_details(frm.doc, r.message);
- }
- cur_frm.set_value("is_opening", "Yes");
- }
- });
- }
}
},
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json
index 4493c72254..8e5ba3718f 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.json
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json
@@ -137,7 +137,8 @@
"fieldname": "finance_book",
"fieldtype": "Link",
"label": "Finance Book",
- "options": "Finance Book"
+ "options": "Finance Book",
+ "read_only": 1
},
{
"fieldname": "2_add_edit_gl_entries",
@@ -538,7 +539,7 @@
"idx": 176,
"is_submittable": 1,
"links": [],
- "modified": "2022-04-06 17:18:46.865259",
+ "modified": "2022-06-23 22:01:32.348337",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 787efd2a42..50df65b183 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -1204,24 +1204,6 @@ def get_payment_entry(ref_doc, args):
return je if args.get("journal_entry") else je.as_dict()
-@frappe.whitelist()
-def get_opening_accounts(company):
- """get all balance sheet accounts for opening entry"""
- accounts = frappe.db.sql_list(
- """select
- name from tabAccount
- where
- is_group=0 and report_type='Balance Sheet' and company={0} and
- name not in (select distinct account from tabWarehouse where
- account is not null and account != '')
- order by name asc""".format(
- frappe.db.escape(company)
- )
- )
-
- return [{"account": a, "balance": get_balance_on(a)} for a in accounts]
-
-
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 1b20c29f94..448ec54b1e 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -792,6 +792,54 @@ class TestSalesInvoice(unittest.TestCase):
jv.cancel()
self.assertEqual(frappe.db.get_value("Sales Invoice", w.name, "outstanding_amount"), 562.0)
+ def test_outstanding_on_cost_center_allocation(self):
+ # setup cost centers
+ from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
+ from erpnext.accounts.doctype.cost_center_allocation.test_cost_center_allocation import (
+ create_cost_center_allocation,
+ )
+
+ cost_centers = [
+ "Main Cost Center 1",
+ "Sub Cost Center 1",
+ "Sub Cost Center 2",
+ ]
+ for cc in cost_centers:
+ create_cost_center(cost_center_name=cc, company="_Test Company")
+
+ cca = create_cost_center_allocation(
+ "_Test Company",
+ "Main Cost Center 1 - _TC",
+ {"Sub Cost Center 1 - _TC": 60, "Sub Cost Center 2 - _TC": 40},
+ )
+
+ # make invoice
+ si = frappe.copy_doc(test_records[0])
+ si.is_pos = 0
+ si.insert()
+ si.submit()
+
+ from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
+
+ # make payment - fully paid
+ pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
+ pe.reference_no = "1"
+ pe.reference_date = nowdate()
+ pe.paid_from_account_currency = si.currency
+ pe.paid_to_account_currency = si.currency
+ pe.source_exchange_rate = 1
+ pe.target_exchange_rate = 1
+ pe.paid_amount = si.outstanding_amount
+ pe.cost_center = cca.main_cost_center
+ pe.insert()
+ pe.submit()
+
+ # cancel cost center allocation
+ cca.cancel()
+
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 0)
+
def test_sales_invoice_gl_entry_without_perpetual_inventory(self):
si = frappe.copy_doc(test_records[1])
si.insert()
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 8146804705..76ef3abb6f 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -132,7 +132,7 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items():
gle = copy.deepcopy(d)
gle.cost_center = sub_cost_center
- for field in ("debit", "credit", "debit_in_account_currency", "credit_in_company_currency"):
+ for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"):
gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
new_gl_map.append(gle)
else:
diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
index 20f7643a1c..9d2deea523 100644
--- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
+++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
@@ -43,7 +43,7 @@ def get_columns():
"options": "Account",
"width": 170,
},
- {"label": _("Amount"), "fieldname": "amount", "width": 120},
+ {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120},
]
return columns
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index e4b561e5f6..e77e828e16 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -425,7 +425,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
update_value_in_dict(totals, "opening", gle)
update_value_in_dict(totals, "closing", gle)
- elif gle.posting_date <= to_date:
+ elif gle.posting_date <= to_date or (cstr(gle.is_opening) == "Yes" and show_opening_entries):
if not group_by_voucher_consolidated:
update_value_in_dict(gle_map[group_by_value].totals, "total", gle)
update_value_in_dict(gle_map[group_by_value].totals, "closing", gle)
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 257488dfc3..a880c2f391 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -252,6 +252,7 @@ class Asset(AccountsController):
number_of_pending_depreciations += 1
skip_row = False
+ should_get_last_day = is_last_day_of_the_month(finance_book.depreciation_start_date)
for n in range(start[finance_book.idx - 1], number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance)
@@ -265,6 +266,9 @@ class Asset(AccountsController):
finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation)
)
+ if should_get_last_day:
+ schedule_date = get_last_day(schedule_date)
+
# schedule date will be a year later from start date
# so monthly schedule date is calculated by removing 11 months from it
monthly_schedule_date = add_months(schedule_date, -finance_book.frequency_of_depreciation + 1)
@@ -849,14 +853,9 @@ class Asset(AccountsController):
if args.get("rate_of_depreciation") and on_validate:
return args.get("rate_of_depreciation")
- no_of_years = (
- flt(args.get("total_number_of_depreciations") * flt(args.get("frequency_of_depreciation")))
- / 12
- )
value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount)
- # square root of flt(salvage_value) / flt(asset_cost)
- depreciation_rate = math.pow(value, 1.0 / flt(no_of_years, 2))
+ depreciation_rate = math.pow(value, 1.0 / flt(args.get("total_number_of_depreciations"), 2))
return 100 * (1 - flt(depreciation_rate, float_precision))
@@ -1105,9 +1104,18 @@ def is_cwip_accounting_enabled(asset_category):
def get_total_days(date, frequency):
period_start_date = add_months(date, cint(frequency) * -1)
+ if is_last_day_of_the_month(date):
+ period_start_date = get_last_day(period_start_date)
+
return date_diff(date, period_start_date)
+def is_last_day_of_the_month(date):
+ last_day_of_the_month = get_last_day(date)
+
+ return getdate(last_day_of_the_month) == getdate(date)
+
+
@erpnext.allow_regional
def get_depreciation_amount(asset, depreciable_value, row):
if row.depreciation_method in ("Straight Line", "Manual"):
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index e759ad0719..f8a8fc551d 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -707,6 +707,39 @@ class TestDepreciationMethods(AssetSetup):
self.assertEqual(schedules, expected_schedules)
+ def test_monthly_depreciation_by_wdv_method(self):
+ asset = create_asset(
+ calculate_depreciation=1,
+ available_for_use_date="2022-02-15",
+ purchase_date="2022-02-15",
+ depreciation_method="Written Down Value",
+ gross_purchase_amount=10000,
+ expected_value_after_useful_life=5000,
+ depreciation_start_date="2022-02-28",
+ total_number_of_depreciations=5,
+ frequency_of_depreciation=1,
+ )
+
+ expected_schedules = [
+ ["2022-02-28", 645.0, 645.0],
+ ["2022-03-31", 1206.8, 1851.8],
+ ["2022-04-30", 1051.12, 2902.92],
+ ["2022-05-31", 915.52, 3818.44],
+ ["2022-06-30", 797.42, 4615.86],
+ ["2022-07-15", 384.14, 5000.0],
+ ]
+
+ schedules = [
+ [
+ cstr(d.schedule_date),
+ flt(d.depreciation_amount, 2),
+ flt(d.accumulated_depreciation_amount, 2),
+ ]
+ for d in asset.get("schedules")
+ ]
+
+ self.assertEqual(schedules, expected_schedules)
+
def test_discounted_wdv_depreciation_rate_for_indian_region(self):
# set indian company
company_flag = frappe.flags.company
@@ -838,7 +871,7 @@ class TestDepreciationBasics(AssetSetup):
expected_values = [["2020-12-31", 30000.0], ["2021-12-31", 30000.0], ["2022-12-31", 30000.0]]
for i, schedule in enumerate(asset.schedules):
- self.assertEqual(expected_values[i][0], schedule.schedule_date)
+ self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
def test_set_accumulated_depreciation(self):
@@ -1333,6 +1366,32 @@ class TestDepreciationBasics(AssetSetup):
asset.cost_center = "Main - _TC"
asset.submit()
+ def test_depreciation_on_final_day_of_the_month(self):
+ """Tests if final day of the month is picked each time, if the depreciation start date is the last day of the month."""
+
+ asset = create_asset(
+ item_code="Macbook Pro",
+ calculate_depreciation=1,
+ purchase_date="2020-01-30",
+ available_for_use_date="2020-02-15",
+ depreciation_start_date="2020-02-29",
+ frequency_of_depreciation=1,
+ total_number_of_depreciations=5,
+ submit=1,
+ )
+
+ expected_dates = [
+ "2020-02-29",
+ "2020-03-31",
+ "2020-04-30",
+ "2020-05-31",
+ "2020-06-30",
+ "2020-07-15",
+ ]
+
+ for i, schedule in enumerate(asset.schedules):
+ self.assertEqual(getdate(expected_dates[i]), getdate(schedule.schedule_date))
+
def create_asset_data():
if not frappe.db.exists("Asset Category", "Computers"):
diff --git a/erpnext/crm/doctype/crm_note/__init__.py b/erpnext/crm/doctype/crm_note/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/crm/doctype/crm_note/crm_note.json b/erpnext/crm/doctype/crm_note/crm_note.json
new file mode 100644
index 0000000000..fc2a4d1192
--- /dev/null
+++ b/erpnext/crm/doctype/crm_note/crm_note.json
@@ -0,0 +1,48 @@
+{
+ "actions": [],
+ "autoname": "autoincrement",
+ "creation": "2022-06-04 15:49:23.416644",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "note",
+ "added_by",
+ "added_on"
+ ],
+ "fields": [
+ {
+ "columns": 5,
+ "fieldname": "note",
+ "fieldtype": "Text Editor",
+ "in_list_view": 1,
+ "label": "Note"
+ },
+ {
+ "fieldname": "added_by",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Added By",
+ "options": "User"
+ },
+ {
+ "fieldname": "added_on",
+ "fieldtype": "Datetime",
+ "in_list_view": 1,
+ "label": "Added On"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2022-06-04 16:29:07.807252",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "CRM Note",
+ "naming_rule": "Autoincrement",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/crm_note/crm_note.py b/erpnext/crm/doctype/crm_note/crm_note.py
new file mode 100644
index 0000000000..6c7eeb4c7e
--- /dev/null
+++ b/erpnext/crm/doctype/crm_note/crm_note.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class CRMNote(Document):
+ pass
diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.json b/erpnext/crm/doctype/crm_settings/crm_settings.json
index a2a19b9e79..26a07d2e85 100644
--- a/erpnext/crm/doctype/crm_settings/crm_settings.json
+++ b/erpnext/crm/doctype/crm_settings/crm_settings.json
@@ -10,12 +10,10 @@
"campaign_naming_by",
"allow_lead_duplication_based_on_emails",
"column_break_4",
- "create_event_on_next_contact_date",
"auto_creation_of_contact",
"opportunity_section",
"close_opportunity_after_days",
"column_break_9",
- "create_event_on_next_contact_date_opportunity",
"quotation_section",
"default_valid_till",
"section_break_13",
@@ -55,12 +53,6 @@
"fieldtype": "Check",
"label": "Auto Creation of Contact"
},
- {
- "default": "1",
- "fieldname": "create_event_on_next_contact_date",
- "fieldtype": "Check",
- "label": "Create Event on Next Contact Date"
- },
{
"fieldname": "opportunity_section",
"fieldtype": "Section Break",
@@ -73,12 +65,6 @@
"fieldtype": "Int",
"label": "Close Replied Opportunity After Days"
},
- {
- "default": "1",
- "fieldname": "create_event_on_next_contact_date_opportunity",
- "fieldtype": "Check",
- "label": "Create Event on Next Contact Date"
- },
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
@@ -105,7 +91,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-12-20 12:51:38.894252",
+ "modified": "2022-06-06 11:22:08.464253",
"modified_by": "Administrator",
"module": "CRM",
"name": "CRM Settings",
@@ -143,5 +129,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js
index 999599ce95..37fb3509ce 100644
--- a/erpnext/crm/doctype/lead/lead.js
+++ b/erpnext/crm/doctype/lead/lead.js
@@ -24,31 +24,39 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
this.frm.set_query("lead_owner", function (doc, cdt, cdn) {
return { query: "frappe.core.doctype.user.user.user_query" }
});
-
- this.frm.set_query("contact_by", function (doc, cdt, cdn) {
- return { query: "frappe.core.doctype.user.user.user_query" }
- });
}
refresh () {
+ var me = this;
let doc = this.frm.doc;
erpnext.toggle_naming_series();
- frappe.dynamic_link = { doc: doc, fieldname: 'name', doctype: 'Lead' }
+ frappe.dynamic_link = {
+ doc: doc,
+ fieldname: 'name',
+ doctype: 'Lead'
+ };
if (!this.frm.is_new() && doc.__onload && !doc.__onload.is_customer) {
this.frm.add_custom_button(__("Customer"), this.make_customer, __("Create"));
- this.frm.add_custom_button(__("Opportunity"), this.make_opportunity, __("Create"));
+ this.frm.add_custom_button(__("Opportunity"), function() {
+ me.frm.trigger("make_opportunity");
+ }, __("Create"));
this.frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
- this.frm.add_custom_button(__("Prospect"), this.make_prospect, __("Create"));
- this.frm.add_custom_button(__('Add to Prospect'), this.add_lead_to_prospect, __('Action'));
+ if (!doc.__onload.linked_prospects.length) {
+ this.frm.add_custom_button(__("Prospect"), this.make_prospect, __("Create"));
+ this.frm.add_custom_button(__('Add to Prospect'), this.add_lead_to_prospect, __('Action'));
+ }
}
if (!this.frm.is_new()) {
frappe.contacts.render_address_and_contact(this.frm);
- cur_frm.trigger('render_contact_day_html');
} else {
frappe.contacts.clear_address_and_contact(this.frm);
}
+
+ this.frm.dashboard.links_area.hide();
+ this.show_notes();
+ this.show_activities();
}
add_lead_to_prospect () {
@@ -74,7 +82,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
}
},
freeze: true,
- freeze_message: __('...Adding Lead to Prospect')
+ freeze_message: __('Adding Lead to Prospect...')
});
}, __('Add Lead to Prospect'), __('Add'));
}
@@ -86,13 +94,6 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
})
}
- make_opportunity () {
- frappe.model.open_mapped_doc({
- method: "erpnext.crm.doctype.lead.lead.make_opportunity",
- frm: cur_frm
- })
- }
-
make_quotation () {
frappe.model.open_mapped_doc({
method: "erpnext.crm.doctype.lead.lead.make_quotation",
@@ -111,9 +112,10 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
prospect.fax = cur_frm.doc.fax;
prospect.website = cur_frm.doc.website;
prospect.prospect_owner = cur_frm.doc.lead_owner;
+ prospect.notes = cur_frm.doc.notes;
- let lead_prospect_row = frappe.model.add_child(prospect, 'prospect_lead');
- lead_prospect_row.lead = cur_frm.doc.name;
+ let leads_row = frappe.model.add_child(prospect, 'leads');
+ leads_row.lead = cur_frm.doc.name;
frappe.set_route("Form", "Prospect", prospect.name);
});
@@ -125,26 +127,109 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
}
}
- contact_date () {
- if (this.frm.doc.contact_date) {
- let d = moment(this.frm.doc.contact_date);
- d.add(1, "day");
- this.frm.set_value("ends_on", d.format(frappe.defaultDatetimeFormat));
- }
+ show_notes() {
+ if (this.frm.doc.docstatus == 1) return;
+
+ const crm_notes = new erpnext.utils.CRMNotes({
+ frm: this.frm,
+ notes_wrapper: $(this.frm.fields_dict.notes_html.wrapper),
+ });
+ crm_notes.refresh();
}
- render_contact_day_html() {
- if (cur_frm.doc.contact_date) {
- let contact_date = frappe.datetime.obj_to_str(cur_frm.doc.contact_date);
- let diff_days = frappe.datetime.get_day_diff(contact_date, frappe.datetime.get_today());
- let color = diff_days > 0 ? "orange" : "green";
- let message = diff_days > 0 ? __("Next Contact Date") : __("Last Contact Date");
- let html = `
- ${message} : ${frappe.datetime.global_date_format(contact_date)}
-
` ;
- cur_frm.dashboard.set_headline_alert(html);
- }
+ show_activities() {
+ if (this.frm.doc.docstatus == 1) return;
+
+ const crm_activities = new erpnext.utils.CRMActivities({
+ frm: this.frm,
+ open_activities_wrapper: $(this.frm.fields_dict.open_activities_html.wrapper),
+ all_activities_wrapper: $(this.frm.fields_dict.all_activities_html.wrapper),
+ form_wrapper: $(this.frm.wrapper),
+ });
+ crm_activities.refresh();
}
};
+
extend_cscript(cur_frm.cscript, new erpnext.LeadController({ frm: cur_frm }));
+
+frappe.ui.form.on("Lead", {
+ make_opportunity: async function(frm) {
+ let existing_prospect = (await frappe.db.get_value("Prospect Lead",
+ {
+ "lead": frm.doc.name
+ },
+ "name", null, "Prospect"
+ )).message.name;
+
+ if (!existing_prospect) {
+ var fields = [
+ {
+ "label": "Create Prospect",
+ "fieldname": "create_prospect",
+ "fieldtype": "Check",
+ "default": 1
+ },
+ {
+ "label": "Prospect Name",
+ "fieldname": "prospect_name",
+ "fieldtype": "Data",
+ "default": frm.doc.company_name,
+ "depends_on": "create_prospect"
+ }
+ ];
+ }
+ let existing_contact = (await frappe.db.get_value("Contact",
+ {
+ "first_name": frm.doc.first_name || frm.doc.lead_name,
+ "last_name": frm.doc.last_name
+ },
+ "name"
+ )).message.name;
+
+ if (!existing_contact) {
+ fields.push(
+ {
+ "label": "Create Contact",
+ "fieldname": "create_contact",
+ "fieldtype": "Check",
+ "default": "1"
+ }
+ );
+ }
+
+ if (fields) {
+ var d = new frappe.ui.Dialog({
+ title: __('Create Opportunity'),
+ fields: fields,
+ primary_action: function() {
+ var data = d.get_values();
+ frappe.call({
+ method: 'create_prospect_and_contact',
+ doc: frm.doc,
+ args: {
+ data: data,
+ },
+ freeze: true,
+ callback: function(r) {
+ if (!r.exc) {
+ frappe.model.open_mapped_doc({
+ method: "erpnext.crm.doctype.lead.lead.make_opportunity",
+ frm: frm
+ });
+ }
+ d.hide();
+ }
+ });
+ },
+ primary_action_label: __('Create')
+ });
+ d.show();
+ } else {
+ frappe.model.open_mapped_doc({
+ method: "erpnext.crm.doctype.lead.lead.make_opportunity",
+ frm: frm
+ });
+ }
+ }
+});
\ No newline at end of file
diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json
index 542977e689..9216c458c8 100644
--- a/erpnext/crm/doctype/lead/lead.json
+++ b/erpnext/crm/doctype/lead/lead.json
@@ -3,78 +3,76 @@
"allow_events_in_timeline": 1,
"allow_import": 1,
"autoname": "naming_series:",
- "creation": "2013-04-10 11:45:37",
+ "creation": "2022-02-08 13:14:41.083327",
"doctype": "DocType",
"document_type": "Document",
"email_append_to": 1,
"engine": "InnoDB",
"field_order": [
- "lead_details",
"naming_series",
"salutation",
"first_name",
"middle_name",
"last_name",
+ "column_break_1",
"lead_name",
- "col_break123",
- "status",
- "company_name",
- "designation",
+ "job_title",
"gender",
- "contact_details_section",
+ "source",
+ "col_break123",
+ "lead_owner",
+ "status",
+ "customer",
+ "type",
+ "request_type",
+ "contact_info_tab",
"email_id",
+ "website",
+ "column_break_20",
"mobile_no",
"whatsapp_no",
"column_break_16",
"phone",
"phone_ext",
- "additional_information_section",
+ "organization_section",
+ "company_name",
"no_of_employees",
+ "column_break_28",
+ "annual_revenue",
"industry",
"market_segment",
- "column_break_22",
+ "column_break_31",
+ "territory",
"fax",
- "website",
- "type",
- "request_type",
"address_section",
"address_html",
- "city",
- "pincode",
- "county",
"column_break2",
"contact_html",
- "state",
- "country",
- "section_break_12",
- "lead_owner",
- "ends_on",
- "column_break_14",
- "contact_by",
- "contact_date",
- "lead_source_details_section",
- "company",
- "territory",
- "language",
- "column_break_50",
- "source",
+ "qualification_tab",
+ "qualification_status",
+ "column_break_64",
+ "qualified_by",
+ "qualified_on",
+ "other_info_tab",
"campaign_name",
+ "company",
+ "column_break_22",
+ "language",
+ "image",
+ "title",
+ "column_break_50",
+ "disabled",
"unsubscribed",
"blog_subscriber",
- "notes_section",
- "notes",
- "other_information_section",
- "customer",
- "image",
- "title"
+ "activities_tab",
+ "open_activities_html",
+ "all_activities_section",
+ "all_activities_html",
+ "notes_tab",
+ "notes_html",
+ "notes"
],
"fields": [
- {
- "fieldname": "lead_details",
- "fieldtype": "Section Break",
- "label": "Lead Details",
- "options": "fa fa-user"
- },
{
"fieldname": "naming_series",
"fieldtype": "Select",
@@ -86,6 +84,7 @@
"set_only_once": 1
},
{
+ "depends_on": "eval:!doc.__islocal",
"fieldname": "lead_name",
"fieldtype": "Data",
"in_global_search": 1,
@@ -108,7 +107,7 @@
{
"fieldname": "email_id",
"fieldtype": "Data",
- "label": "Email Address",
+ "label": "Email",
"oldfieldname": "email_id",
"oldfieldtype": "Data",
"options": "Email",
@@ -189,50 +188,9 @@
"print_hide": 1
},
{
- "fieldname": "section_break_12",
+ "fieldname": "contact_info_tab",
"fieldtype": "Section Break",
- "label": "Follow Up"
- },
- {
- "fieldname": "contact_by",
- "fieldtype": "Link",
- "label": "Next Contact By",
- "oldfieldname": "contact_by",
- "oldfieldtype": "Link",
- "options": "User",
- "width": "100px"
- },
- {
- "fieldname": "column_break_14",
- "fieldtype": "Column Break"
- },
- {
- "bold": 1,
- "fieldname": "contact_date",
- "fieldtype": "Datetime",
- "label": "Next Contact Date",
- "no_copy": 1,
- "oldfieldname": "contact_date",
- "oldfieldtype": "Date",
- "width": "100px"
- },
- {
- "bold": 1,
- "fieldname": "ends_on",
- "fieldtype": "Datetime",
- "label": "Ends On",
- "no_copy": 1
- },
- {
- "collapsible": 1,
- "fieldname": "notes_section",
- "fieldtype": "Section Break",
- "label": "Notes"
- },
- {
- "fieldname": "notes",
- "fieldtype": "Text Editor",
- "label": "Notes"
+ "label": "Contact Info"
},
{
"fieldname": "address_html",
@@ -240,34 +198,6 @@
"label": "Address HTML",
"read_only": 1
},
- {
- "fieldname": "city",
- "fieldtype": "Data",
- "label": "City/Town",
- "mandatory_depends_on": "eval: doc.address_title && doc.address_type"
- },
- {
- "fieldname": "county",
- "fieldtype": "Data",
- "label": "County"
- },
- {
- "fieldname": "state",
- "fieldtype": "Data",
- "label": "State"
- },
- {
- "fieldname": "country",
- "fieldtype": "Link",
- "label": "Country",
- "mandatory_depends_on": "eval: doc.address_title && doc.address_type",
- "options": "Country"
- },
- {
- "fieldname": "pincode",
- "fieldtype": "Data",
- "label": "Postal Code"
- },
{
"fieldname": "column_break2",
"fieldtype": "Column Break"
@@ -289,7 +219,7 @@
{
"fieldname": "mobile_no",
"fieldtype": "Data",
- "label": "Mobile No.",
+ "label": "Mobile No",
"oldfieldname": "mobile_no",
"oldfieldtype": "Data",
"options": "Phone"
@@ -347,8 +277,7 @@
"fieldtype": "Data",
"label": "Website",
"oldfieldname": "website",
- "oldfieldtype": "Data",
- "options": "URL"
+ "oldfieldtype": "Data"
},
{
"fieldname": "territory",
@@ -380,14 +309,6 @@
"label": "Title",
"print_hide": 1
},
- {
- "fieldname": "designation",
- "fieldtype": "Link",
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Designation",
- "options": "Designation"
- },
{
"fieldname": "language",
"fieldtype": "Link",
@@ -410,16 +331,11 @@
"fieldtype": "Data",
"label": "Last Name"
},
- {
- "collapsible": 1,
- "fieldname": "additional_information_section",
- "fieldtype": "Section Break",
- "label": "Additional Information"
- },
{
"fieldname": "no_of_employees",
- "fieldtype": "Int",
- "label": "No. of Employees"
+ "fieldtype": "Select",
+ "label": "No. of Employees",
+ "options": "1-10\n11-20\n21-30\n31-100\n11-50\n51-200\n201-500\n101-500\n500-1000\n501-1000\n>1000\n1000+"
},
{
"fieldname": "column_break_22",
@@ -428,35 +344,13 @@
{
"fieldname": "whatsapp_no",
"fieldtype": "Data",
- "label": "WhatsApp No.",
+ "label": "WhatsApp",
"options": "Phone"
},
- {
- "collapsible": 1,
- "depends_on": "eval: !doc.__islocal",
- "fieldname": "address_section",
- "fieldtype": "Section Break",
- "label": "Address"
- },
- {
- "fieldname": "lead_source_details_section",
- "fieldtype": "Section Break",
- "label": "Lead Source Details"
- },
{
"fieldname": "column_break_50",
"fieldtype": "Column Break"
},
- {
- "fieldname": "other_information_section",
- "fieldtype": "Section Break",
- "label": "Other Information"
- },
- {
- "fieldname": "contact_details_section",
- "fieldtype": "Section Break",
- "label": "Contact Details"
- },
{
"fieldname": "column_break_16",
"fieldtype": "Column Break"
@@ -465,17 +359,136 @@
"fieldname": "phone_ext",
"fieldtype": "Data",
"label": "Phone Ext."
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "qualification_tab",
+ "fieldtype": "Section Break",
+ "label": "Qualification"
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "notes_tab",
+ "fieldtype": "Tab Break",
+ "label": "Notes"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "other_info_tab",
+ "fieldtype": "Section Break",
+ "label": "Additional Information"
+ },
+ {
+ "fieldname": "column_break_1",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "qualified_by",
+ "fieldtype": "Link",
+ "label": "Qualified By",
+ "options": "User"
+ },
+ {
+ "fieldname": "qualified_on",
+ "fieldtype": "Date",
+ "label": "Qualified on"
+ },
+ {
+ "fieldname": "qualification_status",
+ "fieldtype": "Select",
+ "label": "Qualification Status",
+ "options": "Unqualified\nIn Process\nQualified"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "address_section",
+ "fieldtype": "Section Break",
+ "label": "Address & Contacts"
+ },
+ {
+ "fieldname": "column_break_64",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_20",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "job_title",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Job Title"
+ },
+ {
+ "fieldname": "annual_revenue",
+ "fieldtype": "Currency",
+ "label": "Annual Revenue"
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "activities_tab",
+ "fieldtype": "Tab Break",
+ "label": "Activities"
+ },
+ {
+ "fieldname": "organization_section",
+ "fieldtype": "Section Break",
+ "label": "Organization"
+ },
+ {
+ "fieldname": "column_break_28",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_31",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "notes_html",
+ "fieldtype": "HTML",
+ "label": "Notes HTML"
+ },
+ {
+ "fieldname": "open_activities_html",
+ "fieldtype": "HTML",
+ "label": "Open Activities HTML"
+ },
+ {
+ "fieldname": "all_activities_section",
+ "fieldtype": "Section Break",
+ "label": "All Activities"
+ },
+ {
+ "fieldname": "all_activities_html",
+ "fieldtype": "HTML",
+ "label": "All Activities HTML"
+ },
+ {
+ "fieldname": "notes",
+ "fieldtype": "Table",
+ "hidden": 1,
+ "label": "Notes",
+ "no_copy": 1,
+ "options": "CRM Note"
+ },
+ {
+ "default": "0",
+ "fieldname": "disabled",
+ "fieldtype": "Check",
+ "label": "Disabled"
}
],
"icon": "fa fa-user",
"idx": 5,
"image_field": "image",
"links": [],
- "modified": "2021-08-04 00:24:57.208590",
+ "modified": "2022-06-21 15:10:06.613519",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead",
"name_case": "Title Case",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@@ -535,6 +548,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"subject_field": "title",
"title_field": "title"
}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index c9a64ff8e6..0d12499771 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -1,27 +1,19 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-
import frappe
from frappe import _
from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.email.inbox import link_communication_to_document
from frappe.model.mapper import get_mapped_doc
-from frappe.utils import (
- comma_and,
- cstr,
- get_link_to_form,
- getdate,
- has_gravatar,
- nowdate,
- validate_email_address,
-)
+from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_email_address
from erpnext.accounts.party import set_taxes
from erpnext.controllers.selling_controller import SellingController
+from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events
-class Lead(SellingController):
+class Lead(SellingController, CRMNote):
def get_feed(self):
return "{0}: {1}".format(_(self.status), self.lead_name)
@@ -29,6 +21,7 @@ class Lead(SellingController):
customer = frappe.db.get_value("Customer", {"lead_name": self.name})
self.get("__onload").is_customer = customer
load_address_and_contact(self)
+ self.set_onload("linked_prospects", self.get_linked_prospects())
def validate(self):
self.set_full_name()
@@ -37,79 +30,42 @@ class Lead(SellingController):
self.set_status()
self.check_email_id_is_unique()
self.validate_email_id()
- self.validate_contact_date()
- self.set_prev()
+
+ def before_insert(self):
+ self.contact_doc = None
+ if frappe.db.get_single_value("CRM Settings", "auto_creation_of_contact"):
+ self.contact_doc = self.create_contact()
+
+ def after_insert(self):
+ self.link_to_contact()
+
+ def on_update(self):
+ self.update_prospect()
+
+ def on_trash(self):
+ frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
+
+ self.unlink_dynamic_links()
+ self.remove_link_from_prospect()
def set_full_name(self):
if self.first_name:
- self.lead_name = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name]))
-
- def validate_email_id(self):
- if self.email_id:
- if not self.flags.ignore_email_validation:
- validate_email_address(self.email_id, throw=True)
-
- if self.email_id == self.lead_owner:
- frappe.throw(_("Lead Owner cannot be same as the Lead"))
-
- if self.email_id == self.contact_by:
- frappe.throw(_("Next Contact By cannot be same as the Lead Email Address"))
-
- if self.is_new() or not self.image:
- self.image = has_gravatar(self.email_id)
-
- def validate_contact_date(self):
- if self.contact_date and getdate(self.contact_date) < getdate(nowdate()):
- frappe.throw(_("Next Contact Date cannot be in the past"))
-
- if self.ends_on and self.contact_date and (getdate(self.ends_on) < getdate(self.contact_date)):
- frappe.throw(_("Ends On date cannot be before Next Contact Date."))
-
- def on_update(self):
- self.add_calendar_event()
- self.update_prospects()
-
- def set_prev(self):
- if self.is_new():
- self._prev = frappe._dict({"contact_date": None, "ends_on": None, "contact_by": None})
- else:
- self._prev = frappe.db.get_value(
- "Lead", self.name, ["contact_date", "ends_on", "contact_by"], as_dict=1
+ self.lead_name = " ".join(
+ filter(None, [self.salutation, self.first_name, self.middle_name, self.last_name])
)
- def before_insert(self):
- self.contact_doc = self.create_contact()
+ def set_lead_name(self):
+ if not self.lead_name:
+ # Check for leads being created through data import
+ if not self.company_name and not self.email_id and not self.flags.ignore_mandatory:
+ frappe.throw(_("A Lead requires either a person's name or an organization's name"))
+ elif self.company_name:
+ self.lead_name = self.company_name
+ else:
+ self.lead_name = self.email_id.split("@")[0]
- def after_insert(self):
- self.update_links()
-
- def update_links(self):
- # update contact links
- if self.contact_doc:
- self.contact_doc.append(
- "links", {"link_doctype": "Lead", "link_name": self.name, "link_title": self.lead_name}
- )
- self.contact_doc.save()
-
- def add_calendar_event(self, opts=None, force=False):
- if frappe.db.get_single_value("CRM Settings", "create_event_on_next_contact_date"):
- super(Lead, self).add_calendar_event(
- {
- "owner": self.lead_owner,
- "starts_on": self.contact_date,
- "ends_on": self.ends_on or "",
- "subject": ("Contact " + cstr(self.lead_name)),
- "description": ("Contact " + cstr(self.lead_name))
- + (self.contact_by and (". By : " + cstr(self.contact_by)) or ""),
- },
- force,
- )
-
- def update_prospects(self):
- prospects = frappe.get_all("Prospect Lead", filters={"lead": self.name}, fields=["parent"])
- for row in prospects:
- prospect = frappe.get_doc("Prospect", row.parent)
- prospect.save(ignore_permissions=True)
+ def set_title(self):
+ self.title = self.company_name or self.lead_name
def check_email_id_is_unique(self):
if self.email_id:
@@ -124,15 +80,47 @@ class Lead(SellingController):
if duplicate_leads:
frappe.throw(
- _("Email Address must be unique, already exists for {0}").format(comma_and(duplicate_leads)),
+ _("Email Address must be unique, it is already used in {0}").format(
+ comma_and(duplicate_leads)
+ ),
frappe.DuplicateEntryError,
)
- def on_trash(self):
- frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
+ def validate_email_id(self):
+ if self.email_id:
+ if not self.flags.ignore_email_validation:
+ validate_email_address(self.email_id, throw=True)
- self.unlink_dynamic_links()
- self.delete_events()
+ if self.email_id == self.lead_owner:
+ frappe.throw(_("Lead Owner cannot be same as the Lead Email Address"))
+
+ if self.is_new() or not self.image:
+ self.image = has_gravatar(self.email_id)
+
+ def link_to_contact(self):
+ # update contact links
+ if self.contact_doc:
+ self.contact_doc.append(
+ "links", {"link_doctype": "Lead", "link_name": self.name, "link_title": self.lead_name}
+ )
+ self.contact_doc.save()
+
+ def update_prospect(self):
+ lead_row_name = frappe.db.get_value(
+ "Prospect Lead", filters={"lead": self.name}, fieldname="name"
+ )
+ if lead_row_name:
+ lead_row = frappe.get_doc("Prospect Lead", lead_row_name)
+ lead_row.update(
+ {
+ "lead_name": self.lead_name,
+ "email": self.email_id,
+ "mobile_no": self.mobile_no,
+ "lead_owner": self.lead_owner,
+ "status": self.status,
+ }
+ )
+ lead_row.db_update()
def unlink_dynamic_links(self):
links = frappe.get_all(
@@ -155,6 +143,30 @@ class Lead(SellingController):
linked_doc.remove(to_remove)
linked_doc.save(ignore_permissions=True)
+ def remove_link_from_prospect(self):
+ prospects = self.get_linked_prospects()
+
+ for d in prospects:
+ prospect = frappe.get_doc("Prospect", d.parent)
+ if len(prospect.get("leads")) == 1:
+ prospect.delete(ignore_permissions=True)
+ else:
+ to_remove = None
+ for d in prospect.get("leads"):
+ if d.lead == self.name:
+ to_remove = d
+
+ if to_remove:
+ prospect.remove(to_remove)
+ prospect.save(ignore_permissions=True)
+
+ def get_linked_prospects(self):
+ return frappe.get_all(
+ "Prospect Lead",
+ filters={"lead": self.name},
+ fields=["parent"],
+ )
+
def has_customer(self):
return frappe.db.get_value("Customer", {"lead_name": self.name})
@@ -171,50 +183,78 @@ class Lead(SellingController):
"Quotation", {"party_name": self.name, "docstatus": 1, "status": "Lost"}
)
- def set_lead_name(self):
- if not self.lead_name:
- # Check for leads being created through data import
- if not self.company_name and not self.email_id and not self.flags.ignore_mandatory:
- frappe.throw(_("A Lead requires either a person's name or an organization's name"))
- elif self.company_name:
- self.lead_name = self.company_name
- else:
- self.lead_name = self.email_id.split("@")[0]
+ @frappe.whitelist()
+ def create_prospect_and_contact(self, data):
+ data = frappe._dict(data)
+ if data.create_contact:
+ self.create_contact()
- def set_title(self):
- self.title = self.company_name or self.lead_name
+ if data.create_prospect:
+ self.create_prospect(data.prospect_name)
def create_contact(self):
- if frappe.db.get_single_value("CRM Settings", "auto_creation_of_contact"):
- if not self.lead_name:
- self.set_full_name()
- self.set_lead_name()
+ if not self.lead_name:
+ self.set_full_name()
+ self.set_lead_name()
- contact = frappe.new_doc("Contact")
- contact.update(
+ contact = frappe.new_doc("Contact")
+ contact.update(
+ {
+ "first_name": self.first_name or self.lead_name,
+ "last_name": self.last_name,
+ "salutation": self.salutation,
+ "gender": self.gender,
+ "job_title": self.job_title,
+ "company_name": self.company_name,
+ }
+ )
+
+ if self.email_id:
+ contact.append("email_ids", {"email_id": self.email_id, "is_primary": 1})
+
+ if self.phone:
+ contact.append("phone_nos", {"phone": self.phone, "is_primary_phone": 1})
+
+ if self.mobile_no:
+ contact.append("phone_nos", {"phone": self.mobile_no, "is_primary_mobile_no": 1})
+
+ contact.insert(ignore_permissions=True)
+ contact.reload() # load changes by hooks on contact
+
+ return contact
+
+ def create_prospect(self, company_name):
+ try:
+ prospect = frappe.new_doc("Prospect")
+
+ prospect.company_name = company_name or self.company_name
+ prospect.no_of_employees = self.no_of_employees
+ prospect.industry = self.industry
+ prospect.market_segment = self.market_segment
+ prospect.annual_revenue = self.annual_revenue
+ prospect.territory = self.territory
+ prospect.fax = self.fax
+ prospect.website = self.website
+ prospect.prospect_owner = self.lead_owner
+ prospect.company = self.company
+ prospect.notes = self.notes
+
+ prospect.append(
+ "leads",
{
- "first_name": self.first_name or self.lead_name,
- "last_name": self.last_name,
- "salutation": self.salutation,
- "gender": self.gender,
- "designation": self.designation,
- "company_name": self.company_name,
- }
+ "lead": self.name,
+ "lead_name": self.lead_name,
+ "email": self.email_id,
+ "mobile_no": self.mobile_no,
+ "lead_owner": self.lead_owner,
+ "status": self.status,
+ },
)
-
- if self.email_id:
- contact.append("email_ids", {"email_id": self.email_id, "is_primary": 1})
-
- if self.phone:
- contact.append("phone_nos", {"phone": self.phone, "is_primary_phone": 1})
-
- if self.mobile_no:
- contact.append("phone_nos", {"phone": self.mobile_no, "is_primary_mobile_no": 1})
-
- contact.insert(ignore_permissions=True)
- contact.reload() # load changes by hooks on contact
-
- return contact
+ prospect.flags.ignore_permissions = True
+ prospect.flags.ignore_mandatory = True
+ prospect.save()
+ except frappe.DuplicateEntryError:
+ frappe.throw(_("Prospect {0} already exists").format(company_name or self.company_name))
@frappe.whitelist()
@@ -274,6 +314,8 @@ def make_opportunity(source_name, target_doc=None):
"company_name": "customer_name",
"email_id": "contact_email",
"mobile_no": "contact_mobile",
+ "lead_owner": "opportunity_owner",
+ "notes": "notes",
},
}
},
@@ -422,21 +464,25 @@ def get_lead_with_phone_number(number):
return lead
-def daily_open_lead():
- leads = frappe.get_all("Lead", filters=[["contact_date", "Between", [nowdate(), nowdate()]]])
- for lead in leads:
- frappe.db.set_value("Lead", lead.name, "status", "Open")
-
-
@frappe.whitelist()
def add_lead_to_prospect(lead, prospect):
prospect = frappe.get_doc("Prospect", prospect)
- prospect.append("prospect_lead", {"lead": lead})
+ prospect.append("leads", {"lead": lead})
prospect.save(ignore_permissions=True)
+
+ carry_forward_communication_and_comments = frappe.db.get_single_value(
+ "CRM Settings", "carry_forward_communication_and_comments"
+ )
+
+ if carry_forward_communication_and_comments:
+ copy_comments("Lead", lead, prospect)
+ link_communications("Lead", lead, prospect)
+ link_open_events("Lead", lead, prospect)
+
frappe.msgprint(
_("Lead {0} has been added to prospect {1}.").format(
frappe.bold(lead), frappe.bold(prospect.name)
),
- title=_("Lead Added"),
+ title=_("Lead -> Prospect"),
indicator="green",
)
diff --git a/erpnext/crm/doctype/lead/lead_list.js b/erpnext/crm/doctype/lead/lead_list.js
index 75208fa64b..dbeaf608ff 100644
--- a/erpnext/crm/doctype/lead/lead_list.js
+++ b/erpnext/crm/doctype/lead/lead_list.js
@@ -16,7 +16,7 @@ frappe.listview_settings['Lead'] = {
prospect.prospect_owner = r.lead_owner;
leads.forEach(function(lead) {
- let lead_prospect_row = frappe.model.add_child(prospect, 'prospect_lead');
+ let lead_prospect_row = frappe.model.add_child(prospect, 'leads');
lead_prospect_row.lead = lead.name;
});
frappe.set_route("Form", "Prospect", prospect.name);
diff --git a/erpnext/crm/doctype/lead/test_lead.py b/erpnext/crm/doctype/lead/test_lead.py
index 166ae2c353..8fe688de46 100644
--- a/erpnext/crm/doctype/lead/test_lead.py
+++ b/erpnext/crm/doctype/lead/test_lead.py
@@ -5,7 +5,10 @@
import unittest
import frappe
-from frappe.utils import random_string
+from frappe.utils import random_string, today
+
+from erpnext.crm.doctype.lead.lead import make_opportunity
+from erpnext.crm.utils import get_linked_prospect
test_records = frappe.get_test_records("Lead")
@@ -83,6 +86,105 @@ class TestLead(unittest.TestCase):
self.assertEqual(frappe.db.exists("Lead", lead_doc.name), None)
self.assertEqual(len(address_1.get("links")), 1)
+ def test_prospect_creation_from_lead(self):
+ frappe.db.sql("delete from `tabLead` where lead_name='Rahul Tripathi'")
+ frappe.db.sql("delete from `tabProspect` where name='Prospect Company'")
+
+ lead = make_lead(
+ first_name="Rahul",
+ last_name="Tripathi",
+ email_id="rahul@gmail.com",
+ company_name="Prospect Company",
+ )
+
+ event = create_event("Meeting 1", today(), "Lead", lead.name)
+
+ lead.create_prospect(lead.company_name)
+
+ prospect = get_linked_prospect("Lead", lead.name)
+ self.assertEqual(prospect, "Prospect Company")
+
+ event.reload()
+ self.assertEqual(event.event_participants[1].reference_doctype, "Prospect")
+ self.assertEqual(event.event_participants[1].reference_docname, prospect)
+
+ def test_opportunity_from_lead(self):
+ frappe.db.sql("delete from `tabLead` where lead_name='Rahul Tripathi'")
+ frappe.db.sql("delete from `tabOpportunity` where party_name='Rahul Tripathi'")
+
+ lead = make_lead(
+ first_name="Rahul",
+ last_name="Tripathi",
+ email_id="rahul@gmail.com",
+ company_name="Prospect Company",
+ )
+
+ lead.add_note("test note")
+ event = create_event("Meeting 1", today(), "Lead", lead.name)
+ create_todo("followup", "Lead", lead.name)
+
+ opportunity = make_opportunity(lead.name)
+ opportunity.save()
+
+ self.assertEqual(opportunity.get("party_name"), lead.name)
+ self.assertEqual(opportunity.notes[0].note, "test note")
+
+ event.reload()
+ self.assertEqual(event.event_participants[1].reference_doctype, "Opportunity")
+ self.assertEqual(event.event_participants[1].reference_docname, opportunity.name)
+
+ self.assertTrue(
+ frappe.db.get_value(
+ "ToDo", {"reference_type": "Opportunity", "reference_name": opportunity.name}
+ )
+ )
+
+ def test_copy_events_from_lead_to_prospect(self):
+ frappe.db.sql("delete from `tabLead` where lead_name='Rahul Tripathi'")
+ frappe.db.sql("delete from `tabProspect` where name='Prospect Company'")
+
+ lead = make_lead(
+ first_name="Rahul",
+ last_name="Tripathi",
+ email_id="rahul@gmail.com",
+ company_name="Prospect Company",
+ )
+
+ lead.create_prospect(lead.company_name)
+ prospect = get_linked_prospect("Lead", lead.name)
+
+ event = create_event("Meeting", today(), "Lead", lead.name)
+
+ self.assertEqual(len(event.event_participants), 2)
+ self.assertEqual(event.event_participants[1].reference_doctype, "Prospect")
+ self.assertEqual(event.event_participants[1].reference_docname, prospect)
+
+
+def create_event(subject, starts_on, reference_type, reference_name):
+ event = frappe.new_doc("Event")
+ event.subject = subject
+ event.starts_on = starts_on
+ event.event_type = "Private"
+ event.all_day = 1
+ event.owner = "Administrator"
+ event.append(
+ "event_participants", {"reference_doctype": reference_type, "reference_docname": reference_name}
+ )
+ event.reference_type = reference_type
+ event.reference_name = reference_name
+ event.insert()
+ return event
+
+
+def create_todo(description, reference_type, reference_name):
+ todo = frappe.new_doc("ToDo")
+ todo.description = description
+ todo.owner = "Administrator"
+ todo.reference_type = reference_type
+ todo.reference_name = reference_name
+ todo.insert()
+ return todo
+
def make_lead(**args):
args = frappe._dict(args)
@@ -93,6 +195,7 @@ def make_lead(**args):
"first_name": args.first_name or "_Test",
"last_name": args.last_name or "Lead",
"email_id": args.email_id or "new_lead_{}@example.com".format(random_string(5)),
+ "company_name": args.company_name or "_Test Company",
}
).insert()
diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js
index 8e7d67e057..c53ea9d5c3 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.js
+++ b/erpnext/crm/doctype/opportunity/opportunity.js
@@ -32,13 +32,6 @@ frappe.ui.form.on("Opportunity", {
}
},
- contact_date: function(frm) {
- if(frm.doc.contact_date < frappe.datetime.now_datetime()){
- frm.set_value("contact_date", "");
- frappe.throw(__("Next follow up date should be greater than now."))
- }
- },
-
onload_post_render: function(frm) {
frm.get_field("items").grid.set_multiple_add("item_code", "qty");
},
@@ -130,6 +123,13 @@ frappe.ui.form.on("Opportunity", {
});
}
}
+
+ if (!frm.is_new()) {
+ frappe.contacts.render_address_and_contact(frm);
+ // frm.trigger('render_contact_day_html');
+ } else {
+ frappe.contacts.clear_address_and_contact(frm);
+ }
},
set_contact_link: function(frm) {
@@ -227,8 +227,7 @@ frappe.ui.form.on("Opportunity", {
'total': flt(total),
'base_total': flt(base_total)
});
- }
-
+ },
});
frappe.ui.form.on("Opportunity Item", {
calculate: function(frm, cdt, cdn) {
@@ -264,13 +263,14 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
this.frm.trigger('currency');
}
+ refresh() {
+ this.show_notes();
+ this.show_activities();
+ }
+
setup_queries() {
var me = this;
- if(this.frm.fields_dict.contact_by.df.options.match(/^User/)) {
- this.frm.set_query("contact_by", erpnext.queries.user);
- }
-
me.frm.set_query('customer_address', erpnext.queries.address_query);
this.frm.set_query("item_code", "items", function() {
@@ -287,6 +287,14 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
}
else if (me.frm.doc.opportunity_from == "Customer") {
me.frm.set_query('party_name', erpnext.queries['customer']);
+ } else if (me.frm.doc.opportunity_from == "Prospect") {
+ me.frm.set_query('party_name', function() {
+ return {
+ filters: {
+ "company": me.frm.doc.company
+ }
+ };
+ });
}
}
@@ -303,6 +311,24 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
frm: cur_frm
})
}
+
+ show_notes() {
+ const crm_notes = new erpnext.utils.CRMNotes({
+ frm: this.frm,
+ notes_wrapper: $(this.frm.fields_dict.notes_html.wrapper),
+ });
+ crm_notes.refresh();
+ }
+
+ show_activities() {
+ const crm_activities = new erpnext.utils.CRMActivities({
+ frm: this.frm,
+ open_activities_wrapper: $(this.frm.fields_dict.open_activities_html.wrapper),
+ all_activities_wrapper: $(this.frm.fields_dict.all_activities_html.wrapper),
+ form_wrapper: $(this.frm.wrapper),
+ });
+ crm_activities.refresh();
+ }
};
extend_cscript(cur_frm.cscript, new erpnext.crm.Opportunity({frm: cur_frm}));
diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json
index 089f2d2faa..8ddd4e36c2 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.json
+++ b/erpnext/crm/doctype/opportunity/opportunity.json
@@ -1,5 +1,6 @@
{
"actions": [],
+ "allow_events_in_timeline": 1,
"allow_import": 1,
"allow_rename": 1,
"autoname": "naming_series:",
@@ -11,68 +12,84 @@
"email_append_to": 1,
"engine": "InnoDB",
"field_order": [
- "from_section",
"naming_series",
"opportunity_from",
"party_name",
"customer_name",
- "source",
- "column_break0",
- "title",
- "opportunity_type",
"status",
- "converted_by",
+ "column_break0",
+ "opportunity_type",
+ "source",
+ "opportunity_owner",
+ "column_break_10",
"sales_stage",
- "first_response_time",
"expected_closing",
- "next_contact",
- "contact_by",
- "contact_date",
- "column_break2",
- "to_discuss",
+ "probability",
+ "organization_details_section",
+ "no_of_employees",
+ "annual_revenue",
+ "customer_group",
+ "column_break_23",
+ "industry",
+ "market_segment",
+ "column_break_31",
+ "territory",
+ "website",
"section_break_14",
"currency",
+ "column_break_36",
"conversion_rate",
- "base_opportunity_amount",
- "with_items",
"column_break_17",
- "probability",
"opportunity_amount",
+ "base_opportunity_amount",
+ "more_info",
+ "company",
+ "campaign",
+ "transaction_date",
+ "column_break1",
+ "language",
+ "amended_from",
+ "title",
+ "first_response_time",
+ "lost_detail_section",
+ "lost_reasons",
+ "order_lost_reason",
+ "column_break_56",
+ "competitors",
+ "contact_info",
+ "primary_contact_section",
+ "contact_person",
+ "job_title",
+ "column_break_54",
+ "contact_email",
+ "contact_mobile",
+ "column_break_22",
+ "whatsapp",
+ "phone",
+ "phone_ext",
+ "address_contact_section",
+ "address_html",
+ "customer_address",
+ "address_display",
+ "column_break3",
+ "contact_html",
+ "contact_display",
"items_section",
"items",
"section_break_32",
"base_total",
"column_break_33",
"total",
- "contact_info",
- "customer_address",
- "address_display",
- "territory",
- "customer_group",
- "column_break3",
- "contact_person",
- "contact_display",
- "contact_email",
- "contact_mobile",
- "more_info",
- "company",
- "campaign",
- "column_break1",
- "transaction_date",
- "language",
- "amended_from",
- "lost_detail_section",
- "lost_reasons",
- "order_lost_reason",
- "column_break_56",
- "competitors"
+ "activities_tab",
+ "open_activities_html",
+ "all_activities_section",
+ "all_activities_html",
+ "notes_tab",
+ "notes_html",
+ "notes",
+ "dashboard_tab"
],
"fields": [
- {
- "fieldname": "from_section",
- "fieldtype": "Section Break",
- "options": "fa fa-user"
- },
{
"fieldname": "naming_series",
"fieldtype": "Select",
@@ -113,8 +130,9 @@
"bold": 1,
"fieldname": "customer_name",
"fieldtype": "Data",
+ "hidden": 1,
"in_global_search": 1,
- "label": "Customer / Lead Name",
+ "label": "Customer Name",
"read_only": 1
},
{
@@ -166,48 +184,10 @@
"fieldtype": "Date",
"label": "Expected Closing Date"
},
- {
- "collapsible": 1,
- "collapsible_depends_on": "contact_by",
- "fieldname": "next_contact",
- "fieldtype": "Section Break",
- "label": "Follow Up"
- },
- {
- "fieldname": "contact_by",
- "fieldtype": "Link",
- "in_standard_filter": 1,
- "label": "Next Contact By",
- "oldfieldname": "contact_by",
- "oldfieldtype": "Link",
- "options": "User",
- "width": "75px"
- },
- {
- "fieldname": "contact_date",
- "fieldtype": "Datetime",
- "label": "Next Contact Date",
- "oldfieldname": "contact_date",
- "oldfieldtype": "Date"
- },
- {
- "fieldname": "column_break2",
- "fieldtype": "Column Break",
- "oldfieldtype": "Column Break",
- "width": "50%"
- },
- {
- "fieldname": "to_discuss",
- "fieldtype": "Small Text",
- "label": "To Discuss",
- "no_copy": 1,
- "oldfieldname": "to_discuss",
- "oldfieldtype": "Small Text"
- },
{
"fieldname": "section_break_14",
"fieldtype": "Section Break",
- "label": "Sales"
+ "label": "Opportunity Value"
},
{
"fieldname": "currency",
@@ -221,12 +201,6 @@
"label": "Opportunity Amount",
"options": "currency"
},
- {
- "default": "0",
- "fieldname": "with_items",
- "fieldtype": "Check",
- "label": "With Items"
- },
{
"fieldname": "column_break_17",
"fieldtype": "Column Break"
@@ -245,9 +219,8 @@
"label": "Probability (%)"
},
{
- "depends_on": "with_items",
"fieldname": "items_section",
- "fieldtype": "Section Break",
+ "fieldtype": "Tab Break",
"label": "Items",
"oldfieldtype": "Section Break",
"options": "fa fa-shopping-cart"
@@ -262,18 +235,16 @@
"options": "Opportunity Item"
},
{
- "collapsible": 1,
- "collapsible_depends_on": "next_contact_by",
- "depends_on": "eval:doc.party_name",
"fieldname": "contact_info",
- "fieldtype": "Section Break",
- "label": "Contact Info",
+ "fieldtype": "Tab Break",
+ "label": "Contacts",
"options": "fa fa-bullhorn"
},
{
"depends_on": "eval:doc.party_name",
"fieldname": "customer_address",
"fieldtype": "Link",
+ "hidden": 1,
"label": "Customer / Lead Address",
"options": "Address",
"print_hide": 1
@@ -327,19 +298,16 @@
"read_only": 1
},
{
- "depends_on": "eval:doc.party_name",
"fieldname": "contact_email",
"fieldtype": "Data",
"label": "Contact Email",
- "options": "Email",
- "read_only": 1
+ "options": "Email"
},
{
- "depends_on": "eval:doc.party_name",
"fieldname": "contact_mobile",
- "fieldtype": "Small Text",
- "label": "Contact Mobile No",
- "read_only": 1
+ "fieldtype": "Data",
+ "label": "Contact Mobile",
+ "options": "Phone"
},
{
"collapsible": 1,
@@ -416,12 +384,6 @@
"options": "Opportunity Lost Reason Detail",
"read_only": 1
},
- {
- "fieldname": "converted_by",
- "fieldtype": "Link",
- "label": "Converted By",
- "options": "User"
- },
{
"bold": 1,
"fieldname": "first_response_time",
@@ -474,6 +436,7 @@
"fieldtype": "Column Break"
},
{
+ "depends_on": "eval:doc.status===\"Lost\"",
"fieldname": "lost_detail_section",
"fieldtype": "Section Break",
"label": "Lost Reasons"
@@ -488,12 +451,164 @@
"label": "Competitors",
"options": "Competitor Detail",
"read_only": 1
+ },
+ {
+ "fieldname": "column_break_10",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "organization_details_section",
+ "fieldtype": "Section Break",
+ "label": "Organization"
+ },
+ {
+ "fieldname": "no_of_employees",
+ "fieldtype": "Select",
+ "label": "No of Employees",
+ "options": "1-10\n11-20\n21-30\n31-100\n11-50\n51-200\n201-500\n101-500\n500-1000\n501-1000\n>1000\n1000+"
+ },
+ {
+ "fieldname": "annual_revenue",
+ "fieldtype": "Currency",
+ "label": "Annual Revenue"
+ },
+ {
+ "fieldname": "industry",
+ "fieldtype": "Link",
+ "label": "Industry",
+ "options": "Industry Type"
+ },
+ {
+ "fieldname": "market_segment",
+ "fieldtype": "Link",
+ "label": "Market Segment",
+ "options": "Market Segment"
+ },
+ {
+ "fieldname": "column_break_23",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "address_contact_section",
+ "fieldtype": "Section Break",
+ "label": "Address & Contact"
+ },
+ {
+ "fieldname": "column_break_36",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "opportunity_owner",
+ "fieldtype": "Link",
+ "label": "Opportunity Owner",
+ "options": "User"
+ },
+ {
+ "fieldname": "website",
+ "fieldtype": "Data",
+ "label": "Website"
+ },
+ {
+ "fieldname": "column_break_22",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "whatsapp",
+ "fieldtype": "Data",
+ "label": "WhatsApp",
+ "options": "Phone"
+ },
+ {
+ "fieldname": "phone",
+ "fieldtype": "Data",
+ "label": "Phone",
+ "options": "Phone"
+ },
+ {
+ "fieldname": "phone_ext",
+ "fieldtype": "Data",
+ "label": "Phone Ext."
+ },
+ {
+ "fieldname": "column_break_31",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "primary_contact_section",
+ "fieldtype": "Section Break",
+ "label": "Primary Contact"
+ },
+ {
+ "fieldname": "column_break_54",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "dashboard_tab",
+ "fieldtype": "Tab Break",
+ "label": "Dashboard",
+ "show_dashboard": 1
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "notes_tab",
+ "fieldtype": "Tab Break",
+ "label": "Notes"
+ },
+ {
+ "fieldname": "notes_html",
+ "fieldtype": "HTML",
+ "label": "Notes HTML"
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "activities_tab",
+ "fieldtype": "Tab Break",
+ "label": "Activities"
+ },
+ {
+ "fieldname": "job_title",
+ "fieldtype": "Data",
+ "label": "Job Title"
+ },
+ {
+ "fieldname": "address_html",
+ "fieldtype": "HTML",
+ "label": "Address HTML"
+ },
+ {
+ "fieldname": "contact_html",
+ "fieldtype": "HTML",
+ "label": "Contact HTML"
+ },
+ {
+ "fieldname": "open_activities_html",
+ "fieldtype": "HTML",
+ "label": "Open Activities HTML"
+ },
+ {
+ "fieldname": "all_activities_section",
+ "fieldtype": "Section Break",
+ "label": "All Activities"
+ },
+ {
+ "fieldname": "all_activities_html",
+ "fieldtype": "HTML",
+ "label": "All Activities HTML"
+ },
+ {
+ "fieldname": "notes",
+ "fieldtype": "Table",
+ "hidden": 1,
+ "label": "Notes",
+ "no_copy": 1,
+ "options": "CRM Note"
}
],
"icon": "fa fa-info-sign",
"idx": 195,
"links": [],
- "modified": "2022-01-29 19:32:26.382896",
+ "modified": "2022-06-21 15:04:34.363959",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity",
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index c70a4f61b8..08eb472bb9 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -6,53 +6,54 @@ import json
import frappe
from frappe import _
+from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.email.inbox import link_communication_to_document
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import DocType, Interval
from frappe.query_builder.functions import Now
-from frappe.utils import cint, flt, get_fullname
+from frappe.utils import flt, get_fullname
-from erpnext.crm.utils import add_link_in_communication, copy_comments
+from erpnext.crm.utils import (
+ CRMNote,
+ copy_comments,
+ link_communications,
+ link_open_events,
+ link_open_tasks,
+)
from erpnext.setup.utils import get_exchange_rate
from erpnext.utilities.transaction_base import TransactionBase
-class Opportunity(TransactionBase):
+class Opportunity(TransactionBase, CRMNote):
+ def onload(self):
+ ref_doc = frappe.get_doc(self.opportunity_from, self.party_name)
+ load_address_and_contact(ref_doc)
+ self.set("__onload", ref_doc.get("__onload"))
+
def after_insert(self):
if self.opportunity_from == "Lead":
frappe.get_doc("Lead", self.party_name).set_status(update=True)
+ self.disable_lead()
- if self.opportunity_from in ["Lead", "Prospect"]:
+ link_open_tasks(self.opportunity_from, self.party_name, self)
+ link_open_events(self.opportunity_from, self.party_name, self)
if frappe.db.get_single_value("CRM Settings", "carry_forward_communication_and_comments"):
copy_comments(self.opportunity_from, self.party_name, self)
- add_link_in_communication(self.opportunity_from, self.party_name, self)
+ link_communications(self.opportunity_from, self.party_name, self)
def validate(self):
- self._prev = frappe._dict(
- {
- "contact_date": frappe.db.get_value("Opportunity", self.name, "contact_date")
- if (not cint(self.get("__islocal")))
- else None,
- "contact_by": frappe.db.get_value("Opportunity", self.name, "contact_by")
- if (not cint(self.get("__islocal")))
- else None,
- }
- )
-
self.make_new_lead_if_required()
self.validate_item_details()
self.validate_uom_is_integer("uom", "qty")
self.validate_cust_name()
self.map_fields()
+ self.set_exchange_rate()
if not self.title:
self.title = self.customer_name
- if not self.with_items:
- self.items = []
-
- else:
- self.calculate_totals()
+ self.calculate_totals()
+ self.update_prospect()
def map_fields(self):
for field in self.meta.get_valid_columns():
@@ -63,18 +64,65 @@ class Opportunity(TransactionBase):
except Exception:
continue
+ def set_exchange_rate(self):
+ company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
+ if self.currency == company_currency:
+ self.conversion_rate = 1.0
+ return
+
+ if not self.conversion_rate or self.conversion_rate == 1.0:
+ self.conversion_rate = get_exchange_rate(self.currency, company_currency, self.transaction_date)
+
def calculate_totals(self):
total = base_total = 0
for item in self.get("items"):
item.amount = flt(item.rate) * flt(item.qty)
- item.base_rate = flt(self.conversion_rate * item.rate)
- item.base_amount = flt(self.conversion_rate * item.amount)
+ item.base_rate = flt(self.conversion_rate) * flt(item.rate)
+ item.base_amount = flt(self.conversion_rate) * flt(item.amount)
total += item.amount
base_total += item.base_amount
self.total = flt(total)
self.base_total = flt(base_total)
+ def update_prospect(self):
+ prospect_name = None
+ if self.opportunity_from == "Prospect" and self.party_name:
+ prospect_name = self.party_name
+ elif self.opportunity_from == "Lead":
+ prospect_name = frappe.db.get_value("Prospect Lead", {"lead": self.party_name}, "parent")
+
+ if prospect_name:
+ prospect = frappe.get_doc("Prospect", prospect_name)
+
+ opportunity_values = {
+ "opportunity": self.name,
+ "amount": self.opportunity_amount,
+ "stage": self.sales_stage,
+ "deal_owner": self.opportunity_owner,
+ "probability": self.probability,
+ "expected_closing": self.expected_closing,
+ "currency": self.currency,
+ "contact_person": self.contact_person,
+ }
+
+ opportunity_already_added = False
+ for d in prospect.get("opportunities", []):
+ if d.opportunity == self.name:
+ opportunity_already_added = True
+ d.update(opportunity_values)
+ d.db_update()
+
+ if not opportunity_already_added:
+ prospect.append("opportunities", opportunity_values)
+ prospect.flags.ignore_permissions = True
+ prospect.flags.ignore_mandatory = True
+ prospect.save()
+
+ def disable_lead(self):
+ if self.opportunity_from == "Lead":
+ frappe.db.set_value("Lead", self.party_name, {"disabled": 1, "docstatus": 1})
+
def make_new_lead_if_required(self):
"""Set lead against new opportunity"""
if (not self.get("party_name")) and self.contact_email:
@@ -144,11 +192,8 @@ class Opportunity(TransactionBase):
else:
frappe.throw(_("Cannot declare as lost, because Quotation has been made."))
- def on_trash(self):
- self.delete_events()
-
def has_active_quotation(self):
- if not self.with_items:
+ if not self.get("items", []):
return frappe.get_all(
"Quotation",
{"opportunity": self.name, "status": ("not in", ["Lost", "Closed"]), "docstatus": 1},
@@ -165,7 +210,7 @@ class Opportunity(TransactionBase):
)
def has_ordered_quotation(self):
- if not self.with_items:
+ if not self.get("items", []):
return frappe.get_all(
"Quotation", {"opportunity": self.name, "status": "Ordered", "docstatus": 1}, "name"
)
@@ -195,43 +240,20 @@ class Opportunity(TransactionBase):
return True
def validate_cust_name(self):
- if self.party_name and self.opportunity_from == "Customer":
- self.customer_name = frappe.db.get_value("Customer", self.party_name, "customer_name")
- elif self.party_name and self.opportunity_from == "Lead":
- lead_name, company_name = frappe.db.get_value(
- "Lead", self.party_name, ["lead_name", "company_name"]
- )
- self.customer_name = company_name or lead_name
+ if self.party_name:
+ if self.opportunity_from == "Customer":
+ self.customer_name = frappe.db.get_value("Customer", self.party_name, "customer_name")
+ elif self.opportunity_from == "Lead":
+ customer_name = frappe.db.get_value("Prospect Lead", {"lead": self.party_name}, "parent")
+ if not customer_name:
+ lead_name, company_name = frappe.db.get_value(
+ "Lead", self.party_name, ["lead_name", "company_name"]
+ )
+ customer_name = company_name or lead_name
- def on_update(self):
- self.add_calendar_event()
-
- def add_calendar_event(self, opts=None, force=False):
- if frappe.db.get_single_value("CRM Settings", "create_event_on_next_contact_date_opportunity"):
- if not opts:
- opts = frappe._dict()
-
- opts.description = ""
- opts.contact_date = self.contact_date
-
- if self.party_name and self.opportunity_from == "Customer":
- if self.contact_person:
- opts.description = f"Contact {self.contact_person}"
- else:
- opts.description = f"Contact customer {self.party_name}"
- elif self.party_name and self.opportunity_from == "Lead":
- if self.contact_display:
- opts.description = f"Contact {self.contact_display}"
- else:
- opts.description = f"Contact lead {self.party_name}"
-
- opts.subject = opts.description
- opts.description += f". By : {self.contact_by}"
-
- if self.to_discuss:
- opts.description += f" To Discuss : {frappe.render_template(self.to_discuss, {'doc': self})}"
-
- super(Opportunity, self).add_calendar_event(opts, force)
+ self.customer_name = customer_name
+ elif self.opportunity_from == "Prospect":
+ self.customer_name = self.party_name
def validate_item_details(self):
if not self.get("items"):
@@ -295,7 +317,7 @@ def make_quotation(source_name, target_doc=None):
quotation.run_method("set_missing_values")
quotation.run_method("calculate_taxes_and_totals")
- if not source.with_items:
+ if not source.get("items", []):
quotation.opportunity = source.name
doclist = get_mapped_doc(
@@ -440,34 +462,3 @@ def make_opportunity_from_communication(communication, company, ignore_communica
link_communication_to_document(doc, "Opportunity", opportunity.name, ignore_communication_links)
return opportunity.name
-
-
-@frappe.whitelist()
-def get_events(start, end, filters=None):
- """Returns events for Gantt / Calendar view rendering.
- :param start: Start date-time.
- :param end: End date-time.
- :param filters: Filters (JSON).
- """
- from frappe.desk.calendar import get_event_conditions
-
- conditions = get_event_conditions("Opportunity", filters)
-
- data = frappe.db.sql(
- """
- select
- distinct `tabOpportunity`.name, `tabOpportunity`.customer_name, `tabOpportunity`.opportunity_amount,
- `tabOpportunity`.title, `tabOpportunity`.contact_date
- from
- `tabOpportunity`
- where
- (`tabOpportunity`.contact_date between %(start)s and %(end)s)
- {conditions}
- """.format(
- conditions=conditions
- ),
- {"start": start, "end": end},
- as_dict=True,
- update={"allDay": 0},
- )
- return data
diff --git a/erpnext/crm/doctype/opportunity/opportunity_calendar.js b/erpnext/crm/doctype/opportunity/opportunity_calendar.js
deleted file mode 100644
index 58fa2b8cd8..0000000000
--- a/erpnext/crm/doctype/opportunity/opportunity_calendar.js
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-// License: GNU General Public License v3. See license.txt
-frappe.views.calendar["Opportunity"] = {
- field_map: {
- "start": "contact_date",
- "end": "contact_date",
- "id": "name",
- "title": "customer_name",
- "allDay": "allDay"
- },
- options: {
- header: {
- left: 'prev,next today',
- center: 'title',
- right: 'month'
- }
- },
- get_events_method: 'erpnext.crm.doctype.opportunity.opportunity.get_events'
-}
diff --git a/erpnext/crm/doctype/opportunity/test_opportunity.py b/erpnext/crm/doctype/opportunity/test_opportunity.py
index 4a18e940bc..1ff3267e71 100644
--- a/erpnext/crm/doctype/opportunity/test_opportunity.py
+++ b/erpnext/crm/doctype/opportunity/test_opportunity.py
@@ -77,42 +77,6 @@ class TestOpportunity(unittest.TestCase):
create_communication(opp_doc.doctype, opp_doc.name, opp_doc.contact_email)
create_communication(opp_doc.doctype, opp_doc.name, opp_doc.contact_email)
- quotation_doc = make_quotation(opp_doc.name)
- quotation_doc.append("items", {"item_code": "_Test Item", "qty": 1})
- quotation_doc.run_method("set_missing_values")
- quotation_doc.run_method("calculate_taxes_and_totals")
- quotation_doc.save()
-
- quotation_comment_count = frappe.db.count(
- "Comment",
- {
- "reference_doctype": quotation_doc.doctype,
- "reference_name": quotation_doc.name,
- "comment_type": "Comment",
- },
- )
- quotation_communication_count = len(
- get_linked_communication_list(quotation_doc.doctype, quotation_doc.name)
- )
- self.assertEqual(quotation_comment_count, 4)
- self.assertEqual(quotation_communication_count, 4)
-
- def test_render_template_for_to_discuss(self):
- doc = make_opportunity(with_items=0, opportunity_from="Lead")
- doc.contact_by = "test@example.com"
- doc.contact_date = add_days(today(), days=2)
- doc.to_discuss = "{{ doc.name }} test data"
- doc.save()
-
- event = frappe.get_all(
- "Event Participants",
- fields=["parent"],
- filters={"reference_doctype": doc.doctype, "reference_docname": doc.name},
- )
-
- event_description = frappe.db.get_value("Event", event[0].parent, "description")
- self.assertTrue(doc.name in event_description)
-
def make_opportunity_from_lead():
new_lead_email_id = "new{}@example.com".format(random_string(5))
@@ -139,7 +103,6 @@ def make_opportunity(**args):
"opportunity_from": args.opportunity_from or "Customer",
"opportunity_type": "Sales",
"conversion_rate": 1.0,
- "with_items": args.with_items or 0,
"transaction_date": today(),
}
)
diff --git a/erpnext/crm/doctype/opportunity/test_records.json b/erpnext/crm/doctype/opportunity/test_records.json
index a1e0ad921b..f7e8350f30 100644
--- a/erpnext/crm/doctype/opportunity/test_records.json
+++ b/erpnext/crm/doctype/opportunity/test_records.json
@@ -8,7 +8,9 @@
"transaction_date": "2013-12-12",
"items": [{
"item_name": "Test Item",
- "description": "Some description"
+ "description": "Some description",
+ "qty": 5,
+ "rate": 100
}]
}
]
diff --git a/erpnext/crm/doctype/prospect/prospect.js b/erpnext/crm/doctype/prospect/prospect.js
index 8721a5b42d..495ed291ae 100644
--- a/erpnext/crm/doctype/prospect/prospect.js
+++ b/erpnext/crm/doctype/prospect/prospect.js
@@ -27,5 +27,26 @@ frappe.ui.form.on('Prospect', {
} else {
frappe.contacts.clear_address_and_contact(frm);
}
+ frm.trigger("show_notes");
+ frm.trigger("show_activities");
+ },
+
+ show_notes (frm) {
+ const crm_notes = new erpnext.utils.CRMNotes({
+ frm: frm,
+ notes_wrapper: $(frm.fields_dict.notes_html.wrapper),
+ });
+ crm_notes.refresh();
+ },
+
+ show_activities (frm) {
+ const crm_activities = new erpnext.utils.CRMActivities({
+ frm: frm,
+ open_activities_wrapper: $(frm.fields_dict.open_activities_html.wrapper),
+ all_activities_wrapper: $(frm.fields_dict.all_activities_html.wrapper),
+ form_wrapper: $(frm.wrapper),
+ });
+ crm_activities.refresh();
}
+
});
diff --git a/erpnext/crm/doctype/prospect/prospect.json b/erpnext/crm/doctype/prospect/prospect.json
index c9554ba31a..afc6c1dbec 100644
--- a/erpnext/crm/doctype/prospect/prospect.json
+++ b/erpnext/crm/doctype/prospect/prospect.json
@@ -1,33 +1,42 @@
{
"actions": [],
+ "allow_events_in_timeline": 1,
"autoname": "field:company_name",
"creation": "2021-08-19 00:21:06.995448",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
+ "overview_tab",
"company_name",
- "industry",
- "market_segment",
"customer_group",
+ "no_of_employees",
+ "annual_revenue",
+ "column_break_4",
+ "market_segment",
+ "industry",
"territory",
"column_break_6",
- "no_of_employees",
- "currency",
- "annual_revenue",
- "more_details_section",
- "fax",
- "website",
- "column_break_13",
"prospect_owner",
+ "website",
+ "fax",
"company",
- "leads_section",
- "prospect_lead",
"address_and_contact_section",
+ "column_break_16",
+ "contacts_tab",
"address_html",
- "column_break_17",
+ "column_break_18",
"contact_html",
+ "leads_section",
+ "leads",
+ "opportunities_tab",
+ "opportunities",
+ "activities_tab",
+ "open_activities_html",
+ "all_activities_section",
+ "all_activities_html",
"notes_section",
+ "notes_html",
"notes"
],
"fields": [
@@ -71,15 +80,9 @@
},
{
"fieldname": "no_of_employees",
- "fieldtype": "Int",
- "label": "No. of Employees"
- },
- {
- "fieldname": "currency",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Currency",
- "options": "Currency"
+ "fieldtype": "Select",
+ "label": "No. of Employees",
+ "options": "1-10\n11-20\n21-30\n31-100\n11-50\n51-200\n201-500\n101-500\n500-1000\n501-1000\n>1000\n1000+"
},
{
"fieldname": "annual_revenue",
@@ -97,8 +100,7 @@
{
"fieldname": "website",
"fieldtype": "Data",
- "label": "Website",
- "options": "URL"
+ "label": "Website"
},
{
"fieldname": "prospect_owner",
@@ -108,23 +110,14 @@
},
{
"fieldname": "leads_section",
- "fieldtype": "Section Break",
+ "fieldtype": "Tab Break",
"label": "Leads"
},
- {
- "fieldname": "prospect_lead",
- "fieldtype": "Table",
- "options": "Prospect Lead"
- },
{
"fieldname": "address_html",
"fieldtype": "HTML",
"label": "Address HTML"
},
- {
- "fieldname": "column_break_17",
- "fieldtype": "Column Break"
- },
{
"fieldname": "contact_html",
"fieldtype": "HTML",
@@ -132,28 +125,16 @@
},
{
"collapsible": 1,
+ "depends_on": "eval:!doc.__islocal",
"fieldname": "notes_section",
- "fieldtype": "Section Break",
+ "fieldtype": "Tab Break",
"label": "Notes"
},
- {
- "fieldname": "notes",
- "fieldtype": "Text Editor"
- },
- {
- "fieldname": "more_details_section",
- "fieldtype": "Section Break",
- "label": "More Details"
- },
- {
- "fieldname": "column_break_13",
- "fieldtype": "Column Break"
- },
{
"depends_on": "eval: !doc.__islocal",
"fieldname": "address_and_contact_section",
"fieldtype": "Section Break",
- "label": "Address and Contact"
+ "label": "Address"
},
{
"fieldname": "company",
@@ -161,11 +142,83 @@
"label": "Company",
"options": "Company",
"reqd": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "opportunities_tab",
+ "fieldtype": "Tab Break",
+ "label": "Opportunities"
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "activities_tab",
+ "fieldtype": "Tab Break",
+ "label": "Activities"
+ },
+ {
+ "fieldname": "notes_html",
+ "fieldtype": "HTML",
+ "label": "Notes HTML"
+ },
+ {
+ "fieldname": "opportunities",
+ "fieldtype": "Table",
+ "label": "Opportunities",
+ "options": "Prospect Opportunity"
+ },
+ {
+ "fieldname": "contacts_tab",
+ "fieldtype": "Tab Break",
+ "label": "Address & Contact"
+ },
+ {
+ "fieldname": "column_break_18",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "leads",
+ "fieldtype": "Table",
+ "options": "Prospect Lead"
+ },
+ {
+ "fieldname": "column_break_16",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "overview_tab",
+ "fieldtype": "Tab Break",
+ "label": "Overview"
+ },
+ {
+ "fieldname": "open_activities_html",
+ "fieldtype": "HTML",
+ "label": "Open Activities HTML"
+ },
+ {
+ "fieldname": "all_activities_section",
+ "fieldtype": "Section Break",
+ "label": "All Activities"
+ },
+ {
+ "fieldname": "all_activities_html",
+ "fieldtype": "HTML",
+ "label": "All Activities HTML"
+ },
+ {
+ "fieldname": "notes",
+ "fieldtype": "Table",
+ "hidden": 1,
+ "label": "Notes",
+ "no_copy": 1,
+ "options": "CRM Note"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-11-01 13:10:36.759249",
+ "modified": "2022-06-21 15:10:26.887502",
"modified_by": "Administrator",
"module": "CRM",
"name": "Prospect",
@@ -207,6 +260,7 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "company_name",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/prospect/prospect.py b/erpnext/crm/doctype/prospect/prospect.py
index 39436f5918..fbb115883f 100644
--- a/erpnext/crm/doctype/prospect/prospect.py
+++ b/erpnext/crm/doctype/prospect/prospect.py
@@ -3,19 +3,15 @@
import frappe
from frappe.contacts.address_and_contact import load_address_and_contact
-from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
-from erpnext.crm.utils import add_link_in_communication, copy_comments
+from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events
-class Prospect(Document):
+class Prospect(CRMNote):
def onload(self):
load_address_and_contact(self)
- def validate(self):
- self.update_lead_details()
-
def on_update(self):
self.link_with_lead_contact_and_address()
@@ -23,23 +19,24 @@ class Prospect(Document):
self.unlink_dynamic_links()
def after_insert(self):
- if frappe.db.get_single_value("CRM Settings", "carry_forward_communication_and_comments"):
- for row in self.get("prospect_lead"):
- copy_comments("Lead", row.lead, self)
- add_link_in_communication("Lead", row.lead, self)
+ carry_forward_communication_and_comments = frappe.db.get_single_value(
+ "CRM Settings", "carry_forward_communication_and_comments"
+ )
- def update_lead_details(self):
- for row in self.get("prospect_lead"):
- lead = frappe.get_value(
- "Lead", row.lead, ["lead_name", "status", "email_id", "mobile_no"], as_dict=True
- )
- row.lead_name = lead.lead_name
- row.status = lead.status
- row.email = lead.email_id
- row.mobile_no = lead.mobile_no
+ for row in self.get("leads"):
+ if carry_forward_communication_and_comments:
+ copy_comments("Lead", row.lead, self)
+ link_communications("Lead", row.lead, self)
+ link_open_events("Lead", row.lead, self)
+
+ for row in self.get("opportunities"):
+ if carry_forward_communication_and_comments:
+ copy_comments("Opportunity", row.opportunity, self)
+ link_communications("Opportunity", row.opportunity, self)
+ link_open_events("Opportunity", row.opportunity, self)
def link_with_lead_contact_and_address(self):
- for row in self.prospect_lead:
+ for row in self.leads:
links = frappe.get_all(
"Dynamic Link",
filters={"link_doctype": "Lead", "link_name": row.lead},
@@ -116,9 +113,7 @@ def make_opportunity(source_name, target_doc=None):
{
"Prospect": {
"doctype": "Opportunity",
- "field_map": {
- "name": "party_name",
- },
+ "field_map": {"name": "party_name", "prospect_owner": "opportunity_owner"},
}
},
target_doc,
@@ -127,3 +122,25 @@ def make_opportunity(source_name, target_doc=None):
)
return doclist
+
+
+@frappe.whitelist()
+def get_opportunities(prospect):
+ return frappe.get_all(
+ "Opportunity",
+ filters={"opportunity_from": "Prospect", "party_name": prospect},
+ fields=[
+ "opportunity_owner",
+ "sales_stage",
+ "status",
+ "expected_closing",
+ "probability",
+ "opportunity_amount",
+ "currency",
+ "contact_person",
+ "contact_email",
+ "contact_mobile",
+ "creation",
+ "name",
+ ],
+ )
diff --git a/erpnext/crm/doctype/prospect/test_prospect.py b/erpnext/crm/doctype/prospect/test_prospect.py
index ddd7b932aa..874f84ca84 100644
--- a/erpnext/crm/doctype/prospect/test_prospect.py
+++ b/erpnext/crm/doctype/prospect/test_prospect.py
@@ -20,7 +20,7 @@ class TestProspect(unittest.TestCase):
add_lead_to_prospect(lead_doc.name, prospect_doc.name)
prospect_doc.reload()
lead_exists_in_prosoect = False
- for rec in prospect_doc.get("prospect_lead"):
+ for rec in prospect_doc.get("leads"):
if rec.lead == lead_doc.name:
lead_exists_in_prosoect = True
self.assertEqual(lead_exists_in_prosoect, True)
diff --git a/erpnext/crm/doctype/prospect_lead/prospect_lead.json b/erpnext/crm/doctype/prospect_lead/prospect_lead.json
index 3c160d9e80..075c0f9be5 100644
--- a/erpnext/crm/doctype/prospect_lead/prospect_lead.json
+++ b/erpnext/crm/doctype/prospect_lead/prospect_lead.json
@@ -7,12 +7,15 @@
"field_order": [
"lead",
"lead_name",
- "status",
"email",
- "mobile_no"
+ "column_break_4",
+ "mobile_no",
+ "lead_owner",
+ "status"
],
"fields": [
{
+ "columns": 2,
"fieldname": "lead",
"fieldtype": "Link",
"in_list_view": 1,
@@ -21,6 +24,8 @@
"reqd": 1
},
{
+ "columns": 2,
+ "fetch_from": "lead.lead_name",
"fieldname": "lead_name",
"fieldtype": "Data",
"in_list_view": 1,
@@ -28,14 +33,17 @@
"read_only": 1
},
{
+ "columns": 1,
+ "fetch_from": "lead.status",
"fieldname": "status",
- "fieldtype": "Select",
+ "fieldtype": "Data",
"in_list_view": 1,
"label": "Status",
- "options": "Lead\nOpen\nReplied\nOpportunity\nQuotation\nLost Quotation\nInterested\nConverted\nDo Not Contact",
"read_only": 1
},
{
+ "columns": 2,
+ "fetch_from": "lead.email_id",
"fieldname": "email",
"fieldtype": "Data",
"in_list_view": 1,
@@ -44,18 +52,32 @@
"read_only": 1
},
{
+ "columns": 2,
+ "fetch_from": "lead.mobile_no",
"fieldname": "mobile_no",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Mobile No",
"options": "Phone",
"read_only": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "columns": 1,
+ "fetch_from": "lead.lead_owner",
+ "fieldname": "lead_owner",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Lead Owner"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-08-25 12:58:24.638054",
+ "modified": "2022-04-28 20:27:58.805970",
"modified_by": "Administrator",
"module": "CRM",
"name": "Prospect Lead",
@@ -63,5 +85,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/prospect_opportunity/__init__.py b/erpnext/crm/doctype/prospect_opportunity/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/crm/doctype/prospect_opportunity/prospect_opportunity.json b/erpnext/crm/doctype/prospect_opportunity/prospect_opportunity.json
new file mode 100644
index 0000000000..d8c2520176
--- /dev/null
+++ b/erpnext/crm/doctype/prospect_opportunity/prospect_opportunity.json
@@ -0,0 +1,101 @@
+{
+ "actions": [],
+ "autoname": "autoincrement",
+ "creation": "2022-04-27 17:40:37.965161",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "opportunity",
+ "amount",
+ "stage",
+ "deal_owner",
+ "column_break_4",
+ "probability",
+ "expected_closing",
+ "currency",
+ "contact_person"
+ ],
+ "fields": [
+ {
+ "columns": 2,
+ "fieldname": "opportunity",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Opportunity",
+ "options": "Opportunity"
+ },
+ {
+ "columns": 2,
+ "fetch_from": "opportunity.opportunity_amount",
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Amount",
+ "options": "currency"
+ },
+ {
+ "columns": 2,
+ "fetch_from": "opportunity.sales_stage",
+ "fieldname": "stage",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Stage"
+ },
+ {
+ "columns": 1,
+ "fetch_from": "opportunity.probability",
+ "fieldname": "probability",
+ "fieldtype": "Percent",
+ "in_list_view": 1,
+ "label": "Probability"
+ },
+ {
+ "columns": 1,
+ "fetch_from": "opportunity.expected_closing",
+ "fieldname": "expected_closing",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Closing"
+ },
+ {
+ "fetch_from": "opportunity.currency",
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "label": "Currency",
+ "options": "Currency"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "columns": 2,
+ "fetch_from": "opportunity.opportunity_owner",
+ "fieldname": "deal_owner",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Deal Owner"
+ },
+ {
+ "fetch_from": "opportunity.contact_person",
+ "fieldname": "contact_person",
+ "fieldtype": "Link",
+ "label": "Contact Person",
+ "options": "Contact"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2022-04-28 10:05:38.730368",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "Prospect Opportunity",
+ "naming_rule": "Autoincrement",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/prospect_opportunity/prospect_opportunity.py b/erpnext/crm/doctype/prospect_opportunity/prospect_opportunity.py
new file mode 100644
index 0000000000..8f5d19aaf2
--- /dev/null
+++ b/erpnext/crm/doctype/prospect_opportunity/prospect_opportunity.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class ProspectOpportunity(Document):
+ pass
diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.js b/erpnext/crm/report/lost_opportunity/lost_opportunity.js
index 97c56f8c43..927c54df07 100644
--- a/erpnext/crm/report/lost_opportunity/lost_opportunity.js
+++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.js
@@ -57,11 +57,5 @@ frappe.query_reports["Lost Opportunity"] = {
"fieldtype": "Dynamic Link",
"options": "opportunity_from"
},
- {
- "fieldname":"contact_by",
- "label": __("Next Contact By"),
- "fieldtype": "Link",
- "options": "User"
- },
]
};
diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.json b/erpnext/crm/report/lost_opportunity/lost_opportunity.json
index e7a8e12ba7..f6f36bd2b5 100644
--- a/erpnext/crm/report/lost_opportunity/lost_opportunity.json
+++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.json
@@ -7,8 +7,8 @@
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
- "json": "{\"order_by\": \"`tabOpportunity`.`modified` desc\", \"filters\": [[\"Opportunity\", \"status\", \"=\", \"Lost\"]], \"fields\": [[\"name\", \"Opportunity\"], [\"opportunity_from\", \"Opportunity\"], [\"party_name\", \"Opportunity\"], [\"customer_name\", \"Opportunity\"], [\"opportunity_type\", \"Opportunity\"], [\"status\", \"Opportunity\"], [\"contact_by\", \"Opportunity\"], [\"docstatus\", \"Opportunity\"], [\"lost_reason\", \"Lost Reason Detail\"]], \"add_totals_row\": 0, \"add_total_row\": 0, \"page_length\": 20}",
- "modified": "2020-07-29 15:49:02.848845",
+ "json": "{\"order_by\": \"`tabOpportunity`.`modified` desc\", \"filters\": [[\"Opportunity\", \"status\", \"=\", \"Lost\"]], \"fields\": [[\"name\", \"Opportunity\"], [\"opportunity_from\", \"Opportunity\"], [\"party_name\", \"Opportunity\"], [\"customer_name\", \"Opportunity\"], [\"opportunity_type\", \"Opportunity\"], [\"status\", \"Opportunity\"], [\"docstatus\", \"Opportunity\"], [\"lost_reason\", \"Lost Reason Detail\"]], \"add_totals_row\": 0, \"add_total_row\": 0, \"page_length\": 20}",
+ "modified": "2022-06-04 15:49:02.848845",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lost Opportunity",
diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.py b/erpnext/crm/report/lost_opportunity/lost_opportunity.py
index a57b44be47..254511c92f 100644
--- a/erpnext/crm/report/lost_opportunity/lost_opportunity.py
+++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.py
@@ -61,13 +61,6 @@ def get_columns():
"options": "Territory",
"width": 150,
},
- {
- "label": _("Next Contact By"),
- "fieldname": "contact_by",
- "fieldtype": "Link",
- "options": "User",
- "width": 150,
- },
]
return columns
@@ -81,7 +74,6 @@ def get_data(filters):
`tabOpportunity`.party_name,
`tabOpportunity`.customer_name,
`tabOpportunity`.opportunity_type,
- `tabOpportunity`.contact_by,
GROUP_CONCAT(`tabOpportunity Lost Reason Detail`.lost_reason separator ', ') lost_reason,
`tabOpportunity`.sales_stage,
`tabOpportunity`.territory
@@ -115,9 +107,6 @@ def get_conditions(filters):
if filters.get("party_name"):
conditions.append(" and `tabOpportunity`.party_name=%(party_name)s")
- if filters.get("contact_by"):
- conditions.append(" and `tabOpportunity`.contact_by=%(contact_by)s")
-
return " ".join(conditions) if conditions else ""
diff --git a/erpnext/crm/utils.py b/erpnext/crm/utils.py
index 5783b2c661..33441b166d 100644
--- a/erpnext/crm/utils.py
+++ b/erpnext/crm/utils.py
@@ -1,4 +1,6 @@
import frappe
+from frappe.model.document import Document
+from frappe.utils import cstr, now
def update_lead_phone_numbers(contact, method):
@@ -41,7 +43,7 @@ def copy_comments(doctype, docname, doc):
comment.insert()
-def add_link_in_communication(doctype, docname, doc):
+def link_communications(doctype, docname, doc):
communication_list = get_linked_communication_list(doctype, docname)
for communication in communication_list:
@@ -60,3 +62,138 @@ def get_linked_communication_list(doctype, docname):
)
return communications + communication_links
+
+
+def link_communications_with_prospect(communication, method):
+ prospect = get_linked_prospect(communication.reference_doctype, communication.reference_name)
+
+ if prospect:
+ already_linked = any(
+ [
+ d.name
+ for d in communication.get("timeline_links")
+ if d.link_doctype == "Prospect" and d.link_name == prospect
+ ]
+ )
+ if not already_linked:
+ row = communication.append("timeline_links")
+ row.link_doctype = "Prospect"
+ row.link_name = prospect
+ row.db_update()
+
+
+def get_linked_prospect(reference_doctype, reference_name):
+ prospect = None
+ if reference_doctype == "Lead":
+ prospect = frappe.db.get_value("Prospect Lead", {"lead": reference_name}, "parent")
+
+ elif reference_doctype == "Opportunity":
+ opportunity_from, party_name = frappe.db.get_value(
+ "Opportunity", reference_name, ["opportunity_from", "party_name"]
+ )
+ if opportunity_from == "Lead":
+ prospect = frappe.db.get_value(
+ "Prospect Opportunity", {"opportunity": reference_name}, "parent"
+ )
+ if opportunity_from == "Prospect":
+ prospect = party_name
+
+ return prospect
+
+
+def link_events_with_prospect(event, method):
+ if event.event_participants:
+ ref_doctype = event.event_participants[0].reference_doctype
+ ref_docname = event.event_participants[0].reference_docname
+ prospect = get_linked_prospect(ref_doctype, ref_docname)
+ if prospect:
+ event.add_participant("Prospect", prospect)
+ event.save()
+
+
+def link_open_tasks(ref_doctype, ref_docname, doc):
+ todos = get_open_todos(ref_doctype, ref_docname)
+
+ for todo in todos:
+ todo_doc = frappe.get_doc("ToDo", todo.name)
+ todo_doc.reference_type = doc.doctype
+ todo_doc.reference_name = doc.name
+ todo_doc.db_update()
+
+
+def link_open_events(ref_doctype, ref_docname, doc):
+ events = get_open_events(ref_doctype, ref_docname)
+ for event in events:
+ event_doc = frappe.get_doc("Event", event.name)
+ event_doc.add_participant(doc.doctype, doc.name)
+ event_doc.save()
+
+
+@frappe.whitelist()
+def get_open_activities(ref_doctype, ref_docname):
+ tasks = get_open_todos(ref_doctype, ref_docname)
+ events = get_open_events(ref_doctype, ref_docname)
+
+ return {"tasks": tasks, "events": events}
+
+
+def get_open_todos(ref_doctype, ref_docname):
+ return frappe.get_all(
+ "ToDo",
+ filters={"reference_type": ref_doctype, "reference_name": ref_docname, "status": "Open"},
+ fields=[
+ "name",
+ "description",
+ "allocated_to",
+ "date",
+ ],
+ )
+
+
+def get_open_events(ref_doctype, ref_docname):
+ event = frappe.qb.DocType("Event")
+ event_link = frappe.qb.DocType("Event Participants")
+
+ query = (
+ frappe.qb.from_(event)
+ .join(event_link)
+ .on(event_link.parent == event.name)
+ .select(
+ event.name,
+ event.subject,
+ event.event_category,
+ event.starts_on,
+ event.ends_on,
+ event.description,
+ )
+ .where(
+ (event_link.reference_doctype == ref_doctype)
+ & (event_link.reference_docname == ref_docname)
+ & (event.status == "Open")
+ )
+ )
+ data = query.run(as_dict=True)
+
+ return data
+
+
+class CRMNote(Document):
+ @frappe.whitelist()
+ def add_note(self, note):
+ self.append("notes", {"note": note, "added_by": frappe.session.user, "added_on": now()})
+ self.save()
+
+ @frappe.whitelist()
+ def edit_note(self, note, row_id):
+ for d in self.notes:
+ if cstr(d.name) == row_id:
+ d.note = note
+ d.db_update()
+
+ @frappe.whitelist()
+ def delete_note(self, row_id):
+ for d in self.notes:
+ if cstr(d.name) == row_id:
+ self.remove(d)
+ break
+ self.save()
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 7d7f65dfd7..b3c35cfe0b 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -299,7 +299,11 @@ doc_events = {
"on_update": [
"erpnext.support.doctype.service_level_agreement.service_level_agreement.on_communication_update",
"erpnext.support.doctype.issue.issue.set_first_response_time",
- ]
+ ],
+ "after_insert": "erpnext.crm.utils.link_communications_with_prospect",
+ },
+ "Event": {
+ "after_insert": "erpnext.crm.utils.link_events_with_prospect",
},
"Sales Taxes and Charges Template": {
"on_update": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.validate_cart_settings"
@@ -453,7 +457,6 @@ scheduler_events = {
"erpnext.hr.utils.allocate_earned_leaves",
"erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall",
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
- "erpnext.crm.doctype.lead.lead.daily_open_lead",
],
"weekly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_weekly"],
"monthly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_monthly"],
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 8a28454af2..a73b9bcc69 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -25,6 +25,7 @@ from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_childr
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
+from erpnext.utilities.transaction_base import validate_uom_is_integer
class ProductionPlan(Document):
@@ -33,6 +34,7 @@ class ProductionPlan(Document):
self.calculate_total_planned_qty()
self.set_status()
self._rename_temporary_references()
+ validate_uom_is_integer(self, "stock_uom", "planned_qty")
def set_pending_qty_in_row_without_reference(self):
"Set Pending Qty in independent rows (not from SO or MR)."
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index e88049d810..040e791e00 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -679,15 +679,23 @@ class TestProductionPlan(FrappeTestCase):
self.assertFalse(pp.all_items_completed())
def test_production_plan_planned_qty(self):
- pln = create_production_plan(item_code="_Test FG Item", planned_qty=0.55)
- pln.make_work_order()
- work_order = frappe.db.get_value("Work Order", {"production_plan": pln.name}, "name")
- wo_doc = frappe.get_doc("Work Order", work_order)
- wo_doc.update(
- {"wip_warehouse": "Work In Progress - _TC", "fg_warehouse": "Finished Goods - _TC"}
+ # Case 1: When Planned Qty is non-integer and UOM is integer.
+ from erpnext.utilities.transaction_base import UOMMustBeIntegerError
+
+ self.assertRaises(
+ UOMMustBeIntegerError, create_production_plan, item_code="_Test FG Item", planned_qty=0.55
)
- wo_doc.submit()
- self.assertEqual(wo_doc.qty, 0.55)
+
+ # Case 2: When Planned Qty is non-integer and UOM is also non-integer.
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name
+ bom_item = make_item().name
+
+ make_bom(item=fg_item, raw_materials=[bom_item], source_warehouse="_Test Warehouse - _TC")
+
+ pln = create_production_plan(item_code=fg_item, planned_qty=0.55, stock_uom="_Test UOM 1")
+ self.assertEqual(pln.po_items[0].planned_qty, 0.55)
def test_temporary_name_relinking(self):
@@ -751,6 +759,7 @@ def create_production_plan(**args):
"bom_no": frappe.db.get_value("Item", args.item_code, "default_bom"),
"planned_qty": args.planned_qty or 1,
"planned_start_date": args.planned_start_date or now_datetime(),
+ "stock_uom": args.stock_uom or "Nos",
},
)
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 318875d2a4..2addf91976 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -375,3 +375,4 @@ execute:frappe.delete_doc("DocType", "Naming Series")
erpnext.patches.v13_0.set_payroll_entry_status
erpnext.patches.v13_0.job_card_status_on_hold
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
+erpnext.patches.v14_0.crm_ux_cleanup
diff --git a/erpnext/patches/v14_0/crm_ux_cleanup.py b/erpnext/patches/v14_0/crm_ux_cleanup.py
new file mode 100644
index 0000000000..b2df36ff35
--- /dev/null
+++ b/erpnext/patches/v14_0/crm_ux_cleanup.py
@@ -0,0 +1,94 @@
+import frappe
+from frappe.model.utils.rename_field import rename_field
+from frappe.utils import add_months, cstr, today
+
+
+def execute():
+ for doctype in ("CRM Note", "Lead", "Opportunity", "Prospect", "Prospect Lead"):
+ frappe.reload_doc("crm", "doctype", doctype)
+
+ try:
+ rename_field("Lead", "designation", "job_title")
+ rename_field("Opportunity", "converted_by", "opportunity_owner")
+
+ frappe.db.sql(
+ """
+ update `tabProspect Lead`
+ set parentfield='leads'
+ where parentfield='partner_lead'
+ """
+ )
+ except Exception as e:
+ if e.args[0] != 1054:
+ raise
+
+ add_calendar_event_for_leads()
+ add_calendar_event_for_opportunities()
+
+
+def add_calendar_event_for_leads():
+ # create events based on next contact date
+ leads = frappe.db.sql(
+ """
+ select name, contact_date, contact_by, ends_on, lead_name, lead_owner
+ from tabLead
+ where contact_date >= %s
+ """,
+ add_months(today(), -1),
+ as_dict=1,
+ )
+
+ for d in leads:
+ event = frappe.get_doc(
+ {
+ "doctype": "Event",
+ "owner": d.lead_owner,
+ "subject": ("Contact " + cstr(d.lead_name)),
+ "description": (
+ ("Contact " + cstr(d.lead_name)) + (("
By: " + cstr(d.contact_by)) if d.contact_by else "")
+ ),
+ "starts_on": d.contact_date,
+ "ends_on": d.ends_on,
+ "event_type": "Private",
+ }
+ )
+
+ event.append("event_participants", {"reference_doctype": "Lead", "reference_docname": d.name})
+
+ event.insert(ignore_permissions=True)
+
+
+def add_calendar_event_for_opportunities():
+ # create events based on next contact date
+ opportunities = frappe.db.sql(
+ """
+ select name, contact_date, contact_by, to_discuss,
+ party_name, opportunity_owner, contact_person
+ from tabOpportunity
+ where contact_date >= %s
+ """,
+ add_months(today(), -1),
+ as_dict=1,
+ )
+
+ for d in opportunities:
+ event = frappe.get_doc(
+ {
+ "doctype": "Event",
+ "owner": d.opportunity_owner,
+ "subject": ("Contact " + cstr(d.contact_person or d.party_name)),
+ "description": (
+ ("Contact " + cstr(d.contact_person or d.party_name))
+ + (("
By: " + cstr(d.contact_by)) if d.contact_by else "")
+ + (("
Agenda: " + cstr(d.to_discuss)) if d.to_discuss else "")
+ ),
+ "starts_on": d.contact_date,
+ "event_type": "Private",
+ }
+ )
+
+ event.append(
+ "event_participants", {"reference_doctype": "Opportunity", "reference_docname": d.name}
+ )
+
+ event.insert(ignore_permissions=True)
diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py
index 57bfd5b607..7298c037a7 100644
--- a/erpnext/projects/doctype/timesheet/test_timesheet.py
+++ b/erpnext/projects/doctype/timesheet/test_timesheet.py
@@ -84,7 +84,9 @@ class TestTimesheet(unittest.TestCase):
emp = make_employee("test_employee_6@salary.com")
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
- sales_invoice = make_sales_invoice(timesheet.name, "_Test Item", "_Test Customer")
+ sales_invoice = make_sales_invoice(
+ timesheet.name, "_Test Item", "_Test Customer", currency="INR"
+ )
sales_invoice.due_date = nowdate()
sales_invoice.submit()
timesheet = frappe.get_doc("Timesheet", timesheet.name)
diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js
index 3dae6d407b..d545929ce5 100644
--- a/erpnext/public/js/erpnext.bundle.js
+++ b/erpnext/public/js/erpnext.bundle.js
@@ -22,5 +22,8 @@ import "./utils/barcode_scanner";
import "./telephony";
import "./templates/call_link.html";
import "./bulk_transaction_processing";
+import "./utils/crm_activities";
+import "./templates/crm_activities.html";
+import "./templates/crm_notes.html";
// import { sum } from 'frappe/public/utils/util.js'
diff --git a/erpnext/public/js/templates/crm_activities.html b/erpnext/public/js/templates/crm_activities.html
new file mode 100644
index 0000000000..4260319608
--- /dev/null
+++ b/erpnext/public/js/templates/crm_activities.html
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+
+
+
+
+ {{ __("Open Tasks") }}
+
+ {% if (tasks.length) { %}
+ {% for(var i=0, l=tasks.length; i
+
+ {% if(tasks[i].date) { %}
+
+ {%= frappe.datetime.global_date_format(tasks[i].date) %}
+
+ {% } %}
+ {% if(tasks[i].allocated_to) { %}
+
+ {{ __("Allocated To:") }}
+ {%= tasks[i].allocated_to %}
+
+ {% } %}
+
+ {% } %}
+ {% } else { %}
+
+ {{ __("No open task") }}
+
+ {% } %}
+
+
+
+ {{ __("Open Events") }}
+
+ {% if (events.length) { %}
+ {% let icon_set = {"Sent/Received Email": "mail", "Call": "call", "Meeting": "share-people"}; %}
+ {% for(var i=0, l=events.length; i
+
+
+ {%= frappe.datetime.global_date_format(events[i].starts_on) %}
+
+ {% if (events[i].ends_on) { %}
+ {% if (frappe.datetime.obj_to_user(events[i].starts_on) != frappe.datetime.obj_to_user(events[i].ends_on)) %}
+ -
+ {%= frappe.datetime.global_date_format(frappe.datetime.obj_to_user(events[i].ends_on)) %}
+ {%= frappe.datetime.get_time(events[i].ends_on) %}
+ {% } else if (events[i].ends_on) { %}
+ -
+ {%= frappe.datetime.get_time(events[i].ends_on) %}
+ {% } %}
+ {% } %}
+
+
+
+ {% } %}
+ {% } else { %}
+
+ {{ __("No open event") }}
+
+ {% } %}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/erpnext/public/js/templates/crm_notes.html b/erpnext/public/js/templates/crm_notes.html
new file mode 100644
index 0000000000..fddeb1c1cc
--- /dev/null
+++ b/erpnext/public/js/templates/crm_notes.html
@@ -0,0 +1,74 @@
+
+
+
+
+
+ {% if (notes.length) { %}
+ {% for(var i=0, l=notes.length; i
+ {% } %}
+ {% } else { %}
+
+ {{ __("No Notes") }}
+
+ {% } %}
+
+
+
+
\ No newline at end of file
diff --git a/erpnext/public/js/utils/crm_activities.js b/erpnext/public/js/utils/crm_activities.js
new file mode 100644
index 0000000000..bbd9ded8c9
--- /dev/null
+++ b/erpnext/public/js/utils/crm_activities.js
@@ -0,0 +1,234 @@
+erpnext.utils.CRMActivities = class CRMActivities {
+ constructor(opts) {
+ $.extend(this, opts);
+ }
+
+ refresh() {
+ var me = this;
+ $(this.open_activities_wrapper).empty();
+ let cur_form_footer = this.form_wrapper.find('.form-footer');
+
+ // all activities
+ if (!$(this.all_activities_wrapper).find('.form-footer').length) {
+ this.all_activities_wrapper.empty();
+ $(cur_form_footer).appendTo(this.all_activities_wrapper);
+
+ // remove frappe-control class to avoid absolute position for action-btn
+ $(this.all_activities_wrapper).removeClass('frappe-control');
+ // hide new event button
+ $('.timeline-actions').find('.btn-default').hide();
+ // hide new comment box
+ $(".comment-box").hide();
+ // show only communications by default
+ $($('.timeline-content').find('.nav-link')[0]).tab('show');
+ }
+
+ // open activities
+ frappe.call({
+ method: "erpnext.crm.utils.get_open_activities",
+ args: {
+ ref_doctype: this.frm.doc.doctype,
+ ref_docname: this.frm.doc.name
+ },
+ callback: (r) => {
+ if (!r.exc) {
+ var activities_html = frappe.render_template('crm_activities', {
+ tasks: r.message.tasks,
+ events: r.message.events
+ });
+
+ $(activities_html).appendTo(me.open_activities_wrapper);
+
+ $(".open-tasks").find(".completion-checkbox").on("click", function() {
+ me.update_status(this, "ToDo");
+ });
+
+ $(".open-events").find(".completion-checkbox").on("click", function() {
+ me.update_status(this, "Event");
+ });
+
+ me.create_task();
+ me.create_event();
+ }
+ }
+ });
+ }
+
+ create_task () {
+ let me = this;
+ let _create_task = () => {
+ const args = {
+ doc: me.frm.doc,
+ frm: me.frm,
+ title: __("New Task")
+ };
+ let composer = new frappe.views.InteractionComposer(args);
+ composer.dialog.get_field('interaction_type').set_value("ToDo");
+ // hide column having interaction type field
+ $(composer.dialog.get_field('interaction_type').wrapper).closest('.form-column').hide();
+ // hide summary field
+ $(composer.dialog.get_field('summary').wrapper).closest('.form-section').hide();
+ };
+ $(".new-task-btn").click(_create_task);
+ }
+
+ create_event () {
+ let me = this;
+ let _create_event = () => {
+ const args = {
+ doc: me.frm.doc,
+ frm: me.frm,
+ title: __("New Event")
+ };
+ let composer = new frappe.views.InteractionComposer(args);
+ composer.dialog.get_field('interaction_type').set_value("Event");
+ $(composer.dialog.get_field('interaction_type').wrapper).hide();
+ };
+ $(".new-event-btn").click(_create_event);
+ }
+
+ async update_status (input_field, doctype) {
+ let completed = $(input_field).prop("checked") ? 1 : 0;
+ let docname = $(input_field).attr("name");
+ if (completed) {
+ await frappe.db.set_value(doctype, docname, "status", "Closed");
+ this.refresh();
+ }
+ }
+};
+
+erpnext.utils.CRMNotes = class CRMNotes {
+ constructor(opts) {
+ $.extend(this, opts);
+ }
+
+ refresh() {
+ var me = this;
+ this.notes_wrapper.find('.notes-section').remove();
+
+ let notes = this.frm.doc.notes || [];
+ notes.sort(
+ function(a, b) {
+ return new Date(b.added_on) - new Date(a.added_on);
+ }
+ );
+
+ let notes_html = frappe.render_template(
+ 'crm_notes',
+ {
+ notes: notes
+ }
+ );
+ $(notes_html).appendTo(this.notes_wrapper);
+
+ this.add_note();
+
+ $(".notes-section").find(".edit-note-btn").on("click", function() {
+ me.edit_note(this);
+ });
+
+ $(".notes-section").find(".delete-note-btn").on("click", function() {
+ me.delete_note(this);
+ });
+ }
+
+
+ add_note () {
+ let me = this;
+ let _add_note = () => {
+ var d = new frappe.ui.Dialog({
+ title: __('Add a Note'),
+ fields: [
+ {
+ "label": "Note",
+ "fieldname": "note",
+ "fieldtype": "Text Editor",
+ "reqd": 1
+ }
+ ],
+ primary_action: function() {
+ var data = d.get_values();
+ frappe.call({
+ method: "add_note",
+ doc: me.frm.doc,
+ args: {
+ note: data.note
+ },
+ freeze: true,
+ callback: function(r) {
+ if (!r.exc) {
+ me.frm.refresh_field("notes");
+ me.refresh();
+ }
+ d.hide();
+ }
+ });
+ },
+ primary_action_label: __('Add')
+ });
+ d.show();
+ };
+ $(".new-note-btn").click(_add_note);
+ }
+
+ edit_note (edit_btn) {
+ var me = this;
+ let row = $(edit_btn).closest('.comment-content');
+ let row_id = row.attr("name");
+ let row_content = $(row).find(".content").html();
+ if (row_content) {
+ var d = new frappe.ui.Dialog({
+ title: __('Edit Note'),
+ fields: [
+ {
+ "label": "Note",
+ "fieldname": "note",
+ "fieldtype": "Text Editor",
+ "default": row_content
+ }
+ ],
+ primary_action: function() {
+ var data = d.get_values();
+ frappe.call({
+ method: "edit_note",
+ doc: me.frm.doc,
+ args: {
+ note: data.note,
+ row_id: row_id
+ },
+ freeze: true,
+ callback: function(r) {
+ if (!r.exc) {
+ me.frm.refresh_field("notes");
+ me.refresh();
+ d.hide();
+ }
+
+ }
+ });
+ },
+ primary_action_label: __('Done')
+ });
+ d.show();
+ }
+ }
+
+ delete_note (delete_btn) {
+ var me = this;
+ let row_id = $(delete_btn).closest('.comment-content').attr("name");
+ frappe.call({
+ method: "delete_note",
+ doc: me.frm.doc,
+ args: {
+ row_id: row_id
+ },
+ freeze: true,
+ callback: function(r) {
+ if (!r.exc) {
+ me.frm.refresh_field("notes");
+ me.refresh();
+ }
+ }
+ });
+ }
+};
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index d775fa93be..863fbc4059 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -8,7 +8,6 @@ from frappe.model.mapper import get_mapped_doc
from frappe.utils import flt, getdate, nowdate
from erpnext.controllers.selling_controller import SellingController
-from erpnext.crm.utils import add_link_in_communication, copy_comments
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@@ -36,16 +35,6 @@ class Quotation(SellingController):
make_packing_list(self)
- def after_insert(self):
- if frappe.db.get_single_value("CRM Settings", "carry_forward_communication_and_comments"):
- if self.opportunity:
- copy_comments("Opportunity", self.opportunity, self)
- add_link_in_communication("Opportunity", self.opportunity, self)
-
- elif self.quotation_to == "Lead" and self.party_name:
- copy_comments("Lead", self.party_name, self)
- add_link_in_communication("Lead", self.party_name, self)
-
def validate_valid_till(self):
if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date):
frappe.throw(_("Valid till date cannot be before transaction date"))
diff --git a/erpnext/templates/utils.py b/erpnext/templates/utils.py
index 4295188dc0..48b44802a8 100644
--- a/erpnext/templates/utils.py
+++ b/erpnext/templates/utils.py
@@ -34,7 +34,6 @@ def send_message(subject="Website Query", message="", sender="", status="Open"):
status="Open",
title=subject,
contact_email=sender,
- to_discuss=message,
)
)
diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py
index 73cbcd4094..cd1bf9f321 100644
--- a/erpnext/utilities/transaction_base.py
+++ b/erpnext/utilities/transaction_base.py
@@ -5,7 +5,7 @@
import frappe
import frappe.share
from frappe import _
-from frappe.utils import cint, cstr, flt, get_time, now_datetime
+from frappe.utils import cint, flt, get_time, now_datetime
from erpnext.controllers.status_updater import StatusUpdater
@@ -30,64 +30,6 @@ class TransactionBase(StatusUpdater):
except ValueError:
frappe.throw(_("Invalid Posting Time"))
- def add_calendar_event(self, opts, force=False):
- if (
- cstr(self.contact_by) != cstr(self._prev.contact_by)
- or cstr(self.contact_date) != cstr(self._prev.contact_date)
- or force
- or (hasattr(self, "ends_on") and cstr(self.ends_on) != cstr(self._prev.ends_on))
- ):
-
- self.delete_events()
- self._add_calendar_event(opts)
-
- def delete_events(self):
- participations = frappe.get_all(
- "Event Participants",
- filters={
- "reference_doctype": self.doctype,
- "reference_docname": self.name,
- "parenttype": "Event",
- },
- fields=["name", "parent"],
- )
-
- if participations:
- for participation in participations:
- total_participants = frappe.get_all(
- "Event Participants", filters={"parenttype": "Event", "parent": participation.parent}
- )
-
- if len(total_participants) <= 1:
- frappe.db.sql("delete from `tabEvent` where name='%s'" % participation.parent)
-
- frappe.db.sql("delete from `tabEvent Participants` where name='%s'" % participation.name)
-
- def _add_calendar_event(self, opts):
- opts = frappe._dict(opts)
-
- if self.contact_date:
- event = frappe.get_doc(
- {
- "doctype": "Event",
- "owner": opts.owner or self.owner,
- "subject": opts.subject,
- "description": opts.description,
- "starts_on": self.contact_date,
- "ends_on": opts.ends_on,
- "event_type": "Private",
- }
- )
-
- event.append(
- "event_participants", {"reference_doctype": self.doctype, "reference_docname": self.name}
- )
-
- event.insert(ignore_permissions=True)
-
- if frappe.db.exists("User", self.contact_by):
- frappe.share.add("Event", event.name, self.contact_by, flags={"ignore_share_permission": True})
-
def validate_uom_is_integer(self, uom_field, qty_fields):
validate_uom_is_integer(self, uom_field, qty_fields)