From 4a529f8039ccf594628494e15f18744b785cba68 Mon Sep 17 00:00:00 2001 From: Anupam Date: Tue, 24 Aug 2021 09:35:59 +0530 Subject: [PATCH 01/44] feat: Prospect --- erpnext/crm/doctype/lead/lead.js | 2 +- erpnext/crm/doctype/lead/lead.py | 10 ++ erpnext/crm/doctype/lead/lead_list.js | 36 ++++ .../crm/doctype/opportunity/opportunity.js | 10 +- .../crm/doctype/opportunity/opportunity.json | 6 +- erpnext/crm/doctype/prospect/__init__.py | 0 erpnext/crm/doctype/prospect/prospect.js | 39 +++++ erpnext/crm/doctype/prospect/prospect.json | 165 ++++++++++++++++++ erpnext/crm/doctype/prospect/prospect.py | 45 +++++ erpnext/crm/doctype/prospect/test_prospect.py | 8 + erpnext/crm/doctype/prospect_lead/__init__.py | 0 .../doctype/prospect_lead/prospect_lead.json | 71 ++++++++ .../doctype/prospect_lead/prospect_lead.py | 8 + .../selling/doctype/customer/customer.json | 12 +- 14 files changed, 396 insertions(+), 16 deletions(-) create mode 100644 erpnext/crm/doctype/lead/lead_list.js create mode 100644 erpnext/crm/doctype/prospect/__init__.py create mode 100644 erpnext/crm/doctype/prospect/prospect.js create mode 100644 erpnext/crm/doctype/prospect/prospect.json create mode 100644 erpnext/crm/doctype/prospect/prospect.py create mode 100644 erpnext/crm/doctype/prospect/test_prospect.py create mode 100644 erpnext/crm/doctype/prospect_lead/__init__.py create mode 100644 erpnext/crm/doctype/prospect_lead/prospect_lead.json create mode 100644 erpnext/crm/doctype/prospect_lead/prospect_lead.py diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js index 75af937990..9c21099088 100644 --- a/erpnext/crm/doctype/lead/lead.js +++ b/erpnext/crm/doctype/lead/lead.js @@ -51,7 +51,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller make_customer () { frappe.model.open_mapped_doc({ - method: "erpnext.crm.doctype.lead.lead.make_customer", + method: "erpnext.crm.doctype.lead.lead.make_prospect", frm: cur_frm }) } diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 7f028cb316..ecf6a41e6c 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -190,6 +190,16 @@ class Lead(SellingController): return contact +@frappe.whitelist() +def make_prospect(source_name, target_doc=None, leads=None): + print("``````````````````````````") + print(source_name) + print("``````````````````````````") + print(target_doc) + print("``````````````````````````") + print(leads) + print("``````````````````````````") + @frappe.whitelist() def make_customer(source_name, target_doc=None): return _make_customer(source_name, target_doc) diff --git a/erpnext/crm/doctype/lead/lead_list.js b/erpnext/crm/doctype/lead/lead_list.js new file mode 100644 index 0000000000..5cbbf76480 --- /dev/null +++ b/erpnext/crm/doctype/lead/lead_list.js @@ -0,0 +1,36 @@ +frappe.listview_settings['Lead'] = { + onload: function(listview) { + if (frappe.boot.user.can_create.includes("Prospect")) { + listview.page.add_action_item(__("Create Prospect"), function() { + let leads = listview.get_checked_items(); + console.log(listview.get_checked_items()); + frappe.model.open_mapped_doc({ + method: "erpnext.crm.doctype.lead.lead.make_prospect", + frm: cur_frm, + leads: leads + }) + + // listview.call_for_selected_items(method, {"status": "Open"}); + // let prospect_lead = [] + // leads.forEach(lead => { + // prospect_lead.push({ + // "lead": lead.name + // }); + // }); + // console.log("check"); + // console.log(prospect_lead); + // frappe.new_doc("Prospect", { + // "company_name": leads[0].company_name, + // "industry": leads[0].industry, + // "market_segment": leads[0].market_segment, + // "territory": leads[0].territory, + // "no_of_employees": leads[0].no_of_employees, + // "fax": leads[0].fax, + // "website": leads[0].website, + // "prospect_owner": leads[0].lead_owner, + // "prospect_lead": prospect_lead + // }); + }); + } + } +}; diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index e9a7a95fc7..bcfae11a05 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -10,15 +10,7 @@ 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"]], - } - } - }); + }; if (frm.doc.opportunity_from && frm.doc.party_name){ frm.trigger('set_contact_link'); diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json index 4ba4140244..e4b0e47309 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -78,13 +78,13 @@ }, { "fieldname": "opportunity_from", - "fieldtype": "Link", + "fieldtype": "Select", "in_list_view": 1, "in_standard_filter": 1, "label": "Opportunity From", "oldfieldname": "enquiry_from", "oldfieldtype": "Select", - "options": "DocType", + "options": "\nLead\nProspect\nCustomer", "print_hide": 1, "reqd": 1 }, @@ -430,7 +430,7 @@ "icon": "fa fa-info-sign", "idx": 195, "links": [], - "modified": "2021-06-04 10:11:22.831139", + "modified": "2021-08-23 14:43:09.484227", "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 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/prospect/prospect.js b/erpnext/crm/doctype/prospect/prospect.js new file mode 100644 index 0000000000..793afccf3d --- /dev/null +++ b/erpnext/crm/doctype/prospect/prospect.js @@ -0,0 +1,39 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Prospect', { + refresh () { + if (!cur_frm.is_new() && frappe.boot.user.can_create.includes("Customer")) { + cur_frm.add_custom_button(__("Customer"), function() { + frappe.model.open_mapped_doc({ + method: "erpnext.crm.doctype.prospect.prospect.make_customer", + frm: cur_frm + }) + }, __("Create")); + } + if (!cur_frm.is_new() && frappe.boot.user.can_create.includes("Opportunity")) { + cur_frm.add_custom_button(__("Opportunity"), function() { + frappe.model.open_mapped_doc({ + method: "erpnext.crm.doctype.prospect.prospect.make_opportunity", + frm: cur_frm + }) + }, __("Create")); + } + }, + + make_customer () { + console.log("Make Customer"); + frappe.model.open_mapped_doc({ + method: "erpnext.crm.doctype.prospect.prospect.make_customer", + frm: cur_frm + }) + }, + + make_opportunity () { + console.log("Make Opportunity"); + // frappe.model.open_mapped_doc({ + // method: "erpnext.crm.doctype.lead.lead.make_opportunity", + // frm: cur_frm + // }) + } +}); diff --git a/erpnext/crm/doctype/prospect/prospect.json b/erpnext/crm/doctype/prospect/prospect.json new file mode 100644 index 0000000000..6f476a6b65 --- /dev/null +++ b/erpnext/crm/doctype/prospect/prospect.json @@ -0,0 +1,165 @@ +{ + "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", + "fax", + "website", + "prospect_owner", + "leads_section", + "prospect_lead", + "addresses_and_contacts_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", + "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", + "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", + "label": "Currency", + "options": "Currency" + }, + { + "fieldname": "annual_revenue", + "fieldtype": "Currency", + "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" + }, + { + "depends_on": "eval: !doc.__islocal", + "fieldname": "addresses_and_contacts_section", + "fieldtype": "Section Break", + "label": "Addresses and Contacts" + }, + { + "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" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-08-19 00:29:00.767038", + "modified_by": "Administrator", + "module": "CRM", + "name": "Prospect", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "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 0000000000..a0175cd310 --- /dev/null +++ b/erpnext/crm/doctype/prospect/prospect.py @@ -0,0 +1,45 @@ +# 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 + +class Prospect(Document): + pass + +@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 0000000000..f266a50593 --- /dev/null +++ b/erpnext/crm/doctype/prospect/test_prospect.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + +class TestProspect(unittest.TestCase): + pass diff --git a/erpnext/crm/doctype/prospect_lead/__init__.py b/erpnext/crm/doctype/prospect_lead/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..1797712a55 --- /dev/null +++ b/erpnext/crm/doctype/prospect_lead/prospect_lead.json @@ -0,0 +1,71 @@ +{ + "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 + }, + { + "fetch_from": "lead.lead_name", + "fetch_if_empty": 1, + "fieldname": "lead_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Lead Name" + }, + { + "fetch_from": "lead.status", + "fetch_if_empty": 1, + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Lead\nOpen\nReplied\nOpportunity\nQuotation\nLost Quotation\nInterested\nConverted\nDo Not Contact" + }, + { + "fetch_from": "lead.email_id", + "fetch_if_empty": 1, + "fieldname": "email", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Email", + "options": "Email" + }, + { + "fetch_from": "lead.mobile_no", + "fetch_if_empty": 1, + "fieldname": "mobile_no", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Mobile No", + "options": "Phone" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-08-20 01:58:39.387874", + "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 0000000000..2be5a5f39a --- /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 cd94ee101a..608b703862 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", "image", "column_break0", "account_manager", @@ -212,8 +213,7 @@ "fieldtype": "Link", "ignore_user_permissions": 1, "label": "Represents Company", - "options": "Company", - "unique": 1 + "options": "Company" }, { "depends_on": "represents_company", @@ -493,6 +493,12 @@ "fieldtype": "Link", "label": "Tax Withholding Category", "options": "Tax Withholding Category" + }, + { + "fieldname": "prospect", + "fieldtype": "Link", + "label": "Prospect", + "options": "Prospect" } ], "icon": "fa fa-user", @@ -500,7 +506,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2021-01-28 12:54:57.258959", + "modified": "2021-08-23 14:40:15.214350", "modified_by": "Administrator", "module": "Selling", "name": "Customer", From 797b19a14a4b201a2b03dca6a17978793225a22a Mon Sep 17 00:00:00 2001 From: Anupam Date: Tue, 24 Aug 2021 15:50:15 +0530 Subject: [PATCH 02/44] feat: creation of prospect from lead --- erpnext/crm/doctype/lead/lead.js | 8 ++++ erpnext/crm/doctype/lead/lead.py | 19 +++++---- erpnext/crm/doctype/lead/lead_dashboard.py | 2 +- erpnext/crm/doctype/lead/lead_list.js | 46 +++++++++------------- erpnext/crm/doctype/prospect/prospect.py | 21 +++++++++- 5 files changed, 57 insertions(+), 39 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js index 9c21099088..f6eb3f4836 100644 --- a/erpnext/crm/doctype/lead/lead.js +++ b/erpnext/crm/doctype/lead/lead.js @@ -39,6 +39,7 @@ 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")); } if (!this.frm.is_new()) { @@ -70,6 +71,13 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller }) } + make_prospect () { + frappe.model.open_mapped_doc({ + method: "erpnext.crm.doctype.lead.lead.make_prospect", + frm: cur_frm + }) + } + 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 ecf6a41e6c..f76595e978 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -190,16 +190,6 @@ class Lead(SellingController): return contact -@frappe.whitelist() -def make_prospect(source_name, target_doc=None, leads=None): - print("``````````````````````````") - print(source_name) - print("``````````````````````````") - print(target_doc) - print("``````````````````````````") - print(leads) - print("``````````````````````````") - @frappe.whitelist() def make_customer(source_name, target_doc=None): return _make_customer(source_name, target_doc) @@ -272,6 +262,15 @@ def make_quotation(source_name, target_doc=None): return target_doc +@frappe.whitelist() +def make_prospect(source_name, target_doc=None): + target_doc = get_mapped_doc("Lead", source_name, + {"Lead": { + "doctype": "Prospect", + }}, target_doc) + + return target_doc + def _set_missing_values(source, target): address = frappe.get_all('Dynamic Link', { 'link_doctype': source.doctype, diff --git a/erpnext/crm/doctype/lead/lead_dashboard.py b/erpnext/crm/doctype/lead/lead_dashboard.py index 69d8ca7092..6038811a66 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'] }, ] } \ No newline at end of file diff --git a/erpnext/crm/doctype/lead/lead_list.js b/erpnext/crm/doctype/lead/lead_list.js index 5cbbf76480..75208fa64b 100644 --- a/erpnext/crm/doctype/lead/lead_list.js +++ b/erpnext/crm/doctype/lead/lead_list.js @@ -2,34 +2,26 @@ frappe.listview_settings['Lead'] = { onload: function(listview) { if (frappe.boot.user.can_create.includes("Prospect")) { listview.page.add_action_item(__("Create Prospect"), function() { - let leads = listview.get_checked_items(); - console.log(listview.get_checked_items()); - frappe.model.open_mapped_doc({ - method: "erpnext.crm.doctype.lead.lead.make_prospect", - frm: cur_frm, - leads: leads - }) + 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; - // listview.call_for_selected_items(method, {"status": "Open"}); - // let prospect_lead = [] - // leads.forEach(lead => { - // prospect_lead.push({ - // "lead": lead.name - // }); - // }); - // console.log("check"); - // console.log(prospect_lead); - // frappe.new_doc("Prospect", { - // "company_name": leads[0].company_name, - // "industry": leads[0].industry, - // "market_segment": leads[0].market_segment, - // "territory": leads[0].territory, - // "no_of_employees": leads[0].no_of_employees, - // "fax": leads[0].fax, - // "website": leads[0].website, - // "prospect_owner": leads[0].lead_owner, - // "prospect_lead": prospect_lead - // }); + 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/prospect/prospect.py b/erpnext/crm/doctype/prospect/prospect.py index a0175cd310..80e2459a90 100644 --- a/erpnext/crm/doctype/prospect/prospect.py +++ b/erpnext/crm/doctype/prospect/prospect.py @@ -6,7 +6,26 @@ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc class Prospect(Document): - pass + def validate(self): + self.link_with_lead_contact_and_address() + + 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) @frappe.whitelist() def make_customer(source_name, target_doc=None): From 3610882077d5108a0fbc402254026c8f60685359 Mon Sep 17 00:00:00 2001 From: Anupam Date: Tue, 24 Aug 2021 15:51:57 +0530 Subject: [PATCH 03/44] fix: reverting local chnages --- erpnext/crm/doctype/lead/lead.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js index f6eb3f4836..0bf2ab9615 100644 --- a/erpnext/crm/doctype/lead/lead.js +++ b/erpnext/crm/doctype/lead/lead.js @@ -52,7 +52,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller make_customer () { frappe.model.open_mapped_doc({ - method: "erpnext.crm.doctype.lead.lead.make_prospect", + method: "erpnext.crm.doctype.lead.lead.make_customer", frm: cur_frm }) } From 0c6212189ebe7ff08e7d30b05ff904fb11fc4002 Mon Sep 17 00:00:00 2001 From: Anupam Date: Tue, 24 Aug 2021 16:49:53 +0530 Subject: [PATCH 04/44] feat: link lead communication to prospect --- erpnext/crm/doctype/lead/lead.py | 10 ++++++++++ erpnext/crm/doctype/prospect/prospect.py | 22 +++++++++++++++++++++- erpnext/hooks.py | 3 +++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index f76595e978..693fc6a160 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -362,3 +362,13 @@ 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") + +def add_prospect_link_in_communication(communication, method): + if communication.get('reference_doctype') == "Lead": + links = frappe.get_all('Prospect Lead', filters={'lead': communication.get('reference_name')}, fields=['parent', 'parenttype']) + + for link in links: + communication.append('timeline_links', { + 'link_doctype': link['parenttype'], + 'link_name': link['parent'] + }) diff --git a/erpnext/crm/doctype/prospect/prospect.py b/erpnext/crm/doctype/prospect/prospect.py index 80e2459a90..bd278dca35 100644 --- a/erpnext/crm/doctype/prospect/prospect.py +++ b/erpnext/crm/doctype/prospect/prospect.py @@ -6,9 +6,12 @@ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc class Prospect(Document): - def validate(self): + def after_save(self): self.link_with_lead_contact_and_address() + def on_trash(self): + self.unlink_dynamic_links() + 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']) @@ -27,6 +30,23 @@ class Prospect(Document): }) 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): diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 8f7c7db208..8069a1566f 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -249,6 +249,9 @@ doc_events = { "on_update": [ "erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time", "erpnext.support.doctype.issue.issue.set_first_response_time" + ], + "after_insert": [ + "erpnext.crm.doctype.lead.lead.add_prospect_link_in_communication" ] }, ("Sales Taxes and Charges Template", 'Price List'): { From dfcac64b442b60fd329586f01b60b262d1be40de Mon Sep 17 00:00:00 2001 From: Anupam Date: Wed, 25 Aug 2021 16:24:46 +0530 Subject: [PATCH 05/44] fix: review chnages --- erpnext/crm/doctype/lead/lead.js | 51 +++++++++++++++++-- erpnext/crm/doctype/lead/lead.py | 35 +++++++------ .../crm/doctype/opportunity/opportunity.js | 29 +++++++++-- .../crm/doctype/opportunity/opportunity.json | 6 +-- .../crm/doctype/opportunity/opportunity.py | 20 ++++++++ erpnext/crm/doctype/prospect/prospect.js | 21 +++----- erpnext/crm/doctype/prospect/prospect.py | 13 ++++- .../doctype/prospect_lead/prospect_lead.json | 22 ++++---- erpnext/hooks.py | 3 -- 9 files changed, 140 insertions(+), 60 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js index 0bf2ab9615..7f5f1a4cb4 100644 --- a/erpnext/crm/doctype/lead/lead.js +++ b/erpnext/crm/doctype/lead/lead.js @@ -40,6 +40,9 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller 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'), function() { + cur_frm.trigger('add_lead_to_prospect') + }, __('Action')); } if (!this.frm.is_new()) { @@ -50,6 +53,34 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller } } + add_lead_to_prospect () { + 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': cur_frm.doc.name, + 'prospect': data.prospect + }, + callback: function(r) { + if (!r.exc) { + cur_frm.reload_doc(); + } + }, + freeze: true, + freeze_message: __('...Adding Lead to Prospect') + }) + }, __('Add Lead to Prospect'), __('Add')); + } + make_customer () { frappe.model.open_mapped_doc({ method: "erpnext.crm.doctype.lead.lead.make_customer", @@ -72,10 +103,22 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller } make_prospect () { - frappe.model.open_mapped_doc({ - method: "erpnext.crm.doctype.lead.lead.make_prospect", - frm: cur_frm - }) + frappe.model.with_doctype("Prospect", function() { + let prospect = frappe.model.get_new_doc("Prospect"); + prospect.company_name = cur_frm.doc.company_name; + prospect.no_of_employees = cur_frm.doc.no_of_employees; + prospect.industry = cur_frm.doc.industry; + prospect.market_segment = cur_frm.doc.market_segment; + prospect.territory = cur_frm.doc.territory; + prospect.fax = cur_frm.doc.fax; + prospect.website = cur_frm.doc.website; + prospect.prospect_owner = cur_frm.doc.lead_owner; + + let lead_prospect_row = frappe.model.add_child(prospect, 'prospect_lead'); + lead_prospect_row.lead = cur_frm.doc.name; + + frappe.set_route("Form", "Prospect", prospect.name); + }); } company_name () { diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 693fc6a160..c6115ee438 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -62,6 +62,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() @@ -88,6 +89,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 @@ -262,15 +269,6 @@ def make_quotation(source_name, target_doc=None): return target_doc -@frappe.whitelist() -def make_prospect(source_name, target_doc=None): - target_doc = get_mapped_doc("Lead", source_name, - {"Lead": { - "doctype": "Prospect", - }}, target_doc) - - return target_doc - def _set_missing_values(source, target): address = frappe.get_all('Dynamic Link', { 'link_doctype': source.doctype, @@ -363,12 +361,13 @@ def daily_open_lead(): for lead in leads: frappe.db.set_value("Lead", lead.name, "status", "Open") -def add_prospect_link_in_communication(communication, method): - if communication.get('reference_doctype') == "Lead": - links = frappe.get_all('Prospect Lead', filters={'lead': communication.get('reference_name')}, fields=['parent', 'parenttype']) - - for link in links: - communication.append('timeline_links', { - 'link_doctype': link['parenttype'], - 'link_name': link['parent'] - }) +@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/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index bcfae11a05..80588ee002 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -12,6 +12,14 @@ frappe.ui.form.on("Opportunity", { 'Supplier Quotation': 'Supplier Quotation' }; + frm.set_query("opportunity_from", function() { + return{ + "filters": { + "name": ["in", ["Customer", "Lead", "Prospect"]], + } + } + }); + if (frm.doc.opportunity_from && frm.doc.party_name){ frm.trigger('set_contact_link'); } @@ -87,10 +95,18 @@ frappe.ui.form.on("Opportunity", { }, __('Create')); } - frm.add_custom_button(__('Quotation'), - cur_frm.cscript.create_quotation, __('Create')); + if (frm.doc.opportunity_from != "Customer") { + frm.add_custom_button(__('Customer'), + function() { + frm.trigger("make_customer") + }, __('Create')); + } - } + frm.add_custom_button(__('Quotation'), + function() { + frm.trigger("create_quotation") + }, __('Create')); + } if(!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus==0) { if(frm.doc.status==="Open") { @@ -187,6 +203,13 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller { frm: cur_frm }) } + + make_customer() { + frappe.model.open_mapped_doc({ + method: "erpnext.crm.doctype.opportunity.opportunity.make_customer", + frm: cur_frm + }) + } }; 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 e4b0e47309..12a564a9cb 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -78,13 +78,13 @@ }, { "fieldname": "opportunity_from", - "fieldtype": "Select", + "fieldtype": "Link", "in_list_view": 1, "in_standard_filter": 1, "label": "Opportunity From", "oldfieldname": "enquiry_from", "oldfieldtype": "Select", - "options": "\nLead\nProspect\nCustomer", + "options": "DocType", "print_hide": 1, "reqd": 1 }, @@ -430,7 +430,7 @@ "icon": "fa fa-info-sign", "idx": 195, "links": [], - "modified": "2021-08-23 14:43:09.484227", + "modified": "2021-08-25 10:28:24.923543", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity", diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 23ad98a282..9e620312b9 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -265,6 +265,26 @@ def make_quotation(source_name, target_doc=None): return doclist +@frappe.whitelist() +def make_customer(source_name, target_doc=None): + def set_missing_values(source, target): + if source.opportunity_from == "Lead": + target.lead_name = source.party_name + if source.opportunity_from == "Prospect": + target.prospect = source.party_name + + doclist = get_mapped_doc("Opportunity", source_name, { + "Opportunity": { + "doctype": "Customer", + "field_map": { + "currency": "default_currency", + "customer_name": "customer_name" + } + } + }, target_doc, set_missing_values) + + return doclist + @frappe.whitelist() def make_request_for_quotation(source_name, target_doc=None): def update_item(obj, target, source_parent): diff --git a/erpnext/crm/doctype/prospect/prospect.js b/erpnext/crm/doctype/prospect/prospect.js index 793afccf3d..64256cf31f 100644 --- a/erpnext/crm/doctype/prospect/prospect.js +++ b/erpnext/crm/doctype/prospect/prospect.js @@ -19,21 +19,12 @@ frappe.ui.form.on('Prospect', { }) }, __("Create")); } - }, - make_customer () { - console.log("Make Customer"); - frappe.model.open_mapped_doc({ - method: "erpnext.crm.doctype.prospect.prospect.make_customer", - frm: cur_frm - }) - }, - - make_opportunity () { - console.log("Make Opportunity"); - // frappe.model.open_mapped_doc({ - // method: "erpnext.crm.doctype.lead.lead.make_opportunity", - // frm: cur_frm - // }) + if (!cur_frm.is_new()) { + frappe.contacts.render_address_and_contact(cur_frm); + cur_frm.trigger('render_contact_day_html'); + } else { + frappe.contacts.clear_address_and_contact(cur_frm); + } } }); diff --git a/erpnext/crm/doctype/prospect/prospect.py b/erpnext/crm/doctype/prospect/prospect.py index bd278dca35..dc165af825 100644 --- a/erpnext/crm/doctype/prospect/prospect.py +++ b/erpnext/crm/doctype/prospect/prospect.py @@ -6,12 +6,23 @@ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc class Prospect(Document): - def after_save(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']) diff --git a/erpnext/crm/doctype/prospect_lead/prospect_lead.json b/erpnext/crm/doctype/prospect_lead/prospect_lead.json index 1797712a55..3c160d9e80 100644 --- a/erpnext/crm/doctype/prospect_lead/prospect_lead.json +++ b/erpnext/crm/doctype/prospect_lead/prospect_lead.json @@ -21,45 +21,41 @@ "reqd": 1 }, { - "fetch_from": "lead.lead_name", - "fetch_if_empty": 1, "fieldname": "lead_name", "fieldtype": "Data", "in_list_view": 1, - "label": "Lead Name" + "label": "Lead Name", + "read_only": 1 }, { - "fetch_from": "lead.status", - "fetch_if_empty": 1, "fieldname": "status", "fieldtype": "Select", "in_list_view": 1, "label": "Status", - "options": "Lead\nOpen\nReplied\nOpportunity\nQuotation\nLost Quotation\nInterested\nConverted\nDo Not Contact" + "options": "Lead\nOpen\nReplied\nOpportunity\nQuotation\nLost Quotation\nInterested\nConverted\nDo Not Contact", + "read_only": 1 }, { - "fetch_from": "lead.email_id", - "fetch_if_empty": 1, "fieldname": "email", "fieldtype": "Data", "in_list_view": 1, "label": "Email", - "options": "Email" + "options": "Email", + "read_only": 1 }, { - "fetch_from": "lead.mobile_no", - "fetch_if_empty": 1, "fieldname": "mobile_no", "fieldtype": "Data", "in_list_view": 1, "label": "Mobile No", - "options": "Phone" + "options": "Phone", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-08-20 01:58:39.387874", + "modified": "2021-08-25 12:58:24.638054", "modified_by": "Administrator", "module": "CRM", "name": "Prospect Lead", diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 8069a1566f..8f7c7db208 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -249,9 +249,6 @@ doc_events = { "on_update": [ "erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time", "erpnext.support.doctype.issue.issue.set_first_response_time" - ], - "after_insert": [ - "erpnext.crm.doctype.lead.lead.add_prospect_link_in_communication" ] }, ("Sales Taxes and Charges Template", 'Price List'): { From c644fbb1023029edd1a30003e14f3dfb5ef4e1fa Mon Sep 17 00:00:00 2001 From: Anupam Date: Wed, 25 Aug 2021 16:35:34 +0530 Subject: [PATCH 06/44] fix: sider issues --- erpnext/crm/doctype/lead/lead.js | 6 ++--- erpnext/crm/doctype/prospect/prospect.js | 4 ++-- erpnext/crm/doctype/prospect/prospect.json | 27 +++++++++++++++------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js index 7f5f1a4cb4..dfb3b094dc 100644 --- a/erpnext/crm/doctype/lead/lead.js +++ b/erpnext/crm/doctype/lead/lead.js @@ -40,9 +40,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller 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'), function() { - cur_frm.trigger('add_lead_to_prospect') - }, __('Action')); + this.frm.add_custom_button(__('Add to Prospect'), this.add_lead_to_prospect, __('Action')); } if (!this.frm.is_new()) { @@ -77,7 +75,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller }, freeze: true, freeze_message: __('...Adding Lead to Prospect') - }) + }); }, __('Add Lead to Prospect'), __('Add')); } diff --git a/erpnext/crm/doctype/prospect/prospect.js b/erpnext/crm/doctype/prospect/prospect.js index 64256cf31f..814fc2b6db 100644 --- a/erpnext/crm/doctype/prospect/prospect.js +++ b/erpnext/crm/doctype/prospect/prospect.js @@ -8,7 +8,7 @@ frappe.ui.form.on('Prospect', { frappe.model.open_mapped_doc({ method: "erpnext.crm.doctype.prospect.prospect.make_customer", frm: cur_frm - }) + }); }, __("Create")); } if (!cur_frm.is_new() && frappe.boot.user.can_create.includes("Opportunity")) { @@ -16,7 +16,7 @@ frappe.ui.form.on('Prospect', { frappe.model.open_mapped_doc({ method: "erpnext.crm.doctype.prospect.prospect.make_opportunity", frm: cur_frm - }) + }); }, __("Create")); } diff --git a/erpnext/crm/doctype/prospect/prospect.json b/erpnext/crm/doctype/prospect/prospect.json index 6f476a6b65..2eb7cde02d 100644 --- a/erpnext/crm/doctype/prospect/prospect.json +++ b/erpnext/crm/doctype/prospect/prospect.json @@ -15,12 +15,14 @@ "no_of_employees", "currency", "annual_revenue", + "more_details_section", "fax", "website", + "column_break_13", "prospect_owner", "leads_section", "prospect_lead", - "addresses_and_contacts_section", + "address_and_contact_section", "address_html", "column_break_17", "contact_html", @@ -107,12 +109,6 @@ "fieldtype": "Table", "options": "Prospect Lead" }, - { - "depends_on": "eval: !doc.__islocal", - "fieldname": "addresses_and_contacts_section", - "fieldtype": "Section Break", - "label": "Addresses and Contacts" - }, { "fieldname": "address_html", "fieldtype": "HTML", @@ -136,11 +132,26 @@ { "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-19 00:29:00.767038", + "modified": "2021-08-25 12:35:00.759909", "modified_by": "Administrator", "module": "CRM", "name": "Prospect", From 5ad8afcc8d6f06c62685f3cf24155cca4e172bf4 Mon Sep 17 00:00:00 2001 From: Anupam Date: Wed, 25 Aug 2021 17:36:53 +0530 Subject: [PATCH 07/44] revert: creation of customer from opportunity --- .../crm/doctype/opportunity/opportunity.js | 14 ------------- .../crm/doctype/opportunity/opportunity.py | 20 ------------------- 2 files changed, 34 deletions(-) diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index 80588ee002..6ebb0dab57 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -95,13 +95,6 @@ frappe.ui.form.on("Opportunity", { }, __('Create')); } - if (frm.doc.opportunity_from != "Customer") { - frm.add_custom_button(__('Customer'), - function() { - frm.trigger("make_customer") - }, __('Create')); - } - frm.add_custom_button(__('Quotation'), function() { frm.trigger("create_quotation") @@ -203,13 +196,6 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller { frm: cur_frm }) } - - make_customer() { - frappe.model.open_mapped_doc({ - method: "erpnext.crm.doctype.opportunity.opportunity.make_customer", - frm: cur_frm - }) - } }; extend_cscript(cur_frm.cscript, new erpnext.crm.Opportunity({frm: cur_frm})); diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 9e620312b9..23ad98a282 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -265,26 +265,6 @@ def make_quotation(source_name, target_doc=None): return doclist -@frappe.whitelist() -def make_customer(source_name, target_doc=None): - def set_missing_values(source, target): - if source.opportunity_from == "Lead": - target.lead_name = source.party_name - if source.opportunity_from == "Prospect": - target.prospect = source.party_name - - doclist = get_mapped_doc("Opportunity", source_name, { - "Opportunity": { - "doctype": "Customer", - "field_map": { - "currency": "default_currency", - "customer_name": "customer_name" - } - } - }, target_doc, set_missing_values) - - return doclist - @frappe.whitelist() def make_request_for_quotation(source_name, target_doc=None): def update_item(obj, target, source_parent): From fbac5149298332b9ea893080e9ffbc895c4edea8 Mon Sep 17 00:00:00 2001 From: Anupam Date: Thu, 26 Aug 2021 12:14:29 +0530 Subject: [PATCH 08/44] feat: prospect permission --- erpnext/crm/doctype/prospect/prospect.json | 25 ++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/prospect/prospect.json b/erpnext/crm/doctype/prospect/prospect.json index 2eb7cde02d..237c4255c7 100644 --- a/erpnext/crm/doctype/prospect/prospect.json +++ b/erpnext/crm/doctype/prospect/prospect.json @@ -151,7 +151,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-08-25 12:35:00.759909", + "modified": "2021-08-26 12:10:49.191839", "modified_by": "Administrator", "module": "CRM", "name": "Prospect", @@ -159,7 +159,6 @@ "permissions": [ { "create": 1, - "delete": 1, "email": 1, "export": 1, "print": 1, @@ -168,9 +167,31 @@ "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 From de488f68c02654cdd29d2148b1be9919c8d07274 Mon Sep 17 00:00:00 2001 From: Anupam Date: Thu, 26 Aug 2021 14:23:44 +0530 Subject: [PATCH 09/44] adding test cases --- erpnext/crm/doctype/prospect/test_prospect.py | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/prospect/test_prospect.py b/erpnext/crm/doctype/prospect/test_prospect.py index f266a50593..0fffad1939 100644 --- a/erpnext/crm/doctype/prospect/test_prospect.py +++ b/erpnext/crm/doctype/prospect/test_prospect.py @@ -1,8 +1,54 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe +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): - pass + 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 From d701ad5313bf2f33a9e8125f24bbf4c9b49a8525 Mon Sep 17 00:00:00 2001 From: Anupam Date: Fri, 27 Aug 2021 13:09:52 +0530 Subject: [PATCH 10/44] fix: using frm instead of cur_frm --- erpnext/crm/doctype/lead/lead.js | 38 ++++++++++++------------ erpnext/crm/doctype/prospect/prospect.js | 21 +++++++------ erpnext/crm/doctype/prospect/prospect.py | 4 +++ 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js index dfb3b094dc..95cf03241b 100644 --- a/erpnext/crm/doctype/lead/lead.js +++ b/erpnext/crm/doctype/lead/lead.js @@ -51,7 +51,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller } } - add_lead_to_prospect () { + add_lead_to_prospect (frm) { frappe.prompt([ { fieldname: 'prospect', @@ -65,12 +65,12 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller frappe.call({ method: 'erpnext.crm.doctype.lead.lead.add_lead_to_prospect', args: { - 'lead': cur_frm.doc.name, + 'lead': frm.doc.name, 'prospect': data.prospect }, callback: function(r) { if (!r.exc) { - cur_frm.reload_doc(); + frm.reload_doc(); } }, freeze: true, @@ -79,41 +79,41 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller }, __('Add Lead to Prospect'), __('Add')); } - make_customer () { + 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 () { + make_prospect (frm) { frappe.model.with_doctype("Prospect", function() { let prospect = frappe.model.get_new_doc("Prospect"); - prospect.company_name = cur_frm.doc.company_name; - prospect.no_of_employees = cur_frm.doc.no_of_employees; - prospect.industry = cur_frm.doc.industry; - prospect.market_segment = cur_frm.doc.market_segment; - prospect.territory = cur_frm.doc.territory; - prospect.fax = cur_frm.doc.fax; - prospect.website = cur_frm.doc.website; - prospect.prospect_owner = cur_frm.doc.lead_owner; + 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 = cur_frm.doc.name; + lead_prospect_row.lead = frm.doc.name; frappe.set_route("Form", "Prospect", prospect.name); }); diff --git a/erpnext/crm/doctype/prospect/prospect.js b/erpnext/crm/doctype/prospect/prospect.js index 814fc2b6db..67018e1ef9 100644 --- a/erpnext/crm/doctype/prospect/prospect.js +++ b/erpnext/crm/doctype/prospect/prospect.js @@ -2,29 +2,28 @@ // For license information, please see license.txt frappe.ui.form.on('Prospect', { - refresh () { - if (!cur_frm.is_new() && frappe.boot.user.can_create.includes("Customer")) { - cur_frm.add_custom_button(__("Customer"), function() { + 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: cur_frm + frm: frm }); }, __("Create")); } - if (!cur_frm.is_new() && frappe.boot.user.can_create.includes("Opportunity")) { - cur_frm.add_custom_button(__("Opportunity"), function() { + 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: cur_frm + frm: frm }); }, __("Create")); } - if (!cur_frm.is_new()) { - frappe.contacts.render_address_and_contact(cur_frm); - cur_frm.trigger('render_contact_day_html'); + if (!frm.is_new()) { + frappe.contacts.render_address_and_contact(frm); } else { - frappe.contacts.clear_address_and_contact(cur_frm); + frappe.contacts.clear_address_and_contact(frm); } } }); diff --git a/erpnext/crm/doctype/prospect/prospect.py b/erpnext/crm/doctype/prospect/prospect.py index dc165af825..5f5815de5e 100644 --- a/erpnext/crm/doctype/prospect/prospect.py +++ b/erpnext/crm/doctype/prospect/prospect.py @@ -4,8 +4,12 @@ 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() From a285b5b78b8080cda0a917cd45cc06e7de4e6ff9 Mon Sep 17 00:00:00 2001 From: Kamal Johnson Date: Fri, 27 Aug 2021 15:47:11 +0530 Subject: [PATCH 11/44] feat: Accounts, Selling & Assets Onboarding cleanup (#27112) * refactor: company form fields arrangement * refactor: accounts onboarding steps * refactor: re-arrange fields & modify labels in company form * feat: onbording steps for accounts module * fix: updated onboarding steps for selling module * fix: remove unwanted steps from accounts * feat: add more onboarding steps * feat: Refactor onboarding-module for accounting workspace * feat: Add basic asset onboarding steps * feat: Add basic asset item form tour * feat: Add basic selling onboarding-steps * feat: Add basic form tour for purchase recipt * feat: Add basic form tour for existing asset * feat: Adding asset category form tour * feat: Add form tour for asset category * feat: Refactor sales taxes and charges template form tour with new method * refactor: Remove old form tour for accounts settings * refactor: Change asset category form tour with new tour method * refactor: Change asset item form tour with new tour method * refactor: Change purchase receipt form tour with new tour method * refactor: Change asset form tour with new tour method * refactor: Remove old form tour for selling settings * refactor: general fix/typo in onboarding steps * Apply suggestions from code review Co-authored-by: Nabin Hait * refactor: Remove old form tour method for sales order. * fix: removed duplicate section break * fix: Removed duplicate fields Co-authored-by: Saqib Ansari Co-authored-by: Anuja Co-authored-by: Afshan <33727827+AfshanKhan@users.noreply.github.com> Co-authored-by: Nabin Hait --- .../accounts_settings/accounts_settings.js | 43 ------ .../sales_taxes_and_charges_template.js | 22 --- .../accounts_settings/accounts_settings.json | 113 ++++++++++++++++ .../purchase_invoice/purchase_invoice.json | 96 ++++++++++++++ .../sales_taxes_and_charges_template.json | 65 +++++++++ .../module_onboarding/accounts/accounts.json | 16 +-- .../accounts_settings/accounts_settings.json | 21 +++ .../chart_of_accounts/chart_of_accounts.json | 11 +- .../onboarding_step/company/company.json | 22 +++ ...cost_centers_for_report_and_budgeting.json | 21 +++ .../create_your_first_purchase_invoice.json | 7 +- .../financial_statements.json | 23 ++++ .../setup_taxes/setup_taxes.json | 8 +- .../updating_opening_balances.json | 22 +++ erpnext/assets/form_tour/asset/asset.json | 125 ++++++++++++++++++ .../asset_category/asset_category.json | 65 +++++++++ .../module_onboarding/assets/assets.json | 12 +- .../asset_category/asset_category.json | 21 +++ .../asset_item/asset_item.json | 21 +++ .../asset_purchase/asset_purchase.json | 21 +++ .../existing_asset/existing_asset.json | 21 +++ .../fixed_asset_accounts.json | 21 +++ .../selling_settings/selling_settings.js | 23 ---- .../form_tour/sales_order/sales_order.json | 97 ++++++++++++++ .../selling_settings/selling_settings.json | 65 +++++++++ .../module_onboarding/selling/selling.json | 23 +--- .../create_a_customer/create_a_customer.json | 2 +- .../create_a_product/create_a_product.json | 4 +- .../create_a_quotation.json | 2 +- .../create_a_sales_order.json | 21 +++ .../introduction_to_selling.json | 2 +- .../sales_order/sales_order.json | 21 +++ .../selling_settings/selling_settings.json | 8 +- .../setup_your_warehouse.json | 4 +- erpnext/setup/doctype/company/company.json | 20 +-- erpnext/stock/form_tour/item/item.json | 89 +++++++++++++ .../purchase_receipt/purchase_receipt.json | 41 ++++++ 37 files changed, 1066 insertions(+), 153 deletions(-) create mode 100644 erpnext/accounts/form_tour/accounts_settings/accounts_settings.json create mode 100644 erpnext/accounts/form_tour/purchase_invoice/purchase_invoice.json create mode 100644 erpnext/accounts/form_tour/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json create mode 100644 erpnext/accounts/onboarding_step/accounts_settings/accounts_settings.json create mode 100644 erpnext/accounts/onboarding_step/company/company.json create mode 100644 erpnext/accounts/onboarding_step/cost_centers_for_report_and_budgeting/cost_centers_for_report_and_budgeting.json create mode 100644 erpnext/accounts/onboarding_step/financial_statements/financial_statements.json create mode 100644 erpnext/accounts/onboarding_step/updating_opening_balances/updating_opening_balances.json create mode 100644 erpnext/assets/form_tour/asset/asset.json create mode 100644 erpnext/assets/form_tour/asset_category/asset_category.json create mode 100644 erpnext/assets/onboarding_step/asset_category/asset_category.json create mode 100644 erpnext/assets/onboarding_step/asset_item/asset_item.json create mode 100644 erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json create mode 100644 erpnext/assets/onboarding_step/existing_asset/existing_asset.json create mode 100644 erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json create mode 100644 erpnext/selling/form_tour/sales_order/sales_order.json create mode 100644 erpnext/selling/form_tour/selling_settings/selling_settings.json create mode 100644 erpnext/selling/onboarding_step/create_a_sales_order/create_a_sales_order.json create mode 100644 erpnext/selling/onboarding_step/sales_order/sales_order.json create mode 100644 erpnext/stock/form_tour/item/item.json create mode 100644 erpnext/stock/form_tour/purchase_receipt/purchase_receipt.json diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js index e44af3a916..0627675de7 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js @@ -6,46 +6,3 @@ frappe.ui.form.on('Accounts Settings', { } }); - -frappe.tour['Accounts Settings'] = [ - { - fieldname: "acc_frozen_upto", - title: "Accounts Frozen Upto", - description: __("Freeze accounting transactions up to specified date, nobody can make/modify entry except the specified Role."), - }, - { - fieldname: "frozen_accounts_modifier", - title: "Role Allowed to Set Frozen Accounts & Edit Frozen Entries", - description: __("Users with this Role are allowed to set frozen accounts and create/modify accounting entries against frozen accounts.") - }, - { - fieldname: "determine_address_tax_category_from", - title: "Determine Address Tax Category From", - description: __("Tax category can be set on Addresses. An address can be Shipping or Billing address. Set which addres to select when applying Tax Category.") - }, - { - fieldname: "over_billing_allowance", - title: "Over Billing Allowance Percentage", - description: __("The percentage by which you can overbill transactions. For example, if the order value is $100 for an Item and percentage here is set as 10% then you are allowed to bill for $110.") - }, - { - fieldname: "credit_controller", - title: "Credit Controller", - description: __("Select the role that is allowed to submit transactions that exceed credit limits set. The credit limit can be set in the Customer form.") - }, - { - fieldname: "make_payment_via_journal_entry", - title: "Make Payment via Journal Entry", - description: __("When checked, if user proceeds to make payment from an invoice, the system will open a Journal Entry instead of a Payment Entry.") - }, - { - fieldname: "unlink_payment_on_cancellation_of_invoice", - title: "Unlink Payment on Cancellation of Invoice", - description: __("If checked, system will unlink the payment against the respective invoice.") - }, - { - fieldname: "unlink_advance_payment_on_cancelation_of_order", - title: "Unlink Advance Payment on Cancellation of Order", - description: __("Similar to the previous option, this unlinks any advance payments made against Purchase/Sales Orders.") - } -]; diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.js b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.js index 0e011883b1..97a6fdd336 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.js +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.js @@ -5,25 +5,3 @@ cur_frm.cscript.tax_table = "Sales Taxes and Charges"; {% include "erpnext/public/js/controllers/accounts.js" %} -frappe.tour['Sales Taxes and Charges Template'] = [ - { - fieldname: "title", - title: __("Title"), - description: __("A name by which you will identify this template. You can change this later."), - }, - { - fieldname: "company", - title: __("Company"), - description: __("Company for which this tax template will be applicable"), - }, - { - fieldname: "is_default", - title: __("Is this Default?"), - description: __("Set this template as the default for all sales transactions"), - }, - { - fieldname: "taxes", - title: __("Taxes Table"), - description: __("You can add a row for a tax rule here. These rules can be applied on the net total, or can be a flat amount."), - } -]; diff --git a/erpnext/accounts/form_tour/accounts_settings/accounts_settings.json b/erpnext/accounts/form_tour/accounts_settings/accounts_settings.json new file mode 100644 index 0000000000..e2bf50d20a --- /dev/null +++ b/erpnext/accounts/form_tour/accounts_settings/accounts_settings.json @@ -0,0 +1,113 @@ +{ + "creation": "2021-06-29 17:00:18.273054", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-06-29 17:00:26.145996", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Accounts Settings", + "owner": "Administrator", + "reference_doctype": "Accounts Settings", + "save_on_complete": 0, + "steps": [ + { + "description": "The percentage by which you can overbill transactions. For example, if the order value is $100 for an Item and percentage here is set as 10% then you are allowed to bill for $110.", + "field": "", + "fieldname": "over_billing_allowance", + "fieldtype": "Currency", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Over Billing Allowance (%)", + "parent_field": "", + "position": "Right", + "title": "Over Billing Allowance Percentage" + }, + { + "description": "Select the role that is allowed to overbill a transactions.", + "field": "", + "fieldname": "role_allowed_to_over_bill", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Role Allowed to Over Bill ", + "parent_field": "", + "position": "Right", + "title": "Role Allowed to Over Bill" + }, + { + "description": "If checked, system will unlink the payment against the respective invoice.", + "field": "", + "fieldname": "unlink_payment_on_cancellation_of_invoice", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Unlink Payment on Cancellation of Invoice", + "parent_field": "", + "position": "Bottom", + "title": "Unlink Payment on Cancellation of Invoice" + }, + { + "description": "Similar to the previous option, this unlinks any advance payments made against Purchase/Sales Orders.", + "field": "", + "fieldname": "unlink_advance_payment_on_cancelation_of_order", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Unlink Advance Payment on Cancellation of Order", + "parent_field": "", + "position": "Bottom", + "title": "Unlink Advance Payment on Cancellation of Order" + }, + { + "description": "Tax category can be set on Addresses. An address can be Shipping or Billing address. Set which addres to select when applying Tax Category.", + "field": "", + "fieldname": "determine_address_tax_category_from", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Determine Address Tax Category From", + "parent_field": "", + "position": "Right", + "title": "Determine Address Tax Category From" + }, + { + "description": "Freeze accounting transactions up to specified date, nobody can make/modify entry except the specified Role.", + "field": "", + "fieldname": "acc_frozen_upto", + "fieldtype": "Date", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Accounts Frozen Till Date", + "parent_field": "", + "position": "Right", + "title": "Accounts Frozen Upto" + }, + { + "description": "Users with this Role are allowed to set frozen accounts and create/modify accounting entries against frozen accounts.", + "field": "", + "fieldname": "frozen_accounts_modifier", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Role Allowed to Set Frozen Accounts and Edit Frozen Entries", + "parent_field": "", + "position": "Right", + "title": "Role Allowed to Set Frozen Accounts & Edit Frozen Entries" + }, + { + "description": "Select the role that is allowed to submit transactions that exceed credit limits set. The credit limit can be set in the Customer form.", + "field": "", + "fieldname": "credit_controller", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Credit Controller", + "parent_field": "", + "position": "Left", + "title": "Credit Controller" + } + ], + "title": "Accounts Settings" +} \ No newline at end of file diff --git a/erpnext/accounts/form_tour/purchase_invoice/purchase_invoice.json b/erpnext/accounts/form_tour/purchase_invoice/purchase_invoice.json new file mode 100644 index 0000000000..2dffcd1c0e --- /dev/null +++ b/erpnext/accounts/form_tour/purchase_invoice/purchase_invoice.json @@ -0,0 +1,96 @@ +{ + "creation": "2021-06-29 16:31:48.558826", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-06-29 16:31:48.558826", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Purchase Invoice", + "owner": "Administrator", + "reference_doctype": "Purchase Invoice", + "save_on_complete": 1, + "steps": [ + { + "description": "Select Supplier", + "field": "", + "fieldname": "supplier", + "fieldtype": "Link", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Supplier", + "next_step_condition": "supplier", + "parent_field": "", + "position": "Right", + "title": "Select Supplier" + }, + { + "description": "Add items in the table", + "field": "", + "fieldname": "items", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Items", + "parent_field": "", + "position": "Bottom", + "title": "List of Items" + }, + { + "child_doctype": "Purchase Invoice Item", + "description": "Select an item", + "field": "", + "fieldname": "item_code", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 1, + "label": "Item", + "parent_field": "", + "parent_fieldname": "items", + "position": "Right", + "title": "Select Item" + }, + { + "child_doctype": "Purchase Invoice Item", + "description": "Enter the quantity", + "field": "", + "fieldname": "qty", + "fieldtype": "Float", + "has_next_condition": 0, + "is_table_field": 1, + "label": "Accepted Qty", + "parent_field": "", + "parent_fieldname": "items", + "position": "Right", + "title": "Enter Quantity" + }, + { + "child_doctype": "Purchase Invoice Item", + "description": "Enter rate of the item", + "field": "", + "fieldname": "rate", + "fieldtype": "Currency", + "has_next_condition": 0, + "is_table_field": 1, + "label": "Rate", + "parent_field": "", + "parent_fieldname": "items", + "position": "Right", + "title": "Enter Rate" + }, + { + "description": "You can add taxes here", + "field": "", + "fieldname": "taxes", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Purchase Taxes and Charges", + "parent_field": "", + "position": "Bottom", + "title": "Select taxes" + } + ], + "title": "Purchase Invoice" +} \ No newline at end of file diff --git a/erpnext/accounts/form_tour/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json b/erpnext/accounts/form_tour/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json new file mode 100644 index 0000000000..7de9ae1539 --- /dev/null +++ b/erpnext/accounts/form_tour/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json @@ -0,0 +1,65 @@ +{ + "creation": "2021-08-24 12:28:18.044902", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-24 12:28:18.044902", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Sales Taxes and Charges Template", + "owner": "Administrator", + "reference_doctype": "Sales Taxes and Charges Template", + "save_on_complete": 0, + "steps": [ + { + "description": "A name by which you will identify this template. You can change this later.", + "field": "", + "fieldname": "title", + "fieldtype": "Data", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Title", + "parent_field": "", + "position": "Bottom", + "title": "Title" + }, + { + "description": "Company for which this tax template will be applicable", + "field": "", + "fieldname": "company", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Company", + "parent_field": "", + "position": "Bottom", + "title": "Company" + }, + { + "description": "Set this template as the default for all sales transactions", + "field": "", + "fieldname": "is_default", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Default", + "parent_field": "", + "position": "Bottom", + "title": "Is this Default Tax Template?" + }, + { + "description": "You can add a row for a tax rule here. These rules can be applied on the net total, or can be a flat amount.", + "field": "", + "fieldname": "taxes", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Sales Taxes and Charges", + "parent_field": "", + "position": "Bottom", + "title": "Taxes Table" + } + ], + "title": "Sales Taxes and Charges Template" +} \ No newline at end of file diff --git a/erpnext/accounts/module_onboarding/accounts/accounts.json b/erpnext/accounts/module_onboarding/accounts/accounts.json index 6b5c5a1db8..2e0ab4305d 100644 --- a/erpnext/accounts/module_onboarding/accounts/accounts.json +++ b/erpnext/accounts/module_onboarding/accounts/accounts.json @@ -13,12 +13,15 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts", "idx": 0, "is_complete": 0, - "modified": "2020-10-30 15:41:15.547225", + "modified": "2021-08-13 11:59:35.690443", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts", "owner": "Administrator", "steps": [ + { + "step": "Company" + }, { "step": "Chart of Accounts" }, @@ -26,22 +29,19 @@ "step": "Setup Taxes" }, { - "step": "Create a Product" + "step": "Accounts Settings" }, { - "step": "Create a Supplier" + "step": "Cost Centers for Report and Budgeting" }, { "step": "Create Your First Purchase Invoice" }, { - "step": "Create a Customer" + "step": "Updating Opening Balances" }, { - "step": "Create Your First Sales Invoice" - }, - { - "step": "Configure Account Settings" + "step": "Financial Statements" } ], "subtitle": "Accounts, Invoices, Taxation, and more.", diff --git a/erpnext/accounts/onboarding_step/accounts_settings/accounts_settings.json b/erpnext/accounts/onboarding_step/accounts_settings/accounts_settings.json new file mode 100644 index 0000000000..3f44a73685 --- /dev/null +++ b/erpnext/accounts/onboarding_step/accounts_settings/accounts_settings.json @@ -0,0 +1,21 @@ +{ + "action": "Show Form Tour", + "action_label": "Take a quick walk-through of Accounts Settings", + "creation": "2021-06-29 16:42:03.400731", + "description": "# Account Settings\n\nIn ERPNext, Accounting features are configurable as per your business needs. Accounts Settings is the place to define some of your accounting preferences like:\n\n - Credit Limit and over billing settings\n - Taxation preferences\n - Deferred accounting preferences\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 1, + "is_skipped": 0, + "modified": "2021-08-13 11:50:06.227835", + "modified_by": "Administrator", + "name": "Accounts Settings", + "owner": "Administrator", + "reference_document": "Accounts Settings", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Accounts Settings", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json b/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json index fc49bd652b..67553baec7 100644 --- a/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json +++ b/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json @@ -1,10 +1,10 @@ { - "action": "Go to Page", - "action_label": "View Chart of Accounts", + "action": "Watch Video", + "action_label": "Learn more about Chart of Accounts", "callback_message": "You can continue with the onboarding after exploring this page", "callback_title": "Awesome Work", "creation": "2020-05-13 19:58:20.928127", - "description": "# Chart Of Accounts\n\nThe Chart of Accounts is the blueprint of the accounts in your organization.\nIt is a tree view of the names of the Accounts (Ledgers and Groups) that a Company requires to manage its books of accounts. ERPNext sets up a simple chart of accounts for each Company you create, but you can modify it according to your needs and legal requirements.\n\nFor each company, Chart of Accounts signifies the way to classify the accounting entries, mostly\nbased on statutory (tax, compliance to government regulations) requirements.\n\nThere's a brief video tutorial about chart of accounts in the next step.", + "description": "# Chart Of Accounts\n\nERPNext sets up a simple chart of accounts for each Company you create, but you can modify it according to business and legal requirements.", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, @@ -12,7 +12,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-30 14:35:59.474920", + "modified": "2021-08-13 11:46:25.878506", "modified_by": "Administrator", "name": "Chart of Accounts", "owner": "Administrator", @@ -21,5 +21,6 @@ "show_form_tour": 0, "show_full_form": 0, "title": "Review Chart of Accounts", - "validate_action": 0 + "validate_action": 0, + "video_url": "https://www.youtube.com/embed/AcfMCT7wLLo" } \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/company/company.json b/erpnext/accounts/onboarding_step/company/company.json new file mode 100644 index 0000000000..4992e4d018 --- /dev/null +++ b/erpnext/accounts/onboarding_step/company/company.json @@ -0,0 +1,22 @@ +{ + "action": "Go to Page", + "action_label": "Let's Review your Company", + "creation": "2021-06-29 14:47:42.497318", + "description": "# Company\n\nIn ERPNext, you can also create multiple companies, and establish relationships (group/subsidiary) among them.\n\nWithin the company master, you can capture various default accounts for that Company and set crucial settings related to the accounting methodology followed for a company. \n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-13 11:43:35.767341", + "modified_by": "Administrator", + "name": "Company", + "owner": "Administrator", + "path": "app/company", + "reference_document": "Company", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Review Company", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/cost_centers_for_report_and_budgeting/cost_centers_for_report_and_budgeting.json b/erpnext/accounts/onboarding_step/cost_centers_for_report_and_budgeting/cost_centers_for_report_and_budgeting.json new file mode 100644 index 0000000000..252b075697 --- /dev/null +++ b/erpnext/accounts/onboarding_step/cost_centers_for_report_and_budgeting/cost_centers_for_report_and_budgeting.json @@ -0,0 +1,21 @@ +{ + "action": "Go to Page", + "action_label": "View Cost Center Tree", + "creation": "2021-07-12 12:02:05.726608", + "description": "# Cost Centers for Budgeting and Analysis\n\nWhile your Books of Accounts are framed to fulfill statutory requirements, you can set up Cost Center and Accounting Dimensions to address your companies reporting and budgeting requirements.\n\nClick here to learn more about how [Cost Center](https://docs.erpnext.com/docs/v13/user/manual/en/accounts/cost-center) and [Dimensions](https://docs.erpnext.com/docs/v13/user/manual/en/accounts/accounting-dimensions) allow you to get advanced financial analytics reports from ERPNext.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-13 11:55:08.510366", + "modified_by": "Administrator", + "name": "Cost Centers for Report and Budgeting", + "owner": "Administrator", + "path": "cost-center/view/tree", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Cost Centers for Budgeting and Analysis", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/create_your_first_purchase_invoice/create_your_first_purchase_invoice.json b/erpnext/accounts/onboarding_step/create_your_first_purchase_invoice/create_your_first_purchase_invoice.json index ddbc89ec0a..f4e298e701 100644 --- a/erpnext/accounts/onboarding_step/create_your_first_purchase_invoice/create_your_first_purchase_invoice.json +++ b/erpnext/accounts/onboarding_step/create_your_first_purchase_invoice/create_your_first_purchase_invoice.json @@ -1,14 +1,15 @@ { - "action": "Create Entry", + "action": "Show Form Tour", + "action_label": "Let\u2019s create your first Purchase Invoice", "creation": "2020-05-14 22:10:07.049704", - "description": "# What's a Purchase Invoice?\n\nA Purchase Invoice is a bill you receive from your Suppliers against which you need to make the payment.\nPurchase Invoice is the exact opposite of your Sales Invoice. Here you accrue expenses to your Supplier. \n\nThe following is what a typical purchase cycle looks like, however you can create a purchase invoice directly as well.\n\n![Purchase Flow](https://docs.erpnext.com/docs/assets/img/accounts/pi-flow.png)\n\n", + "description": "# Create your first Purchase Invoice\n\nA Purchase Invoice is a bill received from a Supplier for a product(s) or service(s) delivery to your company. You can track payables through Purchase Invoice and process Payment Entries against it.\n\nPurchase Invoices can also be created against a Purchase Order or Purchase Receipt.", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-30 15:30:26.337773", + "modified": "2021-08-13 11:56:11.677253", "modified_by": "Administrator", "name": "Create Your First Purchase Invoice", "owner": "Administrator", diff --git a/erpnext/accounts/onboarding_step/financial_statements/financial_statements.json b/erpnext/accounts/onboarding_step/financial_statements/financial_statements.json new file mode 100644 index 0000000000..85cf9cdd7c --- /dev/null +++ b/erpnext/accounts/onboarding_step/financial_statements/financial_statements.json @@ -0,0 +1,23 @@ +{ + "action": "View Report", + "creation": "2021-07-12 12:08:47.026115", + "description": "# Financial Statements\n\nIn ERPNext, you can get crucial financial reports like [Balance Sheet] and [Profit and Loss] statements with a click of a button. You can run in the report for a different period and plot analytics charts premised on statement data. For more reports, check sections like Financial Statements, General Ledger, and Profitability reports.\n\n[Check Accounting reports](https://docs.erpnext.com/docs/v13/user/manual/en/accounts/accounting-reports)", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-13 11:59:18.767407", + "modified_by": "Administrator", + "name": "Financial Statements", + "owner": "Administrator", + "reference_report": "General Ledger", + "report_description": "General Ledger", + "report_reference_doctype": "GL Entry", + "report_type": "Script Report", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Financial Statements", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json b/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json index a4922013da..9f4c873e34 100644 --- a/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json +++ b/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json @@ -1,21 +1,21 @@ { "action": "Create Entry", - "action_label": "Make a Sales Tax Template", + "action_label": "Manage Sales Tax Templates", "creation": "2020-05-13 19:29:43.844463", - "description": "# Setting up Taxes\n\nAny sophisticated accounting system, including ERPNext will have automatic tax calculations for your transactions. These calculations are based on user defined rules in compliance to local rules and regulations.\n\nERPNext allows this via *Tax Templates*. These templates can be used in Sales Orders and Sales Invoices. Other types of charges that may apply to your invoices (like shipping, insurance etc.) can also be configured as taxes.\n\nFor Tax Accounts that you want to use in the tax templates, go to:\n\n`> Accounting > Taxes > Sales Taxes and Charges Template`\n\nYou can read more about these templates in our documentation [here](https://docs.erpnext.com/docs/user/manual/en/selling/sales-taxes-and-charges-template)\n", + "description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.\n", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-30 14:54:18.087383", + "modified": "2021-08-13 11:48:37.238610", "modified_by": "Administrator", "name": "Setup Taxes", "owner": "Administrator", "reference_document": "Sales Taxes and Charges Template", "show_form_tour": 1, "show_full_form": 1, - "title": "Lets create a Tax Template for Sales ", + "title": "Setting up Taxes", "validate_action": 0 } \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/updating_opening_balances/updating_opening_balances.json b/erpnext/accounts/onboarding_step/updating_opening_balances/updating_opening_balances.json new file mode 100644 index 0000000000..c0849a4ef5 --- /dev/null +++ b/erpnext/accounts/onboarding_step/updating_opening_balances/updating_opening_balances.json @@ -0,0 +1,22 @@ +{ + "action": "Watch Video", + "action_label": "Learn how to update opening balances", + "creation": "2021-07-12 11:53:50.525030", + "description": "# Updating Opening Balances\n\nOnce you close the financial statement in previous accounting software, you can update the same as opening in your ERPNext's Balance Sheet accounts. This will allow you to get complete financial statements from ERPNext in the coming years, and discontinue the parallel accounting system right away.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "intro_video_url": "https://www.youtube.com/embed/U5wPIvEn-0c", + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-13 11:56:45.483418", + "modified_by": "Administrator", + "name": "Updating Opening Balances", + "owner": "Administrator", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Updating Opening Balances", + "validate_action": 1, + "video_url": "https://www.youtube.com/embed/U5wPIvEn-0c" +} \ No newline at end of file diff --git a/erpnext/assets/form_tour/asset/asset.json b/erpnext/assets/form_tour/asset/asset.json new file mode 100644 index 0000000000..7c47a38bd1 --- /dev/null +++ b/erpnext/assets/form_tour/asset/asset.json @@ -0,0 +1,125 @@ +{ + "creation": "2021-08-24 16:55:10.923434", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-24 16:55:10.923434", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset", + "owner": "Administrator", + "reference_doctype": "Asset", + "save_on_complete": 0, + "steps": [ + { + "description": "Select Naming Series based on which Asset ID will be generated", + "field": "", + "fieldname": "naming_series", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Naming Series", + "parent_field": "", + "position": "Bottom", + "title": "Naming Series" + }, + { + "description": "Select an Asset Item", + "field": "", + "fieldname": "item_code", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Item Code", + "parent_field": "", + "position": "Bottom", + "title": "Item Code" + }, + { + "description": "Select a Location", + "field": "", + "fieldname": "location", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Location", + "parent_field": "", + "position": "Bottom", + "title": "Location" + }, + { + "description": "Check Is Existing Asset", + "field": "", + "fieldname": "is_existing_asset", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Is Existing Asset", + "parent_field": "", + "position": "Bottom", + "title": "Is Existing Asset?" + }, + { + "description": "Set Available for use date", + "field": "", + "fieldname": "available_for_use_date", + "fieldtype": "Date", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Available-for-use Date", + "parent_field": "", + "position": "Bottom", + "title": "Available For Use Date" + }, + { + "description": "Set Gross purchase amount", + "field": "", + "fieldname": "gross_purchase_amount", + "fieldtype": "Currency", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Gross Purchase Amount", + "parent_field": "", + "position": "Bottom", + "title": "Gross Purchase Amount" + }, + { + "description": "Set Purchase Date", + "field": "", + "fieldname": "purchase_date", + "fieldtype": "Date", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Purchase Date", + "parent_field": "", + "position": "Bottom", + "title": "Purchase Date" + }, + { + "description": "Check Calculate Depreciation", + "field": "", + "fieldname": "calculate_depreciation", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Calculate Depreciation", + "parent_field": "", + "position": "Bottom", + "title": "Calculate Depreciation" + }, + { + "description": "Enter depreciation which has already been booked for this asset", + "field": "", + "fieldname": "opening_accumulated_depreciation", + "fieldtype": "Currency", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Opening Accumulated Depreciation", + "parent_field": "", + "position": "Bottom", + "title": "Accumulated Depreciation" + } + ], + "title": "Asset" +} \ No newline at end of file diff --git a/erpnext/assets/form_tour/asset_category/asset_category.json b/erpnext/assets/form_tour/asset_category/asset_category.json new file mode 100644 index 0000000000..02834447a5 --- /dev/null +++ b/erpnext/assets/form_tour/asset_category/asset_category.json @@ -0,0 +1,65 @@ +{ + "creation": "2021-08-24 12:48:20.763173", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-24 12:48:20.763173", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Category", + "owner": "Administrator", + "reference_doctype": "Asset Category", + "save_on_complete": 0, + "steps": [ + { + "description": "Name Asset category. You can create categories based on Asset Types like Furniture, Property, Electronics etc.", + "field": "", + "fieldname": "asset_category_name", + "fieldtype": "Data", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Asset Category Name", + "parent_field": "", + "position": "Bottom", + "title": "Asset Category Name" + }, + { + "description": "Check to enable Capital Work in Progress accounting", + "field": "", + "fieldname": "enable_cwip_accounting", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Enable Capital Work in Progress Accounting", + "parent_field": "", + "position": "Bottom", + "title": "Enable CWIP Accounting" + }, + { + "description": "Add a row to define Depreciation Method and other details. Note that you can leave Finance Book blank to have it's accounting done in the primary books of accounts.", + "field": "", + "fieldname": "finance_books", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Finance Books", + "parent_field": "", + "position": "Bottom", + "title": "Finance Book Detail" + }, + { + "description": "Select the Fixed Asset and Depreciation accounts applicable for this Asset Category type", + "field": "", + "fieldname": "accounts", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Accounts", + "parent_field": "", + "position": "Bottom", + "title": "Accounts" + } + ], + "title": "Asset Category" +} \ No newline at end of file diff --git a/erpnext/assets/module_onboarding/assets/assets.json b/erpnext/assets/module_onboarding/assets/assets.json index 1086ab4bcd..e6df88b000 100644 --- a/erpnext/assets/module_onboarding/assets/assets.json +++ b/erpnext/assets/module_onboarding/assets/assets.json @@ -13,26 +13,26 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/asset", "idx": 0, "is_complete": 0, - "modified": "2020-07-08 14:05:51.828497", + "modified": "2021-08-24 17:50:41.573281", "modified_by": "Administrator", "module": "Assets", "name": "Assets", "owner": "Administrator", "steps": [ { - "step": "Introduction to Assets" + "step": "Fixed Asset Accounts" }, { - "step": "Create a Fixed Asset Item" + "step": "Asset Category" }, { - "step": "Create an Asset Category" + "step": "Asset Item" }, { - "step": "Purchase an Asset Item" + "step": "Asset Purchase" }, { - "step": "Create an Asset" + "step": "Existing Asset" } ], "subtitle": "Assets, Depreciations, Repairs, and more.", diff --git a/erpnext/assets/onboarding_step/asset_category/asset_category.json b/erpnext/assets/onboarding_step/asset_category/asset_category.json new file mode 100644 index 0000000000..033e86669c --- /dev/null +++ b/erpnext/assets/onboarding_step/asset_category/asset_category.json @@ -0,0 +1,21 @@ +{ + "action": "Show Form Tour", + "action_label": "Let's review existing Asset Category", + "creation": "2021-08-13 14:26:18.656303", + "description": "# Asset Category\n\nAn Asset Category classifies different assets of a Company.\n\nYou can create an Asset Category based on the type of assets. For example, all your desktops and laptops can be part of an Asset Category named \"Electronic Equipments\". Create a separate category for furniture. Also, you can update default properties for each category, like:\n - Depreciation type and duration\n - Fixed asset account\n - Depreciation account\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-24 12:49:37.665239", + "modified_by": "Administrator", + "name": "Asset Category", + "owner": "Administrator", + "reference_document": "Asset Category", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Define Asset Category", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/assets/onboarding_step/asset_item/asset_item.json b/erpnext/assets/onboarding_step/asset_item/asset_item.json new file mode 100644 index 0000000000..8a174c5b77 --- /dev/null +++ b/erpnext/assets/onboarding_step/asset_item/asset_item.json @@ -0,0 +1,21 @@ +{ + "action": "Show Form Tour", + "action_label": "Let's create a new Asset item", + "creation": "2021-08-13 14:27:07.277167", + "description": "# Asset Item\n\nAsset items are created based on Asset Category. You can create one or multiple items against once Asset Category. The sales and purchase transaction for Asset is done via Asset Item. ", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-16 13:59:18.362233", + "modified_by": "Administrator", + "name": "Asset Item", + "owner": "Administrator", + "reference_document": "Item", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Create an Asset Item", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json b/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json new file mode 100644 index 0000000000..54611edc29 --- /dev/null +++ b/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json @@ -0,0 +1,21 @@ +{ + "action": "Show Form Tour", + "action_label": "Let's create a Purchase Receipt", + "creation": "2021-08-13 14:27:53.678621", + "description": "# Purchase an Asset\n\nAssets purchases process if done following the standard Purchase cycle. If capital work in progress is enabled in Asset Category, Asset will be created as soon as Purchase Receipt is created for it. You can quickly create a Purchase Receipt for Asset and see its impact on books of accounts.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-24 17:26:57.180637", + "modified_by": "Administrator", + "name": "Asset Purchase", + "owner": "Administrator", + "reference_document": "Purchase Receipt", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Purchase an Asset", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/assets/onboarding_step/existing_asset/existing_asset.json b/erpnext/assets/onboarding_step/existing_asset/existing_asset.json new file mode 100644 index 0000000000..052d5e8d77 --- /dev/null +++ b/erpnext/assets/onboarding_step/existing_asset/existing_asset.json @@ -0,0 +1,21 @@ +{ + "action": "Show Form Tour", + "action_label": "Add an existing Asset", + "creation": "2021-08-13 14:28:30.650459", + "description": "# Add an Existing Asset\n\nIf you are just starting with ERPNext, you will need to enter Assets you already possess. You can add them as existing fixed assets in ERPNext. Please note that you will have to make a Journal Entry separately updating the opening balance in the fixed asset account.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-16 14:03:48.850471", + "modified_by": "Administrator", + "name": "Existing Asset", + "owner": "Administrator", + "reference_document": "Asset", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Add an Existing Asset", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json b/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json new file mode 100644 index 0000000000..cebee7a7ea --- /dev/null +++ b/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json @@ -0,0 +1,21 @@ +{ + "action": "Go to Page", + "action_label": "Let's walk-through Chart of Accounts to review setup", + "creation": "2021-08-13 14:23:09.297765", + "description": "# Fixed Asset Accounts\n\nWith the company, a host of fixed asset accounts are pre-configured. To ensure your asset transactions are leading to correct accounting entries, you can review and set up following asset accounts as per your business requirements.\n - Fixed asset accounts (Asset account)\n - Accumulated depreciation\n - Capital Work in progress (CWIP) account\n - Asset Depreciation account (Expense account)", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-24 17:46:37.646174", + "modified_by": "Administrator", + "name": "Fixed Asset Accounts", + "owner": "Administrator", + "path": "app/account/view/tree", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Review Fixed Asset Accounts", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.js b/erpnext/selling/doctype/selling_settings/selling_settings.js index d8d30515f8..cf6fb2806e 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.js +++ b/erpnext/selling/doctype/selling_settings/selling_settings.js @@ -6,26 +6,3 @@ frappe.ui.form.on('Selling Settings', { } }); - -frappe.tour['Selling Settings'] = [ - { - fieldname: "cust_master_name", - title: "Customer Naming By", - description: __("By default, the Customer Name is set as per the Full Name entered. If you want Customers to be named by a ") + "Naming Series" + __(" choose the 'Naming Series' option."), - }, - { - fieldname: "selling_price_list", - title: "Default Selling Price List", - description: __("Configure the default Price List when creating a new Sales transaction. Item prices will be fetched from this Price List.") - }, - { - fieldname: "so_required", - title: "Sales Order Required for Sales Invoice & Delivery Note Creation", - description: __("If this option is configured 'Yes', ERPNext will prevent you from creating a Sales Invoice or Delivery Note without creating a Sales Order first. This configuration can be overridden for a particular Customer by enabling the 'Allow Sales Invoice Creation Without Sales Order' checkbox in the Customer master.") - }, - { - fieldname: "dn_required", - title: "Delivery Note Required for Sales Invoice Creation", - description: __("If this option is configured 'Yes', ERPNext will prevent you from creating a Sales Invoice without creating a Delivery Note first. This configuration can be overridden for a particular Customer by enabling the 'Allow Sales Invoice Creation Without Delivery Note' checkbox in the Customer master.") - } -]; diff --git a/erpnext/selling/form_tour/sales_order/sales_order.json b/erpnext/selling/form_tour/sales_order/sales_order.json new file mode 100644 index 0000000000..a81eb4a043 --- /dev/null +++ b/erpnext/selling/form_tour/sales_order/sales_order.json @@ -0,0 +1,97 @@ +{ + "creation": "2021-06-29 21:13:36.089054", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-06-29 21:13:36.089054", + "modified_by": "Administrator", + "module": "Selling", + "name": "Sales Order", + "owner": "Administrator", + "reference_doctype": "Sales Order", + "save_on_complete": 1, + "steps": [ + { + "description": "Select a customer.", + "field": "", + "fieldname": "customer", + "fieldtype": "Link", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Customer", + "next_step_condition": "customer", + "parent_field": "", + "position": "Right", + "title": "Select Customer" + }, + { + "description": "You can add items here.", + "field": "", + "fieldname": "items", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Items", + "parent_field": "", + "position": "Bottom", + "title": "List of items" + }, + { + "child_doctype": "Sales Order Item", + "description": "Select an item.", + "field": "", + "fieldname": "item_code", + "fieldtype": "Link", + "has_next_condition": 1, + "is_table_field": 1, + "label": "Item Code", + "next_step_condition": "eval: doc.item_code", + "parent_field": "", + "parent_fieldname": "items", + "position": "Right", + "title": "Select Item" + }, + { + "child_doctype": "Sales Order Item", + "description": "Enter quantity.", + "field": "", + "fieldname": "qty", + "fieldtype": "Float", + "has_next_condition": 0, + "is_table_field": 1, + "label": "Quantity", + "parent_field": "", + "parent_fieldname": "items", + "position": "Right", + "title": "Enter Quantity" + }, + { + "child_doctype": "Sales Order Item", + "description": "Enter rate of the item.", + "field": "", + "fieldname": "rate", + "fieldtype": "Currency", + "has_next_condition": 0, + "is_table_field": 1, + "label": "Rate", + "parent_field": "", + "parent_fieldname": "items", + "position": "Right", + "title": "Enter Rate" + }, + { + "description": "You can add sales taxes and charges here.", + "field": "", + "fieldname": "taxes", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Sales Taxes and Charges", + "parent_field": "", + "position": "Bottom", + "title": "Add Sales Taxes and Charges" + } + ], + "title": "Sales Order" +} \ No newline at end of file diff --git a/erpnext/selling/form_tour/selling_settings/selling_settings.json b/erpnext/selling/form_tour/selling_settings/selling_settings.json new file mode 100644 index 0000000000..20c718f8c0 --- /dev/null +++ b/erpnext/selling/form_tour/selling_settings/selling_settings.json @@ -0,0 +1,65 @@ +{ + "creation": "2021-06-29 20:39:19.408763", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-06-29 20:49:01.359489", + "modified_by": "Administrator", + "module": "Selling", + "name": "Selling Settings", + "owner": "Administrator", + "reference_doctype": "Selling Settings", + "save_on_complete": 0, + "steps": [ + { + "description": "By default, the Customer Name is set as per the Full Name entered. If you want Customers to be named by a Naming Series. Choose the 'Naming Series' option.", + "field": "", + "fieldname": "cust_master_name", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Customer Naming By", + "parent_field": "", + "position": "Right", + "title": "Customer Naming By" + }, + { + "description": "If this option is configured 'Yes', ERPNext will prevent you from creating a Sales Invoice or Delivery Note without creating a Sales Order first. This configuration can be overridden for a particular Customer by enabling the 'Allow Sales Invoice Creation Without Sales Order' checkbox in the Customer master.", + "field": "", + "fieldname": "so_required", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Is Sales Order Required for Sales Invoice & Delivery Note Creation?", + "parent_field": "", + "position": "Left", + "title": "Sales Order Required for Sales Invoice & Delivery Note Creation" + }, + { + "description": "If this option is configured 'Yes', ERPNext will prevent you from creating a Sales Invoice without creating a Delivery Note first. This configuration can be overridden for a particular Customer by enabling the 'Allow Sales Invoice Creation Without Delivery Note' checkbox in the Customer master.", + "field": "", + "fieldname": "dn_required", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Is Delivery Note Required for Sales Invoice Creation?", + "parent_field": "", + "position": "Left", + "title": "Delivery Note Required for Sales Invoice Creation" + }, + { + "description": "Configure the default Price List when creating a new Sales transaction. Item prices will be fetched from this Price List.", + "field": "", + "fieldname": "selling_price_list", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Default Price List", + "parent_field": "", + "position": "Right", + "title": "Default Selling Price List" + } + ], + "title": "Selling Settings" +} \ No newline at end of file diff --git a/erpnext/selling/module_onboarding/selling/selling.json b/erpnext/selling/module_onboarding/selling/selling.json index 160208ff68..c02f444b05 100644 --- a/erpnext/selling/module_onboarding/selling/selling.json +++ b/erpnext/selling/module_onboarding/selling/selling.json @@ -19,32 +19,17 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/selling", "idx": 0, "is_complete": 0, - "modified": "2020-07-08 14:05:37.669753", + "modified": "2021-08-24 18:12:38.052434", "modified_by": "Administrator", "module": "Selling", "name": "Selling", "owner": "Administrator", "steps": [ - { - "step": "Introduction to Selling" - }, - { - "step": "Create a Customer" - }, - { - "step": "Setup your Warehouse" - }, - { - "step": "Create a Product" - }, - { - "step": "Create a Quotation" - }, - { - "step": "Create your first Sales Order" - }, { "step": "Selling Settings" + }, + { + "step": "Sales Order" } ], "subtitle": "Products, Sales, Analysis, and more.", diff --git a/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json index 5a403b06cf..64defbfe3d 100644 --- a/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json +++ b/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json @@ -5,7 +5,6 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, "modified": "2020-06-01 13:16:19.731719", @@ -13,6 +12,7 @@ "name": "Create a Customer", "owner": "Administrator", "reference_document": "Customer", + "show_form_tour": 0, "show_full_form": 0, "title": "Create a Customer", "validate_action": 1 diff --git a/erpnext/selling/onboarding_step/create_a_product/create_a_product.json b/erpnext/selling/onboarding_step/create_a_product/create_a_product.json index d2068e167b..52a5861551 100644 --- a/erpnext/selling/onboarding_step/create_a_product/create_a_product.json +++ b/erpnext/selling/onboarding_step/create_a_product/create_a_product.json @@ -5,14 +5,14 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-05-12 18:30:02.489949", + "modified": "2020-10-14 14:53:00.133574", "modified_by": "Administrator", "name": "Create a Product", "owner": "Administrator", "reference_document": "Item", + "show_form_tour": 0, "show_full_form": 0, "title": "Create a Product", "validate_action": 1 diff --git a/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json b/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json index 27253d15b6..6f1837e24c 100644 --- a/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json +++ b/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json @@ -5,7 +5,6 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, "modified": "2020-06-01 13:34:58.958641", @@ -13,6 +12,7 @@ "name": "Create a Quotation", "owner": "Administrator", "reference_document": "Quotation", + "show_form_tour": 0, "show_full_form": 1, "title": "Create a Quotation", "validate_action": 1 diff --git a/erpnext/selling/onboarding_step/create_a_sales_order/create_a_sales_order.json b/erpnext/selling/onboarding_step/create_a_sales_order/create_a_sales_order.json new file mode 100644 index 0000000000..378f295f0c --- /dev/null +++ b/erpnext/selling/onboarding_step/create_a_sales_order/create_a_sales_order.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Create Sales Order", + "creation": "2021-06-29 21:22:54.204880", + "description": "A Sales Order is a confirmation of an order from your customer. It is also referred to as Proforma Invoice.\n\nSales Order at the heart of your sales and purchase transactions. Sales Orders are linked in Delivery Note, Sales Invoices, Material Request, and Maintenance transactions. Through Sales Order, you can track fulfillment of the overall deal towards the customer.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-06-29 21:22:54.204880", + "modified_by": "Administrator", + "name": "Create a Sales Order", + "owner": "Administrator", + "reference_document": "Sales Order", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Create a Sales Order", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json b/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json index d21c1f4954..636453363b 100644 --- a/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json +++ b/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json @@ -5,13 +5,13 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, "modified": "2020-06-01 13:29:13.703177", "modified_by": "Administrator", "name": "Introduction to Selling", "owner": "Administrator", + "show_form_tour": 0, "show_full_form": 0, "title": "Introduction to Selling", "validate_action": 1, diff --git a/erpnext/selling/onboarding_step/sales_order/sales_order.json b/erpnext/selling/onboarding_step/sales_order/sales_order.json new file mode 100644 index 0000000000..d616fae066 --- /dev/null +++ b/erpnext/selling/onboarding_step/sales_order/sales_order.json @@ -0,0 +1,21 @@ +{ + "action": "Show Form Tour", + "action_label": "Let\u2019s convert your first Sales Order against a Quotation", + "creation": "2021-08-13 14:03:49.217866", + "description": "# Sales Order\n\nA Sales Order is a confirmation of an order from your customer. It is also referred to as Proforma Invoice.\n\nSales Order at the heart of your sales and purchase transactions. Sales Orders are linked in Delivery Note, Sales Invoices, Material Request, and Maintenance transactions. Through Sales Order, you can track fulfillment of the overall deal towards the customer.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-24 18:12:32.394992", + "modified_by": "Administrator", + "name": "Sales Order", + "owner": "Administrator", + "reference_document": "Sales Order", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Sales Order", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/selling/onboarding_step/selling_settings/selling_settings.json b/erpnext/selling/onboarding_step/selling_settings/selling_settings.json index 7996d7b159..ec30fd3033 100644 --- a/erpnext/selling/onboarding_step/selling_settings/selling_settings.json +++ b/erpnext/selling/onboarding_step/selling_settings/selling_settings.json @@ -1,19 +1,21 @@ { "action": "Show Form Tour", + "action_label": "Let\u2019s walk-through Selling Settings", "creation": "2020-06-01 13:01:45.615189", + "description": "# Selling Settings\n\nCRM and Selling module\u2019s features are configurable as per your business needs. Selling Settings is the place where you can set your preferences for:\n - Customer naming and default values\n - Billing and shipping preference in sales transactions\n", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 1, "is_skipped": 0, - "modified": "2020-06-01 13:04:14.980743", + "modified": "2021-08-24 18:11:21.264335", "modified_by": "Administrator", "name": "Selling Settings", "owner": "Administrator", "reference_document": "Selling Settings", + "show_form_tour": 0, "show_full_form": 0, - "title": "Configure Selling Settings.", + "title": "Selling Settings", "validate_action": 0 } \ No newline at end of file diff --git a/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json b/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json index 9457deee26..1e20eb0eba 100644 --- a/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json +++ b/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json @@ -5,15 +5,15 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-07-04 12:33:16.970031", + "modified": "2020-10-14 14:53:25.538900", "modified_by": "Administrator", "name": "Setup your Warehouse", "owner": "Administrator", "path": "Tree/Warehouse", "reference_document": "Warehouse", + "show_form_tour": 0, "show_full_form": 0, "title": "Set up your Warehouse", "validate_action": 1 diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index e4ee3ecea7..58cb52c04d 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -715,6 +715,15 @@ "label": "Default Payment Discount Account", "options": "Account" }, + { + "fieldname": "hr_settings_section", + "fieldtype": "Section Break", + "label": "HR & Payroll Settings" + }, + { + "fieldname": "column_break_26", + "fieldtype": "Column Break" + }, { "collapsible": 1, "fieldname": "fixed_asset_defaults", @@ -731,15 +740,6 @@ "fieldname": "section_break_28", "fieldtype": "Section Break", "label": "Chart of Accounts" - }, - { - "fieldname": "hr_settings_section", - "fieldtype": "Section Break", - "label": "HR & Payroll Settings" - }, - { - "fieldname": "column_break_26", - "fieldtype": "Column Break" } ], "icon": "fa fa-building", @@ -808,4 +808,4 @@ "sort_field": "modified", "sort_order": "ASC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/form_tour/item/item.json b/erpnext/stock/form_tour/item/item.json new file mode 100644 index 0000000000..821e91b28d --- /dev/null +++ b/erpnext/stock/form_tour/item/item.json @@ -0,0 +1,89 @@ +{ + "creation": "2021-08-24 17:56:40.754909", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-24 18:04:50.928431", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item", + "owner": "Administrator", + "reference_doctype": "Item", + "save_on_complete": 0, + "steps": [ + { + "description": "Enter code for Asset Item", + "field": "", + "fieldname": "item_code", + "fieldtype": "Data", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Item Code", + "parent_field": "", + "position": "Bottom", + "title": "Asset Item Code" + }, + { + "description": "Enter name for Asset Item", + "field": "", + "fieldname": "item_name", + "fieldtype": "Data", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Item Name", + "parent_field": "", + "position": "Bottom", + "title": "Asset Item Name" + }, + { + "description": "Check this field to make this an Asset Item", + "field": "", + "fieldname": "is_fixed_asset", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Is Fixed Asset", + "parent_field": "", + "position": "Bottom", + "title": "Is this a Fixed Asset?" + }, + { + "description": "On checking it, the system will create an Asset automatically on purchase", + "field": "", + "fieldname": "auto_create_assets", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Auto Create Assets on Purchase", + "parent_field": "", + "position": "Bottom", + "title": "Auto Create Asset on Purchase" + }, + { + "description": "Select an Asset Category for this Asset Item", + "field": "", + "fieldname": "asset_category", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Asset Category", + "parent_field": "", + "position": "Bottom", + "title": "Asset Category" + }, + { + "description": "Select a naming series which will be used to create an Asset automatically", + "field": "", + "fieldname": "asset_naming_series", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Asset Naming Series", + "parent_field": "", + "position": "Bottom", + "title": "Asset Naming Series" + } + ], + "title": "Item" +} diff --git a/erpnext/stock/form_tour/purchase_receipt/purchase_receipt.json b/erpnext/stock/form_tour/purchase_receipt/purchase_receipt.json new file mode 100644 index 0000000000..6fba3f4d68 --- /dev/null +++ b/erpnext/stock/form_tour/purchase_receipt/purchase_receipt.json @@ -0,0 +1,41 @@ +{ + "creation": "2021-08-24 13:03:21.333088", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-24 13:03:21.333088", + "modified_by": "Administrator", + "module": "Stock", + "name": "Purchase Receipt", + "owner": "Administrator", + "reference_doctype": "Purchase Receipt", + "save_on_complete": 0, + "steps": [ + { + "description": "Select Asset Supplier", + "field": "", + "fieldname": "supplier", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Supplier", + "parent_field": "", + "position": "Bottom", + "title": "Supplier" + }, + { + "description": "Select an Asset Item, Enter rate and quantity", + "field": "", + "fieldname": "items", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Items", + "parent_field": "", + "position": "Bottom", + "title": "Items" + } + ], + "title": "Purchase Receipt" +} \ No newline at end of file From bf72ec0598de4688af03d80c34c0241c5b5fb194 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 27 Aug 2021 14:57:42 +0530 Subject: [PATCH 12/44] feat: (consistency) Add Primary Address and Contact section in Supplier - The same is present in customer and is inconsistent with supplier - Helps quickly create primary address and contact via quick entry --- erpnext/buying/doctype/supplier/supplier.js | 43 +++++++++++ erpnext/buying/doctype/supplier/supplier.json | 51 ++++++++++++- erpnext/buying/doctype/supplier/supplier.py | 45 +++++++++++ erpnext/public/build.json | 1 + .../public/js/utils/supplier_quick_entry.js | 75 +++++++++++++++++++ 5 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 erpnext/public/js/utils/supplier_quick_entry.js diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index 1766c2c80c..7ee91961ca 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -24,7 +24,26 @@ frappe.ui.form.on("Supplier", { } } }); + + frm.set_query("supplier_primary_contact", function(doc) { + return { + query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary_contact", + filters: { + "supplier": doc.name + } + }; + }); + + frm.set_query("supplier_primary_address", function(doc) { + return { + filters: { + "link_doctype": "Supplier", + "link_name": doc.name + } + }; + }); }, + refresh: function (frm) { frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Supplier' } @@ -78,6 +97,30 @@ frappe.ui.form.on("Supplier", { }); }, + supplier_primary_address: function(frm) { + if (frm.doc.supplier_primary_address) { + frappe.call({ + method: 'frappe.contacts.doctype.address.address.get_address_display', + args: { + "address_dict": frm.doc.supplier_primary_address + }, + callback: function(r) { + frm.set_value("primary_address", r.message); + } + }); + } + if (!frm.doc.supplier_primary_address) { + frm.set_value("primary_address", ""); + } + }, + + supplier_primary_contact: function(frm) { + if (!frm.doc.supplier_primary_contact) { + frm.set_value("mobile_no", ""); + frm.set_value("email_id", ""); + } + }, + is_internal_supplier: function(frm) { if (frm.doc.is_internal_supplier == 1) { frm.toggle_reqd("represents_company", true); diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index 38b8dfdf48..22e689c101 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -49,6 +49,13 @@ "address_html", "column_break1", "contact_html", + "primary_address_and_contact_detail_section", + "supplier_primary_contact", + "mobile_no", + "email_id", + "column_break_44", + "supplier_primary_address", + "primary_address", "default_payable_accounts", "accounts", "default_tax_withholding_config", @@ -378,6 +385,48 @@ "fieldname": "allow_purchase_invoice_creation_without_purchase_receipt", "fieldtype": "Check", "label": "Allow Purchase Invoice Creation Without Purchase Receipt" + }, + { + "fieldname": "primary_address_and_contact_detail_section", + "fieldtype": "Section Break", + "label": "Primary Address and Contact Detail" + }, + { + "description": "Reselect, if the chosen contact is edited after save", + "fieldname": "supplier_primary_contact", + "fieldtype": "Link", + "label": "Supplier Primary Contact", + "options": "Contact" + }, + { + "depends_on": "mobile_no", + "fetch_from": "supplier_primary_contact.mobile_no", + "fieldname": "mobile_no", + "fieldtype": "Read Only", + "label": "Mobile No" + }, + { + "fetch_from": "supplier_primary_contact.email_id", + "fieldname": "email_id", + "fieldtype": "Read Only", + "label": "Email Id" + }, + { + "fieldname": "column_break_44", + "fieldtype": "Column Break" + }, + { + "fieldname": "primary_address", + "fieldtype": "Text", + "label": "Primary Address", + "read_only": 1 + }, + { + "description": "Reselect, if the chosen address is edited after save", + "fieldname": "supplier_primary_address", + "fieldtype": "Link", + "label": "Supplier Primary Address", + "options": "Address" } ], "icon": "fa fa-user", @@ -390,7 +439,7 @@ "link_fieldname": "supplier" } ], - "modified": "2021-05-18 15:10:11.087191", + "modified": "2021-08-27 13:46:18.212802", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index fd16b23c22..8252b40566 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -42,6 +42,8 @@ class Supplier(TransactionBase): if not self.naming_series: self.naming_series = '' + self.create_primary_contact() + def validate(self): # validation for Naming Series mandatory field... if frappe.defaults.get_global_default('supp_master_name') == 'Naming Series': @@ -76,7 +78,32 @@ class Supplier(TransactionBase): frappe.throw(_("Internal Supplier for company {0} already exists").format( frappe.bold(self.represents_company))) + def create_primary_contact(self): + from erpnext.selling.doctype.customer.customer import make_contact + + if not self.supplier_primary_contact: + if self.mobile_no or self.email_id: + contact = make_contact(self) + self.db_set('supplier_primary_contact', contact.name) + self.db_set('mobile_no', self.mobile_no) + self.db_set('email_id', self.email_id) + + def create_primary_address(self): + from erpnext.selling.doctype.customer.customer import make_address + + if self.flags.is_new_doc and self.get('address_line1'): + make_address(self) + def on_trash(self): + if self.supplier_primary_contact: + frappe.db.sql(f""" + UPDATE `tabSupplier` + SET + supplier_primary_contact=null, + mobile_no=null, + email_id=null + WHERE name='{self.name}'""") + delete_contact_and_address('Supplier', self.name) def after_rename(self, olddn, newdn, merge=False): @@ -104,3 +131,21 @@ class Supplier(TransactionBase): doc.name, args.get('supplier_email_' + str(i))) except frappe.NameError: pass + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters): + supplier = filters.get("supplier") + return frappe.db.sql(""" + SELECT + `tabContact`.name from `tabContact`, + `tabDynamic Link` + WHERE + `tabContact`.name = `tabDynamic Link`.parent + and `tabDynamic Link`.link_name = %(supplier)s + and `tabDynamic Link`.link_doctype = 'Supplier' + and `tabContact`.name like %(txt)s + """, { + 'supplier': supplier, + 'txt': '%%%s%%' % txt + }) diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 3c60e3ee50..6b70dab803 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -38,6 +38,7 @@ "public/js/templates/item_quick_entry.html", "public/js/utils/item_quick_entry.js", "public/js/utils/customer_quick_entry.js", + "public/js/utils/supplier_quick_entry.js", "public/js/education/student_button.html", "public/js/education/assessment_result_tool.html", "public/js/hub/hub_factory.js", diff --git a/erpnext/public/js/utils/supplier_quick_entry.js b/erpnext/public/js/utils/supplier_quick_entry.js new file mode 100644 index 0000000000..f650d1f101 --- /dev/null +++ b/erpnext/public/js/utils/supplier_quick_entry.js @@ -0,0 +1,75 @@ +frappe.provide('frappe.ui.form'); + +frappe.ui.form.SupplierQuickEntryForm = class SupplierQuickEntryForm extends frappe.ui.form.QuickEntryForm { + constructor(doctype, after_insert, init_callback, doc, force) { + super(doctype, after_insert, init_callback, doc, force); + this.skip_redirect_on_error = true; + } + + render_dialog() { + this.mandatory = this.mandatory.concat(this.get_variant_fields()); + super.render_dialog(); + } + + get_variant_fields() { + var variant_fields = [{ + fieldtype: "Section Break", + label: __("Primary Contact Details"), + collapsible: 1 + }, + { + label: __("Email Id"), + fieldname: "email_id", + fieldtype: "Data" + }, + { + fieldtype: "Column Break" + }, + { + label: __("Mobile Number"), + fieldname: "mobile_no", + fieldtype: "Data" + }, + { + fieldtype: "Section Break", + label: __("Primary Address Details"), + collapsible: 1 + }, + { + label: __("Address Line 1"), + fieldname: "address_line1", + fieldtype: "Data" + }, + { + label: __("Address Line 2"), + fieldname: "address_line2", + fieldtype: "Data" + }, + { + label: __("ZIP Code"), + fieldname: "pincode", + fieldtype: "Data" + }, + { + fieldtype: "Column Break" + }, + { + label: __("City"), + fieldname: "city", + fieldtype: "Data" + }, + { + label: __("State"), + fieldname: "state", + fieldtype: "Data" + }, + { + label: __("Country"), + fieldname: "country", + fieldtype: "Link", + options: "Country" + }]; + + return variant_fields; + } +}; From e170ad2df362f2b7fe086e5f0062896c3371fda3 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 27 Aug 2021 16:26:12 +0530 Subject: [PATCH 13/44] chore: add more fields to Prospect list view --- erpnext/crm/doctype/prospect/prospect.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/prospect/prospect.json b/erpnext/crm/doctype/prospect/prospect.json index 237c4255c7..3d6fba5123 100644 --- a/erpnext/crm/doctype/prospect/prospect.json +++ b/erpnext/crm/doctype/prospect/prospect.json @@ -39,6 +39,8 @@ { "fieldname": "industry", "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, "label": "Industry", "options": "Industry Type" }, @@ -57,6 +59,8 @@ { "fieldname": "territory", "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, "label": "Territory", "options": "Territory" }, @@ -72,12 +76,14 @@ { "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" }, @@ -151,7 +157,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-08-26 12:10:49.191839", + "modified": "2021-08-27 16:24:42.961967", "modified_by": "Administrator", "module": "CRM", "name": "Prospect", From 17e0fa7a8b8c7d6471a13dc50b4556d82d6c8592 Mon Sep 17 00:00:00 2001 From: Shadrak Gurupnor <30501401+shadrak98@users.noreply.github.com> Date: Fri, 27 Aug 2021 16:42:25 +0530 Subject: [PATCH 14/44] fix: patches were breaking while migrating (#27195) * fix: patches were breaking while migrating * fix: Removed duplicate function Co-authored-by: Nabin Hait --- erpnext/patches.txt | 2 +- .../patches/v13_0/check_is_income_tax_component.py | 4 ++-- erpnext/patches/v13_0/delete_old_purchase_reports.py | 11 +++++++++++ erpnext/patches/v13_0/delete_old_sales_reports.py | 2 ++ erpnext/patches/v13_0/rename_issue_doctype_fields.py | 3 +++ erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py | 1 + 6 files changed, 20 insertions(+), 3 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0f6a606cb1..ade19ff1d5 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -214,6 +214,7 @@ erpnext.patches.v13_0.delete_old_sales_reports execute:frappe.delete_doc_if_exists("DocType", "Bank Reconciliation") erpnext.patches.v13_0.move_doctype_reports_and_notification_from_hr_to_payroll #22-06-2020 erpnext.patches.v13_0.move_payroll_setting_separately_from_hr_settings #22-06-2020 +erpnext.patches.v12_0.create_itc_reversal_custom_fields erpnext.patches.v13_0.check_is_income_tax_component #22-06-2020 erpnext.patches.v13_0.loyalty_points_entry_for_pos_invoice #22-07-2020 erpnext.patches.v12_0.add_taxjar_integration_field @@ -266,7 +267,6 @@ erpnext.patches.v13_0.rename_discharge_date_in_ip_record erpnext.patches.v12_0.create_taxable_value_field erpnext.patches.v12_0.add_gst_category_in_delivery_note erpnext.patches.v12_0.purchase_receipt_status -erpnext.patches.v12_0.create_itc_reversal_custom_fields erpnext.patches.v13_0.fix_non_unique_represents_company erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021 diff --git a/erpnext/patches/v13_0/check_is_income_tax_component.py b/erpnext/patches/v13_0/check_is_income_tax_component.py index ebae3ad715..7a52dc88d2 100644 --- a/erpnext/patches/v13_0/check_is_income_tax_component.py +++ b/erpnext/patches/v13_0/check_is_income_tax_component.py @@ -19,10 +19,10 @@ def execute(): ] for doctype in doctypes: - frappe.reload_doc('Payroll', 'doctype', doctype) + frappe.reload_doc('Payroll', 'doctype', doctype, force=True) - reports = ['Professional Tax Deductions', 'Provident Fund Deductions'] + reports = ['Professional Tax Deductions', 'Provident Fund Deductions', 'E-Invoice Summary'] for report in reports: frappe.reload_doc('Regional', 'Report', report) frappe.reload_doc('Regional', 'Report', report) diff --git a/erpnext/patches/v13_0/delete_old_purchase_reports.py b/erpnext/patches/v13_0/delete_old_purchase_reports.py index c17aad06c7..360a82e5c0 100644 --- a/erpnext/patches/v13_0/delete_old_purchase_reports.py +++ b/erpnext/patches/v13_0/delete_old_purchase_reports.py @@ -13,6 +13,7 @@ def execute(): for report in reports_to_delete: if frappe.db.exists("Report", report): delete_auto_email_reports(report) + check_linked_reports(report) frappe.delete_doc("Report", report) @@ -21,3 +22,13 @@ def delete_auto_email_reports(report): auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"]) for auto_email_report in auto_email_reports: frappe.delete_doc("Auto Email Report", auto_email_report[0]) + +def check_linked_reports(report): + """ Check if reports are referenced in Desktop Icon """ + icons = frappe.get_all("Desktop Icon", + fields = ['name'], + filters = { + "_report": report + }) + if icons: + frappe.delete_doc("Desktop Icon", icons) \ No newline at end of file diff --git a/erpnext/patches/v13_0/delete_old_sales_reports.py b/erpnext/patches/v13_0/delete_old_sales_reports.py index 671c012c8a..69493e2c00 100644 --- a/erpnext/patches/v13_0/delete_old_sales_reports.py +++ b/erpnext/patches/v13_0/delete_old_sales_reports.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe +from erpnext.patches.v13_0.delete_old_purchase_reports import check_linked_reports def execute(): reports_to_delete = ["Ordered Items To Be Delivered", "Ordered Items To Be Billed"] @@ -11,6 +12,7 @@ def execute(): for report in reports_to_delete: if frappe.db.exists("Report", report): delete_auto_email_reports(report) + check_linked_reports(report) frappe.delete_doc("Report", report) diff --git a/erpnext/patches/v13_0/rename_issue_doctype_fields.py b/erpnext/patches/v13_0/rename_issue_doctype_fields.py index 41c51c36dc..4885c0b7af 100644 --- a/erpnext/patches/v13_0/rename_issue_doctype_fields.py +++ b/erpnext/patches/v13_0/rename_issue_doctype_fields.py @@ -41,6 +41,7 @@ def execute(): rename_field('Opportunity', 'mins_to_first_response', 'first_response_time') # change fieldtype to duration + frappe.reload_doc('crm', 'doctype', 'opportunity', force=True) count = 0 for entry in opportunities: mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, 'Minutes') @@ -58,6 +59,8 @@ def execute(): def convert_to_seconds(value, unit): seconds = 0 + if value == 0: + return seconds if unit == 'Hours': seconds = value * 3600 if unit == 'Minutes': diff --git a/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py b/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py index e642547ef8..a5769d2957 100644 --- a/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py +++ b/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py @@ -8,6 +8,7 @@ def execute(): frappe.reload_doc('stock', 'doctype', 'purchase_receipt_item') frappe.reload_doc('stock', 'doctype', 'delivery_note') frappe.reload_doc('stock', 'doctype', 'delivery_note_item') + frappe.reload_doc('stock', 'doctype', 'stock_settings') def update_from_return_docs(doctype): for return_doc in frappe.get_all(doctype, filters={'is_return' : 1, 'docstatus' : 1}): From 743375748980426b1b7fe82141ce4f0de1ecc8c6 Mon Sep 17 00:00:00 2001 From: Shadrak Gurupnor <30501401+shadrak98@users.noreply.github.com> Date: Fri, 27 Aug 2021 18:00:16 +0530 Subject: [PATCH 15/44] fix: patches were breaking during migration (#27200) * fix: patches were breaking during migrating * fix: patches were breaking during migration --- erpnext/accounts/utils.py | 11 ++++++++ erpnext/patches.txt | 3 ++- .../move_item_tax_to_item_tax_template.py | 3 ++- .../v13_0/delete_old_purchase_reports.py | 13 ++-------- .../patches/v13_0/delete_old_sales_reports.py | 4 +-- .../v13_0/validate_options_for_data_field.py | 25 +++++++++++++++++++ 6 files changed, 44 insertions(+), 15 deletions(-) create mode 100644 erpnext/patches/v13_0/validate_options_for_data_field.py diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 118f628abe..5ed4a1947c 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1086,3 +1086,14 @@ def get_journal_entry(account, stock_adjustment_account, amount): db_or_cr_stock_adjustment_account : abs(amount) }] } + +def check_and_delete_linked_reports(report): + """ Check if reports are referenced in Desktop Icon """ + icons = frappe.get_all("Desktop Icon", + fields = ['name'], + filters = { + "_report": report + }) + if icons: + for icon in icons: + frappe.delete_doc("Desktop Icon", icon) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index ade19ff1d5..05c385b6ba 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -299,4 +299,5 @@ erpnext.patches.v13_0.migrate_stripe_api erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries erpnext.patches.v13_0.einvoicing_deprecation_warning erpnext.patches.v14_0.delete_einvoicing_doctypes -erpnext.patches.v13_0.set_operation_time_based_on_operating_cost \ No newline at end of file +erpnext.patches.v13_0.set_operation_time_based_on_operating_cost +erpnext.patches.v13_0.validate_options_for_data_field \ No newline at end of file diff --git a/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py b/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py index a6471eb53c..5c3fa5991c 100644 --- a/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py +++ b/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py @@ -91,8 +91,9 @@ def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parenttyp item_tax_template.title = make_autoname("Item Tax Template-.####") for tax_type, tax_rate in iteritems(item_tax_map): - account_details = frappe.db.get_value("Account", tax_type, ['name', 'account_type'], as_dict=1) + account_details = frappe.db.get_value("Account", tax_type, ['name', 'account_type', 'company'], as_dict=1) if account_details: + item_tax_template.company = account_details.company if account_details.account_type not in ('Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation'): frappe.db.set_value('Account', account_details.name, 'account_type', 'Chargeable') else: diff --git a/erpnext/patches/v13_0/delete_old_purchase_reports.py b/erpnext/patches/v13_0/delete_old_purchase_reports.py index 360a82e5c0..57620d3e98 100644 --- a/erpnext/patches/v13_0/delete_old_purchase_reports.py +++ b/erpnext/patches/v13_0/delete_old_purchase_reports.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe +from erpnext.accounts.utils import check_and_delete_linked_reports def execute(): reports_to_delete = ["Requested Items To Be Ordered", @@ -13,7 +14,7 @@ def execute(): for report in reports_to_delete: if frappe.db.exists("Report", report): delete_auto_email_reports(report) - check_linked_reports(report) + check_and_delete_linked_reports(report) frappe.delete_doc("Report", report) @@ -22,13 +23,3 @@ def delete_auto_email_reports(report): auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"]) for auto_email_report in auto_email_reports: frappe.delete_doc("Auto Email Report", auto_email_report[0]) - -def check_linked_reports(report): - """ Check if reports are referenced in Desktop Icon """ - icons = frappe.get_all("Desktop Icon", - fields = ['name'], - filters = { - "_report": report - }) - if icons: - frappe.delete_doc("Desktop Icon", icons) \ No newline at end of file diff --git a/erpnext/patches/v13_0/delete_old_sales_reports.py b/erpnext/patches/v13_0/delete_old_sales_reports.py index 69493e2c00..905a42c0c4 100644 --- a/erpnext/patches/v13_0/delete_old_sales_reports.py +++ b/erpnext/patches/v13_0/delete_old_sales_reports.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from erpnext.patches.v13_0.delete_old_purchase_reports import check_linked_reports +from erpnext.accounts.utils import check_and_delete_linked_reports def execute(): reports_to_delete = ["Ordered Items To Be Delivered", "Ordered Items To Be Billed"] @@ -12,7 +12,7 @@ def execute(): for report in reports_to_delete: if frappe.db.exists("Report", report): delete_auto_email_reports(report) - check_linked_reports(report) + check_and_delete_linked_reports(report) frappe.delete_doc("Report", report) diff --git a/erpnext/patches/v13_0/validate_options_for_data_field.py b/erpnext/patches/v13_0/validate_options_for_data_field.py new file mode 100644 index 0000000000..568d1a4b0c --- /dev/null +++ b/erpnext/patches/v13_0/validate_options_for_data_field.py @@ -0,0 +1,25 @@ +# Copyright (c) 2021, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model import data_field_options + +def execute(): + + for field in frappe.get_all('Custom Field', + fields = ['name'], + filters = { + 'fieldtype': 'Data', + 'options': ['!=', None] + }): + + if field not in data_field_options: + frappe.db.sql(""" + UPDATE + `tabCustom Field` + SET + options=NULL + WHERE + name=%s + """, (field)) From 2a3ef03388c4581550a247e2dbe99d8ca496883c Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 27 Aug 2021 18:06:51 +0530 Subject: [PATCH 16/44] fix: Popup stale build and data consistency - Include `supplier_quick_entry.js` in erpnext.bundle.js - Create primary supplier address on update - Set newly created address (quick entry) in Supplier and Customer - Clear address set in supplier and customer on delete (dependency) --- erpnext/buying/doctype/supplier/supplier.json | 3 +-- erpnext/buying/doctype/supplier/supplier.py | 14 +++++++++++-- erpnext/public/js/erpnext.bundle.js | 1 + .../public/js/utils/supplier_quick_entry.js | 3 ++- erpnext/selling/doctype/customer/customer.py | 20 +++++++++++++++---- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index 22e689c101..c7a5db5994 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -399,7 +399,6 @@ "options": "Contact" }, { - "depends_on": "mobile_no", "fetch_from": "supplier_primary_contact.mobile_no", "fieldname": "mobile_no", "fieldtype": "Read Only", @@ -439,7 +438,7 @@ "link_fieldname": "supplier" } ], - "modified": "2021-08-27 13:46:18.212802", + "modified": "2021-08-27 18:02:44.314077", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 8252b40566..207485e1bc 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -43,8 +43,11 @@ class Supplier(TransactionBase): self.naming_series = '' self.create_primary_contact() + self.create_primary_address() def validate(self): + self.flags.is_new_doc = self.is_new() + # validation for Naming Series mandatory field... if frappe.defaults.get_global_default('supp_master_name') == 'Naming Series': if not self.naming_series: @@ -90,9 +93,14 @@ class Supplier(TransactionBase): def create_primary_address(self): from erpnext.selling.doctype.customer.customer import make_address + from frappe.contacts.doctype.address.address import get_address_display if self.flags.is_new_doc and self.get('address_line1'): - make_address(self) + address = make_address(self) + address_display = get_address_display(address.name) + + self.db_set("supplier_primary_address", address.name) + self.db_set("primary_address", address_display) def on_trash(self): if self.supplier_primary_contact: @@ -100,8 +108,10 @@ class Supplier(TransactionBase): UPDATE `tabSupplier` SET supplier_primary_contact=null, + supplier_primary_address=null, mobile_no=null, - email_id=null + email_id=null, + primary_address=null WHERE name='{self.name}'""") delete_contact_and_address('Supplier', self.name) diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js index 9f7f29ad72..febdb24da3 100644 --- a/erpnext/public/js/erpnext.bundle.js +++ b/erpnext/public/js/erpnext.bundle.js @@ -15,6 +15,7 @@ import "./agriculture/ternary_plot"; import "./templates/item_quick_entry.html"; import "./utils/item_quick_entry"; import "./utils/customer_quick_entry"; +import "./utils/supplier_quick_entry"; import "./education/student_button.html"; import "./education/assessment_result_tool.html"; import "./hub/hub_factory"; diff --git a/erpnext/public/js/utils/supplier_quick_entry.js b/erpnext/public/js/utils/supplier_quick_entry.js index f650d1f101..e4a3812cf4 100644 --- a/erpnext/public/js/utils/supplier_quick_entry.js +++ b/erpnext/public/js/utils/supplier_quick_entry.js @@ -12,7 +12,8 @@ frappe.ui.form.SupplierQuickEntryForm = class SupplierQuickEntryForm extends fra } get_variant_fields() { - var variant_fields = [{ + var variant_fields = [ + { fieldtype: "Section Break", label: __("Primary Contact Details"), collapsible: 1 diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index abf146c43f..05cabcbbe2 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -150,8 +150,14 @@ class Customer(TransactionBase): self.db_set('email_id', self.email_id) def create_primary_address(self): + from frappe.contacts.doctype.address.address import get_address_display + if self.flags.is_new_doc and self.get('address_line1'): - make_address(self) + address = make_address(self) + address_display = get_address_display(address.name) + + self.db_set("customer_primary_address", address.name) + self.db_set("primary_address", address_display) def update_lead_status(self): '''If Customer created from Lead, update lead status to "Converted" @@ -246,9 +252,15 @@ class Customer(TransactionBase): def on_trash(self): if self.customer_primary_contact: - frappe.db.sql("""update `tabCustomer` - set customer_primary_contact=null, mobile_no=null, email_id=null - where name=%s""", self.name) + frappe.db.sql(f""" + UPDATE `tabCustomer` + SET + customer_primary_contact=null, + customer_primary_address=null, + mobile_no=null, + email_id=null, + primary_address=null + WHERE name='{self.name}'""") delete_contact_and_address('Customer', self.name) if self.lead_name: From 072151e16d6c0003d71c9a889ac8f00c10a7869c Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Fri, 27 Aug 2021 10:14:57 -0400 Subject: [PATCH 17/44] feat: Payment reconciliation on accounts module workspace (#25925) * feat: add payment reconciliation to accounting workspace * fix: update modified and duplicate the link Co-authored-by: Ankush Menat --- .../workspace/accounting/accounting.json | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json index b5bd14d137..2b26ac5090 100644 --- a/erpnext/accounts/workspace/accounting/accounting.json +++ b/erpnext/accounts/workspace/accounting/accounting.json @@ -233,6 +233,15 @@ "onboard": 0, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Payment Reconciliation", + "link_to": "Payment Reconciliation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "dependencies": "Sales Invoice", "hidden": 0, @@ -340,6 +349,15 @@ "onboard": 0, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Payment Reconciliation", + "link_to": "Payment Reconciliation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "dependencies": "Purchase Invoice", "hidden": 0, @@ -1188,7 +1206,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:15:52.872470", + "modified": "2021-08-27 12:15:52.872470", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", @@ -1249,4 +1267,4 @@ } ], "title": "Accounting" -} \ No newline at end of file +} From 2caa09099101a304c3152f814a9b3e9df8fd25ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Aug 2021 19:59:36 +0530 Subject: [PATCH 18/44] chore(deps): bump tar from 6.1.0 to 6.1.11 (#27203) Bumps [tar](https://github.com/npm/node-tar) from 6.1.0 to 6.1.11. - [Release notes](https://github.com/npm/node-tar/releases) - [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/npm/node-tar/compare/v6.1.0...v6.1.11) --- updated-dependencies: - dependency-name: tar dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 82e98215d1..0a49c52f0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3244,9 +3244,9 @@ tar-stream@^2.1.0: readable-stream "^3.1.1" tar@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83" - integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA== + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== dependencies: chownr "^2.0.0" fs-minipass "^2.0.0" From 327071cb9078840aa463fa3c2d80ac2ef8a6ab8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20de=20Ryckel?= Date: Sat, 28 Aug 2021 12:25:11 +0300 Subject: [PATCH 19/44] replace cur_frm with frm (#27210) --- erpnext/accounts/doctype/account/account.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/account/account.js b/erpnext/accounts/doctype/account/account.js index f7f1a5fb15..7a1d735948 100644 --- a/erpnext/accounts/doctype/account/account.js +++ b/erpnext/accounts/doctype/account/account.js @@ -74,7 +74,7 @@ frappe.ui.form.on('Account', { }); } else if (cint(frm.doc.is_group) == 0 && frappe.boot.user.can_read.indexOf("GL Entry") !== -1) { - cur_frm.add_custom_button(__('Ledger'), function () { + frm.add_custom_button(__('Ledger'), function () { frappe.route_options = { "account": frm.doc.name, "from_date": frappe.sys_defaults.year_start_date, From f5cdbf161d2b03e088ac6882553d381c39ed5ba7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sat, 28 Aug 2021 14:55:51 +0530 Subject: [PATCH 20/44] fix: LTV ratio comparison (#27207) --- .../doctype/loan_repayment/loan_repayment.py | 13 +++++++------ .../loan_security_shortfall.py | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 57aec2e5c9..b67b4c4842 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -107,12 +107,13 @@ class LoanRepayment(AccountsController): lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual': process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1) - self.append('repayment_details', { - 'loan_interest_accrual': lia.name, - 'paid_interest_amount': flt(self.total_interest_paid - self.interest_payable, precision), - 'paid_principal_amount': 0.0, - 'accrual_type': 'Repayment' - }) + if lia: + self.append('repayment_details', { + 'loan_interest_accrual': lia.name, + 'paid_interest_amount': flt(self.total_interest_paid - self.interest_payable, precision), + 'paid_principal_amount': 0.0, + 'accrual_type': 'Repayment' + }) def update_paid_amount(self): loan = frappe.get_doc("Loan", self.against_loan) diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py index cd7694b7b1..61f1778048 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -71,7 +71,7 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): - flt(loan.total_principal_paid) pledged_securities = get_pledged_security_qty(loan.name) - ltv_ratio = '' + ltv_ratio = 0.0 security_value = 0.0 for security, qty in pledged_securities.items(): From e5e00700e5415d9abc91ef5e119649a17c347d9a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 30 Aug 2021 12:07:11 +0530 Subject: [PATCH 21/44] ci: use node action instead of apt (#27220) --- .github/helper/install.sh | 7 +------ .github/workflows/patch.yml | 8 +++++++- .github/workflows/server-tests.yml | 6 ++++++ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/helper/install.sh b/.github/helper/install.sh index 455ab861f9..ac623e947d 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -4,11 +4,7 @@ set -e cd ~ || exit -sudo apt-get install redis-server - -sudo apt install nodejs - -sudo apt install npm +sudo apt-get install redis-server libcups2-dev pip install frappe-bench @@ -32,7 +28,6 @@ wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/w tar -xf /tmp/wkhtmltox.tar.xz -C /tmp sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf sudo chmod o+x /usr/local/bin/wkhtmltopdf -sudo apt-get install libcups2-dev cd ~/frappe-bench || exit diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 72d4028ce6..f65b0001fe 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -31,7 +31,13 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: 3.6 + python-version: 3.7 + + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: 14 + check-latest: true - name: Add to Hosts run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 3a1ecd399c..0a73d74d6b 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -43,6 +43,12 @@ jobs: with: python-version: 3.7 + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: 14 + check-latest: true + - name: Add to Hosts run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts From 7b78473da35fa66476d93c5212f30fd07b63336d Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 30 Aug 2021 12:50:24 +0530 Subject: [PATCH 22/44] fix: Indentation and removed f-strings - Sider: fixed indentation in js - Dont use f-strings in queries --- erpnext/buying/doctype/supplier/supplier.py | 4 +- .../public/js/utils/supplier_quick_entry.js | 115 +++++++++--------- erpnext/selling/doctype/customer/customer.py | 4 +- 3 files changed, 62 insertions(+), 61 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 207485e1bc..c9750caa65 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -104,7 +104,7 @@ class Supplier(TransactionBase): def on_trash(self): if self.supplier_primary_contact: - frappe.db.sql(f""" + frappe.db.sql(""" UPDATE `tabSupplier` SET supplier_primary_contact=null, @@ -112,7 +112,7 @@ class Supplier(TransactionBase): mobile_no=null, email_id=null, primary_address=null - WHERE name='{self.name}'""") + WHERE name=%(name)s""", {"name": self.name}) delete_contact_and_address('Supplier', self.name) diff --git a/erpnext/public/js/utils/supplier_quick_entry.js b/erpnext/public/js/utils/supplier_quick_entry.js index e4a3812cf4..8d591a9651 100644 --- a/erpnext/public/js/utils/supplier_quick_entry.js +++ b/erpnext/public/js/utils/supplier_quick_entry.js @@ -13,63 +13,64 @@ frappe.ui.form.SupplierQuickEntryForm = class SupplierQuickEntryForm extends fra get_variant_fields() { var variant_fields = [ - { - fieldtype: "Section Break", - label: __("Primary Contact Details"), - collapsible: 1 - }, - { - label: __("Email Id"), - fieldname: "email_id", - fieldtype: "Data" - }, - { - fieldtype: "Column Break" - }, - { - label: __("Mobile Number"), - fieldname: "mobile_no", - fieldtype: "Data" - }, - { - fieldtype: "Section Break", - label: __("Primary Address Details"), - collapsible: 1 - }, - { - label: __("Address Line 1"), - fieldname: "address_line1", - fieldtype: "Data" - }, - { - label: __("Address Line 2"), - fieldname: "address_line2", - fieldtype: "Data" - }, - { - label: __("ZIP Code"), - fieldname: "pincode", - fieldtype: "Data" - }, - { - fieldtype: "Column Break" - }, - { - label: __("City"), - fieldname: "city", - fieldtype: "Data" - }, - { - label: __("State"), - fieldname: "state", - fieldtype: "Data" - }, - { - label: __("Country"), - fieldname: "country", - fieldtype: "Link", - options: "Country" - }]; + { + fieldtype: "Section Break", + label: __("Primary Contact Details"), + collapsible: 1 + }, + { + label: __("Email Id"), + fieldname: "email_id", + fieldtype: "Data" + }, + { + fieldtype: "Column Break" + }, + { + label: __("Mobile Number"), + fieldname: "mobile_no", + fieldtype: "Data" + }, + { + fieldtype: "Section Break", + label: __("Primary Address Details"), + collapsible: 1 + }, + { + label: __("Address Line 1"), + fieldname: "address_line1", + fieldtype: "Data" + }, + { + label: __("Address Line 2"), + fieldname: "address_line2", + fieldtype: "Data" + }, + { + label: __("ZIP Code"), + fieldname: "pincode", + fieldtype: "Data" + }, + { + fieldtype: "Column Break" + }, + { + label: __("City"), + fieldname: "city", + fieldtype: "Data" + }, + { + label: __("State"), + fieldname: "state", + fieldtype: "Data" + }, + { + label: __("Country"), + fieldname: "country", + fieldtype: "Link", + options: "Country" + } + ]; return variant_fields; } diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 05cabcbbe2..1164f40237 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -252,7 +252,7 @@ class Customer(TransactionBase): def on_trash(self): if self.customer_primary_contact: - frappe.db.sql(f""" + frappe.db.sql(""" UPDATE `tabCustomer` SET customer_primary_contact=null, @@ -260,7 +260,7 @@ class Customer(TransactionBase): mobile_no=null, email_id=null, primary_address=null - WHERE name='{self.name}'""") + WHERE name=%(name)s""", {"name": self.name}) delete_contact_and_address('Customer', self.name) if self.lead_name: From 212eb4bc1a3e94ce02079e6b27f1a94a9362c1a1 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 Aug 2021 13:10:18 +0530 Subject: [PATCH 23/44] feat(Healthcare): Capacity for Service Unit, concurrent appointments based on capacity, Patient Appointments (#27219) * feat(Healthcare): Capacity for Service Unit, concurrent appointments based on Capacity, Patient enhancements * fix: appointment test Co-authored-by: Anoop <3326959+akurungadam@users.noreply.github.com> --- .../test_clinical_procedure.py | 4 +- .../doctype/fee_validity/test_fee_validity.py | 2 +- .../healthcare_service_unit.js | 17 +- .../healthcare_service_unit.json | 45 +++- .../healthcare_service_unit.py | 95 ++++++++- .../healthcare_service_unit_tree.js | 194 +++++++++++++++-- .../healthcare_service_unit_type.js | 4 +- .../healthcare_service_unit_type.json | 36 +++- .../inpatient_record/test_inpatient_record.py | 8 +- erpnext/healthcare/doctype/patient/patient.js | 46 ++-- .../healthcare/doctype/patient/patient.json | 118 +++++++++-- erpnext/healthcare/doctype/patient/patient.py | 126 +++++++++-- .../doctype/patient/patient_dashboard.py | 25 ++- .../patient_appointment.js | 196 +++++++++++------- .../patient_appointment.json | 4 +- .../patient_appointment.py | 76 +++++-- .../test_patient_appointment.py | 196 +++++++++++++----- .../test_patient_medical_record.py | 5 +- .../doctype/therapy_plan/test_therapy_plan.py | 11 +- .../doctype/therapy_type/test_therapy_type.py | 6 +- .../test_inpatient_medication_orders.py | 12 +- erpnext/healthcare/utils.py | 110 ++++++---- erpnext/hooks.py | 9 +- 23 files changed, 1027 insertions(+), 318 deletions(-) diff --git a/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py index 81a3982c4b..0326e5e9da 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py +++ b/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py @@ -11,7 +11,7 @@ test_dependencies = ['Item'] class TestClinicalProcedure(unittest.TestCase): def test_procedure_template_item(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() procedure_template = create_clinical_procedure_template() self.assertTrue(frappe.db.exists('Item', procedure_template.item)) @@ -20,7 +20,7 @@ class TestClinicalProcedure(unittest.TestCase): self.assertEqual(frappe.db.get_value('Item', procedure_template.item, 'disabled'), 1) def test_consumables(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() procedure_template = create_clinical_procedure_template() procedure_template.allow_stock_consumption = 1 consumable = create_consumable() diff --git a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py index 4a178723a0..957f85211d 100644 --- a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py +++ b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py @@ -27,7 +27,7 @@ class TestFeeValidity(unittest.TestCase): healthcare_settings.automate_appointment_invoicing = 1 healthcare_settings.op_consulting_charge_item = item healthcare_settings.save(ignore_permissions=True) - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() # For first appointment, invoice is generated. First appointment not considered in fee validity appointment = create_appointment(patient, practitioner, nowdate()) diff --git a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.js b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.js index 2cdd550656..2d1caf7efc 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.js +++ b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.js @@ -7,8 +7,8 @@ frappe.ui.form.on('Healthcare Service Unit', { // get query select healthcare service unit frm.fields_dict['parent_healthcare_service_unit'].get_query = function(doc) { - return{ - filters:[ + return { + filters: [ ['Healthcare Service Unit', 'is_group', '=', 1], ['Healthcare Service Unit', 'name', '!=', doc.healthcare_service_unit_name] ] @@ -21,6 +21,14 @@ frappe.ui.form.on('Healthcare Service Unit', { frm.add_custom_button(__('Healthcare Service Unit Tree'), function() { frappe.set_route('Tree', 'Healthcare Service Unit'); }); + + frm.set_query('warehouse', function() { + return { + filters: { + 'company': frm.doc.company + } + }; + }); }, set_root_readonly: function(frm) { // read-only for root healthcare service unit @@ -43,5 +51,10 @@ frappe.ui.form.on('Healthcare Service Unit', { else { frm.set_df_property('service_unit_type', 'reqd', 1); } + }, + overlap_appointments: function(frm) { + if (frm.doc.overlap_appointments == 0) { + frm.set_value('service_unit_capacity', ''); + } } }); diff --git a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json index 9ee865a62a..8935ec7d3c 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json +++ b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json @@ -16,6 +16,7 @@ "service_unit_type", "allow_appointments", "overlap_appointments", + "service_unit_capacity", "inpatient_occupancy", "occupancy_status", "column_break_9", @@ -31,6 +32,8 @@ { "fieldname": "healthcare_service_unit_name", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "in_global_search": 1, "in_list_view": 1, "label": "Service Unit", @@ -41,6 +44,8 @@ "bold": 1, "fieldname": "parent_healthcare_service_unit", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "ignore_user_permissions": 1, "in_list_view": 1, "label": "Parent Service Unit", @@ -52,6 +57,8 @@ "depends_on": "eval:doc.inpatient_occupancy != 1 && doc.allow_appointments != 1", "fieldname": "is_group", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Is Group" }, { @@ -59,6 +66,8 @@ "depends_on": "eval:doc.is_group != 1", "fieldname": "service_unit_type", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Service Unit Type", "options": "Healthcare Service Unit Type" }, @@ -68,6 +77,8 @@ "fetch_from": "service_unit_type.allow_appointments", "fieldname": "allow_appointments", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, "label": "Allow Appointments", "no_copy": 1, @@ -79,6 +90,8 @@ "fetch_from": "service_unit_type.overlap_appointments", "fieldname": "overlap_appointments", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Allow Overlap", "no_copy": 1, "read_only": 1 @@ -90,6 +103,8 @@ "fetch_from": "service_unit_type.inpatient_occupancy", "fieldname": "inpatient_occupancy", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, "label": "Inpatient Occupancy", "no_copy": 1, @@ -100,6 +115,8 @@ "depends_on": "eval:doc.inpatient_occupancy == 1", "fieldname": "occupancy_status", "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, "label": "Occupancy Status", "no_copy": 1, "options": "Vacant\nOccupied", @@ -107,13 +124,17 @@ }, { "fieldname": "column_break_9", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "bold": 1, "depends_on": "eval:doc.is_group != 1", "fieldname": "warehouse", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Warehouse", "no_copy": 1, "options": "Warehouse" @@ -121,6 +142,8 @@ { "fieldname": "company", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "ignore_user_permissions": 1, "in_list_view": 1, "in_standard_filter": 1, @@ -134,6 +157,8 @@ "fieldname": "lft", "fieldtype": "Int", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "lft", "no_copy": 1, "print_hide": 1, @@ -143,6 +168,8 @@ "fieldname": "rgt", "fieldtype": "Int", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "rgt", "no_copy": 1, "print_hide": 1, @@ -152,6 +179,8 @@ "fieldname": "old_parent", "fieldtype": "Link", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "ignore_user_permissions": 1, "label": "Old Parent", "no_copy": 1, @@ -163,14 +192,26 @@ "collapsible": 1, "fieldname": "tree_details_section", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Tree Details" + }, + { + "depends_on": "eval:doc.overlap_appointments == 1", + "fieldname": "service_unit_capacity", + "fieldtype": "Int", + "label": "Service Unit Capacity", + "mandatory_depends_on": "eval:doc.overlap_appointments == 1", + "non_negative": 1 } ], + "is_tree": 1, "links": [], - "modified": "2020-05-20 18:26:56.065543", + "modified": "2021-08-19 14:09:11.643464", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare Service Unit", + "nsm_parent_field": "parent_healthcare_service_unit", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py index 9e0417a2be..989d426789 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py +++ b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py @@ -5,14 +5,21 @@ from __future__ import unicode_literals from frappe.utils.nestedset import NestedSet +from frappe.utils import cint, cstr import frappe +from frappe import _ +import json + class HealthcareServiceUnit(NestedSet): nsm_parent_field = 'parent_healthcare_service_unit' + def validate(self): + self.set_service_unit_properties() + def autoname(self): if self.company: - suffix = " - " + frappe.get_cached_value('Company', self.company, "abbr") + suffix = " - " + frappe.get_cached_value('Company', self.company, 'abbr') if not self.healthcare_service_unit_name.endswith(suffix): self.name = self.healthcare_service_unit_name + suffix else: @@ -22,16 +29,86 @@ class HealthcareServiceUnit(NestedSet): super(HealthcareServiceUnit, self).on_update() self.validate_one_root() - def after_insert(self): + def set_service_unit_properties(self): if self.is_group: - self.allow_appointments = 0 - self.overlap_appointments = 0 - self.inpatient_occupancy = 0 - elif self.service_unit_type: + self.allow_appointments = False + self.overlap_appointments = False + self.inpatient_occupancy = False + self.service_unit_capacity = 0 + self.occupancy_status = '' + self.service_unit_type = '' + elif self.service_unit_type != '': service_unit_type = frappe.get_doc('Healthcare Service Unit Type', self.service_unit_type) self.allow_appointments = service_unit_type.allow_appointments - self.overlap_appointments = service_unit_type.overlap_appointments self.inpatient_occupancy = service_unit_type.inpatient_occupancy - if self.inpatient_occupancy: + + if self.inpatient_occupancy and self.occupancy_status != '': self.occupancy_status = 'Vacant' - self.overlap_appointments = 0 + + if service_unit_type.overlap_appointments: + self.overlap_appointments = True + else: + self.overlap_appointments = False + self.service_unit_capacity = 0 + + if self.overlap_appointments: + if not self.service_unit_capacity: + frappe.throw(_('Please set a valid Service Unit Capacity to enable Overlapping Appointments'), + title=_('Mandatory')) + + +@frappe.whitelist() +def add_multiple_service_units(parent, data): + ''' + parent - parent service unit under which the service units are to be created + data (dict) - company, healthcare_service_unit_name, count, service_unit_type, warehouse, service_unit_capacity + ''' + if not parent or not data: + return + + data = json.loads(data) + company = data.get('company') or \ + frappe.defaults.get_defaults().get('company') or \ + frappe.db.get_single_value('Global Defaults', 'default_company') + + if not data.get('healthcare_service_unit_name') or not company: + frappe.throw(_('Service Unit Name and Company are mandatory to create Healthcare Service Units'), + title=_('Missing Required Fields')) + + count = cint(data.get('count') or 0) + if count <= 0: + frappe.throw(_('Number of Service Units to be created should at least be 1'), + title=_('Invalid Number of Service Units')) + + capacity = cint(data.get('service_unit_capacity') or 1) + + service_unit = { + 'doctype': 'Healthcare Service Unit', + 'parent_healthcare_service_unit': parent, + 'service_unit_type': data.get('service_unit_type') or None, + 'service_unit_capacity': capacity if capacity > 0 else 1, + 'warehouse': data.get('warehouse') or None, + 'company': company + } + + service_unit_name = '{}'.format(data.get('healthcare_service_unit_name').strip(' -')) + + last_suffix = frappe.db.sql("""SELECT + IFNULL(MAX(CAST(SUBSTRING(name FROM %(start)s FOR 4) AS UNSIGNED)), 0) + FROM `tabHealthcare Service Unit` + WHERE name like %(prefix)s AND company=%(company)s""", + {'start': len(service_unit_name)+2, 'prefix': '{}-%'.format(service_unit_name), 'company': company}, + as_list=1)[0][0] + start_suffix = cint(last_suffix) + 1 + + failed_list = [] + for i in range(start_suffix, count + start_suffix): + # name to be in the form WARD-#### + service_unit['healthcare_service_unit_name'] = '{}-{}'.format(service_unit_name, cstr('%0*d' % (4, i))) + service_unit_doc = frappe.get_doc(service_unit) + try: + service_unit_doc.insert() + except Exception: + failed_list.append(service_unit['healthcare_service_unit_name']) + + return failed_list diff --git a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit_tree.js b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit_tree.js index b75f271827..ea3fea6b7a 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit_tree.js +++ b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit_tree.js @@ -1,35 +1,185 @@ -frappe.treeview_settings["Healthcare Service Unit"] = { - breadcrumbs: "Healthcare Service Unit", - title: __("Healthcare Service Unit"), +frappe.provide("frappe.treeview_settings"); + +frappe.treeview_settings['Healthcare Service Unit'] = { + breadcrumbs: 'Healthcare Service Unit', + title: __('Service Unit Tree'), get_tree_root: false, - filters: [{ - fieldname: "company", - fieldtype: "Select", - options: erpnext.utils.get_tree_options("company"), - label: __("Company"), - default: erpnext.utils.get_tree_default("company") - }], get_tree_nodes: 'erpnext.healthcare.utils.get_children', - ignore_fields:["parent_healthcare_service_unit"], - onrender: function(node) { - if (node.data.occupied_out_of_vacant!==undefined) { - $('' - + " " + node.data.occupied_out_of_vacant + filters: [{ + fieldname: 'company', + fieldtype: 'Select', + options: erpnext.utils.get_tree_options('company'), + label: __('Company'), + default: erpnext.utils.get_tree_default('company') + }], + fields: [ + { + fieldtype: 'Data', fieldname: 'healthcare_service_unit_name', label: __('New Service Unit Name'), + reqd: true + }, + { + fieldtype: 'Check', fieldname: 'is_group', label: __('Is Group'), + description: __("Child nodes can be only created under 'Group' type nodes") + }, + { + fieldtype: 'Link', fieldname: 'service_unit_type', label: __('Service Unit Type'), + options: 'Healthcare Service Unit Type', description: __('Type of the new Service Unit'), + depends_on: 'eval:!doc.is_group', default: '', + onchange: () => { + if (cur_dialog) { + if (cur_dialog.fields_dict.service_unit_type.value) { + frappe.db.get_value('Healthcare Service Unit Type', + cur_dialog.fields_dict.service_unit_type.value, 'overlap_appointments') + .then(r => { + if (r.message.overlap_appointments) { + cur_dialog.set_df_property('service_unit_capacity', 'hidden', false); + cur_dialog.set_df_property('service_unit_capacity', 'reqd', true); + } else { + cur_dialog.set_df_property('service_unit_capacity', 'hidden', true); + cur_dialog.set_df_property('service_unit_capacity', 'reqd', false); + } + }); + } else { + cur_dialog.set_df_property('service_unit_capacity', 'hidden', true); + cur_dialog.set_df_property('service_unit_capacity', 'reqd', false); + } + } + } + }, + { + fieldtype: 'Int', fieldname: 'service_unit_capacity', label: __('Service Unit Capacity'), + description: __('Sets the number of concurrent appointments allowed'), reqd: false, + depends_on: "eval:!doc.is_group && doc.service_unit_type != ''", hidden: true + }, + { + fieldtype: 'Link', fieldname: 'warehouse', label: __('Warehouse'), options: 'Warehouse', + description: __('Optional, if you want to manage stock separately for this Service Unit'), + depends_on: 'eval:!doc.is_group' + }, + { + fieldtype: 'Link', fieldname: 'company', label: __('Company'), options: 'Company', reqd: true, + default: () => { + return cur_page.page.page.fields_dict.company.value; + } + } + ], + ignore_fields: ['parent_healthcare_service_unit'], + onrender: function (node) { + if (node.data.occupied_of_available !== undefined) { + $("" + + ' ' + node.data.occupied_of_available + '').insertBefore(node.$ul); } - if (node.data && node.data.inpatient_occupancy!==undefined) { + if (node.data && node.data.inpatient_occupancy !== undefined) { if (node.data.inpatient_occupancy == 1) { - if (node.data.occupancy_status == "Occupied") { - $('' - + " " + node.data.occupancy_status + if (node.data.occupancy_status == 'Occupied') { + $("" + + ' ' + node.data.occupancy_status + '').insertBefore(node.$ul); } - if (node.data.occupancy_status == "Vacant") { - $('' - + " " + node.data.occupancy_status + if (node.data.occupancy_status == 'Vacant') { + $("" + + ' ' + node.data.occupancy_status + '').insertBefore(node.$ul); } } } }, + post_render: function (treeview) { + frappe.treeview_settings['Healthcare Service Unit'].treeview = {}; + $.extend(frappe.treeview_settings['Healthcare Service Unit'].treeview, treeview); + }, + toolbar: [ + { + label: __('Add Multiple'), + condition: function (node) { + return node.expandable; + }, + click: function (node) { + const dialog = new frappe.ui.Dialog({ + title: __('Add Multiple Service Units'), + fields: [ + { + fieldtype: 'Data', fieldname: 'healthcare_service_unit_name', label: __('Service Unit Name'), + reqd: true, description: __("Will be serially suffixed to maintain uniquness. Example: 'Ward' will be named as 'Ward-####'"), + }, + { + fieldtype: 'Int', fieldname: 'count', label: __('Number of Service Units'), + reqd: true + }, + { + fieldtype: 'Link', fieldname: 'service_unit_type', label: __('Service Unit Type'), + options: 'Healthcare Service Unit Type', description: __('Type of the new Service Unit'), + depends_on: 'eval:!doc.is_group', default: '', reqd: true, + onchange: () => { + if (cur_dialog) { + if (cur_dialog.fields_dict.service_unit_type.value) { + frappe.db.get_value('Healthcare Service Unit Type', + cur_dialog.fields_dict.service_unit_type.value, 'overlap_appointments') + .then(r => { + if (r.message.overlap_appointments) { + cur_dialog.set_df_property('service_unit_capacity', 'hidden', false); + cur_dialog.set_df_property('service_unit_capacity', 'reqd', true); + } else { + cur_dialog.set_df_property('service_unit_capacity', 'hidden', true); + cur_dialog.set_df_property('service_unit_capacity', 'reqd', false); + } + }); + } else { + cur_dialog.set_df_property('service_unit_capacity', 'hidden', true); + cur_dialog.set_df_property('service_unit_capacity', 'reqd', false); + } + } + } + }, + { + fieldtype: 'Int', fieldname: 'service_unit_capacity', label: __('Service Unit Capacity'), + description: __('Sets the number of concurrent appointments allowed'), reqd: false, + depends_on: "eval:!doc.is_group && doc.service_unit_type != ''", hidden: true + }, + { + fieldtype: 'Link', fieldname: 'warehouse', label: __('Warehouse'), options: 'Warehouse', + description: __('Optional, if you want to manage stock separately for this Service Unit'), + }, + { + fieldtype: 'Link', fieldname: 'company', label: __('Company'), options: 'Company', reqd: true, + default: () => { + return cur_page.page.page.fields_dict.company.get_value(); + } + } + ], + primary_action: () => { + dialog.hide(); + let vals = dialog.get_values(); + if (!vals) return; + + return frappe.call({ + method: 'erpnext.healthcare.doctype.healthcare_service_unit.healthcare_service_unit.add_multiple_service_units', + args: { + parent: node.data.value, + data: vals + }, + callback: function (r) { + if (!r.exc && r.message) { + frappe.treeview_settings['Healthcare Service Unit'].treeview.tree.load_children(node, true); + + frappe.show_alert({ + message: __('{0} Service Units created', [vals.count - r.message.length]), + indicator: 'green' + }); + } else { + frappe.msgprint(__('Could not create Service Units')); + } + }, + freeze: true, + freeze_message: __('Creating {0} Service Units', [vals.count]) + }); + }, + primary_action_label: __('Create') + }); + dialog.show(); + } + } + ], + extend_toolbar: true }; diff --git a/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.js b/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.js index eb33ab68c0..ecf4aa1a4b 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.js +++ b/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.js @@ -68,8 +68,8 @@ let change_item_code = function(frm, doc) { if (values) { frappe.call({ "method": "erpnext.healthcare.doctype.healthcare_service_unit_type.healthcare_service_unit_type.change_item_code", - "args": {item: doc.item, item_code: values['item_code'], doc_name: doc.name}, - callback: function () { + "args": { item: doc.item, item_code: values['item_code'], doc_name: doc.name }, + callback: function() { frm.reload_doc(); } }); diff --git a/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.json b/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.json index 4b8503d028..9c81c65f6b 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.json +++ b/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.json @@ -29,6 +29,8 @@ { "fieldname": "service_unit_type", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, "label": "Service Unit Type", "no_copy": 1, @@ -41,6 +43,8 @@ "depends_on": "eval:doc.inpatient_occupancy != 1", "fieldname": "allow_appointments", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Allow Appointments" }, { @@ -49,6 +53,8 @@ "depends_on": "eval:doc.allow_appointments == 1 && doc.inpatient_occupany != 1", "fieldname": "overlap_appointments", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Allow Overlap" }, { @@ -57,6 +63,8 @@ "depends_on": "eval:doc.allow_appointments != 1", "fieldname": "inpatient_occupancy", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Inpatient Occupancy" }, { @@ -65,17 +73,23 @@ "depends_on": "eval:doc.inpatient_occupancy == 1 && doc.allow_appointments != 1", "fieldname": "is_billable", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Is Billable" }, { "depends_on": "is_billable", "fieldname": "item_details", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Item Details" }, { "fieldname": "item", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Item", "no_copy": 1, "options": "Item", @@ -84,6 +98,8 @@ { "fieldname": "item_code", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "label": "Item Code", "mandatory_depends_on": "eval: doc.is_billable == 1", "no_copy": 1 @@ -91,6 +107,8 @@ { "fieldname": "item_group", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Item Group", "mandatory_depends_on": "eval: doc.is_billable == 1", "options": "Item Group" @@ -98,6 +116,8 @@ { "fieldname": "uom", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "UOM", "mandatory_depends_on": "eval: doc.is_billable == 1", "options": "UOM" @@ -105,28 +125,38 @@ { "fieldname": "no_of_hours", "fieldtype": "Int", + "hide_days": 1, + "hide_seconds": 1, "label": "UOM Conversion in Hours", "mandatory_depends_on": "eval: doc.is_billable == 1" }, { "fieldname": "column_break_11", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "rate", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Rate / UOM" }, { "default": "0", "fieldname": "disabled", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Disabled", "no_copy": 1 }, { "fieldname": "description", "fieldtype": "Small Text", + "hide_days": 1, + "hide_seconds": 1, "label": "Description" }, { @@ -134,11 +164,13 @@ "fieldname": "change_in_item", "fieldtype": "Check", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "Change in Item" } ], "links": [], - "modified": "2020-05-20 15:31:09.627516", + "modified": "2021-08-19 17:52:30.266667", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare Service Unit Type", diff --git a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py index a8c7720a0a..b4a961264f 100644 --- a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py @@ -151,7 +151,7 @@ def get_healthcare_service_unit(unit_name=None): if not service_unit: service_unit = frappe.new_doc("Healthcare Service Unit") - service_unit.healthcare_service_unit_name = unit_name or "Test Service Unit Ip Occupancy" + service_unit.healthcare_service_unit_name = unit_name or "_Test Service Unit Ip Occupancy" service_unit.company = "_Test Company" service_unit.service_unit_type = get_service_unit_type() service_unit.inpatient_occupancy = 1 @@ -159,12 +159,12 @@ def get_healthcare_service_unit(unit_name=None): service_unit.is_group = 0 service_unit_parent_name = frappe.db.exists({ "doctype": "Healthcare Service Unit", - "healthcare_service_unit_name": "All Healthcare Service Units", + "healthcare_service_unit_name": "_Test All Healthcare Service Units", "is_group": 1 }) if not service_unit_parent_name: parent_service_unit = frappe.new_doc("Healthcare Service Unit") - parent_service_unit.healthcare_service_unit_name = "All Healthcare Service Units" + parent_service_unit.healthcare_service_unit_name = "_Test All Healthcare Service Units" parent_service_unit.is_group = 1 parent_service_unit.save(ignore_permissions = True) service_unit.parent_healthcare_service_unit = parent_service_unit.name @@ -180,7 +180,7 @@ def get_service_unit_type(): if not service_unit_type: service_unit_type = frappe.new_doc("Healthcare Service Unit Type") - service_unit_type.service_unit_type = "Test Service Unit Type Ip Occupancy" + service_unit_type.service_unit_type = "_Test Service Unit Type Ip Occupancy" service_unit_type.inpatient_occupancy = 1 service_unit_type.save(ignore_permissions = True) return service_unit_type.name diff --git a/erpnext/healthcare/doctype/patient/patient.js b/erpnext/healthcare/doctype/patient/patient.js index bce42e51d0..9266467155 100644 --- a/erpnext/healthcare/doctype/patient/patient.js +++ b/erpnext/healthcare/doctype/patient/patient.js @@ -26,31 +26,39 @@ frappe.ui.form.on('Patient', { } if (frm.doc.patient_name && frappe.user.has_role('Physician')) { + frm.add_custom_button(__('Patient Progress'), function() { + frappe.route_options = {'patient': frm.doc.name}; + frappe.set_route('patient-progress'); + }, __('View')); + frm.add_custom_button(__('Patient History'), function() { frappe.route_options = {'patient': frm.doc.name}; frappe.set_route('patient_history'); - },'View'); + }, __('View')); } - if (!frm.doc.__islocal && (frappe.user.has_role('Nursing User') || frappe.user.has_role('Physician'))) { - frm.add_custom_button(__('Vital Signs'), function () { - create_vital_signs(frm); - }, 'Create'); - frm.add_custom_button(__('Medical Record'), function () { - create_medical_record(frm); - }, 'Create'); - frm.add_custom_button(__('Patient Encounter'), function () { - create_encounter(frm); - }, 'Create'); - frm.toggle_enable(['customer'], 0); // ToDo, allow change only if no transactions booked or better, add merge option + frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Patient'}; + frm.toggle_display(['address_html', 'contact_html'], !frm.is_new()); + + if (!frm.is_new()) { + if ((frappe.user.has_role('Nursing User') || frappe.user.has_role('Physician'))) { + frm.add_custom_button(__('Medical Record'), function () { + create_medical_record(frm); + }, 'Create'); + frm.toggle_enable(['customer'], 0); + } + frappe.contacts.render_address_and_contact(frm); + erpnext.utils.set_party_dashboard_indicators(frm); + } else { + frappe.contacts.clear_address_and_contact(frm); } }, + onload: function (frm) { - if (!frm.doc.dob) { - $(frm.fields_dict['age_html'].wrapper).html(''); - } if (frm.doc.dob) { $(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${get_age(frm.doc.dob)}`); + } else { + $(frm.fields_dict['age_html'].wrapper).html(''); } } }); @@ -59,16 +67,14 @@ frappe.ui.form.on('Patient', 'dob', function(frm) { if (frm.doc.dob) { let today = new Date(); let birthDate = new Date(frm.doc.dob); - if (today < birthDate){ + if (today < birthDate) { frappe.msgprint(__('Please select a valid Date')); frappe.model.set_value(frm.doctype,frm.docname, 'dob', ''); - } - else { + } else { let age_str = get_age(frm.doc.dob); $(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${age_str}`); } - } - else { + } else { $(frm.fields_dict['age_html'].wrapper).html(''); } }); diff --git a/erpnext/healthcare/doctype/patient/patient.json b/erpnext/healthcare/doctype/patient/patient.json index 8af1a9ccd7..4092a6a768 100644 --- a/erpnext/healthcare/doctype/patient/patient.json +++ b/erpnext/healthcare/doctype/patient/patient.json @@ -1,6 +1,6 @@ { "actions": [], - "allow_copy": 1, + "allow_events_in_timeline": 1, "allow_import": 1, "allow_rename": 1, "autoname": "naming_series:", @@ -24,12 +24,19 @@ "image", "column_break_14", "status", + "uid", "inpatient_record", "inpatient_status", "report_preference", "mobile", - "email", "phone", + "email", + "invite_user", + "user_id", + "address_contacts", + "address_html", + "column_break_22", + "contact_html", "customer_details_section", "customer", "customer_group", @@ -74,6 +81,7 @@ "fieldtype": "Select", "in_preview": 1, "label": "Inpatient Status", + "no_copy": 1, "options": "\nAdmission Scheduled\nAdmitted\nDischarge Scheduled", "read_only": 1 }, @@ -81,6 +89,7 @@ "fieldname": "inpatient_record", "fieldtype": "Link", "label": "Inpatient Record", + "no_copy": 1, "options": "Inpatient Record", "read_only": 1 }, @@ -101,6 +110,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Full Name", + "no_copy": 1, "read_only": 1, "search_index": 1 }, @@ -118,6 +128,7 @@ "fieldtype": "Select", "in_preview": 1, "label": "Blood Group", + "no_copy": 1, "options": "\nA Positive\nA Negative\nAB Positive\nAB Negative\nB Positive\nB Negative\nO Positive\nO Negative" }, { @@ -125,7 +136,8 @@ "fieldname": "dob", "fieldtype": "Date", "in_preview": 1, - "label": "Date of birth" + "label": "Date of birth", + "no_copy": 1 }, { "fieldname": "age_html", @@ -167,6 +179,7 @@ "fieldtype": "Link", "ignore_user_permissions": 1, "label": "Customer", + "no_copy": 1, "options": "Customer", "set_only_once": 1 }, @@ -183,6 +196,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Mobile", + "no_copy": 1, "options": "Phone" }, { @@ -192,6 +206,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Email", + "no_copy": 1, "options": "Email" }, { @@ -199,6 +214,7 @@ "fieldtype": "Data", "in_filter": 1, "label": "Phone", + "no_copy": 1, "options": "Phone" }, { @@ -230,7 +246,8 @@ "fieldname": "medication", "fieldtype": "Small Text", "ignore_xss_filter": 1, - "label": "Medication" + "label": "Medication", + "no_copy": 1 }, { "fieldname": "column_break_20", @@ -240,13 +257,15 @@ "fieldname": "medical_history", "fieldtype": "Small Text", "ignore_xss_filter": 1, - "label": "Medical History" + "label": "Medical History", + "no_copy": 1 }, { "fieldname": "surgical_history", "fieldtype": "Small Text", "ignore_xss_filter": 1, - "label": "Surgical History" + "label": "Surgical History", + "no_copy": 1 }, { "collapsible": 1, @@ -258,8 +277,8 @@ "fieldname": "occupation", "fieldtype": "Data", "ignore_xss_filter": 1, - "in_standard_filter": 1, - "label": "Occupation" + "label": "Occupation", + "no_copy": 1 }, { "fieldname": "column_break_25", @@ -269,6 +288,7 @@ "fieldname": "marital_status", "fieldtype": "Select", "label": "Marital Status", + "no_copy": 1, "options": "\nSingle\nMarried\nDivorced\nWidow" }, { @@ -281,25 +301,29 @@ "fieldname": "tobacco_past_use", "fieldtype": "Data", "ignore_xss_filter": 1, - "label": "Tobacco Consumption (Past)" + "label": "Tobacco Consumption (Past)", + "no_copy": 1 }, { "fieldname": "tobacco_current_use", "fieldtype": "Data", "ignore_xss_filter": 1, - "label": "Tobacco Consumption (Present)" + "label": "Tobacco Consumption (Present)", + "no_copy": 1 }, { "fieldname": "alcohol_past_use", "fieldtype": "Data", "ignore_xss_filter": 1, - "label": "Alcohol Consumption (Past)" + "label": "Alcohol Consumption (Past)", + "no_copy": 1 }, { "fieldname": "alcohol_current_use", "fieldtype": "Data", "ignore_user_permissions": 1, - "label": "Alcohol Consumption (Present)" + "label": "Alcohol Consumption (Present)", + "no_copy": 1 }, { "fieldname": "column_break_32", @@ -309,13 +333,15 @@ "fieldname": "surrounding_factors", "fieldtype": "Small Text", "ignore_xss_filter": 1, - "label": "Occupational Hazards and Environmental Factors" + "label": "Occupational Hazards and Environmental Factors", + "no_copy": 1 }, { "fieldname": "other_risk_factors", "fieldtype": "Small Text", "ignore_xss_filter": 1, - "label": "Other Risk Factors" + "label": "Other Risk Factors", + "no_copy": 1 }, { "collapsible": 1, @@ -331,7 +357,8 @@ "fieldname": "patient_details", "fieldtype": "Text", "ignore_xss_filter": 1, - "label": "Patient Details" + "label": "Patient Details", + "no_copy": 1 }, { "fieldname": "default_currency", @@ -342,19 +369,22 @@ { "fieldname": "last_name", "fieldtype": "Data", - "label": "Last Name" + "label": "Last Name", + "no_copy": 1 }, { "fieldname": "first_name", "fieldtype": "Data", "label": "First Name", + "no_copy": 1, "oldfieldtype": "Data", "reqd": 1 }, { "fieldname": "middle_name", "fieldtype": "Data", - "label": "Middle Name (optional)" + "label": "Middle Name (optional)", + "no_copy": 1 }, { "collapsible": 1, @@ -389,13 +419,63 @@ "fieldtype": "Link", "label": "Print Language", "options": "Language" + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "address_contacts", + "fieldtype": "Section Break", + "label": "Address and Contact", + "options": "fa fa-map-marker" + }, + { + "fieldname": "address_html", + "fieldtype": "HTML", + "label": "Address HTML", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_22", + "fieldtype": "Column Break" + }, + { + "fieldname": "contact_html", + "fieldtype": "HTML", + "label": "Contact HTML", + "no_copy": 1, + "read_only": 1 + }, + { + "allow_in_quick_entry": 1, + "default": "1", + "fieldname": "invite_user", + "fieldtype": "Check", + "label": "Invite as User", + "no_copy": 1, + "read_only_depends_on": "eval: doc.user_id" + }, + { + "fieldname": "user_id", + "fieldtype": "Read Only", + "label": "User ID", + "no_copy": 1, + "options": "User" + }, + { + "allow_in_quick_entry": 1, + "bold": 1, + "fieldname": "uid", + "fieldtype": "Data", + "in_standard_filter": 1, + "label": "Identification Number (UID)", + "unique": 1 } ], "icon": "fa fa-user", "image_field": "image", "links": [], "max_attachments": 50, - "modified": "2020-04-25 17:24:32.146415", + "modified": "2021-03-14 13:21:09.759906", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient", @@ -453,7 +533,7 @@ ], "quick_entry": 1, "restrict_to_domain": "Healthcare", - "search_fields": "patient_name,mobile,email,phone", + "search_fields": "patient_name,mobile,email,phone,uid", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "ASC", diff --git a/erpnext/healthcare/doctype/patient/patient.py b/erpnext/healthcare/doctype/patient/patient.py index f77ad70633..9dae1f68b0 100644 --- a/erpnext/healthcare/doctype/patient/patient.py +++ b/erpnext/healthcare/doctype/patient/patient.py @@ -8,24 +8,27 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import cint, cstr, getdate import dateutil +from frappe.contacts.address_and_contact import load_address_and_contact +from frappe.contacts.doctype.contact.contact import get_default_contact from frappe.model.naming import set_name_by_naming_series from frappe.utils.nestedset import get_root_of from erpnext import get_default_currency from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account, send_registration_sms +from erpnext.accounts.party import get_dashboard_info class Patient(Document): + def onload(self): + '''Load address and contacts in `__onload`''' + load_address_and_contact(self) + self.load_dashboard_info() + def validate(self): self.set_full_name() - self.add_as_website_user() def before_insert(self): self.set_missing_customer_details() def after_insert(self): - self.add_as_website_user() - self.reload() - if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient') and not self.customer: - create_customer(self) if frappe.db.get_single_value('Healthcare Settings', 'collect_registration_fee'): frappe.db.set_value('Patient', self.name, 'status', 'Disabled') else: @@ -49,6 +52,16 @@ class Patient(Document): else: create_customer(self) + self.set_contact() # add or update contact + + if not self.user_id and self.email and self.invite_user: + self.create_website_user() + + def load_dashboard_info(self): + if self.customer: + info = get_dashboard_info('Customer', self.customer, None) + self.set_onload('dashboard_info', info) + def set_full_name(self): if self.last_name: self.patient_name = ' '.join(filter(None, [self.first_name, self.last_name])) @@ -71,18 +84,24 @@ class Patient(Document): if not self.language: self.language = frappe.db.get_single_value('System Settings', 'language') - def add_as_website_user(self): - if self.email: - if not frappe.db.exists ('User', self.email): - user = frappe.get_doc({ - 'doctype': 'User', - 'first_name': self.first_name, - 'last_name': self.last_name, - 'email': self.email, - 'user_type': 'Website User' - }) - user.flags.ignore_permissions = True - user.add_roles('Patient') + def create_website_user(self): + if self.email and not frappe.db.exists('User', self.email): + user = frappe.get_doc({ + 'doctype': 'User', + 'first_name': self.first_name, + 'last_name': self.last_name, + 'email': self.email, + 'user_type': 'Website User', + 'gender': self.sex, + 'phone': self.phone, + 'mobile_no': self.mobile, + 'birth_date': self.dob + }) + user.flags.ignore_permissions = True + user.enabled = True + user.send_welcome_email = True + user.add_roles('Patient') + frappe.db.set_value(self.doctype, self.name, 'user_id', user.name) def autoname(self): patient_name_by = frappe.db.get_single_value('Healthcare Settings', 'patient_name_by') @@ -114,7 +133,7 @@ class Patient(Document): age = self.age if not age: return - age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)") + age_str = f'{str(age.years)} {_("Years(s)")} {str(age.months)} {_("Month(s)")} {str(age.days)} {_("Day(s)")}' return age_str @frappe.whitelist() @@ -131,6 +150,58 @@ class Patient(Document): return {'invoice': sales_invoice.name} + def set_contact(self): + if frappe.db.exists('Dynamic Link', {'parenttype':'Contact', 'link_doctype':'Patient', 'link_name':self.name}): + old_doc = self.get_doc_before_save() + if old_doc.email != self.email or old_doc.mobile != self.mobile or old_doc.phone != self.phone: + self.update_contact() + else: + self.reload() + if self.email or self.mobile or self.phone: + contact = frappe.get_doc({ + 'doctype': 'Contact', + 'first_name': self.first_name, + 'middle_name': self.middle_name, + 'last_name': self.last_name, + 'gender': self.sex, + 'is_primary_contact': 1 + }) + contact.append('links', dict(link_doctype='Patient', link_name=self.name)) + if self.customer: + contact.append('links', dict(link_doctype='Customer', link_name=self.customer)) + + contact.insert(ignore_permissions=True) + self.update_contact(contact) # update email, mobile and phone + + def update_contact(self, contact=None): + if not contact: + contact_name = get_default_contact(self.doctype, self.name) + if contact_name: + contact = frappe.get_doc('Contact', contact_name) + + if contact: + if self.email and self.email != contact.email_id: + for email in contact.email_ids: + email.is_primary = True if email.email_id == self.email else False + contact.add_email(self.email, is_primary=True) + contact.set_primary_email() + + if self.mobile and self.mobile != contact.mobile_no: + for mobile in contact.phone_nos: + mobile.is_primary_mobile_no = True if mobile.phone == self.mobile else False + contact.add_phone(self.mobile, is_primary_mobile_no=True) + contact.set_primary('mobile_no') + + if self.phone and self.phone != contact.phone: + for phone in contact.phone_nos: + phone.is_primary_phone = True if phone.phone == self.phone else False + contact.add_phone(self.phone, is_primary_phone=True) + contact.set_primary('phone') + + contact.flags.ignore_validate = True # disable hook TODO: safe? + contact.save(ignore_permissions=True) + + def create_customer(doc): customer = frappe.get_doc({ 'doctype': 'Customer', @@ -156,8 +227,8 @@ def make_invoice(patient, company): sales_invoice.debit_to = get_receivable_account(company) item_line = sales_invoice.append('items') - item_line.item_name = 'Registeration Fee' - item_line.description = 'Registeration Fee' + item_line.item_name = 'Registration Fee' + item_line.description = 'Registration Fee' item_line.qty = 1 item_line.uom = uom item_line.conversion_factor = 1 @@ -181,8 +252,11 @@ def get_patient_detail(patient): return details def get_timeline_data(doctype, name): - """Return timeline data from medical records""" - return dict(frappe.db.sql(''' + ''' + Return Patient's timeline data from medical records + Also include the associated Customer timeline data + ''' + patient_timeline_data = dict(frappe.db.sql(''' SELECT unix_timestamp(communication_date), count(*) FROM @@ -191,3 +265,11 @@ def get_timeline_data(doctype, name): patient=%s and `communication_date` > date_sub(curdate(), interval 1 year) GROUP BY communication_date''', name)) + + customer = frappe.db.get_value(doctype, name, 'customer') + if customer: + from erpnext.accounts.party import get_timeline_data + customer_timeline_data = get_timeline_data('Customer', customer) + patient_timeline_data.update(customer_timeline_data) + + return patient_timeline_data diff --git a/erpnext/healthcare/doctype/patient/patient_dashboard.py b/erpnext/healthcare/doctype/patient/patient_dashboard.py index 39603f77a0..7f7cfa8e5b 100644 --- a/erpnext/healthcare/doctype/patient/patient_dashboard.py +++ b/erpnext/healthcare/doctype/patient/patient_dashboard.py @@ -6,22 +6,33 @@ def get_data(): 'heatmap': True, 'heatmap_message': _('This is based on transactions against this Patient. See timeline below for details'), 'fieldname': 'patient', + 'non_standard_fieldnames': { + 'Payment Entry': 'party' + }, 'transactions': [ { - 'label': _('Appointments and Patient Encounters'), - 'items': ['Patient Appointment', 'Patient Encounter'] + 'label': _('Appointments and Encounters'), + 'items': ['Patient Appointment', 'Vital Signs', 'Patient Encounter'] }, { 'label': _('Lab Tests and Vital Signs'), - 'items': ['Lab Test', 'Sample Collection', 'Vital Signs'] + 'items': ['Lab Test', 'Sample Collection'] }, { - 'label': _('Billing'), - 'items': ['Sales Invoice'] + 'label': _('Rehab and Physiotherapy'), + 'items': ['Patient Assessment', 'Therapy Session', 'Therapy Plan'] }, { - 'label': _('Orders'), - 'items': ['Inpatient Medication Order'] + 'label': _('Surgery'), + 'items': ['Clinical Procedure'] + }, + { + 'label': _('Admissions'), + 'items': ['Inpatient Record', 'Inpatient Medication Order'] + }, + { + 'label': _('Billing and Payments'), + 'items': ['Sales Invoice', 'Payment Entry'] } ] } diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js index c6e489ec17..49847d5bc8 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js @@ -17,9 +17,9 @@ frappe.ui.form.on('Patient Appointment', { }, refresh: function(frm) { - frm.set_query('patient', function () { + frm.set_query('patient', function() { return { - filters: {'status': 'Active'} + filters: { 'status': 'Active' } }; }); @@ -64,7 +64,7 @@ frappe.ui.form.on('Patient Appointment', { } else { frappe.call({ method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.check_payment_fields_reqd', - args: {'patient': frm.doc.patient}, + args: { 'patient': frm.doc.patient }, callback: function(data) { if (data.message == true) { if (frm.doc.mode_of_payment && frm.doc.paid_amount) { @@ -97,7 +97,7 @@ frappe.ui.form.on('Patient Appointment', { if (frm.doc.patient) { frm.add_custom_button(__('Patient History'), function() { - frappe.route_options = {'patient': frm.doc.patient}; + frappe.route_options = { 'patient': frm.doc.patient }; frappe.set_route('patient_history'); }, __('View')); } @@ -111,14 +111,14 @@ frappe.ui.form.on('Patient Appointment', { }); if (frm.doc.procedure_template) { - frm.add_custom_button(__('Clinical Procedure'), function(){ + frm.add_custom_button(__('Clinical Procedure'), function() { frappe.model.open_mapped_doc({ method: 'erpnext.healthcare.doctype.clinical_procedure.clinical_procedure.make_procedure', frm: frm, }); }, __('Create')); } else if (frm.doc.therapy_type) { - frm.add_custom_button(__('Therapy Session'),function(){ + frm.add_custom_button(__('Therapy Session'), function() { frappe.model.open_mapped_doc({ method: 'erpnext.healthcare.doctype.therapy_session.therapy_session.create_therapy_session', frm: frm, @@ -148,7 +148,7 @@ frappe.ui.form.on('Patient Appointment', { doctype: 'Patient', name: frm.doc.patient }, - callback: function (data) { + callback: function(data) { let age = null; if (data.message.dob) { age = calculate_age(data.message.dob); @@ -165,7 +165,7 @@ frappe.ui.form.on('Patient Appointment', { }, practitioner: function(frm) { - if (frm.doc.practitioner ) { + if (frm.doc.practitioner) { frm.events.set_payment_details(frm); } }, @@ -230,7 +230,7 @@ frappe.ui.form.on('Patient Appointment', { toggle_payment_fields: function(frm) { frappe.call({ method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.check_payment_fields_reqd', - args: {'patient': frm.doc.patient}, + args: { 'patient': frm.doc.patient }, callback: function(data) { if (data.message.fee_validity) { // if fee validity exists and automated appointment invoicing is enabled, @@ -254,7 +254,7 @@ frappe.ui.form.on('Patient Appointment', { frm.toggle_display('paid_amount', data.message ? 1 : 0); frm.toggle_display('billing_item', data.message ? 1 : 0); frm.toggle_reqd('mode_of_payment', data.message ? 1 : 0); - frm.toggle_reqd('paid_amount', data.message ? 1 :0); + frm.toggle_reqd('paid_amount', data.message ? 1 : 0); frm.toggle_reqd('billing_item', data.message ? 1 : 0); } } @@ -265,7 +265,7 @@ frappe.ui.form.on('Patient Appointment', { if (frm.doc.patient) { frappe.call({ method: "erpnext.healthcare.doctype.patient_appointment.patient_appointment.get_prescribed_therapies", - args: {patient: frm.doc.patient}, + args: { patient: frm.doc.patient }, callback: function(r) { if (r.message) { show_therapy_types(frm, r.message); @@ -302,13 +302,13 @@ let check_and_set_availability = function(frm) { let d = new frappe.ui.Dialog({ title: __('Available slots'), fields: [ - { fieldtype: 'Link', options: 'Medical Department', reqd: 1, fieldname: 'department', label: 'Medical Department'}, - { fieldtype: 'Column Break'}, - { fieldtype: 'Link', options: 'Healthcare Practitioner', reqd: 1, fieldname: 'practitioner', label: 'Healthcare Practitioner'}, - { fieldtype: 'Column Break'}, - { fieldtype: 'Date', reqd: 1, fieldname: 'appointment_date', label: 'Date'}, - { fieldtype: 'Section Break'}, - { fieldtype: 'HTML', fieldname: 'available_slots'} + { fieldtype: 'Link', options: 'Medical Department', reqd: 1, fieldname: 'department', label: 'Medical Department' }, + { fieldtype: 'Column Break' }, + { fieldtype: 'Link', options: 'Healthcare Practitioner', reqd: 1, fieldname: 'practitioner', label: 'Healthcare Practitioner' }, + { fieldtype: 'Column Break' }, + { fieldtype: 'Date', reqd: 1, fieldname: 'appointment_date', label: 'Date' }, + { fieldtype: 'Section Break' }, + { fieldtype: 'HTML', fieldname: 'available_slots' } ], primary_action_label: __('Book'), @@ -386,59 +386,22 @@ let check_and_set_availability = function(frm) { let $wrapper = d.fields_dict.available_slots.$wrapper; // make buttons for each slot - let slot_details = data.slot_details; - let slot_html = ''; - for (let i = 0; i < slot_details.length; i++) { - slot_html = slot_html + ``; - slot_html = slot_html + `
` + slot_details[i].avail_slot.map(slot => { - let disabled = ''; - let start_str = slot.from_time; - let slot_start_time = moment(slot.from_time, 'HH:mm:ss'); - let slot_to_time = moment(slot.to_time, 'HH:mm:ss'); - let interval = (slot_to_time - slot_start_time)/60000 | 0; - // iterate in all booked appointments, update the start time and duration - slot_details[i].appointments.forEach(function(booked) { - let booked_moment = moment(booked.appointment_time, 'HH:mm:ss'); - let end_time = booked_moment.clone().add(booked.duration, 'minutes'); - // Deal with 0 duration appointments - if (booked_moment.isSame(slot_start_time) || booked_moment.isBetween(slot_start_time, slot_to_time)) { - if(booked.duration == 0){ - disabled = 'disabled="disabled"'; - return false; - } - } - // Check for overlaps considering appointment duration - if (slot_start_time.isBefore(end_time) && slot_to_time.isAfter(booked_moment)) { - // There is an overlap - disabled = 'disabled="disabled"'; - return false; - } - }); - return ``; - }).join(""); - slot_html = slot_html + `
`; - } + let slot_html = get_slots(data.slot_details); $wrapper .css('margin-bottom', 0) .addClass('text-center') .html(slot_html); - // blue button when clicked + // highlight button when clicked $wrapper.on('click', 'button', function() { let $btn = $(this); - $wrapper.find('button').removeClass('btn-primary'); - $btn.addClass('btn-primary'); + $wrapper.find('button').removeClass('btn-outline-primary'); + $btn.addClass('btn-outline-primary'); selected_slot = $btn.attr('data-name'); service_unit = $btn.attr('data-service-unit'); duration = $btn.attr('data-duration'); - // enable dialog action + // enable primary action 'Book' d.get_primary_btn().attr('disabled', null); }); @@ -448,19 +411,102 @@ let check_and_set_availability = function(frm) { } }, freeze: true, - freeze_message: __('Fetching records......') + freeze_message: __('Fetching Schedule...') }); } else { fd.available_slots.html(__('Appointment date and Healthcare Practitioner are Mandatory').bold()); } } + + function get_slots(slot_details) { + let slot_html = ''; + let appointment_count = 0; + let disabled = false; + let start_str, slot_start_time, slot_end_time, interval, count, count_class, tool_tip, available_slots; + + slot_details.forEach((slot_info) => { + slot_html += `
+ ${__('Practitioner Schedule:')} ${slot_info.slot_name}
+ ${__('Service Unit:')} ${slot_info.service_unit} `; + + if (slot_info.service_unit_capacity) { + slot_html += `
${__('Maximum Capacity:')} ${slot_info.service_unit_capacity} `; + } + + slot_html += '


'; + + slot_html += slot_info.avail_slot.map(slot => { + appointment_count = 0; + disabled = false; + start_str = slot.from_time; + slot_start_time = moment(slot.from_time, 'HH:mm:ss'); + slot_end_time = moment(slot.to_time, 'HH:mm:ss'); + interval = (slot_end_time - slot_start_time) / 60000 | 0; + + // iterate in all booked appointments, update the start time and duration + slot_info.appointments.forEach((booked) => { + let booked_moment = moment(booked.appointment_time, 'HH:mm:ss'); + let end_time = booked_moment.clone().add(booked.duration, 'minutes'); + + // Deal with 0 duration appointments + if (booked_moment.isSame(slot_start_time) || booked_moment.isBetween(slot_start_time, slot_end_time)) { + if (booked.duration == 0) { + disabled = true; + return false; + } + } + + // Check for overlaps considering appointment duration + if (slot_info.allow_overlap != 1) { + if (slot_start_time.isBefore(end_time) && slot_end_time.isAfter(booked_moment)) { + // There is an overlap + disabled = true; + return false; + } + } else { + if (slot_start_time.isBefore(end_time) && slot_end_time.isAfter(booked_moment)) { + appointment_count++; + } + if (appointment_count >= slot_info.service_unit_capacity) { + // There is an overlap + disabled = true; + return false; + } + } + }); + + if (slot_info.allow_overlap == 1 && slot_info.service_unit_capacity > 1) { + available_slots = slot_info.service_unit_capacity - appointment_count; + count = `${(available_slots > 0 ? available_slots : __('Full'))}`; + count_class = `${(available_slots > 0 ? 'badge-success' : 'badge-danger')}`; + tool_tip =`${available_slots} ${__('slots available for booking')}`; + } + return ` + `; + }).join(""); + + if (slot_info.service_unit_capacity) { + slot_html += `
${__('Each slot indicates the capacity currently available for booking')}`; + } + slot_html += `

`; + }); + + return slot_html; + } }; let get_prescribed_procedure = function(frm) { if (frm.doc.patient) { frappe.call({ method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.get_procedure_prescribed', - args: {patient: frm.doc.patient}, + args: { patient: frm.doc.patient }, callback: function(r) { if (r.message && r.message.length) { show_procedure_templates(frm, r.message); @@ -480,7 +526,7 @@ let get_prescribed_procedure = function(frm) { } }; -let show_procedure_templates = function(frm, result){ +let show_procedure_templates = function(frm, result) { let d = new frappe.ui.Dialog({ title: __('Prescribed Procedures'), fields: [ @@ -500,9 +546,11 @@ let show_procedure_templates = function(frm, result){ data-encounter="%(encounter)s" data-practitioner="%(practitioner)s"\ data-date="%(date)s" data-department="%(department)s">\

', {name:y[0], procedure_template: y[1], - encounter:y[2], consulting_practitioner:y[3], encounter_date:y[4], - practitioner:y[5]? y[5]:'', date: y[6]? y[6]:'', department: y[7]? y[7]:''})).appendTo(html_field); +

', { + name: y[0], procedure_template: y[1], + encounter: y[2], consulting_practitioner: y[3], encounter_date: y[4], + practitioner: y[5] ? y[5] : '', date: y[6] ? y[6] : '', department: y[7] ? y[7] : '' + })).appendTo(html_field); row.find("a").click(function() { frm.doc.procedure_template = $(this).attr('data-procedure-template'); frm.doc.procedure_prescription = $(this).attr('data-name'); @@ -520,7 +568,7 @@ let show_procedure_templates = function(frm, result){ }); if (!result) { let msg = __('There are no procedure prescribed for ') + frm.doc.patient; - $(repl('
%(msg)s
', {msg: msg})).appendTo(html_field); + $(repl('
%(msg)s
', { msg: msg })).appendTo(html_field); } d.show(); }; @@ -535,7 +583,7 @@ let show_therapy_types = function(frm, result) { ] }); var html_field = d.fields_dict.therapy_type.$wrapper; - $.each(result, function(x, y){ + $.each(result, function(x, y) { var row = $(repl('
\
%(encounter)s
%(practitioner)s
%(date)s
\
%(therapy)s
\ @@ -544,9 +592,11 @@ let show_therapy_types = function(frm, result) { data-encounter="%(encounter)s" data-practitioner="%(practitioner)s"\ data-date="%(date)s" data-department="%(department)s">\

', {therapy:y[0], - name: y[1], encounter:y[2], practitioner:y[3], date:y[4], - department:y[6]? y[6]:'', therapy_plan:y[5]})).appendTo(html_field); +

', { + therapy: y[0], + name: y[1], encounter: y[2], practitioner: y[3], date: y[4], + department: y[6] ? y[6] : '', therapy_plan: y[5] + })).appendTo(html_field); row.find("a").click(function() { frm.doc.therapy_type = $(this).attr("data-therapy"); @@ -581,13 +631,13 @@ let create_vital_signs = function(frm) { frappe.new_doc('Vital Signs'); }; -let update_status = function(frm, status){ +let update_status = function(frm, status) { let doc = frm.doc; frappe.confirm(__('Are you sure you want to cancel this appointment?'), function() { frappe.call({ method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_status', - args: {appointment_id: doc.name, status:status}, + args: { appointment_id: doc.name, status: status }, callback: function(data) { if (!data.exc) { frm.reload_doc(); diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json index 73ec3bc325..28d3a6dadf 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json @@ -131,7 +131,7 @@ "fieldtype": "Link", "label": "Service Unit", "options": "Healthcare Service Unit", - "set_only_once": 1 + "read_only": 1 }, { "depends_on": "eval:doc.practitioner;", @@ -349,7 +349,7 @@ } ], "links": [], - "modified": "2021-06-16 00:40:26.841794", + "modified": "2021-08-30 09:00:41.329387", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient Appointment", diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index 7db4fa616a..36047c4838 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -15,6 +15,11 @@ from erpnext.hr.doctype.employee.employee import is_holiday from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account from erpnext.healthcare.utils import check_fee_validity, get_service_item_and_practitioner_charge, manage_fee_validity +class MaximumCapacityError(frappe.ValidationError): + pass +class OverlapError(frappe.ValidationError): + pass + class PatientAppointment(Document): def validate(self): self.validate_overlaps() @@ -49,26 +54,49 @@ class PatientAppointment(Document): end_time = datetime.datetime.combine(getdate(self.appointment_date), get_time(self.appointment_time)) \ + datetime.timedelta(minutes=flt(self.duration)) - overlaps = frappe.db.sql(""" - select - name, practitioner, patient, appointment_time, duration - from - `tabPatient Appointment` - where - appointment_date=%s and name!=%s and status NOT IN ("Closed", "Cancelled") - and (practitioner=%s or patient=%s) and - ((appointment_time<%s and appointment_time + INTERVAL duration MINUTE>%s) or - (appointment_time>%s and appointment_time<%s) or - (appointment_time=%s)) - """, (self.appointment_date, self.name, self.practitioner, self.patient, - self.appointment_time, end_time.time(), self.appointment_time, end_time.time(), self.appointment_time)) + # all appointments for both patient and practitioner overlapping the duration of this appointment + overlapping_appointments = frappe.db.sql(""" + SELECT + name, practitioner, patient, appointment_time, duration, service_unit + FROM + `tabPatient Appointment` + WHERE + appointment_date=%(appointment_date)s AND name!=%(name)s AND status NOT IN ("Closed", "Cancelled") AND + (practitioner=%(practitioner)s OR patient=%(patient)s) AND + ((appointment_time<%(appointment_time)s AND appointment_time + INTERVAL duration MINUTE>%(appointment_time)s) OR + (appointment_time>%(appointment_time)s AND appointment_time<%(end_time)s) OR + (appointment_time=%(appointment_time)s)) + """, + { + 'appointment_date': self.appointment_date, + 'name': self.name, + 'practitioner': self.practitioner, + 'patient': self.patient, + 'appointment_time': self.appointment_time, + 'end_time':end_time.time() + }, + as_dict = True + ) + + if not overlapping_appointments: + return # No overlaps, nothing to validate! + + if self.service_unit: # validate service unit capacity if overlap enabled + allow_overlap, service_unit_capacity = frappe.get_value('Healthcare Service Unit', self.service_unit, + ['overlap_appointments', 'service_unit_capacity']) + if allow_overlap: + service_unit_appointments = list(filter(lambda appointment: appointment['service_unit'] == self.service_unit and + appointment['patient'] != self.patient, overlapping_appointments)) # if same patient already booked, it should be an overlap + if len(service_unit_appointments) >= (service_unit_capacity or 1): + frappe.throw(_("Not allowed, {} cannot exceed maximum capacity {}") + .format(frappe.bold(self.service_unit), frappe.bold(service_unit_capacity or 1)), MaximumCapacityError) + else: # service_unit_appointments within capacity, remove from overlapping_appointments + overlapping_appointments = [appointment for appointment in overlapping_appointments if appointment not in service_unit_appointments] + + if overlapping_appointments: + frappe.throw(_("Not allowed, cannot overlap appointment {}") + .format(frappe.bold(', '.join([appointment['name'] for appointment in overlapping_appointments]))), OverlapError) - if overlaps: - overlapping_details = _('Appointment overlaps with ') - overlapping_details += "{0}
".format(overlaps[0][0]) - overlapping_details += _('{0} has appointment scheduled with {1} at {2} having {3} minute(s) duration.').format( - overlaps[0][1], overlaps[0][2], overlaps[0][3], overlaps[0][4]) - frappe.throw(overlapping_details, title=_('Appointments Overlapping')) def validate_service_unit(self): if self.inpatient_record and self.service_unit: @@ -325,6 +353,8 @@ def get_available_slots(practitioner_doc, date): if available_slots: appointments = [] + allow_overlap = 0 + service_unit_capacity = 0 # fetch all appointments to practitioner by service unit filters = { 'practitioner': practitioner, @@ -334,8 +364,8 @@ def get_available_slots(practitioner_doc, date): } if schedule_entry.service_unit: - slot_name = schedule_entry.schedule + ' - ' + schedule_entry.service_unit - allow_overlap = frappe.get_value('Healthcare Service Unit', schedule_entry.service_unit, 'overlap_appointments') + slot_name = f'{schedule_entry.schedule}' + allow_overlap, service_unit_capacity = frappe.get_value('Healthcare Service Unit', schedule_entry.service_unit, ['overlap_appointments', 'service_unit_capacity']) if not allow_overlap: # fetch all appointments to service unit filters.pop('practitioner') @@ -350,8 +380,8 @@ def get_available_slots(practitioner_doc, date): filters=filters, fields=['name', 'appointment_time', 'duration', 'status']) - slot_details.append({'slot_name':slot_name, 'service_unit':schedule_entry.service_unit, - 'avail_slot':available_slots, 'appointments': appointments}) + slot_details.append({'slot_name': slot_name, 'service_unit': schedule_entry.service_unit, 'avail_slot': available_slots, + 'appointments': appointments, 'allow_overlap': allow_overlap, 'service_unit_capacity': service_unit_capacity}) return slot_details diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index 157b3e1162..f5477c096a 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -16,9 +16,11 @@ class TestPatientAppointment(unittest.TestCase): frappe.db.sql("""delete from `tabFee Validity`""") frappe.db.sql("""delete from `tabPatient Encounter`""") make_pos_profile() + frappe.db.sql("""delete from `tabHealthcare Service Unit` where name like '_Test %'""") + frappe.db.sql("""delete from `tabHealthcare Service Unit` where name like '_Test Service Unit Type%'""") def test_status(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0) appointment = create_appointment(patient, practitioner, nowdate()) self.assertEqual(appointment.status, 'Open') @@ -30,7 +32,7 @@ class TestPatientAppointment(unittest.TestCase): self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open') def test_start_encounter(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4), invoice = 1) appointment.reload() @@ -44,7 +46,7 @@ class TestPatientAppointment(unittest.TestCase): self.assertEqual(encounter.invoiced, frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced')) def test_auto_invoicing(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0) appointment = create_appointment(patient, practitioner, nowdate()) @@ -60,13 +62,14 @@ class TestPatientAppointment(unittest.TestCase): self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount) def test_auto_invoicing_based_on_department(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() + medical_department = create_medical_department() frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) appointment_type = create_appointment_type() appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2), - invoice=1, appointment_type=appointment_type.name, department='_Test Medical Department') + invoice=1, appointment_type=appointment_type.name, department=medical_department) appointment.reload() self.assertEqual(appointment.invoiced, 1) @@ -78,7 +81,7 @@ class TestPatientAppointment(unittest.TestCase): self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount) def test_auto_invoicing_according_to_appointment_type_charge(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) @@ -104,7 +107,7 @@ class TestPatientAppointment(unittest.TestCase): self.assertTrue(sales_invoice_name) def test_appointment_cancel(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1) appointment = create_appointment(patient, practitioner, nowdate()) fee_validity = frappe.db.get_value('Fee Validity', {'patient': patient, 'practitioner': practitioner}) @@ -112,7 +115,7 @@ class TestPatientAppointment(unittest.TestCase): self.assertTrue(fee_validity) # first follow up appointment - appointment = create_appointment(patient, practitioner, nowdate()) + appointment = create_appointment(patient, practitioner, add_days(nowdate(), 1)) self.assertEqual(frappe.db.get_value('Fee Validity', fee_validity, 'visited'), 1) update_status(appointment.name, 'Cancelled') @@ -121,7 +124,7 @@ class TestPatientAppointment(unittest.TestCase): frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) - appointment = create_appointment(patient, practitioner, nowdate(), invoice=1) + appointment = create_appointment(patient, practitioner, add_days(nowdate(), 1), invoice=1) update_status(appointment.name, 'Cancelled') # check invoice cancelled sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent') @@ -133,7 +136,7 @@ class TestPatientAppointment(unittest.TestCase): create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy frappe.db.sql("""delete from `tabInpatient Record`""") - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() patient = create_patient() # Schedule Admission ip_record = create_inpatient(patient) @@ -141,7 +144,7 @@ class TestPatientAppointment(unittest.TestCase): ip_record.save(ignore_permissions = True) # Admit - service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy') + service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy') admit_patient(ip_record, service_unit, now_datetime()) appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit) @@ -159,7 +162,7 @@ class TestPatientAppointment(unittest.TestCase): create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy frappe.db.sql("""delete from `tabInpatient Record`""") - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() patient = create_patient() # Schedule Admission ip_record = create_inpatient(patient) @@ -167,10 +170,10 @@ class TestPatientAppointment(unittest.TestCase): ip_record.save(ignore_permissions = True) # Admit - service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy') + service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy') admit_patient(ip_record, service_unit, now_datetime()) - appointment_service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy for Appointment') + appointment_service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy for Appointment') appointment = create_appointment(patient, practitioner, nowdate(), service_unit=appointment_service_unit, save=0) self.assertRaises(frappe.exceptions.ValidationError, appointment.save) @@ -192,7 +195,7 @@ class TestPatientAppointment(unittest.TestCase): assert payment_required is True def test_sales_invoice_should_be_generated_for_new_patient_appointment(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) invoice_count = frappe.db.count('Sales Invoice') @@ -203,10 +206,10 @@ class TestPatientAppointment(unittest.TestCase): assert new_invoice_count == invoice_count + 1 def test_patient_appointment_should_consider_permissions_while_fetching_appointments(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() create_appointment(patient, practitioner, nowdate()) - patient, medical_department, new_practitioner = create_healthcare_docs(practitioner_name='Dr. John') + patient, new_practitioner = create_healthcare_docs(id=5) create_appointment(patient, new_practitioner, nowdate()) roles = [{"doctype": "Has Role", "role": "Physician"}] @@ -223,41 +226,102 @@ class TestPatientAppointment(unittest.TestCase): appointments = frappe.get_list('Patient Appointment') assert len(appointments) == 2 -def create_healthcare_docs(practitioner_name=None): - if not practitioner_name: - practitioner_name = '_Test Healthcare Practitioner' + def test_overlap_appointment(self): + from erpnext.healthcare.doctype.patient_appointment.patient_appointment import OverlapError + patient, practitioner = create_healthcare_docs(id=1) + patient_1, practitioner_1 = create_healthcare_docs(id=2) + service_unit = create_service_unit(id=0) + service_unit_1 = create_service_unit(id=1) + appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit) # valid - patient = create_patient() - practitioner = frappe.db.exists('Healthcare Practitioner', practitioner_name) - medical_department = frappe.db.exists('Medical Department', '_Test Medical Department') + # patient and practitioner cannot have overlapping appointments + appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit, save=0) + self.assertRaises(OverlapError, appointment.save) + appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit_1, save=0) # diff service unit + self.assertRaises(OverlapError, appointment.save) + appointment = create_appointment(patient, practitioner, nowdate(), save=0) # with no service unit link + self.assertRaises(OverlapError, appointment.save) - if not medical_department: - medical_department = frappe.new_doc('Medical Department') - medical_department.department = '_Test Medical Department' - medical_department.save(ignore_permissions=True) - medical_department = medical_department.name + # patient cannot have overlapping appointments with other practitioners + appointment = create_appointment(patient, practitioner_1, nowdate(), service_unit=service_unit, save=0) + self.assertRaises(OverlapError, appointment.save) + appointment = create_appointment(patient, practitioner_1, nowdate(), service_unit=service_unit_1, save=0) + self.assertRaises(OverlapError, appointment.save) + appointment = create_appointment(patient, practitioner_1, nowdate(), save=0) + self.assertRaises(OverlapError, appointment.save) - if not practitioner: - practitioner = frappe.new_doc('Healthcare Practitioner') - practitioner.first_name = practitioner_name - practitioner.gender = 'Female' - practitioner.department = medical_department - practitioner.op_consulting_charge = 500 - practitioner.inpatient_visit_charge = 500 - practitioner.save(ignore_permissions=True) - practitioner = practitioner.name + # practitioner cannot have overlapping appointments with other patients + appointment = create_appointment(patient_1, practitioner, nowdate(), service_unit=service_unit, save=0) + self.assertRaises(OverlapError, appointment.save) + appointment = create_appointment(patient_1, practitioner, nowdate(), service_unit=service_unit_1, save=0) + self.assertRaises(OverlapError, appointment.save) + appointment = create_appointment(patient_1, practitioner, nowdate(), save=0) + self.assertRaises(OverlapError, appointment.save) - return patient, medical_department, practitioner + def test_service_unit_capacity(self): + from erpnext.healthcare.doctype.patient_appointment.patient_appointment import MaximumCapacityError, OverlapError + practitioner = create_practitioner() + capacity = 3 + overlap_service_unit_type = create_service_unit_type(id=10, allow_appointments=1, overlap_appointments=1) + overlap_service_unit = create_service_unit(id=100, service_unit_type=overlap_service_unit_type, service_unit_capacity=capacity) + + for i in range(0, capacity): + patient = create_patient(id=i) + create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit) # valid + appointment = create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit, save=0) # overlap + self.assertRaises(OverlapError, appointment.save) + + patient = create_patient(id=capacity) + appointment = create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit, save=0) + self.assertRaises(MaximumCapacityError, appointment.save) + + +def create_healthcare_docs(id=0): + patient = create_patient(id) + practitioner = create_practitioner(id) + + return patient, practitioner + + +def create_patient(id=0): + if frappe.db.exists('Patient', {'firstname':f'_Test Patient {str(id)}'}): + patient = frappe.db.get_value('Patient', {'first_name': f'_Test Patient {str(id)}'}, ['name']) + return patient + + patient = frappe.new_doc('Patient') + patient.first_name = f'_Test Patient {str(id)}' + patient.sex = 'Female' + patient.save(ignore_permissions=True) + + return patient.name + + +def create_medical_department(id=0): + if frappe.db.exists('Medical Department', f'_Test Medical Department {str(id)}'): + return f'_Test Medical Department {str(id)}' + + medical_department = frappe.new_doc('Medical Department') + medical_department.department = f'_Test Medical Department {str(id)}' + medical_department.save(ignore_permissions=True) + + return medical_department.name + + +def create_practitioner(id=0, medical_department=None): + if frappe.db.exists('Healthcare Practitioner', {'firstname':f'_Test Healthcare Practitioner {str(id)}'}): + practitioner = frappe.db.get_value('Healthcare Practitioner', {'firstname':f'_Test Healthcare Practitioner {str(id)}'}, ['name']) + return practitioner + + practitioner = frappe.new_doc('Healthcare Practitioner') + practitioner.first_name = f'_Test Healthcare Practitioner {str(id)}' + practitioner.gender = 'Female' + practitioner.department = medical_department or create_medical_department(id) + practitioner.op_consulting_charge = 500 + practitioner.inpatient_visit_charge = 500 + practitioner.save(ignore_permissions=True) + + return practitioner.name -def create_patient(): - patient = frappe.db.exists('Patient', '_Test Patient') - if not patient: - patient = frappe.new_doc('Patient') - patient.first_name = '_Test Patient' - patient.sex = 'Female' - patient.save(ignore_permissions=True) - patient = patient.name - return patient def create_encounter(appointment): if appointment: @@ -270,8 +334,10 @@ def create_encounter(appointment): encounter.company = appointment.company encounter.save() encounter.submit() + return encounter + def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0, service_unit=None, appointment_type=None, save=1, department=None): item = create_healthcare_service_items() @@ -284,6 +350,7 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce appointment.appointment_date = appointment_date appointment.company = '_Test Company' appointment.duration = 15 + if service_unit: appointment.service_unit = service_unit if invoice: @@ -294,11 +361,14 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce appointment.procedure_template = create_clinical_procedure_template().get('name') if save: appointment.save(ignore_permissions=True) + return appointment + def create_healthcare_service_items(): if frappe.db.exists('Item', 'HLC-SI-001'): return 'HLC-SI-001' + item = frappe.new_doc('Item') item.item_code = 'HLC-SI-001' item.item_name = 'Consulting Charges' @@ -306,11 +376,14 @@ def create_healthcare_service_items(): item.is_stock_item = 0 item.stock_uom = 'Nos' item.save() + return item.name + def create_clinical_procedure_template(): if frappe.db.exists('Clinical Procedure Template', 'Knee Surgery and Rehab'): return frappe.get_doc('Clinical Procedure Template', 'Knee Surgery and Rehab') + template = frappe.new_doc('Clinical Procedure Template') template.template = 'Knee Surgery and Rehab' template.item_code = 'Knee Surgery and Rehab' @@ -319,8 +392,10 @@ def create_clinical_procedure_template(): template.description = 'Knee Surgery and Rehab' template.rate = 50000 template.save() + return template + def create_appointment_type(args=None): if not args: args = frappe.local.form_dict @@ -359,3 +434,30 @@ def create_user(email=None, roles=None): "roles": roles, }).insert() return user + + +def create_service_unit_type(id=0, allow_appointments=1, overlap_appointments=0): + if frappe.db.exists('Healthcare Service Unit Type', f'_Test Service Unit Type {str(id)}'): + return f'_Test Service Unit Type {str(id)}' + + service_unit_type = frappe.new_doc('Healthcare Service Unit Type') + service_unit_type.service_unit_type = f'_Test Service Unit Type {str(id)}' + service_unit_type.allow_appointments = allow_appointments + service_unit_type.overlap_appointments = overlap_appointments + service_unit_type.save(ignore_permissions=True) + + return service_unit_type.name + + +def create_service_unit(id=0, service_unit_type=None, service_unit_capacity=0): + if frappe.db.exists('Healthcare Service Unit', f'_Test Service Unit {str(id)}'): + return f'_Test service_unit {str(id)}' + + service_unit = frappe.new_doc('Healthcare Service Unit') + service_unit.is_group = 0 + service_unit.healthcare_service_unit_name= f'_Test Service Unit {str(id)}' + service_unit.service_unit_type = service_unit_type or create_service_unit_type(id) + service_unit.service_unit_capacity = service_unit_capacity + service_unit.save(ignore_permissions=True) + + return service_unit.name diff --git a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py index f8ccc8a002..5b7d8d62c8 100644 --- a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py +++ b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import unittest import frappe from frappe.utils import nowdate -from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_encounter, create_healthcare_docs, create_appointment +from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_encounter, create_healthcare_docs, create_appointment, create_medical_department from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile class TestPatientMedicalRecord(unittest.TestCase): @@ -15,7 +15,8 @@ class TestPatientMedicalRecord(unittest.TestCase): make_pos_profile() def test_medical_record(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() + medical_department = create_medical_department() appointment = create_appointment(patient, practitioner, nowdate(), invoice=1) encounter = create_encounter(appointment) diff --git a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py index 113fa513f9..983fba9f5f 100644 --- a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py +++ b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py @@ -8,11 +8,13 @@ import unittest from frappe.utils import getdate, flt, nowdate from erpnext.healthcare.doctype.therapy_type.test_therapy_type import create_therapy_type from erpnext.healthcare.doctype.therapy_plan.therapy_plan import make_therapy_session, make_sales_invoice -from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient, create_appointment +from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import \ + create_healthcare_docs, create_patient, create_appointment, create_medical_department class TestTherapyPlan(unittest.TestCase): def test_creation_on_encounter_submission(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() + medical_department = create_medical_department() encounter = create_encounter(patient, medical_department, practitioner) self.assertTrue(frappe.db.exists('Therapy Plan', encounter.therapy_plan)) @@ -28,8 +30,9 @@ class TestTherapyPlan(unittest.TestCase): frappe.get_doc(session).submit() self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed') - patient, medical_department, practitioner = create_healthcare_docs() - appointment = create_appointment(patient, practitioner, nowdate()) + patient, practitioner = create_healthcare_docs() + appointment = create_appointment(patient, practitioner, nowdate()) + session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name) session = frappe.get_doc(session) session.submit() diff --git a/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py b/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py index a5dad293e3..80fc83fd6c 100644 --- a/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py +++ b/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py @@ -34,7 +34,8 @@ def create_therapy_type(): }) therapy_type.save() else: - therapy_type = frappe.get_doc('Therapy Type', 'Basic Rehab') + therapy_type = frappe.get_doc('Therapy Type', therapy_type) + return therapy_type def create_exercise_type(): @@ -47,4 +48,7 @@ def create_exercise_type(): 'description': 'Squat and Rise' }) exercise_type.save() + else: + exercise_type = frappe.get_doc('Exercise Type', exercise_type) + return exercise_type diff --git a/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py b/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py index 4b461f1a97..fae5ecef84 100644 --- a/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py +++ b/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py @@ -25,7 +25,7 @@ class TestInpatientMedicationOrders(unittest.TestCase): 'from_date': getdate(), 'to_date': getdate(), 'patient': '_Test IPD Patient', - 'service_unit': 'Test Service Unit Ip Occupancy - _TC' + 'service_unit': '_Test Service Unit Ip Occupancy - _TC' } report = execute(filters) @@ -42,7 +42,7 @@ class TestInpatientMedicationOrders(unittest.TestCase): 'date': getdate(), 'time': datetime.timedelta(seconds=32400), 'is_completed': 0, - 'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC' + 'healthcare_service_unit': '_Test Service Unit Ip Occupancy - _TC' }, { 'patient': '_Test IPD Patient', @@ -55,7 +55,7 @@ class TestInpatientMedicationOrders(unittest.TestCase): 'date': getdate(), 'time': datetime.timedelta(seconds=50400), 'is_completed': 0, - 'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC' + 'healthcare_service_unit': '_Test Service Unit Ip Occupancy - _TC' }, { 'patient': '_Test IPD Patient', @@ -68,7 +68,7 @@ class TestInpatientMedicationOrders(unittest.TestCase): 'date': getdate(), 'time': datetime.timedelta(seconds=75600), 'is_completed': 0, - 'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC' + 'healthcare_service_unit': '_Test Service Unit Ip Occupancy - _TC' } ] @@ -83,7 +83,7 @@ class TestInpatientMedicationOrders(unittest.TestCase): 'from_date': getdate(), 'to_date': getdate(), 'patient': '_Test IPD Patient', - 'service_unit': 'Test Service Unit Ip Occupancy - _TC', + 'service_unit': '_Test Service Unit Ip Occupancy - _TC', 'show_completed_orders': 0 } @@ -119,7 +119,7 @@ def create_records(patient): ip_record.expected_length_of_stay = 0 ip_record.save() ip_record.reload() - service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy') + service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy') admit_patient(ip_record, service_unit, now_datetime()) ipmo = create_ipmo(patient) diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py index d3d22c80b6..ffecf4dce2 100644 --- a/erpnext/healthcare/utils.py +++ b/erpnext/healthcare/utils.py @@ -543,58 +543,43 @@ def get_drugs_to_invoice(encounter): @frappe.whitelist() -def get_children(doctype, parent, company, is_root=False): - parent_fieldname = "parent_" + doctype.lower().replace(" ", "_") +def get_children(doctype, parent=None, company=None, is_root=False): + parent_fieldname = 'parent_' + doctype.lower().replace(' ', '_') fields = [ - "name as value", - "is_group as expandable", - "lft", - "rgt" + 'name as value', + 'is_group as expandable', + 'lft', + 'rgt' ] - # fields = [ "name", "is_group", "lft", "rgt" ] - filters = [["ifnull(`{0}`,'')".format(parent_fieldname), "=", "" if is_root else parent]] + + filters = [["ifnull(`{0}`,'')".format(parent_fieldname), + '=', '' if is_root else parent]] if is_root: - fields += ["service_unit_type"] if doctype == "Healthcare Service Unit" else [] - filters.append(["company", "=", company]) - + fields += ['service_unit_type'] if doctype == 'Healthcare Service Unit' else [] + filters.append(['company', '=', company]) else: - fields += ["service_unit_type", "allow_appointments", "inpatient_occupancy", "occupancy_status"] if doctype == "Healthcare Service Unit" else [] - fields += [parent_fieldname + " as parent"] + fields += ['service_unit_type', 'allow_appointments', 'inpatient_occupancy', + 'occupancy_status'] if doctype == 'Healthcare Service Unit' else [] + fields += [parent_fieldname + ' as parent'] - hc_service_units = frappe.get_list(doctype, fields=fields, filters=filters) + service_units = frappe.get_list(doctype, fields=fields, filters=filters) + for each in service_units: + if each['expandable'] == 1: # group node + available_count = frappe.db.count('Healthcare Service Unit', filters={ + 'parent_healthcare_service_unit': each['value'], + 'inpatient_occupancy': 1}) - if doctype == "Healthcare Service Unit": - for each in hc_service_units: - occupancy_msg = "" - if each["expandable"] == 1: - occupied = False - vacant = False - child_list = frappe.db.sql( - ''' - SELECT - name, occupancy_status - FROM - `tabHealthcare Service Unit` - WHERE - inpatient_occupancy = 1 - and lft > %s and rgt < %s - ''', (each['lft'], each['rgt'])) + if available_count > 0: + occupied_count = frappe.db.count('Healthcare Service Unit', { + 'parent_healthcare_service_unit': each['value'], + 'inpatient_occupancy': 1, + 'occupancy_status': 'Occupied'}) + # set occupancy status of group node + each['occupied_of_available'] = str( + occupied_count) + ' Occupied of ' + str(available_count) - for child in child_list: - if not occupied: - occupied = 0 - if child[1] == "Occupied": - occupied += 1 - if not vacant: - vacant = 0 - if child[1] == "Vacant": - vacant += 1 - if vacant and occupied: - occupancy_total = vacant + occupied - occupancy_msg = str(occupied) + " Occupied out of " + str(occupancy_total) - each["occupied_out_of_vacant"] = occupancy_msg - return hc_service_units + return service_units @frappe.whitelist() @@ -717,3 +702,40 @@ def render_doc_as_html(doctype, docname, exclude_fields = []): doc_html = "
" %(doctype, docname) + doc_html + '
' return {'html': doc_html} + + +def update_address_links(address, method): + ''' + Hook validate Address + If Patient is linked in Address, also link the associated Customer + ''' + if 'Healthcare' not in frappe.get_active_domains(): + return + + patient_links = list(filter(lambda link: link.get('link_doctype') == 'Patient', address.links)) + + for link in patient_links: + customer = frappe.db.get_value('Patient', link.get('link_name'), 'customer') + if customer and not address.has_link('Customer', customer): + address.append('links', dict(link_doctype = 'Customer', link_name = customer)) + + +def update_patient_email_and_phone_numbers(contact, method): + ''' + Hook validate Contact + Update linked Patients' primary mobile and phone numbers + ''' + if 'Healthcare' not in frappe.get_active_domains(): + return + + if contact.is_primary_contact and (contact.email_id or contact.mobile_no or contact.phone): + patient_links = list(filter(lambda link: link.get('link_doctype') == 'Patient', contact.links)) + + for link in patient_links: + contact_details = frappe.db.get_value('Patient', link.get('link_name'), ['email', 'mobile', 'phone'], as_dict=1) + if contact.email_id and contact.email_id != contact_details.get('email'): + frappe.db.set_value('Patient', link.get('link_name'), 'email', contact.email_id) + if contact.mobile_no and contact.mobile_no != contact_details.get('mobile'): + frappe.db.set_value('Patient', link.get('link_name'), 'mobile', contact.mobile_no) + if contact.phone and contact.phone != contact_details.get('phone'): + frappe.db.set_value('Patient', link.get('link_name'), 'phone', contact.phone) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 4854bfd1e1..b1a64f95bf 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -290,7 +290,12 @@ doc_events = { "on_trash": "erpnext.regional.check_deletion_permission" }, 'Address': { - 'validate': ['erpnext.regional.india.utils.validate_gstin_for_india', 'erpnext.regional.italy.utils.set_state_code', 'erpnext.regional.india.utils.update_gst_category'] + 'validate': [ + 'erpnext.regional.india.utils.validate_gstin_for_india', + 'erpnext.regional.italy.utils.set_state_code', + 'erpnext.regional.india.utils.update_gst_category', + 'erpnext.healthcare.utils.update_address_links' + ], }, 'Supplier': { 'validate': 'erpnext.regional.india.utils.validate_pan_for_india' @@ -301,7 +306,7 @@ doc_events = { "Contact": { "on_trash": "erpnext.support.doctype.issue.issue.update_issue", "after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations", - "validate": "erpnext.crm.utils.update_lead_phone_numbers" + "validate": ["erpnext.crm.utils.update_lead_phone_numbers", "erpnext.healthcare.utils.update_patient_email_and_phone_numbers"] }, "Email Unsubscribe": { "after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient" From 9cb642238f6c9477487f0e6dd3f48ff1d1b27145 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Mon, 30 Aug 2021 13:17:22 +0530 Subject: [PATCH 24/44] fix: expense claim reimbursed amount update (#27204) --- .../doctype/journal_entry/journal_entry.py | 5 +- .../doctype/payment_entry/payment_entry.py | 9 ++-- .../hr/doctype/expense_claim/expense_claim.py | 20 ++------ .../expense_claim/test_expense_claim.py | 47 ++++++++++++++++++- 4 files changed, 61 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 937597bc55..72648753a7 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -643,7 +643,10 @@ class JournalEntry(AccountsController): for d in self.accounts: if d.reference_type=="Expense Claim" and d.reference_name: doc = frappe.get_doc("Expense Claim", d.reference_name) - update_reimbursed_amount(doc, jv=self.name) + if self.docstatus == 2: + update_reimbursed_amount(doc, -1 * d.debit) + else: + update_reimbursed_amount(doc, d.debit) def validate_expense_claim(self): diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index b259b11c63..a5fcad4996 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -75,9 +75,9 @@ class PaymentEntry(AccountsController): if self.difference_amount: frappe.throw(_("Difference Amount must be zero")) self.make_gl_entries() + self.update_expense_claim() self.update_outstanding_amounts() self.update_advance_paid() - self.update_expense_claim() self.update_donation() self.update_payment_schedule() self.set_status() @@ -85,9 +85,9 @@ class PaymentEntry(AccountsController): def on_cancel(self): self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') self.make_gl_entries(cancel=1) + self.update_expense_claim() self.update_outstanding_amounts() self.update_advance_paid() - self.update_expense_claim() self.update_donation(cancel=1) self.delink_advance_entry_references() self.update_payment_schedule(cancel=1) @@ -831,7 +831,10 @@ class PaymentEntry(AccountsController): for d in self.get("references"): if d.reference_doctype=="Expense Claim" and d.reference_name: doc = frappe.get_doc("Expense Claim", d.reference_name) - update_reimbursed_amount(doc, self.name) + if self.docstatus == 2: + update_reimbursed_amount(doc, -1 * d.allocated_amount) + else: + update_reimbursed_amount(doc, d.allocated_amount) def update_donation(self, cancel=0): if self.payment_type == "Receive" and self.party_type == "Donor" and self.party: diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 95e2806aed..4dc089cb1d 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -77,7 +77,7 @@ class ExpenseClaim(AccountsController): self.make_gl_entries() if self.is_paid: - update_reimbursed_amount(self) + update_reimbursed_amount(self, self.grand_total) self.set_status(update=True) self.update_claimed_amount_in_employee_advance() @@ -89,7 +89,7 @@ class ExpenseClaim(AccountsController): self.make_gl_entries(cancel=True) if self.is_paid: - update_reimbursed_amount(self) + update_reimbursed_amount(self, -1 * self.grand_total) self.update_claimed_amount_in_employee_advance() @@ -270,20 +270,10 @@ class ExpenseClaim(AccountsController): if not expense.default_account or not validate: expense.default_account = get_expense_claim_account(expense.expense_type, self.company)["account"] -def update_reimbursed_amount(doc, jv=None): +def update_reimbursed_amount(doc, amount): - condition = "" - - if jv: - condition += "and voucher_no = '{0}'".format(jv) - - amt = frappe.db.sql("""select ifnull(sum(debit_in_account_currency), 0) - ifnull(sum(credit_in_account_currency), 0)as amt - from `tabGL Entry` where against_voucher_type = 'Expense Claim' and against_voucher = %s - and party = %s {condition}""".format(condition=condition), #nosec - (doc.name, doc.employee) ,as_dict=1)[0].amt - - doc.total_amount_reimbursed = amt - frappe.db.set_value("Expense Claim", doc.name , "total_amount_reimbursed", amt) + doc.total_amount_reimbursed += amount + frappe.db.set_value("Expense Claim", doc.name , "total_amount_reimbursed", doc.total_amount_reimbursed) doc.set_status() frappe.db.set_value("Expense Claim", doc.name , "status", doc.status) diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index c2bd1e9f9f..b5fc1fbe75 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils import random_string, nowdate +from frappe.utils import random_string, nowdate, flt from erpnext.hr.doctype.expense_claim.expense_claim import make_bank_entry from erpnext.accounts.doctype.account.test_account import create_account from erpnext.hr.doctype.employee.test_employee import make_employee @@ -138,6 +138,31 @@ class TestExpenseClaim(unittest.TestCase): expense_claim.submit() frappe.set_user("Administrator") + def test_multiple_payment_entries_against_expense(self): + # Creating expense claim + payable_account = get_payable_account("_Test Company") + expense_claim = make_expense_claim(payable_account, 5500, 5500, "_Test Company", "Travel Expenses - _TC") + expense_claim.save() + expense_claim.submit() + + # Payment entry 1: paying 500 + make_payment_entry(expense_claim, payable_account,500) + outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts(expense_claim) + self.assertEqual(outstanding_amount, 5000) + self.assertEqual(total_amount_reimbursed, 500) + + # Payment entry 1: paying 2000 + make_payment_entry(expense_claim, payable_account,2000) + outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts(expense_claim) + self.assertEqual(outstanding_amount, 3000) + self.assertEqual(total_amount_reimbursed, 2500) + + # Payment entry 1: paying 3000 + make_payment_entry(expense_claim, payable_account,3000) + outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts(expense_claim) + self.assertEqual(outstanding_amount, 0) + self.assertEqual(total_amount_reimbursed, 5500) + def get_payable_account(company): return frappe.get_cached_value('Company', company, 'default_payable_account') @@ -191,3 +216,23 @@ def make_expense_claim(payable_account, amount, sanctioned_amount, company, acco return expense_claim expense_claim.submit() return expense_claim + +def get_outstanding_and_total_reimbursed_amounts(expense_claim): + outstanding_amount = flt(frappe.db.get_value("Expense Claim", expense_claim.name, "total_sanctioned_amount")) - \ + flt(frappe.db.get_value("Expense Claim", expense_claim.name, "total_amount_reimbursed")) + total_amount_reimbursed = flt(frappe.db.get_value("Expense Claim", expense_claim.name, "total_amount_reimbursed")) + + return outstanding_amount,total_amount_reimbursed + +def make_payment_entry(expense_claim, payable_account, amt): + from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + + pe = get_payment_entry("Expense Claim", expense_claim.name, bank_account="_Test Bank USD - _TC", bank_amount=amt) + pe.reference_no = "1" + pe.reference_date = nowdate() + pe.source_exchange_rate = 1 + pe.paid_to = payable_account + pe.references[0].allocated_amount = amt + pe.insert() + pe.submit() + From 9de0f7538f87d95dc1af4cbba2d9560145014efd Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 30 Aug 2021 14:07:13 +0530 Subject: [PATCH 25/44] ci: concurrency control for CI jobs (#27230) similar to https://github.com/frappe/frappe/pull/14061 This will ensure that only one instance of the following tests run per PR and cancel previous running/queued jobs when new commits are pushed. - Server / unit tests - UI tests - Patch test --- .github/workflows/patch.yml | 3 +++ .github/workflows/server-tests.yml | 4 ++++ .github/workflows/ui-tests.yml | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index f65b0001fe..7bba4b2e9c 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -7,6 +7,9 @@ on: - '**.md' workflow_dispatch: +concurrency: + group: patch-develop-${{ github.event.number }} + cancel-in-progress: true jobs: test: diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 0a73d74d6b..511a292ee9 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -12,6 +12,10 @@ on: - '**.js' - '**.md' +concurrency: + group: server-develop-${{ github.event.number }} + cancel-in-progress: true + jobs: test: runs-on: ubuntu-18.04 diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 0ece0d8ee6..06067ff416 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -6,6 +6,10 @@ on: - '**.md' workflow_dispatch: +concurrency: + group: ui-develop-${{ github.event.number }} + cancel-in-progress: true + jobs: test: runs-on: ubuntu-18.04 From fd467e6d326fc6abb7820efc458e6106c38e307f Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 30 Aug 2021 17:53:59 +0530 Subject: [PATCH 26/44] fix: Correct company address not getting copied from Purchase Order to Invoice (#27217) * fix: Correct company adderess not getting copied from Purchase Order to Invoice * fix: Linting issues --- erpnext/public/js/controllers/transaction.js | 26 +++++++++++--------- erpnext/public/js/utils/party.js | 4 +-- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 2538852bfa..5f8966f88a 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -866,21 +866,25 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) { - erpnext.utils.get_shipping_address(this.frm, function(){ + erpnext.utils.get_shipping_address(this.frm, function() { set_party_account(set_pricing); }); // Get default company billing address in Purchase Invoice, Order and Receipt - frappe.call({ - 'method': 'frappe.contacts.doctype.address.address.get_default_address', - 'args': { - 'doctype': 'Company', - 'name': this.frm.doc.company - }, - 'callback': function(r) { - me.frm.set_value('billing_address', r.message); - } - }); + if (this.frm.doc.company && frappe.meta.get_docfield(this.frm.doctype, "billing_address")) { + frappe.call({ + method: "erpnext.setup.doctype.company.company.get_default_company_address", + args: {name: this.frm.doc.company, existing_address: this.frm.doc.billing_address || ""}, + debounce: 2000, + callback: function(r) { + if (r.message) { + me.frm.set_value("billing_address", r.message); + } else { + me.frm.set_value("company_address", ""); + } + } + }); + } } else { set_party_account(set_pricing); diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 4d432e3d5c..a492b32a9f 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -289,8 +289,8 @@ erpnext.utils.get_shipping_address = function(frm, callback) { company: frm.doc.company, address: frm.doc.shipping_address }, - callback: function(r){ - if (r.message){ + callback: function(r) { + if (r.message) { frm.set_value("shipping_address", r.message[0]) //Address title or name frm.set_value("shipping_address_display", r.message[1]) //Address to be displayed on the page } From 5fd04101d4e79dead5e023e68ba24f4149d90426 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Mon, 30 Aug 2021 18:11:33 +0530 Subject: [PATCH 27/44] fix: Stock Ageing report issues for serialized items (#27228) * fix: incorrect calculation in get_range_age * fix: remove serial numbers not in stock from fifo_queue * refactor: make serial_no condition explicit * refactor: reduce code duplication Co-authored-by: Ankush Menat --- .../stock/report/stock_ageing/stock_ageing.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 623dc2ffd9..8a9f0a5e58 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -26,7 +26,7 @@ def execute(filters=None): average_age = get_average_age(fifo_queue, to_date) earliest_age = date_diff(to_date, fifo_queue[0][1]) latest_age = date_diff(to_date, fifo_queue[-1][1]) - range1, range2, range3, above_range3 = get_range_age(filters, fifo_queue, to_date) + range1, range2, range3, above_range3 = get_range_age(filters, fifo_queue, to_date, item_dict) row = [details.name, details.item_name, details.description, details.item_group, details.brand] @@ -58,19 +58,21 @@ def get_average_age(fifo_queue, to_date): return flt(age_qty / total_qty, 2) if total_qty else 0.0 -def get_range_age(filters, fifo_queue, to_date): +def get_range_age(filters, fifo_queue, to_date, item_dict): range1 = range2 = range3 = above_range3 = 0.0 + for item in fifo_queue: age = date_diff(to_date, item[1]) + qty = flt(item[0]) if not item_dict["has_serial_no"] else 1.0 if age <= filters.range1: - range1 += flt(item[0]) + range1 += qty elif age <= filters.range2: - range2 += flt(item[0]) + range2 += qty elif age <= filters.range3: - range3 += flt(item[0]) + range3 += qty else: - above_range3 += flt(item[0]) + above_range3 += qty return range1, range2, range3, above_range3 @@ -197,9 +199,7 @@ def get_fifo_queue(filters, sle=None): fifo_queue.append([d.actual_qty, d.posting_date]) else: if serial_no_list: - for serial_no in fifo_queue: - if serial_no[0] in serial_no_list: - fifo_queue.remove(serial_no) + fifo_queue[:] = [serial_no for serial_no in fifo_queue if serial_no[0] not in serial_no_list] else: qty_to_pop = abs(d.actual_qty) while qty_to_pop: @@ -222,14 +222,16 @@ def get_fifo_queue(filters, sle=None): else: item_details[key]["total_qty"] += d.actual_qty + item_details[key]["has_serial_no"] = d.has_serial_no + return item_details def get_stock_ledger_entries(filters): return frappe.db.sql("""select - item.name, item.item_name, item_group, brand, description, item.stock_uom, + item.name, item.item_name, item_group, brand, description, item.stock_uom, item.has_serial_no, actual_qty, posting_date, voucher_type, voucher_no, serial_no, batch_no, qty_after_transaction, warehouse from `tabStock Ledger Entry` sle, - (select name, item_name, description, stock_uom, brand, item_group + (select name, item_name, description, stock_uom, brand, item_group, has_serial_no from `tabItem` {item_conditions}) item where item_code = item.name and company = %(company)s and From 1682402a9ff157cf31b8871bc4addddefbf4ae12 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 Aug 2021 18:26:56 +0530 Subject: [PATCH 28/44] refactor: Healthcare Redesign Changes (#27236) --- .../clinical_procedures.json | 6 +- .../clinical_procedures_status.json | 6 +- .../dashboard_chart/diagnoses/diagnoses.json | 7 +- .../dashboard_chart/lab_tests/lab_tests.json | 6 +- .../dashboard_chart/symptoms/symptoms.json | 6 +- .../healthcare/doctype/lab_test/lab_test.py | 8 +- .../test_patient_appointment.py | 14 +- .../patient_history_settings.py | 6 +- .../test_patient_history_settings.py | 3 +- .../healthcare/healthcare.json | 2 +- .../create_healthcare_practitioner.json | 4 +- .../create_patient/create_patient.json | 6 +- .../create_practitioner_schedule.json | 6 +- .../explore_clinical_procedure_templates.json | 4 +- .../explore_healthcare_settings.json | 4 +- ...troduction_to_healthcare_practitioner.json | 6 +- .../page/patient_history/patient_history.css | 42 +- .../page/patient_history/patient_history.html | 38 +- .../page/patient_history/patient_history.js | 774 ++++++++++-------- .../patient_history_sidebar.html | 21 + .../patient_progress/patient_progress.css | 8 +- .../patient_progress/patient_progress.html | 13 +- .../page/patient_progress/patient_progress.js | 65 +- .../report/lab_test_report/lab_test_report.py | 2 +- erpnext/healthcare/utils.py | 165 ++-- .../workspace/healthcare/healthcare.json | 254 +++--- 26 files changed, 825 insertions(+), 651 deletions(-) create mode 100644 erpnext/healthcare/page/patient_history/patient_history_sidebar.html diff --git a/erpnext/healthcare/dashboard_chart/clinical_procedures/clinical_procedures.json b/erpnext/healthcare/dashboard_chart/clinical_procedures/clinical_procedures.json index a59f149ee5..6803528156 100644 --- a/erpnext/healthcare/dashboard_chart/clinical_procedures/clinical_procedures.json +++ b/erpnext/healthcare/dashboard_chart/clinical_procedures/clinical_procedures.json @@ -12,15 +12,15 @@ "idx": 0, "is_public": 1, "is_standard": 1, - "last_synced_on": "2020-07-22 13:22:47.008622", - "modified": "2020-07-22 13:36:48.114479", + "last_synced_on": "2021-01-30 21:03:30.086891", + "modified": "2021-02-01 13:36:04.469863", "modified_by": "Administrator", "module": "Healthcare", "name": "Clinical Procedures", "number_of_groups": 0, "owner": "Administrator", "timeseries": 0, - "type": "Percentage", + "type": "Bar", "use_report_chart": 0, "y_axis": [] } \ No newline at end of file diff --git a/erpnext/healthcare/dashboard_chart/clinical_procedures_status/clinical_procedures_status.json b/erpnext/healthcare/dashboard_chart/clinical_procedures_status/clinical_procedures_status.json index 6d560f74bf..dae9db19b8 100644 --- a/erpnext/healthcare/dashboard_chart/clinical_procedures_status/clinical_procedures_status.json +++ b/erpnext/healthcare/dashboard_chart/clinical_procedures_status/clinical_procedures_status.json @@ -12,15 +12,15 @@ "idx": 0, "is_public": 1, "is_standard": 1, - "last_synced_on": "2020-07-22 13:22:46.691764", - "modified": "2020-07-22 13:40:17.215775", + "last_synced_on": "2021-02-01 13:36:38.787783", + "modified": "2021-02-01 13:37:18.718275", "modified_by": "Administrator", "module": "Healthcare", "name": "Clinical Procedures Status", "number_of_groups": 0, "owner": "Administrator", "timeseries": 0, - "type": "Pie", + "type": "Bar", "use_report_chart": 0, "y_axis": [] } \ No newline at end of file diff --git a/erpnext/healthcare/dashboard_chart/diagnoses/diagnoses.json b/erpnext/healthcare/dashboard_chart/diagnoses/diagnoses.json index 0195aac8b7..82145d6024 100644 --- a/erpnext/healthcare/dashboard_chart/diagnoses/diagnoses.json +++ b/erpnext/healthcare/dashboard_chart/diagnoses/diagnoses.json @@ -5,21 +5,22 @@ "docstatus": 0, "doctype": "Dashboard Chart", "document_type": "Patient Encounter Diagnosis", + "dynamic_filters_json": "", "filters_json": "[]", "group_by_based_on": "diagnosis", "group_by_type": "Count", "idx": 0, "is_public": 1, "is_standard": 1, - "last_synced_on": "2020-07-22 13:22:47.895521", - "modified": "2020-07-22 13:43:32.369481", + "last_synced_on": "2021-01-30 21:03:33.729487", + "modified": "2021-02-01 13:34:57.385335", "modified_by": "Administrator", "module": "Healthcare", "name": "Diagnoses", "number_of_groups": 0, "owner": "Administrator", "timeseries": 0, - "type": "Percentage", + "type": "Bar", "use_report_chart": 0, "y_axis": [] } \ No newline at end of file diff --git a/erpnext/healthcare/dashboard_chart/lab_tests/lab_tests.json b/erpnext/healthcare/dashboard_chart/lab_tests/lab_tests.json index 052483533e..70293b158e 100644 --- a/erpnext/healthcare/dashboard_chart/lab_tests/lab_tests.json +++ b/erpnext/healthcare/dashboard_chart/lab_tests/lab_tests.json @@ -12,15 +12,15 @@ "idx": 0, "is_public": 1, "is_standard": 1, - "last_synced_on": "2020-07-22 13:22:47.344055", - "modified": "2020-07-22 13:37:34.490129", + "last_synced_on": "2021-01-30 21:03:28.272914", + "modified": "2021-02-01 13:36:08.391433", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Tests", "number_of_groups": 0, "owner": "Administrator", "timeseries": 0, - "type": "Percentage", + "type": "Bar", "use_report_chart": 0, "y_axis": [] } \ No newline at end of file diff --git a/erpnext/healthcare/dashboard_chart/symptoms/symptoms.json b/erpnext/healthcare/dashboard_chart/symptoms/symptoms.json index 8fc86a1c59..65e5472aa1 100644 --- a/erpnext/healthcare/dashboard_chart/symptoms/symptoms.json +++ b/erpnext/healthcare/dashboard_chart/symptoms/symptoms.json @@ -12,15 +12,15 @@ "idx": 0, "is_public": 1, "is_standard": 1, - "last_synced_on": "2020-07-22 13:22:47.296748", - "modified": "2020-07-22 13:40:59.655129", + "last_synced_on": "2021-01-30 21:03:32.067473", + "modified": "2021-02-01 13:35:30.953718", "modified_by": "Administrator", "module": "Healthcare", "name": "Symptoms", "number_of_groups": 0, "owner": "Administrator", "timeseries": 0, - "type": "Percentage", + "type": "Bar", "use_report_chart": 0, "y_axis": [] } \ No newline at end of file diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.py b/erpnext/healthcare/doctype/lab_test/lab_test.py index 4b57cd073d..74495a8591 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/lab_test.py @@ -34,7 +34,7 @@ class LabTest(Document): frappe.db.set_value('Lab Prescription', self.prescription, 'lab_test_created', 1) if frappe.db.get_value('Lab Prescription', self.prescription, 'invoiced'): self.invoiced = True - if not self.lab_test_name and self.template: + if self.template: self.load_test_from_template() self.reload() @@ -50,7 +50,7 @@ class LabTest(Document): item.secondary_uom_result = float(item.result_value) * float(item.conversion_factor) except: item.secondary_uom_result = '' - frappe.msgprint(_('Row #{0}: Result for Secondary UOM not calculated'.format(item.idx)), title = _('Warning')) + frappe.msgprint(_('Row #{0}: Result for Secondary UOM not calculated').format(item.idx), title = _('Warning')) def validate_result_values(self): if self.normal_test_items: @@ -229,9 +229,9 @@ def create_sample_doc(template, patient, invoice, company = None): sample_collection = frappe.get_doc('Sample Collection', sample_exists[0][0]) quantity = int(sample_collection.sample_qty) + int(template.sample_qty) if template.sample_details: - sample_details = sample_collection.sample_details + '\n-\n' + _('Test: ') + sample_details = sample_collection.sample_details + '\n-\n' + _('Test :') sample_details += (template.get('lab_test_name') or template.get('template')) + '\n' - sample_details += _('Collection Details: ') + '\n\t' + template.sample_details + sample_details += _('Collection Details:') + '\n\t' + template.sample_details frappe.db.set_value('Sample Collection', sample_collection.name, 'sample_details', sample_details) frappe.db.set_value('Sample Collection', sample_collection.name, 'sample_qty', quantity) diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index f5477c096a..36ef2d1623 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -66,7 +66,7 @@ class TestPatientAppointment(unittest.TestCase): medical_department = create_medical_department() frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) - appointment_type = create_appointment_type() + appointment_type = create_appointment_type({'medical_department': medical_department}) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2), invoice=1, appointment_type=appointment_type.name, department=medical_department) @@ -91,9 +91,9 @@ class TestPatientAppointment(unittest.TestCase): 'op_consulting_charge': 300 }] appointment_type = create_appointment_type(args={ - 'name': 'Generic Appointment Type charge', - 'items': items - }) + 'name': 'Generic Appointment Type charge', + 'items': items + }) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2), invoice=1, appointment_type=appointment_type.name) @@ -408,9 +408,9 @@ def create_appointment_type(args=None): else: item = create_healthcare_service_items() items = [{ - 'medical_department': '_Test Medical Department', - 'op_consulting_charge_item': item, - 'op_consulting_charge': 200 + 'medical_department': args.get('medical_department') or '_Test Medical Department', + 'op_consulting_charge_item': item, + 'op_consulting_charge': 200 }] return frappe.get_doc({ 'doctype': 'Appointment Type', diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py index 63b00859d7..9e0d3c3e27 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -18,7 +18,7 @@ class PatientHistorySettings(Document): def validate_submittable_doctypes(self): for entry in self.custom_doctypes: if not cint(frappe.db.get_value('DocType', entry.document_type, 'is_submittable')): - msg = _('Row #{0}: Document Type {1} is not submittable. ').format( + msg = _('Row #{0}: Document Type {1} is not submittable.').format( entry.idx, frappe.bold(entry.document_type)) msg += _('Patient Medical Record can only be created for submittable document types.') frappe.throw(msg) @@ -116,12 +116,12 @@ def set_subject_field(doc): fieldname = entry.get('fieldname') if entry.get('fieldtype') == 'Table' and doc.get(fieldname): formatted_value = get_formatted_value_for_table_field(doc.get(fieldname), meta.get_field(fieldname)) - subject += frappe.bold(_(entry.get('label')) + ': ') + '
' + cstr(formatted_value) + '
' + subject += frappe.bold(_(entry.get('label')) + ':') + '
' + cstr(formatted_value) + '
' else: if doc.get(fieldname): formatted_value = format_value(doc.get(fieldname), meta.get_field(fieldname), doc) - subject += frappe.bold(_(entry.get('label')) + ': ') + cstr(formatted_value) + '
' + subject += frappe.bold(_(entry.get('label')) + ':') + cstr(formatted_value) + '
' return subject diff --git a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py index 33119d8185..9169ea642b 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py @@ -38,13 +38,12 @@ class TestPatientHistorySettings(unittest.TestCase): # tests for medical record creation of standard doctypes in test_patient_medical_record.py patient = create_patient() doc = create_doc(patient) - # check for medical record medical_rec = frappe.db.exists("Patient Medical Record", {"status": "Open", "reference_name": doc.name}) self.assertTrue(medical_rec) medical_rec = frappe.get_doc("Patient Medical Record", medical_rec) - expected_subject = "Date: {0}Rating: 3Feedback: Test Patient History Settings".format( + expected_subject = "Date:{0}Rating:3Feedback:Test Patient History Settings".format( frappe.utils.format_date(getdate())) self.assertEqual(strip_html(medical_rec.subject), expected_subject) self.assertEqual(medical_rec.patient, patient) diff --git a/erpnext/healthcare/module_onboarding/healthcare/healthcare.json b/erpnext/healthcare/module_onboarding/healthcare/healthcare.json index 56c3c13559..0aa8f9a027 100644 --- a/erpnext/healthcare/module_onboarding/healthcare/healthcare.json +++ b/erpnext/healthcare/module_onboarding/healthcare/healthcare.json @@ -10,7 +10,7 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/healthcare", "idx": 0, "is_complete": 0, - "modified": "2020-07-08 14:06:19.512946", + "modified": "2021-01-30 19:22:20.273766", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare", diff --git a/erpnext/healthcare/onboarding_step/create_healthcare_practitioner/create_healthcare_practitioner.json b/erpnext/healthcare/onboarding_step/create_healthcare_practitioner/create_healthcare_practitioner.json index c45a347080..3f25a9d676 100644 --- a/erpnext/healthcare/onboarding_step/create_healthcare_practitioner/create_healthcare_practitioner.json +++ b/erpnext/healthcare/onboarding_step/create_healthcare_practitioner/create_healthcare_practitioner.json @@ -5,14 +5,14 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 1, "is_single": 0, "is_skipped": 0, - "modified": "2020-05-26 23:16:31.965521", + "modified": "2021-01-30 12:02:22.849260", "modified_by": "Administrator", "name": "Create Healthcare Practitioner", "owner": "Administrator", "reference_document": "Healthcare Practitioner", + "show_form_tour": 0, "show_full_form": 1, "title": "Create Healthcare Practitioner", "validate_action": 1 diff --git a/erpnext/healthcare/onboarding_step/create_patient/create_patient.json b/erpnext/healthcare/onboarding_step/create_patient/create_patient.json index 77bc5bd7ad..b46bb15b48 100644 --- a/erpnext/healthcare/onboarding_step/create_patient/create_patient.json +++ b/erpnext/healthcare/onboarding_step/create_patient/create_patient.json @@ -5,14 +5,14 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 1, "is_single": 0, "is_skipped": 0, - "modified": "2020-05-19 12:26:24.023418", - "modified_by": "Administrator", + "modified": "2021-01-30 00:09:28.786428", + "modified_by": "ruchamahabal2@gmail.com", "name": "Create Patient", "owner": "Administrator", "reference_document": "Patient", + "show_form_tour": 0, "show_full_form": 1, "title": "Create Patient", "validate_action": 1 diff --git a/erpnext/healthcare/onboarding_step/create_practitioner_schedule/create_practitioner_schedule.json b/erpnext/healthcare/onboarding_step/create_practitioner_schedule/create_practitioner_schedule.json index 65980ef668..7ce122d5c0 100644 --- a/erpnext/healthcare/onboarding_step/create_practitioner_schedule/create_practitioner_schedule.json +++ b/erpnext/healthcare/onboarding_step/create_practitioner_schedule/create_practitioner_schedule.json @@ -5,14 +5,14 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 1, "is_single": 0, "is_skipped": 0, - "modified": "2020-05-19 12:27:09.437825", - "modified_by": "Administrator", + "modified": "2021-01-30 00:09:28.794602", + "modified_by": "ruchamahabal2@gmail.com", "name": "Create Practitioner Schedule", "owner": "Administrator", "reference_document": "Practitioner Schedule", + "show_form_tour": 0, "show_full_form": 1, "title": "Create Practitioner Schedule", "validate_action": 1 diff --git a/erpnext/healthcare/onboarding_step/explore_clinical_procedure_templates/explore_clinical_procedure_templates.json b/erpnext/healthcare/onboarding_step/explore_clinical_procedure_templates/explore_clinical_procedure_templates.json index 697b761e52..dfe9f71a76 100644 --- a/erpnext/healthcare/onboarding_step/explore_clinical_procedure_templates/explore_clinical_procedure_templates.json +++ b/erpnext/healthcare/onboarding_step/explore_clinical_procedure_templates/explore_clinical_procedure_templates.json @@ -5,14 +5,14 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-05-26 23:10:24.504030", + "modified": "2021-01-30 19:22:08.257160", "modified_by": "Administrator", "name": "Explore Clinical Procedure Templates", "owner": "Administrator", "reference_document": "Clinical Procedure Template", + "show_form_tour": 0, "show_full_form": 0, "title": "Explore Clinical Procedure Templates", "validate_action": 1 diff --git a/erpnext/healthcare/onboarding_step/explore_healthcare_settings/explore_healthcare_settings.json b/erpnext/healthcare/onboarding_step/explore_healthcare_settings/explore_healthcare_settings.json index b2d5aef431..2d952f3093 100644 --- a/erpnext/healthcare/onboarding_step/explore_healthcare_settings/explore_healthcare_settings.json +++ b/erpnext/healthcare/onboarding_step/explore_healthcare_settings/explore_healthcare_settings.json @@ -5,14 +5,14 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 1, "is_single": 1, "is_skipped": 0, - "modified": "2020-05-26 23:10:24.507648", + "modified": "2021-01-30 19:22:07.275735", "modified_by": "Administrator", "name": "Explore Healthcare Settings", "owner": "Administrator", "reference_document": "Healthcare Settings", + "show_form_tour": 0, "show_full_form": 0, "title": "Explore Healthcare Settings", "validate_action": 1 diff --git a/erpnext/healthcare/onboarding_step/introduction_to_healthcare_practitioner/introduction_to_healthcare_practitioner.json b/erpnext/healthcare/onboarding_step/introduction_to_healthcare_practitioner/introduction_to_healthcare_practitioner.json index fa4c9036d7..baa8358c06 100644 --- a/erpnext/healthcare/onboarding_step/introduction_to_healthcare_practitioner/introduction_to_healthcare_practitioner.json +++ b/erpnext/healthcare/onboarding_step/introduction_to_healthcare_practitioner/introduction_to_healthcare_practitioner.json @@ -6,14 +6,14 @@ "field": "schedule", "idx": 0, "is_complete": 0, - "is_mandatory": 1, "is_single": 0, "is_skipped": 0, - "modified": "2020-05-26 22:07:07.482530", - "modified_by": "Administrator", + "modified": "2021-01-30 00:09:28.807129", + "modified_by": "ruchamahabal2@gmail.com", "name": "Introduction to Healthcare Practitioner", "owner": "Administrator", "reference_document": "Healthcare Practitioner", + "show_form_tour": 0, "show_full_form": 0, "title": "Introduction to Healthcare Practitioner", "validate_action": 0 diff --git a/erpnext/healthcare/page/patient_history/patient_history.css b/erpnext/healthcare/page/patient_history/patient_history.css index 1bb589164e..74b5e7eb91 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.css +++ b/erpnext/healthcare/page/patient_history/patient_history.css @@ -9,6 +9,26 @@ cursor: pointer; } +.patient-image-container { + margin-top: 17px; + } + +.patient-image { + display: inline-block; + width: 100%; + height: 0; + padding: 50% 0px; + background-size: cover; + background-repeat: no-repeat; + background-position: center center; + border-radius: 4px; +} + +.patient-name { + font-size: 20px; + margin-top: 25px; +} + .medical_record-label { max-width: 100px; margin-bottom: -4px; @@ -19,19 +39,19 @@ } .date-indicator { - background:none; - font-size:12px; - vertical-align:middle; - font-weight:bold; - color:#6c7680; + background:none; + font-size:12px; + vertical-align:middle; + font-weight:bold; + color:#6c7680; } .date-indicator::after { - margin:0 -4px 0 12px; - content:''; - display:inline-block; - height:8px; - width:8px; - border-radius:8px; + margin:0 -4px 0 12px; + content:''; + display:inline-block; + height:8px; + width:8px; + border-radius:8px; background: #d1d8dd; } diff --git a/erpnext/healthcare/page/patient_history/patient_history.html b/erpnext/healthcare/page/patient_history/patient_history.html index f1706557f4..d16b38637c 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.html +++ b/erpnext/healthcare/page/patient_history/patient_history.html @@ -1,26 +1,18 @@ -
-
-

-
+
+
+
+
+
+
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
- -
+ +
+
+
+
+
+
+
+
diff --git a/erpnext/healthcare/page/patient_history/patient_history.js b/erpnext/healthcare/page/patient_history/patient_history.js index 54343aae44..bf947cac21 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.js +++ b/erpnext/healthcare/page/patient_history/patient_history.js @@ -1,403 +1,455 @@ frappe.provide('frappe.patient_history'); frappe.pages['patient_history'].on_page_load = function(wrapper) { - let me = this; - let page = frappe.ui.make_app_page({ + frappe.ui.make_app_page({ parent: wrapper, - title: 'Patient History', - single_column: true + title: __('Patient History') }); - frappe.breadcrumbs.add('Healthcare'); - let pid = ''; - page.main.html(frappe.render_template('patient_history', {})); - page.main.find('.header-separator').hide(); - - let patient = frappe.ui.form.make_control({ - parent: page.main.find('.patient'), - df: { - fieldtype: 'Link', - options: 'Patient', - fieldname: 'patient', - placeholder: __('Select Patient'), - only_select: true, - change: function() { - let patient_id = patient.get_value(); - if (pid != patient_id && patient_id) { - me.start = 0; - me.page.main.find('.patient_documents_list').html(''); - setup_filters(patient_id, me); - get_documents(patient_id, me); - show_patient_info(patient_id, me); - show_patient_vital_charts(patient_id, me, 'bp', 'mmHg', 'Blood Pressure'); - } - pid = patient_id; - } - }, - }); - patient.refresh(); - - if (frappe.route_options) { - patient.set_value(frappe.route_options.patient); - } - - this.page.main.on('click', '.btn-show-chart', function() { - let btn_show_id = $(this).attr('data-show-chart-id'), pts = $(this).attr('data-pts'); - let title = $(this).attr('data-title'); - show_patient_vital_charts(patient.get_value(), me, btn_show_id, pts, title); - }); - - this.page.main.on('click', '.btn-more', function() { - let doctype = $(this).attr('data-doctype'), docname = $(this).attr('data-docname'); - if (me.page.main.find('.'+docname).parent().find('.document-html').attr('data-fetched') == '1') { - me.page.main.find('.'+docname).hide(); - me.page.main.find('.'+docname).parent().find('.document-html').show(); - } else { - if (doctype && docname) { - let exclude = ['patient', 'patient_name', 'patient_sex', 'encounter_date']; - frappe.call({ - method: 'erpnext.healthcare.utils.render_doc_as_html', - args:{ - doctype: doctype, - docname: docname, - exclude_fields: exclude - }, - freeze: true, - callback: function(r) { - if (r.message) { - me.page.main.find('.' + docname).hide(); - - me.page.main.find('.' + docname).parent().find('.document-html').html( - `${r.message.html} -
- - -
- `); - - me.page.main.find('.' + docname).parent().find('.document-html').show(); - me.page.main.find('.' + docname).parent().find('.document-html').attr('data-fetched', '1'); - } - } - }); - } - } - }); - - this.page.main.on('click', '.btn-less', function() { - let docname = $(this).attr('data-docname'); - me.page.main.find('.' + docname).parent().find('.document-id').show(); - me.page.main.find('.' + docname).parent().find('.document-html').hide(); - }); - me.start = 0; - me.page.main.on('click', '.btn-get-records', function() { - get_documents(patient.get_value(), me); + let patient_history = new PatientHistory(wrapper); + $(wrapper).bind('show', ()=> { + patient_history.show(); }); }; -let setup_filters = function(patient, me) { - $('.doctype-filter').empty(); - frappe.xcall( - 'erpnext.healthcare.page.patient_history.patient_history.get_patient_history_doctypes' - ).then(document_types => { - let doctype_filter = frappe.ui.form.make_control({ - parent: $('.doctype-filter'), +class PatientHistory { + constructor(wrapper) { + this.wrapper = $(wrapper); + this.page = wrapper.page; + this.sidebar = this.wrapper.find('.layout-side-section'); + this.main_section = this.wrapper.find('.layout-main-section'); + this.start = 0; + } + + show() { + frappe.breadcrumbs.add('Healthcare'); + this.sidebar.empty(); + + let me = this; + let patient = frappe.ui.form.make_control({ + parent: me.sidebar, df: { - fieldtype: 'MultiSelectList', - fieldname: 'document_type', - placeholder: __('Select Document Type'), - input_class: 'input-xs', + fieldtype: 'Link', + options: 'Patient', + fieldname: 'patient', + placeholder: __('Select Patient'), + only_select: true, change: () => { - me.start = 0; - me.page.main.find('.patient_documents_list').html(''); - get_documents(patient, me, doctype_filter.get_value(), date_range_field.get_value()); - }, - get_data: () => { - return document_types.map(document_type => { - return { - description: document_type, - value: document_type - }; - }); - }, + me.patient_id = ''; + if (me.patient_id != patient.get_value() && patient.get_value()) { + me.start = 0; + me.patient_id = patient.get_value(); + me.make_patient_profile(); + } + } } }); - doctype_filter.refresh(); + patient.refresh(); - $('.date-filter').empty(); - let date_range_field = frappe.ui.form.make_control({ - df: { - fieldtype: 'DateRange', - fieldname: 'date_range', - placeholder: __('Date Range'), - input_class: 'input-xs', - change: () => { - let selected_date_range = date_range_field.get_value(); - if (selected_date_range && selected_date_range.length === 2) { + if (frappe.route_options && !this.patient_id) { + patient.set_value(frappe.route_options.patient); + this.patient_id = frappe.route_options.patient; + } + + this.sidebar.find('[data-fieldname="patient"]').append('
'); + } + + make_patient_profile() { + this.page.set_title(__('Patient History')); + this.main_section.empty().append(frappe.render_template('patient_history')); + this.setup_filters(); + this.setup_documents(); + this.show_patient_info(); + this.setup_buttons(); + this.show_patient_vital_charts('bp', 'mmHg', 'Blood Pressure'); + } + + setup_filters() { + $('.doctype-filter').empty(); + let me = this; + + frappe.xcall( + 'erpnext.healthcare.page.patient_history.patient_history.get_patient_history_doctypes' + ).then(document_types => { + let doctype_filter = frappe.ui.form.make_control({ + parent: $('.doctype-filter'), + df: { + fieldtype: 'MultiSelectList', + fieldname: 'document_type', + placeholder: __('Select Document Type'), + change: () => { me.start = 0; me.page.main.find('.patient_documents_list').html(''); - get_documents(patient, me, doctype_filter.get_value(), selected_date_range); - } + this.setup_documents(doctype_filter.get_value(), date_range_field.get_value()); + }, + get_data: () => { + return document_types.map(document_type => { + return { + description: document_type, + value: document_type + }; + }); + }, } - }, - parent: $('.date-filter') + }); + doctype_filter.refresh(); + + $('.date-filter').empty(); + let date_range_field = frappe.ui.form.make_control({ + df: { + fieldtype: 'DateRange', + fieldname: 'date_range', + placeholder: __('Date Range'), + input_class: 'input-xs', + change: () => { + let selected_date_range = date_range_field.get_value(); + if (selected_date_range && selected_date_range.length === 2) { + me.start = 0; + me.page.main.find('.patient_documents_list').html(''); + this.setup_documents(doctype_filter.get_value(), date_range_field.get_value()); + } + } + }, + parent: $('.date-filter') + }); + date_range_field.refresh(); }); - date_range_field.refresh(); - }); -}; + } -let get_documents = function(patient, me, document_types="", selected_date_range="") { - let filters = { - name: patient, - start: me.start, - page_length: 20 - }; - if (document_types) - filters['document_types'] = document_types; - if (selected_date_range) - filters['date_range'] = selected_date_range; + setup_documents(document_types="", selected_date_range="") { + let filters = { + name: this.patient_id, + start: this.start, + page_length: 20 + }; + if (document_types) + filters['document_types'] = document_types; + if (selected_date_range) + filters['date_range'] = selected_date_range; - frappe.call({ - 'method': 'erpnext.healthcare.page.patient_history.patient_history.get_feed', - args: filters, - callback: function(r) { - let data = r.message; - if (data.length) { - add_to_records(me, data); - } else { - me.page.main.find('.patient_documents_list').append(` -
-

${__('No more records..')}

-
`); - me.page.main.find('.btn-get-records').hide(); + let me = this; + frappe.call({ + 'method': 'erpnext.healthcare.page.patient_history.patient_history.get_feed', + args: filters, + callback: function(r) { + let data = r.message; + if (data.length) { + me.add_to_records(data); + } else { + me.page.main.find('.patient_documents_list').append(` +
+

${__('No more records..')}

+
`); + me.page.main.find('.btn-get-records').hide(); + } } - } - }); -}; + }); + } -let add_to_records = function(me, data) { - let details = "
`; + } + } + + this.page.main.find('.patient_documents_list').append(details); + this.start += data.length; + + if (data.length === 20) { + this.page.main.find(".btn-get-records").show(); + } else { + this.page.main.find(".btn-get-records").hide(); + this.page.main.find(".patient_documents_list").append(` +
+

${__('No more records..')}

+
`); } } - details += ''; - me.page.main.find('.patient_documents_list').append(details); - me.start += data.length; + add_date_separator(data) { + let date = frappe.datetime.str_to_obj(data.communication_date); + let pdate = ''; + let diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), + frappe.datetime.obj_to_str(date)); - if (data.length === 20) { - me.page.main.find(".btn-get-records").show(); - } else { - me.page.main.find(".btn-get-records").hide(); - me.page.main.find(".patient_documents_list").append(` -
-

${__('No more records..')}

-
`); - } -}; - -let add_date_separator = function(data) { - let date = frappe.datetime.str_to_obj(data.communication_date); - let pdate = ''; - let diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), frappe.datetime.obj_to_str(date)); - - if (diff < 1) { - pdate = __('Today'); - } else if (diff < 2) { - pdate = __('Yesterday'); - } else { - pdate = __('on ') + frappe.datetime.global_date_format(date); - } - data.date_sep = pdate; - return data; -}; - -let show_patient_info = function(patient, me) { - frappe.call({ - 'method': 'erpnext.healthcare.doctype.patient.patient.get_patient_detail', - args: { - patient: patient - }, - callback: function(r) { - let data = r.message; - let details = ''; - if (data.image) { - details += `
`; - } - - details += ` ${data.patient_name}
${data.sex}`; - if (data.email) details += `
${data.email}`; - if (data.mobile) details += `
${data.mobile}`; - if (data.occupation) details += `

${__('Occupation')} : ${data.occupation}`; - if (data.blood_group) details += `
${__('Blood Group')} : ${data.blood_group}`; - if (data.allergies) details += `

${__('Allerigies')} : ${data.allergies.replace("\n", ", ")}`; - if (data.medication) details += `
${__('Medication')} : ${data.medication.replace("\n", ", ")}`; - if (data.alcohol_current_use) details += `

${__('Alcohol use')} : ${data.alcohol_current_use}`; - if (data.alcohol_past_use) details += `
${__('Alcohol past use')} : ${data.alcohol_past_use}`; - if (data.tobacco_current_use) details += `
${__('Tobacco use')} : ${data.tobacco_current_use}`; - if (data.tobacco_past_use) details += `
${__('Tobacco past use')} : ${data.tobacco_past_use}`; - if (data.medical_history) details += `

${__('Medical history')} : ${data.medical_history.replace("\n", ", ")}`; - if (data.surgical_history) details += `
${__('Surgical history')} : ${data.surgical_history.replace("\n", ", ")}`; - if (data.surrounding_factors) details += `

${__('Occupational hazards')} : ${data.surrounding_factors.replace("\n", ", ")}`; - if (data.other_risk_factors) details += `
${__('Other risk factors')} : ${data.other_risk_factors.replace("\n", ", ")}`; - if (data.patient_details) details += `

${__('More info')} : ${data.patient_details.replace("\n", ", ")}`; - - if (details) { - details = `
` + details + `
`; - } - me.page.main.find('.patient_details').html(details); + if (diff < 1) { + pdate = __('Today'); + } else if (diff < 2) { + pdate = __('Yesterday'); + } else { + pdate = __('on {0}', [frappe.datetime.global_date_format(date)]); } - }); -}; + data.date_sep = pdate; + return data; + } -let show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) { - frappe.call({ - method: 'erpnext.healthcare.utils.get_patient_vitals', - args:{ - patient: patient - }, - callback: function(r) { - if (r.message) { - let show_chart_btns_html = ` - `; + show_patient_info() { + this.get_patient_info().then(() => { + $('.patient-info').empty().append(frappe.render_template('patient_history_sidebar', { + patient_image: this.patient.image, + patient_name: this.patient.patient_name, + patient_gender: this.patient.sex, + patient_mobile: this.patient.mobile + })); + this.show_patient_details(); + }); + } - me.page.main.find('.show_chart_btns').html(show_chart_btns_html); + show_patient_details() { + let me = this; + frappe.call({ + 'method': 'erpnext.healthcare.doctype.patient.patient.get_patient_detail', + args: { + patient: me.patient_id + }, + callback: function(r) { let data = r.message; - let labels = [], datasets = []; - let bp_systolic = [], bp_diastolic = [], temperature = []; - let pulse = [], respiratory_rate = [], bmi = [], height = [], weight = []; + let details = ``; - for (let i=0; i
${__('Occupation')} : ${data.occupation}`; + if (data.blood_group) details += `
${__('Blood Group')} : ${data.blood_group}`; + if (data.allergies) details += `

${__('Allerigies')} : ${data.allergies.replace("\n", ", ")}`; + if (data.medication) details += `
${__('Medication')} : ${data.medication.replace("\n", ", ")}`; + if (data.alcohol_current_use) details += `

${__('Alcohol use')} : ${data.alcohol_current_use}`; + if (data.alcohol_past_use) details += `
${__('Alcohol past use')} : ${data.alcohol_past_use}`; + if (data.tobacco_current_use) details += `
${__('Tobacco use')} : ${data.tobacco_current_use}`; + if (data.tobacco_past_use) details += `
${__('Tobacco past use')} : ${data.tobacco_past_use}`; + if (data.medical_history) details += `

${__('Medical history')} : ${data.medical_history.replace("\n", ", ")}`; + if (data.surgical_history) details += `
${__('Surgical history')} : ${data.surgical_history.replace("\n", ", ")}`; + if (data.surrounding_factors) details += `

${__('Occupational hazards')} : ${data.surrounding_factors.replace("\n", ", ")}`; + if (data.other_risk_factors) details += `
${__('Other risk factors')} : ${data.other_risk_factors.replace("\n", ", ")}`; + if (data.patient_details) details += `

${__('More info')} : ${data.patient_details.replace("\n", ", ")}`; - if (btn_show_id === 'bp') { - bp_systolic.push(data[i].bp_systolic); - bp_diastolic.push(data[i].bp_diastolic); - } - if (btn_show_id === 'temperature') { - temperature.push(data[i].temperature); - } - if (btn_show_id === 'pulse_rate') { - pulse.push(data[i].pulse); - respiratory_rate.push(data[i].respiratory_rate); - } - if (btn_show_id === 'bmi') { - bmi.push(data[i].bmi); - height.push(data[i].height); - weight.push(data[i].weight); - } + if (details) { + details = `
` + details + `
`; } - if (btn_show_id === 'temperature') { - datasets.push({name: 'Temperature', values: temperature, chartType: 'line'}); - } - if (btn_show_id === 'bmi') { - datasets.push({name: 'BMI', values: bmi, chartType: 'line'}); - datasets.push({name: 'Height', values: height, chartType: 'line'}); - datasets.push({name: 'Weight', values: weight, chartType: 'line'}); - } - if (btn_show_id === 'bp') { - datasets.push({name: 'BP Systolic', values: bp_systolic, chartType: 'line'}); - datasets.push({name: 'BP Diastolic', values: bp_diastolic, chartType: 'line'}); - } - if (btn_show_id === 'pulse_rate') { - datasets.push({name: 'Heart Rate / Pulse', values: pulse, chartType: 'line'}); - datasets.push({name: 'Respiratory Rate', values: respiratory_rate, chartType: 'line'}); - } - new frappe.Chart('.patient_vital_charts', { - data: { - labels: labels, - datasets: datasets - }, - title: title, - type: 'axis-mixed', - height: 200, - colors: ['purple', '#ffa3ef', 'light-blue'], - - tooltipOptions: { - formatTooltipX: d => (d + '').toUpperCase(), - formatTooltipY: d => d + ' ' + pts, - } - }); - me.page.main.find('.header-separator').show(); - } else { - me.page.main.find('.patient_vital_charts').html(''); - me.page.main.find('.show_chart_btns').html(''); - me.page.main.find('.header-separator').hide(); + me.sidebar.find('.patient-details').html(details); } - } - }); -}; + }); + } + + get_patient_info() { + return frappe.xcall('frappe.client.get', { + doctype: 'Patient', + name: this.patient_id, + }).then((patient) => { + if (patient) { + this.patient = patient; + } + }); + } + + setup_buttons() { + let me = this; + this.page.main.on("click", ".btn-show-chart", function() { + let btn_id = $(this).attr("data-show-chart-id"), scale_unit = $(this).attr("data-pts"); + let title = $(this).attr("data-title"); + me.show_patient_vital_charts(btn_id, scale_unit, title); + }); + + this.page.main.on('click', '.btn-more', function() { + let doctype = $(this).attr('data-doctype'), docname = $(this).attr('data-docname'); + if (me.page.main.find('.'+docname).parent().find('.document-html').attr('data-fetched') == '1') { + me.page.main.find('.'+docname).hide(); + me.page.main.find('.'+docname).parent().find('.document-html').show(); + } else { + if (doctype && docname) { + let exclude = ['patient', 'patient_name', 'patient_sex', 'encounter_date', 'naming_series']; + frappe.call({ + method: 'erpnext.healthcare.utils.render_doc_as_html', + args: { + doctype: doctype, + docname: docname, + exclude_fields: exclude + }, + freeze: true, + callback: function(r) { + if (r.message) { + me.page.main.find('.' + docname).hide(); + + me.page.main.find('.' + docname).parent().find('.document-html').html( + `${r.message.html} +
+
+ + +
+ `); + + me.page.main.find('.' + docname).parent().find('.document-html').attr('hidden', false); + me.page.main.find('.' + docname).parent().find('.document-html').attr('data-fetched', '1'); + } + } + }); + } + } + }); + + this.page.main.on('click', '.btn-less', function() { + let docname = $(this).attr('data-docname'); + me.page.main.find('.' + docname).parent().find('.document-id').show(); + me.page.main.find('.' + docname).parent().find('.document-html').hide(); + }); + + me.page.main.on('click', '.btn-get-records', function() { + this.setup_documents(); + }); + } + + show_patient_vital_charts(btn_id, scale_unit, title) { + let me = this; + + frappe.call({ + method: 'erpnext.healthcare.utils.get_patient_vitals', + args: { + patient: me.patient_id + }, + callback: function(r) { + if (r.message) { + let show_chart_btns_html = ` + `; + + me.page.main.find('.show_chart_btns').html(show_chart_btns_html); + let data = r.message; + let labels = [], datasets = []; + let bp_systolic = [], bp_diastolic = [], temperature = []; + let pulse = [], respiratory_rate = [], bmi = [], height = [], weight = []; + + for (let i=0; i (d + '').toUpperCase(), + formatTooltipY: d => d + ' ' + scale_unit, + } + }); + me.page.main.find('.header-separator').show(); + } else { + me.page.main.find('.patient_vital_charts').html(''); + me.page.main.find('.show_chart_btns').html(''); + me.page.main.find('.header-separator').hide(); + } + } + }); + } +} \ No newline at end of file diff --git a/erpnext/healthcare/page/patient_history/patient_history_sidebar.html b/erpnext/healthcare/page/patient_history/patient_history_sidebar.html new file mode 100644 index 0000000000..4560e7e125 --- /dev/null +++ b/erpnext/healthcare/page/patient_history/patient_history_sidebar.html @@ -0,0 +1,21 @@ +
+
+ {% if patient_image %} +
+ {% endif %} +
+
+ {% if patient_name %} +

{{patient_name}}

+ {% endif %} + {% if patient_gender %} +

{%=__("Gender: ") %} {{patient_gender}}

+ {% endif %} + {% if patient_mobile %} +

{%=__("Contact: ") %} {{patient_mobile}}

+ {% endif %} +
+
+
+
+ diff --git a/erpnext/healthcare/page/patient_progress/patient_progress.css b/erpnext/healthcare/page/patient_progress/patient_progress.css index 5d85a7487f..737b2e0ea2 100644 --- a/erpnext/healthcare/page/patient_progress/patient_progress.css +++ b/erpnext/healthcare/page/patient_progress/patient_progress.css @@ -29,6 +29,7 @@ .patient-name { font-size: 20px; + margin-top: 25px; } /* heatmap */ @@ -55,6 +56,7 @@ } .heatmap-container .chart-filter { + z-index: 1; position: relative; top: 5px; margin-right: 10px; @@ -111,10 +113,13 @@ text.title { } .chart-column-container { - border-bottom: 1px solid #d1d8dd; margin: 5px 0; } +.progress-graphs .progress-container { + margin-bottom: var(--margin-xl); +} + .line-chart-container .frappe-chart { margin-top: -20px; } @@ -146,6 +151,7 @@ text.title { } .percentage-chart-container .chart-filter { + z-index: 1; position: relative; top: 12px; margin-right: 10px; diff --git a/erpnext/healthcare/page/patient_progress/patient_progress.html b/erpnext/healthcare/page/patient_progress/patient_progress.html index 30064bd165..ee60065618 100644 --- a/erpnext/healthcare/page/patient_progress/patient_progress.html +++ b/erpnext/healthcare/page/patient_progress/patient_progress.html @@ -1,14 +1,15 @@
-