diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js index 75af9379900..95cf03241bc 100644 --- a/erpnext/crm/doctype/lead/lead.js +++ b/erpnext/crm/doctype/lead/lead.js @@ -39,6 +39,8 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller 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(__("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 (!this.frm.is_new()) { @@ -49,27 +51,74 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller } } - make_customer () { + add_lead_to_prospect (frm) { + frappe.prompt([ + { + fieldname: 'prospect', + label: __('Prospect'), + fieldtype: 'Link', + options: 'Prospect', + reqd: 1 + } + ], + function(data) { + frappe.call({ + method: 'erpnext.crm.doctype.lead.lead.add_lead_to_prospect', + args: { + 'lead': frm.doc.name, + 'prospect': data.prospect + }, + callback: function(r) { + if (!r.exc) { + frm.reload_doc(); + } + }, + freeze: true, + freeze_message: __('...Adding Lead to Prospect') + }); + }, __('Add Lead to Prospect'), __('Add')); + } + + make_customer (frm) { frappe.model.open_mapped_doc({ method: "erpnext.crm.doctype.lead.lead.make_customer", - frm: cur_frm + frm: frm }) } - make_opportunity () { + make_opportunity (frm) { frappe.model.open_mapped_doc({ method: "erpnext.crm.doctype.lead.lead.make_opportunity", - frm: cur_frm + frm: frm }) } - make_quotation () { + make_quotation (frm) { frappe.model.open_mapped_doc({ method: "erpnext.crm.doctype.lead.lead.make_quotation", - frm: cur_frm + frm: frm }) } + make_prospect (frm) { + frappe.model.with_doctype("Prospect", function() { + let prospect = frappe.model.get_new_doc("Prospect"); + prospect.company_name = frm.doc.company_name; + prospect.no_of_employees = frm.doc.no_of_employees; + prospect.industry = frm.doc.industry; + prospect.market_segment = frm.doc.market_segment; + prospect.territory = frm.doc.territory; + prospect.fax = frm.doc.fax; + prospect.website = frm.doc.website; + prospect.prospect_owner = frm.doc.lead_owner; + + let lead_prospect_row = frappe.model.add_child(prospect, 'prospect_lead'); + lead_prospect_row.lead = frm.doc.name; + + frappe.set_route("Form", "Prospect", prospect.name); + }); + } + company_name () { if (!this.frm.doc.lead_name) { this.frm.set_value("lead_name", this.frm.doc.company_name); diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index cad17a3bee8..aa6c07b1be9 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -63,6 +63,7 @@ class Lead(SellingController): def on_update(self): self.add_calendar_event() + self.update_prospects() def before_insert(self): self.contact_doc = self.create_contact() @@ -89,6 +90,12 @@ class Lead(SellingController): "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 check_email_id_is_unique(self): if self.email_id: # validate email is unique @@ -354,3 +361,14 @@ 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.save(ignore_permissions=True) + frappe.msgprint(_('Lead {0} has been added to prospect {1}.').format(frappe.bold(lead), frappe.bold(prospect.name)), + title=_('Lead Added'), indicator='green') + \ No newline at end of file diff --git a/erpnext/crm/doctype/lead/lead_dashboard.py b/erpnext/crm/doctype/lead/lead_dashboard.py index 3950d063f22..50e88a5188e 100644 --- a/erpnext/crm/doctype/lead/lead_dashboard.py +++ b/erpnext/crm/doctype/lead/lead_dashboard.py @@ -13,7 +13,7 @@ def get_data(): }, 'transactions': [ { - 'items': ['Opportunity', 'Quotation'] + 'items': ['Opportunity', 'Quotation', 'Prospect'] }, ] } diff --git a/erpnext/crm/doctype/lead/lead_list.js b/erpnext/crm/doctype/lead/lead_list.js new file mode 100644 index 00000000000..75208fa64ba --- /dev/null +++ b/erpnext/crm/doctype/lead/lead_list.js @@ -0,0 +1,28 @@ +frappe.listview_settings['Lead'] = { + onload: function(listview) { + if (frappe.boot.user.can_create.includes("Prospect")) { + listview.page.add_action_item(__("Create Prospect"), function() { + frappe.model.with_doctype("Prospect", function() { + let prospect = frappe.model.get_new_doc("Prospect"); + let leads = listview.get_checked_items(); + frappe.db.get_value("Lead", leads[0].name, ["company_name", "no_of_employees", "industry", "market_segment", "territory", "fax", "website", "lead_owner"], (r) => { + prospect.company_name = r.company_name; + prospect.no_of_employees = r.no_of_employees; + prospect.industry = r.industry; + prospect.market_segment = r.market_segment; + prospect.territory = r.territory; + prospect.fax = r.fax; + prospect.website = r.website; + prospect.prospect_owner = r.lead_owner; + + leads.forEach(function(lead) { + let lead_prospect_row = frappe.model.add_child(prospect, 'prospect_lead'); + lead_prospect_row.lead = lead.name; + }); + frappe.set_route("Form", "Prospect", prospect.name); + }); + }); + }); + } + } +}; diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index cb95881cb4c..3866fc263e6 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -10,12 +10,12 @@ frappe.ui.form.on("Opportunity", { frm.custom_make_buttons = { 'Quotation': 'Quotation', 'Supplier Quotation': 'Supplier Quotation' - }, + }; frm.set_query("opportunity_from", function() { return{ "filters": { - "name": ["in", ["Customer", "Lead"]], + "name": ["in", ["Customer", "Lead", "Prospect"]], } } }); diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json index 4ba41402449..12a564a9cb3 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -430,7 +430,7 @@ "icon": "fa fa-info-sign", "idx": 195, "links": [], - "modified": "2021-06-04 10:11:22.831139", + "modified": "2021-08-25 10:28:24.923543", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity", diff --git a/erpnext/crm/doctype/prospect/__init__.py b/erpnext/crm/doctype/prospect/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/crm/doctype/prospect/prospect.js b/erpnext/crm/doctype/prospect/prospect.js new file mode 100644 index 00000000000..67018e1ef9c --- /dev/null +++ b/erpnext/crm/doctype/prospect/prospect.js @@ -0,0 +1,29 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Prospect', { + refresh (frm) { + if (!frm.is_new() && frappe.boot.user.can_create.includes("Customer")) { + frm.add_custom_button(__("Customer"), function() { + frappe.model.open_mapped_doc({ + method: "erpnext.crm.doctype.prospect.prospect.make_customer", + frm: frm + }); + }, __("Create")); + } + if (!frm.is_new() && frappe.boot.user.can_create.includes("Opportunity")) { + frm.add_custom_button(__("Opportunity"), function() { + frappe.model.open_mapped_doc({ + method: "erpnext.crm.doctype.prospect.prospect.make_opportunity", + frm: frm + }); + }, __("Create")); + } + + if (!frm.is_new()) { + frappe.contacts.render_address_and_contact(frm); + } else { + frappe.contacts.clear_address_and_contact(frm); + } + } +}); diff --git a/erpnext/crm/doctype/prospect/prospect.json b/erpnext/crm/doctype/prospect/prospect.json new file mode 100644 index 00000000000..3d6fba5123a --- /dev/null +++ b/erpnext/crm/doctype/prospect/prospect.json @@ -0,0 +1,203 @@ +{ + "actions": [], + "autoname": "field:company_name", + "creation": "2021-08-19 00:21:06.995448", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company_name", + "industry", + "market_segment", + "customer_group", + "territory", + "column_break_6", + "no_of_employees", + "currency", + "annual_revenue", + "more_details_section", + "fax", + "website", + "column_break_13", + "prospect_owner", + "leads_section", + "prospect_lead", + "address_and_contact_section", + "address_html", + "column_break_17", + "contact_html", + "notes_section", + "notes" + ], + "fields": [ + { + "fieldname": "company_name", + "fieldtype": "Data", + "label": "Company Name", + "unique": 1 + }, + { + "fieldname": "industry", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Industry", + "options": "Industry Type" + }, + { + "fieldname": "market_segment", + "fieldtype": "Link", + "label": "Market Segment", + "options": "Market Segment" + }, + { + "fieldname": "customer_group", + "fieldtype": "Link", + "label": "Customer Group", + "options": "Customer Group" + }, + { + "fieldname": "territory", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Territory", + "options": "Territory" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "no_of_employees", + "fieldtype": "Int", + "label": "No. of Employees" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Currency", + "options": "Currency" + }, + { + "fieldname": "annual_revenue", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Annual Revenue", + "options": "currency" + }, + { + "fieldname": "fax", + "fieldtype": "Data", + "label": "Fax", + "options": "Phone" + }, + { + "fieldname": "website", + "fieldtype": "Data", + "label": "Website", + "options": "URL" + }, + { + "fieldname": "prospect_owner", + "fieldtype": "Link", + "label": "Prospect Owner", + "options": "User" + }, + { + "fieldname": "leads_section", + "fieldtype": "Section 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", + "label": "Contact HTML" + }, + { + "collapsible": 1, + "fieldname": "notes_section", + "fieldtype": "Section 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" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-08-27 16:24:42.961967", + "modified_by": "Administrator", + "module": "CRM", + "name": "Prospect", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "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 new file mode 100644 index 00000000000..5f5815de5ed --- /dev/null +++ b/erpnext/crm/doctype/prospect/prospect.py @@ -0,0 +1,99 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe.model.mapper import get_mapped_doc +from frappe.contacts.address_and_contact import load_address_and_contact + +class Prospect(Document): + 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() + + def on_trash(self): + self.unlink_dynamic_links() + + 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 + + def link_with_lead_contact_and_address(self): + for row in self.prospect_lead: + links = frappe.get_all('Dynamic Link', filters={'link_doctype': 'Lead', 'link_name': row.lead}, fields=['parent', 'parenttype']) + for link in links: + linked_doc = frappe.get_doc(link['parenttype'], link['parent']) + exists = False + + for d in linked_doc.get('links'): + if d.link_doctype == self.doctype and d.link_name == self.name: + exists = True + + if not exists: + linked_doc.append('links', { + 'link_doctype': self.doctype, + 'link_name': self.name + }) + linked_doc.save(ignore_permissions=True) + + def unlink_dynamic_links(self): + links = frappe.get_all('Dynamic Link', filters={'link_doctype': self.doctype, 'link_name': self.name}, fields=['parent', 'parenttype']) + + for link in links: + linked_doc = frappe.get_doc(link['parenttype'], link['parent']) + + if len(linked_doc.get('links')) == 1: + linked_doc.delete(ignore_permissions=True) + else: + to_remove = None + for d in linked_doc.get('links'): + if d.link_doctype == self.doctype and d.link_name == self.name: + to_remove = d + if to_remove: + linked_doc.remove(to_remove) + linked_doc.save(ignore_permissions=True) + +@frappe.whitelist() +def make_customer(source_name, target_doc=None): + def set_missing_values(source, target): + target.customer_type = "Company" + target.company_name = source.name + target.customer_group = source.customer_group or frappe.db.get_default("Customer Group") + + doclist = get_mapped_doc("Prospect", source_name, + {"Prospect": { + "doctype": "Customer", + "field_map": { + "company_name": "customer_name", + "currency": "default_currency", + "fax": "fax" + } + }}, target_doc, set_missing_values, ignore_permissions=False) + + return doclist + +@frappe.whitelist() +def make_opportunity(source_name, target_doc=None): + def set_missing_values(source, target): + target.opportunity_from = "Prospect" + target.customer_name = source.company_name + target.customer_group = source.customer_group or frappe.db.get_default("Customer Group") + + doclist = get_mapped_doc("Prospect", source_name, + {"Prospect": { + "doctype": "Opportunity", + "field_map": { + "name": "party_name", + } + }}, target_doc, set_missing_values, ignore_permissions=False) + + return doclist diff --git a/erpnext/crm/doctype/prospect/test_prospect.py b/erpnext/crm/doctype/prospect/test_prospect.py new file mode 100644 index 00000000000..0fffad19395 --- /dev/null +++ b/erpnext/crm/doctype/prospect/test_prospect.py @@ -0,0 +1,54 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +import unittest +from frappe.utils import random_string +from erpnext.crm.doctype.lead.test_lead import make_lead +from erpnext.crm.doctype.lead.lead import add_lead_to_prospect + + +class TestProspect(unittest.TestCase): + def test_add_lead_to_prospect_and_address_linking(self): + lead_doc = make_lead() + address_doc = make_address(address_title=lead_doc.name) + address_doc.append('links', { + "link_doctype": lead_doc.doctype, + "link_name": lead_doc.name + }) + address_doc.save() + prospect_doc = make_prospect() + 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'): + if rec.lead == lead_doc.name: + lead_exists_in_prosoect = True + self.assertEqual(lead_exists_in_prosoect, True) + address_doc.reload() + self.assertEqual(address_doc.has_link('Prospect', prospect_doc.name), True) + + +def make_prospect(**args): + args = frappe._dict(args) + + prospect_doc = frappe.get_doc({ + "doctype": "Prospect", + "company_name": args.company_name or "_Test Company {}".format(random_string(3)), + }).insert() + + return prospect_doc + +def make_address(**args): + args = frappe._dict(args) + + address_doc = frappe.get_doc({ + "doctype": "Address", + "address_title": args.address_title or "Address Title", + "address_type": args.address_type or "Billing", + "city": args.city or "Mumbai", + "address_line1": args.address_line1 or "Vidya Vihar West", + "country": args.country or "India" + }).insert() + + return address_doc diff --git a/erpnext/crm/doctype/prospect_lead/__init__.py b/erpnext/crm/doctype/prospect_lead/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/crm/doctype/prospect_lead/prospect_lead.json b/erpnext/crm/doctype/prospect_lead/prospect_lead.json new file mode 100644 index 00000000000..3c160d9e802 --- /dev/null +++ b/erpnext/crm/doctype/prospect_lead/prospect_lead.json @@ -0,0 +1,67 @@ +{ + "actions": [], + "creation": "2021-08-19 00:14:14.857421", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "lead", + "lead_name", + "status", + "email", + "mobile_no" + ], + "fields": [ + { + "fieldname": "lead", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Lead", + "options": "Lead", + "reqd": 1 + }, + { + "fieldname": "lead_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Lead Name", + "read_only": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Lead\nOpen\nReplied\nOpportunity\nQuotation\nLost Quotation\nInterested\nConverted\nDo Not Contact", + "read_only": 1 + }, + { + "fieldname": "email", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Email", + "options": "Email", + "read_only": 1 + }, + { + "fieldname": "mobile_no", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Mobile No", + "options": "Phone", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-08-25 12:58:24.638054", + "modified_by": "Administrator", + "module": "CRM", + "name": "Prospect Lead", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/prospect_lead/prospect_lead.py b/erpnext/crm/doctype/prospect_lead/prospect_lead.py new file mode 100644 index 00000000000..2be5a5f39ad --- /dev/null +++ b/erpnext/crm/doctype/prospect_lead/prospect_lead.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class ProspectLead(Document): + pass diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 2acc64cb433..5913b849eb7 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -20,6 +20,7 @@ "tax_withholding_category", "default_bank_account", "lead_name", + "prospect", "opportunity_name", "image", "column_break0", @@ -213,8 +214,7 @@ "fieldtype": "Link", "ignore_user_permissions": 1, "label": "Represents Company", - "options": "Company", - "unique": 1 + "options": "Company" }, { "depends_on": "represents_company", @@ -497,6 +497,14 @@ "label": "Tax Withholding Category", "options": "Tax Withholding Category" }, + { + "fieldname": "prospect", + "fieldtype": "Link", + "label": "Prospect", + "no_copy": 1, + "options": "Prospect", + "print_hide": 1 + }, { "fieldname": "opportunity_name", "fieldtype": "Link",