From 5d5f026a0d4ac2d088b722f254f7b58db27b8a4f Mon Sep 17 00:00:00 2001 From: Anupam K Date: Fri, 24 Jul 2020 02:32:45 +0530 Subject: [PATCH 001/175] fix: Target variance report signs --- .../item_group_wise_sales_target_variance.py | 20 +++++++++---------- ...ner_target_variance_based_on_item_group.js | 17 +++++++++++++++- ...son_target_variance_based_on_item_group.js | 17 +++++++++++++++- ...ory_target_variance_based_on_item_group.js | 17 +++++++++++++++- 4 files changed, 58 insertions(+), 13 deletions(-) diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py index 857b9823e0..ae216ca5d6 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py @@ -63,13 +63,13 @@ def get_columns(filters, period_list, partner_doctype): "label": _(partner_doctype), "fieldtype": "Link", "options": partner_doctype, - "width": 100 + "width": 150 }, { "fieldname": "item_group", "label": _("Item Group"), "fieldtype": "Link", "options": "Item Group", - "width": 100 + "width": 150 }] for period in period_list: @@ -81,19 +81,19 @@ def get_columns(filters, period_list, partner_doctype): "label": _("Target ({})").format(period.label), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }, { "fieldname": period.key, "label": _("Achieved ({})").format(period.label), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }, { "fieldname": variance_key, "label": _("Variance ({})").format(period.label), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }]) columns.extend([{ @@ -101,19 +101,19 @@ def get_columns(filters, period_list, partner_doctype): "label": _("Total Target"), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }, { "fieldname": "total_achieved", "label": _("Total Achieved"), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }, { "fieldname": "total_variance", "label": _("Total Variance"), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }]) return columns @@ -154,10 +154,10 @@ def prepare_data(filters, sales_users_data, actual_data, date_field, period_list if (r.get(sales_field) == d.parent and r.item_group == d.item_group and period.from_date <= r.get(date_field) and r.get(date_field) <= period.to_date): details[p_key] += r.get(qty_or_amount_field, 0) - details[variance_key] = details.get(target_key) - details.get(p_key) + details[variance_key] = details.get(p_key) - details.get(target_key) details["total_achieved"] += details.get(p_key) - details["total_variance"] = details.get("total_target") - details.get("total_achieved") + details["total_variance"] = details.get("total_achieved") - details.get("total_target") return rows diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js index f99f68c524..38bb127e23 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js @@ -44,5 +44,20 @@ frappe.query_reports["Sales Partner Target Variance based on Item Group"] = { options: "Quantity\nAmount", default: "Quantity" }, - ] + ], + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname.includes('variance')) { + + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } + else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } + + return value; + } } diff --git a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js index 9f6bfc41df..a8e2fad373 100644 --- a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js +++ b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js @@ -44,5 +44,20 @@ frappe.query_reports["Sales Person Target Variance Based On Item Group"] = { options: "Quantity\nAmount", default: "Quantity" }, - ] + ], + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname.includes('variance')) { + + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } + else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } + + return value; + } } diff --git a/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js b/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js index dd9607ffbd..263391a7f7 100644 --- a/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js +++ b/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js @@ -44,5 +44,20 @@ frappe.query_reports["Territory Target Variance Based On Item Group"] = { options: "Quantity\nAmount", default: "Quantity" }, - ] + ], + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname.includes('variance')) { + + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } + else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } + + return value; + } } From 8718daa11e11c17ec64d550df661b3f3ac7c2eac Mon Sep 17 00:00:00 2001 From: Anupam K Date: Sat, 25 Jul 2020 23:56:11 +0530 Subject: [PATCH 002/175] Adding formatter in budget variance report --- .../budget_variance_report.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js index 3ec4d306c3..30415d1e65 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js @@ -71,7 +71,22 @@ frappe.query_reports["Budget Variance Report"] = { fieldtype: "Check", default: 0, }, - ] + ], + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname.includes('variance')) { + + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } + else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } + + return value; + } } erpnext.dimension_filters.forEach((dimension) => { From 9ceeefbd2b19eae898db07e7bc1ec90b7b40611b Mon Sep 17 00:00:00 2001 From: Anupam K Date: Mon, 27 Jul 2020 19:56:07 +0530 Subject: [PATCH 003/175] refactor: CRM Reports --- .../crm/report/lead_details/lead_details.js | 52 ++++++ .../crm/report/lead_details/lead_details.json | 4 +- .../crm/report/lead_details/lead_details.py | 164 ++++++++++++++++++ 3 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 erpnext/crm/report/lead_details/lead_details.js create mode 100644 erpnext/crm/report/lead_details/lead_details.py diff --git a/erpnext/crm/report/lead_details/lead_details.js b/erpnext/crm/report/lead_details/lead_details.js new file mode 100644 index 0000000000..f92070daf3 --- /dev/null +++ b/erpnext/crm/report/lead_details/lead_details.js @@ -0,0 +1,52 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Lead Details"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, + { + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -12), + "reqd": 1 + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.get_today(), + "reqd": 1 + }, + { + "fieldname":"status", + "label": __("Status"), + "fieldtype": "Select", + options: [ + { "value": "Lead", "label": __("Lead") }, + { "value": "Open", "label": __("Open") }, + { "value": "Replied", "label": __("Replied") }, + { "value": "Opportunity", "label": __("Opportunity") }, + { "value": "Quotation", "label": __("Quotation") }, + { "value": "Lost Quotation", "label": __("Lost Quotation") }, + { "value": "Interested", "label": __("Interested") }, + { "value": "Converted", "label": __("Converted") }, + { "value": "Do Not Contact", "label": __("Do Not Contact") }, + ], + }, + { + "fieldname":"territory", + "label": __("Territory"), + "fieldtype": "Link", + "options": "Territory", + } + ] +}; \ No newline at end of file diff --git a/erpnext/crm/report/lead_details/lead_details.json b/erpnext/crm/report/lead_details/lead_details.json index cdeb6bbe38..6ba7305dc7 100644 --- a/erpnext/crm/report/lead_details/lead_details.json +++ b/erpnext/crm/report/lead_details/lead_details.json @@ -7,7 +7,7 @@ "doctype": "Report", "idx": 3, "is_standard": "Yes", - "modified": "2020-01-22 16:51:56.591110", + "modified": "2020-07-26 23:59:49.897577", "modified_by": "Administrator", "module": "CRM", "name": "Lead Details", @@ -16,7 +16,7 @@ "query": "SELECT\n `tabLead`.name as \"Lead Id:Link/Lead:120\",\n `tabLead`.lead_name as \"Lead Name::120\",\n\t`tabLead`.company_name as \"Company Name::120\",\n\t`tabLead`.status as \"Status::120\",\n\tconcat_ws(', ', \n\t\ttrim(',' from `tabAddress`.address_line1), \n\t\ttrim(',' from tabAddress.address_line2)\n\t) as 'Address::180',\n\t`tabAddress`.state as \"State::100\",\n\t`tabAddress`.pincode as \"Pincode::70\",\n\t`tabAddress`.country as \"Country::100\",\n\t`tabLead`.phone as \"Phone::100\",\n\t`tabLead`.mobile_no as \"Mobile No::100\",\n\t`tabLead`.email_id as \"Email Id::120\",\n\t`tabLead`.lead_owner as \"Lead Owner::120\",\n\t`tabLead`.source as \"Source::120\",\n\t`tabLead`.territory as \"Territory::120\",\n\t`tabLead`.notes as \"Notes::360\",\n `tabLead`.owner as \"Owner:Link/User:120\"\nFROM\n\t`tabLead`\n\tleft join `tabDynamic Link` on (\n\t\t`tabDynamic Link`.link_name=`tabLead`.name \n\t\tand `tabDynamic Link`.parenttype = 'Address'\n\t)\n\tleft join `tabAddress` on (\n\t\t`tabAddress`.name=`tabDynamic Link`.parent\n\t)\nWHERE\n\t`tabLead`.docstatus<2\nORDER BY\n\t`tabLead`.name asc", "ref_doctype": "Lead", "report_name": "Lead Details", - "report_type": "Query Report", + "report_type": "Script Report", "roles": [ { "role": "Sales User" diff --git a/erpnext/crm/report/lead_details/lead_details.py b/erpnext/crm/report/lead_details/lead_details.py new file mode 100644 index 0000000000..5b1849a256 --- /dev/null +++ b/erpnext/crm/report/lead_details/lead_details.py @@ -0,0 +1,164 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from frappe import _ +import frappe + +def execute(filters=None): + columns, data = get_columns(), get_leads(filters) + return columns, data + +def get_columns(): + columns = [ + { + "label": _("Lead"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Lead", + "width": 150, + }, + { + "label": _("Lead Name"), + "fieldname": "lead_name", + "fieldtype": "Data", + "width": 120 + }, + { + "fieldname":"status", + "label": _("Status"), + "fieldtype": "Data", + "width": 100 + }, + { + "fieldname":"lead_owner", + "label": _("Lead Owner"), + "fieldtype": "Link", + "options": "User", + "width": 100 + }, + { + "label": _("Territory"), + "fieldname": "territory", + "fieldtype": "Link", + "options": "Territory", + "width": 100 + }, + { + "label": _("Source"), + "fieldname": "source", + "fieldtype": "Data", + "width": 120 + }, + { + "label": _("Email"), + "fieldname": "email_id", + "fieldtype": "Data", + "width": 120 + }, + { + "label": _("Mobile"), + "fieldname": "mobile_no", + "fieldtype": "Data", + "width": 120 + }, + { + "label": _("Phone"), + "fieldname": "phone", + "fieldtype": "Data", + "width": 120 + }, + { + "label": _("Notes"), + "fieldname": "notes", + "fieldtype": "Data", + "width": 120 + }, + { + "label": _("Owner"), + "fieldname": "owner", + "fieldtype": "Link", + "options": "user", + "width": 120 + }, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 120 + }, + { + "fieldname":"address", + "label": _("Address"), + "fieldtype": "Data", + "width": 130 + }, + { + "fieldname":"state", + "label": _("State"), + "fieldtype": "Data", + "width": 100 + }, + { + "fieldname":"pincode", + "label": _("Postal Code"), + "fieldtype": "Data", + "width": 90 + }, + { + "fieldname":"country", + "label": _("Country"), + "fieldtype": "Link", + "options": "Country", + "width": 100 + }, + + ] + return columns + +def get_leads(filters): + return frappe.db.sql(""" + SELECT + `tabLead`.name, + `tabLead`.lead_name, + `tabLead`.status, + `tabLead`.lead_owner, + `tabLead`.territory, + `tabLead`.source, + `tabLead`.email_id, + `tabLead`.mobile_no, + `tabLead`.phone, + `tabLead`.notes, + `tabLead`.owner, + `tabLead`.company, + concat_ws(', ', + trim(',' from `tabAddress`.address_line1), + trim(',' from tabAddress.address_line2) + ) AS address, + `tabAddress`.state, + `tabAddress`.pincode, + `tabAddress`.country + FROM + `tabLead` left join `tabDynamic Link` on ( + `tabLead`.name = `tabDynamic Link`.link_name and + `tabDynamic Link`.parenttype = 'Address') + left join `tabAddress` on ( + `tabAddress`.name=`tabDynamic Link`.parent) + WHERE + company = %(company)s + AND `tabLead`.creation BETWEEN %(from_date)s AND %(to_date)s + {conditions} + ORDER BY + `tabLead`.creation asc """.format(conditions=get_conditions(filters)), filters, as_dict=1) + +def get_conditions(filters) : + conditions = [] + + if filters.get("territory"): + conditions.append("territory=%(territory)s") + + if filters.get("status"): + conditions.append("status=%(status)s") + + return " and {}".format(" and ".join(conditions)) if conditions else "" From 6bab21483c947b8d53afc1d142a5afc6f81a5cf4 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Thu, 30 Jul 2020 00:41:06 +0530 Subject: [PATCH 004/175] report lost opportunity --- .../crm/report/lead_details/lead_details.py | 4 +- .../lost_opportunity/lost_opportunity.js | 41 +++++++++ .../lost_opportunity/lost_opportunity.json | 5 +- .../lost_opportunity/lost_opportunity.py | 86 +++++++++++++++++++ 4 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 erpnext/crm/report/lost_opportunity/lost_opportunity.js create mode 100644 erpnext/crm/report/lost_opportunity/lost_opportunity.py diff --git a/erpnext/crm/report/lead_details/lead_details.py b/erpnext/crm/report/lead_details/lead_details.py index 5b1849a256..079f32e040 100644 --- a/erpnext/crm/report/lead_details/lead_details.py +++ b/erpnext/crm/report/lead_details/lead_details.py @@ -6,7 +6,7 @@ from frappe import _ import frappe def execute(filters=None): - columns, data = get_columns(), get_leads(filters) + columns, data = get_columns(), get_data(filters) return columns, data def get_columns(): @@ -117,7 +117,7 @@ def get_columns(): ] return columns -def get_leads(filters): +def get_data(filters): return frappe.db.sql(""" SELECT `tabLead`.name, diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.js b/erpnext/crm/report/lost_opportunity/lost_opportunity.js new file mode 100644 index 0000000000..c6bf888da3 --- /dev/null +++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.js @@ -0,0 +1,41 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Lost Opportunity"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, + { + "fieldname":"opportunity_from", + "label": __("Opportunity From"), + "fieldtype": "Link", + "options": "DocType", + "get_query": function() { + return { + "filters": { + "name": ["in", ["Customer", "Lead"]], + } + } + } + }, + { + "fieldname":"party_name", + "label": __("Party"), + "fieldtype": "Dynamic Link", + "options": "opportunity_from" + }, + { + "fieldname":"contact_by", + "label": __("Next Contact By"), + "fieldtype": "Link", + "options": "User" + }, + ] +}; \ No newline at end of file diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.json b/erpnext/crm/report/lost_opportunity/lost_opportunity.json index e7c5068b86..e7a8e12ba7 100644 --- a/erpnext/crm/report/lost_opportunity/lost_opportunity.json +++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.json @@ -1,13 +1,14 @@ { "add_total_row": 0, "creation": "2018-12-31 16:30:57.188837", + "disable_prepared_report": 0, "disabled": 0, "docstatus": 0, "doctype": "Report", "idx": 0, "is_standard": "Yes", "json": "{\"order_by\": \"`tabOpportunity`.`modified` desc\", \"filters\": [[\"Opportunity\", \"status\", \"=\", \"Lost\"]], \"fields\": [[\"name\", \"Opportunity\"], [\"opportunity_from\", \"Opportunity\"], [\"party_name\", \"Opportunity\"], [\"customer_name\", \"Opportunity\"], [\"opportunity_type\", \"Opportunity\"], [\"status\", \"Opportunity\"], [\"contact_by\", \"Opportunity\"], [\"docstatus\", \"Opportunity\"], [\"lost_reason\", \"Lost Reason Detail\"]], \"add_totals_row\": 0, \"add_total_row\": 0, \"page_length\": 20}", - "modified": "2019-06-26 16:33:08.083618", + "modified": "2020-07-29 15:49:02.848845", "modified_by": "Administrator", "module": "CRM", "name": "Lost Opportunity", @@ -15,7 +16,7 @@ "prepared_report": 0, "ref_doctype": "Opportunity", "report_name": "Lost Opportunity", - "report_type": "Report Builder", + "report_type": "Script Report", "roles": [ { "role": "Sales User" diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.py b/erpnext/crm/report/lost_opportunity/lost_opportunity.py new file mode 100644 index 0000000000..75c0b2d981 --- /dev/null +++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.py @@ -0,0 +1,86 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from frappe import _ +import frappe + +def execute(filters=None): + columns, data = get_columns(), get_data(filters) + return columns, data + +def get_columns(): + columns = [ + { + "label": _("Opportunity"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Opportunity", + "width": 170, + }, + { + "label": _("Opportunity From"), + "fieldname": "opportunity_from", + "fieldtype": "Link", + "options": "DocType", + "width": 130 + }, + { + "label": _("Party"), + "fieldname":"party_name", + "fieldtype": "Dynamic Link", + "options": "opportunity_from", + "width": 160 + }, + { + "label": _("Customer/Lead Name"), + "fieldname":"customer_name", + "fieldtype": "Data", + "width": 150 + }, + { + "label": _("Opportunity Type"), + "fieldname": "opportunity_type", + "fieldtype": "Data", + "width": 130 + }, + { + "label": _("Next Contact By"), + "fieldname": "contact_by", + "fieldtype": "Link", + "options": "User", + "width": 120 + } + ] + return columns + +def get_data(filters): + return frappe.db.sql(""" + SELECT + `tabOpportunity`.name, + `tabOpportunity`.opportunity_from, + `tabOpportunity`.party_name, + `tabOpportunity`.customer_name, + `tabOpportunity`.opportunity_type, + `tabOpportunity`.contact_by + FROM + `tabOpportunity` + WHERE + status = 'Lost' and company = %(company)s + {conditions} + ORDER BY + creation asc """.format(conditions=get_conditions(filters)), filters, as_dict=1) + +def get_conditions(filters) : + conditions = [] + + if filters.get("opportunity_from"): + conditions.append("opportunity_from=%(opportunity_from)s") + + if filters.get("party_name"): + conditions.append("party_name=%(party_name)s") + + if filters.get("contact_by"): + conditions.append("contact_by=%(contact_by)s") + + return " and {}".format(" and ".join(conditions)) if conditions else "" From 440c7525292a804995d9decc48b76310a793a480 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 30 Jul 2020 12:16:18 +0530 Subject: [PATCH 005/175] chore: Lab Test Template form clean-up --- .../lab_test_template/lab_test_template.json | 8 ++-- .../lab_test_template/lab_test_template.py | 37 ++++++++++--------- .../lab_test_template_list.js | 2 +- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json index db64297269..fc6a1e1790 100644 --- a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json @@ -112,7 +112,7 @@ { "default": "1", "depends_on": "eval:doc.lab_test_template_type != 'Grouped'", - "description": "If unchecked, the item wont be appear in Sales Invoice, but can be used in group test creation. ", + "description": "If unchecked, the item will not be available in Sales Invoices for billing but can be used in group test creation. ", "fieldname": "is_billable", "fieldtype": "Check", "label": "Is Billable", @@ -184,7 +184,7 @@ "depends_on": "eval:doc.lab_test_template_type == 'Descriptive'", "fieldname": "section_break_special", "fieldtype": "Section Break", - "label": "Descriptive" + "label": "Descriptive Test" }, { "default": "0", @@ -196,7 +196,7 @@ "depends_on": "eval:doc.lab_test_template_type == 'Grouped'", "fieldname": "section_break_group", "fieldtype": "Section Break", - "label": "Group" + "label": "Group Tests" }, { "fieldname": "lab_test_groups", @@ -314,7 +314,7 @@ } ], "links": [], - "modified": "2020-07-13 12:57:09.925436", + "modified": "2020-07-30 11:55:43.093828", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Test Template", diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py index 6f0d08cf85..e4fbdd9d86 100644 --- a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py @@ -15,7 +15,8 @@ class LabTestTemplate(Document): def validate(self): if self.is_billable and (not self.lab_test_rate or self.lab_test_rate <= 0.0): - frappe.throw(_("Standard Selling Rate should be greater than zero.")) + frappe.throw(_('Standard Selling Rate should be greater than zero.')) + self.validate_conversion_factor() self.enable_disable_item() @@ -42,7 +43,9 @@ class LabTestTemplate(Document): # Remove template reference from item and disable item if self.item: try: - frappe.delete_doc('Item', self.item) + item = self.item + self.db_set('item', '') + frappe.delete_doc('Item', item) except Exception: frappe.throw(_('Not permitted. Please disable the Lab Test Template')) @@ -63,26 +66,26 @@ class LabTestTemplate(Document): 'standard_rate': self.lab_test_rate, 'description': self.lab_test_description }) - item.save() + item.flags.ignore_mandatory = True + item.save(ignore_permissions=True) def item_price_exists(self): item_price = frappe.db.exists({'doctype': 'Item Price', 'item_code': self.lab_test_code}) if item_price: return item_price[0][0] - else: - return False + return False def validate_conversion_factor(self): - if self.lab_test_template_type == "Single" and self.secondary_uom and not self.conversion_factor: - frappe.throw(_("Conversion Factor is mandatory")) - if self.lab_test_template_type == "Compound": + if self.lab_test_template_type == 'Single' and self.secondary_uom and not self.conversion_factor: + frappe.throw(_('Conversion Factor is mandatory')) + if self.lab_test_template_type == 'Compound': for item in self.normal_test_templates: if item.secondary_uom and not item.conversion_factor: - frappe.throw(_("Conversion Factor is mandatory")) - if self.lab_test_template_type == "Grouped": + frappe.throw(_('Conversion Factor is mandatory')) + if self.lab_test_template_type == 'Grouped': for group in self.lab_test_groups: - if group.template_or_new_line == "Add New Line" and group.secondary_uom and not group.conversion_factor: - frappe.throw(_("Conversion Factor is mandatory")) + if group.template_or_new_line == 'Add New Line' and group.secondary_uom and not group.conversion_factor: + frappe.throw(_('Conversion Factor is mandatory')) def create_item_from_template(doc): @@ -101,9 +104,9 @@ def create_item_from_template(doc): 'include_item_in_manufacturing': 0, 'show_in_website': 0, 'is_pro_applicable': 0, - 'disabled': 0 if doc.is_billable and not doc.disabled else doc.disabled, + 'disabled': 0 if doc.is_billable and not doc.disabled else doc.disabled, 'stock_uom': uom - }).insert(ignore_permissions = True, ignore_mandatory = True) + }).insert(ignore_permissions=True, ignore_mandatory=True) # Insert item price if doc.is_billable and doc.lab_test_rate != 0.0: @@ -123,7 +126,7 @@ def make_item_price(item, price_list_name, item_price): 'price_list': price_list_name, 'item_code': item, 'price_list_rate': item_price - }).insert(ignore_permissions = True, ignore_mandatory = True) + }).insert(ignore_permissions=True, ignore_mandatory=True) @frappe.whitelist() def change_test_code_from_template(lab_test_code, doc): @@ -132,8 +135,8 @@ def change_test_code_from_template(lab_test_code, doc): if frappe.db.exists({'doctype': 'Item', 'item_code': lab_test_code}): frappe.throw(_('Lab Test Item {0} already exist').format(lab_test_code)) else: - rename_doc('Item', doc.name, lab_test_code, ignore_permissions = True) + rename_doc('Item', doc.name, lab_test_code, ignore_permissions=True) frappe.db.set_value('Lab Test Template', doc.name, 'lab_test_code', lab_test_code) frappe.db.set_value('Lab Test Template', doc.name, 'lab_test_name', lab_test_code) - rename_doc('Lab Test Template', doc.name, lab_test_code, ignore_permissions = True) + rename_doc('Lab Test Template', doc.name, lab_test_code, ignore_permissions=True) return lab_test_code diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template_list.js b/erpnext/healthcare/doctype/lab_test_template/lab_test_template_list.js index a3417ebdfc..08fc2cddda 100644 --- a/erpnext/healthcare/doctype/lab_test_template/lab_test_template_list.js +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template_list.js @@ -3,5 +3,5 @@ */ frappe.listview_settings['Lab Test Template'] = { add_fields: ['lab_test_name', 'lab_test_code', 'lab_test_rate'], - filters: [['disabled', '=', 0]] + filters: [['disabled', '=', 'No']] }; From ff0b9bffcf373a8781ed76008aec551b2648cba1 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 30 Jul 2020 12:16:56 +0530 Subject: [PATCH 006/175] feat: added dashboard for Lab Test Template --- .../lab_test_template_dashboard.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 erpnext/healthcare/doctype/lab_test_template/lab_test_template_dashboard.py diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template_dashboard.py b/erpnext/healthcare/doctype/lab_test_template/lab_test_template_dashboard.py new file mode 100644 index 0000000000..94dfeea7a4 --- /dev/null +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template_dashboard.py @@ -0,0 +1,13 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'template', + 'transactions': [ + { + 'label': _('Lab Tests'), + 'items': ['Lab Test'] + } + ] + } From b00f870c77c2fcd0e6ee829a5c858550fe8c9e88 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 30 Jul 2020 13:49:13 +0530 Subject: [PATCH 007/175] fix: Lab Test Completed status not visible in list view --- .../healthcare/doctype/lab_test/lab_test_list.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/erpnext/healthcare/doctype/lab_test/lab_test_list.js b/erpnext/healthcare/doctype/lab_test/lab_test_list.js index b7f157c38b..0a6ed20f3e 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test_list.js +++ b/erpnext/healthcare/doctype/lab_test/lab_test_list.js @@ -3,13 +3,16 @@ */ frappe.listview_settings['Lab Test'] = { add_fields: ['name', 'status', 'invoiced'], - filters: [['docstatus', '=', '0']], + filters: [['docstatus', '=', '1']], get_indicator: function (doc) { - if (doc.status == 'Approved') { - return [__('Approved'), 'green', 'status, = ,Approved']; - } - if (doc.status == 'Rejected') { + if (doc.status === 'Approved') { + return [__('Approved'), 'green', 'status, =, Approved']; + } else if (doc.status === 'Rejected') { return [__('Rejected'), 'orange', 'status, =, Rejected']; + } else if (doc.status === 'Completed') { + return [__('Completed'), 'green', 'status, =, Completed']; + } else if (doc.status === 'Cancelled') { + return [__('Cancelled'), 'red', 'status, =, Cancelled']; } }, onload: function (listview) { From c33703d54c3294793d4d644d7a541216c6b34338 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 30 Jul 2020 14:25:40 +0530 Subject: [PATCH 008/175] fix: move result value validations to server side --- .../healthcare/doctype/lab_test/lab_test.js | 17 -------- .../healthcare/doctype/lab_test/lab_test.json | 23 ++++++----- .../healthcare/doctype/lab_test/lab_test.py | 40 +++++++++++++------ .../lab_test_group_template.json | 5 ++- .../lab_test_template/lab_test_template.py | 4 +- 5 files changed, 46 insertions(+), 43 deletions(-) diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.js b/erpnext/healthcare/doctype/lab_test/lab_test.js index 8036c7dc13..87d9c83763 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.js +++ b/erpnext/healthcare/doctype/lab_test/lab_test.js @@ -179,23 +179,6 @@ var show_lab_tests = function (frm, lab_test_list) { d.show(); }; -cur_frm.cscript.custom_before_submit = function (doc) { - if (doc.normal_test_items) { - for (let result in doc.normal_test_items) { - if (!doc.normal_test_items[result].result_value && !doc.normal_test_items[result].allow_blank && doc.normal_test_items[result].require_result_value) { - frappe.throw(__('Please input all required result values')); - } - } - } - if (doc.descriptive_test_items) { - for (let result in doc.descriptive_test_items) { - if (!doc.descriptive_test_items[result].result_value && !doc.descriptive_test_items[result].allow_blank && doc.descriptive_test_items[result].require_result_value) { - frappe.throw(__('Please input all required result values')); - } - } - } -}; - var make_dialog = function (frm, emailed, printed) { var number = frm.doc.mobile; diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.json b/erpnext/healthcare/doctype/lab_test/lab_test.json index 2eb8014b7e..575a2659db 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.json +++ b/erpnext/healthcare/doctype/lab_test/lab_test.json @@ -84,7 +84,7 @@ "fieldname": "naming_series", "fieldtype": "Select", "label": "Series", - "options": "LP-", + "options": "HLC-LAB-.YYYY.-", "print_hide": 1, "report_hide": 1, "reqd": 1 @@ -197,11 +197,10 @@ { "fieldname": "status", "fieldtype": "Select", + "in_list_view": 1, "label": "Status", "options": "Draft\nCompleted\nApproved\nRejected\nCancelled", - "print_hide": 1, "read_only": 1, - "report_hide": 1, "search_index": 1 }, { @@ -354,7 +353,8 @@ }, { "fieldname": "sb_normal", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Compound Test Result" }, { "fieldname": "normal_test_items", @@ -369,11 +369,13 @@ { "depends_on": "descriptive_toggle", "fieldname": "organisms_section", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Organism Test Result" }, { "fieldname": "sb_sensitivity", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Sensitivity Test Result" }, { "fieldname": "sensitivity_test_items", @@ -383,8 +385,10 @@ "report_hide": 1 }, { + "collapsible": 1, "fieldname": "sb_comments", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Comments" }, { "fieldname": "lab_test_comment", @@ -531,7 +535,8 @@ }, { "fieldname": "sb_descriptive", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Descriptive Test Result" }, { "default": "0", @@ -550,7 +555,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-07-16 13:35:24.811062", + "modified": "2020-07-30 14:03:00.166003", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Test", diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.py b/erpnext/healthcare/doctype/lab_test/lab_test.py index 865f4a14e3..c676dfb8a8 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/lab_test.py @@ -9,20 +9,21 @@ from frappe.model.document import Document from frappe.utils import getdate, cstr class LabTest(Document): + def validate(self): + if not self.is_new(): + self.set_secondary_uom_result() + def on_submit(self): + self.validate_result_values() self.db_set('submitted_date', getdate()) self.db_set('status', 'Completed') insert_lab_test_to_medical_record(self) def on_cancel(self): - delete_lab_test_from_medical_record(self) self.db_set('status', 'Cancelled') + delete_lab_test_from_medical_record(self) self.reload() - def validate(self): - if not self.is_new(): - self.set_secondary_uom_result() - def on_update(self): if self.sensitivity_test_items: sensitivity = sorted(self.sensitivity_test_items, key=lambda x: x.antibiotic_sensitivity) @@ -51,7 +52,20 @@ class LabTest(Document): item.secondary_uom_result = float(item.result_value) * float(item.conversion_factor) except: item.secondary_uom_result = '' - frappe.msgprint(_('Result for Secondary UOM not calculated for row #{0}'.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: + for item in self.normal_test_items: + if not item.result_value and not item.allow_blank and item.require_result_value: + frappe.throw(_('Row #{0}: Please enter the result value for {1}').format( + item.idx, frappe.bold(item.lab_test_name)), title=_('Mandatory Results')) + + if self.descriptive_test_items: + for item in self.descriptive_test_items: + if not item.result_value and not item.allow_blank and item.require_result_value: + frappe.throw(_('Row #{0}: Please enter the result value {1}').format( + item.idx, frappe.bold(item.lab_test_name)), title=_('Mandatory Results')) def create_test_from_template(lab_test): @@ -263,8 +277,7 @@ def load_result_format(lab_test, template, prescription, invoice): for lab_test_group in template.lab_test_groups: # Template_in_group = None if lab_test_group.lab_test_template: - template_in_group = frappe.get_doc('Lab Test Template', - lab_test_group.lab_test_template) + template_in_group = frappe.get_doc('Lab Test Template', lab_test_group.lab_test_template) if template_in_group: if template_in_group.lab_test_template_type == 'Single': create_normals(template_in_group, lab_test) @@ -302,9 +315,10 @@ def load_result_format(lab_test, template, prescription, invoice): @frappe.whitelist() def get_employee_by_user_id(user_id): - emp_id = frappe.db.get_value('Employee', { 'user_id': user_id }) - employee = frappe.get_doc('Employee', emp_id) - return employee + emp_id = frappe.db.exists('Employee', { 'user_id': user_id }) + if emp_id: + return frappe.get_doc('Employee', emp_id) + return None def insert_lab_test_to_medical_record(doc): table_row = False @@ -325,7 +339,7 @@ def insert_lab_test_to_medical_record(doc): table_row += ' ' + frappe.bold(_('Lab Test Result: ')) + item.result_value if item.normal_range: - table_row += ' ' + _('Normal Range:') + item.normal_range + table_row += ' ' + _('Normal Range: ') + item.normal_range table_row += ' ' + comment elif doc.descriptive_test_items: @@ -356,7 +370,7 @@ def insert_lab_test_to_medical_record(doc): medical_record.save(ignore_permissions = True) def delete_lab_test_from_medical_record(self): - medical_record_id = frappe.db.sql('select name from `tabPatient Medical Record` where reference_name= %s', (self.name)) + medical_record_id = frappe.db.sql('select name from `tabPatient Medical Record` where reference_name=%s', (self.name)) if medical_record_id and medical_record_id[0][0]: frappe.delete_doc('Patient Medical Record', medical_record_id[0][0]) diff --git a/erpnext/healthcare/doctype/lab_test_group_template/lab_test_group_template.json b/erpnext/healthcare/doctype/lab_test_group_template/lab_test_group_template.json index beea7a357e..2767f7ec77 100644 --- a/erpnext/healthcare/doctype/lab_test_group_template/lab_test_group_template.json +++ b/erpnext/healthcare/doctype/lab_test_group_template/lab_test_group_template.json @@ -93,7 +93,8 @@ "depends_on": "secondary_uom", "fieldname": "conversion_factor", "fieldtype": "Float", - "label": "Conversion Factor" + "label": "Conversion Factor", + "mandatory_depends_on": "secondary_uom" }, { "default": "0", @@ -106,7 +107,7 @@ ], "istable": 1, "links": [], - "modified": "2020-06-24 10:59:01.921924", + "modified": "2020-07-30 12:36:03.082391", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Test Group Template", diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py index e4fbdd9d86..543dee27eb 100644 --- a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py @@ -81,11 +81,11 @@ class LabTestTemplate(Document): if self.lab_test_template_type == 'Compound': for item in self.normal_test_templates: if item.secondary_uom and not item.conversion_factor: - frappe.throw(_('Conversion Factor is mandatory')) + frappe.throw(_('Row #{0}: Conversion Factor is mandatory').format(item.idx)) if self.lab_test_template_type == 'Grouped': for group in self.lab_test_groups: if group.template_or_new_line == 'Add New Line' and group.secondary_uom and not group.conversion_factor: - frappe.throw(_('Conversion Factor is mandatory')) + frappe.throw(_('Row #{0}: Conversion Factor is mandatory').format(group.idx)) def create_item_from_template(doc): From 73edb12ab3a579794782ae63173517f1c01a20ba Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 30 Jul 2020 14:50:30 +0530 Subject: [PATCH 009/175] fix: msgprint for sample collection doc created on Lab Test creation --- erpnext/healthcare/doctype/lab_test/lab_test.py | 6 ++++-- .../lab_test_template/lab_test_template.json | 15 ++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.py b/erpnext/healthcare/doctype/lab_test/lab_test.py index c676dfb8a8..2bf4a3a7db 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/lab_test.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import getdate, cstr +from frappe.utils import getdate, cstr, get_link_to_form class LabTest(Document): def validate(self): @@ -262,7 +262,9 @@ def create_sample_collection(lab_test, template, patient, invoice): sample_collection = create_sample_doc(template, patient, invoice, lab_test.company) if sample_collection: lab_test.sample = sample_collection.name - + sample_collection_doc = get_link_to_form('Sample Collection', sample_collection.name) + frappe.msgprint(_('Sample Collection {0} has been created').format(sample_collection_doc), + title=_('Sample Collection'), indicator='green') return lab_test def load_result_format(lab_test, template, prescription, invoice): diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json index fc6a1e1790..c3fc842047 100644 --- a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json @@ -34,14 +34,15 @@ "descriptive_test_templates", "section_break_group", "lab_test_groups", - "medical_coding_section", - "medical_code_standard", - "medical_code", "sb_sample_collection", "sample", "sample_uom", "sample_qty", + "column_break_33", "sample_details", + "medical_coding_section", + "medical_code", + "medical_code_standard", "worksheet_section", "worksheet_instructions", "result_legend_section", @@ -128,6 +129,7 @@ "mandatory_depends_on": "eval:doc.is_billable == 1" }, { + "collapsible": 1, "fieldname": "medical_coding_section", "fieldtype": "Section Break", "label": "Medical Coding" @@ -217,7 +219,6 @@ "no_copy": 1 }, { - "collapsible": 1, "fieldname": "sb_sample_collection", "fieldtype": "Section Break", "label": "Sample Collection" @@ -311,10 +312,14 @@ "fieldname": "descriptive_test_templates", "fieldtype": "Table", "options": "Descriptive Test Template" + }, + { + "fieldname": "column_break_33", + "fieldtype": "Column Break" } ], "links": [], - "modified": "2020-07-30 11:55:43.093828", + "modified": "2020-07-30 14:32:40.449818", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Test Template", From afa001a1d1f6a756b22e362535ac79b0e61f9a7d Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 30 Jul 2020 15:25:04 +0530 Subject: [PATCH 010/175] fix: code clean-up --- erpnext/healthcare/doctype/lab_test/lab_test.js | 16 ++++++++-------- erpnext/healthcare/doctype/lab_test/lab_test.py | 14 +++++++++++--- .../healthcare/doctype/lab_test/lab_test_list.js | 4 ++-- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.js b/erpnext/healthcare/doctype/lab_test/lab_test.js index 87d9c83763..f1634c1294 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.js +++ b/erpnext/healthcare/doctype/lab_test/lab_test.js @@ -34,10 +34,10 @@ frappe.ui.form.on('Lab Test', { if (frm.doc.docstatus === 1 && frm.doc.status !== 'Approved' && frm.doc.status !== 'Rejected') { frm.add_custom_button(__('Approve'), function () { status_update(1, frm); - }); + }, __('Actions')); frm.add_custom_button(__('Reject'), function () { status_update(0, frm); - }); + }, __('Actions')); } } @@ -186,7 +186,7 @@ var make_dialog = function (frm, emailed, printed) { title: 'Send SMS', width: 400, fields: [ - { fieldname: 'sms_type', fieldtype: 'Select', label: 'Type', options: ['Emailed', 'Printed'] }, + { fieldname: 'result_format', fieldtype: 'Select', label: 'Result Format', options: ['Emailed', 'Printed'] }, { fieldname: 'number', fieldtype: 'Data', label: 'Mobile Number', reqd: 1 }, { fieldname: 'message', fieldtype: 'Small Text', label: 'Message', reqd: 1 } ], @@ -200,22 +200,22 @@ var make_dialog = function (frm, emailed, printed) { dialog.hide(); } }); - if (frm.doc.report_preference == 'Print') { + if (frm.doc.report_preference === 'Print') { dialog.set_values({ - 'sms_type': 'Printed', + 'result_format': 'Printed', 'number': number, 'message': printed }); } else { dialog.set_values({ - 'sms_type': 'Emailed', + 'result_format': 'Emailed', 'number': number, 'message': emailed }); } var fd = dialog.fields_dict; - $(fd.sms_type.input).change(function () { - if (dialog.get_value('sms_type') == 'Emailed') { + $(fd.result_format.input).change(function () { + if (dialog.get_value('result_format') === 'Emailed') { dialog.set_values({ 'number': number, 'message': emailed diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.py b/erpnext/healthcare/doctype/lab_test/lab_test.py index 2bf4a3a7db..8dc26b09bc 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/lab_test.py @@ -103,7 +103,7 @@ def create_multiple(doctype, docname): lab_test_created = create_lab_test_from_encounter(docname) if lab_test_created: - frappe.msgprint(_('Lab Test(s) {0} created'.format(lab_test_created))) + frappe.msgprint(_('Lab Test(s) {0} created'.format(lab_test_created)), indicator='green') else: frappe.msgprint(_('No Lab Tests created')) @@ -225,8 +225,9 @@ def create_sample_doc(template, patient, invoice, company = None): 'docstatus': 0, 'sample': template.sample }) + if sample_exists: - # Update Sample Collection by adding quantity + # update sample collection by adding quantity 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: @@ -252,7 +253,7 @@ def create_sample_doc(template, patient, invoice, company = None): sample_collection.company = company if template.sample_details: - sample_collection.sample_details = 'Test :' + (template.get('lab_test_name') or template.get('template')) + '\n' + 'Collection Detials:\n\t' + template.sample_details + sample_collection.sample_details = _('Test :') + (template.get('lab_test_name') or template.get('template')) + '\n' + 'Collection Detials:\n\t' + template.sample_details sample_collection.save(ignore_permissions=True) return sample_collection @@ -270,10 +271,13 @@ def create_sample_collection(lab_test, template, patient, invoice): def load_result_format(lab_test, template, prescription, invoice): if template.lab_test_template_type == 'Single': create_normals(template, lab_test) + elif template.lab_test_template_type == 'Compound': create_compounds(template, lab_test, False) + elif template.lab_test_template_type == 'Descriptive': create_descriptives(template, lab_test) + elif template.lab_test_template_type == 'Grouped': # Iterate for each template in the group and create one result for all. for lab_test_group in template.lab_test_groups: @@ -283,6 +287,7 @@ def load_result_format(lab_test, template, prescription, invoice): if template_in_group: if template_in_group.lab_test_template_type == 'Single': create_normals(template_in_group, lab_test) + elif template_in_group.lab_test_template_type == 'Compound': normal_heading = lab_test.append('normal_test_items') normal_heading.lab_test_name = template_in_group.lab_test_name @@ -290,6 +295,7 @@ def load_result_format(lab_test, template, prescription, invoice): normal_heading.allow_blank = 1 normal_heading.template = template_in_group.name create_compounds(template_in_group, lab_test, True) + elif template_in_group.lab_test_template_type == 'Descriptive': descriptive_heading = lab_test.append('descriptive_test_items') descriptive_heading.lab_test_name = template_in_group.lab_test_name @@ -297,6 +303,7 @@ def load_result_format(lab_test, template, prescription, invoice): descriptive_heading.allow_blank = 1 descriptive_heading.template = template_in_group.name create_descriptives(template_in_group, lab_test) + else: # Lab Test Group - Add New Line normal = lab_test.append('normal_test_items') normal.lab_test_name = lab_test_group.group_event @@ -307,6 +314,7 @@ def load_result_format(lab_test, template, prescription, invoice): normal.allow_blank = lab_test_group.allow_blank normal.require_result_value = 1 normal.template = template.name + if template.lab_test_template_type != 'No Result': if prescription: lab_test.prescription = prescription diff --git a/erpnext/healthcare/doctype/lab_test/lab_test_list.js b/erpnext/healthcare/doctype/lab_test/lab_test_list.js index 0a6ed20f3e..7b5b9d922a 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test_list.js +++ b/erpnext/healthcare/doctype/lab_test/lab_test_list.js @@ -24,7 +24,7 @@ frappe.listview_settings['Lab Test'] = { var create_multiple_dialog = function (listview) { var dialog = new frappe.ui.Dialog({ - title: 'Create Multiple Lab Test', + title: 'Create Multiple Lab Tests', width: 100, fields: [ { fieldtype: 'Link', label: 'Patient', fieldname: 'patient', options: 'Patient', reqd: 1 }, @@ -44,7 +44,7 @@ var create_multiple_dialog = function (listview) { } } ], - primary_action_label: __('Create Lab Test'), + primary_action_label: __('Create'), primary_action: function () { frappe.call({ method: 'erpnext.healthcare.doctype.lab_test.lab_test.create_multiple', From fa2c20ea9e9f127adc871b7ba188f7ce44b1a155 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 30 Jul 2020 16:56:11 +0530 Subject: [PATCH 011/175] chore: Sample Collection Form clean-up --- .../healthcare/doctype/lab_test/lab_test.py | 6 +- .../sample_collection/sample_collection.js | 20 ++--- .../sample_collection/sample_collection.json | 85 ++++++++++++++++--- 3 files changed, 85 insertions(+), 26 deletions(-) diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.py b/erpnext/healthcare/doctype/lab_test/lab_test.py index 8dc26b09bc..2db7743865 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/lab_test.py @@ -64,8 +64,8 @@ class LabTest(Document): if self.descriptive_test_items: for item in self.descriptive_test_items: if not item.result_value and not item.allow_blank and item.require_result_value: - frappe.throw(_('Row #{0}: Please enter the result value {1}').format( - item.idx, frappe.bold(item.lab_test_name)), title=_('Mandatory Results')) + frappe.throw(_('Row #{0}: Please enter the result value for {1}').format( + item.idx, frappe.bold(item.lab_test_particulars)), title=_('Mandatory Results')) def create_test_from_template(lab_test): @@ -103,7 +103,7 @@ def create_multiple(doctype, docname): lab_test_created = create_lab_test_from_encounter(docname) if lab_test_created: - frappe.msgprint(_('Lab Test(s) {0} created'.format(lab_test_created)), indicator='green') + frappe.msgprint(_('Lab Test(s) {0} created successfully').format(lab_test_created), indicator='green') else: frappe.msgprint(_('No Lab Tests created')) diff --git a/erpnext/healthcare/doctype/sample_collection/sample_collection.js b/erpnext/healthcare/doctype/sample_collection/sample_collection.js index 2f5278b2d5..0390391235 100644 --- a/erpnext/healthcare/doctype/sample_collection/sample_collection.js +++ b/erpnext/healthcare/doctype/sample_collection/sample_collection.js @@ -3,29 +3,29 @@ frappe.ui.form.on('Sample Collection', { refresh: function(frm) { - if(frappe.defaults.get_default("create_sample_collection_for_lab_test")){ - frm.add_custom_button(__("View Lab Tests"), function() { - frappe.route_options = {"sample": frm.doc.name}; - frappe.set_route("List", "Lab Test"); + if (frappe.defaults.get_default('create_sample_collection_for_lab_test')) { + frm.add_custom_button(__('View Lab Tests'), function() { + frappe.route_options = {'sample': frm.doc.name}; + frappe.set_route('List', 'Lab Test'); }); } } }); -frappe.ui.form.on("Sample Collection", "patient", function(frm) { +frappe.ui.form.on('Sample Collection', 'patient', function(frm) { if(frm.doc.patient){ frappe.call({ - "method": "erpnext.healthcare.doctype.patient.patient.get_patient_detail", + 'method': 'erpnext.healthcare.doctype.patient.patient.get_patient_detail', args: { patient: frm.doc.patient }, callback: function (data) { var age = null; - if(data.message.dob){ + if (data.message.dob){ age = calculate_age(data.message.dob); } - frappe.model.set_value(frm.doctype,frm.docname, "patient_age", age); - frappe.model.set_value(frm.doctype,frm.docname, "patient_sex", data.message.sex); + frappe.model.set_value(frm.doctype,frm.docname, 'patient_age', age); + frappe.model.set_value(frm.doctype,frm.docname, 'patient_sex', data.message.sex); } }); } @@ -36,5 +36,5 @@ var calculate_age = function(birth) { var age = new Date(); age.setTime(ageMS); var years = age.getFullYear() - 1970; - return years + " Year(s) " + age.getMonth() + " Month(s) " + age.getDate() + " Day(s)"; + return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)'; }; diff --git a/erpnext/healthcare/doctype/sample_collection/sample_collection.json b/erpnext/healthcare/doctype/sample_collection/sample_collection.json index 016cfbc3ae..83383e3445 100644 --- a/erpnext/healthcare/doctype/sample_collection/sample_collection.json +++ b/erpnext/healthcare/doctype/sample_collection/sample_collection.json @@ -9,8 +9,10 @@ "document_type": "Document", "engine": "InnoDB", "field_order": [ + "patient_details_section", "naming_series", "patient", + "patient_name", "patient_age", "patient_sex", "column_break_4", @@ -25,15 +27,17 @@ "collected_by", "collected_time", "num_print", - "amended_from", "section_break_15", - "sample_details" + "sample_details", + "amended_from" ], "fields": [ { "fetch_from": "patient.inpatient_record", "fieldname": "inpatient_record", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Inpatient Record", "options": "Inpatient Record", "read_only": 1 @@ -42,6 +46,8 @@ "bold": 1, "fieldname": "naming_series", "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, "label": "Series", "no_copy": 1, "options": "HLC-SC-.YYYY.-", @@ -52,6 +58,8 @@ "default": "0", "fieldname": "invoiced", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Invoiced", "no_copy": 1, "read_only": 1, @@ -61,41 +69,60 @@ "fetch_from": "inpatient_record.patient", "fieldname": "patient", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "ignore_user_permissions": 1, "in_standard_filter": 1, "label": "Patient", "options": "Patient", + "reqd": 1, "search_index": 1 }, { "fieldname": "column_break_4", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "patient_age", "fieldtype": "Data", - "label": "Age" + "hide_days": 1, + "hide_seconds": 1, + "label": "Age", + "read_only": 1 }, { "fetch_from": "patient.sex", "fieldname": "patient_sex", - "fieldtype": "Data", - "label": "Gender" + "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, + "label": "Gender", + "options": "Gender", + "read_only": 1 }, { "fieldname": "company", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "in_standard_filter": 1, "label": "Company", "options": "Company" }, { "fieldname": "section_break_6", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, + "label": "Sample Details" }, { "fieldname": "sample", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "ignore_user_permissions": 1, "in_list_view": 1, "in_standard_filter": 1, @@ -108,16 +135,23 @@ "fetch_from": "sample.sample_uom", "fieldname": "sample_uom", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, - "label": "UOM" + "label": "UOM", + "read_only": 1 }, { "fieldname": "column_break_10", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "collected_by", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "ignore_user_permissions": 1, "label": "Collected By", "options": "User" @@ -125,20 +159,27 @@ { "fieldname": "collected_time", "fieldtype": "Datetime", - "label": "Collected Time" + "hide_days": 1, + "hide_seconds": 1, + "label": "Collected On" }, { "allow_on_submit": 1, "default": "1", + "description": "Number of prints required for labelling the samples", "fieldname": "num_print", "fieldtype": "Int", - "label": "No. of print", + "hide_days": 1, + "hide_seconds": 1, + "label": "No. of prints", "print_hide": 1, "report_hide": 1 }, { "fieldname": "amended_from", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Amended From", "no_copy": 1, "options": "Sample Collection", @@ -147,25 +188,43 @@ }, { "fieldname": "section_break_15", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "default": "0", "fieldname": "sample_qty", "fieldtype": "Float", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, "label": "Quantity" }, { "fieldname": "sample_details", "fieldtype": "Long Text", + "hide_days": 1, + "hide_seconds": 1, "ignore_xss_filter": 1, "label": "Collection Details" + }, + { + "fieldname": "patient_details_section", + "fieldtype": "Section Break", + "label": "Patient Details" + }, + { + "fetch_from": "patient.patient_name", + "fieldname": "patient_name", + "fieldtype": "Data", + "label": "Patient Name", + "read_only": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-05-25 14:36:46.990469", + "modified": "2020-07-30 16:53:13.076104", "modified_by": "Administrator", "module": "Healthcare", "name": "Sample Collection", From ca1b389c99529eeccf892fbfde8a2b34ce7a0153 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 30 Jul 2020 16:58:19 +0530 Subject: [PATCH 012/175] fix: validate negative quantity for sample collection --- .../doctype/sample_collection/sample_collection.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/healthcare/doctype/sample_collection/sample_collection.py b/erpnext/healthcare/doctype/sample_collection/sample_collection.py index 2c64320fac..461f809507 100644 --- a/erpnext/healthcare/doctype/sample_collection/sample_collection.py +++ b/erpnext/healthcare/doctype/sample_collection/sample_collection.py @@ -3,7 +3,12 @@ # For license information, please see license.txt from __future__ import unicode_literals +import frappe from frappe.model.document import Document +from frappe.utils import flt +from frappe import _ class SampleCollection(Document): - pass + def validate(self): + if flt(self.sample_qty) <= 0: + frappe.throw(_('Sample Quantity cannot be negative or 0'), title=_('Invalid Quantity')) From c5ccf38cdf849431f27d3601756159391c237cd4 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 30 Jul 2020 18:56:16 +0530 Subject: [PATCH 013/175] refactor: Lab Test Report --- .../healthcare/doctype/lab_test/lab_test.json | 4 +- .../report/lab_test_report/lab_test_report.js | 39 ++++- .../lab_test_report/lab_test_report.json | 39 ++--- .../report/lab_test_report/lab_test_report.py | 139 ++++++++++++++---- 4 files changed, 165 insertions(+), 56 deletions(-) diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.json b/erpnext/healthcare/doctype/lab_test/lab_test.json index 575a2659db..edf1d911aa 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.json +++ b/erpnext/healthcare/doctype/lab_test/lab_test.json @@ -248,8 +248,8 @@ { "fieldname": "result_date", "fieldtype": "Date", - "hidden": 1, "label": "Result Date", + "read_only": 1, "search_index": 1 }, { @@ -555,7 +555,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-07-30 14:03:00.166003", + "modified": "2020-07-30 18:18:38.516215", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Test", diff --git a/erpnext/healthcare/report/lab_test_report/lab_test_report.js b/erpnext/healthcare/report/lab_test_report/lab_test_report.js index 3128f819bb..7754e2e196 100644 --- a/erpnext/healthcare/report/lab_test_report/lab_test_report.js +++ b/erpnext/healthcare/report/lab_test_report/lab_test_report.js @@ -4,29 +4,54 @@ frappe.query_reports["Lab Test Report"] = { "filters": [ { - "fieldname":"from_date", + "fieldname": "from_date", "label": __("From Date"), "fieldtype": "Date", - "default": frappe.datetime.now_date(), - "width": "80" + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + "reqd": 1 }, { - "fieldname":"to_date", + "fieldname": "to_date", "label": __("To Date"), "fieldtype": "Date", - "default": frappe.datetime.now_date() + "default": frappe.datetime.now_date(), + "reqd": 1 }, { - "fieldname":"patient", + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "default": frappe.defaults.get_default("Company"), + "options": "Company" + }, + { + "fieldname": "template", + "label": __("Lab Test Template"), + "fieldtype": "Link", + "options": "Lab Test Template" + }, + { + "fieldname": "patient", "label": __("Patient"), "fieldtype": "Link", "options": "Patient" }, { - "fieldname":"department", + "fieldname": "department", "label": __("Medical Department"), "fieldtype": "Link", "options": "Medical Department" + }, + { + "fieldname": "status", + "label": __("Status"), + "fieldtype": "Select", + "options": "\nCompleted\nApproved\nRejected" + }, + { + "fieldname": "invoiced", + "label": __("Invoiced"), + "fieldtype": "Check" } ] }; diff --git a/erpnext/healthcare/report/lab_test_report/lab_test_report.json b/erpnext/healthcare/report/lab_test_report/lab_test_report.json index 30e5a5fd56..aeb42897b8 100644 --- a/erpnext/healthcare/report/lab_test_report/lab_test_report.json +++ b/erpnext/healthcare/report/lab_test_report/lab_test_report.json @@ -1,30 +1,31 @@ { - "add_total_row": 1, - "creation": "2013-04-23 18:15:29", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 1, - "is_standard": "Yes", - "modified": "2018-08-06 11:41:50.218737", - "modified_by": "Administrator", - "module": "Healthcare", - "name": "Lab Test Report", - "owner": "Administrator", - "prepared_report": 0, - "ref_doctype": "Lab Test", - "report_name": "Lab Test Report", - "report_type": "Script Report", + "add_total_row": 0, + "creation": "2013-04-23 18:15:29", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 1, + "is_standard": "Yes", + "modified": "2020-07-30 18:53:20.102873", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Lab Test Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Lab Test", + "report_name": "Lab Test Report", + "report_type": "Script Report", "roles": [ { "role": "Laboratory User" - }, + }, { "role": "Nursing User" - }, + }, { "role": "LabTest Approver" - }, + }, { "role": "Healthcare Administrator" } diff --git a/erpnext/healthcare/report/lab_test_report/lab_test_report.py b/erpnext/healthcare/report/lab_test_report/lab_test_report.py index 17f25fa7a7..be2d06193e 100644 --- a/erpnext/healthcare/report/lab_test_report/lab_test_report.py +++ b/erpnext/healthcare/report/lab_test_report/lab_test_report.py @@ -8,51 +8,134 @@ from frappe import msgprint, _ def execute(filters=None): if not filters: filters = {} - lab_test_list = get_lab_test(filters) + data, columns = [], [] + columns = get_columns() + lab_test_list = get_lab_tests(filters) if not lab_test_list: - msgprint(_("No record found")) + msgprint(_("No records found")) return columns, lab_test_list data = [] for lab_test in lab_test_list: - row = [ lab_test.lab_test_name, lab_test.patient, lab_test.practitioner, lab_test.invoiced, lab_test.status, lab_test.result_date, lab_test.department] + row = frappe._dict({ + 'test': lab_test.name, + 'template': lab_test.template, + 'company': lab_test.company, + 'patient': lab_test.patient, + 'patient_name': lab_test.patient_name, + 'practitioner': lab_test.practitioner, + 'employee': lab_test.employee, + 'status': lab_test.status, + 'invoiced': lab_test.invoiced, + 'result_date': lab_test.result_date, + 'department': lab_test.department + }) data.append(row) return columns, data def get_columns(): - columns = [ - _("Test") + ":Data:120", - _("Patient") + ":Link/Patient:180", - _("Healthcare Practitioner") + ":Link/Healthcare Practitioner:120", - _("Invoiced") + ":Check:100", - _("Status") + ":Data:120", - _("Result Date") + ":Date:120", - _("Department") + ":Data:120", + return [ + { + "fieldname": "test", + "label": _("Lab Test"), + "fieldtype": "Link", + "options": "Lab Test", + "width": "120" + }, + { + "fieldname": "template", + "label": _("Lab Test Template"), + "fieldtype": "Link", + "options": "Lab Test Template", + "width": "120" + }, + { + "fieldname": "company", + "label": _("Company"), + "fieldtype": "Link", + "options": "Company", + "width": "120" + }, + { + "fieldname": "patient", + "label": _("Patient"), + "fieldtype": "Link", + "options": "Patient", + "width": "120" + }, + { + "fieldname": "patient_name", + "label": _("Patient Name"), + "fieldtype": "Data", + "width": "120" + }, + { + "fieldname": "practitioner", + "label": _("Requesting Practitioner"), + "fieldtype": "Link", + "options": "Healthcare Practitioner", + "width": "120" + }, + { + "fieldname": "employee", + "label": _("Lab Technician"), + "fieldtype": "Link", + "options": "Employee", + "width": "120" + }, + { + "fieldname": "status", + "label": _("Status"), + "fieldtype": "Data", + "width": "100" + }, + { + "fieldname": "invoiced", + "label": _("Invoiced"), + "fieldtype": "Check", + "width": "100" + }, + { + "fieldname": "result_date", + "label": _("Result Date"), + "fieldtype": "Date", + "width": "100" + }, + { + "fieldname": "department", + "label": _("Medical Department"), + "fieldtype": "Link", + "options": "Medical Department", + "width": "100" + } ] - return columns +def get_lab_tests(filters): + conditions = get_conditions(filters) + data = frappe.get_all( + doctype='Lab Test', + fields=['name', 'template', 'company', 'patient', 'patient_name', 'practitioner', 'employee', 'status', 'invoiced', 'result_date', 'department'], + filters=conditions, + order_by='submitted_date desc' + ) + return data def get_conditions(filters): - conditions = "" + conditions = { + 'docstatus': ('=', 1) + } - if filters.get("patient"): - conditions += "and patient = %(patient)s" - if filters.get("from_date"): - conditions += "and result_date >= %(from_date)s" - if filters.get("to_date"): - conditions += " and result_date <= %(to_date)s" - if filters.get("department"): - conditions += " and department = %(department)s" + if filters.get('from_date') and filters.get('to_date'): + conditions['result_date'] = ('between', (filters.get('from_date'), filters.get('to_date'))) + filters.pop('from_date') + filters.pop('to_date') - return conditions + for key, value in filters.items(): + if filters.get(key): + conditions[key] = value -def get_lab_test(filters): - conditions = get_conditions(filters) - return frappe.db.sql("""select name, patient, lab_test_name, patient_name, status, result_date, practitioner, invoiced, department - from `tabLab Test` - where docstatus<2 %s order by submitted_date desc, name desc""" % - conditions, filters, as_dict=1) + return conditions \ No newline at end of file From b6675f8dd4f3c70bfbe8d1cedbc5590809041dc7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 30 Jul 2020 19:33:08 +0530 Subject: [PATCH 014/175] feat: Lab Test Report Summary and Chart --- .../report/lab_test_report/lab_test_report.py | 178 ++++++++++++------ 1 file changed, 124 insertions(+), 54 deletions(-) diff --git a/erpnext/healthcare/report/lab_test_report/lab_test_report.py b/erpnext/healthcare/report/lab_test_report/lab_test_report.py index be2d06193e..2e59bed037 100644 --- a/erpnext/healthcare/report/lab_test_report/lab_test_report.py +++ b/erpnext/healthcare/report/lab_test_report/lab_test_report.py @@ -14,7 +14,7 @@ def execute(filters=None): lab_test_list = get_lab_tests(filters) if not lab_test_list: - msgprint(_("No records found")) + msgprint(_('No records found')) return columns, lab_test_list data = [] @@ -34,83 +34,85 @@ def execute(filters=None): }) data.append(row) - return columns, data + chart = get_chart_data(data) + report_summary = get_report_summary(data) + return columns, data, None, chart, report_summary def get_columns(): return [ { - "fieldname": "test", - "label": _("Lab Test"), - "fieldtype": "Link", - "options": "Lab Test", - "width": "120" + 'fieldname': 'test', + 'label': _('Lab Test'), + 'fieldtype': 'Link', + 'options': 'Lab Test', + 'width': '120' }, { - "fieldname": "template", - "label": _("Lab Test Template"), - "fieldtype": "Link", - "options": "Lab Test Template", - "width": "120" + 'fieldname': 'template', + 'label': _('Lab Test Template'), + 'fieldtype': 'Link', + 'options': 'Lab Test Template', + 'width': '120' }, { - "fieldname": "company", - "label": _("Company"), - "fieldtype": "Link", - "options": "Company", - "width": "120" + 'fieldname': 'company', + 'label': _('Company'), + 'fieldtype': 'Link', + 'options': 'Company', + 'width': '120' }, { - "fieldname": "patient", - "label": _("Patient"), - "fieldtype": "Link", - "options": "Patient", - "width": "120" + 'fieldname': 'patient', + 'label': _('Patient'), + 'fieldtype': 'Link', + 'options': 'Patient', + 'width': '120' }, { - "fieldname": "patient_name", - "label": _("Patient Name"), - "fieldtype": "Data", - "width": "120" + 'fieldname': 'patient_name', + 'label': _('Patient Name'), + 'fieldtype': 'Data', + 'width': '120' }, { - "fieldname": "practitioner", - "label": _("Requesting Practitioner"), - "fieldtype": "Link", - "options": "Healthcare Practitioner", - "width": "120" + 'fieldname': 'employee', + 'label': _('Lab Technician'), + 'fieldtype': 'Link', + 'options': 'Employee', + 'width': '120' }, { - "fieldname": "employee", - "label": _("Lab Technician"), - "fieldtype": "Link", - "options": "Employee", - "width": "120" + 'fieldname': 'status', + 'label': _('Status'), + 'fieldtype': 'Data', + 'width': '100' }, { - "fieldname": "status", - "label": _("Status"), - "fieldtype": "Data", - "width": "100" + 'fieldname': 'invoiced', + 'label': _('Invoiced'), + 'fieldtype': 'Check', + 'width': '100' }, { - "fieldname": "invoiced", - "label": _("Invoiced"), - "fieldtype": "Check", - "width": "100" + 'fieldname': 'result_date', + 'label': _('Result Date'), + 'fieldtype': 'Date', + 'width': '100' }, { - "fieldname": "result_date", - "label": _("Result Date"), - "fieldtype": "Date", - "width": "100" + 'fieldname': 'practitioner', + 'label': _('Requesting Practitioner'), + 'fieldtype': 'Link', + 'options': 'Healthcare Practitioner', + 'width': '120' }, { - "fieldname": "department", - "label": _("Medical Department"), - "fieldtype": "Link", - "options": "Medical Department", - "width": "100" + 'fieldname': 'department', + 'label': _('Medical Department'), + 'fieldtype': 'Link', + 'options': 'Medical Department', + 'width': '100' } ] @@ -138,4 +140,72 @@ def get_conditions(filters): if filters.get(key): conditions[key] = value - return conditions \ No newline at end of file + return conditions + +def get_chart_data(data): + if not data: + return None + + labels = ['Completed', 'Approved', 'Rejected'] + + status_wise_data = { + 'Completed': 0, + 'Approved': 0, + 'Rejected': 0 + } + + datasets = [] + + for entry in data: + status_wise_data[entry.status] += 1 + + datasets.append({ + 'name': 'Lab Test Status', + 'values': [status_wise_data.get('Completed'), status_wise_data.get('Approved'), status_wise_data.get('Rejected')] + }) + + chart = { + 'data': { + 'labels': labels, + 'datasets': datasets + }, + 'type': 'donut', + 'height': 300, + } + + return chart + + +def get_report_summary(data): + if not data: + return None + + total_lab_tests = len(data) + invoiced_lab_tests, unbilled_lab_tests = 0, 0 + + for entry in data: + if entry.invoiced: + invoiced_lab_tests += 1 + else: + unbilled_lab_tests += 1 + + return [ + { + 'value': total_lab_tests, + 'indicator': 'Blue', + 'label': 'Total Lab Tests', + 'datatype': 'Int', + }, + { + 'value': invoiced_lab_tests, + 'indicator': 'Green', + 'label': 'Invoiced Lab Tests', + 'datatype': 'Int', + }, + { + 'value': unbilled_lab_tests, + 'indicator': 'Red', + 'label': 'Unbilled Lab Tests', + 'datatype': 'Int', + } + ] From a306af8c089a07d3ac8f65439868997a5cd5f37c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 6 Aug 2020 20:52:02 +0530 Subject: [PATCH 015/175] fix: Add help link in navbar settings --- erpnext/patches.txt | 1 + .../v13_0/add_standard_navbar_items.py | 7 +++ erpnext/public/js/conf.js | 26 ----------- erpnext/setup/install.py | 45 ++++++++++++++++++- 4 files changed, 52 insertions(+), 27 deletions(-) create mode 100644 erpnext/patches/v13_0/add_standard_navbar_items.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 3bd416952f..3f63bf651b 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -718,3 +718,4 @@ erpnext.patches.v13_0.delete_report_requested_items_to_order erpnext.patches.v12_0.update_item_tax_template_company erpnext.patches.v13_0.move_branch_code_to_bank_account erpnext.patches.v13_0.healthcare_lab_module_rename_doctypes +erpnext.patches.v13_0.add_standard_navbar_items #4 diff --git a/erpnext/patches/v13_0/add_standard_navbar_items.py b/erpnext/patches/v13_0/add_standard_navbar_items.py new file mode 100644 index 0000000000..5de99a5abc --- /dev/null +++ b/erpnext/patches/v13_0/add_standard_navbar_items.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals +import frappe +from erpnext.setup.install import add_standard_navbar_items + +def execute(): + # Add standard navbar items for ERPNext in Navbar Settings + add_standard_navbar_items() \ No newline at end of file diff --git a/erpnext/public/js/conf.js b/erpnext/public/js/conf.js index 9870f81910..2af9140f9e 100644 --- a/erpnext/public/js/conf.js +++ b/erpnext/public/js/conf.js @@ -3,32 +3,6 @@ frappe.provide('erpnext'); -// add toolbar icon -$(document).bind('toolbar_setup', function() { - frappe.app.name = "ERPNext"; - - frappe.help_feedback_link = '

Feedback

' - - - $('[data-link="docs"]').attr("href", "https://erpnext.com/docs") - $('[data-link="issues"]').attr("href", "https://github.com/frappe/erpnext/issues") - - - // default documentation goes to erpnext - // $('[data-link-type="documentation"]').attr('data-path', '/erpnext/manual/index'); - - // additional help links for erpnext - var $help_menu = $('.dropdown-help ul .documentation-links'); - $('
  • '+__('Documentation')+'
  • ').insertBefore($help_menu); - $('
  • '+__('User Forum')+'
  • ').insertBefore($help_menu); - $('
  • '+__('Report an Issue')+'
  • ').insertBefore($help_menu); - -}); - // preferred modules for breadcrumbs $.extend(frappe.breadcrumbs.preferred, { "Item Group": "Stock", diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index aa9fbc0a92..b2f0fa2ed6 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -25,12 +25,13 @@ def after_install(): create_default_success_action() create_default_energy_point_rules() add_company_to_session_defaults() + add_standard_navbar_items() frappe.db.commit() def check_setup_wizard_not_completed(): if frappe.db.get_default('desktop:home_page') != 'setup-wizard': - message = """ERPNext can only be installed on a fresh site where the setup wizard is not completed. + message = """ERPNext can only be installed on a fresh site where the setup wizard is not completed. You can reinstall this site (after saving your data) using: bench --site [sitename] reinstall""" frappe.throw(message) @@ -103,3 +104,45 @@ def add_company_to_session_defaults(): "ref_doctype": "Company" }) settings.save() + +def add_standard_navbar_items(): + navbar_settings = frappe.get_single("Navbar Settings") + + erpnext_navbar_items = [ + { + 'item_label': 'Documentation', + 'item_type': 'Route', + 'route': 'https://erpnext.com/docs/user/manual', + 'is_standard': 1 + }, + { + 'item_label': 'User Forum', + 'item_type': 'Route', + 'route': 'https://discuss.erpnext.com', + 'is_standard': 1 + }, + { + 'item_label': 'Report an Issue', + 'item_type': 'Route', + 'route': 'https://github.com/frappe/erpnext/issues', + 'is_standard': 1 + } + ] + + current_nabvar_items = navbar_settings.help_dropdown + navbar_settings.set('help_dropdown', []) + + for item in erpnext_navbar_items: + navbar_settings.append('help_dropdown', item) + + for item in current_nabvar_items: + navbar_settings.append('help_dropdown', { + 'item_label': item.item_label, + 'item_type': item.item_type, + 'route': item.route, + 'action': item.action, + 'is_standard': item.is_standard, + 'hidden': item.hidden + }) + + navbar_settings.save() From af1f46f2d97dbdcfbd1ee020ac79072edcc375fa Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 7 Aug 2020 19:44:20 +0530 Subject: [PATCH 016/175] fix: Add default billing address for purchase documents --- erpnext/public/js/controllers/transaction.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 4e50f3d7f6..862b6fbf9b 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -781,10 +781,23 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ else var date = this.frm.doc.transaction_date; if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && - in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)){ + in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) { 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); + } + }); + } else { set_party_account(set_pricing); } From d01caac85260f692a647d62fa5d46ff5566d81ca Mon Sep 17 00:00:00 2001 From: Anupam K Date: Mon, 3 Aug 2020 09:41:16 +0530 Subject: [PATCH 017/175] report lead detail --- .../crm/report/lead_details/lead_details.py | 15 ++++----------- .../lost_opportunity/lost_opportunity.py | 18 +++++++++++++++--- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/erpnext/crm/report/lead_details/lead_details.py b/erpnext/crm/report/lead_details/lead_details.py index 079f32e040..9f4ae60679 100644 --- a/erpnext/crm/report/lead_details/lead_details.py +++ b/erpnext/crm/report/lead_details/lead_details.py @@ -68,12 +68,6 @@ def get_columns(): "fieldtype": "Data", "width": 120 }, - { - "label": _("Notes"), - "fieldname": "notes", - "fieldtype": "Data", - "width": 120 - }, { "label": _("Owner"), "fieldname": "owner", @@ -129,7 +123,6 @@ def get_data(filters): `tabLead`.email_id, `tabLead`.mobile_no, `tabLead`.phone, - `tabLead`.notes, `tabLead`.owner, `tabLead`.company, concat_ws(', ', @@ -153,12 +146,12 @@ def get_data(filters): `tabLead`.creation asc """.format(conditions=get_conditions(filters)), filters, as_dict=1) def get_conditions(filters) : - conditions = [] + conditions = "" if filters.get("territory"): - conditions.append("territory=%(territory)s") + conditions+=" and territory=%(territory)s " if filters.get("status"): - conditions.append("status=%(status)s") + conditions+=" and status=%(status)s " - return " and {}".format(" and ".join(conditions)) if conditions else "" + return conditions diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.py b/erpnext/crm/report/lost_opportunity/lost_opportunity.py index 75c0b2d981..094fa961f7 100644 --- a/erpnext/crm/report/lost_opportunity/lost_opportunity.py +++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.py @@ -50,6 +50,12 @@ def get_columns(): "fieldtype": "Link", "options": "User", "width": 120 + }, + { + "label": _("Lost Reasons"), + "fieldname": "lost_reason", + "fieldtype": "Data", + "width": 220 } ] return columns @@ -62,14 +68,20 @@ def get_data(filters): `tabOpportunity`.party_name, `tabOpportunity`.customer_name, `tabOpportunity`.opportunity_type, - `tabOpportunity`.contact_by + `tabOpportunity`.contact_by, + GROUP_CONCAT(`tabLost Reason Detail`.lost_reason separator ', ') lost_reason FROM `tabOpportunity` + LEFT JOIN `tabLost Reason Detail` + ON `tabLost Reason Detail`.parenttype = 'Opportunity' and `tabLost Reason Detail`.parent = `tabOpportunity`.name WHERE - status = 'Lost' and company = %(company)s + `tabOpportunity`.status = 'Lost' and `tabOpportunity`.company = %(company)s {conditions} + GROUP BY + `tabOpportunity`.name ORDER BY - creation asc """.format(conditions=get_conditions(filters)), filters, as_dict=1) + `tabOpportunity`.creation asc """.format(conditions=get_conditions(filters)), filters, as_dict=1) + def get_conditions(filters) : conditions = [] From 069a54e5c38c10f8e97d16e4e279119adea69423 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 10 Aug 2020 16:01:01 +0530 Subject: [PATCH 018/175] fix: Cancellation of accounting transactions within closed accounting period --- erpnext/accounts/general_ledger.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index cf3deb828f..01d3903d28 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -45,8 +45,8 @@ def validate_accounting_period(gl_map): }, as_dict=1) if accounting_periods: - frappe.throw(_("You can't create accounting entries in the closed accounting period {0}") - .format(accounting_periods[0].name), ClosedAccountingPeriod) + frappe.throw(_("You cannot create or cancel any accounting entries with in the closed Accounting Period {0}") + .format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod) def process_gl_map(gl_map, merge_entries=True): if merge_entries: @@ -301,8 +301,9 @@ def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None, }) if gl_entries: - set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no']) + validate_accounting_period(gl_entries) check_freezing_date(gl_entries[0]["posting_date"], adv_adj) + set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no']) for entry in gl_entries: entry['name'] = None @@ -342,7 +343,7 @@ def set_as_cancel(voucher_type, voucher_no): """ Set is_cancelled=1 in all original gl entries for the voucher """ - frappe.db.sql("""update `tabGL Entry` set is_cancelled = 1, + frappe.db.sql("""UPDATE `tabGL Entry` SET is_cancelled = 1, modified=%s, modified_by=%s where voucher_type=%s and voucher_no=%s and is_cancelled = 0""", (now(), frappe.session.user, voucher_type, voucher_no)) From 58d05ac0e0a8ef89db41dc8dd5dea7686da8b74e Mon Sep 17 00:00:00 2001 From: Saurabh Date: Mon, 10 Aug 2020 19:36:45 +0530 Subject: [PATCH 019/175] fix: check setup complete flag insted setup page --- erpnext/setup/install.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index aa9fbc0a92..50f9d84fce 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -7,6 +7,7 @@ import frappe from erpnext.accounts.doctype.cash_flow_mapper.default_cash_flow_mapper import DEFAULT_MAPPERS from .default_success_action import get_default_success_action from frappe import _ +from frappe.utils import cint from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to from frappe.custom.doctype.custom_field.custom_field import create_custom_field from erpnext.setup.default_energy_point_rules import get_default_energy_point_rules @@ -29,8 +30,8 @@ def after_install(): def check_setup_wizard_not_completed(): - if frappe.db.get_default('desktop:home_page') != 'setup-wizard': - message = """ERPNext can only be installed on a fresh site where the setup wizard is not completed. + if cint(frappe.db.get_single_value('System Settings', 'setup_complete') or 0): + message = """ERPNext can only be installed on a fresh site where the setup wizard is not completed. You can reinstall this site (after saving your data) using: bench --site [sitename] reinstall""" frappe.throw(message) From 49dd8782fdaed88aa71cdc3ac38fcbc79900e6fc Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 11 Aug 2020 14:39:04 +0530 Subject: [PATCH 020/175] fix: margin overflow cutting the focus state --- erpnext/public/less/products.less | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/public/less/products.less b/erpnext/public/less/products.less index 79f57b331a..5e744ceac5 100644 --- a/erpnext/public/less/products.less +++ b/erpnext/public/less/products.less @@ -22,6 +22,8 @@ } .filter-options { + margin-left: -5px; + padding-left: 5px; max-height: 300px; overflow: auto; } From bfb219612e143139b535d273556898b7d0ecaeaf Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 13 Aug 2020 11:34:05 +0530 Subject: [PATCH 021/175] feat: enable total row in Gross Profit Report --- .../report/gross_profit/gross_profit.json | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.json b/erpnext/accounts/report/gross_profit/gross_profit.json index 9cfb0627d3..cd6bac2d77 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.json +++ b/erpnext/accounts/report/gross_profit/gross_profit.json @@ -1,24 +1,23 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2013-02-25 17:03:34", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2017-02-24 20:12:22.464240", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Gross Profit", - "owner": "Administrator", - "ref_doctype": "Sales Invoice", - "report_name": "Gross Profit", - "report_type": "Script Report", + "add_total_row": 1, + "creation": "2013-02-25 17:03:34", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 3, + "is_standard": "Yes", + "modified": "2020-08-13 11:26:39.112352", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Gross Profit", + "owner": "Administrator", + "ref_doctype": "Sales Invoice", + "report_name": "Gross Profit", + "report_type": "Script Report", "roles": [ { "role": "Accounts Manager" - }, + }, { "role": "Accounts User" } From b3bd780d46cd9d9d48033d9bb8b074a25942e598 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Thu, 13 Aug 2020 17:07:15 +0530 Subject: [PATCH 022/175] fix: codasy dix --- .../report/budget_variance_report/budget_variance_report.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js index 30415d1e65..f547ca619b 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js @@ -74,9 +74,9 @@ frappe.query_reports["Budget Variance Report"] = { ], "formatter": function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - + if (column.fieldname.includes('variance')) { - + if (data[column.fieldname] < 0) { value = "" + value + ""; } From a16b24d50a7580862e1610a95838de2545bd1a58 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 28 Jul 2020 11:06:42 +0530 Subject: [PATCH 023/175] feat: add enable or disable invoicing and print format field --- .../membership_settings.js | 9 +++++++ .../membership_settings.json | 26 +++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index 8c0e3a4fa7..bbfece31d4 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -10,6 +10,15 @@ frappe.ui.form.on("Membership Settings", { }) }); } + + frm.set_query('print_format', function(doc) { + return { + filters: { + "doc_type": "Sales Invoice" + } + }; + }); + frm.trigger("add_generate_button"); }, diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 52b9d01088..29013fafdc 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -9,7 +9,10 @@ "razorpay_settings_section", "billing_cycle", "billing_frequency", - "webhook_secret" + "webhook_secret", + "column_break_6", + "enable_auto_invoicing", + "print_format" ], "fields": [ { @@ -41,11 +44,30 @@ "fieldtype": "Password", "label": "Webhook Secret", "read_only": 1 + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "enable_auto_invoicing", + "fieldtype": "Check", + "label": "Enable Auto Invoicing" + }, + { + "depends_on": "eval:doc.enable_auto_invoicing", + "fieldname": "print_format", + "fieldtype": "Link", + "label": "Print Format", + "mandatory_depends_on": "eval:doc.enable_auto_invoicing", + "options": "Print Format" } ], + "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-05-22 12:38:27.103759", + "modified": "2020-07-28 11:01:40.125896", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From 3a1868dae813a41ee9e124ae05f20374214c35db Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 28 Jul 2020 11:07:35 +0530 Subject: [PATCH 024/175] feat: add item field --- .../doctype/membership_type/membership_type.js | 6 +++++- .../doctype/membership_type/membership_type.json | 12 ++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js index 226981dc78..43311a2c96 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.js +++ b/erpnext/non_profit/doctype/membership_type/membership_type.js @@ -5,6 +5,10 @@ frappe.ui.form.on('Membership Type', { refresh: function(frm) { frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false); - }) + }); + + frappe.db.get_single_value("Membership Settings", "enable_auto_invoicing").then(val => { + if (val) frm.set_df_property('linked_item', 'hidden', false); + }); } }); diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.json b/erpnext/non_profit/doctype/membership_type/membership_type.json index 319078fd6c..a163568bb9 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.json +++ b/erpnext/non_profit/doctype/membership_type/membership_type.json @@ -8,7 +8,8 @@ "field_order": [ "membership_type", "amount", - "razorpay_plan_id" + "razorpay_plan_id", + "linked_item" ], "fields": [ { @@ -33,10 +34,17 @@ "hidden": 1, "label": "Razorpay Plan ID", "unique": 1 + }, + { + "fieldname": "linked_item", + "fieldtype": "Link", + "label": "Linked Item", + "options": "Item" } ], + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-03-30 12:54:07.850857", + "modified": "2020-07-28 10:57:50.821375", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Type", From 96dc67c35edf360721c83ac4ae87e8d76d3851f0 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 28 Jul 2020 11:43:54 +0530 Subject: [PATCH 025/175] feat: add copy button and docs link --- .../membership_settings/membership_settings.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index bbfece31d4..f5e0274c3a 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -19,7 +19,12 @@ frappe.ui.form.on("Membership Settings", { }; }); + let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership"; + + frm.set_intro(__("You can learn more about memberships in the manual. ") + `${__('ERPNext Docs')}`, true); + frm.trigger("add_generate_button"); + frm.trigger("add_copy_buttonn"); }, add_generate_button: function(frm) { @@ -36,4 +41,12 @@ frappe.ui.form.on("Membership Settings", { }); }); }, + + add_copy_buttonn: function(frm) { + if (frm.doc.webhook_secret) { + frm.add_custom_button(__("Copy Webhook URL"), () => { + frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.membership.membership.trigger_razorpay_subscription`); + }); + } + } }); From 811ac909f93d543cd537241fa8522393281c9bcf Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 28 Jul 2020 12:27:37 +0530 Subject: [PATCH 026/175] feat: add auto invoice creation fields and controllers --- .../doctype/membership/membership.py | 29 ++++++++++ .../membership_settings.js | 10 ++++ .../membership_settings.json | 57 +++++++++++++++++-- 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 729e111e57..eea18393f1 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -57,6 +57,35 @@ class Membership(Document): self.load_from_db() self.db_set('paid', 1) + def generate_and_send_invoice(self): + if not self.paid: + frappe.throw(_("The payment for this membership is not paid. To generate invoice mark the paid check")) + + member = frappe.get_doc("Member", self.member) + plan = frappe.get_doc("Membership Type", self.membership_type) + settings = frappe.get_doc("Membership Settings") + + invoice = make_invoice(self, member, plan, settings) + +def make_invoice(membership, member, plan, settings): + invoice = frappe.get_doc({ + 'doctype': 'Sales Invoice', + 'customer': member.customer, + 'debit_to': settings.debit_account, + 'currency': membership.currency, + 'is_pos': 0, + 'items': [ + { + 'item_code': plan.linked_item, + 'rate': membership.amount, + 'qty': 1 + } + ] + }) + + invoice.insert(ignore_permissions=True) + invoice.submit() + def get_member_based_on_subscription(subscription_id, email): members = frappe.get_all("Member", filters={ 'subscription_id': subscription_id, diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index f5e0274c3a..02ef0292ea 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -19,6 +19,16 @@ frappe.ui.form.on("Membership Settings", { }; }); + frm.set_query('debit_account', function(doc) { + return { + filters: { + 'account_type': 'Receivable', + 'is_group': 0, + 'company': frm.doc.company + } + }; + }); + let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership"; frm.set_intro(__("You can learn more about memberships in the manual. ") + `${__('ERPNext Docs')}`, true); diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 29013fafdc..37bea49826 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -11,8 +11,15 @@ "billing_frequency", "webhook_secret", "column_break_6", + "print_format", + "company", + "debit_account", + "column_break_9", "enable_auto_invoicing", - "print_format" + "send_invoice", + "section_break_10", + "new_member", + "renewal" ], "fields": [ { @@ -47,7 +54,8 @@ }, { "fieldname": "column_break_6", - "fieldtype": "Column Break" + "fieldtype": "Section Break", + "label": "Invoicing" }, { "default": "0", @@ -56,18 +64,57 @@ "label": "Enable Auto Invoicing" }, { - "depends_on": "eval:doc.enable_auto_invoicing", "fieldname": "print_format", "fieldtype": "Link", "label": "Print Format", - "mandatory_depends_on": "eval:doc.enable_auto_invoicing", "options": "Print Format" + }, + { + "fieldname": "new_member", + "fieldtype": "Text Editor", + "label": "Message for New Member", + "mandatory_depends_on": "eval:doc.send_invoice" + }, + { + "fieldname": "renewal", + "fieldtype": "Text Editor", + "label": "Message for Renewal", + "mandatory_depends_on": "eval:doc.send_invoice" + }, + { + "depends_on": "eval:doc.company", + "fieldname": "debit_account", + "fieldtype": "Link", + "label": "Debit Account", + "options": "Account" + }, + { + "depends_on": "eval:doc.send_invoice", + "fieldname": "section_break_10", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "default": "0", + "fieldname": "send_invoice", + "fieldtype": "Check", + "label": "Send Email with Invoice" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-07-28 11:01:40.125896", + "modified": "2020-07-28 12:18:35.289893", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From 15340e0c7f7c21da75403a54b66b502650f04a07 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 28 Jul 2020 12:56:14 +0530 Subject: [PATCH 027/175] feat: send invoice via email --- .../doctype/membership/membership.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index eea18393f1..82b3145a07 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -67,6 +67,25 @@ class Membership(Document): invoice = make_invoice(self, member, plan, settings) + if invoice and settings.send_invoice and self.membership_status in ["New", "Current"]: + print("Sending") + message = settings.new_message if self.membership_status == "New" else settings.renewal + email = member.email_id if member.email_id else member.email + + email_args = { + "recipients": [email], + "message": message, + "subject": _('Here is your invoice'), + "attachments": [frappe.attach_print("Sales Invoice", invoice.name, print_format=settings.print_format)], + "reference_doctype": self.doctype, + "reference_name": self.name + } + if not frappe.flags.in_test: + frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) + else: + frappe.sendmail(**email_args) + + def make_invoice(membership, member, plan, settings): invoice = frappe.get_doc({ 'doctype': 'Sales Invoice', @@ -86,6 +105,8 @@ def make_invoice(membership, member, plan, settings): invoice.insert(ignore_permissions=True) invoice.submit() + return invoice + def get_member_based_on_subscription(subscription_id, email): members = frappe.get_all("Member", filters={ 'subscription_id': subscription_id, From 62e344188a2f82f70eca1910081dc6218b1655c0 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 13:46:50 +0530 Subject: [PATCH 028/175] feat: update settings and link filters --- .../membership_settings.js | 10 ++- .../membership_settings.json | 83 ++++++++++++------- 2 files changed, 60 insertions(+), 33 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index 02ef0292ea..1d894027b0 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -11,7 +11,7 @@ frappe.ui.form.on("Membership Settings", { }); } - frm.set_query('print_format', function(doc) { + frm.set_query('inv_print_format', function(doc) { return { filters: { "doc_type": "Sales Invoice" @@ -19,6 +19,14 @@ frappe.ui.form.on("Membership Settings", { }; }); + frm.set_query('membership_print_format', function(doc) { + return { + filters: { + "doc_type": "Membership" + } + }; + }); + frm.set_query('debit_account', function(doc) { return { filters: { diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 37bea49826..7eeb7fbde2 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -11,15 +11,14 @@ "billing_frequency", "webhook_secret", "column_break_6", - "print_format", + "enable_auto_invoicing", "company", "debit_account", "column_break_9", - "enable_auto_invoicing", + "send_email", "send_invoice", - "section_break_10", - "new_member", - "renewal" + "membership_print_format", + "inv_print_format" ], "fields": [ { @@ -61,60 +60,63 @@ "default": "0", "fieldname": "enable_auto_invoicing", "fieldtype": "Check", - "label": "Enable Auto Invoicing" - }, - { - "fieldname": "print_format", - "fieldtype": "Link", - "label": "Print Format", - "options": "Print Format" - }, - { - "fieldname": "new_member", - "fieldtype": "Text Editor", - "label": "Message for New Member", + "label": "Enable Auto Invoicing", "mandatory_depends_on": "eval:doc.send_invoice" }, { - "fieldname": "renewal", - "fieldtype": "Text Editor", - "label": "Message for Renewal", - "mandatory_depends_on": "eval:doc.send_invoice" - }, - { - "depends_on": "eval:doc.company", + "depends_on": "eval:doc.enable_auto_invoicing", "fieldname": "debit_account", "fieldtype": "Link", "label": "Debit Account", + "mandatory_depends_on": "eval:doc.enable_auto_invoicing", "options": "Account" }, - { - "depends_on": "eval:doc.send_invoice", - "fieldname": "section_break_10", - "fieldtype": "Section Break", - "hide_border": 1 - }, { "fieldname": "column_break_9", "fieldtype": "Column Break" }, { + "depends_on": "eval:doc.enable_auto_invoicing", "fieldname": "company", "fieldtype": "Link", "label": "Company", + "mandatory_depends_on": "eval:doc.enable_auto_invoicing", "options": "Company" }, { "default": "0", + "depends_on": "eval:doc.enable_auto_invoicing && doc.send_email", "fieldname": "send_invoice", "fieldtype": "Check", - "label": "Send Email with Invoice" + "label": "Send Invoice with Email" + }, + { + "default": "0", + "fieldname": "send_email", + "fieldtype": "Check", + "label": "Send Acknowledge Email" + }, + { + "depends_on": "eval: doc.send_invoice", + "fieldname": "inv_print_format", + "fieldtype": "Link", + "label": "Invoice Print Format", + "mandatory_depends_on": "eval: doc.send_invoice", + "options": "Print Format" + }, + { + "depends_on": "eval:doc.send_email", + "fieldname": "membership_print_format", + "fieldtype": "Link", + "label": "Membership Print Format", + "mandatory_depends_on": "eval:doc.send_email", + "options": "Print Format" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-07-28 12:18:35.289893", + "modified": "2020-07-31 13:45:28.868235", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", @@ -129,6 +131,23 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "Non Profit Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "print": 1, + "read": 1, + "role": "Non Profit Member", + "share": 1 } ], "quick_entry": 1, From 1678eb9b506af6a3f4a1e20c1e91136c17493a13 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 14:00:22 +0530 Subject: [PATCH 029/175] feat: drop payload column --- .../doctype/membership/membership.json | 16 +++++++--------- erpnext/patches.txt | 1 + .../v13_0/drop_razorpay_payload_column.py | 7 +++++++ 3 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 erpnext/patches/v13_0/drop_razorpay_payload_column.py diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json index 238f4c31fd..95bb3a5d84 100644 --- a/erpnext/non_profit/doctype/membership/membership.json +++ b/erpnext/non_profit/doctype/membership/membership.json @@ -19,10 +19,10 @@ "paid", "currency", "amount", + "invoice", "razorpay_details_section", "subscription_id", - "payment_id", - "webhook_payload" + "payment_id" ], "fields": [ { @@ -118,17 +118,15 @@ "read_only": 1 }, { - "fieldname": "webhook_payload", - "fieldtype": "Code", - "hidden": 1, - "label": "Webhook Payload", - "options": "JSON", - "read_only": 1 + "fieldname": "invoice", + "fieldtype": "Link", + "label": "Invoice", + "options": "Sales Invoice" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-07-27 14:28:11.532696", + "modified": "2020-07-31 13:57:02.328995", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 361fe8352a..17b46ab649 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -721,3 +721,4 @@ erpnext.patches.v13_0.healthcare_lab_module_rename_doctypes erpnext.patches.v13_0.stock_entry_enhancements erpnext.patches.v12_0.update_state_code_for_daman_and_diu erpnext.patches.v12_0.rename_lost_reason_detail +erpnext.patches.v13_0.drop_razorpay_payload_column diff --git a/erpnext/patches/v13_0/drop_razorpay_payload_column.py b/erpnext/patches/v13_0/drop_razorpay_payload_column.py new file mode 100644 index 0000000000..62f0373008 --- /dev/null +++ b/erpnext/patches/v13_0/drop_razorpay_payload_column.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + if frappe.db.exists("DocType", "Membership Settings"): + if 'webhook_payload' in frappe.db.get_table_columns("Membership Settings"): + frappe.db.sql("alter table `tabMembership Settings` drop column webhook_payload") \ No newline at end of file From c8d9e7f77bb82284eeb322290f28273a6c2d46e6 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 14:31:24 +0530 Subject: [PATCH 030/175] feat: add message field --- .../membership_settings.json | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 7eeb7fbde2..2452a763ce 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -18,7 +18,9 @@ "send_email", "send_invoice", "membership_print_format", - "inv_print_format" + "inv_print_format", + "section_break_15", + "message" ], "fields": [ { @@ -111,12 +113,24 @@ "label": "Membership Print Format", "mandatory_depends_on": "eval:doc.send_email", "options": "Print Format" + }, + { + "depends_on": "eval:doc.send_email", + "fieldname": "section_break_15", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "depends_on": "eval:doc.send_email", + "fieldname": "message", + "fieldtype": "Text Editor", + "label": "Message" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-07-31 13:45:28.868235", + "modified": "2020-07-31 14:30:15.701767", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From 8e0314e7f684f0ae52963d4daf5a692ccd0308fa Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 14:31:39 +0530 Subject: [PATCH 031/175] feat: separate invoice generation and email --- .../doctype/membership/membership.py | 69 ++++++++++++++----- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 82b3145a07..c960daa1d3 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -57,34 +57,65 @@ class Membership(Document): self.load_from_db() self.db_set('paid', 1) - def generate_and_send_invoice(self): - if not self.paid: - frappe.throw(_("The payment for this membership is not paid. To generate invoice mark the paid check")) + def generate_invoice(self, save=True): + if not (self.paid or self.currency or self.amount): + frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details")) + + if self.invoice: + frappe.throw(_("An invoice is already linked to this document")) member = frappe.get_doc("Member", self.member) plan = frappe.get_doc("Membership Type", self.membership_type) settings = frappe.get_doc("Membership Settings") + attachments = [] + + if not member.customer: + frappe.throw(_("No customer linked to member {}", [member.name])) + + if not settings.debit_account: + frappe.throw(_("You need to set Debit Account in Membership Settings")) + + if not settings.company: + frappe.throw(_("You need to set Default Company for invoicing in Membership Settings")) invoice = make_invoice(self, member, plan, settings) + self.invoice = invoice.name - if invoice and settings.send_invoice and self.membership_status in ["New", "Current"]: - print("Sending") - message = settings.new_message if self.membership_status == "New" else settings.renewal - email = member.email_id if member.email_id else member.email + if save: + self.save() - email_args = { - "recipients": [email], - "message": message, - "subject": _('Here is your invoice'), - "attachments": [frappe.attach_print("Sales Invoice", invoice.name, print_format=settings.print_format)], - "reference_doctype": self.doctype, - "reference_name": self.name - } - if not frappe.flags.in_test: - frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) - else: - frappe.sendmail(**email_args) + return invoice + def send_acknowlement(self): + settings = frappe.get_doc("Membership Settings") + if not settings.send_email: + frappe.throw(_("You need to enable Send Acknowledge Email in Membership Settings")) + + member = frappe.get_doc("Member", self.member) + plan = frappe.get_doc("Membership Type", self.membership_type) + email = member.email_id if member.email_id else member.email + attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)] + + if self.invoice and settings.send_invoice: + attachments.append(frappe.attach_print("Sales Invoice", self.invoice, print_format=settings.inv_print_format)) + + email_args = { + "recipients": [email], + "message": settings.message, + "subject": _('Here is your invoice'), + "attachments": [frappe.attach_print("Sales Invoice", invoice.name, print_format=settings.inv_print_format)], + "reference_doctype": self.doctype, + "reference_name": self.name + } + + if not frappe.flags.in_test: + frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) + else: + frappe.sendmail(**email_args) + + def generate_and_send_invoice(self): + invoice = self.generate_invoice(False) + self.send_acknowlement() def make_invoice(membership, member, plan, settings): invoice = frappe.get_doc({ From 3a67a78ece8ad5a102e0d9623e3666c607532df4 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 14:54:06 +0530 Subject: [PATCH 032/175] feat: add custom buttons for invoice and email --- .../non_profit/doctype/membership/membership.js | 16 ++++++++++++++++ .../non_profit/doctype/membership/membership.py | 1 - 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js index 554549a0bd..8408a6a1f0 100644 --- a/erpnext/non_profit/doctype/membership/membership.js +++ b/erpnext/non_profit/doctype/membership/membership.js @@ -8,6 +8,22 @@ frappe.ui.form.on('Membership', { }) }, + refresh: function(frm) { + !frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => { + frm.call("generate_invoice", { + save: true + }).then(() => { + frm.reload_doc(); + }); + }); + + frm.add_custom_button("Send Acknowledgement", () => { + frm.call("send_acknowlement").then(() => { + frm.reload_doc(); + }); + }); + }, + onload: function(frm) { frm.add_fetch('membership_type', 'amount', 'amount'); } diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index c960daa1d3..8d1e44da5d 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -67,7 +67,6 @@ class Membership(Document): member = frappe.get_doc("Member", self.member) plan = frappe.get_doc("Membership Type", self.membership_type) settings = frappe.get_doc("Membership Settings") - attachments = [] if not member.customer: frappe.throw(_("No customer linked to member {}", [member.name])) From 733bde31c9e3de279a77de361c4392fa649777ec Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 15:06:51 +0530 Subject: [PATCH 033/175] fix: doctype name in patch --- erpnext/patches/v13_0/drop_razorpay_payload_column.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v13_0/drop_razorpay_payload_column.py b/erpnext/patches/v13_0/drop_razorpay_payload_column.py index 62f0373008..8980fd0039 100644 --- a/erpnext/patches/v13_0/drop_razorpay_payload_column.py +++ b/erpnext/patches/v13_0/drop_razorpay_payload_column.py @@ -2,6 +2,6 @@ from __future__ import unicode_literals import frappe def execute(): - if frappe.db.exists("DocType", "Membership Settings"): - if 'webhook_payload' in frappe.db.get_table_columns("Membership Settings"): - frappe.db.sql("alter table `tabMembership Settings` drop column webhook_payload") \ No newline at end of file + if frappe.db.exists("DocType", "Membership"): + if 'webhook_payload' in frappe.db.get_table_columns("Membership"): + frappe.db.sql("alter table `tabMembership` drop column webhook_payload") \ No newline at end of file From 6fbe9b54041b1325284774b60e9e475f8b2208a3 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sat, 1 Aug 2020 13:58:21 +0530 Subject: [PATCH 034/175] fix: remove payload --- erpnext/non_profit/doctype/membership/membership.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 8d1e44da5d..bfc2661a8f 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -200,7 +200,6 @@ def trigger_razorpay_subscription(*args, **kwargs): "currency": "INR", "paid": 1, "payment_id": payment.id, - "webhook_payload": data_json, "from_date": datetime.fromtimestamp(subscription.current_start), "to_date": datetime.fromtimestamp(subscription.current_end), "amount": payment.amount / 100 # Convert to rupees from paise From 87ddec069e2da7d624554696eeeba7f9d7db3e56 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sat, 1 Aug 2020 14:01:41 +0530 Subject: [PATCH 035/175] refactor: try block across the function --- erpnext/non_profit/doctype/membership/membership.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index bfc2661a8f..a3a1b5b275 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -157,9 +157,7 @@ def verify_signature(data): controller.verify_signature(data, signature, key) - -@frappe.whitelist(allow_guest=True) -def trigger_razorpay_subscription(*args, **kwargs): +def make_membership_entry(*args, **kwargs): data = frappe.request.get_data(as_text=True) try: verify_signature(data) @@ -218,6 +216,14 @@ def trigger_razorpay_subscription(*args, **kwargs): return { status: 'Success' } +@frappe.whitelist(allow_guest=True) +def trigger_razorpay_subscription(*args, **kwargs): + try: + return make_membership_entry(*args, **kwargs) + except Exception as e: + log = frappe.log_error(e, "Webhook Failed") + return { status: 'Failed' } + def notify_failure(log): try: From d7139bbd437f8dc4f58e64ae36e6733539b66952 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sun, 2 Aug 2020 12:03:01 +0530 Subject: [PATCH 036/175] fix: type error --- erpnext/non_profit/doctype/membership/membership.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index a3a1b5b275..dc243d8c75 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -182,10 +182,10 @@ def make_membership_entry(*args, **kwargs): except Exception as e: error_log = frappe.log_error(frappe.get_traceback() + '\n' + data_json , _("Membership Webhook Failed")) notify_failure(error_log) - return { status: 'Failed' } + return { 'status': 'Failed' } if not member: - return { status: 'Failed' } + return { 'status': 'Failed' } try: if data.event == "subscription.activated": member.customer_id = payment.customer_id @@ -212,9 +212,9 @@ def make_membership_entry(*args, **kwargs): except Exception as e: log = frappe.log_error(e, "Error creating membership entry") notify_failure(log) - return { status: 'Failed' } + return { 'status': 'Failed' } - return { status: 'Success' } + return { 'status': 'Success' } @frappe.whitelist(allow_guest=True) def trigger_razorpay_subscription(*args, **kwargs): @@ -222,7 +222,7 @@ def trigger_razorpay_subscription(*args, **kwargs): return make_membership_entry(*args, **kwargs) except Exception as e: log = frappe.log_error(e, "Webhook Failed") - return { status: 'Failed' } + return { 'status': 'Failed' } def notify_failure(log): From 98bcedd09c510c52fe823342e849bc707d9b20e5 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sun, 2 Aug 2020 14:50:27 +0530 Subject: [PATCH 037/175] feat: create member if not already exists --- erpnext/non_profit/doctype/member/member.py | 2 +- .../doctype/membership/membership.py | 85 +++++++++++-------- .../membership_type/membership_type.py | 2 +- 3 files changed, 50 insertions(+), 39 deletions(-) diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index c52082ca23..6c2da07bb7 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -78,7 +78,7 @@ def create_member(user_details): member.update({ "member_name": user_details.fullname, "email_id": user_details.email, - "pan_number": user_details.pan, + "pan_number": user_details.pan or None, "membership_type": user_details.plan_id, "customer": create_customer(user_details) }) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index dc243d8c75..fc89396fa8 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -10,6 +10,7 @@ from datetime import datetime from frappe.model.document import Document from frappe.email import sendmail_to_system_managers from frappe.utils import add_days, add_years, nowdate, getdate, add_months, get_link_to_form +from erpnext.non_profit.doctype.member.member import create_member from frappe import _ import erpnext @@ -142,6 +143,7 @@ def get_member_based_on_subscription(subscription_id, email): 'subscription_id': subscription_id, 'email_id': email }, order_by="creation desc") + try: return frappe.get_doc("Member", members[0]['name']) except: @@ -157,14 +159,15 @@ def verify_signature(data): controller.verify_signature(data, signature, key) -def make_membership_entry(*args, **kwargs): +@frappe.whitelist(allow_guest=True) +def trigger_razorpay_subscription(*args, **kwargs): data = frappe.request.get_data(as_text=True) try: verify_signature(data) except Exception as e: - signature = frappe.request.headers.get('X-Razorpay-Signature') - log = "{0} \n\n {1} \n\n {2} \n\n {3}".format(e, frappe.get_traceback(), signature, data) - frappe.log_error(e, "Webhook Verification Error") + log = frappe.log_error(e, "Webhook Verification Error") + notify_failure(log) + return { 'status': 'Failed', 'reason': e} if isinstance(data, six.string_types): data = json.loads(data) @@ -177,34 +180,42 @@ def make_membership_entry(*args, **kwargs): payment = frappe._dict(payment) try: - data_json = json.dumps(data, indent=4, sort_keys=True) + if not data.event == "subscription.charged": + return + member = get_member_based_on_subscription(subscription.id, payment.email) - except Exception as e: - error_log = frappe.log_error(frappe.get_traceback() + '\n' + data_json , _("Membership Webhook Failed")) - notify_failure(error_log) - return { 'status': 'Failed' } + if not member: + member = create_member(frappe._dict({ + 'fullname': payment.email, + 'email': payment.email, + 'plan_id': get_plan_from_razorpay_id(subscription.plan_id) + })) - if not member: - return { 'status': 'Failed' } - try: - if data.event == "subscription.activated": + member.subscription_id = subscription.id member.customer_id = payment.customer_id - elif data.event == "subscription.charged": - membership = frappe.new_doc("Membership") - membership.update({ - "member": member.name, - "membership_status": "Current", - "membership_type": member.membership_type, - "currency": "INR", - "paid": 1, - "payment_id": payment.id, - "from_date": datetime.fromtimestamp(subscription.current_start), - "to_date": datetime.fromtimestamp(subscription.current_end), - "amount": payment.amount / 100 # Convert to rupees from paise - }) - membership.insert(ignore_permissions=True) + if subscription.notes and type(subscription.notes) == dict: + notes = '\n'.join("{}: {}".format(k, v) for k, v in subscription.notes.items()) + member.add_comment("Comment", notes) + elif subscription.notes and type(subscription.notes) == str: + member.add_comment("Comment", subscription.notes) - # Update these values anyway + + # Update Membership + membership = frappe.new_doc("Membership") + membership.update({ + "member": member.name, + "membership_status": "Current", + "membership_type": member.membership_type, + "currency": "INR", + "paid": 1, + "payment_id": payment.id, + "from_date": datetime.fromtimestamp(subscription.current_start), + "to_date": datetime.fromtimestamp(subscription.current_end), + "amount": payment.amount / 100 # Convert to rupees from paise + }) + membership.insert(ignore_permissions=True) + + # Update membership values member.subscription_start = datetime.fromtimestamp(subscription.start_at) member.subscription_end = datetime.fromtimestamp(subscription.end_at) member.subscription_activated = 1 @@ -212,18 +223,10 @@ def make_membership_entry(*args, **kwargs): except Exception as e: log = frappe.log_error(e, "Error creating membership entry") notify_failure(log) - return { 'status': 'Failed' } + return { 'status': 'Failed', 'reason': e} return { 'status': 'Success' } -@frappe.whitelist(allow_guest=True) -def trigger_razorpay_subscription(*args, **kwargs): - try: - return make_membership_entry(*args, **kwargs) - except Exception as e: - log = frappe.log_error(e, "Webhook Failed") - return { 'status': 'Failed' } - def notify_failure(log): try: @@ -237,3 +240,11 @@ Administrator""".format(get_link_to_form("Error Log", log.name)) sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content) except: pass + +def get_plan_from_razorpay_id(plan_id): + plan = frappe.get_all("Membership Type", filters={'razorpay_plan_id': plan_id}, order_by="creation desc") + + try: + return plan[0]['name'] + except: + return None \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.py b/erpnext/non_profit/doctype/membership_type/membership_type.py index ed6b549600..7fa98a9481 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.py +++ b/erpnext/non_profit/doctype/membership_type/membership_type.py @@ -6,4 +6,4 @@ from __future__ import unicode_literals from frappe.model.document import Document class MembershipType(Document): - pass + pass \ No newline at end of file From e4c58c37c84c65f3f6eaac8ad4d2287e760df8a0 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 3 Aug 2020 14:56:01 +0530 Subject: [PATCH 038/175] feat: allow member creation via API --- erpnext/non_profit/doctype/member/member.py | 42 ++++++++++++++++--- .../membership_type/membership_type.json | 6 +-- .../membership_type/membership_type.py | 6 ++- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 6c2da07bb7..797736a3db 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -9,6 +9,7 @@ from frappe.model.document import Document from frappe.contacts.address_and_contact import load_address_and_contact from frappe.utils import cint from frappe.integrations.utils import get_payment_gateway_controller +from erpnext.non_profit.doctype.membership_type.membership_type import get_membership_type class Member(Document): def onload(self): @@ -74,19 +75,23 @@ def get_or_create_member(user_details): return create_member(user_details) def create_member(user_details): + user_details = frappe._dict(user_details) member = frappe.new_doc("Member") member.update({ "member_name": user_details.fullname, "email_id": user_details.email, "pan_number": user_details.pan or None, "membership_type": user_details.plan_id, - "customer": create_customer(user_details) + "subscription_id": user_details.subscription_id or None }) member.insert(ignore_permissions=True) + member.customer = create_customer(user_details, member.name) + member.save(ignore_permissions=True) + return member -def create_customer(user_details): +def create_customer(user_details, member=None): customer = frappe.new_doc("Customer") customer.customer_name = user_details.fullname customer.customer_type = "Individual" @@ -107,7 +112,13 @@ def create_customer(user_details): "link_name": customer.name }) - contact.save() + if member: + contact.append("links", { + "link_doctype": "Member", + "link_name": member + }) + + contact.save(ignore_permissions=True) except frappe.DuplicateEntryError: return customer.name @@ -139,12 +150,31 @@ def create_member_subscription_order(user_details): user_details = frappe._dict(user_details) member = get_or_create_member(user_details) - if not member: - member = create_member(user_details) subscription = member.setup_subscription() member.subscription_id = subscription.get('subscription_id') member.save(ignore_permissions=True) - return subscription \ No newline at end of file + return subscription + +@frappe.whitelist(allow_guest=True) +def register_member(fullname, email, rzpay_plan_id, subscription_id, pan=None, mobile=None): + plan = get_membership_type(rzpay_plan_id) + if not plan: + raise frappe.DoesNotExistError + + member = frappe.db.exists("Member", {'email': email, 'subscription_id': subscription_id }) + if member: + return member + else: + member = create_member(dict( + fullname=fullname, + email=email, + plan_id=plan, + subscription_id=subscription_id, + pan=pan, + mobile=mobile + )) + + return member.name \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.json b/erpnext/non_profit/doctype/membership_type/membership_type.json index a163568bb9..6ce1ecde12 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.json +++ b/erpnext/non_profit/doctype/membership_type/membership_type.json @@ -39,12 +39,12 @@ "fieldname": "linked_item", "fieldtype": "Link", "label": "Linked Item", - "options": "Item" + "options": "Item", + "unique": 1 } ], - "index_web_pages_for_search": 1, "links": [], - "modified": "2020-07-28 10:57:50.821375", + "modified": "2020-08-05 15:21:43.595745", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Type", diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.py b/erpnext/non_profit/doctype/membership_type/membership_type.py index 7fa98a9481..b95b04316f 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.py +++ b/erpnext/non_profit/doctype/membership_type/membership_type.py @@ -4,6 +4,10 @@ from __future__ import unicode_literals from frappe.model.document import Document +import frappe class MembershipType(Document): - pass \ No newline at end of file + pass + +def get_membership_type(razorpay_id): + return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id}) \ No newline at end of file From b4d3666e69f8a469d358ecb2cd082c45bce9f065 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 5 Aug 2020 17:33:18 +0530 Subject: [PATCH 039/175] feat: show send acknowledgement button if enabled in settings --- erpnext/non_profit/doctype/membership/membership.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js index 8408a6a1f0..ee8a8c0a7b 100644 --- a/erpnext/non_profit/doctype/membership/membership.js +++ b/erpnext/non_profit/doctype/membership/membership.js @@ -17,11 +17,13 @@ frappe.ui.form.on('Membership', { }); }); - frm.add_custom_button("Send Acknowledgement", () => { - frm.call("send_acknowlement").then(() => { - frm.reload_doc(); + frappe.db.get_single_value("Membership Settings", "send_email").then(val => { + if (val) frm.add_custom_button("Send Acknowledgement", () => { + frm.call("send_acknowlement").then(() => { + frm.reload_doc(); + }); }); - }); + }) }, onload: function(frm) { From 6b68eaad1ec1eb770e3cb2ae0b73fe4f43d1b219 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 5 Aug 2020 17:33:33 +0530 Subject: [PATCH 040/175] feat: use email template for membership acknowledgement --- .../doctype/membership/membership.py | 9 +++++--- .../membership_settings.json | 23 +++++++------------ 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index fc89396fa8..f058004ff9 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -99,11 +99,14 @@ class Membership(Document): if self.invoice and settings.send_invoice: attachments.append(frappe.attach_print("Sales Invoice", self.invoice, print_format=settings.inv_print_format)) + email_template = frappe.get_doc("Email Template", settings.email_template) + context = { "doc": self, "member": member} + email_args = { "recipients": [email], - "message": settings.message, - "subject": _('Here is your invoice'), - "attachments": [frappe.attach_print("Sales Invoice", invoice.name, print_format=settings.inv_print_format)], + "message": frappe.render_template(email_template.get("response"), context), + "subject": frappe.render_template(email_template.get("subject"), context), + "attachments": attachments, "reference_doctype": self.doctype, "reference_name": self.name } diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 2452a763ce..5b6bab5b0a 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -19,8 +19,7 @@ "send_invoice", "membership_print_format", "inv_print_format", - "section_break_15", - "message" + "email_template" ], "fields": [ { @@ -96,7 +95,7 @@ "default": "0", "fieldname": "send_email", "fieldtype": "Check", - "label": "Send Acknowledge Email" + "label": "Send Membership Acknowledgement" }, { "depends_on": "eval: doc.send_invoice", @@ -111,26 +110,20 @@ "fieldname": "membership_print_format", "fieldtype": "Link", "label": "Membership Print Format", - "mandatory_depends_on": "eval:doc.send_email", "options": "Print Format" }, { "depends_on": "eval:doc.send_email", - "fieldname": "section_break_15", - "fieldtype": "Section Break", - "hide_border": 1 - }, - { - "depends_on": "eval:doc.send_email", - "fieldname": "message", - "fieldtype": "Text Editor", - "label": "Message" + "fieldname": "email_template", + "fieldtype": "Link", + "label": "Email Template", + "mandatory_depends_on": "eval:doc.send_email", + "options": "Email Template" } ], - "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-07-31 14:30:15.701767", + "modified": "2020-08-05 17:26:37.287395", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From 3daa9de998e7acbbd440643258b131b863a5b363 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 6 Aug 2020 10:06:23 +0530 Subject: [PATCH 041/175] feat: add email option to field email_id --- erpnext/non_profit/doctype/member/member.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json index bb73a843ee..77cdb94b3d 100644 --- a/erpnext/non_profit/doctype/member/member.json +++ b/erpnext/non_profit/doctype/member/member.json @@ -133,7 +133,8 @@ { "fieldname": "email_id", "fieldtype": "Data", - "label": "Email Address" + "label": "Email Address", + "options": "Email" }, { "fieldname": "subscription_id", @@ -176,7 +177,7 @@ ], "image_field": "image", "links": [], - "modified": "2020-04-07 14:20:33.215700", + "modified": "2020-08-06 10:06:01.153564", "modified_by": "Administrator", "module": "Non Profit", "name": "Member", From d7b69e57aeb62cd5ea5dc83a9702b8072e755313 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Mon, 17 Aug 2020 11:20:56 +0530 Subject: [PATCH 042/175] fix: deleting unused Stock Entry Type --- erpnext/patches/v13_0/stock_entry_enhancements.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v13_0/stock_entry_enhancements.py b/erpnext/patches/v13_0/stock_entry_enhancements.py index dcc4f956f7..0bdcc9c0e8 100644 --- a/erpnext/patches/v13_0/stock_entry_enhancements.py +++ b/erpnext/patches/v13_0/stock_entry_enhancements.py @@ -24,4 +24,8 @@ def execute(): if not frappe.db.exists('Warehouse Type', 'Transit'): doc = frappe.new_doc('Warehouse Type') doc.name = 'Transit' - doc.insert() \ No newline at end of file + doc.insert() + + frappe.reload_doc("stock", "doctype", "stock_entry_type") + frappe.delete_doc_if_exists("Stock Entry Type", "Send to Warehouse") + frappe.delete_doc_if_exists("Stock Entry Type", "Receive at Warehouse") \ No newline at end of file From eb241c6c424e7bbdd50c33edf31a8883c1126686 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 17 Aug 2020 12:44:52 +0530 Subject: [PATCH 043/175] fix: General Ledger filter validation --- .../accounts/report/general_ledger/general_ledger.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index fcd36e4e6e..779cdbde9b 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -43,8 +43,11 @@ def execute(filters=None): def validate_filters(filters, account_details): - if not filters.get('company'): - frappe.throw(_('{0} is mandatory').format(_('Company'))) + if not filters.get("company"): + frappe.throw(_("{0} is mandatory").format(_("Company"))) + + if not filters.get("from_date") and not filters.get("to_date"): + frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date")))) if filters.get("account") and not account_details.get(filters.account): frappe.throw(_("Account {0} does not exists").format(filters.account)) @@ -132,7 +135,7 @@ def get_gl_entries(filters): if filters and filters.get('cost_center'): select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit, credit*(DCC_allocation.percentage_allocation/100) as credit, debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency, credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency """ - + distributed_cost_center_query = """ UNION ALL SELECT name as gl_entry, @@ -146,7 +149,7 @@ def get_gl_entries(filters): against_voucher_type, against_voucher, account_currency, - remarks, against, + remarks, against, is_opening, `tabGL Entry`.creation {select_fields_with_percentage} FROM `tabGL Entry`, ( From 205df14a480fb305504eca16957c891c99f079e2 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 17 Aug 2020 16:23:44 +0530 Subject: [PATCH 044/175] fix: Credit Limit Email not working --- erpnext/selling/doctype/customer/customer.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 93d4832173..1f955fcd52 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -184,10 +184,10 @@ class Customer(TransactionBase): def validate_credit_limit_on_change(self): if self.get("__islocal") or not self.credit_limits: return - + past_credit_limits = [d.credit_limit for d in frappe.db.get_all("Customer Credit Limit", filters={'parent': self.name}, fields=["credit_limit"], order_by="company")] - + current_credit_limits = [d.credit_limit for d in sorted(self.credit_limits, key=lambda k: k.company)] if past_credit_limits == current_credit_limits: @@ -396,13 +396,12 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, credit_controller_users = get_users_with_role(credit_controller_role or "Sales Master Manager") # form a list of emails and names to show to the user - credit_controller_users = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users] - - if not credit_controller_users: + credit_controller_users_formatted = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users] + if not credit_controller_users_formatted: frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.".format(customer))) message = """Please contact any of the following users to extend the credit limits for {0}: -

    • {1}
    """.format(customer, '
  • '.join(credit_controller_users)) +

    • {1}
    """.format(customer, '
  • '.join(credit_controller_users_formatted)) # if the current user does not have permissions to override credit limit, # prompt them to send out an email to the controller users @@ -427,7 +426,7 @@ def send_emails(args): subject = (_("Credit limit reached for customer {0}").format(args.get('customer'))) message = (_("Credit limit has been crossed for customer {0} ({1}/{2})") .format(args.get('customer'), args.get('customer_outstanding'), args.get('credit_limit'))) - frappe.sendmail(recipients=[args.get('credit_controller_users_list')], subject=subject, message=message) + frappe.sendmail(recipients=args.get('credit_controller_users_list'), subject=subject, message=message) def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=False, cost_center=None): # Outstanding based on GL Entries From b2604d1f77ed221a07f1f4c50183692af8f23fb7 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 14 Aug 2020 06:42:54 +0000 Subject: [PATCH 045/175] fix(General Ledger): include Accounting Dimension columns in report Prior to this commit, custom Accounting Dimensions are not (by default) shown or even considered in generating a General Ledger report. Moreover, as they are not considered, they are not used to distinguish GL Entry lines in the same voucher with the same Account when the "Group By" value is "Group by Voucher (Consolidated)". This situation leads to lines with different values for the accounting dimension to be ganged together in the General Ledger view of a Journal entry, making the entry difficult for an accountant to read. This commit alleviates the problem by adding a checkbox to control whether Accounting Dimension columns are considered in the General Ledger report. When they are considered, they are displayed and also included in the key that distinguishes lines in the "Group by Voucher (Consolidated)" mode. Resolves #23033. --- .../report/general_ledger/general_ledger.js | 6 +++ .../report/general_ledger/general_ledger.py | 51 ++++++++++++++----- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 1fc0f79478..fb0d359926 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -146,6 +146,12 @@ frappe.query_reports["General Ledger"] = { return frappe.db.get_link_options('Project', txt); } }, + { + "fieldname": "include_dimensions", + "label": __("Consider Accounting Dimensions"), + "fieldtype": "Check", + "default": 0 + }, { "fieldname": "show_opening_entries", "label": __("Show Opening Entries"), diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index fcd36e4e6e..ba0159e1ed 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -106,15 +106,20 @@ def set_account_currency(filters): return filters def get_result(filters, account_details): - gl_entries = get_gl_entries(filters) + accounting_dimensions = [] + if filters.get("include_dimensions"): + accounting_dimensions = get_accounting_dimensions() - data = get_data_with_opening_closing(filters, account_details, gl_entries) + gl_entries = get_gl_entries(filters, accounting_dimensions) + + data = get_data_with_opening_closing(filters, account_details, + accounting_dimensions, gl_entries) result = get_result_as_list(data, filters) return result -def get_gl_entries(filters): +def get_gl_entries(filters, accounting_dimensions): currency_map = get_currency(filters) select_fields = """, debit, credit, debit_in_account_currency, credit_in_account_currency """ @@ -128,6 +133,10 @@ def get_gl_entries(filters): filters['company_fb'] = frappe.db.get_value("Company", filters.get("company"), 'default_finance_book') + dimension_fields = "" + if accounting_dimensions: + dimension_fields = ', '.join(accounting_dimensions) + ',' + distributed_cost_center_query = "" if filters and filters.get('cost_center'): select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit, credit*(DCC_allocation.percentage_allocation/100) as credit, debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency, @@ -141,7 +150,7 @@ def get_gl_entries(filters): party_type, party, voucher_type, - voucher_no, + voucher_no, {dimension_fields} cost_center, project, against_voucher_type, against_voucher, @@ -160,13 +169,14 @@ def get_gl_entries(filters): {conditions} AND posting_date <= %(to_date)s AND cost_center = DCC_allocation.parent - """.format(select_fields_with_percentage=select_fields_with_percentage, conditions=get_conditions(filters).replace("and cost_center in %(cost_center)s ", '')) + """.format(dimension_fields=dimension_fields,select_fields_with_percentage=select_fields_with_percentage, conditions=get_conditions(filters).replace("and cost_center in %(cost_center)s ", '')) gl_entries = frappe.db.sql( """ select name as gl_entry, posting_date, account, party_type, party, - voucher_type, voucher_no, cost_center, project, + voucher_type, voucher_no, {dimension_fields} + cost_center, project, against_voucher_type, against_voucher, account_currency, remarks, against, is_opening, creation {select_fields} from `tabGL Entry` @@ -174,7 +184,7 @@ def get_gl_entries(filters): {distributed_cost_center_query} {order_by_statement} """.format( - select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query, + dimension_fields=dimension_fields, select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query, order_by_statement=order_by_statement ), filters, as_dict=1) @@ -247,12 +257,12 @@ def get_conditions(filters): return "and {}".format(" and ".join(conditions)) if conditions else "" -def get_data_with_opening_closing(filters, account_details, gl_entries): +def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries): data = [] gle_map = initialize_gle_map(gl_entries, filters) - totals, entries = get_accountwise_gle(filters, gl_entries, gle_map) + totals, entries = get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map) # Opening for filtered account data.append(totals.opening) @@ -318,7 +328,7 @@ def initialize_gle_map(gl_entries, filters): return gle_map -def get_accountwise_gle(filters, gl_entries, gle_map): +def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): totals = get_totals_dict() entries = [] consolidated_gle = OrderedDict() @@ -350,8 +360,11 @@ def get_accountwise_gle(filters, gl_entries, gle_map): if filters.get("group_by") != _('Group by Voucher (Consolidated)'): gle_map[gle.get(group_by)].entries.append(gle) elif filters.get("group_by") == _('Group by Voucher (Consolidated)'): - key = (gle.get("voucher_type"), gle.get("voucher_no"), - gle.get("account"), gle.get("cost_center")) + keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")] + for dim in accounting_dimensions: + keylist.append(gle.get(dim)) + keylist.append(gle.get("cost_center")) + key = tuple(keylist) if key not in consolidated_gle: consolidated_gle.setdefault(key, gle) else: @@ -478,7 +491,19 @@ def get_columns(filters): "options": "Project", "fieldname": "project", "width": 100 - }, + } + ]) + + if filters.get("include_dimensions"): + for dim in get_accounting_dimensions(as_list = False): + columns.append({ + "label": _(dim.label), + "options": dim.label, + "fieldname": dim.fieldname, + "width": 100 + }) + + columns.extend([ { "label": _("Cost Center"), "options": "Cost Center", From 434bab800396294f4aa951d2985918fdcc3749bb Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 19 Aug 2020 01:00:59 +0530 Subject: [PATCH 046/175] fix: Print Language for Customer not set for Print Receipt in POS --- .../doctype/pos_invoice/pos_invoice.py | 44 +++++++++---------- .../point_of_sale/pos_past_order_summary.js | 20 +++++---- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 8680b710ac..ba68df7673 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -21,7 +21,7 @@ from six import iteritems class POSInvoice(SalesInvoice): def __init__(self, *args, **kwargs): super(POSInvoice, self).__init__(*args, **kwargs) - + def validate(self): if not cint(self.is_pos): frappe.throw(_("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment"))) @@ -58,7 +58,7 @@ class POSInvoice(SalesInvoice): if self.redeem_loyalty_points and self.loyalty_points: self.apply_loyalty_points() self.set_status(update=True) - + def on_cancel(self): # run on cancel method of selling controller super(SalesInvoice, self).on_cancel() @@ -68,10 +68,10 @@ class POSInvoice(SalesInvoice): against_psi_doc = frappe.get_doc("POS Invoice", self.return_against) against_psi_doc.delete_loyalty_point_entry() against_psi_doc.make_loyalty_point_entry() - + def validate_stock_availablility(self): allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock') - + for d in self.get('items'): if d.serial_no: filters = { @@ -89,11 +89,11 @@ class POSInvoice(SalesInvoice): for s in serial_nos: if s in reserved_serial_nos: invalid_serial_nos.append(s) - + if len(invalid_serial_nos): multiple_nos = 's' if len(invalid_serial_nos) > 1 else '' frappe.throw(_("Row #{}: Serial No{}. {} has already been transacted into another POS Invoice. \ - Please select valid serial no.".format(d.idx, multiple_nos, + Please select valid serial no.".format(d.idx, multiple_nos, frappe.bold(', '.join(invalid_serial_nos)))), title=_("Not Available")) else: if allow_negative_stock: @@ -105,9 +105,9 @@ class POSInvoice(SalesInvoice): .format(d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse))), title=_("Not Available")) elif flt(available_stock) < flt(d.qty): frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. \ - Available quantity {}.'.format(d.idx, frappe.bold(d.item_code), + Available quantity {}.'.format(d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty))), title=_("Not Available")) - + def validate_serialised_or_batched_item(self): for d in self.get("items"): serialized = d.get("has_serial_no") @@ -125,7 +125,7 @@ class POSInvoice(SalesInvoice): if batched and no_batch_selected: frappe.throw(_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.' .format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item")) - + def validate_return_items(self): if not self.get("is_return"): return @@ -158,7 +158,7 @@ class POSInvoice(SalesInvoice): frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx)) if self.is_return and entry.amount > 0: frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx)) - + def validate_pos_return(self): if self.is_pos and self.is_return: total_amount_in_payments = 0 @@ -167,12 +167,12 @@ class POSInvoice(SalesInvoice): invoice_total = self.rounded_total or self.grand_total if total_amount_in_payments < invoice_total: frappe.throw(_("Total payments amount can't be greater than {}".format(-invoice_total))) - + def validate_loyalty_transaction(self): if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center): expense_account, cost_center = frappe.db.get_value('Loyalty Program', self.loyalty_program, ["expense_account", "cost_center"]) if not self.loyalty_redemption_account: - self.loyalty_redemption_account = expense_account + self.loyalty_redemption_account = expense_account if not self.loyalty_redemption_cost_center: self.loyalty_redemption_cost_center = cost_center @@ -212,7 +212,7 @@ class POSInvoice(SalesInvoice): if update: self.db_set('status', self.status, update_modified = update_modified) - + def set_pos_fields(self, for_validate=False): """Set retail related fields from POS Profiles""" from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile @@ -315,25 +315,25 @@ class POSInvoice(SalesInvoice): @frappe.whitelist() def get_stock_availability(item_code, warehouse): - latest_sle = frappe.db.sql("""select qty_after_transaction - from `tabStock Ledger Entry` + latest_sle = frappe.db.sql("""select qty_after_transaction + from `tabStock Ledger Entry` where item_code = %s and warehouse = %s order by posting_date desc, posting_time desc limit 1""", (item_code, warehouse), as_dict=1) - + pos_sales_qty = frappe.db.sql("""select sum(p_item.qty) as qty from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item - where p.name = p_item.parent - and p.consolidated_invoice is NULL + where p.name = p_item.parent + and p.consolidated_invoice is NULL and p.docstatus = 1 and p_item.docstatus = 1 and p_item.item_code = %s and p_item.warehouse = %s """, (item_code, warehouse), as_dict=1) - + sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0 pos_sales_qty = pos_sales_qty[0].qty or 0 if pos_sales_qty else 0 - + if sle_qty and pos_sales_qty and sle_qty > pos_sales_qty: return sle_qty - pos_sales_qty else: @@ -360,14 +360,14 @@ def make_merge_log(invoices): merge_log = frappe.new_doc("POS Invoice Merge Log") merge_log.posting_date = getdate(nowdate()) for inv in invoices: - inv_data = frappe.db.get_values("POS Invoice", inv.get('name'), + inv_data = frappe.db.get_values("POS Invoice", inv.get('name'), ["customer", "posting_date", "grand_total"], as_dict=1)[0] merge_log.customer = inv_data.customer merge_log.append("pos_invoices", { 'pos_invoice': inv.get('name'), 'customer': inv_data.customer, 'posting_date': inv_data.posting_date, - 'grand_total': inv_data.grand_total + 'grand_total': inv_data.grand_total }) if merge_log.get('pos_invoices'): diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index 24326b2256..30e0918ba6 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -86,7 +86,7 @@ erpnext.PointOfSale.PastOrderSummary = class { this.$summary_container.append( `
    ` ) - + this.$summary_btns = this.$summary_container.find('.summary-btns'); } @@ -110,7 +110,10 @@ erpnext.PointOfSale.PastOrderSummary = class { {fieldname:'print', fieldtype:'Data', label:'Print Preview'} ], primary_action: () => { - this.events.get_frm().print_preview.printit(true); + const frm = this.events.get_frm(); + frm.doc = this.doc; + frm.print_preview.lang_code = frm.doc.language; + frm.print_preview.printit(true); }, primary_action_label: __('Print'), }); @@ -174,7 +177,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
    Tax Charges
    - ${ + ${ doc.taxes.map((t, i) => { let margin_left = ''; if (i !== 0) margin_left = 'ml-2'; @@ -271,6 +274,7 @@ erpnext.PointOfSale.PastOrderSummary = class { // this.print_dialog.show(); const frm = this.events.get_frm(); frm.doc = this.doc; + frm.print_preview.lang_code = frm.doc.language; frm.print_preview.printit(true); }); } @@ -284,9 +288,9 @@ erpnext.PointOfSale.PastOrderSummary = class { this.$summary_container.find('.print-btn').click(); }); } - + toggle_component(show) { - show ? + show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none'); } @@ -372,9 +376,9 @@ erpnext.PointOfSale.PastOrderSummary = class { } get_condition_btn_map(after_submission) { - if (after_submission) + if (after_submission) return [{ condition: true, visible_btns: ['Print Receipt', 'Email Receipt', 'New Order'] }]; - + return [ { condition: this.doc.docstatus === 0, visible_btns: ['Edit Order'] }, { condition: !this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt', 'Return']}, @@ -384,7 +388,7 @@ erpnext.PointOfSale.PastOrderSummary = class { load_summary_of(doc, after_submission=false) { this.$summary_wrapper.removeClass("d-none"); - + after_submission ? this.switch_to_post_submit_summary() : this.switch_to_recent_invoice_summary(); From 182ee5e7c116fe70b6d4c9e52045b2c9a13729ac Mon Sep 17 00:00:00 2001 From: Diksha Jadhav Date: Tue, 18 Aug 2020 00:35:04 +0530 Subject: [PATCH 047/175] feat(queries): sort warehouses based on item quantity --- erpnext/controllers/queries.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 31e34987be..a14f4124e5 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -468,24 +468,18 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters): conditions, bin_conditions = [], [] filter_dict = get_doctype_wise_filters(filters) - sub_query = """ select round(`tabBin`.actual_qty, 2) from `tabBin` - where `tabBin`.warehouse = `tabWarehouse`.name - {bin_conditions} """.format( - bin_conditions=get_filters_cond(doctype, filter_dict.get("Bin"), - bin_conditions, ignore_permissions=True)) - query = """select `tabWarehouse`.name, - CONCAT_WS(" : ", "Actual Qty", ifnull( ({sub_query}), 0) ) as actual_qty - from `tabWarehouse` + CONCAT_WS(" : ", "Actual Qty", ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty + from `tabWarehouse` left join `tabBin` + on `tabBin`.warehouse = `tabWarehouse`.name {bin_conditions} where - `tabWarehouse`.`{key}` like {txt} + `tabWarehouse`.`{key}` like {txt} {fcond} {mcond} - order by - `tabWarehouse`.name desc + order by ifnull(`tabBin`.actual_qty, 0) desc limit {start}, {page_len} """.format( - sub_query=sub_query, + bin_conditions=get_filters_cond(doctype, filter_dict.get("Bin"),bin_conditions, ignore_permissions=True), key=searchfield, fcond=get_filters_cond(doctype, filter_dict.get("Warehouse"), conditions), mcond=get_match_cond(doctype), From aaeb3980bcdc0df493c52d20c4440b71f4ad4af2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 19 Aug 2020 15:13:30 +0530 Subject: [PATCH 048/175] feat: JSON download for HSN wise outward summary --- .../hsn_wise_summary_of_outward_supplies.js | 23 +++++ .../hsn_wise_summary_of_outward_supplies.py | 95 +++++++++++++++++-- 2 files changed, 108 insertions(+), 10 deletions(-) diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.js b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.js index dfdf9dc095..b757d53aa2 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.js +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.js @@ -46,5 +46,28 @@ frappe.query_reports["HSN-wise-summary of outward supplies"] = { ], onload: (report) => { fetch_gstins(report); + + report.page.add_inner_button(__("Download JSON"), function () { + var filters = report.get_values(); + + frappe.call({ + method: 'erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies.get_json', + args: { + data: report.data, + report_name: report.report_name, + filters: filters + }, + callback: function(r) { + if (r.message) { + const args = { + cmd: 'erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies.download_json_file', + data: r.message.data, + report_name: r.message.report_name + }; + open_url_post(frappe.request.url, args); + } + } + }); + }); } }; diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index a3ed4cebb1..6f3fff2932 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -4,11 +4,13 @@ from __future__ import unicode_literals import frappe, erpnext from frappe import _ -from frappe.utils import flt +from frappe.utils import flt, getdate, cstr from frappe.model.meta import get_field_precision from frappe.utils.xlsxutils import handle_html from six import iteritems import json +from erpnext.regional.india.utils import get_gst_accounts +from erpnext.regional.report.gstr_1.gstr_1 import get_company_gstin_number def execute(filters=None): return _execute(filters) @@ -141,7 +143,7 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic tax_details = frappe.db.sql(""" select - parent, description, item_wise_tax_detail, + parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount from `tab%s` where @@ -153,11 +155,11 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic """ % (tax_doctype, '%s', ', '.join(['%s']*len(invoice_item_row)), conditions), tuple([doctype] + list(invoice_item_row))) - for parent, description, item_wise_tax_detail, tax_amount in tax_details: - description = handle_html(description) - if description not in tax_columns and tax_amount: + for parent, account_head, item_wise_tax_detail, tax_amount in tax_details: + + if account_head not in tax_columns and tax_amount: # as description is text editor earlier and markup can break the column convention in reports - tax_columns.append(description) + tax_columns.append(account_head) if item_wise_tax_detail: try: @@ -175,17 +177,17 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic for d in item_row_map.get(parent, {}).get(item_code, []): item_tax_amount = tax_amount if item_tax_amount: - itemised_tax.setdefault((parent, item_code), {})[description] = frappe._dict({ + itemised_tax.setdefault((parent, item_code), {})[account_head] = frappe._dict({ "tax_amount": flt(item_tax_amount, tax_amount_precision) }) except ValueError: continue tax_columns.sort() - for desc in tax_columns: + for account_head in tax_columns: columns.append({ - "label": desc, - "fieldname": frappe.scrub(desc), + "label": account_head, + "fieldname": frappe.scrub(account_head), "fieldtype": "Float", "width": 110 }) @@ -212,3 +214,76 @@ def get_merged_data(columns, data): return result +@frappe.whitelist() +def get_json(filters, report_name, data): + filters = json.loads(filters) + report_data = json.loads(data) + gstin = filters.get('company_gstin') or get_company_gstin_number(filters["company"]) + + if not filters.get('from_date') or not filters.get('to_date'): + frappe.throw(_("Please enter From Date and To Date to generate JSON")) + + fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) + + gst_json = {"gstin": "", "version": "GST2.3.4", + "hash": "hash", "gstin": gstin, "fp": fp} + + gst_json["hsn"] = { + "data": get_hsn_wise_json_data(filters, report_data) + } + + return { + 'report_name': report_name, + 'data': gst_json + } + +@frappe.whitelist() +def download_json_file(): + ''' download json content in a file ''' + data = frappe._dict(frappe.local.form_dict) + frappe.response['filename'] = frappe.scrub("{0}".format(data['report_name'])) + '.json' + frappe.response['filecontent'] = data['data'] + frappe.response['content_type'] = 'application/json' + frappe.response['type'] = 'download' + +def get_hsn_wise_json_data(filters, report_data): + + filters = frappe._dict(filters) + gst_accounts = get_gst_accounts(filters.company) + data = [] + count = 1 + + for hsn in report_data: + row = { + "num": count, + "hsn_sc": hsn.get("gst_hsn_code"), + "desc": hsn.get("description"), + "uqc": hsn.get("stock_uom").upper(), + "qty": hsn.get("stock_qty"), + "val": flt(hsn.get("total_amount"), 2), + "txval": flt(hsn.get("taxable_amount", 2)), + "iamt": 0.0, + "camt": 0.0, + "samt": 0.0, + "csamt": 0.0 + + } + + for account in gst_accounts.get('igst_account'): + row['iamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + for account in gst_accounts.get('cgst_account'): + row['camt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + for account in gst_accounts.get('sgst_account'): + row['samt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + for account in gst_accounts.get('cess_account'): + row['csamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + data.append(row) + count +=1 + + return data + + From 8aed48fe31b0de795b6d33fb2bf54f22f6e1c7ef Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 19 Aug 2020 18:30:18 +0530 Subject: [PATCH 049/175] fix: Do not update total for RCM invvoices if net taxes are zero --- erpnext/regional/india/utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 2c81748c86..844e34b9ca 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -674,6 +674,9 @@ def update_grand_total_for_rcm(doc, method): if country != 'India': return + if not doc.total_taxes_and_charges: + return + if doc.reverse_charge == 'Y': gst_accounts = get_gst_accounts(doc.company) gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ @@ -721,7 +724,10 @@ def make_regional_gl_entries(gl_entries, doc): country = frappe.get_cached_value('Company', doc.company, 'country') if country != 'India': - return + return gl_entries + + if not doc.total_taxes_and_charges: + return gl_entries if doc.reverse_charge == 'Y': gst_accounts = get_gst_accounts(doc.company) From 2c26144a6b5f22e8c2f2fd9a2e8930e440ae76a1 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 19 Aug 2020 18:55:06 +0200 Subject: [PATCH 050/175] Update erpnext/patches/v13_0/add_standard_navbar_items.py --- erpnext/patches/v13_0/add_standard_navbar_items.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/patches/v13_0/add_standard_navbar_items.py b/erpnext/patches/v13_0/add_standard_navbar_items.py index 5de99a5abc..d05b258db0 100644 --- a/erpnext/patches/v13_0/add_standard_navbar_items.py +++ b/erpnext/patches/v13_0/add_standard_navbar_items.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals -import frappe +# import frappe from erpnext.setup.install import add_standard_navbar_items def execute(): # Add standard navbar items for ERPNext in Navbar Settings - add_standard_navbar_items() \ No newline at end of file + add_standard_navbar_items() From 019d4debb27e4ec3b46dbc251d480d7f9307b009 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 20 Aug 2020 15:53:58 +0530 Subject: [PATCH 051/175] fix: incorrect stock value in return case (#23102) --- .../purchase_invoice_item.json | 15 ++++++++++++--- .../sales_invoice_item/sales_invoice_item.json | 16 +++++++++++++--- erpnext/controllers/buying_controller.py | 16 +++++++++++++--- erpnext/controllers/sales_and_purchase_return.py | 3 +++ erpnext/controllers/selling_controller.py | 14 +++++++++++--- erpnext/controllers/stock_controller.py | 9 +++++++-- 6 files changed, 59 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 52a5be0984..f6d76e5050 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -1,5 +1,4 @@ { - "actions": [], "autoname": "hash", "creation": "2013-05-22 12:43:10", "doctype": "DocType", @@ -82,6 +81,7 @@ "item_tax_rate", "bom", "include_exploded_items", + "purchase_invoice_item", "col_break6", "purchase_order", "po_detail", @@ -769,12 +769,21 @@ "collapsible": 1, "fieldname": "col_break7", "fieldtype": "Column Break" + }, + { + "depends_on": "eval:parent.update_stock == 1", + "fieldname": "purchase_invoice_item", + "fieldtype": "Data", + "ignore_user_permissions": 1, + "label": "Purchase Invoice Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, - "links": [], - "modified": "2020-04-22 10:37:35.103176", + "modified": "2020-08-20 11:48:01.398356", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 004d358ef9..fb3dd6a92a 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -1,5 +1,4 @@ { - "actions": [], "autoname": "hash", "creation": "2013-06-04 11:02:19", "doctype": "DocType", @@ -87,6 +86,7 @@ "edit_references", "sales_order", "so_detail", + "sales_invoice_item", "column_break_74", "delivery_note", "dn_detail", @@ -790,12 +790,22 @@ "fieldtype": "Link", "label": "Project", "options": "Project" - } + }, + { + "depends_on": "eval:parent.update_stock == 1", + "fieldname": "sales_invoice_item", + "fieldtype": "Data", + "ignore_user_permissions": 1, + "label": "Sales Invoice Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-07-18 12:24:41.749986", + "modified": "2020-08-20 11:24:41.749986", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 89b48f07ee..f982700c01 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -559,9 +559,19 @@ class BuyingController(StockController): "serial_no": cstr(d.serial_no).strip() }) if self.is_return: - original_incoming_rate = frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Purchase Receipt", "voucher_no": self.return_against, - "item_code": d.item_code}, "incoming_rate") + filters = { + "voucher_type": self.doctype, + "voucher_no": self.return_against, + "item_code": d.item_code + } + + if (self.doctype == "Purchase Invoice" and self.update_stock + and d.get("purchase_invoice_item")): + filters["voucher_detail_no"] = d.purchase_invoice_item + elif self.doctype == "Purchase Receipt" and d.get("purchase_receipt_item"): + filters["voucher_detail_no"] = d.purchase_receipt_item + + original_incoming_rate = frappe.db.get_value("Stock Ledger Entry", filters, "incoming_rate") sle.update({ "outgoing_rate": original_incoming_rate diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 3f127a201e..a03dee1174 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -281,6 +281,8 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.rejected_warehouse = source_doc.rejected_warehouse target_doc.po_detail = source_doc.po_detail target_doc.pr_detail = source_doc.pr_detail + target_doc.purchase_invoice_item = source_doc.name + elif doctype == "Delivery Note": target_doc.against_sales_order = source_doc.against_sales_order target_doc.against_sales_invoice = source_doc.against_sales_invoice @@ -296,6 +298,7 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.so_detail = source_doc.so_detail target_doc.dn_detail = source_doc.dn_detail target_doc.expense_account = source_doc.expense_account + target_doc.sales_invoice_item = source_doc.name if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index b696ac39f6..17f3ae53e7 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -217,7 +217,9 @@ class SellingController(StockController): 'target_warehouse': p.target_warehouse, 'company': self.company, 'voucher_type': self.doctype, - 'allow_zero_valuation': d.allow_zero_valuation_rate + 'allow_zero_valuation': d.allow_zero_valuation_rate, + 'sales_invoice_item': d.get("sales_invoice_item"), + 'delivery_note_item': d.get("dn_detail") })) else: il.append(frappe._dict({ @@ -233,7 +235,9 @@ class SellingController(StockController): 'target_warehouse': d.target_warehouse, 'company': self.company, 'voucher_type': self.doctype, - 'allow_zero_valuation': d.allow_zero_valuation_rate + 'allow_zero_valuation': d.allow_zero_valuation_rate, + 'sales_invoice_item': d.get("sales_invoice_item"), + 'delivery_note_item': d.get("dn_detail") })) return il @@ -302,7 +306,11 @@ class SellingController(StockController): d.conversion_factor = get_conversion_factor(d.item_code, d.uom).get("conversion_factor") or 1.0 return_rate = 0 if cint(self.is_return) and self.return_against and self.docstatus==1: - return_rate = self.get_incoming_rate_for_return(d.item_code, self.return_against) + against_document_no = (d.get("sales_invoice_item") + if self.doctype == "Sales Invoice" else d.get("delivery_note_item")) + + return_rate = self.get_incoming_rate_for_return(d.item_code, + self.return_against, against_document_no) # On cancellation or if return entry submission, make stock ledger entry for # target warehouse first, to update serial no values properly diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index e8483da544..394883d239 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -301,14 +301,19 @@ class StockController(AccountsController): return serialized_items - def get_incoming_rate_for_return(self, item_code, against_document): + def get_incoming_rate_for_return(self, item_code, against_document, against_document_no=None): incoming_rate = 0.0 + cond = '' if against_document and item_code: + if against_document_no: + cond = " and voucher_detail_no = %s" %(frappe.db.escape(against_document_no)) + incoming_rate = frappe.db.sql("""select abs(stock_value_difference / actual_qty) from `tabStock Ledger Entry` where voucher_type = %s and voucher_no = %s - and item_code = %s limit 1""", + and item_code = %s {0} limit 1""".format(cond), (self.doctype, against_document, item_code)) + incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0 return incoming_rate From afd2dd3570ef8db60a2d0bde01549cb356a326db Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 20 Aug 2020 16:31:38 +0530 Subject: [PATCH 052/175] fix: Unable to submit reverse charge invoice --- erpnext/regional/india/utils.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 844e34b9ca..69e47a43c4 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -726,9 +726,6 @@ def make_regional_gl_entries(gl_entries, doc): if country != 'India': return gl_entries - if not doc.total_taxes_and_charges: - return gl_entries - if doc.reverse_charge == 'Y': gst_accounts = get_gst_accounts(doc.company) gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ @@ -738,6 +735,7 @@ def make_regional_gl_entries(gl_entries, doc): if tax.category not in ("Total", "Valuation and Total"): continue + dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list: account_currency = get_account_currency(tax.account_head) @@ -747,8 +745,8 @@ def make_regional_gl_entries(gl_entries, doc): "cost_center": tax.cost_center, "posting_date": doc.posting_date, "against": doc.supplier, - "credit": tax.base_tax_amount_after_discount_amount, - "credits_in_account_currency": tax.base_tax_amount_after_discount_amount \ + dr_or_cr: tax.base_tax_amount_after_discount_amount, + dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount \ if account_currency==doc.company_currency \ else tax.tax_amount_after_discount_amount }, account_currency, item=tax) From 4061186429b8bb1d88c8cc8211b0f5aeaa3e413e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 20 Aug 2020 18:01:20 +0530 Subject: [PATCH 053/175] fix: POS page link not visible on Desk --- erpnext/selling/desk_page/retail/retail.json | 29 ++++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/desk_page/retail/retail.json b/erpnext/selling/desk_page/retail/retail.json index 7b30af20cc..581e14cf81 100644 --- a/erpnext/selling/desk_page/retail/retail.json +++ b/erpnext/selling/desk_page/retail/retail.json @@ -3,7 +3,7 @@ { "hidden": 0, "label": "Retail Operations", - "links": "[\n {\n \"description\": \"Setup default values for POS Invoices\",\n \"label\": \"Point-of-Sale Profile\",\n \"name\": \"POS Profile\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"POS Profile\"\n ],\n \"description\": \"Point of Sale\",\n \"label\": \"POS\",\n \"name\": \"pos\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"description\": \"Cashier Closing\",\n \"label\": \"Cashier Closing\",\n \"name\": \"Cashier Closing\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup mode of POS (Online / Offline)\",\n \"label\": \"POS Settings\",\n \"name\": \"POS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To make Customer based incentive schemes.\",\n \"label\": \"Loyalty Program\",\n \"name\": \"Loyalty Program\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To view logs of Loyalty Points assigned to a Customer.\",\n \"label\": \"Loyalty Point Entry\",\n \"name\": \"Loyalty Point Entry\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"description\": \"Setup default values for POS Invoices\",\n \"label\": \"Point of Sale Profile\",\n \"name\": \"POS Profile\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"POS Profile\"\n ],\n \"description\": \"Point of Sale\",\n \"label\": \"Point of Sale\",\n \"name\": \"point-of-sale\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"description\": \"Setup mode of POS (Online / Offline)\",\n \"label\": \"POS Settings\",\n \"name\": \"POS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Cashier Closing\",\n \"label\": \"Cashier Closing\",\n \"name\": \"Cashier Closing\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To make Customer based incentive schemes.\",\n \"label\": \"Loyalty Program\",\n \"name\": \"Loyalty Program\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To view logs of Loyalty Points assigned to a Customer.\",\n \"label\": \"Loyalty Point Entry\",\n \"name\": \"Loyalty Point Entry\",\n \"type\": \"doctype\"\n }\n]" } ], "category": "Domains", @@ -14,10 +14,11 @@ "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Retail", - "modified": "2020-04-26 22:42:39.346750", + "modified": "2020-08-20 18:00:07.515691", "modified_by": "Administrator", "module": "Selling", "name": "Retail", @@ -25,5 +26,27 @@ "pin_to_bottom": 0, "pin_to_top": 0, "restrict_to_domain": "Retail", - "shortcuts": [] + "shortcuts": [ + { + "color": "#9deca2", + "doc_view": "", + "format": "{} Active", + "label": "Point of Sale Profile", + "link_to": "POS Profile", + "stats_filter": "{\n \"disabled\": 0\n}", + "type": "DocType" + }, + { + "doc_view": "", + "label": "Point of Sale", + "link_to": "point-of-sale", + "type": "Page" + }, + { + "doc_view": "", + "label": "POS Settings", + "link_to": "POS Settings", + "type": "DocType" + } + ] } \ No newline at end of file From c23797e350a2210fc61f1821445365be31ffaa37 Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 20 Aug 2020 19:12:12 +0530 Subject: [PATCH 054/175] fix: Create Opportunity without Default Company from Email (#23097) * fix: Create Opoortunity without Default Company from Email * fix: Add Prompt to Select Company * Update communication.js Co-authored-by: Nabin Hait --- .../crm/doctype/opportunity/opportunity.py | 3 +- erpnext/public/js/communication.js | 45 +++++++++++++------ 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index e152850f17..6096053136 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -325,7 +325,7 @@ def auto_close_opportunity(): doc.save() @frappe.whitelist() -def make_opportunity_from_communication(communication, ignore_communication_links=False): +def make_opportunity_from_communication(communication, company, ignore_communication_links=False): from erpnext.crm.doctype.lead.lead import make_lead_from_communication doc = frappe.get_doc("Communication", communication) @@ -337,6 +337,7 @@ def make_opportunity_from_communication(communication, ignore_communication_link opportunity = frappe.get_doc({ "doctype": "Opportunity", + "company": company, "opportunity_from": opportunity_from, "party_name": lead }).insert(ignore_permissions=True) diff --git a/erpnext/public/js/communication.js b/erpnext/public/js/communication.js index 9432d42175..26e5ab8b32 100644 --- a/erpnext/public/js/communication.js +++ b/erpnext/public/js/communication.js @@ -7,7 +7,7 @@ frappe.ui.form.on("Communication", { }, setup_custom_buttons: (frm) => { - let confirm_msg = "Are you sure you want to create {0} from this email"; + let confirm_msg = "Are you sure you want to create {0} from this email?"; if(frm.doc.reference_doctype !== "Issue") { frm.add_custom_button(__("Issue"), () => { frappe.confirm(__(confirm_msg, [__("Issue")]), () => { @@ -62,17 +62,36 @@ frappe.ui.form.on("Communication", { }, make_opportunity_from_communication: (frm) => { - return frappe.call({ - method: "erpnext.crm.doctype.opportunity.opportunity.make_opportunity_from_communication", - args: { - communication: frm.doc.name - }, - freeze: true, - callback: (r) => { - if(r.message) { - frm.reload_doc() + const fields = [{ + fieldtype: 'Link', + label: __('Select a Company'), + fieldname: 'company', + options: 'Company', + reqd: 1, + default: frappe.defaults.get_user_default("Company") + }]; + + frappe.prompt(fields, data => { + frappe.call({ + method: "erpnext.crm.doctype.opportunity.opportunity.make_opportunity_from_communication", + args: { + communication: frm.doc.name, + company: data.company + }, + freeze: true, + callback: (r) => { + if(r.message) { + frm.reload_doc(); + frappe.show_alert({ + message: __("Opportunity {0} created", + ['' + r.message + '']), + indicator: 'green' + }); + } } - } - }) + }); + }, + 'Create an Opportunity', + 'Create'); } -}); \ No newline at end of file +}); From 1ba62c9732cd4d606b5295a2cbf094e65e7b5e1f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 20 Aug 2020 19:39:10 +0530 Subject: [PATCH 055/175] fix:Validate Job offer against vacancies (#23109) --- erpnext/hr/doctype/job_offer/job_offer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/job_offer/job_offer.py b/erpnext/hr/doctype/job_offer/job_offer.py index 3d68bc8d8e..c397a3f5ca 100644 --- a/erpnext/hr/doctype/job_offer/job_offer.py +++ b/erpnext/hr/doctype/job_offer/job_offer.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe +from frappe.utils import cint from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc from frappe import _ @@ -24,8 +25,7 @@ class JobOffer(Document): check_vacancies = frappe.get_single("HR Settings").check_vacancies if staffing_plan and check_vacancies: job_offers = self.get_job_offer(staffing_plan.from_date, staffing_plan.to_date) - - if not staffing_plan.get("vacancies") or staffing_plan.vacancies - len(job_offers) <= 0: + if not staffing_plan.get("vacancies") or cint(staffing_plan.vacancies) - len(job_offers) <= 0: error_variable = 'for ' + frappe.bold(self.designation) if staffing_plan.get("parent"): error_variable = frappe.bold(get_link_to_form("Staffing Plan", staffing_plan.parent)) @@ -65,7 +65,7 @@ def get_staffing_plan_detail(designation, company, offer_date): AND %s between sp.from_date and sp.to_date """, (designation, company, offer_date), as_dict=1) - return frappe._dict(detail[0]) if detail else None + return frappe._dict(detail[0]) if (detail and detail[0].parent) else None @frappe.whitelist() def make_employee(source_name, target_doc=None): From 111183d0804c6740089547d29eea4a870ffbdd42 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sat, 22 Aug 2020 12:31:06 +0530 Subject: [PATCH 056/175] fix: Installation failed due to global variable (#23114) --- .../consolidated_financial_statement.py | 2 +- .../accounts/report/financial_statements.py | 2 +- .../report/general_ledger/general_ledger.py | 2 +- erpnext/accounts/report/utils.py | 22 +++++-------------- 4 files changed, 8 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index c2c7207e37..219871b1d6 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -378,7 +378,7 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g if filters and filters.get('presentation_currency') != d.default_currency: currency_info['company'] = d.name currency_info['company_currency'] = d.default_currency - convert_to_presentation_currency(gl_entries, currency_info) + convert_to_presentation_currency(gl_entries, currency_info, filters.get('company')) for entry in gl_entries: key = entry.account_number or entry.account_name diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index d5b8cdb1d4..1b65a318b6 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -423,7 +423,7 @@ def set_gl_entries_by_account( distributed_cost_center_query=distributed_cost_center_query), gl_filters, as_dict=True) #nosec if filters and filters.get('presentation_currency'): - convert_to_presentation_currency(gl_entries, get_currency(filters)) + convert_to_presentation_currency(gl_entries, get_currency(filters), filters.get('company')) for entry in gl_entries: gl_entries_by_account.setdefault(entry.account, []).append(entry) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index fcd36e4e6e..0a72f6a450 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -180,7 +180,7 @@ def get_gl_entries(filters): filters, as_dict=1) if filters.get('presentation_currency'): - return convert_to_presentation_currency(gl_entries, currency_map) + return convert_to_presentation_currency(gl_entries, currency_map, filters.get('company')) else: return gl_entries diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index 4a9af490cf..9de8d19f2a 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -6,10 +6,6 @@ from erpnext.accounts.doctype.fiscal_year.fiscal_year import get_from_and_to_dat from frappe.utils import cint, get_datetime_str, formatdate, flt __exchange_rates = {} -P_OR_L_ACCOUNTS = list( - sum(frappe.get_list('Account', fields=['name'], or_filters=[{'root_type': 'Income'}, {'root_type': 'Expense'}], as_list=True), ()) -) - def get_currency(filters): """ @@ -73,18 +69,7 @@ def get_rate_as_at(date, from_currency, to_currency): return rate - -def is_p_or_l_account(account_name): - """ - Check if the given `account name` is an `Account` with `root_type` of either 'Income' - or 'Expense'. - :param account_name: - :return: Boolean - """ - return account_name in P_OR_L_ACCOUNTS - - -def convert_to_presentation_currency(gl_entries, currency_info): +def convert_to_presentation_currency(gl_entries, currency_info, company): """ Take a list of GL Entries and change the 'debit' and 'credit' values to currencies in `currency_info`. @@ -96,6 +81,9 @@ def convert_to_presentation_currency(gl_entries, currency_info): presentation_currency = currency_info['presentation_currency'] company_currency = currency_info['company_currency'] + pl_accounts = [d.name for d in frappe.get_list('Account', + filters={'report_type': 'Profit and Loss', 'company': company})] + for entry in gl_entries: account = entry['account'] debit = flt(entry['debit']) @@ -107,7 +95,7 @@ def convert_to_presentation_currency(gl_entries, currency_info): if account_currency != presentation_currency: value = debit or credit - date = currency_info['report_date'] if not is_p_or_l_account(account) else entry['posting_date'] + date = entry['posting_date'] if account in pl_accounts else currency_info['report_date'] converted_value = convert(value, presentation_currency, company_currency, date) if entry.get('debit'): From df175cc8fc481aa73f4d01ed471d5f77f2f8718e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sat, 22 Aug 2020 19:17:18 +0530 Subject: [PATCH 057/175] fix: GLE for subcontracted PR is fg item rate is zero --- .../purchase_receipt/purchase_receipt.py | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index d0ba001d7e..4e173fff4d 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -227,6 +227,14 @@ class PurchaseReceipt(BuyingController): if not stock_value_diff: continue + # If PR is sub-contracted and fg item rate is zero + # in that case if account for shource and target warehouse are same, + # then GL entries should not be posted + if flt(stock_value_diff) == flt(d.rm_supp_cost) \ + and warehouse_account.get(self.supplier_warehouse) \ + and warehouse_account[d.warehouse]["account"] == warehouse_account[self.supplier_warehouse]["account"]: + continue + gl_entries.append(self.get_gl_dict({ "account": warehouse_account[d.warehouse]["account"], "against": stock_rbnb, @@ -242,16 +250,16 @@ class PurchaseReceipt(BuyingController): credit_amount = flt(d.base_net_amount, d.precision("base_net_amount")) \ if credit_currency == self.company_currency else flt(d.net_amount, d.precision("net_amount")) - - gl_entries.append(self.get_gl_dict({ - "account": warehouse_account[d.from_warehouse]['account'] \ - if d.from_warehouse else stock_rbnb, - "against": warehouse_account[d.warehouse]["account"], - "cost_center": d.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "debit": -1 * flt(d.base_net_amount, d.precision("base_net_amount")), - "debit_in_account_currency": -1 * credit_amount - }, credit_currency, item=d)) + if credit_amount: + gl_entries.append(self.get_gl_dict({ + "account": warehouse_account[d.from_warehouse]['account'] \ + if d.from_warehouse else stock_rbnb, + "against": warehouse_account[d.warehouse]["account"], + "cost_center": d.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "debit": -1 * flt(d.base_net_amount, d.precision("base_net_amount")), + "debit_in_account_currency": -1 * credit_amount + }, credit_currency, item=d)) negative_expense_to_be_booked += flt(d.item_tax_amount) From 337ee0a4bf5419f43700468ac39f8966fc512f77 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sat, 22 Aug 2020 20:02:27 +0530 Subject: [PATCH 058/175] test: Test case for GLE in subcontracted PR if FG item rate is zero --- .../purchase_receipt/test_purchase_receipt.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index d97b9e82c3..2a094a01de 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -121,6 +121,22 @@ class TestPurchaseReceipt(unittest.TestCase): rm_supp_cost = sum([d.amount for d in pr.get("supplied_items")]) self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2)) + def test_subcontracting_gle_fg_item_rate_zero(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + set_perpetual_inventory() + frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM") + make_stock_entry(item_code="_Test Item", target="Work In Progress - TCP1", qty=100, basic_rate=100, company="_Test Company with perpetual inventory") + make_stock_entry(item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1", + qty=100, basic_rate=100, company="_Test Company with perpetual inventory") + pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=0, is_subcontracted="Yes", + company="_Test Company with perpetual inventory", warehouse='Stores - TCP1', supplier_warehouse='Work In Progress - TCP1') + + gl_entries = get_gl_entries("Purchase Receipt", pr.name) + + self.assertFalse(gl_entries) + + set_perpetual_inventory(0) + def test_serial_no_supplier(self): pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"), @@ -688,7 +704,7 @@ def make_purchase_receipt(**args): "received_qty": received_qty, "rejected_qty": rejected_qty, "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "", - "rate": args.rate or 50, + "rate": args.rate if args.rate != None else 50, "conversion_factor": args.conversion_factor or 1.0, "serial_no": args.serial_no, "stock_uom": args.stock_uom or "_Test UOM", From cb0bc2dcafe2dde05ff765a302c68fa761670dc5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 23 Aug 2020 20:43:50 +0530 Subject: [PATCH 059/175] fix: Negative SLE not created for fraction qty or qty less than 1 --- erpnext/stock/stock_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index e1b3730f2f..f4490f1b01 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -31,7 +31,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc sle['posting_time'] = now_datetime().strftime('%H:%M:%S.%f') if cancel: - sle['actual_qty'] = -flt(sle.get('actual_qty'), 0) + sle['actual_qty'] = -flt(sle.get('actual_qty')) if sle['actual_qty'] < 0 and not sle.get('outgoing_rate'): sle['outgoing_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code, From 8331a38acee2e1c7101120f6e31df0d21e8aacde Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 23 Aug 2020 21:23:26 +0530 Subject: [PATCH 060/175] fix: Test Case for qty less than 1 --- .../purchase_receipt/test_purchase_receipt.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index d97b9e82c3..52e5e5568e 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -18,6 +18,28 @@ class TestPurchaseReceipt(unittest.TestCase): set_perpetual_inventory(0) frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) + def test_reverse_purchase_receipt_sle(self): + + frappe.db.set_value('UOM', '_Test UOM', 'must_be_whole_number', 0) + + pr = make_purchase_receipt(qty=0.5) + + sl_entry = frappe.db.get_all("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", + "voucher_no": pr.name}, ['actual_qty']) + + self.assertEqual(len(sl_entry), 1) + self.assertEqual(sl_entry[0].actual_qty, 0.5) + + pr.cancel() + + sl_entry_cancelled = frappe.db.get_all("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", + "voucher_no": pr.name}, ['actual_qty'], order_by='creation') + + self.assertEqual(len(sl_entry_cancelled), 2) + self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5) + + frappe.db.set_value('UOM', '_Test UOM', 'must_be_whole_number', 1) + def test_make_purchase_invoice(self): pr = make_purchase_receipt(do_not_save=True) self.assertRaises(frappe.ValidationError, make_purchase_invoice, pr.name) From e292fd6c5877c346c74f3c7cde02e4b7677ca912 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Mon, 24 Aug 2020 00:45:13 +0530 Subject: [PATCH 061/175] feat: add integrations desk page --- .../erpnext_integrations.json | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 erpnext/erpnext_integrations/desk_page/erpnext_integrations/erpnext_integrations.json diff --git a/erpnext/erpnext_integrations/desk_page/erpnext_integrations/erpnext_integrations.json b/erpnext/erpnext_integrations/desk_page/erpnext_integrations/erpnext_integrations.json new file mode 100644 index 0000000000..8dcc77d174 --- /dev/null +++ b/erpnext/erpnext_integrations/desk_page/erpnext_integrations/erpnext_integrations.json @@ -0,0 +1,40 @@ +{ + "cards": [ + { + "hidden": 0, + "label": "Marketplace", + "links": "[\n {\n \"description\": \"Woocommerce marketplace settings\",\n \"label\": \"Woocommerce Settings\",\n \"name\": \"Woocommerce Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Amazon MWS settings\",\n \"label\": \"Amazon MWS Settings\",\n \"name\": \"Amazon MWS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Shopify settings\",\n \"label\": \"Shopify Settings\",\n \"name\": \"Shopify Settings\",\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Payments", + "links": "[\n {\n \"description\": \"GoCardless payment gateway settings\",\n \"label\": \"GoCardless Settings\",\n \"name\": \"GoCardless Settings\",\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Settings", + "links": "[\n {\n \"description\": \"Plaid settings\",\n \"label\": \"Plaid Settings\",\n \"name\": \"Plaid Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Exotel settings\",\n \"label\": \"Exotel Settings\",\n \"name\": \"Exotel Settings\",\n \"type\": \"doctype\"\n }\n]" + } + ], + "category": "Modules", + "charts": [], + "creation": "2020-08-20 19:30:48.138801", + "developer_mode_only": 0, + "disable_user_customization": 0, + "docstatus": 0, + "doctype": "Desk Page", + "extends": "Integrations", + "extends_another_page": 1, + "hide_custom": 1, + "idx": 0, + "is_standard": 1, + "label": "ERPNext Integrations", + "modified": "2020-08-23 16:30:51.494655", + "modified_by": "Administrator", + "module": "ERPNext Integrations", + "name": "ERPNext Integrations", + "owner": "Administrator", + "pin_to_bottom": 0, + "pin_to_top": 0, + "shortcuts": [] +} \ No newline at end of file From 64e84643fb82fb2e4509e661bade43ab929fd80f Mon Sep 17 00:00:00 2001 From: Anupam K Date: Sun, 23 Aug 2020 17:15:40 +0530 Subject: [PATCH 062/175] lost opportunity --- .../crm/report/lead_details/lead_details.py | 11 +-- .../lost_opportunity/lost_opportunity.js | 26 +++++++ .../lost_opportunity/lost_opportunity.py | 71 ++++++++++++++----- 3 files changed, 84 insertions(+), 24 deletions(-) diff --git a/erpnext/crm/report/lead_details/lead_details.py b/erpnext/crm/report/lead_details/lead_details.py index 9f4ae60679..eeaaec2bce 100644 --- a/erpnext/crm/report/lead_details/lead_details.py +++ b/erpnext/crm/report/lead_details/lead_details.py @@ -140,18 +140,19 @@ def get_data(filters): `tabAddress`.name=`tabDynamic Link`.parent) WHERE company = %(company)s - AND `tabLead`.creation BETWEEN %(from_date)s AND %(to_date)s + AND `tabLead`.creation BETWEEN %(from_date)s AND %(to_date)s {conditions} ORDER BY `tabLead`.creation asc """.format(conditions=get_conditions(filters)), filters, as_dict=1) def get_conditions(filters) : - conditions = "" + conditions = [] if filters.get("territory"): - conditions+=" and territory=%(territory)s " + conditions.append(" and `tabLead`.territory=%(territory)s") if filters.get("status"): - conditions+=" and status=%(status)s " + conditions.append(" and `tabLead`.status=%(status)s") + + return " ".join(conditions) if conditions else "" - return conditions diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.js b/erpnext/crm/report/lost_opportunity/lost_opportunity.js index c6bf888da3..d79f8c8480 100644 --- a/erpnext/crm/report/lost_opportunity/lost_opportunity.js +++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.js @@ -12,6 +12,32 @@ frappe.query_reports["Lost Opportunity"] = { "default": frappe.defaults.get_user_default("Company"), "reqd": 1 }, + { + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -12), + "reqd": 1 + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.get_today(), + "reqd": 1 + }, + { + "fieldname":"lost_reason", + "label": __("Lost Reason"), + "fieldtype": "Link", + "options": "Opportunity Lost Reason" + }, + { + "fieldname":"territory", + "label": __("Territory"), + "fieldtype": "Link", + "options": "Territory" + }, { "fieldname":"opportunity_from", "label": __("Opportunity From"), diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.py b/erpnext/crm/report/lost_opportunity/lost_opportunity.py index 094fa961f7..1aa4afe186 100644 --- a/erpnext/crm/report/lost_opportunity/lost_opportunity.py +++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.py @@ -44,18 +44,32 @@ def get_columns(): "fieldtype": "Data", "width": 130 }, - { - "label": _("Next Contact By"), - "fieldname": "contact_by", - "fieldtype": "Link", - "options": "User", - "width": 120 - }, { "label": _("Lost Reasons"), "fieldname": "lost_reason", "fieldtype": "Data", "width": 220 + }, + { + "label": _("Sales Stage"), + "fieldname": "sales_stage", + "fieldtype": "Link", + "options": "Sales Stage", + "width": 150 + }, + { + "label": _("Territory"), + "fieldname": "territory", + "fieldtype": "Link", + "options": "Territory", + "width": 150 + }, + { + "label": _("Next Contact By"), + "fieldname": "contact_by", + "fieldtype": "Link", + "options": "User", + "width": 150 } ] return columns @@ -69,30 +83,49 @@ def get_data(filters): `tabOpportunity`.customer_name, `tabOpportunity`.opportunity_type, `tabOpportunity`.contact_by, - GROUP_CONCAT(`tabLost Reason Detail`.lost_reason separator ', ') lost_reason + GROUP_CONCAT(`tabOpportunity Lost Reason Detail`.lost_reason separator ', ') lost_reason, + `tabOpportunity`.sales_stage, + `tabOpportunity`.territory FROM - `tabOpportunity` - LEFT JOIN `tabLost Reason Detail` - ON `tabLost Reason Detail`.parenttype = 'Opportunity' and `tabLost Reason Detail`.parent = `tabOpportunity`.name + `tabOpportunity` + {join} WHERE `tabOpportunity`.status = 'Lost' and `tabOpportunity`.company = %(company)s - {conditions} + AND `tabOpportunity`.modified BETWEEN %(from_date)s AND %(to_date)s + {conditions} GROUP BY - `tabOpportunity`.name + `tabOpportunity`.name ORDER BY - `tabOpportunity`.creation asc """.format(conditions=get_conditions(filters)), filters, as_dict=1) + `tabOpportunity`.creation asc """.format(conditions=get_conditions(filters), join=get_join(filters)), filters, as_dict=1) -def get_conditions(filters) : +def get_conditions(filters): conditions = [] + if filters.get("territory"): + conditions.append(" and `tabOpportunity`.territory=%(territory)s") + if filters.get("opportunity_from"): - conditions.append("opportunity_from=%(opportunity_from)s") + conditions.append(" and `tabOpportunity`.opportunity_from=%(opportunity_from)s") if filters.get("party_name"): - conditions.append("party_name=%(party_name)s") + conditions.append(" and `tabOpportunity`.party_name=%(party_name)s") if filters.get("contact_by"): - conditions.append("contact_by=%(contact_by)s") + conditions.append(" and `tabOpportunity`.contact_by=%(contact_by)s") - return " and {}".format(" and ".join(conditions)) if conditions else "" + return " ".join(conditions) if conditions else "" + +def get_join(filters): + join = """LEFT JOIN `tabOpportunity Lost Reason Detail` + ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and + `tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name""" + + if filters.get("lost_reason"): + join = """JOIN `tabOpportunity Lost Reason Detail` + ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and + `tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name and + `tabOpportunity Lost Reason Detail`.lost_reason = '{0}' + """.format(filters.get("lost_reason")) + + return join \ No newline at end of file From f49665077c59716db6a985639dfb4388a43db976 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 24 Aug 2020 18:26:48 +0530 Subject: [PATCH 063/175] feat: Quoted Item Comparison Report Enhancements v2 --- .../quoted_item_comparison.js | 58 +++++++++++-- .../quoted_item_comparison.py | 83 +++++++++++++------ 2 files changed, 109 insertions(+), 32 deletions(-) diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js index a76ffeec2e..8718e4e2ec 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js @@ -12,7 +12,22 @@ frappe.query_reports["Quoted Item Comparison"] = { "reqd": 1 }, { - reqd: 1, + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "width": "80", + "reqd": 1, + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "width": "80", + "reqd": 1, + "default": frappe.datetime.get_today() + }, + { default: "", options: "Item", label: __("Item"), @@ -45,13 +60,12 @@ frappe.query_reports["Quoted Item Comparison"] = { } }, { - fieldtype: "Link", + fieldtype: "MultiSelectList", label: __("Supplier Quotation"), - options: "Supplier Quotation", fieldname: "supplier_quotation", default: "", - get_query: () => { - return { filters: { "docstatus": ["<", 2] } } + get_data: function(txt) { + return frappe.db.get_link_options('Supplier Quotation', txt); } }, { @@ -63,9 +77,30 @@ frappe.query_reports["Quoted Item Comparison"] = { get_query: () => { return { filters: { "docstatus": ["<", 2] } } } + }, + { + fieldtype: "Check", + label: __("Include Expired"), + fieldname: "include_expired", + default: 0 } ], + formatter: (value, row, column, data, default_formatter) => { + value = default_formatter(value, row, column, data); + + if(column.fieldname === "valid_till" && data.valid_till){ + if(frappe.datetime.get_diff(data.valid_till, frappe.datetime.nowdate()) <= 1){ + value = `
    ${value}
    `; + } + else if (frappe.datetime.get_diff(data.valid_till, frappe.datetime.nowdate()) <= 7){ + value = `
    ${value}
    `; + } + } + + return value; + }, + onload: (report) => { // Create a button for setting the default supplier report.page.add_inner_button(__("Select Default Supplier"), () => { @@ -75,6 +110,19 @@ frappe.query_reports["Quoted Item Comparison"] = { reporter.make_default_supplier_dialog(report); }, 'Tools'); + const status_message = ` + + Valid till :    + + + Expires in a week + +      + + Expires today / Already Expired + ` + report.$status.html(status_message).show(); + }, make_default_supplier_dialog: (report) => { // Get the name of the item to change diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py index a33867a525..ffa138f1e9 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py @@ -16,44 +16,48 @@ def execute(filters=None): supplier_quotation_data = get_data(filters, conditions) columns = get_columns() - data, chart_data = prepare_data(supplier_quotation_data) + data, chart_data = prepare_data(supplier_quotation_data, filters) return columns, data, None, chart_data def get_conditions(filters): conditions = "" + if filters.get("item_code"): + conditions += " AND sqi.item_code = %(item_code)s" + if filters.get("supplier_quotation"): - conditions += " AND sqi.parent = %(supplier_quotation)s" + conditions += " AND sqi.parent in %(supplier_quotation)s" if filters.get("request_for_quotation"): conditions += " AND sqi.request_for_quotation = %(request_for_quotation)s" if filters.get("supplier"): conditions += " AND sq.supplier in %(supplier)s" + + if not filters.get("include_expired"): + conditions += " AND sq.status != 'Expired'" + return conditions def get_data(filters, conditions): - if not filters.get("item_code"): - return [] - supplier_quotation_data = frappe.db.sql("""SELECT - sqi.parent, sqi.qty, sqi.rate, sqi.uom, sqi.request_for_quotation, - sq.supplier + sqi.parent, sqi.item_code, sqi.qty, sqi.rate, sqi.uom, sqi.request_for_quotation, + sqi.lead_time_days, sq.supplier, sq.valid_till FROM `tabSupplier Quotation Item` sqi, `tabSupplier Quotation` sq WHERE - sqi.item_code = %(item_code)s - AND sqi.parent = sq.name + sqi.parent = sq.name AND sqi.docstatus < 2 AND sq.company = %(company)s - AND sq.status != 'Expired' - {0}""".format(conditions), filters, as_dict=1) + AND sq.transaction_date between %(from_date)s and %(to_date)s + {0} + order by sq.transaction_date, sqi.item_code""".format(conditions), filters, as_dict=1) return supplier_quotation_data -def prepare_data(supplier_quotation_data): - out, suppliers, qty_list = [], [], [] +def prepare_data(supplier_quotation_data, filters): + out, suppliers, qty_list, chart_data = [], [], [], [] supplier_wise_map = defaultdict(list) supplier_qty_price_map = {} @@ -70,20 +74,24 @@ def prepare_data(supplier_quotation_data): exchange_rate = 1 row = { + "item_code": data.get('item_code'), "quotation": data.get("parent"), "qty": data.get("qty"), "price": flt(data.get("rate") * exchange_rate, float_precision), "uom": data.get("uom"), "request_for_quotation": data.get("request_for_quotation"), + "valid_till": data.get('valid_till'), + "lead_time_days": data.get('lead_time_days') } # map for report view of form {'supplier1':[{},{},...]} supplier_wise_map[supplier].append(row) # map for chart preparation of the form {'supplier1': {'qty': 'price'}} - if not supplier in supplier_qty_price_map: - supplier_qty_price_map[supplier] = {} - supplier_qty_price_map[supplier][row["qty"]] = row["price"] + if filters.get("item_code"): + if not supplier in supplier_qty_price_map: + supplier_qty_price_map[supplier] = {} + supplier_qty_price_map[supplier][row["qty"]] = row["price"] suppliers.append(supplier) qty_list.append(data.get("qty")) @@ -97,7 +105,8 @@ def prepare_data(supplier_quotation_data): for entry in supplier_wise_map[supplier]: out.append(entry) - chart_data = prepare_chart_data(suppliers, qty_list, supplier_qty_price_map) + if filters.get("item_code"): + chart_data = prepare_chart_data(suppliers, qty_list, supplier_qty_price_map) return out, chart_data @@ -117,9 +126,10 @@ def prepare_chart_data(suppliers, qty_list, supplier_qty_price_map): data_points_map[qty].append(None) dataset = [] + currency_symbol = frappe.db.get_value("Currency", frappe.db.get_default("currency"), "symbol") for qty in qty_list: datapoints = { - "name": _("Price for Qty ") + str(qty), + "name": currency_symbol + " (Qty " + str(qty) + " )", "values": data_points_map[qty] } dataset.append(datapoints) @@ -140,14 +150,21 @@ def get_columns(): "label": _("Supplier"), "fieldtype": "Link", "options": "Supplier", + "width": 150 + }, + { + "fieldname": "item_code", + "label": _("Item"), + "fieldtype": "Link", + "options": "Item", "width": 200 }, { - "fieldname": "quotation", - "label": _("Supplier Quotation"), + "fieldname": "uom", + "label": _("UOM"), "fieldtype": "Link", - "options": "Supplier Quotation", - "width": 200 + "options": "UOM", + "width": 90 }, { "fieldname": "qty", @@ -163,18 +180,30 @@ def get_columns(): "width": 110 }, { - "fieldname": "uom", - "label": _("UOM"), + "fieldname": "quotation", + "label": _("Supplier Quotation"), "fieldtype": "Link", - "options": "UOM", - "width": 90 + "options": "Supplier Quotation", + "width": 200 + }, + { + "fieldname": "valid_till", + "label": _("Valid Till"), + "fieldtype": "Date", + "width": 100 + }, + { + "fieldname": "lead_time_days", + "label": _("Lead Time (Days)"), + "fieldtype": "Int", + "width": 100 }, { "fieldname": "request_for_quotation", "label": _("Request for Quotation"), "fieldtype": "Link", "options": "Request for Quotation", - "width": 200 + "width": 150 } ] From 8f452b86630b306dd47c6360d074a9d1ec2ad040 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 24 Aug 2020 18:57:07 +0530 Subject: [PATCH 064/175] fix: Codacy and indicator message --- .../report/quoted_item_comparison/quoted_item_comparison.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js index 8718e4e2ec..ad390c446b 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js @@ -115,12 +115,12 @@ frappe.query_reports["Quoted Item Comparison"] = { Valid till :    - Expires in a week + Expires in a week or less      Expires today / Already Expired - ` + `; report.$status.html(status_message).show(); }, From 7556517a4fde103476cc39e0d32289595e56271e Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 24 Aug 2020 14:07:09 +0000 Subject: [PATCH 065/175] feat: validate if removed item attributes exist in variants (#22911) * feat: validate if removed item attributes exist in variants * Update erpnext/stock/doctype/item/item.py Co-authored-by: Marica * Update erpnext/stock/doctype/item/item.py Co-authored-by: Marica * refactor: smaller loop Co-authored-by: marination * feat: don't run validation for new entries Co-authored-by: Marica * fix: use tuple as is Co-authored-by: Marica * refactor: error description Co-authored-by: Marica * refactor: remove unused variable Co-authored-by: Marica --- erpnext/stock/doctype/item/item.py | 72 ++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index d209f48353..d22fda85f4 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -111,6 +111,7 @@ class Item(WebsiteGenerator): self.synced_with_hub = 0 self.validate_has_variants() + self.validate_attributes_in_variants() self.validate_stock_exists_for_template_item() self.validate_attributes() self.validate_variant_attributes() @@ -806,6 +807,77 @@ class Item(WebsiteGenerator): if frappe.db.exists("Item", {"variant_of": self.name}): frappe.throw(_("Item has variants.")) + def validate_attributes_in_variants(self): + if not self.has_variants or self.get("__islocal"): + return + + old_doc = self.get_doc_before_save() + old_doc_attributes = set([attr.attribute for attr in old_doc.attributes]) + own_attributes = [attr.attribute for attr in self.attributes] + + # Check if old attributes were removed from the list + # Is old_attrs is a subset of new ones + # that means we need not check any changes + if old_doc_attributes.issubset(set(own_attributes)): + return + + from collections import defaultdict + + # get all item variants + items = [item["name"] for item in frappe.get_all("Item", {"variant_of": self.name})] + + # get all deleted attributes + deleted_attribute = list(old_doc_attributes.difference(set(own_attributes))) + + # fetch all attributes of these items + item_attributes = frappe.get_all( + "Item Variant Attribute", + filters={ + "parent": ["in", items], + "attribute": ["in", deleted_attribute] + }, + fields=["attribute", "parent"] + ) + not_included = defaultdict(list) + + for attr in item_attributes: + if attr["attribute"] not in own_attributes: + not_included[attr["parent"]].append(attr["attribute"]) + + if not len(not_included): + return + + def body(docnames): + docnames.sort() + return "
    ".join(docnames) + + def table_row(title, body): + return """ + {0} + {1} + """.format(title, body) + + rows = '' + for docname, attr_list in not_included.items(): + link = "{0}".format(frappe.bold(_(docname))) + rows += table_row(link, body(attr_list)) + + error_description = _('The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template.') + + message = """ +
    {0}

    + + + + + + {3} +
    {1}{2}
    + """.format(error_description, _('Variant Items'), _('Attributes'), rows) + + frappe.throw(message, title=_("Variant Attribute Error"), is_minimizable=True, wide=True) + + def validate_stock_exists_for_template_item(self): if self.stock_ledger_created() and self._doc_before_save: if (cint(self._doc_before_save.has_variants) != cint(self.has_variants) From 28f4417fc526eef80d3f24911ed1a0917d1359ea Mon Sep 17 00:00:00 2001 From: Anupam K Date: Tue, 25 Aug 2020 08:59:37 +0530 Subject: [PATCH 066/175] fix: added filter show in website for filtering product --- erpnext/portal/product_configurator/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index 9eef16bed3..9ba4cdc514 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -13,13 +13,15 @@ def get_field_filter_data(): for f in fields: doctype = f.get_link_doctype() - # apply enable/disable filter + # apply enable/disable/show_in_website filter meta = frappe.get_meta(doctype) filters = {} if meta.has_field('enabled'): filters['enabled'] = 1 if meta.has_field('disabled'): filters['disabled'] = 0 + if meta.has_field('show_in_website'): + filters['show_in_website'] = 1 values = [d.name for d in frappe.get_all(doctype, filters)] filter_data.append([f, values]) From ff5d19609fc89a56ea70bb219dea74b3ec5729a9 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 25 Aug 2020 11:59:57 +0530 Subject: [PATCH 067/175] fix(hot): Pricing Rule encoding fixed --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 11 ++++------- erpnext/accounts/doctype/pricing_rule/utils.py | 11 ++++++++--- erpnext/controllers/accounts_controller.py | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index cff7d5ba22..a2e35bc7c4 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -1,5 +1,4 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt # For license information, please see license.txt @@ -9,6 +8,8 @@ import json import copy from frappe import throw, _ from frappe.utils import flt, cint, getdate +from erpnext.accounts.doctype.pricing_rule.utils import (get_pricing_rules, + get_applied_pricing_rules, get_pricing_rule_items, get_product_discount_rule) from frappe.model.document import Document @@ -207,9 +208,6 @@ def get_serial_no_for_item(args): return item_details def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False): - from erpnext.accounts.doctype.pricing_rule.utils import (get_pricing_rules, - get_applied_pricing_rules, get_pricing_rule_items, get_product_discount_rule) - if isinstance(doc, string_types): doc = json.loads(doc) @@ -237,7 +235,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa update_args_for_pricing_rule(args) - pricing_rules = (get_applied_pricing_rules(args) + pricing_rules = (get_applied_pricing_rules(args.get('pricing_rules')) if for_validate and args.get("pricing_rules") else get_pricing_rules(args, doc)) if pricing_rules: @@ -365,8 +363,7 @@ def set_discount_amount(rate, item_details): item_details.rate = rate def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): - from erpnext.accounts.doctype.pricing_rule.utils import get_pricing_rule_items - for d in json.loads(pricing_rules): + for d in get_applied_pricing_rules(pricing_rules): if not d or not frappe.db.exists("Pricing Rule", d): continue pricing_rule = frappe.get_cached_doc('Pricing Rule', d) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 3fd316f75e..53b0cf7bba 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -447,9 +447,14 @@ def apply_pricing_rule_on_transaction(doc): apply_pricing_rule_for_free_items(doc, item_details.free_item_data) doc.set_missing_values() -def get_applied_pricing_rules(item_row): - return (json.loads(item_row.get("pricing_rules")) - if item_row.get("pricing_rules") else []) +def get_applied_pricing_rules(pricing_rules): + if pricing_rules: + if pricing_rules.startswith('['): + return json.loads(pricing_rules) + else: + return pricing_rules.split(',') + + return [] def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): free_item = pricing_rule.free_item diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 3091193b8d..d61e44b53d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -325,7 +325,7 @@ class AccountsController(TransactionBase): apply_pricing_rule_for_free_items(self, pricing_rule_args.get('free_item_data')) elif pricing_rule_args.get("validate_applied_rule"): - for pricing_rule in get_applied_pricing_rules(item): + for pricing_rule in get_applied_pricing_rules(item.get('pricing_rules')): pricing_rule_doc = frappe.get_cached_doc("Pricing Rule", pricing_rule) for field in ['discount_percentage', 'discount_amount', 'rate']: if item.get(field) < pricing_rule_doc.get(field): From 8ad0fa48bf068302e4582c018e82dc752e13a565 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 25 Aug 2020 12:24:31 +0530 Subject: [PATCH 068/175] fix(minor): circular imports? --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index a2e35bc7c4..f4e5ec69dc 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -8,8 +8,6 @@ import json import copy from frappe import throw, _ from frappe.utils import flt, cint, getdate -from erpnext.accounts.doctype.pricing_rule.utils import (get_pricing_rules, - get_applied_pricing_rules, get_pricing_rule_items, get_product_discount_rule) from frappe.model.document import Document @@ -208,6 +206,9 @@ def get_serial_no_for_item(args): return item_details def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False): + from erpnext.accounts.doctype.pricing_rule.utils import (get_pricing_rules, + get_applied_pricing_rules, get_pricing_rule_items, get_product_discount_rule) + if isinstance(doc, string_types): doc = json.loads(doc) @@ -363,6 +364,7 @@ def set_discount_amount(rate, item_details): item_details.rate = rate def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): + from erpnext.accounts.doctype.pricing_rule.utils import get_applied_pricing_rules for d in get_applied_pricing_rules(pricing_rules): if not d or not frappe.db.exists("Pricing Rule", d): continue pricing_rule = frappe.get_cached_doc('Pricing Rule', d) From b5b8f9e72d1b9bb0375c293bf71d9add533a7a22 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 25 Aug 2020 14:14:23 +0530 Subject: [PATCH 069/175] fix(minor): import --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index f4e5ec69dc..aa6194cbc3 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -364,7 +364,8 @@ def set_discount_amount(rate, item_details): item_details.rate = rate def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): - from erpnext.accounts.doctype.pricing_rule.utils import get_applied_pricing_rules + from erpnext.accounts.doctype.pricing_rule.utils import (get_applied_pricing_rules, + get_pricing_rule_items) for d in get_applied_pricing_rules(pricing_rules): if not d or not frappe.db.exists("Pricing Rule", d): continue pricing_rule = frappe.get_cached_doc('Pricing Rule', d) From ee921052c0025a88a6d7f2e4030bb5c4e874d606 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 25 Aug 2020 14:35:49 +0530 Subject: [PATCH 070/175] fix(minor): test dependencies --- erpnext/accounts/doctype/coupon_code/test_coupon_code.py | 3 ++- erpnext/accounts/doctype/pos_profile/test_pos_profile.py | 4 +++- .../doctype/clinical_procedure/test_clinical_procedure.py | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py index 3a0d4162ae..340b9dd58a 100644 --- a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py +++ b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py @@ -9,6 +9,8 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde from erpnext.stock.get_item_details import get_item_details from frappe.test_runner import make_test_objects +test_dependencies = ['Item'] + def test_create_test_data(): frappe.set_user("Administrator") # create test item @@ -95,7 +97,6 @@ def test_create_test_data(): }) coupon_code.insert() - class TestCouponCode(unittest.TestCase): def setUp(self): test_create_test_data() diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py index 8a4050cf9e..edf86590c8 100644 --- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py @@ -8,6 +8,8 @@ import unittest from erpnext.stock.get_item_details import get_pos_profile from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes +test_dependencies = ['Item'] + class TestPOSProfile(unittest.TestCase): def test_pos_profile(self): make_pos_profile() @@ -88,7 +90,7 @@ def make_pos_profile(**args): "write_off_account": args.write_off_account or "_Test Write Off - _TC", "write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC" }) - + payments = [{ 'mode_of_payment': 'Cash', 'default': 1 diff --git a/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py index 207351ff20..4ee5f6bad3 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py +++ b/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py @@ -7,6 +7,8 @@ import unittest import frappe from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_clinical_procedure_template +test_dependencies = ['Item'] + class TestClinicalProcedure(unittest.TestCase): def test_procedure_template_item(self): patient, medical_department, practitioner = create_healthcare_docs() From 42092cc3776ff8d36ce39607555bb8b2bac5911d Mon Sep 17 00:00:00 2001 From: Anupam K Date: Tue, 25 Aug 2020 15:29:18 +0530 Subject: [PATCH 071/175] fix: Homepage Featured Product image fetch issue --- erpnext/portal/doctype/homepage/homepage.js | 28 -- .../homepage_featured_product.json | 383 +++++------------- 2 files changed, 99 insertions(+), 312 deletions(-) diff --git a/erpnext/portal/doctype/homepage/homepage.js b/erpnext/portal/doctype/homepage/homepage.js index ca34d69576..c7c66e0055 100644 --- a/erpnext/portal/doctype/homepage/homepage.js +++ b/erpnext/portal/doctype/homepage/homepage.js @@ -21,34 +21,6 @@ frappe.ui.form.on('Homepage', { }); frappe.ui.form.on('Homepage Featured Product', { - item_code: function(frm, cdt, cdn) { - var featured_product = frappe.model.get_doc(cdt, cdn); - if (featured_product.item_code) { - frappe.call({ - method: 'frappe.client.get_value', - args: { - 'doctype': 'Item', - 'filters': {'name': featured_product.item_code}, - 'fieldname': [ - 'item_name', - 'web_long_description', - 'description', - 'image', - 'thumbnail' - ] - }, - callback: function(r) { - if (!r.exc) { - $.extend(featured_product, r.message); - if (r.message.web_long_description) { - featured_product.description = r.message.web_long_description; - } - frm.refresh_field('products'); - } - } - }); - } - }, view: function(frm, cdt, cdn){ var child= locals[cdt][cdn] diff --git a/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json b/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json index c8b4ae9b74..01c32efec9 100644 --- a/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json +++ b/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json @@ -1,301 +1,116 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "hash", - "beta": 0, - "creation": "2016-04-22 05:57:06.261401", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, + "actions": [], + "autoname": "hash", + "creation": "2016-04-22 05:57:06.261401", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "col_break1", + "item_name", + "view", + "section_break_5", + "description", + "column_break_7", + "image", + "thumbnail", + "route" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "fieldname": "item_code", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_list_view": 1, - "label": "Item Code", - "length": 0, - "no_copy": 0, - "oldfieldname": "item_code", - "oldfieldtype": "Link", - "options": "Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "150px", - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "unique": 0, + "bold": 1, + "fieldname": "item_code", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "label": "Item Code", + "oldfieldname": "item_code", + "oldfieldtype": "Link", + "options": "Item", + "print_width": "150px", + "reqd": 1, + "search_index": 1, "width": "150px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "col_break1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "col_break1", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "item_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Item Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "item_name", - "oldfieldtype": "Data", - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": "150", - "read_only": 1, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fetch_from": "item_code.item_name", + "fetch_if_empty": 1, + "fieldname": "item_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Item Name", + "oldfieldname": "item_name", + "oldfieldtype": "Data", + "print_hide": 1, + "print_width": "150", + "read_only": 1, + "reqd": 1, "width": "150" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "view", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "View", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "view", + "fieldtype": "Button", + "in_list_view": 1, + "label": "View" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "fieldname": "section_break_5", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "collapsible": 1, + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "label": "Description" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "description", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_list_view": 1, - "label": "Description", - "length": 0, - "no_copy": 0, - "oldfieldname": "description", - "oldfieldtype": "Small Text", - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "300px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fetch_from": "item_code.web_long_description", + "fieldname": "description", + "fieldtype": "Text Editor", + "in_filter": 1, + "in_list_view": 1, + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Small Text", + "print_width": "300px", "width": "300px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "column_break_7", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "image", - "fieldtype": "Attach Image", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Image", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fetch_from": "item_code.website_image", + "fetch_if_empty": 1, + "fieldname": "image", + "fieldtype": "Attach Image", + "label": "Image" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "thumbnail", - "fieldtype": "Attach Image", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Thumbnail", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "thumbnail", + "fieldtype": "Attach Image", + "hidden": 1, + "label": "Thumbnail" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "route", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "route", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "route", + "fieldtype": "Small Text", + "label": "route", + "read_only": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2016-08-09 06:09:34.731971", - "modified_by": "Administrator", - "module": "Portal", - "name": "Homepage Featured Product", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_seen": 0 + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-08-25 15:27:49.573537", + "modified_by": "Administrator", + "module": "Portal", + "name": "Homepage Featured Product", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file From 2030b66fc6ca831c21e6a8d0a1dab82e0601f2b3 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 25 Aug 2020 17:09:53 +0530 Subject: [PATCH 072/175] feat: add customer link to call log --- .../communication/doctype/call_log/call_log.json | 16 ++++++++++++++-- .../communication/doctype/call_log/call_log.py | 3 +++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/erpnext/communication/doctype/call_log/call_log.json b/erpnext/communication/doctype/call_log/call_log.json index cfc08eb084..31e79f17cd 100644 --- a/erpnext/communication/doctype/call_log/call_log.json +++ b/erpnext/communication/doctype/call_log/call_log.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "field:id", "creation": "2019-06-05 12:07:02.634534", "doctype": "DocType", @@ -14,6 +15,7 @@ "contact", "contact_name", "column_break_10", + "customer", "lead", "lead_name", "section_break_5", @@ -28,7 +30,8 @@ }, { "fieldname": "section_break_5", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Call Details" }, { "fieldname": "id", @@ -125,10 +128,19 @@ "in_list_view": 1, "label": "Lead Name", "read_only": 1 + }, + { + "fieldname": "customer", + "fieldtype": "Link", + "label": "Customer", + "options": "Customer", + "read_only": 1 } ], "in_create": 1, - "modified": "2019-08-06 05:46:53.144683", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-08-25 17:08:34.085731", "modified_by": "Administrator", "module": "Communication", "name": "Call Log", diff --git a/erpnext/communication/doctype/call_log/call_log.py b/erpnext/communication/doctype/call_log/call_log.py index 5fe3c4edbb..b31b757a37 100644 --- a/erpnext/communication/doctype/call_log/call_log.py +++ b/erpnext/communication/doctype/call_log/call_log.py @@ -16,6 +16,9 @@ class CallLog(Document): self.contact = get_contact_with_phone_number(number) self.lead = get_lead_with_phone_number(number) + contact = frappe.get_doc("Contact", self.contact) + self.customer = contact.get_link_for("Customer") + def after_insert(self): self.trigger_call_popup() From de4ac0c9052aede26eee0208d3b66013a7fd908e Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 25 Aug 2020 17:10:05 +0530 Subject: [PATCH 073/175] chore: remove stray tabs --- erpnext/selling/doctype/customer/customer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 93d4832173..911fe511ae 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -184,10 +184,10 @@ class Customer(TransactionBase): def validate_credit_limit_on_change(self): if self.get("__islocal") or not self.credit_limits: return - + past_credit_limits = [d.credit_limit for d in frappe.db.get_all("Customer Credit Limit", filters={'parent': self.name}, fields=["credit_limit"], order_by="company")] - + current_credit_limits = [d.credit_limit for d in sorted(self.credit_limits, key=lambda k: k.company)] if past_credit_limits == current_credit_limits: From d43d1408cfcd674107613d1f1f1310cd6b1f7a57 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 25 Aug 2020 17:13:05 +0530 Subject: [PATCH 074/175] fix: conversion factor for BOM exploded item rate --- erpnext/manufacturing/doctype/bom/bom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index c51f655a66..3189433837 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -494,7 +494,7 @@ class BOM(WebsiteGenerator): 'image' : d.image, 'stock_uom' : d.stock_uom, 'stock_qty' : flt(d.stock_qty), - 'rate' : flt(d.base_rate) / flt(d.conversion_factor), + 'rate' : flt(d.base_rate) / (flt(d.conversion_factor) or 1.0), 'include_item_in_manufacturing': d.include_item_in_manufacturing })) From 5bf0a51ed4c26ebbd7221c4d33a1eba6204f3fc7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 25 Aug 2020 18:22:06 +0530 Subject: [PATCH 075/175] feat: add Maintenance Visit link in Customer dashboard --- erpnext/selling/doctype/customer/customer_dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/customer/customer_dashboard.py b/erpnext/selling/doctype/customer/customer_dashboard.py index 09e474dc2e..cf234650c8 100644 --- a/erpnext/selling/doctype/customer/customer_dashboard.py +++ b/erpnext/selling/doctype/customer/customer_dashboard.py @@ -33,7 +33,7 @@ def get_data(): }, { 'label': _('Support'), - 'items': ['Issue'] + 'items': ['Issue', 'Maintenance Visit'] }, { 'label': _('Projects'), From 12f7aeb14746413f721580a7e5737f0e1977d535 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 25 Aug 2020 19:03:35 +0530 Subject: [PATCH 076/175] feat: add Maintenance Visit link in Sales Order dashboard --- erpnext/selling/doctype/sales_order/sales_order_dashboard.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py index 4126bc6a70..05a760de27 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py +++ b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py @@ -10,6 +10,7 @@ def get_data(): 'Payment Entry': 'reference_name', 'Payment Request': 'reference_name', 'Auto Repeat': 'reference_document', + 'Maintenance Visit': 'prevdoc_docname' }, 'internal_links': { 'Quotation': ['items', 'prevdoc_docname'] @@ -17,7 +18,7 @@ def get_data(): 'transactions': [ { 'label': _('Fulfillment'), - 'items': ['Sales Invoice', 'Pick List', 'Delivery Note'] + 'items': ['Sales Invoice', 'Pick List', 'Delivery Note', 'Maintenance Visit'] }, { 'label': _('Purchasing'), From 258b256dc646aee1757bf0c34b934b688d8272ca Mon Sep 17 00:00:00 2001 From: Michelle Alva <50285544+michellealva@users.noreply.github.com> Date: Tue, 25 Aug 2020 20:37:39 +0530 Subject: [PATCH 077/175] fix: Add Delivery Note link in Sales Invoice Dashboard (#23161) --- .../accounts/doctype/sales_invoice/sales_invoice_dashboard.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index f1069282ed..2980213f3b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -13,7 +13,8 @@ def get_data(): 'Auto Repeat': 'reference_document', }, 'internal_links': { - 'Sales Order': ['items', 'sales_order'] + 'Sales Order': ['items', 'sales_order'], + 'Delivery Note': ['items', 'delivery_note'] }, 'transactions': [ { From 91461a0016a4e70b8472494e8aa1a51e6ef3b19f Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Tue, 25 Aug 2020 21:01:17 +0530 Subject: [PATCH 078/175] fix: heading is hidden in the mobile view (#23145) --- erpnext/support/doctype/issue/issue.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index 9e15757ce0..858564a527 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -209,11 +209,11 @@ function set_time_to_resolve_and_response(frm) { frm.dashboard.set_headline_alert( '
    ' + - '
    ' + - ' ' + + '
    ' + + 'Time to Respond: '+ time_to_respond.diff_display +' ' + '
    ' + - '
    ' + - ' ' + + '
    ' + + 'Time to Resolve: '+ time_to_resolve.diff_display +' ' + '
    ' + '
    ' ); From 7a927cefbd050990d8b72f93be0bf0a138e3ec78 Mon Sep 17 00:00:00 2001 From: Michelle Alva <50285544+michellealva@users.noreply.github.com> Date: Wed, 26 Aug 2020 10:32:26 +0530 Subject: [PATCH 079/175] fix: Social Media Status hidden in Mobie View (#23158) --- erpnext/crm/doctype/social_media_post/social_media_post.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js index 3a14f2d2e9..0ce8b44e19 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.js +++ b/erpnext/crm/doctype/social_media_post/social_media_post.js @@ -30,14 +30,14 @@ frappe.ui.form.on('Social Media Post', { let color = frm.doc.twitter_post_id ? "green" : "red"; let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted"; html += `
    - + Twitter : ${status}
    ` ; } if (frm.doc.linkedin){ let color = frm.doc.linkedin_post_id ? "green" : "red"; let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; html += `
    - + LinkedIn : ${status}
    ` ; } html = `
    ${html}
    `; From f4b939754c84111e9417734ea248e1c85a16ce1d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 26 Aug 2020 12:10:12 +0530 Subject: [PATCH 080/175] fix: Codacy fixes --- .../hsn_wise_summary_of_outward_supplies.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index 6f3fff2932..59389ce326 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -225,7 +225,7 @@ def get_json(filters, report_name, data): fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) - gst_json = {"gstin": "", "version": "GST2.3.4", + gst_json = {"version": "GST2.3.4", "hash": "hash", "gstin": gstin, "fp": fp} gst_json["hsn"] = { @@ -239,7 +239,7 @@ def get_json(filters, report_name, data): @frappe.whitelist() def download_json_file(): - ''' download json content in a file ''' + '''download json content in a file''' data = frappe._dict(frappe.local.form_dict) frappe.response['filename'] = frappe.scrub("{0}".format(data['report_name'])) + '.json' frappe.response['filecontent'] = data['data'] From d888a59bd1684890b4db52e9c470cc93f7da78a7 Mon Sep 17 00:00:00 2001 From: bhavesh95863 <34086262+bhavesh95863@users.noreply.github.com> Date: Wed, 26 Aug 2020 12:44:20 +0530 Subject: [PATCH 081/175] fix: set company filter in condition --- .../hr/report/employee_leave_balance/employee_leave_balance.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index db1d191758..1b92358184 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -132,6 +132,9 @@ def get_conditions(filters): if filters.get('employee'): conditions['name'] = filters.get('employee') + if filters.get('company'): + conditions['company'] = filters.get('company') + return conditions def get_department_leave_approver_map(department=None): From 38fad588340e30a11137a123330cb03f64fad887 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 25 Aug 2020 16:30:09 +0530 Subject: [PATCH 082/175] fix(patch) --- erpnext/patches/v12_0/create_irs_1099_field_united_states.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches/v12_0/create_irs_1099_field_united_states.py b/erpnext/patches/v12_0/create_irs_1099_field_united_states.py index 43bd0ccdd7..bce4ab10b3 100644 --- a/erpnext/patches/v12_0/create_irs_1099_field_united_states.py +++ b/erpnext/patches/v12_0/create_irs_1099_field_united_states.py @@ -7,6 +7,7 @@ def execute(): frappe.reload_doc('accounts', 'doctype', 'allowed_to_transact_with', force=True) frappe.reload_doc('accounts', 'doctype', 'pricing_rule_detail', force=True) frappe.reload_doc('crm', 'doctype', 'lost_reason_detail', force=True) + frappe.reload_doc('crm', 'doctype', 'quotation_lost_reason_detail', force=True) company = frappe.get_all('Company', filters = {'country': 'United States'}) if not company: From 544d18327c67c95bceb9d8b9b11b4958da3ff27c Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 25 Aug 2020 20:01:31 +0530 Subject: [PATCH 083/175] fix(patch): quotation_lost_reason_detail --- erpnext/patches/v12_0/create_irs_1099_field_united_states.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches/v12_0/create_irs_1099_field_united_states.py b/erpnext/patches/v12_0/create_irs_1099_field_united_states.py index bce4ab10b3..7feaffdf40 100644 --- a/erpnext/patches/v12_0/create_irs_1099_field_united_states.py +++ b/erpnext/patches/v12_0/create_irs_1099_field_united_states.py @@ -7,7 +7,7 @@ def execute(): frappe.reload_doc('accounts', 'doctype', 'allowed_to_transact_with', force=True) frappe.reload_doc('accounts', 'doctype', 'pricing_rule_detail', force=True) frappe.reload_doc('crm', 'doctype', 'lost_reason_detail', force=True) - frappe.reload_doc('crm', 'doctype', 'quotation_lost_reason_detail', force=True) + frappe.reload_doc('setup', 'doctype', 'quotation_lost_reason_detail', force=True) company = frappe.get_all('Company', filters = {'country': 'United States'}) if not company: From a163d8bc843296bed1095f55c19fd0e3a210e1ae Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 26 Aug 2020 12:00:45 +0530 Subject: [PATCH 084/175] fix(minor): rename_lost_reason_detail --- erpnext/patches/v12_0/rename_lost_reason_detail.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v12_0/rename_lost_reason_detail.py b/erpnext/patches/v12_0/rename_lost_reason_detail.py index 044d0232e0..d0dc356bd0 100644 --- a/erpnext/patches/v12_0/rename_lost_reason_detail.py +++ b/erpnext/patches/v12_0/rename_lost_reason_detail.py @@ -3,6 +3,7 @@ import frappe def execute(): if frappe.db.exists("DocType", "Lost Reason Detail"): + frappe.reload_doc("crm", "doctype", "opportunity_lost_reason") frappe.reload_doc("crm", "doctype", "opportunity_lost_reason_detail") frappe.reload_doc("setup", "doctype", "quotation_lost_reason_detail") @@ -10,8 +11,8 @@ def execute(): frappe.db.sql("""INSERT INTO `tabQuotation Lost Reason Detail` SELECT * FROM `tabLost Reason Detail` WHERE `parenttype` = 'Quotation'""") - frappe.db.sql("""INSERT INTO `tabQuotation Lost Reason` (`name`, `creation`, `modified`, `modified_by`, `owner`, `docstatus`, `parent`, `parentfield`, `parenttype`, `idx`, `_comments`, `_assign`, `_user_tags`, `_liked_by`, `order_lost_reason`) - SELECT o.`name`, o.`creation`, o.`modified`, o.`modified_by`, o.`owner`, o.`docstatus`, o.`parent`, o.`parentfield`, o.`parenttype`, o.`idx`, o.`_comments`, o.`_assign`, o.`_user_tags`, o.`_liked_by`, o.`lost_reason` + frappe.db.sql("""INSERT INTO `tabQuotation Lost Reason` (`name`, `creation`, `modified`, `modified_by`, `owner`, `docstatus`, `parent`, `parentfield`, `parenttype`, `idx`, `_comments`, `_assign`, `_user_tags`, `_liked_by`, `order_lost_reason`) + SELECT o.`name`, o.`creation`, o.`modified`, o.`modified_by`, o.`owner`, o.`docstatus`, o.`parent`, o.`parentfield`, o.`parenttype`, o.`idx`, o.`_comments`, o.`_assign`, o.`_user_tags`, o.`_liked_by`, o.`lost_reason` FROM `tabOpportunity Lost Reason` o LEFT JOIN `tabQuotation Lost Reason` q ON q.name = o.name WHERE q.name IS NULL""") - + frappe.delete_doc("DocType", "Lost Reason Detail") \ No newline at end of file From 73f648473f9ac70afd469d845b3103cd3f9e9829 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 26 Aug 2020 13:09:58 +0530 Subject: [PATCH 085/175] fix(minor): update_start_end_date_for_old_shift_assignment.py --- .../v13_0/update_start_end_date_for_old_shift_assignment.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v13_0/update_start_end_date_for_old_shift_assignment.py b/erpnext/patches/v13_0/update_start_end_date_for_old_shift_assignment.py index 7c07b987f3..0f521cb57a 100644 --- a/erpnext/patches/v13_0/update_start_end_date_for_old_shift_assignment.py +++ b/erpnext/patches/v13_0/update_start_end_date_for_old_shift_assignment.py @@ -7,4 +7,7 @@ import frappe def execute(): frappe.reload_doc('hr', 'doctype', 'shift_assignment') - frappe.db.sql("update `tabShift Assignment` set end_date=date, start_date=date where date IS NOT NULL and start_date IS NULL and end_date IS NULL;") + if frappe.db.has_column('Shift Assignment', 'date'): + frappe.db.sql("""update `tabShift Assignment` + set end_date=date, start_date=date + where date IS NOT NULL and start_date IS NULL and end_date IS NULL;""") From 9766adbf4eaf6abf990746bbbabfabf928dd4819 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 26 Aug 2020 14:53:23 +0530 Subject: [PATCH 086/175] fix: don't overwrite appointment duration if already specified --- .../doctype/patient_appointment/patient_appointment.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js index f7ed31bfea..2d6b64532b 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js @@ -226,7 +226,9 @@ let check_and_set_availability = function(frm) { primary_action_label: __('Book'), primary_action: function() { frm.set_value('appointment_time', selected_slot); - frm.set_value('duration', duration); + if (!frm.doc.duration) { + frm.set_value('duration', duration); + } frm.set_value('practitioner', d.get_value('practitioner')); frm.set_value('department', d.get_value('department')); frm.set_value('appointment_date', d.get_value('appointment_date')); From c7830f71072509cb7fb751b9836bd8f4d6a9e65d Mon Sep 17 00:00:00 2001 From: Anupam K Date: Wed, 26 Aug 2020 15:28:22 +0530 Subject: [PATCH 087/175] feat: Added phone field in product Inquiry --- erpnext/templates/generators/item/item_inquiry.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/templates/generators/item/item_inquiry.js b/erpnext/templates/generators/item/item_inquiry.js index 52ddae2624..e7db3a368d 100644 --- a/erpnext/templates/generators/item/item_inquiry.js +++ b/erpnext/templates/generators/item/item_inquiry.js @@ -20,6 +20,13 @@ frappe.ready(() => { options: 'Email', reqd: 1 }, + { + fieldtype: 'Data', + label: __('Phone Number'), + fieldname: 'phone', + options: 'Phone', + reqd: 1 + }, { fieldtype: 'Data', label: __('Subject'), From 733fd5f03c7c855457c27600285fd2d0ace4bad6 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 26 Aug 2020 18:23:12 +0530 Subject: [PATCH 088/175] fix: get_applied_pricing_rule in taxes_and_totals --- erpnext/controllers/taxes_and_totals.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 2a14be8532..92cfdb7f1a 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -9,6 +9,7 @@ from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction from erpnext.controllers.accounts_controller import validate_conversion_rate, \ validate_taxes_and_charges, validate_inclusive_tax from erpnext.stock.get_item_details import _get_item_tax_template +from erpnext.accounts.doctype.pricing_rule.utils import get_applied_pricing_rules class calculate_taxes_and_totals(object): def __init__(self, doc): @@ -209,7 +210,7 @@ class calculate_taxes_and_totals(object): elif tax.charge_type == "On Previous Row Total": current_tax_fraction = (tax_rate / 100.0) * \ self.doc.get("taxes")[cint(tax.row_id) - 1].grand_total_fraction_for_current_item - + elif tax.charge_type == "On Item Quantity": inclusive_tax_amount_per_qty = flt(tax_rate) @@ -607,7 +608,7 @@ class calculate_taxes_and_totals(object): base_rate_with_margin = 0.0 if item.price_list_rate: if item.pricing_rules and not self.doc.ignore_pricing_rule: - for d in json.loads(item.pricing_rules): + for d in get_applied_pricing_rules(item.pricing_rules): pricing_rule = frappe.get_cached_doc('Pricing Rule', d) if (pricing_rule.margin_type == 'Amount' and pricing_rule.currency == self.doc.currency)\ From bb5d886930da4caf37ba0ea5de38ede957155752 Mon Sep 17 00:00:00 2001 From: bhavesh95863 <34086262+bhavesh95863@users.noreply.github.com> Date: Wed, 26 Aug 2020 18:23:21 +0530 Subject: [PATCH 089/175] fix: account & cost center filter by company --- .../doctype/shipping_rule/shipping_rule.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.js b/erpnext/accounts/doctype/shipping_rule/shipping_rule.js index 53ee08a773..d0904eec3e 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.js +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.js @@ -3,6 +3,22 @@ frappe.ui.form.on('Shipping Rule', { refresh: function(frm) { + frm.set_query("cost_center", function() { + return { + filters: { + company: frm.doc.company + } + } + }) + + frm.set_query("account", function() { + return { + filters: { + company: frm.doc.company + } + } + }) + frm.trigger('toggle_reqd'); }, calculate_based_on: function(frm) { @@ -12,4 +28,4 @@ frappe.ui.form.on('Shipping Rule', { frm.toggle_reqd("shipping_amount", frm.doc.calculate_based_on === 'Fixed'); frm.toggle_reqd("conditions", frm.doc.calculate_based_on !== 'Fixed'); } -}); \ No newline at end of file +}); From 7600960d2b8b2b39c54b6176c9ff9e2331033e36 Mon Sep 17 00:00:00 2001 From: Afshan Date: Wed, 26 Aug 2020 18:40:11 +0530 Subject: [PATCH 090/175] fix: returned empty list if non US based company --- erpnext/regional/report/irs_1099/irs_1099.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py index 67834d1221..a06efc89c6 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.py +++ b/erpnext/regional/report/irs_1099/irs_1099.py @@ -16,9 +16,14 @@ from frappe.utils.jinja import render_template def execute(filters=None): filters = filters if isinstance(filters, _dict) else _dict(filters) + company = frappe.db.get_default("company") if not filters: filters.setdefault('fiscal_year', get_fiscal_year(nowdate())[0]) - filters.setdefault('company', frappe.db.get_default("company")) + filters.setdefault('company', company) + + region = frappe.db.get_value("Company", fieldname = ["country"], filters = { "name": company }) + if region != 'United States': + return [],[] data = [] columns = get_columns() data = frappe.db.sql(""" From b719620d0cdcb68c08b9805fd1066b7db47c9059 Mon Sep 17 00:00:00 2001 From: Afshan Date: Wed, 26 Aug 2020 20:55:16 +0530 Subject: [PATCH 091/175] fix: handleling condition if default company doesn't exist --- erpnext/regional/report/irs_1099/irs_1099.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py index a06efc89c6..d3509e500f 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.py +++ b/erpnext/regional/report/irs_1099/irs_1099.py @@ -16,14 +16,15 @@ from frappe.utils.jinja import render_template def execute(filters=None): filters = filters if isinstance(filters, _dict) else _dict(filters) - company = frappe.db.get_default("company") + if not filters: filters.setdefault('fiscal_year', get_fiscal_year(nowdate())[0]) - filters.setdefault('company', company) + filters.setdefault('company', frappe.db.get_default("company")) - region = frappe.db.get_value("Company", fieldname = ["country"], filters = { "name": company }) + region = frappe.db.get_value("Company", fieldname = ["country"], filters = { "name": filters.company }) if region != 'United States': return [],[] + data = [] columns = get_columns() data = frappe.db.sql(""" From 1bd83e69eeb3cb62afa2b6bbd80f5af80b6658c8 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 27 Aug 2020 13:06:52 +0530 Subject: [PATCH 092/175] feat: added Installation Note and Warranty Claim links to Customer dashboard (#23183) --- erpnext/selling/doctype/customer/customer_dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/customer/customer_dashboard.py b/erpnext/selling/doctype/customer/customer_dashboard.py index cf234650c8..532c11b86e 100644 --- a/erpnext/selling/doctype/customer/customer_dashboard.py +++ b/erpnext/selling/doctype/customer/customer_dashboard.py @@ -33,7 +33,7 @@ def get_data(): }, { 'label': _('Support'), - 'items': ['Issue', 'Maintenance Visit'] + 'items': ['Issue', 'Maintenance Visit', 'Installation Note', 'Warranty Claim'] }, { 'label': _('Projects'), From ead2f6abf0a8d11700ad16f022abdd99273666c1 Mon Sep 17 00:00:00 2001 From: michellealva Date: Thu, 27 Aug 2020 13:57:15 +0530 Subject: [PATCH 093/175] fix: Chage fiedtype of Customer's PO in Sales Invoice --- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 31613e50b0..2397b7d0cb 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -447,7 +447,7 @@ { "allow_on_submit": 1, "fieldname": "po_no", - "fieldtype": "Small Text", + "fieldtype": "Data", "hide_days": 1, "hide_seconds": 1, "label": "Customer's Purchase Order", @@ -1946,7 +1946,7 @@ "idx": 181, "is_submittable": 1, "links": [], - "modified": "2020-08-03 23:31:12.675040", + "modified": "2020-08-27 01:56:28.532140", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", From c8eea556d178607f2b4f09f1b683c64eed7f2bfc Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 27 Aug 2020 18:54:25 +0530 Subject: [PATCH 094/175] fix: BOM Update Tool failing due to Too Many Writes error --- .../manufacturing/doctype/bom_update_tool/bom_update_tool.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index e6c10ad12b..742d18c4cd 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -90,6 +90,7 @@ def update_latest_price_in_all_boms(): update_cost() def replace_bom(args): + frappe.db.auto_commit_on_many_writes = 1 args = frappe._dict(args) doc = frappe.get_doc("BOM Update Tool") @@ -97,6 +98,8 @@ def replace_bom(args): doc.new_bom = args.new_bom doc.replace_bom() + frappe.db.auto_commit_on_many_writes = 0 + def update_cost(): frappe.db.auto_commit_on_many_writes = 1 bom_list = get_boms_in_bottom_up_order() From 33f984c7af1f6b140b8306f0c8d5ee690bdf4805 Mon Sep 17 00:00:00 2001 From: bhavesh95863 <34086262+bhavesh95863@users.noreply.github.com> Date: Thu, 27 Aug 2020 21:49:21 +0530 Subject: [PATCH 095/175] fix: can't multiply sequence by non-int of type 'float --- erpnext/selling/page/point_of_sale/pos_item_cart.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index c23a6ad58f..62fc4163e1 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -356,7 +356,7 @@ erpnext.PointOfSale.ItemCart = class { onchange: function() { if (this.value || this.value == 0) { const frm = me.events.get_frm(); - frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', this.value); + frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', parseFloat(this.value)); me.hide_discount_control(this.value); } }, @@ -948,4 +948,4 @@ erpnext.PointOfSale.ItemCart = class { show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none'); } -} \ No newline at end of file +} From 3e9f493f15b05c364a6a2df11b11113899d0006c Mon Sep 17 00:00:00 2001 From: bhavesh95863 <34086262+bhavesh95863@users.noreply.github.com> Date: Fri, 28 Aug 2020 11:15:02 +0530 Subject: [PATCH 096/175] fix: change parseFloat to flt --- erpnext/selling/page/point_of_sale/pos_item_cart.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 62fc4163e1..eadeb8fde8 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -356,7 +356,7 @@ erpnext.PointOfSale.ItemCart = class { onchange: function() { if (this.value || this.value == 0) { const frm = me.events.get_frm(); - frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', parseFloat(this.value)); + frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', flt(this.value)); me.hide_discount_control(this.value); } }, From 6887382937874ee962336c43674acf715852cce8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Fri, 28 Aug 2020 11:52:00 +0530 Subject: [PATCH 097/175] fix: Ignore cpmpany and bank account doctype while deleting company transactions (#22953) --- erpnext/setup/doctype/company/delete_company_transactions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/delete_company_transactions.py b/erpnext/setup/doctype/company/delete_company_transactions.py index 8ecc13b2fb..c94831ef93 100644 --- a/erpnext/setup/doctype/company/delete_company_transactions.py +++ b/erpnext/setup/doctype/company/delete_company_transactions.py @@ -26,7 +26,8 @@ def delete_company_transactions(company_name): tabDocField where fieldtype='Link' and options='Company'"""): if doctype not in ("Account", "Cost Center", "Warehouse", "Budget", "Party Account", "Employee", "Sales Taxes and Charges Template", - "Purchase Taxes and Charges Template", "POS Profile", 'BOM'): + "Purchase Taxes and Charges Template", "POS Profile", "BOM", + "Company", "Bank Account"): delete_for_doctype(doctype, company_name) # reset company values From e9274283bd27415bfb410d5b1991730ea147272d Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 28 Aug 2020 12:21:10 +0530 Subject: [PATCH 098/175] fix: Raise Error on over receipt/consumption for sub-contrcated PR --- erpnext/controllers/buying_controller.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index f982700c01..ac567b7dea 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -276,6 +276,9 @@ class BuyingController(StockController): qty_to_be_received_map = get_qty_to_be_received(purchase_orders) for item in self.get('items'): + if not item.purchase_order: + continue + # reset raw_material cost item.rm_supp_cost = 0 @@ -288,6 +291,12 @@ class BuyingController(StockController): fg_yet_to_be_received = qty_to_be_received_map.get(item_key) + if not fg_yet_to_be_received: + frappe.throw(_("Row #{0}: Item {1} is already fully received in Purchase Order {2}") + .format(item.idx, frappe.bold(item.item_code), + frappe.utils.get_link_to_form("Purchase Order", item.purchase_order)), + title=_("Limit Crossed")) + transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code) backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code) From 7586c3408e3fa55b4277ce5616709649bc91a88c Mon Sep 17 00:00:00 2001 From: Afshan Date: Fri, 28 Aug 2020 12:56:37 +0530 Subject: [PATCH 099/175] fix: test case --- erpnext/regional/united_states/test_united_states.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/united_states/test_united_states.py b/erpnext/regional/united_states/test_united_states.py index 688f14576c..af16f973bf 100644 --- a/erpnext/regional/united_states/test_united_states.py +++ b/erpnext/regional/united_states/test_united_states.py @@ -24,7 +24,7 @@ class TestUnitedStates(unittest.TestCase): def test_irs_1099_report(self): make_payment_entry_to_irs_1099_supplier() - filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company"}) + filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company 1"}) columns, data = execute_1099_report(filters) print(columns, data) expected_row = {'supplier': '_US 1099 Test Supplier', @@ -42,7 +42,7 @@ def make_payment_entry_to_irs_1099_supplier(): pe = frappe.new_doc("Payment Entry") pe.payment_type = "Pay" - pe.company = "_Test Company" + pe.company = "_Test Company 1" pe.posting_date = "2016-01-10" pe.paid_from = "_Test Bank USD - _TC" pe.paid_to = "_Test Payable USD - _TC" From d659774190c60cb43816cf31a163e692360d5915 Mon Sep 17 00:00:00 2001 From: Afshan Date: Fri, 28 Aug 2020 13:40:48 +0530 Subject: [PATCH 100/175] fix:tests --- erpnext/regional/united_states/test_united_states.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/united_states/test_united_states.py b/erpnext/regional/united_states/test_united_states.py index af16f973bf..ad95010a9a 100644 --- a/erpnext/regional/united_states/test_united_states.py +++ b/erpnext/regional/united_states/test_united_states.py @@ -44,8 +44,8 @@ def make_payment_entry_to_irs_1099_supplier(): pe.payment_type = "Pay" pe.company = "_Test Company 1" pe.posting_date = "2016-01-10" - pe.paid_from = "_Test Bank USD - _TC" - pe.paid_to = "_Test Payable USD - _TC" + pe.paid_from = "_Test Bank USD - _TC1" + pe.paid_to = "_Test Payable USD - _TC1" pe.paid_amount = 100 pe.received_amount = 100 pe.reference_no = "For IRS 1099 testing" From 731bae170e58da232972f621d9436b397c909411 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 28 Aug 2020 14:09:02 +0530 Subject: [PATCH 101/175] fix: Test for Over Receipt via PRs on a PO --- .../purchase_receipt/test_purchase_receipt.py | 68 ++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 4a8236dd11..67161aa6dd 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import unittest +import json import frappe, erpnext import frappe.defaults from frappe.utils import cint, flt, cstr, today, random_string @@ -152,13 +153,78 @@ class TestPurchaseReceipt(unittest.TestCase): qty=100, basic_rate=100, company="_Test Company with perpetual inventory") pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=0, is_subcontracted="Yes", company="_Test Company with perpetual inventory", warehouse='Stores - TCP1', supplier_warehouse='Work In Progress - TCP1') - + gl_entries = get_gl_entries("Purchase Receipt", pr.name) self.assertFalse(gl_entries) set_perpetual_inventory(0) + def test_subcontracting_over_receipt(self): + """ + Behaviour: Raise multiple PRs against one PO that in total + receive more than the required qty in the PO. + Expected Result: Error Raised for Over Receipt against PO. + """ + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + from erpnext.buying.doctype.purchase_order.test_purchase_order import (update_backflush_based_on, + make_subcontracted_item, create_purchase_order) + from erpnext.buying.doctype.purchase_order.purchase_order import (make_purchase_receipt, + make_rm_stock_entry as make_subcontract_transfer_entry) + + update_backflush_based_on("Material Transferred for Subcontract") + item_code = "_Test Subcontracted FG Item 1" + make_subcontracted_item(item_code) + + po = create_purchase_order(item_code=item_code, qty=1, + is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") + + #stock raw materials in a warehouse before transfer + make_stock_entry(target="_Test Warehouse - _TC", + item_code="_Test Item Home Desktop 100", qty=1, basic_rate=100) + make_stock_entry(target="_Test Warehouse - _TC", + item_code = "Test Extra Item 1", qty=1, basic_rate=100) + make_stock_entry(target="_Test Warehouse - _TC", + item_code = "_Test Item", qty=1, basic_rate=100) + + rm_items = [ + { + "item_code": item_code, + "rm_item_code": po.supplied_items[0].rm_item_code, + "item_name": "_Test Item", + "qty": po.supplied_items[0].required_qty, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos" + }, + { + "item_code": item_code, + "rm_item_code": po.supplied_items[1].rm_item_code, + "item_name": "Test Extra Item 1", + "qty": po.supplied_items[1].required_qty, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos" + }, + { + "item_code": item_code, + "rm_item_code": po.supplied_items[2].rm_item_code, + "item_name": "_Test Item Home Desktop 100", + "qty": po.supplied_items[2].required_qty, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos" + } + ] + rm_item_string = json.dumps(rm_items) + se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) + se.to_warehouse = "_Test Warehouse 1 - _TC" + se.save() + se.submit() + + pr1 = make_purchase_receipt(po.name) + pr2 = make_purchase_receipt(po.name) + + pr1.submit() + self.assertRaises(frappe.ValidationError, pr2.submit) + def test_serial_no_supplier(self): pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"), From a787882a8f55333d7f41a4df9c09e01d05b540df Mon Sep 17 00:00:00 2001 From: Frappe ERPnext Date: Fri, 28 Aug 2020 13:28:52 +0200 Subject: [PATCH 102/175] feat: Option to print UOM after quantity Standard for ERPNext prints was UOM before quantity. Now user has a choice. --- erpnext/controllers/print_settings.py | 12 +++++++++--- .../includes/item_table_qty_swapped.html | 5 +++++ 2 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 erpnext/templates/print_formats/includes/item_table_qty_swapped.html diff --git a/erpnext/controllers/print_settings.py b/erpnext/controllers/print_settings.py index c41db25253..d24b69162b 100644 --- a/erpnext/controllers/print_settings.py +++ b/erpnext/controllers/print_settings.py @@ -7,9 +7,15 @@ from frappe.utils import cint def print_settings_for_item_table(doc): - doc.print_templates = { - "qty": "templates/print_formats/includes/item_table_qty.html" - } + if frappe.db.get_single_value("Print Settings", "print_uom_after_quantity") != 1: + doc.print_templates = { + "qty": "templates/print_formats/includes/item_table_qty.html" + } + else: + doc.print_templates = { + "qty": "templates/print_formats/includes/item_table_qty_swapped.html" + } + doc.hide_in_print_layout = ["uom", "stock_uom"] doc.flags.compact_item_print = cint(frappe.db.get_single_value("Print Settings", "compact_item_print")) diff --git a/erpnext/templates/print_formats/includes/item_table_qty_swapped.html b/erpnext/templates/print_formats/includes/item_table_qty_swapped.html new file mode 100644 index 0000000000..c6c70f2cf7 --- /dev/null +++ b/erpnext/templates/print_formats/includes/item_table_qty_swapped.html @@ -0,0 +1,5 @@ +{{ doc.get_formatted("qty", doc) }} +{% if (doc.uom and not doc.is_print_hide("uom")) %}{{ _(doc.uom) }} +{% elif (doc.stock_uom and not doc.is_print_hide("stock_uom")) %}{{ _(doc.stock_uom) }} +{%- endif %} + From b23840bf7b668971364ac05cb2425d396b617dd5 Mon Sep 17 00:00:00 2001 From: Syed Mujeer Hashmi Date: Sat, 29 Aug 2020 12:48:48 +0530 Subject: [PATCH 103/175] fix: Filter out cancelled entries in customer ledger summary Signed-off-by: Syed Mujeer Hashmi --- .../report/customer_ledger_summary/customer_ledger_summary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index 2cb10b11e1..10b32fea56 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -173,7 +173,7 @@ class PartyLedgerSummaryReport(object): from `tabGL Entry` gle {join} where - gle.docstatus < 2 and gle.party_type=%(party_type)s and ifnull(gle.party, '') != '' + gle.docstatus < 2 and gle.is_cancelled = 0 and gle.party_type=%(party_type)s and ifnull(gle.party, '') != '' and gle.posting_date <= %(to_date)s {conditions} order by gle.posting_date """.format(join=join, join_field=join_field, conditions=conditions), self.filters, as_dict=True) @@ -248,7 +248,7 @@ class PartyLedgerSummaryReport(object): from `tabGL Entry` where - docstatus < 2 + docstatus < 2 and is_cancelled = 0 and (voucher_type, voucher_no) in ( select voucher_type, voucher_no from `tabGL Entry` gle, `tabAccount` acc where acc.name = gle.account and acc.account_type = '{income_or_expense}' From c71e37c988c79ed126d38ffe59c3b1b74f7718a0 Mon Sep 17 00:00:00 2001 From: michellealva Date: Sun, 30 Aug 2020 19:42:24 +0530 Subject: [PATCH 104/175] feat: Allow Rename for Tax Category --- .../doctype/tax_category/tax_category.json | 90 +++---------------- 1 file changed, 11 insertions(+), 79 deletions(-) diff --git a/erpnext/accounts/doctype/tax_category/tax_category.json b/erpnext/accounts/doctype/tax_category/tax_category.json index 1e3ae455b3..6f682a0466 100644 --- a/erpnext/accounts/doctype/tax_category/tax_category.json +++ b/erpnext/accounts/doctype/tax_category/tax_category.json @@ -1,134 +1,66 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, + "actions": [], + "allow_rename": 1, "autoname": "field:title", - "beta": 0, "creation": "2018-11-22 23:38:39.668804", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "title" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "title", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Title", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, "unique": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2020-01-15 17:14:28.951793", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-08-30 19:41:25.783852", "modified_by": "Administrator", "module": "Accounts", "name": "Tax Category", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Accounts Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Accounts User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 0 + "share": 1 } ], "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} + "track_changes": 1 +} \ No newline at end of file From e268d294b3390daad2a6031db22bdc264a3fda53 Mon Sep 17 00:00:00 2001 From: marination Date: Sun, 30 Aug 2020 21:01:34 +0530 Subject: [PATCH 105/175] fix: Better error feedback on creating SO from Quotation --- erpnext/selling/doctype/quotation/quotation.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index ab095ebfe0..20ae19f5db 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -285,9 +285,17 @@ def _make_customer(source_name, ignore_permissions=False): return customer else: raise - except frappe.MandatoryError: + except frappe.MandatoryError as e: + mandatory_fields = e.args[0].split(':')[1].split(',') + mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields] + frappe.local.message_log = [] - frappe.throw(_("Please create Customer from Lead {0}").format(lead_name)) + lead_link = frappe.utils.get_link_to_form("Lead", lead_name) + message = _("Could not auto create Customer due to the following missing mandatory field(s):") + "
    " + message += "
    • " + "
    • ".join(mandatory_fields) + "
    " + message += _("Please create Customer from Lead {0}.").format(lead_link) + + frappe.throw(message, title=_("Mandatory Missing")) else: return customer_name else: From a4259208e7482df727d8efc1beace9eb4906467c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 30 Aug 2020 23:09:23 +0530 Subject: [PATCH 106/175] feat: Utility function to get possible loan disbursal amount --- .../loan_management/doctype/loan/test_loan.py | 51 ++++++++++++++++++ .../loan_application/loan_application.js | 10 ++-- .../loan_disbursement/loan_disbursement.py | 53 +++++++++++-------- .../loan_interest_accrual.py | 14 +++-- .../doctype/loan_repayment/loan_repayment.py | 7 ++- .../loan_security_shortfall.py | 12 +++-- 6 files changed, 111 insertions(+), 36 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 23815d5982..b75f7bdd75 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -17,6 +17,7 @@ from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loa from erpnext.loan_management.doctype.loan.loan import create_loan_security_unpledge from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge +from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount class TestLoan(unittest.TestCase): def setUp(self): @@ -323,6 +324,56 @@ class TestLoan(unittest.TestCase): self.assertEqual(loan.status, 'Closed') self.assertEquals(sum(pledged_qty.values()), 0) + def test_disbursal_check_with_shortfall(self): + pledges = [{ + "loan_security": "Test Security 2", + "qty": 8000.00, + "haircut": 50, + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, + 'Stock Loan', pledges, "Repay Over Number of Periods", 12) + + create_pledge(loan_application) + + loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application) + loan.submit() + + #Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge + make_loan_disbursement_entry(loan.name, 700000) + + frappe.db.sql("""UPDATE `tabLoan Security Price` SET loan_security_price = 100 + where loan_security='Test Security 2'""") + + create_process_loan_security_shortfall() + loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name}) + self.assertTrue(loan_security_shortfall) + + self.assertEqual(get_disbursal_amount(loan.name), 0) + + frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 250 + where loan_security='Test Security 2'""") + + def test_disbursal_check_without_shortfall(self): + pledges = [{ + "loan_security": "Test Security 2", + "qty": 8000.00, + "haircut": 50, + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, + 'Stock Loan', pledges, "Repay Over Number of Periods", 12) + + create_pledge(loan_application) + + loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application) + loan.submit() + + #Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge + make_loan_disbursement_entry(loan.name, 700000) + + self.assertEqual(get_disbursal_amount(loan.name), 300000) + def create_loan_accounts(): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.js b/erpnext/loan_management/doctype/loan_application/loan_application.js index b56fce1d7c..1365274971 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.js +++ b/erpnext/loan_management/doctype/loan_application/loan_application.js @@ -33,18 +33,18 @@ frappe.ui.form.on('Loan Application', { if (frm.doc.is_secured_loan) { frappe.db.get_value("Loan Security Pledge", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { - if (!r) { + if (Object.keys(r).length === 0) { frm.add_custom_button(__('Loan Security Pledge'), function() { - frm.trigger('create_loan_security_pledge') + frm.trigger('create_loan_security_pledge'); },__('Create')) } }); } frappe.db.get_value("Loan", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { - if (!r) { + if (Object.keys(r).length === 0) { frm.add_custom_button(__('Loan'), function() { - frm.trigger('create_loan') + frm.trigger('create_loan'); },__('Create')) } else { frm.set_df_property('status', 'read_only', 1); @@ -54,7 +54,7 @@ frappe.ui.form.on('Loan Application', { }, create_loan: function(frm) { if (frm.doc.status != "Approved") { - frappe.throw(__("Cannot create loan until application is approved")) + frappe.throw(__("Cannot create loan until application is approved")); } frappe.model.open_mapped_doc({ diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index 6c27e12134..260fada893 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -67,28 +67,10 @@ class LoanDisbursement(AccountsController): disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount total_payment = loan_details.total_payment - if disbursed_amount > loan_details.loan_amount and loan_details.is_term_loan: - frappe.throw(_("Disbursed Amount cannot be greater than loan amount")) + possible_disbursal_amount = get_disbursal_amount(self.against_loan) - if loan_details.status == 'Disbursed': - pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \ - - flt(loan_details.total_principal_paid) - else: - pending_principal_amount = loan_details.disbursed_amount - - security_value = 0.0 - if loan_details.is_secured_loan: - security_value = get_total_pledged_security_value(self.against_loan) - - if not security_value: - security_value = loan_details.loan_amount - - if pending_principal_amount + self.disbursed_amount > flt(security_value): - allowed_amount = security_value - pending_principal_amount - if allowed_amount < 0: - allowed_amount = 0 - - frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(allowed_amount)) + if self.disbursed_amount > possible_disbursal_amount: + frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount)) if loan_details.status == "Disbursed" and not loan_details.is_term_loan: process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1), @@ -176,3 +158,32 @@ def get_total_pledged_security_value(loan): security_value += (loan_security_price_map.get(security) * qty * hair_cut_map.get(security))/100 return security_value + +@frappe.whitelist() +def get_disbursal_amount(loan): + loan_details = frappe.get_all("Loan", fields = ["loan_amount", "disbursed_amount", "total_payment", + "total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan"], + filters= { "name": loan })[0] + + if loan_details.is_secured_loan and frappe.get_all('Loan Security Shortfall', filters={'loan': loan, + 'status': 'Pending'}): + return 0 + + if loan_details.status == 'Disbursed': + pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \ + - flt(loan_details.total_principal_paid) + else: + pending_principal_amount = flt(loan_details.disbursed_amount) + + security_value = 0.0 + if loan_details.is_secured_loan: + security_value = get_total_pledged_security_value(loan) + + if not security_value and not loan_details.is_secured_loan: + security_value = flt(loan_details.loan_amount) + + disbursal_amount = flt(security_value) - flt(pending_principal_amount) + + return disbursal_amount + + diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index c5111fdc93..1d3fa71068 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -85,8 +85,11 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i if no_of_days <= 0: return - pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ - - flt(loan.total_principal_paid) + if loan.status == 'Disbursed': + pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ + - flt(loan.total_principal_paid) + else: + pending_principal_amount = loan.disbursed_amount interest_per_day = (pending_principal_amount * loan.rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100) payable_interest = interest_per_day * no_of_days @@ -107,7 +110,7 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None): query_filters = { - "status": "Disbursed", + "status": ('in', ['Disbursed', 'Partially Disbursed']), "docstatus": 1 } @@ -118,8 +121,9 @@ def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_inte if not open_loans: open_loans = frappe.get_all("Loan", - fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", "is_term_loan", - "disbursement_date", "applicant_type", "applicant", "rate_of_interest", "total_interest_payable", "repayment_start_date"], + fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", + "is_term_loan", "status", "disbursement_date", "disbursed_amount", "applicant_type", "applicant", + "rate_of_interest", "total_interest_payable", "total_principal_paid", "repayment_start_date"], filters=query_filters) for loan in open_loans: diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 9605045777..451ae85afb 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -281,7 +281,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): due_date = add_days(entry.posting_date, 1) no_of_late_days = date_diff(posting_date, - add_days(due_date, loan_type_details.grace_period_in_days)) + add_days(due_date, loan_type_details.grace_period_in_days)) if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary): penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)/365 @@ -297,7 +297,10 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): if not final_due_date: final_due_date = add_days(due_date, loan_type_details.grace_period_in_days) - pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable + if against_loan_doc.status == 'Disbursed': + pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable + else: + pending_principal_amount = against_loan_doc.disbursed_amount if payment_type == "Loan Closure": if due_date: 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 02efe240bd..c3ea882809 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 @@ -51,13 +51,19 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): "valid_upto": (">=", update_time) }, as_list=1)) - loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid'], - filters={'status': 'Disbursed', 'is_secured_loan': 1}) + loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid', 'total_payment', + 'total_interest_payable', 'disbursed_amount'], + filters={'status': ('in',['Disbursed','Partially Disbursed']), 'is_secured_loan': 1}) loan_security_map = {} for loan in loans: - outstanding_amount = loan.loan_amount - loan.total_principal_paid + if loan.status == 'Disbursed': + outstanding_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ + - flt(loan.total_principal_paid) + else: + outstanding_amount = loan.disbursed_amount + pledged_securities = get_pledged_security_qty(loan.name) ltv_ratio = '' security_value = 0.0 From ce29757bff235030d49bc1caf3d2a17d4fd2b941 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 31 Aug 2020 11:26:51 +0530 Subject: [PATCH 107/175] fix: Import flt --- .../doctype/loan_security_shortfall/loan_security_shortfall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c3ea882809..71e741ccf0 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 @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import get_datetime +from frappe.utils import get_datetime, flt from frappe.model.document import Document from six import iteritems from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty From e8b121c2c2dc4fd00f78f32c8d7a26089c9ecddb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 31 Aug 2020 11:31:48 +0530 Subject: [PATCH 108/175] fix: Add status in field list --- .../doctype/loan_security_shortfall/loan_security_shortfall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 71e741ccf0..0f42bde3c4 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 @@ -52,7 +52,7 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): }, as_list=1)) loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid', 'total_payment', - 'total_interest_payable', 'disbursed_amount'], + 'total_interest_payable', 'disbursed_amount', 'status'], filters={'status': ('in',['Disbursed','Partially Disbursed']), 'is_secured_loan': 1}) loan_security_map = {} From d70e711aea9221b4a0d42f56081eb28d6b059b2e Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 31 Aug 2020 13:14:32 +0530 Subject: [PATCH 109/175] fix: events not deleted on cancelling maintenance schedule (#22954) * feat: add participant to event_participant child table * feat: add tests * chore: update function name Co-authored-by: Marica Co-authored-by: Marica --- .../maintenance_schedule.py | 8 ++-- .../test_maintenance_schedule.py | 38 ++++++++++++++++++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index add7bbfa57..cba6a2d014 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -67,16 +67,16 @@ class MaintenanceSchedule(TransactionBase): for key in scheduled_date: description =frappe._("Reference: {0}, Item Code: {1} and Customer: {2}").format(self.name, d.item_code, self.customer) - frappe.get_doc({ + event = frappe.get_doc({ "doctype": "Event", "owner": email_map.get(d.sales_person, self.owner), "subject": description, "description": description, "starts_on": cstr(key["scheduled_date"]) + " 10:00:00", "event_type": "Private", - "ref_type": self.doctype, - "ref_name": self.name - }).insert(ignore_permissions=1) + }) + event.add_participant(self.doctype, self.name) + event.insert(ignore_permissions=1) frappe.db.set(self, 'status', 'Submitted') diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py index d8ae17b4c7..3c307e920f 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py @@ -2,6 +2,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals +from frappe.utils.data import get_datetime, add_days import frappe import unittest @@ -9,4 +10,39 @@ import unittest # test_records = frappe.get_test_records('Maintenance Schedule') class TestMaintenanceSchedule(unittest.TestCase): - pass + def test_events_should_be_created_and_deleted(self): + ms = make_maintenance_schedule() + ms.generate_schedule() + ms.submit() + + all_events = get_events(ms) + self.assertTrue(len(all_events) > 0) + + ms.cancel() + events_after_cancel = get_events(ms) + self.assertTrue(len(events_after_cancel) == 0) + +def get_events(ms): + return frappe.get_all("Event Participants", filters={ + "reference_doctype": ms.doctype, + "reference_docname": ms.name, + "parenttype": "Event" + }) + +def make_maintenance_schedule(): + ms = frappe.new_doc("Maintenance Schedule") + ms.company = "_Test Company" + ms.customer = "_Test Customer" + ms.transaction_date = get_datetime() + + ms.append("items", { + "item_code": "_Test Item", + "start_date": get_datetime(), + "end_date": add_days(get_datetime(), 32), + "periodicity": "Weekly", + "no_of_visits": 4, + "sales_person": "Sales Team", + }) + ms.insert(ignore_permissions=True) + + return ms From e7ab53a711a822e801f4d9c2c8b1006a5e4c3c9c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 22 Jun 2020 12:36:23 +0530 Subject: [PATCH 110/175] fix: completed qty not updated in work order --- erpnext/manufacturing/doctype/job_card/job_card.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index c29d4ba3d5..8dd90e612b 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -233,7 +233,7 @@ class JobCard(Document): work_order_field = "name" if field == "operation_id" else field for data in wo.operations: - if data.get(work_order_field) == self.get(field) and data.workstation == self.workstation: + if data.get(work_order_field) == self.get(field): data.completed_qty = for_quantity data.actual_operation_time = time_in_mins data.actual_start_time = time_data[0].start_time if time_data else None From a5963e1b2c94cb7f028f49cb2957154c76cff9a4 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 24 Aug 2020 17:55:23 +0530 Subject: [PATCH 111/175] fix: user created manual job card not linking job card operations with work order operations --- .../doctype/job_card/job_card.js | 61 ++++++++++++++++++- .../doctype/job_card/job_card.json | 10 ++- .../doctype/job_card/job_card.py | 34 +++++++++++ .../doctype/job_card/test_job_card.py | 24 +++++++- 4 files changed, 124 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index bab0dfb6b4..7ac0b11ecb 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -2,6 +2,17 @@ // For license information, please see license.txt frappe.ui.form.on('Job Card', { + setup: function(frm) { + frm.set_query('operation', function() { + return { + query: 'erpnext.manufacturing.doctype.job_card.job_card.get_operations', + filters: { + 'work_order': frm.doc.work_order + } + }; + }); + }, + refresh: function(frm) { frappe.flags.pause_job = 0; frappe.flags.resume_job = 0; @@ -20,12 +31,60 @@ frappe.ui.form.on('Job Card', { } } + frm.trigger("toggle_operation_number"); + if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) - && (!frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { + && (!frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { frm.trigger("prepare_timer_buttons"); } }, + operation: function(frm) { + frm.trigger("toggle_operation_number"); + + if (frm.doc.operation && frm.doc.work_order) { + frappe.call({ + method: "erpnext.manufacturing.doctype.job_card.job_card.get_operation_details", + args: { + "work_order":frm.doc.work_order, + "operation":frm.doc.operation + }, + callback: function (r) { + if (r.message) { + if (r.message.length == 1) { + frm.set_value("operation_id", r.message[0].name); + } else { + let args = []; + + r.message.forEach((row) => { + args.push({ "label": row.idx, "value": row.name }); + }); + + let description = __("Operation {0} added multiple times in the work order {1}", + [frm.doc.operation, frm.doc.work_order]); + + frm.set_df_property("operation_row_number", "options", args); + frm.set_df_property("operation_row_number", "description", description); + } + + frm.trigger("toggle_operation_number"); + } + } + }) + } + }, + + operation_row_number(frm) { + if (frm.doc.operation_row_number) { + frm.set_value("operation_id", frm.doc.operation_row_number); + } + }, + + toggle_operation_number(frm) { + frm.toggle_display("operation_row_number", !frm.doc.operation_id && frm.doc.operation); + frm.toggle_reqd("operation_row_number", !frm.doc.operation_id && frm.doc.operation); + }, + prepare_timer_buttons: function(frm) { frm.trigger("make_dashboard"); if (!frm.doc.job_started) { diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index fba670c1c1..087ab6b484 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -11,6 +11,7 @@ "bom_no", "workstation", "operation", + "operation_row_number", "column_break_4", "posting_date", "company", @@ -291,11 +292,15 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "operation_row_number", + "fieldtype": "Select", + "label": "Operation Row Number" } ], "is_submittable": 1, - "links": [], - "modified": "2020-04-20 15:14:00.273441", + "modified": "2020-08-24 15:21:21.398267", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", @@ -347,7 +352,6 @@ "write": 1 } ], - "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", "title_field": "operation", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 8dd90e612b..a87e6e5037 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -15,10 +15,13 @@ from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings class OverlapError(frappe.ValidationError): pass +class OperationMismatchError(frappe.ValidationError): pass + class JobCard(Document): def validate(self): self.validate_time_logs() self.set_status() + self.validate_operation_id() def validate_time_logs(self): self.total_completed_qty = 0.0 @@ -306,6 +309,37 @@ class JobCard(Document): if update_status: self.db_set('status', self.status) + def validate_operation_id(self): + if (self.get("operation_id") and self.get("operation_row_number") and self.operation and self.work_order and + frappe.get_cached_value("Work Order Operation", self.operation_row_number, "name") != self.operation_id): + work_order = frappe.bold(get_link_to_form("Work Order", self.work_order)) + frappe.throw(_("Operation {0} does not belong to the work order {1}") + .format(frappe.bold(self.operation), work_order), OperationMismatchError) + +@frappe.whitelist() +def get_operation_details(work_order, operation): + if work_order and operation: + return frappe.get_all("Work Order Operation", fields = ["name", "idx"], + filters = { + "parent": work_order, + "operation": operation + } + ) + +@frappe.whitelist() +def get_operations(doctype, txt, searchfield, start, page_len, filters): + if filters.get("work_order"): + args = {"parent": filters.get("work_order")} + if txt: + args["operation"] = ("like", "%{0}%".format(txt)) + + return frappe.get_all("Work Order Operation", + filters = args, + fields = ["distinct operation as operation"], + limit_start = start, + limit_page_length = page_len, + order_by="idx asc", as_list=1) + @frappe.whitelist() def make_material_request(source_name, target_doc=None): def update_item(obj, target, source_parent): diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index ca05fea0f6..2a6c35fc04 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -4,6 +4,28 @@ from __future__ import unicode_literals import unittest +import frappe +from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record +from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError class TestJobCard(unittest.TestCase): - pass + def test_job_card(self): + data = frappe.get_cached_value('BOM', + {'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) + + if data: + bom, bom_item = data + + work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom) + + job_cards = frappe.get_all('Job Card', + filters = {'work_order': work_order.name}, fields = ["operation_id", "name"]) + + if job_cards: + job_card = job_cards[0] + frappe.db.set_value("Job Card", job_card.name, "operation_row_number", job_card.operation_id) + + doc = frappe.get_doc("Job Card", job_card.name) + doc.operation_id = "Test Data" + self.assertRaises(OperationMismatchError, doc.save) + From d0aafece32bc779963cb28951cf73617f1d36ef4 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 31 Aug 2020 11:57:57 +0530 Subject: [PATCH 112/175] fix: incorrect completed qty against operation in work order if workstation is different in job card --- .../doctype/job_card/job_card.py | 12 +++---- .../doctype/job_card/test_job_card.py | 36 +++++++++++++++++++ .../doctype/workstation/test_workstation.py | 15 ++++++++ 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index a87e6e5037..8855e0acf5 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -212,11 +212,10 @@ class JobCard(Document): for_quantity, time_in_mins = 0, 0 from_time_list, to_time_list = [], [] - field = "operation_id" if self.operation_id else "operation" + field = "operation_id" data = frappe.get_all('Job Card', fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], - filters = {"docstatus": 1, "work_order": self.work_order, - "workstation": self.workstation, field: self.get(field)}) + filters = {"docstatus": 1, "work_order": self.work_order, field: self.get(field)}) if data and len(data) > 0: for_quantity = data[0].completed_qty @@ -229,14 +228,13 @@ class JobCard(Document): FROM `tabJob Card` jc, `tabJob Card Time Log` jctl WHERE jctl.parent = jc.name and jc.work_order = %s - and jc.workstation = %s and jc.{0} = %s and jc.docstatus = 1 - """.format(field), (self.work_order, self.workstation, self.get(field)), as_dict=1) + and jc.{0} = %s and jc.docstatus = 1 + """.format(field), (self.work_order, self.get(field)), as_dict=1) wo = frappe.get_doc('Work Order', self.work_order) - work_order_field = "name" if field == "operation_id" else field for data in wo.operations: - if data.get(work_order_field) == self.get(field): + if data.get("name") == self.get(field): data.completed_qty = for_quantity data.actual_operation_time = time_in_mins data.actual_start_time = time_data[0].start_time if time_data else None diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 2a6c35fc04..353f6d281a 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -5,6 +5,8 @@ from __future__ import unicode_literals import unittest import frappe +from frappe.utils import random_string +from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError @@ -29,3 +31,37 @@ class TestJobCard(unittest.TestCase): doc.operation_id = "Test Data" self.assertRaises(OperationMismatchError, doc.save) + def test_job_card_with_different_work_station(self): + data = frappe.get_cached_value('BOM', + {'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) + + if data: + bom, bom_item = data + + work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom) + + job_card = frappe.get_all('Job Card', + filters = {'work_order': work_order.name}, + fields = ["operation_id", "workstation", "name", "for_quantity"])[0] + + if job_card: + workstation = frappe.db.get_value("Workstation", + {"name": ("not in", [job_card.workstation])}, "name") + + if not workstation or job_card.workstation == workstation: + workstation = make_workstation(workstation_name=random_string(5)).name + + doc = frappe.get_doc("Job Card", job_card.name) + doc.workstation = workstation + doc.append("time_logs", { + "from_time": "2009-01-01 12:06:25", + "to_time": "2009-01-01 12:37:25", + "time_in_mins": "31.00002", + "completed_qty": job_card.for_quantity + }) + doc.submit() + + completed_qty = frappe.db.get_value("Work Order Operation", job_card.operation_id, "completed_qty") + self.assertEqual(completed_qty, job_card.for_quantity) + + doc.cancel() \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index 2169260854..8266cf7b77 100644 --- a/erpnext/manufacturing/doctype/workstation/test_workstation.py +++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py @@ -20,3 +20,18 @@ class TestWorkstation(unittest.TestCase): "_Test Workstation 1", "Operation 1", "2013-02-02 05:00:00", "2013-02-02 20:00:00") self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours, "_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00") + +def make_workstation(**args): + args = frappe._dict(args) + + try: + doc = frappe.get_doc({ + "doctype": "Workstation", + "workstation_name": args.workstation_name + }) + + doc.insert() + + return doc + except frappe.DuplicateEntryError: + return frappe.get_doc("Workstation", args.workstation_name) \ No newline at end of file From f9f26d2fa419aaec5c27b6c51086c979bb78907a Mon Sep 17 00:00:00 2001 From: Anupam K Date: Mon, 31 Aug 2020 14:00:27 +0530 Subject: [PATCH 113/175] fix: contact us button issue --- .../generators/item/item_add_to_cart.html | 31 ++++++++++--------- .../generators/item/item_configure.html | 7 ++--- .../generators/item/item_inquiry.html | 11 +++++++ 3 files changed, 30 insertions(+), 19 deletions(-) create mode 100644 erpnext/templates/generators/item/item_inquiry.html diff --git a/erpnext/templates/generators/item/item_add_to_cart.html b/erpnext/templates/generators/item/item_add_to_cart.html index 2a70d8dbe9..40bc0c749b 100644 --- a/erpnext/templates/generators/item/item_add_to_cart.html +++ b/erpnext/templates/generators/item/item_add_to_cart.html @@ -27,22 +27,25 @@ {% endif %}
    {% endif %} - {% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.in_stock) %}
    - - {{ _("View in Cart") }} - - + {% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.in_stock) %} + + {{ _("View in Cart") }} + + + {% endif %} + {% if cart_settings.show_contact_us_button %} + {% include "templates/generators/item/item_inquiry.html" %} + {% endif %}
    - {% endif %}
    diff --git a/erpnext/templates/generators/item/item_configure.html b/erpnext/templates/generators/item/item_configure.html index b8b0d98bdc..73f9ec99b3 100644 --- a/erpnext/templates/generators/item/item_configure.html +++ b/erpnext/templates/generators/item/item_configure.html @@ -10,14 +10,11 @@ {{ _('Configure') }} {% endif %} - {% if cart_settings.show_contact_us_button | int %} - + {% if cart_settings.show_contact_us_button %} + {% include "templates/generators/item/item_inquiry.html" %} {% endif %}
    {% endif %} diff --git a/erpnext/templates/generators/item/item_inquiry.html b/erpnext/templates/generators/item/item_inquiry.html new file mode 100644 index 0000000000..83653b6821 --- /dev/null +++ b/erpnext/templates/generators/item/item_inquiry.html @@ -0,0 +1,11 @@ +{% if shopping_cart and shopping_cart.cart_settings.enabled %} +{% set cart_settings = shopping_cart.cart_settings %} + {% if cart_settings.show_contact_us_button | int %} + + {% endif %} + +{% endif %} From 25042a22eb407486bb80099b8e8b416aec022f50 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 31 Aug 2020 19:17:29 +0530 Subject: [PATCH 114/175] fix: Pending amount after loan closure request --- .../loan_management/doctype/loan_repayment/loan_repayment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 451ae85afb..d1bb6ccedf 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -297,7 +297,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): if not final_due_date: final_due_date = add_days(due_date, loan_type_details.grace_period_in_days) - if against_loan_doc.status == 'Disbursed': + if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested'): pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable else: pending_principal_amount = against_loan_doc.disbursed_amount From bc6c4e864dfe36835bffc7e0af03a27b32d0eb38 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 31 Aug 2020 20:32:17 +0530 Subject: [PATCH 115/175] fix: Status in Report and filter query --- .../quoted_item_comparison.js | 15 +-------------- .../quoted_item_comparison.py | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js index ad390c446b..518d665e7e 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js @@ -65,7 +65,7 @@ frappe.query_reports["Quoted Item Comparison"] = { fieldname: "supplier_quotation", default: "", get_data: function(txt) { - return frappe.db.get_link_options('Supplier Quotation', txt); + return frappe.db.get_link_options('Supplier Quotation', txt, {'docstatus': ["<", 2]}); } }, { @@ -110,19 +110,6 @@ frappe.query_reports["Quoted Item Comparison"] = { reporter.make_default_supplier_dialog(report); }, 'Tools'); - const status_message = ` - - Valid till :    - - - Expires in a week or less - -      - - Expires today / Already Expired - `; - report.$status.html(status_message).show(); - }, make_default_supplier_dialog: (report) => { // Get the name of the item to change diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py index ffa138f1e9..4426560c16 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py @@ -17,8 +17,9 @@ def execute(filters=None): columns = get_columns() data, chart_data = prepare_data(supplier_quotation_data, filters) + message = get_message() - return columns, data, None, chart_data + return columns, data, message, chart_data def get_conditions(filters): conditions = "" @@ -207,4 +208,16 @@ def get_columns(): } ] - return columns \ No newline at end of file + return columns + +def get_message(): + return """ + Valid till :    + + + Expires in a week or less + +    + + Expires today / Already Expired + """ \ No newline at end of file From 1dc9a9b669cb502e905318952bda9d8aa2234b22 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Mon, 31 Aug 2020 20:35:05 +0530 Subject: [PATCH 116/175] fix: reverse journal entry for multi-currency (#23165) * fix: reverse journal entry for multi-currency * fix: test case for reverse journal entry --- .../doctype/journal_entry/journal_entry.js | 20 +++------ .../doctype/journal_entry/journal_entry.py | 31 +++++++++++++ .../journal_entry/test_journal_entry.py | 43 +++++++++++++++++++ 3 files changed, 80 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index a09face791..409c15f75c 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -638,20 +638,12 @@ $.extend(erpnext.journal_entry, { return { filters: filters }; }, - reverse_journal_entry: function(frm) { - var me = frm.doc; - for(var i=0; i Date: Mon, 31 Aug 2020 21:21:05 +0530 Subject: [PATCH 117/175] fix: Add unit test for pending principal amount --- .../loan_management/doctype/loan/test_loan.py | 53 +++++++++++++++---- .../doctype/loan_repayment/loan_repayment.py | 8 +++ .../loan_security_unpledge.py | 8 +-- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index b75f7bdd75..2f6cd25a36 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -18,6 +18,7 @@ from erpnext.loan_management.doctype.loan.loan import create_loan_security_unple from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount +from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts class TestLoan(unittest.TestCase): def setUp(self): @@ -194,18 +195,14 @@ class TestLoan(unittest.TestCase): make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', 'paid_principal_amount']) - unaccrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 6) \ - / (days_in_year(get_datetime(first_date).year) * 100) - - self.assertEquals(flt(amounts[0] + unaccrued_interest_amount, 3), - flt(accrued_interest_amount, 3)) + self.assertEquals(flt(amounts[0], 2),flt(accrued_interest_amount, 2)) self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) loan.load_from_db() @@ -307,9 +304,6 @@ class TestLoan(unittest.TestCase): "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() - amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', - 'paid_principal_amount']) - loan.load_from_db() self.assertEquals(loan.status, "Loan Closure Requested") @@ -374,6 +368,47 @@ class TestLoan(unittest.TestCase): self.assertEqual(get_disbursal_amount(loan.name), 300000) + def test_pending_loan_amount_after_closure_request(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + + no_of_days += 6 + + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime(first_date).year) * 100) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + + amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry.submit() + + amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', + 'paid_principal_amount']) + + loan.load_from_db() + self.assertEquals(loan.status, "Loan Closure Requested") + + amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") + self.assertEquals(amounts['pending_principal_amount'], 0.0) def create_loan_accounts(): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index d1bb6ccedf..7d83e32213 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -116,6 +116,7 @@ class LoanRepayment(AccountsController): def allocate_amounts(self, paid_entries): self.set('repayment_details', []) self.principal_amount_paid = 0 + total_interest_paid = 0 interest_paid = self.amount_paid - self.penalty_amount if self.amount_paid - self.penalty_amount > 0 and paid_entries: @@ -137,12 +138,19 @@ class LoanRepayment(AccountsController): interest_paid = 0 paid_principal=0 + total_interest_paid += interest_amount self.append('repayment_details', { 'loan_interest_accrual': lia, 'paid_interest_amount': interest_amount, 'paid_principal_amount': paid_principal }) + if self.payment_type == 'Loan Closure' and total_interest_paid < self.interest_payable: + unaccrued_interest = self.interest_payable - total_interest_paid + interest_paid -= unaccrued_interest + if self.repayment_details: + self.repayment_details[-1].paid_interest_amount += unaccrued_interest + if interest_paid: self.principal_amount_paid += interest_paid diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index 5e9d82aa91..f6b28dae75 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -43,8 +43,10 @@ class LoanSecurityUnpledge(Document): "valid_upto": (">=", get_datetime()) }, as_list=1)) - loan_amount, principal_paid = frappe.get_value("Loan", self.loan, ['loan_amount', 'total_principal_paid']) - pending_principal_amount = loan_amount - principal_paid + total_payment, principal_paid, interest_payable = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid', + 'total_interest_payable']) + + pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) security_value = 0 for security in self.securities: @@ -60,7 +62,7 @@ class LoanSecurityUnpledge(Document): security_value += qty_after_unpledge * loan_security_price_map.get(security.loan_security) - if not security_value and pending_principal_amount > 0: + if not security_value and flt(pending_principal_amount, 2) > 0: frappe.throw("Cannot Unpledge, loan to value ratio is breaching") if security_value and (pending_principal_amount/security_value) * 100 > ltv_ratio: From eb4b2aa14252161433bb3f3f5e2be45476f0af8b Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 1 Sep 2020 15:01:32 +0530 Subject: [PATCH 118/175] fix: get_items from product bundle for purchase order (#22821) * fix: get_items from product bundle for purchase order * fix: Don't overwrite doctype while setting attributes in child row Co-authored-by: Marica --- erpnext/public/js/controllers/buying.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index a4cc68b3e2..cb76c87b62 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -503,11 +503,11 @@ erpnext.buying.get_items_from_product_bundle = function(frm) { if(!r.exc && r.message) { remove_empty_first_row(frm); - for ( var i=0; i< r.message.length; i++ ) { + for (var i=0; i< r.message.length; i++) { var d = frm.add_child("items"); var item = r.message[i]; - for ( var key in item) { - if ( !is_null(item[key]) ) { + for (var key in item) { + if (!is_null(item[key]) && key !== "doctype") { d[key] = item[key]; } } From cebf8034552821a4c54104ac3170b54ffc7a8504 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 1 Sep 2020 16:15:28 +0530 Subject: [PATCH 119/175] fix: Update tax template on supplier address change --- erpnext/regional/india/taxes.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js index fbccc6b078..3b6a28f52c 100644 --- a/erpnext/regional/india/taxes.js +++ b/erpnext/regional/india/taxes.js @@ -6,6 +6,9 @@ erpnext.setup_auto_gst_taxation = (doctype) => { shipping_address: function(frm) { frm.trigger('get_tax_template'); }, + supplier_address: function(frm) { + frm.trigger('get_tax_template'); + }, tax_category: function(frm) { frm.trigger('get_tax_template'); }, From 86a190652e4643a59862b9663f7dfdd10343d1ef Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 1 Sep 2020 14:02:34 +0530 Subject: [PATCH 120/175] fix: incorrect job card timer issue --- erpnext/manufacturing/doctype/job_card/job_card.js | 12 ++++++------ .../doctype/job_card/test_job_card.py | 14 +++++++++++--- .../doctype/work_order/test_work_order.py | 2 +- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 7ac0b11ecb..b051b3243f 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -34,7 +34,7 @@ frappe.ui.form.on('Job Card', { frm.trigger("toggle_operation_number"); if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) - && (!frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { + && (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { frm.trigger("prepare_timer_buttons"); } }, @@ -94,9 +94,9 @@ frappe.ui.form.on('Job Card', { fieldname: 'employee'}, d => { if (d.employee) { frm.set_value("employee", d.employee); + } else { + frm.events.start_job(frm); } - - frm.events.start_job(frm); }, __("Enter Value"), __("Start")); } else { frm.events.start_job(frm); @@ -141,9 +141,7 @@ frappe.ui.form.on('Job Card', { frm.set_value('current_time' , 0); } - frm.save("Save", () => {}, "", () => { - frm.doc.time_logs.pop(-1); - }); + frm.save(); }, complete_job: function(frm, completed_time, completed_qty) { @@ -175,6 +173,8 @@ frappe.ui.form.on('Job Card', { employee: function(frm) { if (frm.doc.job_started && !frm.doc.current_time) { frm.trigger("reset_timer"); + } else { + frm.events.start_job(frm); } }, diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 353f6d281a..b6a6c33d37 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -31,6 +31,9 @@ class TestJobCard(unittest.TestCase): doc.operation_id = "Test Data" self.assertRaises(OperationMismatchError, doc.save) + for d in job_cards: + frappe.delete_doc("Job Card", d.name) + def test_job_card_with_different_work_station(self): data = frappe.get_cached_value('BOM', {'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) @@ -40,9 +43,11 @@ class TestJobCard(unittest.TestCase): work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom) - job_card = frappe.get_all('Job Card', + job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}, - fields = ["operation_id", "workstation", "name", "for_quantity"])[0] + fields = ["operation_id", "workstation", "name", "for_quantity"]) + + job_card = job_cards[0] if job_card: workstation = frappe.db.get_value("Workstation", @@ -64,4 +69,7 @@ class TestJobCard(unittest.TestCase): completed_qty = frappe.db.get_value("Work Order Operation", job_card.operation_id, "completed_qty") self.assertEqual(completed_qty, job_card.for_quantity) - doc.cancel() \ No newline at end of file + doc.cancel() + + for d in job_cards: + frappe.delete_doc("Job Card", d.name) \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 2260befb3f..b7c7c32869 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -7,7 +7,7 @@ import unittest import frappe from frappe.utils import flt, time_diff_in_hours, now, add_months, cint, today from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory -from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry, +from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry, ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError) from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.utils import get_bin From d2881ba4dde0eb3157b52ef905517996de17191f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 2 Sep 2020 11:52:59 +0530 Subject: [PATCH 121/175] fix: set conversion factor while creating RFQ from Opportunity --- erpnext/crm/doctype/opportunity/opportunity.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 6096053136..ad12941fda 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -267,6 +267,12 @@ def make_quotation(source_name, target_doc=None): @frappe.whitelist() def make_request_for_quotation(source_name, target_doc=None): + def set_missing_values(source, target): + rfq = frappe.get_doc(target) + for item in rfq.items: + # opportunity item is not multi-uom + item.conversion_factor = 1.0 + doclist = get_mapped_doc("Opportunity", source_name, { "Opportunity": { "doctype": "Request for Quotation" @@ -279,7 +285,7 @@ def make_request_for_quotation(source_name, target_doc=None): ["uom", "uom"] ] } - }, target_doc) + }, target_doc, set_missing_values) return doclist From a1b5d570768d5b71c4a199a02ea7038c7ac409de Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 2 Sep 2020 13:14:01 +0530 Subject: [PATCH 122/175] test: make rfq from opportunity --- .../test_request_for_quotation.py | 19 +++++++++++++++++++ .../doctype/opportunity/test_opportunity.py | 3 ++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index 3de9526c4f..019cefc0bd 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -11,6 +11,8 @@ from erpnext.stock.doctype.item.test_item import make_item from erpnext.templates.pages.rfq import check_supplier_has_docname_access from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation from erpnext.buying.doctype.request_for_quotation.request_for_quotation import create_supplier_quotation +from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity +from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq class TestRequestforQuotation(unittest.TestCase): def test_quote_status(self): @@ -110,6 +112,23 @@ class TestRequestforQuotation(unittest.TestCase): self.assertEqual(supplier_quotation.items[0].qty, 5) self.assertEqual(supplier_quotation.items[0].stock_qty, 10) + def test_make_rfq_from_opportunity(self): + opportunity = make_opportunity(with_items=1) + supplier_data = get_supplier_data() + rfq = make_rfq(opportunity.name) + + self.assertEqual(len(rfq.get("items")), len(opportunity.get("items"))) + rfq.message_for_supplier = 'Please supply the specified items at the best possible rates.' + + for item in rfq.items: + item.warehouse = "_Test Warehouse - _TC" + + for data in supplier_data: + rfq.append('suppliers', data) + + rfq.status = 'Draft' + rfq.submit() + def make_request_for_quotation(**args): """ :param supplier_data: List containing supplier data diff --git a/erpnext/crm/doctype/opportunity/test_opportunity.py b/erpnext/crm/doctype/opportunity/test_opportunity.py index 33d90076c4..04cd8a26ca 100644 --- a/erpnext/crm/doctype/opportunity/test_opportunity.py +++ b/erpnext/crm/doctype/opportunity/test_opportunity.py @@ -82,7 +82,8 @@ def make_opportunity(**args): if args.with_items: opp_doc.append('items', { "item_code": args.item_code or "_Test Item", - "qty": args.qty or 1 + "qty": args.qty or 1, + "uom": "_Test UOM" }) opp_doc.insert() From c3984691b39cd1209a323f5e83e8f50576b1b4f8 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Wed, 2 Sep 2020 20:01:14 +0530 Subject: [PATCH 123/175] fix: Sales funnel data is inconsistent (#23110) * fix: Sales funnel data is inconsistent * fix: data inconsistency * fix: Converted Count Co-authored-by: Marica --- .../selling/page/sales_funnel/sales_funnel.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.py b/erpnext/selling/page/sales_funnel/sales_funnel.py index dba24ef5b0..b613718c7e 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.py +++ b/erpnext/selling/page/sales_funnel/sales_funnel.py @@ -20,29 +20,28 @@ def get_funnel_data(from_date, to_date, company): validate_filters(from_date, to_date, company) active_leads = frappe.db.sql("""select count(*) from `tabLead` - where (date(`modified`) between %s and %s) - and status != "Do Not Contact" and company=%s""", (from_date, to_date, company))[0][0] - - active_leads += frappe.db.sql("""select count(distinct contact.name) from `tabContact` contact - left join `tabDynamic Link` dl on (dl.parent=contact.name) where dl.link_doctype='Customer' - and (date(contact.modified) between %s and %s) and status != "Passive" """, (from_date, to_date))[0][0] + where (date(`creation`) between %s and %s) + and company=%s""", (from_date, to_date, company))[0][0] opportunities = frappe.db.sql("""select count(*) from `tabOpportunity` where (date(`creation`) between %s and %s) - and status != "Lost" and company=%s""", (from_date, to_date, company))[0][0] + and opportunity_from='Lead' and company=%s""", (from_date, to_date, company))[0][0] quotations = frappe.db.sql("""select count(*) from `tabQuotation` where docstatus = 1 and (date(`creation`) between %s and %s) - and status != "Lost" and company=%s""", (from_date, to_date, company))[0][0] + and (opportunity!="" or quotation_to="Lead") and company=%s""", (from_date, to_date, company))[0][0] + + converted = frappe.db.sql("""select count(*) from `tabCustomer` + JOIN `tabLead` ON `tabLead`.name = `tabCustomer`.lead_name + WHERE (date(`tabCustomer`.creation) between %s and %s) + and `tabLead`.company=%s""", (from_date, to_date, company))[0][0] - sales_orders = frappe.db.sql("""select count(*) from `tabSales Order` - where docstatus = 1 and (date(`creation`) between %s and %s) and company=%s""", (from_date, to_date, company))[0][0] return [ - { "title": _("Active Leads / Customers"), "value": active_leads, "color": "#B03B46" }, + { "title": _("Active Leads"), "value": active_leads, "color": "#B03B46" }, { "title": _("Opportunities"), "value": opportunities, "color": "#F09C00" }, { "title": _("Quotations"), "value": quotations, "color": "#006685" }, - { "title": _("Sales Orders"), "value": sales_orders, "color": "#00AD65" } + { "title": _("Converted"), "value": converted, "color": "#00AD65" } ] @frappe.whitelist() From b872035fd9bbe362847c12604a7d8021ce82491d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2020 17:22:19 +0000 Subject: [PATCH 124/175] chore(deps): bump bl from 3.0.0 to 3.0.1 Bumps [bl](https://github.com/rvagg/bl) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/rvagg/bl/releases) - [Commits](https://github.com/rvagg/bl/compare/v3.0.0...v3.0.1) Signed-off-by: dependabot[bot] --- yarn.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/yarn.lock b/yarn.lock index b19f566fd0..97a063597d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -282,9 +282,9 @@ balanced-match@^1.0.0: integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= bl@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88" - integrity sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A== + version "3.0.1" + resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.1.tgz#1cbb439299609e419b5a74d7fce2f8b37d8e5c6f" + integrity sha512-jrCW5ZhfQ/Vt07WX1Ngs+yn9BDqPL/gw28S7s9H6QK/gupnizNzJAss5akW20ISgOrbLTlXOOCTJeNUQqruAWQ== dependencies: readable-stream "^3.0.1" @@ -866,12 +866,12 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -inherits@2.0.4, inherits@~2.0.1: +inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.1: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1447,9 +1447,9 @@ readable-stream@2, readable-stream@~2.3.6: util-deprecate "~1.0.1" readable-stream@^3.0.1, readable-stream@^3.1.1: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.5.0.tgz#465d70e6d1087f6162d079cd0b5db7fbebfd1606" - integrity sha512-gSz026xs2LfxBPudDuI41V1lka8cxg64E66SGe78zJlsUofOg/yqwezdIcdfwik6B4h8LFmWPA9ef9X3FiNFLA== + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" @@ -1505,9 +1505,9 @@ safe-buffer@^5.0.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== safe-buffer@~5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" - integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== "safer-buffer@>= 2.1.2 < 3": version "2.1.2" From f28bef868db05c1482f6534ef9950a90caf7fc51 Mon Sep 17 00:00:00 2001 From: Frappe ERPnext Date: Wed, 2 Sep 2020 21:42:52 +0200 Subject: [PATCH 125/175] feat: Checkbox for activation of printing UOM after quantity Checkbox will be added to Frappe after installation of ERPNext. (Print Settings docType is part of frappe app, not erpnext app) --- erpnext/setup/install.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 4f0f5721c2..2225fe169f 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -20,6 +20,7 @@ def after_install(): frappe.get_doc({'doctype': "Role", "role_name": "Analytics"}).insert() set_single_defaults() create_compact_item_print_custom_field() + create_print_uom_after_qty_custom_field() create_print_zero_amount_taxes_custom_field() add_all_roles_to("Administrator") create_default_cash_flow_mapper_templates() @@ -66,6 +67,16 @@ def create_compact_item_print_custom_field(): }) +def create_print_uom_after_qty_custom_field(): + create_custom_field('Print Settings', { + 'label': _('Print UOM after Quantity'), + 'fieldname': 'print_uom_after_quantity', + 'fieldtype': 'Check', + 'default': 0, + 'insert_after': 'compact_item_print' + }) + + def create_print_zero_amount_taxes_custom_field(): create_custom_field('Print Settings', { 'label': _('Print taxes with zero amount'), From 0857e6d6699570443afaf52ff55ed0ae53b60b21 Mon Sep 17 00:00:00 2001 From: michellealva Date: Thu, 3 Sep 2020 10:37:43 +0530 Subject: [PATCH 126/175] fix: Add "Bank Clearance" and "Bank Reconciliation" in desk --- erpnext/accounts/desk_page/accounting/accounting.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json index a2497838ee..2c5231491c 100644 --- a/erpnext/accounts/desk_page/accounting/accounting.json +++ b/erpnext/accounts/desk_page/accounting/accounting.json @@ -43,7 +43,7 @@ { "hidden": 0, "label": "Bank Statement", - "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"label\": \"Bank Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -98,7 +98,7 @@ "idx": 0, "is_standard": 1, "label": "Accounting", - "modified": "2020-06-19 12:42:44.054598", + "modified": "2020-09-03 10:37:07.865801", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", @@ -158,4 +158,4 @@ "type": "Dashboard" } ] -} +} \ No newline at end of file From 979ad180fa640536f8c8c831517aecb7ba2a5e2f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 3 Sep 2020 11:03:22 +0530 Subject: [PATCH 127/175] fix: update rfq item in postprocess --- erpnext/crm/doctype/opportunity/opportunity.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index ad12941fda..47b05f306b 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -267,11 +267,8 @@ def make_quotation(source_name, target_doc=None): @frappe.whitelist() def make_request_for_quotation(source_name, target_doc=None): - def set_missing_values(source, target): - rfq = frappe.get_doc(target) - for item in rfq.items: - # opportunity item is not multi-uom - item.conversion_factor = 1.0 + def update_item(obj, target, source_parent): + target.conversion_factor = 1.0 doclist = get_mapped_doc("Opportunity", source_name, { "Opportunity": { @@ -283,9 +280,10 @@ def make_request_for_quotation(source_name, target_doc=None): ["name", "opportunity_item"], ["parent", "opportunity"], ["uom", "uom"] - ] + ], + "postprocess": update_item } - }, target_doc, set_missing_values) + }, target_doc) return doclist From 54cd194b4127dfc7b767c96ddd108a5466f9f176 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 3 Sep 2020 13:51:26 +0530 Subject: [PATCH 128/175] feat: removed roles for some reginal report and added via setup --- erpnext/patches.txt | 1 + ..._custom_roles_for_some_regional_reports.py | 10 +++++ erpnext/regional/india/setup.py | 13 ++++++ erpnext/regional/report/gstr_1/gstr_1.json | 14 +----- erpnext/regional/report/gstr_2/gstr_2.json | 44 +++++++------------ .../hsn_wise_summary_of_outward_supplies.json | 42 +++++++----------- 6 files changed, 59 insertions(+), 65 deletions(-) create mode 100644 erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 4b9c566f04..771babef6a 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -724,3 +724,4 @@ erpnext.patches.v12_0.update_state_code_for_daman_and_diu erpnext.patches.v12_0.rename_lost_reason_detail erpnext.patches.v13_0.drop_razorpay_payload_column erpnext.patches.v13_0.update_start_end_date_for_old_shift_assignment +erpnext.patches.v13_0.setting_custom_roles_for_some_regional_reports diff --git a/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py b/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py new file mode 100644 index 0000000000..ecc7822e1d --- /dev/null +++ b/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +import frappe +from erpnext.regional.india.setup import add_custom_roles_for_reports + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + add_custom_roles_for_reports() \ No newline at end of file diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 290694a789..cbcd6e3203 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -73,6 +73,19 @@ def add_custom_roles_for_reports(): ] )).insert() + for report_name in ('HSN-wise-summary of outward supplies', 'GSTR-1', 'GSTR-2'): + + if not frappe.db.get_value('Custom Role', dict(report=report_name)): + frappe.get_doc(dict( + doctype='Custom Role', + report=report_name, + roles= [ + dict(role='Accounts User'), + dict(role='Accounts Manager'), + dict(role='Auditor') + ] + )).insert() + def add_permissions(): for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'): add_permission(doctype, 'All', 0) diff --git a/erpnext/regional/report/gstr_1/gstr_1.json b/erpnext/regional/report/gstr_1/gstr_1.json index 2012bb8840..75aed8cffc 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.json +++ b/erpnext/regional/report/gstr_1/gstr_1.json @@ -7,7 +7,7 @@ "doctype": "Report", "idx": 0, "is_standard": "Yes", - "modified": "2019-06-30 19:33:59.769385", + "modified": "2019-09-03 19:33:59.769385", "modified_by": "Administrator", "module": "Regional", "name": "GSTR-1", @@ -16,15 +16,5 @@ "ref_doctype": "GL Entry", "report_name": "GSTR-1", "report_type": "Script Report", - "roles": [ - { - "role": "Accounts User" - }, - { - "role": "Accounts Manager" - }, - { - "role": "Auditor" - } - ] + "roles": [] } \ No newline at end of file diff --git a/erpnext/regional/report/gstr_2/gstr_2.json b/erpnext/regional/report/gstr_2/gstr_2.json index 929ed914d8..b70d0f9416 100644 --- a/erpnext/regional/report/gstr_2/gstr_2.json +++ b/erpnext/regional/report/gstr_2/gstr_2.json @@ -1,29 +1,19 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2018-01-29 12:59:55.650445", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "modified": "2018-01-29 12:59:55.650445", - "modified_by": "Administrator", - "module": "Regional", - "name": "GSTR-2", - "owner": "Administrator", - "ref_doctype": "GL Entry", - "report_name": "GSTR-2", - "report_type": "Script Report", - "roles": [ - { - "role": "Accounts User" - }, - { - "role": "Accounts Manager" - }, - { - "role": "Auditor" - } - ] + "add_total_row": 0, + "apply_user_permissions": 1, + "creation": "2018-01-29 12:59:55.650445", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2018-09-03 12:59:55.650445", + "modified_by": "Administrator", + "module": "Regional", + "name": "GSTR-2", + "owner": "Administrator", + "ref_doctype": "GL Entry", + "report_name": "GSTR-2", + "report_type": "Script Report", + "roles": [] } \ No newline at end of file diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.json b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.json index 124a720134..cc6ad574af 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.json +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.json @@ -1,28 +1,18 @@ { - "add_total_row": 0, - "creation": "2018-04-26 10:49:29.159400", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "modified": "2019-04-26 12:59:38.603649", - "modified_by": "Administrator", - "module": "Regional", - "name": "HSN-wise-summary of outward supplies", - "owner": "Administrator", - "ref_doctype": "Sales Invoice", - "report_name": "HSN-wise-summary of outward supplies", - "report_type": "Script Report", - "roles": [ - { - "role": "Accounts User" - }, - { - "role": "Accounts Manager" - }, - { - "role": "Auditor" - } - ] + "add_total_row": 0, + "creation": "2018-04-26 10:49:29.159400", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2019-09-03 12:59:38.603649", + "modified_by": "Administrator", + "module": "Regional", + "name": "HSN-wise-summary of outward supplies", + "owner": "Administrator", + "ref_doctype": "Sales Invoice", + "report_name": "HSN-wise-summary of outward supplies", + "report_type": "Script Report", + "roles": [] } \ No newline at end of file From 68b0108301cc8ecab255e6f2c3afc3edbf145723 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 10 Jul 2020 14:23:12 +0530 Subject: [PATCH 129/175] fix: incorrect qty after transaction in stock ledger entry --- .../stock_reconciliation/stock_reconciliation.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 43fbc00466..b81f8a086d 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -258,6 +258,7 @@ class StockReconciliation(StockController): sl_entries.append(args) + qty_after_transaction = 0 for serial_no in serial_nos: args = self.get_sle_for_items(row, [serial_no]) @@ -271,11 +272,19 @@ class StockReconciliation(StockController): if previous_sle and row.warehouse != previous_sle.get("warehouse"): # If serial no exists in different warehouse + warehouse = previous_sle.get("warehouse", '') or row.warehouse + + if not qty_after_transaction: + qty_after_transaction = get_stock_balance(row.item_code, + warehouse, self.posting_date, self.posting_time) + + qty_after_transaction -= 1 + new_args = args.copy() new_args.update({ 'actual_qty': -1, - 'qty_after_transaction': cint(previous_sle.get('qty_after_transaction')) - 1, - 'warehouse': previous_sle.get("warehouse", '') or row.warehouse, + 'qty_after_transaction': qty_after_transaction, + 'warehouse': warehouse, 'valuation_rate': previous_sle.get("valuation_rate") }) From 7952f89596c9f05e749494c8b51fcd88a7b0672a Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 3 Sep 2020 15:21:35 +0530 Subject: [PATCH 130/175] feat: Overlap validation additional salary --- .../additional_salary/additional_salary.py | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index ef174bdea2..cf2bff0405 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -5,8 +5,8 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document -from frappe import _ -from frappe.utils import getdate, date_diff +from frappe import _, bold +from frappe.utils import getdate, date_diff, comma_and, formatdate class AdditionalSalary(Document): @@ -22,9 +22,37 @@ class AdditionalSalary(Document): def validate(self): self.validate_dates() + self.validate_recurring_additional_salary_overlap() if self.amount < 0: frappe.throw(_("Amount should not be less than zero.")) + def validate_recurring_additional_salary_overlap(self): + if self.is_recurring: + additional_salaries = frappe.db.sql(""" + SELECT + name + FROM `tabAdditional Salary` + WHERE + employee=%s + AND name <> %s + AND docstatus=1 + AND is_recurring=1 + AND salary_component = %s + AND to_date >= %s + AND from_date <= %s""", + (self.employee, self.name, self.salary_component, self.from_date, self.to_date), as_dict = 1) + + additional_salaries = [salary.name for salary in additional_salaries] + + if len(additional_salaries): + frappe.throw(_("Additional Salary: {0} already exist for Salary Component: {1} for period {2} and {3}").format( + bold(comma_and(additional_salaries)), + bold(self.salary_component), + bold(formatdate(self.from_date)), + bold(formatdate(self.to_date) + ))) + + def validate_dates(self): date_of_joining, relieving_date = frappe.db.get_value("Employee", self.employee, ["date_of_joining", "relieving_date"]) From e5b7cb0c15a3278469c775d5ff4fece8ca5137ef Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 3 Sep 2020 16:59:21 +0530 Subject: [PATCH 131/175] fix: data was not properly maped --- erpnext/hr/doctype/attendance/attendance.py | 3 ++- erpnext/hr/doctype/attendance/attendance_calendar.js | 6 ------ erpnext/hr/doctype/shift_assignment/shift_assignment.py | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index 45b7060610..373b94008e 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -98,7 +98,8 @@ def add_attendance(events, start, end, conditions=None): e = { "name": d.name, "doctype": "Attendance", - "date": d.attendance_date, + "start": d.attendance_date, + "end": d.attendance_date, "title": cstr(d.status), "docstatus": d.docstatus } diff --git a/erpnext/hr/doctype/attendance/attendance_calendar.js b/erpnext/hr/doctype/attendance/attendance_calendar.js index 104f09d69f..4566489696 100644 --- a/erpnext/hr/doctype/attendance/attendance_calendar.js +++ b/erpnext/hr/doctype/attendance/attendance_calendar.js @@ -1,12 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt frappe.views.calendar["Attendance"] = { - field_map: { - "start": "attendance_date", - "end": "attendance_date", - "id": "name", - "docstatus": 1 - }, options: { header: { left: 'prev,next today', diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index f8b73349c1..b1f9fd58e3 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -103,7 +103,7 @@ def add_assignments(events, start, end, conditions=None): "doctype": "Shift Assignment", "start_date": d.start_date, "end_date": d.end_date if d.end_date else nowdate(), - "title": cstr(d.employee_name) + \ + "title": cstr(d.employee_name) + ":"+ \ cstr(d.shift_type), "docstatus": d.docstatus } From 9b681770d06ed09213da30603a9b2524bc2137a4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 3 Sep 2020 18:09:02 +0530 Subject: [PATCH 132/175] fix: Account filter in Process Deferred Accounting doctype --- .../process_deferred_accounting.js | 6 ++++-- .../process_deferred_accounting.json | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.js b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.js index 2800c195ce..1ec6805ae0 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.js +++ b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.js @@ -10,13 +10,15 @@ frappe.ui.form.on('Process Deferred Accounting', { } }; }); + }, - if (frm.doc.company) { + type: function(frm) { + if (frm.doc.company && frm.doc.type) { frm.set_query("account", function() { return { filters: { 'company': frm.doc.company, - 'root_type': 'Liability', + 'root_type': frm.doc.type === 'Income' ? 'Liability' : 'Asset', 'is_group': 0 } }; diff --git a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json index 4daafef3ec..457e98ca54 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json +++ b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json @@ -60,6 +60,7 @@ "reqd": 1 }, { + "depends_on": "eval: doc.type", "fieldname": "account", "fieldtype": "Link", "label": "Account", @@ -73,9 +74,10 @@ "reqd": 1 } ], + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-02-06 18:18:09.852844", + "modified": "2020-09-03 18:07:02.463754", "modified_by": "Administrator", "module": "Accounts", "name": "Process Deferred Accounting", From 2a56000460da2f8c3c12d1da18cd77b6f9a92fdc Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 3 Sep 2020 19:45:04 +0530 Subject: [PATCH 133/175] fix: Supplier Leaderboard fix --- erpnext/startup/leaderboard.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/startup/leaderboard.py b/erpnext/startup/leaderboard.py index 5545f13e8c..ef238f1165 100644 --- a/erpnext/startup/leaderboard.py +++ b/erpnext/startup/leaderboard.py @@ -123,7 +123,8 @@ def get_all_suppliers(date_range, company, field, limit = None): if field == "outstanding_amount": filters = [['docstatus', '=', '1'], ['company', '=', company]] if date_range: - filters.append(['posting_date', 'between' [date_range[0], date_range[1]]]) + date_range = frappe.parse_json(date_range) + filters.append(['posting_date', 'between', [date_range[0], date_range[1]]]) return frappe.db.get_all('Purchase Invoice', fields = ['supplier as name', 'sum(outstanding_amount) as value'], filters = filters, From a5d5e70bf4f256e70064fc710c8597b32077e68f Mon Sep 17 00:00:00 2001 From: Frappe ERPnext Date: Thu, 3 Sep 2020 17:30:25 +0200 Subject: [PATCH 134/175] Revert "feat: Option to print UOM after quantity" This reverts commit a787882a8f55333d7f41a4df9c09e01d05b540df. --- erpnext/controllers/print_settings.py | 12 +++--------- .../includes/item_table_qty_swapped.html | 5 ----- 2 files changed, 3 insertions(+), 14 deletions(-) delete mode 100644 erpnext/templates/print_formats/includes/item_table_qty_swapped.html diff --git a/erpnext/controllers/print_settings.py b/erpnext/controllers/print_settings.py index d24b69162b..c41db25253 100644 --- a/erpnext/controllers/print_settings.py +++ b/erpnext/controllers/print_settings.py @@ -7,15 +7,9 @@ from frappe.utils import cint def print_settings_for_item_table(doc): - if frappe.db.get_single_value("Print Settings", "print_uom_after_quantity") != 1: - doc.print_templates = { - "qty": "templates/print_formats/includes/item_table_qty.html" - } - else: - doc.print_templates = { - "qty": "templates/print_formats/includes/item_table_qty_swapped.html" - } - + doc.print_templates = { + "qty": "templates/print_formats/includes/item_table_qty.html" + } doc.hide_in_print_layout = ["uom", "stock_uom"] doc.flags.compact_item_print = cint(frappe.db.get_single_value("Print Settings", "compact_item_print")) diff --git a/erpnext/templates/print_formats/includes/item_table_qty_swapped.html b/erpnext/templates/print_formats/includes/item_table_qty_swapped.html deleted file mode 100644 index c6c70f2cf7..0000000000 --- a/erpnext/templates/print_formats/includes/item_table_qty_swapped.html +++ /dev/null @@ -1,5 +0,0 @@ -{{ doc.get_formatted("qty", doc) }} -{% if (doc.uom and not doc.is_print_hide("uom")) %}{{ _(doc.uom) }} -{% elif (doc.stock_uom and not doc.is_print_hide("stock_uom")) %}{{ _(doc.stock_uom) }} -{%- endif %} - From 9355641c49835327a0efcbb03e7a3cded7104b15 Mon Sep 17 00:00:00 2001 From: Frappe ERPnext Date: Thu, 3 Sep 2020 17:31:21 +0200 Subject: [PATCH 135/175] refactor: logic moved to standard template Previously print_settings.py controller decided which template to use. Now standard template decides what to print according to settings. --- .../includes/item_table_qty.html | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/erpnext/templates/print_formats/includes/item_table_qty.html b/erpnext/templates/print_formats/includes/item_table_qty.html index 239859eea1..ecaaef42b5 100644 --- a/erpnext/templates/print_formats/includes/item_table_qty.html +++ b/erpnext/templates/print_formats/includes/item_table_qty.html @@ -1,6 +1,15 @@ -{% if (doc.uom and not doc.is_print_hide("uom")) %} - {{ _(doc.uom) }} -{% elif (doc.stock_uom and not doc.is_print_hide("stock_uom")) %} - {{ _(doc.stock_uom) }} +{% set qty_first=frappe.db.get_single_value("Print Settings", "print_uom_after_quantity") %} +{% if qty_first %} + {{ doc.get_formatted("qty", doc) }} + {% if (doc.uom and not doc.is_print_hide("uom")) %} {{ _(doc.uom) }} + {% elif (doc.stock_uom and not doc.is_print_hide("stock_uom")) %} {{ _(doc.stock_uom) }} + {%- endif %} +{% else %} + {% if (doc.uom and not doc.is_print_hide("uom")) %} + {{ _(doc.uom) }} + {% elif (doc.stock_uom and not doc.is_print_hide("stock_uom")) %} + {{ _(doc.stock_uom) }} + {%- endif %} + {{ doc.get_formatted("qty", doc) }} {%- endif %} -{{ doc.get_formatted("qty", doc) }} + From 8d8af941823ab1f054c3e59229c962f034f78227 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 4 Sep 2020 12:17:56 +0530 Subject: [PATCH 136/175] fix: capture advance payments in payment order --- .../doctype/payment_entry/payment_entry.py | 29 +++++++------------ .../payment_order_reference.json | 13 +++------ 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 842c64fdbe..bb312bf72e 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1172,30 +1172,23 @@ def make_payment_order(source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc def set_missing_values(source, target): target.payment_order_type = "Payment Entry" + target.append('references', dict( + reference_doctype="Payment Entry", + reference_name=source.name, + bank_account=source.party_bank_account, + amount=source.paid_amount, + account=source.paid_to, + supplier=source.party, + mode_of_payment=source.mode_of_payment, + )) - def update_item(source_doc, target_doc, source_parent): - target_doc.bank_account = source_parent.party_bank_account - target_doc.amount = source_doc.allocated_amount - target_doc.account = source_parent.paid_to - target_doc.payment_entry = source_parent.name - target_doc.supplier = source_parent.party - target_doc.mode_of_payment = source_parent.mode_of_payment - - - doclist = get_mapped_doc("Payment Entry", source_name, { + doclist = get_mapped_doc("Payment Entry", source_name, { "Payment Entry": { "doctype": "Payment Order", - "validation": { - "docstatus": ["=", 1] - } - }, - "Payment Entry Reference": { - "doctype": "Payment Order Reference", "validation": { "docstatus": ["=", 1] }, - "postprocess": update_item - }, + } }, target_doc, set_missing_values) diff --git a/erpnext/accounts/doctype/payment_order_reference/payment_order_reference.json b/erpnext/accounts/doctype/payment_order_reference/payment_order_reference.json index db0b76135d..d94ba74c8c 100644 --- a/erpnext/accounts/doctype/payment_order_reference/payment_order_reference.json +++ b/erpnext/accounts/doctype/payment_order_reference/payment_order_reference.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2018-07-20 16:38:06.630813", "doctype": "DocType", "editable_grid": 1, @@ -10,7 +11,6 @@ "column_break_4", "supplier", "payment_request", - "payment_entry", "mode_of_payment", "bank_account_details", "bank_account", @@ -103,17 +103,12 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 - }, - { - "fieldname": "payment_entry", - "fieldtype": "Link", - "label": "Payment Entry", - "options": "Payment Entry", - "read_only": 1 } ], + "index_web_pages_for_search": 1, "istable": 1, - "modified": "2019-05-08 13:56:25.724557", + "links": [], + "modified": "2020-09-04 08:29:51.014390", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Order Reference", From e1889d0c1b213df492f56f37cac0361fef3ed486 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 4 Sep 2020 14:45:19 +0530 Subject: [PATCH 137/175] fix(payment entry): update payment order status --- erpnext/accounts/doctype/payment_order/payment_order.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_order/payment_order.py b/erpnext/accounts/doctype/payment_order/payment_order.py index e5880aa67a..8d29ae70ae 100644 --- a/erpnext/accounts/doctype/payment_order/payment_order.py +++ b/erpnext/accounts/doctype/payment_order/payment_order.py @@ -21,10 +21,15 @@ class PaymentOrder(Document): if cancel: status = 'Initiated' - ref_field = "status" if self.payment_order_type == "Payment Request" else "payment_order_status" + if self.payment_order_type == "Payment Request": + ref_field = "status" + ref_doc_field = frappe.scrub(self.payment_order_type) + else: + ref_field = "payment_order_status" + ref_doc_field = "reference_name" for d in self.references: - frappe.db.set_value(self.payment_order_type, d.get(frappe.scrub(self.payment_order_type)), ref_field, status) + frappe.db.set_value(self.payment_order_type, d.get(ref_doc_field), ref_field, status) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs From faebef21b747992f91b5afa68da60390e5b93644 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Fri, 4 Sep 2020 15:09:16 +0530 Subject: [PATCH 138/175] fix: SE quantity data type issue --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 30bcccdda6..a92d04ff8c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -513,7 +513,7 @@ class StockEntry(StockController): d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount")) elif self.purpose == "Repack" and total_fg_qty and not d.set_basic_rate_manually: d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty) - d.basic_amount = d.basic_rate * d.qty + d.basic_amount = d.basic_rate * flt(d.qty) def distribute_additional_costs(self): if self.purpose == "Material Issue": From 4a238a2093da2b802b933a46f3b71070ca0ead0e Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 4 Sep 2020 15:18:16 +0530 Subject: [PATCH 139/175] feat: don't allow guests --- erpnext/non_profit/doctype/member/member.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 797736a3db..44b975e9e9 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -158,7 +158,7 @@ def create_member_subscription_order(user_details): return subscription -@frappe.whitelist(allow_guest=True) +@frappe.whitelist() def register_member(fullname, email, rzpay_plan_id, subscription_id, pan=None, mobile=None): plan = get_membership_type(rzpay_plan_id) if not plan: From d9e48833926bca7ad0eb8700e5d4d46719eaabe7 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 4 Sep 2020 18:55:14 +0530 Subject: [PATCH 140/175] test: check payment order creation against payment entry --- .../bank_transaction/test_bank_transaction.py | 37 ++++++++++--------- .../payment_order/test_payment_order.py | 35 +++++++++++++++++- 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index 0fe57c3239..47d2de1816 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -91,28 +91,31 @@ class TestBankTransaction(unittest.TestCase): self.assertEqual(frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0) self.assertTrue(frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date") is not None) +def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): + try: + frappe.get_doc({ + "doctype": "Bank", + "bank_name":bank_name, + }).insert() + except frappe.DuplicateEntryError: + pass + + try: + doc = frappe.get_doc({ + "doctype": "Bank Account", + "account_name":"Checking Account", + "bank": bank_name, + "account": account_name + }).insert() + except frappe.DuplicateEntryError: + pass + def add_transactions(): if frappe.flags.test_bank_transactions_created: return frappe.set_user("Administrator") - try: - frappe.get_doc({ - "doctype": "Bank", - "bank_name":"Citi Bank", - }).insert() - except frappe.DuplicateEntryError: - pass - - try: - frappe.get_doc({ - "doctype": "Bank Account", - "account_name":"Checking Account", - "bank": "Citi Bank", - "account": "_Test Bank - _TC" - }).insert() - except frappe.DuplicateEntryError: - pass + create_bank_account() doc = frappe.get_doc({ "doctype": "Bank Transaction", diff --git a/erpnext/accounts/doctype/payment_order/test_payment_order.py b/erpnext/accounts/doctype/payment_order/test_payment_order.py index 711c4cc1df..d871e3b71f 100644 --- a/erpnext/accounts/doctype/payment_order/test_payment_order.py +++ b/erpnext/accounts/doctype/payment_order/test_payment_order.py @@ -5,6 +5,39 @@ from __future__ import unicode_literals import frappe import unittest +from frappe.utils import getdate +from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry, InvalidPaymentEntry, make_payment_order +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice, make_purchase_invoice_against_cost_center class TestPaymentOrder(unittest.TestCase): - pass + def setUp(self): + create_bank_account() + + def test_payment_order_creation_against_payment_entry(self): + purchase_invoice = make_purchase_invoice() + payment_entry = get_payment_entry("Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC") + payment_entry.reference_no = "_Test_Payment_Order" + payment_entry.reference_date = getdate() + payment_entry.party_bank_account = "Checking Account - Citi Bank" + payment_entry.insert() + payment_entry.submit() + + doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry") + reference_doc = doc.get("references")[0] + self.assertEquals(reference_doc.reference_name, payment_entry.name) + self.assertEquals(reference_doc.reference_doctype, "Payment Entry") + self.assertEquals(reference_doc.supplier, "_Test Supplier") + self.assertEquals(reference_doc.amount, 250) + +def create_payment_order_against_payment_entry(ref_doc, order_type): + payment_order = frappe.get_doc(dict( + doctype="Payment Order", + company="_Test Company", + payment_order_type=order_type, + company_bank_account="Checking Account - Citi Bank" + )) + doc = make_payment_order(ref_doc.name, payment_order) + doc.save() + doc.submit() + return doc \ No newline at end of file From a76bf4d7af53dfe96274bc2c75a6c31b10ac8add Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 4 Sep 2020 19:08:08 +0530 Subject: [PATCH 141/175] fix: remove unused imports --- .../doctype/bank_transaction/test_bank_transaction.py | 2 +- erpnext/accounts/doctype/payment_order/test_payment_order.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index 47d2de1816..27546335c9 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -101,7 +101,7 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): pass try: - doc = frappe.get_doc({ + frappe.get_doc({ "doctype": "Bank Account", "account_name":"Checking Account", "bank": bank_name, diff --git a/erpnext/accounts/doctype/payment_order/test_payment_order.py b/erpnext/accounts/doctype/payment_order/test_payment_order.py index d871e3b71f..1093a9b60a 100644 --- a/erpnext/accounts/doctype/payment_order/test_payment_order.py +++ b/erpnext/accounts/doctype/payment_order/test_payment_order.py @@ -7,8 +7,8 @@ import frappe import unittest from frappe.utils import getdate from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account -from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry, InvalidPaymentEntry, make_payment_order -from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice, make_purchase_invoice_against_cost_center +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry, make_payment_order +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice class TestPaymentOrder(unittest.TestCase): def setUp(self): From 7456ffca0f89ed608733887b26f7f68524d7d3f4 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 4 Sep 2020 19:59:01 +0530 Subject: [PATCH 142/175] fix: teardown created payment order --- .../accounts/doctype/payment_order/test_payment_order.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/accounts/doctype/payment_order/test_payment_order.py b/erpnext/accounts/doctype/payment_order/test_payment_order.py index 1093a9b60a..1c23e2a0ec 100644 --- a/erpnext/accounts/doctype/payment_order/test_payment_order.py +++ b/erpnext/accounts/doctype/payment_order/test_payment_order.py @@ -14,6 +14,12 @@ class TestPaymentOrder(unittest.TestCase): def setUp(self): create_bank_account() + def tearDown(self): + for bt in frappe.get_all("Payment Order"): + doc = frappe.get_doc("Payment Order", bt.name) + doc.cancel() + doc.delete() + def test_payment_order_creation_against_payment_entry(self): purchase_invoice = make_purchase_invoice() payment_entry = get_payment_entry("Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC") From 4d636fde3af438aee340bcc0c942f163a46550bb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 4 Sep 2020 22:23:30 +0530 Subject: [PATCH 143/175] fix: API changes in loan security unpledge utility --- erpnext/loan_management/doctype/loan/loan.js | 6 +- erpnext/loan_management/doctype/loan/loan.py | 56 +++++++++++++------ .../loan_management/doctype/loan/test_loan.py | 4 +- .../loan_security_unpledge.py | 21 +++++-- 4 files changed, 58 insertions(+), 29 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index ffef60b6b0..6696386950 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -119,12 +119,10 @@ frappe.ui.form.on('Loan', { create_loan_security_unpledge: function(frm) { frappe.call({ - method: "erpnext.loan_management.doctype.loan.loan.create_loan_security_unpledge", + method: "erpnext.loan_management.doctype.loan.loan.unpledge_security", args : { "loan": frm.doc.name, - "applicant_type": frm.doc.applicant_type, - "applicant": frm.doc.applicant, - "company": frm.doc.company + "as_dict": 1 }, callback: function(r) { if (r.message) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index e20b484fc0..e2e27dd45d 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -7,7 +7,7 @@ import frappe, math, json import erpnext from frappe import _ from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime - +from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.controllers.accounts_controller import AccountsController class Loan(AccountsController): @@ -223,30 +223,52 @@ def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as return repayment_entry @frappe.whitelist() -def create_loan_security_unpledge(loan, applicant_type, applicant, company, as_dict=1): - loan_security_pledge_details = frappe.db.sql(""" - SELECT p.loan_security, sum(p.qty) as qty - FROM `tabLoan Security Pledge` lsp , `tabPledge` p - WHERE p.parent = lsp.name AND lsp.loan = %s AND lsp.docstatus = 1 - GROUP BY p.loan_security - """,(loan), as_dict=1) +def unpledge_security(loan=None, loan_security_pledge=None, as_dict=0, save=0, submit=0, approve=0): + # if loan is passed it will be considered as full unpledge + if loan: + pledge_qty_map = get_pledged_security_qty(loan) + loan_doc = frappe.get_doc('Loan', loan) + unpledge_request = create_loan_security_unpledge(pledge_qty_map, loan_doc.name, loan_doc.company, + loan_doc.applicant_type, loan_doc.applicant) + # will unpledge qty based on loan security pledge + elif loan_security_pledge: + security_map = {} + pledge_doc = frappe.get_doc('Loan Security Pledge', loan_security_pledge) + for security in pledge_doc.securities: + security_map.setdefault(security.loan_security, security.qty) + unpledge_request = create_loan_security_unpledge(security_map, pledge_doc.loan, + pledge_doc.company, pledge_doc.applicant_type, pledge_doc.applicant) + + if approve: + unpledge_request.status = 'Approved' + + if save: + unpledge_request.save() + + if submit: + unpledge_request.submit() + + if as_dict: + return unpledge_request + else: + return unpledge_request + +def create_loan_security_unpledge(unpledge_map, loan, company, applicant_type, applicant): unpledge_request = frappe.new_doc("Loan Security Unpledge") unpledge_request.applicant_type = applicant_type unpledge_request.applicant = applicant unpledge_request.loan = loan unpledge_request.company = company - for loan_security in loan_security_pledge_details: - unpledge_request.append('securities', { - "loan_security": loan_security.loan_security, - "qty": loan_security.qty - }) + for security, qty in unpledge_map.items(): + if qty: + unpledge_request.append('securities', { + "loan_security": security, + "qty": qty + }) - if as_dict: - return unpledge_request.as_dict() - else: - return unpledge_request + return unpledge_request diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 2f6cd25a36..5faf80e625 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -14,7 +14,7 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_ process_loan_interest_accrual_for_term_loans) from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall -from erpnext.loan_management.doctype.loan.loan import create_loan_security_unpledge +from erpnext.loan_management.doctype.loan.loan import unpledge_security from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount @@ -307,7 +307,7 @@ class TestLoan(unittest.TestCase): loan.load_from_db() self.assertEquals(loan.status, "Loan Closure Requested") - unpledge_request = create_loan_security_unpledge(loan.name, loan.applicant_type, loan.applicant, loan.company, as_dict=0) + unpledge_request = unpledge_security(loan=loan.name, save=1) unpledge_request.submit() unpledge_request.status = 'Approved' unpledge_request.save() diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index f6b28dae75..4cd3dad369 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -17,10 +17,12 @@ class LoanSecurityUnpledge(Document): self.validate_unpledge_qty() def on_cancel(self): - self.update_loan_security_pledge(cancel=1) self.update_loan_status(cancel=1) self.db_set('status', 'Requested') + def on_submit(self): + self.approve() + def validate_duplicate_securities(self): security_list = [] for d in self.securities: @@ -32,6 +34,7 @@ class LoanSecurityUnpledge(Document): def validate_unpledge_qty(self): pledge_qty_map = get_pledged_security_qty(self.loan) + print(pledge_qty_map, "$$$$$$$$") ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type", fields=["name", "loan_to_value_ratio"], as_list=1)) @@ -50,8 +53,7 @@ class LoanSecurityUnpledge(Document): security_value = 0 for security in self.securities: - pledged_qty = pledge_qty_map.get(security.loan_security) - + pledged_qty = pledge_qty_map.get(security.loan_security, 0) if security.qty > pledged_qty: frappe.throw(_("""Row {0}: {1} {2} of {3} is pledged against Loan {4}. You are trying to unpledge more""").format(security.idx, pledged_qty, security.uom, @@ -60,16 +62,23 @@ class LoanSecurityUnpledge(Document): qty_after_unpledge = pledged_qty - security.qty ltv_ratio = ltv_ratio_map.get(security.loan_security_type) - security_value += qty_after_unpledge * loan_security_price_map.get(security.loan_security) + current_price = loan_security_price_map.get(security.loan_security) + if not current_price: + frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(security.loan_security))) + + security_value += qty_after_unpledge * current_price if not security_value and flt(pending_principal_amount, 2) > 0: frappe.throw("Cannot Unpledge, loan to value ratio is breaching") - if security_value and (pending_principal_amount/security_value) * 100 > ltv_ratio: + if security_value and flt(pending_principal_amount/security_value) * 100 > ltv_ratio: frappe.throw("Cannot Unpledge, loan to value ratio is breaching") def on_update_after_submit(self): - if self.status == "Approved": + self.approve() + + def approve(self): + if self.status == "Approved" and not self.unpledge_time: self.update_loan_status() self.db_set('unpledge_time', get_datetime()) From e4c38e5fe5139f5f7a43fd324a89444afdb6e8b5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 4 Sep 2020 22:30:13 +0530 Subject: [PATCH 144/175] fix: Remove print statement --- .../doctype/loan_security_unpledge/loan_security_unpledge.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index 4cd3dad369..a87d832b1c 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -34,7 +34,6 @@ class LoanSecurityUnpledge(Document): def validate_unpledge_qty(self): pledge_qty_map = get_pledged_security_qty(self.loan) - print(pledge_qty_map, "$$$$$$$$") ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type", fields=["name", "loan_to_value_ratio"], as_list=1)) From 1b4ed454d296145b4be458ba32a9ec180c1c2024 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 4 Sep 2020 22:42:41 +0530 Subject: [PATCH 145/175] fix: Add reference no and descripiton field in Loan Security Pledge and Unpledge --- .../loan_security_pledge.json | 29 ++++++++++++++++++- .../loan_security_unpledge.json | 29 ++++++++++++++++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json index 4572e99299..7dd5725e2e 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json @@ -21,6 +21,10 @@ "total_security_value", "column_break_11", "maximum_loan_value", + "more_information_section", + "reference_no", + "column_break_18", + "description", "amended_from" ], "fields": [ @@ -129,11 +133,34 @@ "label": "Applicant Type", "options": "Employee\nMember\nCustomer", "reqd": 1 + }, + { + "collapsible": 1, + "fieldname": "more_information_section", + "fieldtype": "Section Break", + "label": "More Information" + }, + { + "allow_on_submit": 1, + "fieldname": "reference_no", + "fieldtype": "Data", + "label": "Reference No" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "description", + "fieldtype": "Text", + "label": "Description" } ], + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-07-02 23:38:24.002382", + "modified": "2020-09-04 22:38:19.894488", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Pledge", diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json index aece46ffda..2e2b2518d2 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json @@ -16,6 +16,10 @@ "status", "loan_security_details_section", "securities", + "more_information_section", + "reference_no", + "column_break_13", + "description", "amended_from" ], "fields": [ @@ -95,11 +99,34 @@ "label": "Applicant Type", "options": "Employee\nMember\nCustomer", "reqd": 1 + }, + { + "collapsible": 1, + "fieldname": "more_information_section", + "fieldtype": "Section Break", + "label": "More Information" + }, + { + "allow_on_submit": 1, + "fieldname": "reference_no", + "fieldtype": "Data", + "label": "Reference No" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "description", + "fieldtype": "Text", + "label": "Description" } ], + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-05-05 07:23:18.440058", + "modified": "2020-09-04 22:39:57.756146", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Unpledge", From a59f8640d15006481ab1864c392ffe51877b7780 Mon Sep 17 00:00:00 2001 From: kasgel Date: Sat, 5 Sep 2020 12:15:49 +0200 Subject: [PATCH 146/175] fix: pass stock_qty to get_pricing_rule_for_item get_pricing_rule_for_item uses the "stock_qty" and "qty" arguments to filter pricing rules. Not passing stock_qty prevents for example the shopping cart from filtering pricing rules appropriately based on Quantity. --- erpnext/utilities/product.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py index c23c1f7096..66d6cd3888 100644 --- a/erpnext/utilities/product.py +++ b/erpnext/utilities/product.py @@ -82,6 +82,7 @@ def get_price(item_code, price_list, customer_group, company, qty=1): pricing_rule = get_pricing_rule_for_item(frappe._dict({ "item_code": item_code, "qty": qty, + "stock_qty": qty, "transaction_type": "selling", "price_list": price_list, "customer_group": customer_group, From 5158d0fbbaf5eb61b3fa05a55bb815308f936bfa Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 5 Sep 2020 21:16:57 +0530 Subject: [PATCH 147/175] fix: Repayment Method not visible correctly --- erpnext/loan_management/doctype/loan/loan.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index 6696386950..9b4c21770e 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -73,8 +73,8 @@ frappe.ui.form.on('Loan', { loan_type: function(frm) { frm.toggle_reqd("repayment_method", frm.doc.is_term_loan); - frm.toggle_display("repayment_method", 1 - frm.doc.is_term_loan); - frm.toggle_display("repayment_periods", s1 - frm.doc.is_term_loan); + frm.toggle_display("repayment_method", frm.doc.is_term_loan); + frm.toggle_display("repayment_periods", frm.doc.is_term_loan); }, From 209f7c76c926ce4efc8ea84df8249347cedbf9fa Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Mon, 7 Sep 2020 11:50:24 +0530 Subject: [PATCH 148/175] feat: added transaction_type in leave ledger enter (#23258) --- .../leave_ledger_entry/leave_ledger_entry.json | 6 +++--- .../leave_ledger_entry/leave_ledger_entry_list.js | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry_list.js diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json index a5ac3f3d47..4abba5f2d4 100644 --- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json @@ -1,5 +1,4 @@ { - "actions": [], "creation": "2019-05-09 15:47:39.760406", "doctype": "DocType", "engine": "InnoDB", @@ -54,6 +53,7 @@ { "fieldname": "transaction_type", "fieldtype": "Link", + "in_standard_filter": 1, "label": "Transaction Type", "options": "DocType" }, @@ -109,9 +109,9 @@ } ], "in_create": 1, + "index_web_pages_for_search": 1, "is_submittable": 1, - "links": [], - "modified": "2020-02-27 14:40:10.502605", + "modified": "2020-09-04 12:16:36.569066", "modified_by": "Administrator", "module": "HR", "name": "Leave Ledger Entry", diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry_list.js b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry_list.js new file mode 100644 index 0000000000..889325bf2b --- /dev/null +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry_list.js @@ -0,0 +1,13 @@ +frappe.listview_settings['Leave Ledger Entry'] = { + onload: function(listview) { + if(listview.page.fields_dict.transaction_type) { + listview.page.fields_dict.transaction_type.get_query = function() { + return { + "filters": { + "name": ["in", ["Leave Allocation", "Leave Application", "Leave Encashment"]], + } + }; + }; + } + } +}; From 06130ee56bf6b5e6ba2b09b0279d7c7ad6776658 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Mon, 7 Sep 2020 11:53:50 +0530 Subject: [PATCH 149/175] fix: leave ledger patch (#23182) --- erpnext/patches.txt | 2 +- .../v12_0/generate_leave_ledger_entries.py | 27 ++++++------------- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 771babef6a..aa7996e3e1 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -632,7 +632,7 @@ execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_source') execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart') execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_field') erpnext.patches.v12_0.remove_bank_remittance_custom_fields -erpnext.patches.v12_0.generate_leave_ledger_entries +erpnext.patches.v12_0.generate_leave_ledger_entries #27-08-2020 execute:frappe.delete_doc_if_exists("Report", "Loan Repayment") erpnext.patches.v12_0.move_credit_limit_to_customer_credit_limit erpnext.patches.v12_0.add_variant_of_in_item_attribute_table diff --git a/erpnext/patches/v12_0/generate_leave_ledger_entries.py b/erpnext/patches/v12_0/generate_leave_ledger_entries.py index c5bec19fed..342c12996d 100644 --- a/erpnext/patches/v12_0/generate_leave_ledger_entries.py +++ b/erpnext/patches/v12_0/generate_leave_ledger_entries.py @@ -36,8 +36,7 @@ def generate_allocation_ledger_entries(): for allocation in allocation_list: if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name}): - allocation.update(dict(doctype="Leave Allocation")) - allocation_obj = frappe.get_doc(allocation) + allocation_obj = frappe.get_doc("Leave Allocation", allocation) allocation_obj.create_leave_ledger_entry() def generate_application_leave_ledger_entries(): @@ -46,8 +45,7 @@ def generate_application_leave_ledger_entries(): for application in leave_applications: if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Application', 'transaction_name': application.name}): - application.update(dict(doctype="Leave Application")) - frappe.get_doc(application).create_leave_ledger_entry() + frappe.get_doc("Leave Application", application.name).create_leave_ledger_entry() def generate_encashment_leave_ledger_entries(): ''' fix ledger entries for missing leave encashment transaction ''' @@ -55,8 +53,7 @@ def generate_encashment_leave_ledger_entries(): for encashment in leave_encashments: if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Encashment', 'transaction_name': encashment.name}): - encashment.update(dict(doctype="Leave Encashment")) - frappe.get_doc(encashment).create_leave_ledger_entry() + frappe.get_doc("Leave Enchashment", encashment).create_leave_ledger_entry() def generate_expiry_allocation_ledger_entries(): ''' fix ledger entries for missing leave allocation transaction ''' @@ -65,24 +62,16 @@ def generate_expiry_allocation_ledger_entries(): for allocation in allocation_list: if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name, 'is_expired': 1}): - allocation.update(dict(doctype="Leave Allocation")) - allocation_obj = frappe.get_doc(allocation) + allocation_obj = frappe.get_doc("Leave Allocation", allocation) if allocation_obj.to_date <= getdate(today()): expire_allocation(allocation_obj) def get_allocation_records(): - return frappe.get_all("Leave Allocation", filters={ - "docstatus": 1 - }, fields=['name', 'employee', 'leave_type', 'new_leaves_allocated', - 'unused_leaves', 'from_date', 'to_date', 'carry_forward' - ], order_by='to_date ASC') + return frappe.get_all("Leave Allocation", filters={"docstatus": 1}, + fields=['name'], order_by='to_date ASC') def get_leaves_application_records(): - return frappe.get_all("Leave Application", filters={ - "docstatus": 1 - }, fields=['name', 'employee', 'leave_type', 'total_leave_days', 'from_date', 'to_date']) + return frappe.get_all("Leave Application", filters={"docstatus": 1}, fields=['name']) def get_leave_encashment_records(): - return frappe.get_all("Leave Encashment", filters={ - "docstatus": 1 - }, fields=['name', 'employee', 'leave_type', 'encashable_days', 'encashment_date']) + return frappe.get_all("Leave Encashment", filters={"docstatus": 1}, fields=['name']) From da6074a84c029b73f1508de31689689956f495dc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 7 Sep 2020 14:55:04 +0530 Subject: [PATCH 150/175] fix: Pending loan interest accrual on loan closure --- .../loan_management/doctype/loan/test_loan.py | 5 ++-- .../loan_interest_accrual.py | 3 ++- .../doctype/loan_repayment/loan_repayment.py | 26 +++++++++++++++++-- .../process_loan_interest_accrual.py | 4 +++ 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 5faf80e625..f225409f62 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -199,10 +199,9 @@ class TestLoan(unittest.TestCase): "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() - amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', - 'paid_principal_amount']) + amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)']) - self.assertEquals(flt(amounts[0], 2),flt(accrued_interest_amount, 2)) + self.assertEquals(flt(amount, 2),flt(accrued_interest_amount, 2)) self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) loan.load_from_db() diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index 1d3fa71068..2d959bf3be 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -213,7 +213,8 @@ def get_last_accural_date_in_current_month(loan): WHERE loan = %s""", (loan.name)) if last_posting_date[0][0]: - return last_posting_date[0][0] + # interest for last interest accrual date is already booked, so add 1 day + return add_days(last_posting_date[0][0], 1) else: return loan.disbursement_date diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 7d83e32213..a970b4eb34 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -13,6 +13,7 @@ from frappe.utils import date_diff, add_days, getdate, add_months, get_first_day from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.general_ledger import make_gl_entries from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import update_shortfall_status +from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans class LoanRepayment(AccountsController): @@ -22,6 +23,9 @@ class LoanRepayment(AccountsController): self.validate_amount() self.allocate_amounts(amounts['pending_accrual_entries']) + def before_submit(self): + self.book_unaccrued_interest() + def on_submit(self): self.update_paid_amount() self.make_gl_entries() @@ -72,6 +76,26 @@ class LoanRepayment(AccountsController): msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount) frappe.throw(msg) + def book_unaccrued_interest(self): + if self.payment_type == 'Loan Closure': + total_interest_paid = 0 + for payment in self.repayment_details: + total_interest_paid += payment.paid_interest_amount + + if total_interest_paid < self.interest_payable: + if not self.is_term_loan: + process = process_loan_interest_accrual_for_demand_loans(posting_date=self.posting_date, + loan=self.against_loan) + + 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': lia.interest_amount, + 'paid_principal_amount': lia.payable_principal_amount + }) + def update_paid_amount(self): precision = cint(frappe.db.get_default("currency_precision")) or 2 @@ -148,8 +172,6 @@ class LoanRepayment(AccountsController): if self.payment_type == 'Loan Closure' and total_interest_paid < self.interest_payable: unaccrued_interest = self.interest_payable - total_interest_paid interest_paid -= unaccrued_interest - if self.repayment_details: - self.repayment_details[-1].paid_interest_amount += unaccrued_interest if interest_paid: self.principal_amount_paid += interest_paid diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py index cd3cf7ec96..0fa96860d0 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py @@ -36,6 +36,8 @@ def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type= loan_process.submit() + return loan_process.name + def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=None, loan=None): if not term_loan_accrual_pending(posting_date or nowdate()): @@ -49,6 +51,8 @@ def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=No loan_process.submit() + return loan_process.name + def term_loan_accrual_pending(date): pending_accrual = frappe.db.get_value('Repayment Schedule', { 'payment_date': ('<=', date), From afefa878b04781f7d6b005e2110824bebce9ec28 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 7 Sep 2020 14:57:44 +0530 Subject: [PATCH 151/175] fix: Pending loan interest accrual on loan closure --- .../doctype/loan_repayment/loan_repayment.py | 14 +++++++------- 1 file changed, 7 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 a970b4eb34..47fb885f8a 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -87,14 +87,14 @@ class LoanRepayment(AccountsController): process = process_loan_interest_accrual_for_demand_loans(posting_date=self.posting_date, loan=self.against_loan) - lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual': - process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1) + 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': lia.interest_amount, - 'paid_principal_amount': lia.payable_principal_amount - }) + self.append('repayment_details', { + 'loan_interest_accrual': lia.name, + 'paid_interest_amount': lia.interest_amount, + 'paid_principal_amount': lia.payable_principal_amount + }) def update_paid_amount(self): precision = cint(frappe.db.get_default("currency_precision")) or 2 From 74ab1084b31e8c38129aeb61b6820a01386f3a76 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 7 Sep 2020 17:55:10 +0530 Subject: [PATCH 152/175] fix: Apply TDS on Purchase Invoice creation from Purchase Order and Purchase Receipt --- .../accounts/doctype/purchase_invoice/purchase_invoice.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 4f6be59c65..b5c1cd7e1e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -25,6 +25,12 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ this.frm.set_df_property("credit_to", "print_hide", 0); } } + + // Trigger supplier event on load if supplier is available + // The reason for this is PI can be created from PR or PO and supplier is pre populated + if (this.frm.doc.supplier) { + this.frm.trigger('supplier'); + } }, refresh: function(doc) { From 06f89a1b94e98aeaa76c4745914a676d8654bb16 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 7 Sep 2020 19:46:53 +0530 Subject: [PATCH 153/175] fix: Set student email id mandatory only if skip user creation is disabled --- erpnext/education/doctype/student/student.js | 20 +- .../education/doctype/student/student.json | 3 +- .../student_applicant/student_applicant.js | 13 +- .../student_applicant/student_applicant.json | 1437 +++-------------- 4 files changed, 272 insertions(+), 1201 deletions(-) diff --git a/erpnext/education/doctype/student/student.js b/erpnext/education/doctype/student/student.js index 1936dcbd3e..9272d35543 100644 --- a/erpnext/education/doctype/student/student.js +++ b/erpnext/education/doctype/student/student.js @@ -3,15 +3,15 @@ frappe.ui.form.on('Student', { setup: function(frm) { - frm.add_fetch("guardian", "guardian_name", "guardian_name"); - frm.add_fetch("student", "title", "full_name"); - frm.add_fetch("student", "gender", "gender"); - frm.add_fetch("student", "date_of_birth", "date_of_birth"); + frm.add_fetch('guardian', 'guardian_name', 'guardian_name'); + frm.add_fetch('student', 'title', 'full_name'); + frm.add_fetch('student', 'gender', 'gender'); + frm.add_fetch('student', 'date_of_birth', 'date_of_birth'); - frm.set_query("student", "siblings", function(doc, cdt, cdn) { + frm.set_query('student', 'siblings', function(doc, cdt, cdn) { return { - "filters": { - "name": ["!=", doc.name] + 'filters': { + 'name': ['!=', doc.name] } }; }) @@ -25,6 +25,12 @@ frappe.ui.form.on('Student', { {party_type:'Student', party:frm.doc.name}); }); } + + frappe.db.get_value('Education Settings', {name: 'Education Settings'}, 'user_creation_skip', (r) => { + if (r.user_creation_skip === "0") { + frm.set_df_property('student_email_id', 'reqd', 1); + } + }); } }); diff --git a/erpnext/education/doctype/student/student.json b/erpnext/education/doctype/student/student.json index ac65c0cd7b..8ba9a7fa11 100644 --- a/erpnext/education/doctype/student/student.json +++ b/erpnext/education/doctype/student/student.json @@ -102,7 +102,6 @@ "fieldtype": "Data", "in_global_search": 1, "label": "Student Email Address", - "reqd": 1, "unique": 1 }, { @@ -255,7 +254,7 @@ ], "image_field": "image", "links": [], - "modified": "2020-07-23 18:14:06.366442", + "modified": "2020-09-07 19:28:08.914568", "modified_by": "Administrator", "module": "Education", "name": "Student", diff --git a/erpnext/education/doctype/student_applicant/student_applicant.js b/erpnext/education/doctype/student_applicant/student_applicant.js index 83621c5725..27b3377267 100644 --- a/erpnext/education/doctype/student_applicant/student_applicant.js +++ b/erpnext/education/doctype/student_applicant/student_applicant.js @@ -7,7 +7,7 @@ frappe.ui.form.on("Student Applicant", { }, refresh: function(frm) { - if(frm.doc.application_status== "Applied" && frm.doc.docstatus== 1 ) { + if (frm.doc.application_status==="Applied" && frm.doc.docstatus===1 ) { frm.add_custom_button(__("Approve"), function() { frm.set_value("application_status", "Approved"); frm.save_or_update(); @@ -20,10 +20,11 @@ frappe.ui.form.on("Student Applicant", { }, 'Actions'); } - if(frm.doc.application_status== "Approved" && frm.doc.docstatus== 1 ) { + if (frm.doc.application_status === "Approved" && frm.doc.docstatus === 1) { frm.add_custom_button(__("Enroll"), function() { frm.events.enroll(frm) }).addClass("btn-primary"); + frm.add_custom_button(__("Reject"), function() { frm.set_value("application_status", "Rejected"); frm.save_or_update(); @@ -35,7 +36,13 @@ frappe.ui.form.on("Student Applicant", { frappe.hide_msgprint(true); frappe.show_progress(__("Enrolling student"), data.progress[0],data.progress[1]); } - }) + }); + + frappe.db.get_value("Education Settings", {name: "Education Settings"}, "user_creation_skip", (r) => { + if (r.user_creation_skip === "0") { + frm.set_df_property("student_email_id", "reqd", 1); + } + }); }, enroll: function(frm) { diff --git a/erpnext/education/doctype/student_applicant/student_applicant.json b/erpnext/education/doctype/student_applicant/student_applicant.json index e5d0bd37de..bca38fb264 100644 --- a/erpnext/education/doctype/student_applicant/student_applicant.json +++ b/erpnext/education/doctype/student_applicant/student_applicant.json @@ -1,1233 +1,292 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "naming_series:", - "beta": 0, - "creation": "2015-09-11 11:50:09.740807", - "custom": 0, - "default_print_format": "", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 0, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "autoname": "naming_series:", + "creation": "2015-09-11 11:50:09.740807", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "first_name", + "middle_name", + "last_name", + "program", + "lms_only", + "paid", + "column_break_8", + "naming_series", + "application_status", + "application_date", + "academic_year", + "academic_term", + "student_admission", + "image", + "section_break_4", + "date_of_birth", + "gender", + "blood_group", + "column_break_12", + "student_email_id", + "student_mobile_number", + "nationality", + "home_address", + "address_line_1", + "address_line_2", + "pincode", + "column_break_23", + "city", + "state", + "section_break_20", + "guardians", + "section_break_21", + "siblings", + "section_break_23", + "title", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "first_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "First Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "first_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "First Name", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "middle_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Middle Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "middle_name", + "fieldtype": "Data", + "label": "Middle Name" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "last_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Last Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "last_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Last Name" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "program", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Program", - "length": 0, - "no_copy": 0, - "options": "Program", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "program", + "fieldtype": "Link", + "in_filter": 1, + "in_global_search": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Program", + "options": "Program", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "lms_only", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "LMS Only", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "lms_only", + "fieldtype": "Check", + "label": "LMS Only" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "paid", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Paid", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "paid", + "fieldtype": "Check", + "label": "Paid" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_8", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "naming_series", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Naming Series", - "length": 0, - "no_copy": 1, - "options": "EDU-APP-.YYYY.-", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 1, - "translatable": 0, - "unique": 0 - }, + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "no_copy": 1, + "options": "EDU-APP-.YYYY.-", + "set_only_once": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.docstatus != 0", - "fieldname": "application_status", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Application Status", - "length": 0, - "no_copy": 1, - "options": "Applied\nApproved\nRejected\nAdmitted", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "allow_on_submit": 1, + "depends_on": "eval:doc.docstatus != 0", + "fieldname": "application_status", + "fieldtype": "Select", + "in_filter": 1, + "label": "Application Status", + "no_copy": 1, + "options": "Applied\nApproved\nRejected\nAdmitted" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fieldname": "application_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Application Date", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "Today", + "fieldname": "application_date", + "fieldtype": "Date", + "label": "Application Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "academic_year", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Academic Year", - "length": 0, - "no_copy": 0, - "options": "Academic Year", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "academic_year", + "fieldtype": "Link", + "label": "Academic Year", + "options": "Academic Year" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "academic_term", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Academic Term", - "length": 0, - "no_copy": 0, - "options": "Academic Term", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "academic_term", + "fieldtype": "Link", + "label": "Academic Term", + "options": "Academic Term" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "student_admission", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Student Admission", - "length": 0, - "no_copy": 0, - "options": "Student Admission", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "student_admission", + "fieldtype": "Link", + "label": "Student Admission", + "options": "Student Admission" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "image", - "fieldtype": "Attach Image", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Image", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "image", + "fieldtype": "Attach Image", + "hidden": 1, + "label": "Image" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": "", - "columns": 0, - "fieldname": "section_break_4", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Personal Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "label": "Personal Details" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "date_of_birth", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Date of Birth", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "date_of_birth", + "fieldtype": "Date", + "label": "Date of Birth" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gender", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Gender", - "length": 0, - "no_copy": 0, - "options": "\nMale\nFemale", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "gender", + "fieldtype": "Select", + "label": "Gender", + "options": "\nMale\nFemale" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "blood_group", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Blood Group", - "length": 0, - "no_copy": 0, - "options": "\nA+\nA-\nB+\nB-\nO+\nO-\nAB+\nAB-", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "blood_group", + "fieldtype": "Select", + "label": "Blood Group", + "options": "\nA+\nA-\nB+\nB-\nO+\nO-\nAB+\nAB-" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_12", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "student_email_id", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Student Email Address", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "fieldname": "student_email_id", + "fieldtype": "Data", + "label": "Student Email Address", "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "student_mobile_number", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Student Mobile Number", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "student_mobile_number", + "fieldtype": "Data", + "label": "Student Mobile Number" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "nationality", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Nationality", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "nationality", + "fieldtype": "Data", + "label": "Nationality" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "home_address", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Home Address", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "home_address", + "fieldtype": "Section Break", + "label": "Home Address" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_line_1", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Address Line 1", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "address_line_1", + "fieldtype": "Data", + "label": "Address Line 1" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_line_2", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Address Line 2", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "address_line_2", + "fieldtype": "Data", + "label": "Address Line 2" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "pincode", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Pincode", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "pincode", + "fieldtype": "Data", + "label": "Pincode" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_23", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_23", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "city", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "City", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "city", + "fieldtype": "Data", + "label": "City" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "state", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "State", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "state", + "fieldtype": "Data", + "label": "State" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "section_break_20", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Guardian Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "collapsible": 1, + "fieldname": "section_break_20", + "fieldtype": "Section Break", + "label": "Guardian Details" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "guardians", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Guardians", - "length": 0, - "no_copy": 0, - "options": "Student Guardian", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "guardians", + "fieldtype": "Table", + "label": "Guardians", + "options": "Student Guardian" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "section_break_21", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sibling Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "collapsible": 1, + "fieldname": "section_break_21", + "fieldtype": "Section Break", + "label": "Sibling Details" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "siblings", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Siblings", - "length": 0, - "no_copy": 0, - "options": "Student Sibling", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "siblings", + "fieldtype": "Table", + "label": "Siblings", + "options": "Student Sibling" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_23", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_23", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "title", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Title", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amended_from", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Student Applicant", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Student Applicant", + "print_hide": 1, + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_field": "image", - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2018-10-05 13:15:59.283862", - "modified_by": "Administrator", - "module": "Education", - "name": "Student Applicant", - "name_case": "", - "owner": "Administrator", + ], + "image_field": "image", + "is_submittable": 1, + "links": [], + "modified": "2020-09-07 19:31:30.063563", + "modified_by": "Administrator", + "module": "Education", + "name": "Student Applicant", + "owner": "Administrator", "permissions": [ { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Academics User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Academics User", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Education", - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "title", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 -} + ], + "restrict_to_domain": "Education", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "title" +} \ No newline at end of file From 4673bed67ed79e6e95f39da67dc547b2cfd4a0f3 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 7 Sep 2020 20:12:29 +0530 Subject: [PATCH 154/175] fix: codacy issues --- erpnext/education/doctype/student/student.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/education/doctype/student/student.js b/erpnext/education/doctype/student/student.js index 9272d35543..5b05bc4f9a 100644 --- a/erpnext/education/doctype/student/student.js +++ b/erpnext/education/doctype/student/student.js @@ -8,7 +8,7 @@ frappe.ui.form.on('Student', { frm.add_fetch('student', 'gender', 'gender'); frm.add_fetch('student', 'date_of_birth', 'date_of_birth'); - frm.set_query('student', 'siblings', function(doc, cdt, cdn) { + frm.set_query('student', 'siblings', function(doc) { return { 'filters': { 'name': ['!=', doc.name] @@ -27,7 +27,7 @@ frappe.ui.form.on('Student', { } frappe.db.get_value('Education Settings', {name: 'Education Settings'}, 'user_creation_skip', (r) => { - if (r.user_creation_skip === "0") { + if (r.user_creation_skip === "0") { frm.set_df_property('student_email_id', 'reqd', 1); } }); From 03b0ad4f98aaf0b6555f8df7899fa14f30ffe0bc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 7 Sep 2020 23:22:21 +0530 Subject: [PATCH 155/175] fix: Only submitted Loan security pledges should be approved --- erpnext/loan_management/doctype/loan/loan.py | 10 +++++++--- .../loan_security_unpledge/loan_security_unpledge.py | 3 --- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index e2e27dd45d..d1b7589a17 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -240,15 +240,19 @@ def unpledge_security(loan=None, loan_security_pledge=None, as_dict=0, save=0, s unpledge_request = create_loan_security_unpledge(security_map, pledge_doc.loan, pledge_doc.company, pledge_doc.applicant_type, pledge_doc.applicant) - if approve: - unpledge_request.status = 'Approved' - if save: unpledge_request.save() if submit: unpledge_request.submit() + if approve: + if unpledge_request.docstatus == 1: + unpledge_request.status = 'Approved' + unpledge_request.save() + else: + frappe.throw(_('Only submittted unpledge requests can be approved')) + if as_dict: return unpledge_request else: diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index a87d832b1c..b3eb6001e4 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -20,9 +20,6 @@ class LoanSecurityUnpledge(Document): self.update_loan_status(cancel=1) self.db_set('status', 'Requested') - def on_submit(self): - self.approve() - def validate_duplicate_securities(self): security_list = [] for d in self.securities: From 5b381ac5b782030d0bc9aff0777c27d440efa199 Mon Sep 17 00:00:00 2001 From: barry86m <65860880+barry86m@users.noreply.github.com> Date: Tue, 8 Sep 2020 00:27:57 +0100 Subject: [PATCH 156/175] fix: consolidated financial statement sums values into wrong parent fix proposed by Andriesvn in bug report closes #22180 --- .../consolidated_financial_statement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 219871b1d6..d0116890b6 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -256,7 +256,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies): """accumulate children's values in parent accounts""" for d in reversed(accounts): if d.parent_account: - account = d.parent_account.split('-')[0].strip() + account = d.parent_account.split(' - ')[0].strip() if not accounts_by_name.get(account): continue From 842cb6d1d638d3ca946d6dcdc5ce51524e87e8a7 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 8 Sep 2020 10:14:04 +0530 Subject: [PATCH 157/175] Update shift_assignment.py --- erpnext/hr/doctype/shift_assignment/shift_assignment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index b1f9fd58e3..2c385e80f4 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -103,7 +103,7 @@ def add_assignments(events, start, end, conditions=None): "doctype": "Shift Assignment", "start_date": d.start_date, "end_date": d.end_date if d.end_date else nowdate(), - "title": cstr(d.employee_name) + ":"+ \ + "title": cstr(d.employee_name) + ": "+ \ cstr(d.shift_type), "docstatus": d.docstatus } From 7d7fa79efab8da0bfa6adfa3ece9c5dcbd3c73a6 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 8 Sep 2020 10:18:18 +0530 Subject: [PATCH 158/175] Update additional_salary.py --- erpnext/payroll/doctype/additional_salary/additional_salary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index cf2bff0405..e3dc9070ec 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -44,7 +44,7 @@ class AdditionalSalary(Document): additional_salaries = [salary.name for salary in additional_salaries] - if len(additional_salaries): + if additional_salaries and len(additional_salaries): frappe.throw(_("Additional Salary: {0} already exist for Salary Component: {1} for period {2} and {3}").format( bold(comma_and(additional_salaries)), bold(self.salary_component), From 9691422fb766b9d0f628fd9a5a1c712376896ddf Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 8 Sep 2020 11:57:15 +0530 Subject: [PATCH 159/175] feat: added report for mismatch in serial nos and stock quantity in warehouse (#22669) * feat: added report for mismatch in serial nos and stock quantity in warehouse * style: removed print statement * fix: rename of reports and refactor of code * fix: handled null condition for actual qty and added report link in stock desk page * style: breaking code in multiple lines --- erpnext/stock/desk_page/stock/stock.json | 4 +- .../stock_qty_vs_serial_no_count/__init__.py | 0 .../stock_qty_vs_serial_no_count.js | 42 ++++++++++ .../stock_qty_vs_serial_no_count.json | 27 +++++++ .../stock_qty_vs_serial_no_count.py | 80 +++++++++++++++++++ 5 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 erpnext/stock/report/stock_qty_vs_serial_no_count/__init__.py create mode 100644 erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js create mode 100644 erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.json create mode 100644 erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py diff --git a/erpnext/stock/desk_page/stock/stock.json b/erpnext/stock/desk_page/stock/stock.json index 1bf81f7f0e..2fba5fa804 100644 --- a/erpnext/stock/desk_page/stock/stock.json +++ b/erpnext/stock/desk_page/stock/stock.json @@ -33,7 +33,7 @@ { "hidden": 0, "label": "Key Reports", - "links": "[\n {\n \"dependencies\": [\n \"Item Price\"\n ],\n \"doctype\": \"Item Price\",\n \"is_query_report\": false,\n \"label\": \"Item-wise Price List Rate\",\n \"name\": \"Item-wise Price List Rate\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Stock Entry\"\n ],\n \"doctype\": \"Stock Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Analytics\",\n \"name\": \"Stock Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Delivery Note\"\n ],\n \"doctype\": \"Delivery Note\",\n \"is_query_report\": true,\n \"label\": \"Delivery Note Trends\",\n \"name\": \"Delivery Note Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Receipt\"\n ],\n \"doctype\": \"Purchase Receipt\",\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Order\"\n ],\n \"doctype\": \"Purchase Order\",\n \"is_query_report\": true,\n \"label\": \"Purchase Order Analysis\",\n \"name\": \"Purchase Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Bin\"\n ],\n \"doctype\": \"Bin\",\n \"is_query_report\": true,\n \"label\": \"Item Shortage Report\",\n \"name\": \"Item Shortage Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Batch\"\n ],\n \"doctype\": \"Batch\",\n \"is_query_report\": true,\n \"label\": \"Batch-Wise Balance History\",\n \"name\": \"Batch-Wise Balance History\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item Price\"\n ],\n \"doctype\": \"Item Price\",\n \"is_query_report\": false,\n \"label\": \"Item-wise Price List Rate\",\n \"name\": \"Item-wise Price List Rate\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Stock Entry\"\n ],\n \"doctype\": \"Stock Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Analytics\",\n \"name\": \"Stock Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Qty vs Serial No Count\",\n \"name\": \"Stock Qty vs Serial No Count\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Delivery Note\"\n ],\n \"doctype\": \"Delivery Note\",\n \"is_query_report\": true,\n \"label\": \"Delivery Note Trends\",\n \"name\": \"Delivery Note Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Receipt\"\n ],\n \"doctype\": \"Purchase Receipt\",\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Order\"\n ],\n \"doctype\": \"Purchase Order\",\n \"is_query_report\": true,\n \"label\": \"Purchase Order Analysis\",\n \"name\": \"Purchase Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Bin\"\n ],\n \"doctype\": \"Bin\",\n \"is_query_report\": true,\n \"label\": \"Item Shortage Report\",\n \"name\": \"Item Shortage Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Batch\"\n ],\n \"doctype\": \"Batch\",\n \"is_query_report\": true,\n \"label\": \"Batch-Wise Balance History\",\n \"name\": \"Batch-Wise Balance History\",\n \"type\": \"report\"\n }\n]" }, { "hidden": 0, @@ -58,7 +58,7 @@ "idx": 0, "is_standard": 1, "label": "Stock", - "modified": "2020-05-30 17:32:11.062681", + "modified": "2020-08-11 17:29:32.626067", "modified_by": "Administrator", "module": "Stock", "name": "Stock", diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/__init__.py b/erpnext/stock/report/stock_qty_vs_serial_no_count/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js new file mode 100644 index 0000000000..2a0fd4025c --- /dev/null +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js @@ -0,0 +1,42 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Stock Qty vs Serial No Count"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, + { + "fieldname":"warehouse", + "label": __("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse", + "get_query": function() { + const company = frappe.query_report.get_filter_value('company'); + return { + filters: { 'company': company } + } + }, + "reqd": 1 + }, + ], + + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + if (column.fieldname == "difference" && data) { + if (data.difference > 0) { + value = "" + value + ""; + } + else if (data.difference < 0) { + value = "" + value + ""; + } + } + return value; + } +}; diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.json b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.json new file mode 100644 index 0000000000..c7108b513e --- /dev/null +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.json @@ -0,0 +1,27 @@ +{ + "add_total_row": 0, + "creation": "2020-07-23 19:31:32.395011", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-07-23 19:32:02.168185", + "modified_by": "Administrator", + "module": "Stock", + "name": "Stock Qty vs Serial No Count", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Item", + "report_name": "Stock Qty vs Serial No Count", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock Manager" + }, + { + "role": "Stock User" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py new file mode 100644 index 0000000000..55f041c95c --- /dev/null +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py @@ -0,0 +1,80 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +def execute(filters=None): + columns = get_columns() + data = get_data(filters.warehouse) + return columns, data + +def get_columns(): + columns = [ + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 200 + }, + { + "label": _("Item Name"), + "fieldname": "item_name", + "fieldtype": "Data", + "width": 200 + }, + { + "label": _("Serial No Count"), + "fieldname": "total", + "fieldtype": "Float", + "width": 150 + }, + { + "label": _("Stock Qty"), + "fieldname": "stock_qty", + "fieldtype": "Float", + "width": 150 + }, + { + "label": _("Difference"), + "fieldname": "difference", + "fieldtype": "Float", + "width": 150 + }, + ] + + return columns + +def get_data(warehouse): + serial_item_list = frappe.get_all("Item", filters={ + 'has_serial_no': True, + }, fields=['item_code', 'item_name']) + + status_list = ['Active', 'Expired'] + data = [] + for item in serial_item_list: + total_serial_no = frappe.db.count("Serial No", + filters={"item_code": item.item_code, "status": ("in", status_list), "warehouse": warehouse}) + + actual_qty = frappe.db.get_value('Bin', fieldname=['actual_qty'], + filters={"warehouse": warehouse, "item_code": item.item_code}) + + # frappe.db.get_value returns null if no record exist. + if not actual_qty: + actual_qty = 0 + + difference = total_serial_no - actual_qty + + row = { + "item_code": item.item_code, + "item_name": item.item_name, + "total": total_serial_no, + "stock_qty": actual_qty, + "difference": difference, + } + + data.append(row) + + return data \ No newline at end of file From f7619df8b22e4579d27f1150f76b6b9e94823147 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 8 Sep 2020 11:57:45 +0530 Subject: [PATCH 160/175] fix: Lock row in subquery while setting delivered qty (#23100) --- erpnext/controllers/status_updater.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 0dc9878afd..9feac78770 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -255,7 +255,7 @@ class StatusUpdater(Document): args['second_source_condition'] = """ + ifnull((select sum(%(second_source_field)s) from `tab%(second_source_dt)s` where `%(second_join_field)s`="%(detail_id)s" - and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s), 0) """ % args + and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s FOR UPDATE), 0)""" % args if args['detail_id']: if not args.get("extra_cond"): args["extra_cond"] = "" From 12ab8ebc95d925815e856e360809694afb3e0f8d Mon Sep 17 00:00:00 2001 From: Afshan Date: Tue, 8 Sep 2020 12:55:42 +0530 Subject: [PATCH 161/175] fix: removed ignore permission flag --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 0a385d0af7..34c262e27f 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -1023,7 +1023,7 @@ def make_inter_company_journal_entry(name, voucher_type, company): return journal_entry.as_dict() @frappe.whitelist() -def make_reverse_journal_entry(source_name, target_doc=None, ignore_permissions=False): +def make_reverse_journal_entry(source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc def update_accounts(source, target, source_parent): @@ -1049,6 +1049,6 @@ def make_reverse_journal_entry(source_name, target_doc=None, ignore_permissions= }, "postprocess": update_accounts, }, - }, target_doc, ignore_permissions=ignore_permissions) + }, target_doc) return doclist \ No newline at end of file From f7731b0715a4b7ba07cc216998e9984f84c9f0ff Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 8 Sep 2020 13:41:04 +0530 Subject: [PATCH 162/175] fix: TDS applicable on creating PI from get items button --- .../accounts/doctype/purchase_invoice/purchase_invoice.js | 2 ++ .../accounts/doctype/purchase_invoice/purchase_invoice.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index b5c1cd7e1e..2bfa4a572e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -141,6 +141,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ } }); } + + this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1); }, unblock_invoice: function() { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 7b1062f654..b4ee7c999e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -132,6 +132,11 @@ class PurchaseInvoice(BuyingController): if not self.due_date: self.due_date = get_due_date(self.posting_date, "Supplier", self.supplier, self.company, self.bill_date) + tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category") + if tds_category and not for_validate: + self.apply_tds = 1 + self.tax_withholding_category = tds_category + super(PurchaseInvoice, self).set_missing_values(for_validate) def check_conversion_rate(self): From 8f43cf2c9a6a2564d2173d7a4fc174f5d5ab6787 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 8 Sep 2020 17:44:47 +0530 Subject: [PATCH 163/175] fix: asset movement date for backdated asset entry (#23298) --- erpnext/assets/doctype/asset/asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 0bd03a8dbe..9d08d9212d 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -146,7 +146,7 @@ class Asset(AccountsController): 'assets': assets, 'purpose': 'Receipt', 'company': self.company, - 'transaction_date': getdate(nowdate()), + 'transaction_date': getdate(self.purchase_date), 'reference_doctype': reference_doctype, 'reference_name': reference_docname }).insert() From f247686ace6462ae43e3788161e96c8f9f2698ca Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 8 Sep 2020 21:26:06 +0530 Subject: [PATCH 164/175] fix: Amount for closed states --- .../loan_management/doctype/loan_repayment/loan_repayment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 7d83e32213..be221dfa7f 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -305,7 +305,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): if not final_due_date: final_due_date = add_days(due_date, loan_type_details.grace_period_in_days) - if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested'): + if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested', 'Closed'): pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable else: pending_principal_amount = against_loan_doc.disbursed_amount From 846e6d480321b5366264562ae11f8a95fa45d141 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 8 Sep 2020 21:32:06 +0530 Subject: [PATCH 165/175] fix: Add test --- erpnext/loan_management/doctype/loan/test_loan.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 5faf80e625..ba84921a2e 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -318,6 +318,11 @@ class TestLoan(unittest.TestCase): self.assertEqual(loan.status, 'Closed') self.assertEquals(sum(pledged_qty.values()), 0) + amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") + self.assertEqual(amounts['pending_principal_amount'], 0) + self.assertEqual(amounts['payable_principal_amount'], 0) + self.assertEqual(amounts['interest_amount'], 0) + def test_disbursal_check_with_shortfall(self): pledges = [{ "loan_security": "Test Security 2", From ec6a97fb6a0d6db5fc03b76fd27050db6e9a2c12 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 9 Sep 2020 10:54:14 +0530 Subject: [PATCH 166/175] fix: cannot delete pos page if linked with desk page (#22993) * fix: cannot delete pos page if linked with desk page * fix: replace pos page links with point-of-sale page Co-authored-by: gavin --- erpnext/patches.txt | 2 +- .../v13_0/replace_pos_page_with_point_of_sale_page.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index aa7996e3e1..6c58f2f452 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -697,7 +697,7 @@ erpnext.patches.v12_0.update_bom_in_so_mr execute:frappe.delete_doc("Report", "Department Analytics") execute:frappe.rename_doc("Desk Page", "Loan Management", "Loan", force=True) erpnext.patches.v12_0.update_uom_conversion_factor -execute:frappe.delete_doc_if_exists("Page", "pos") #29-05-2020 +erpnext.patches.v13_0.replace_pos_page_with_point_of_sale_page erpnext.patches.v13_0.delete_old_purchase_reports erpnext.patches.v12_0.set_italian_import_supplier_invoice_permissions erpnext.patches.v13_0.update_subscription diff --git a/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py b/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py new file mode 100644 index 0000000000..390e217cad --- /dev/null +++ b/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + if frappe.db.exists("Page", "point-of-sale"): + frappe.rename_doc("Page", "pos", "point-of-sale", 1, 1) \ No newline at end of file From 1915a608348d89f8dac040a8c81832fb502723dc Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 10 Sep 2020 12:13:58 +0530 Subject: [PATCH 167/175] fix: education settings check --- erpnext/education/doctype/student/student.js | 2 +- .../education/doctype/student_applicant/student_applicant.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/education/doctype/student/student.js b/erpnext/education/doctype/student/student.js index 5b05bc4f9a..fd23ae41ef 100644 --- a/erpnext/education/doctype/student/student.js +++ b/erpnext/education/doctype/student/student.js @@ -27,7 +27,7 @@ frappe.ui.form.on('Student', { } frappe.db.get_value('Education Settings', {name: 'Education Settings'}, 'user_creation_skip', (r) => { - if (r.user_creation_skip === "0") { + if (cint(r.user_creation_skip) !== 1) { frm.set_df_property('student_email_id', 'reqd', 1); } }); diff --git a/erpnext/education/doctype/student_applicant/student_applicant.js b/erpnext/education/doctype/student_applicant/student_applicant.js index 27b3377267..b4cfdf16e0 100644 --- a/erpnext/education/doctype/student_applicant/student_applicant.js +++ b/erpnext/education/doctype/student_applicant/student_applicant.js @@ -39,7 +39,7 @@ frappe.ui.form.on("Student Applicant", { }); frappe.db.get_value("Education Settings", {name: "Education Settings"}, "user_creation_skip", (r) => { - if (r.user_creation_skip === "0") { + if (cint(r.user_creation_skip) !== 1) { frm.set_df_property("student_email_id", "reqd", 1); } }); From c0f9d1b65a46b92d97e76a4f0a3341302bc252f6 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 10 Sep 2020 17:33:52 +0530 Subject: [PATCH 168/175] test: Lab Module --- .../doctype/lab_test/test_lab_test.py | 202 +++++++++++++++++- .../test_patient_medical_record.py | 6 +- 2 files changed, 202 insertions(+), 6 deletions(-) diff --git a/erpnext/healthcare/doctype/lab_test/test_lab_test.py b/erpnext/healthcare/doctype/lab_test/test_lab_test.py index 4131ad99b7..d949c46d16 100644 --- a/erpnext/healthcare/doctype/lab_test/test_lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/test_lab_test.py @@ -3,8 +3,204 @@ # See license.txt from __future__ import unicode_literals import unittest - -# test_records = frappe.get_test_records('Lab Test') +import frappe +from frappe.utils import getdate, nowtime +from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient +from erpnext.healthcare.doctype.lab_test.lab_test import create_multiple +from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account +from erpnext.healthcare.doctype.patient_medical_record.test_patient_medical_record import create_lab_test_template as create_blood_test_template class TestLabTest(unittest.TestCase): - pass + def test_lab_test_item(self): + lab_template = create_lab_test_template() + self.assertTrue(frappe.db.exists('Item', lab_template.item)) + self.assertEqual(frappe.db.get_value('Item Price', {'item_code':lab_template.item}, 'price_list_rate'), lab_template.lab_test_rate) + + lab_template.disabled = 1 + lab_template.save() + self.assertEquals(frappe.db.get_value('Item', lab_template.item, 'disabled'), 1) + + lab_template.reload() + + lab_template.disabled = 0 + lab_template.save() + + def test_descriptive_lab_test(self): + lab_template = create_lab_test_template() + + # blank result value not allowed as per template + lab_test = create_lab_test(lab_template) + lab_test.descriptive_test_items[0].result_value = 12 + lab_test.descriptive_test_items[2].result_value = 1 + lab_test.save() + self.assertRaises(frappe.ValidationError, lab_test.submit) + + def test_sample_collection(self): + frappe.db.set_value('Healthcare Settings', 'Healthcare Settings', 'create_sample_collection_for_lab_test', 1) + lab_template = create_lab_test_template() + + lab_test = create_lab_test(lab_template) + lab_test.descriptive_test_items[0].result_value = 12 + lab_test.descriptive_test_items[1].result_value = 1 + lab_test.descriptive_test_items[2].result_value = 2.3 + lab_test.save() + + # check sample collection created + self.assertTrue(frappe.db.exists('Sample Collection', {'sample': lab_template.sample})) + + frappe.db.set_value('Healthcare Settings', 'Healthcare Settings', 'create_sample_collection_for_lab_test', 0) + lab_test = create_lab_test(lab_template) + lab_test.descriptive_test_items[0].result_value = 12 + lab_test.descriptive_test_items[1].result_value = 1 + lab_test.descriptive_test_items[2].result_value = 2.3 + lab_test.save() + + # sample collection should not be created + lab_test.reload() + self.assertEquals(lab_test.sample, None) + + def test_create_lab_tests_from_sales_invoice(self): + sales_invoice = create_sales_invoice() + create_multiple('Sales Invoice', sales_invoice.name) + sales_invoice.reload() + self.assertIsNotNone(sales_invoice.items[0].reference_dn) + self.assertIsNotNone(sales_invoice.items[1].reference_dn) + + def test_create_lab_tests_from_patient_encounter(self): + patient_encounter = create_patient_encounter() + create_multiple('Patient Encounter', patient_encounter.name) + patient_encounter.reload() + self.assertTrue(patient_encounter.lab_test_prescription[0].lab_test_created) + self.assertTrue(patient_encounter.lab_test_prescription[0].lab_test_created) + + +def create_lab_test_template(test_sensitivity=0, sample_collection=1): + medical_department = create_medical_department() + if frappe.db.exists('Lab Test Template', 'Insulin Resistance'): + return frappe.get_doc('Lab Test Template', 'Insulin Resistance') + template = frappe.new_doc('Lab Test Template') + template.lab_test_name = 'Insulin Resistance' + template.lab_test_template_type = 'Descriptive' + template.lab_test_code = 'Insulin Resistance' + template.lab_test_group = 'Services' + template.department = medical_department + template.is_billable = 1 + template.lab_test_description = 'Insulin Resistance' + template.lab_test_rate = 2000 + + for entry in ['FBS', 'Insulin', 'IR']: + template.append('descriptive_test_templates', { + 'particulars': entry, + 'allow_blank': 1 if entry=='IR' else 0 + }) + + if test_sensitivity: + template.sensitivity = 1 + + if sample_collection: + template.sample = create_lab_test_sample() + template.sample_qty = 5.0 + + template.save() + return template + +def create_medical_department(): + medical_department = frappe.db.exists('Medical Department', '_Test Medical Department') + if not medical_department: + medical_department = frappe.new_doc('Medical Department') + medical_department.department = '_Test Medical Department' + medical_department.save() + medical_department = medical_department.name + + return medical_department + +def create_lab_test(lab_template): + patient = create_patient() + lab_test = frappe.new_doc('Lab Test') + lab_test.template = lab_template.name + lab_test.patient = patient + lab_test.patient_sex = 'Female' + lab_test.save() + + return lab_test + +def create_lab_test_sample(): + blood_sample = frappe.db.exists('Lab Test Sample', 'Blood Sample') + if blood_sample: + return blood_sample + + sample = frappe.new_doc('Lab Test Sample') + sample.sample = 'Blood Sample' + sample.sample_uom = 'U/ml' + sample.save() + + return sample.name + +def create_sales_invoice(): + patient = create_patient() + medical_department = create_medical_department() + insulin_resistance_template = create_lab_test_template() + blood_test_template = create_blood_test_template(medical_department) + + sales_invoice = frappe.new_doc('Sales Invoice') + sales_invoice.patient = patient + sales_invoice.customer = frappe.db.get_value('Patient', patient, 'customer') + sales_invoice.due_date = getdate() + sales_invoice.company = '_Test Company' + sales_invoice.debit_to = get_receivable_account('_Test Company') + + tests = [insulin_resistance_template, blood_test_template] + for entry in tests: + item_line = sales_invoice.append('items', { + 'item_code': entry.item, + 'item_name': entry.lab_test_name, + 'description': entry.lab_test_description, + 'qty': 1, + 'uom': 'Nos', + 'conversion_factor': 1, + 'income_account': get_income_account(None, '_Test Company'), + 'rate': entry.lab_test_rate, + 'amount': entry.lab_test_rate + }) + + sales_invoice.set_missing_values() + + sales_invoice.submit() + return sales_invoice + +def create_patient_encounter(): + patient = create_patient() + medical_department = create_medical_department() + insulin_resistance_template = create_lab_test_template() + blood_test_template = create_blood_test_template(medical_department) + + patient_encounter = frappe.new_doc('Patient Encounter') + patient_encounter.patient = patient + patient_encounter.practitioner = create_practitioner() + patient_encounter.encounter_date = getdate() + patient_encounter.encounter_time = nowtime() + + tests = [insulin_resistance_template, blood_test_template] + for entry in tests: + patient_encounter.append('lab_test_prescription', { + 'lab_test_code': entry.item, + 'lab_test_name': entry.lab_test_name + }) + + patient_encounter.submit() + return patient_encounter + + +def create_practitioner(): + practitioner = frappe.db.exists('Healthcare Practitioner', '_Test Healthcare Practitioner') + + if not practitioner: + practitioner = frappe.new_doc('Healthcare Practitioner') + practitioner.first_name = '_Test Healthcare Practitioner' + practitioner.gender = 'Female' + practitioner.op_consulting_charge = 500 + practitioner.inpatient_visit_charge = 500 + practitioner.save(ignore_permissions=True) + practitioner = practitioner.name + + return practitioner 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 e5a5e4c010..aa85a23113 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 @@ -34,7 +34,7 @@ class TestPatientMedicalRecord(unittest.TestCase): self.assertTrue(medical_rec) template = create_lab_test_template(medical_department) - lab_test = create_lab_test(template, patient) + lab_test = create_lab_test(template.name, patient) # check for lab test medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': lab_test.name}) self.assertTrue(medical_rec) @@ -66,7 +66,7 @@ def create_vital_signs(appointment): def create_lab_test_template(medical_department): if frappe.db.exists('Lab Test Template', 'Blood Test'): - return 'Blood Test' + return frappe.get_doc('Lab Test Template', 'Blood Test') template = frappe.new_doc('Lab Test Template') template.lab_test_name = 'Blood Test' @@ -76,7 +76,7 @@ def create_lab_test_template(medical_department): template.is_billable = 1 template.lab_test_rate = 2000 template.save() - return template.name + return template def create_lab_test(template, patient): lab_test = frappe.new_doc('Lab Test') From a29436dc91f75abe619d53d6b53689faef109a07 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 10 Sep 2020 17:45:17 +0530 Subject: [PATCH 169/175] fix: codacy --- erpnext/healthcare/doctype/lab_test/test_lab_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/healthcare/doctype/lab_test/test_lab_test.py b/erpnext/healthcare/doctype/lab_test/test_lab_test.py index d949c46d16..79ab8a4d7f 100644 --- a/erpnext/healthcare/doctype/lab_test/test_lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/test_lab_test.py @@ -151,7 +151,7 @@ def create_sales_invoice(): tests = [insulin_resistance_template, blood_test_template] for entry in tests: - item_line = sales_invoice.append('items', { + sales_invoice.append('items', { 'item_code': entry.item, 'item_name': entry.lab_test_name, 'description': entry.lab_test_description, From cd89994b33bdb120111f9fa9cdf5b145b2c04d91 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 10 Sep 2020 19:28:46 +0530 Subject: [PATCH 170/175] fix: multiple pos issues (#23297) * fix: returns can be made against unconsolidated invoices * fix: indentation * fix: mode of payment not fetching for pos returns * patch: default pos profile print format * fix: tests * chore: clean up retail desk page --- .../desk_page/accounting/accounting.json | 9 +- .../pos_closing_entry/pos_closing_entry.js | 54 ++- .../doctype/pos_invoice/pos_invoice.json | 6 +- .../doctype/pos_invoice/test_pos_invoice.py | 6 +- .../pos_invoice_merge_log.py | 21 +- .../controllers/sales_and_purchase_return.py | 3 +- erpnext/patches.txt | 1 + .../v13_0/change_default_pos_print_format.py | 8 + .../public/js/controllers/taxes_and_totals.js | 25 +- erpnext/selling/desk_page/retail/retail.json | 33 +- .../page/point_of_sale/point_of_sale.js | 2 +- .../page/point_of_sale/pos_controller.js | 31 +- .../page/point_of_sale/pos_item_cart.js | 248 ++++++------ .../page/point_of_sale/pos_item_details.js | 114 +++--- .../page/point_of_sale/pos_item_selector.js | 374 +++++++++--------- .../page/point_of_sale/pos_number_pad.js | 81 ++-- 16 files changed, 521 insertions(+), 495 deletions(-) create mode 100644 erpnext/patches/v13_0/change_default_pos_print_format.py diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json index 2c5231491c..3f23ba9019 100644 --- a/erpnext/accounts/desk_page/accounting/accounting.json +++ b/erpnext/accounts/desk_page/accounting/accounting.json @@ -43,7 +43,7 @@ { "hidden": 0, "label": "Bank Statement", - "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"label\": \"Bank Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -98,7 +98,7 @@ "idx": 0, "is_standard": 1, "label": "Accounting", - "modified": "2020-09-03 10:37:07.865801", + "modified": "2020-09-09 11:45:33.766400", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", @@ -147,11 +147,6 @@ "link_to": "Trial Balance", "type": "Report" }, - { - "label": "Point of Sale", - "link_to": "point-of-sale", - "type": "Page" - }, { "label": "Dashboard", "link_to": "Accounts", diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js index 8dcd2e4a72..9336fc3706 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -55,14 +55,48 @@ frappe.ui.form.on('POS Closing Entry', { }, callback: (r) => { let pos_docs = r.message; - set_form_data(pos_docs, frm) - refresh_fields(frm) - set_html_data(frm) + set_form_data(pos_docs, frm); + refresh_fields(frm); + set_html_data(frm); } }) } }); +cur_frm.cscript.before_pos_transactions_remove = function(doc, cdt, cdn) { + const removed_row = locals[cdt][cdn]; + + if (!removed_row.pos_invoice) return; + + frappe.db.get_doc("POS Invoice", removed_row.pos_invoice).then(doc => { + cur_frm.doc.grand_total -= flt(doc.grand_total); + cur_frm.doc.net_total -= flt(doc.net_total); + cur_frm.doc.total_quantity -= flt(doc.total_qty); + refresh_payments(doc, cur_frm, 1); + refresh_taxes(doc, cur_frm, 1); + refresh_fields(cur_frm); + set_html_data(cur_frm); + }); +} + +frappe.ui.form.on('POS Invoice Reference', { + pos_invoice(frm, cdt, cdn) { + const added_row = locals[cdt][cdn]; + + if (!added_row.pos_invoice) return; + + frappe.db.get_doc("POS Invoice", added_row.pos_invoice).then(doc => { + frm.doc.grand_total += flt(doc.grand_total); + frm.doc.net_total += flt(doc.net_total); + frm.doc.total_quantity += flt(doc.total_qty); + refresh_payments(doc, frm); + refresh_taxes(doc, frm); + refresh_fields(frm); + set_html_data(frm); + }); + } +}) + frappe.ui.form.on('POS Closing Entry Detail', { closing_amount: (frm, cdt, cdn) => { const row = locals[cdt][cdn]; @@ -76,8 +110,8 @@ function set_form_data(data, frm) { frm.doc.grand_total += flt(d.grand_total); frm.doc.net_total += flt(d.net_total); frm.doc.total_quantity += flt(d.total_qty); - add_to_payments(d, frm); - add_to_taxes(d, frm); + refresh_payments(d, frm); + refresh_taxes(d, frm); }); } @@ -90,11 +124,12 @@ function add_to_pos_transaction(d, frm) { }) } -function add_to_payments(d, frm) { +function refresh_payments(d, frm, remove) { d.payments.forEach(p => { const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment); if (payment) { - payment.expected_amount += flt(p.amount); + if (!remove) payment.expected_amount += flt(p.amount); + else payment.expected_amount -= flt(p.amount); } else { frm.add_child("payment_reconciliation", { mode_of_payment: p.mode_of_payment, @@ -105,11 +140,12 @@ function add_to_payments(d, frm) { }) } -function add_to_taxes(d, frm) { +function refresh_taxes(d, frm, remove) { d.taxes.forEach(t => { const tax = frm.doc.taxes.find(tx => tx.account_head === t.account_head && tx.rate === t.rate); if (tax) { - tax.amount += flt(t.tax_amount); + if (!remove) tax.amount += flt(t.tax_amount); + else tax.amount -= flt(t.tax_amount); } else { frm.add_child("taxes", { account_head: t.account_head, diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index 2a2e3df8ae..4780688471 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -279,7 +279,8 @@ "fieldtype": "Check", "label": "Is Return (Credit Note)", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "set_only_once": 1 }, { "fieldname": "column_break1", @@ -1578,9 +1579,10 @@ } ], "icon": "fa fa-file-text", + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-05-29 15:08:39.337385", + "modified": "2020-09-07 12:43:09.138720", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 9c62a87677..514a2acd8c 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -182,8 +182,9 @@ class TestPOSInvoice(unittest.TestCase): def test_pos_returns_with_repayment(self): pos = create_pos_invoice(qty = 10, do_not_save=True) + pos.set('payments', []) pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500}) - pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500}) + pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500, 'default': 1}) pos.insert() pos.submit() @@ -200,8 +201,9 @@ class TestPOSInvoice(unittest.TestCase): income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105, cost_center = "Main - _TC", do_not_save=True) + pos.set('payments', []) pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 50}) - pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60}) + pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60, 'default': 1}) pos.insert() pos.submit() diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 00dbad5fa0..11b9d2509e 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -24,11 +24,20 @@ class POSInvoiceMergeLog(Document): def validate_pos_invoice_status(self): for d in self.pos_invoices: - status, docstatus = frappe.db.get_value('POS Invoice', d.pos_invoice, ['status', 'docstatus']) + status, docstatus, is_return, return_against = frappe.db.get_value( + 'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against']) + if docstatus != 1: frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, d.pos_invoice)) - if status in ['Consolidated']: + if status == "Consolidated": frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, d.pos_invoice, status)) + if is_return and return_against not in [d.pos_invoice for d in self.pos_invoices] and status != "Consolidated": + # if return entry is not getting merged in the current pos closing and if it is not consolidated + frappe.throw( + _("Row #{}: Return Invoice {} cannot be made against unconsolidated invoice. \ + You can add original invoice {} manually to proceed.") + .format(d.idx, frappe.bold(d.pos_invoice), frappe.bold(return_against)) + ) def on_submit(self): pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] @@ -36,12 +45,12 @@ class POSInvoiceMergeLog(Document): returns = [d for d in pos_invoice_docs if d.get('is_return') == 1] sales = [d for d in pos_invoice_docs if d.get('is_return') == 0] - sales_invoice = self.process_merging_into_sales_invoice(sales) + sales_invoice, credit_note = "", "" + if sales: + sales_invoice = self.process_merging_into_sales_invoice(sales) - if len(returns): + if returns: credit_note = self.process_merging_into_credit_note(returns) - else: - credit_note = "" self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index a03dee1174..afc5f8179f 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -242,7 +242,8 @@ def make_return_doc(doctype, source_name, target_doc=None): 'type': data.type, 'amount': -1 * paid_amount, 'base_amount': -1 * base_paid_amount, - 'account': data.account + 'account': data.account, + 'default': data.default }) if doc.is_pos: doc.paid_amount = -1 * source.paid_amount diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 6c58f2f452..9ce570e6d0 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -725,3 +725,4 @@ erpnext.patches.v12_0.rename_lost_reason_detail erpnext.patches.v13_0.drop_razorpay_payload_column erpnext.patches.v13_0.update_start_end_date_for_old_shift_assignment erpnext.patches.v13_0.setting_custom_roles_for_some_regional_reports +erpnext.patches.v13_0.change_default_pos_print_format diff --git a/erpnext/patches/v13_0/change_default_pos_print_format.py b/erpnext/patches/v13_0/change_default_pos_print_format.py new file mode 100644 index 0000000000..605a29e477 --- /dev/null +++ b/erpnext/patches/v13_0/change_default_pos_print_format.py @@ -0,0 +1,8 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.db.sql( + """UPDATE `tabPOS Profile` profile + SET profile.`print_format` = 'POS Invoice' + WHERE profile.`print_format` = 'Point of Sale'""") \ No newline at end of file diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 6951539026..6d58fd2f3c 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -673,23 +673,14 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ ); } - frappe.db.get_value('Sales Invoice Payment', {'parent': this.frm.doc.pos_profile, 'default': 1}, - ['mode_of_payment', 'account', 'type'], (value) => { - if (this.frm.is_dirty()) { - frappe.model.clear_table(this.frm.doc, 'payments'); - if (value) { - let row = frappe.model.add_child(this.frm.doc, 'Sales Invoice Payment', 'payments'); - row.mode_of_payment = value.mode_of_payment; - row.type = value.type; - row.account = value.account; - row.default = 1; - row.amount = total_amount_to_pay; - } else { - this.frm.set_value('is_pos', 1); - } - this.frm.refresh_fields(); - } - }, 'Sales Invoice'); + this.frm.doc.payments.find(pay => { + if (pay.default) { + pay.amount = total_amount_to_pay; + } else { + pay.amount = 0.0 + } + }); + this.frm.refresh_fields(); this.calculate_paid_amount(); }, diff --git a/erpnext/selling/desk_page/retail/retail.json b/erpnext/selling/desk_page/retail/retail.json index 581e14cf81..c4ddf26a90 100644 --- a/erpnext/selling/desk_page/retail/retail.json +++ b/erpnext/selling/desk_page/retail/retail.json @@ -2,8 +2,18 @@ "cards": [ { "hidden": 0, - "label": "Retail Operations", - "links": "[\n {\n \"description\": \"Setup default values for POS Invoices\",\n \"label\": \"Point of Sale Profile\",\n \"name\": \"POS Profile\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"POS Profile\"\n ],\n \"description\": \"Point of Sale\",\n \"label\": \"Point of Sale\",\n \"name\": \"point-of-sale\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"description\": \"Setup mode of POS (Online / Offline)\",\n \"label\": \"POS Settings\",\n \"name\": \"POS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Cashier Closing\",\n \"label\": \"Cashier Closing\",\n \"name\": \"Cashier Closing\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To make Customer based incentive schemes.\",\n \"label\": \"Loyalty Program\",\n \"name\": \"Loyalty Program\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To view logs of Loyalty Points assigned to a Customer.\",\n \"label\": \"Loyalty Point Entry\",\n \"name\": \"Loyalty Point Entry\",\n \"type\": \"doctype\"\n }\n]" + "label": "Settings & Configurations", + "links": "[\n {\n \"description\": \"Setup default values for POS Invoices\",\n \"label\": \"Point-of-Sale Profile\",\n \"name\": \"POS Profile\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"POS Settings\",\n \"name\": \"POS Settings\",\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Loyalty Program", + "links": "[\n {\n \"description\": \"To make Customer based incentive schemes.\",\n \"label\": \"Loyalty Program\",\n \"name\": \"Loyalty Program\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To view logs of Loyalty Points assigned to a Customer.\",\n \"label\": \"Loyalty Point Entry\",\n \"name\": \"Loyalty Point Entry\",\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Opening & Closing", + "links": "[\n {\n \"label\": \"POS Opening Entry\",\n \"name\": \"POS Opening Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"POS Closing Entry\",\n \"name\": \"POS Closing Entry\",\n \"type\": \"doctype\"\n }\n]" } ], "category": "Domains", @@ -18,7 +28,7 @@ "idx": 0, "is_standard": 1, "label": "Retail", - "modified": "2020-08-20 18:00:07.515691", + "modified": "2020-09-09 11:46:28.297435", "modified_by": "Administrator", "module": "Selling", "name": "Retail", @@ -28,25 +38,10 @@ "restrict_to_domain": "Retail", "shortcuts": [ { - "color": "#9deca2", "doc_view": "", - "format": "{} Active", - "label": "Point of Sale Profile", - "link_to": "POS Profile", - "stats_filter": "{\n \"disabled\": 0\n}", - "type": "DocType" - }, - { - "doc_view": "", - "label": "Point of Sale", + "label": "Point Of Sale", "link_to": "point-of-sale", "type": "Page" - }, - { - "doc_view": "", - "label": "POS Settings", - "link_to": "POS Settings", - "type": "DocType" } ] } \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 2ce0b270f9..8d4ac78422 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -9,7 +9,7 @@ frappe.pages['point-of-sale'].on_page_load = function(wrapper) { title: __('Point of Sale'), single_column: true }); - // online + wrapper.pos = new erpnext.PointOfSale.Controller(wrapper); window.cur_pos = wrapper.pos; }; \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index ae5471b900..5018254b0a 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -8,7 +8,7 @@ {% include "erpnext/selling/page/point_of_sale/pos_past_order_summary.js" %} erpnext.PointOfSale.Controller = class { - constructor(wrapper) { + constructor(wrapper) { this.wrapper = $(wrapper).find('.layout-main-section'); this.page = wrapper.page; @@ -36,7 +36,7 @@ erpnext.PointOfSale.Controller = class { const table_fields = [ { fieldname: "mode_of_payment", fieldtype: "Link", in_list_view: 1, label: "Mode of Payment", options: "Mode of Payment", reqd: 1 }, { fieldname: "opening_amount", fieldtype: "Currency", default: 0, in_list_view: 1, label: "Opening Amount", - options: "company:company_currency", reqd: 1 } + options: "company:company_currency" } ]; const dialog = new frappe.ui.Dialog({ @@ -51,29 +51,16 @@ erpnext.PointOfSale.Controller = class { options: 'POS Profile', fieldname: 'pos_profile', reqd: 1, onchange: () => { const pos_profile = dialog.fields_dict.pos_profile.get_value(); - const company = dialog.fields_dict.company.get_value(); - const user = frappe.session.user - if (!pos_profile || !company || !user) return; + if (!pos_profile) return; - // auto fetch last closing entry's balance details - frappe.db.get_list("POS Closing Entry", { - filters: { company, pos_profile, user }, - limit: 1, - order_by: 'period_end_date desc' - }).then((res) => { - if (!res.length) return; - const pos_closing_entry = res[0]; - frappe.db.get_doc("POS Closing Entry", pos_closing_entry.name).then(({ payment_reconciliation }) => { - dialog.fields_dict.balance_details.df.data = []; - payment_reconciliation.forEach(pay => { - const { mode_of_payment } = pay; - dialog.fields_dict.balance_details.df.data.push({ - mode_of_payment: mode_of_payment - }); - }); - dialog.fields_dict.balance_details.grid.refresh(); + frappe.db.get_doc("POS Profile", pos_profile).then(doc => { + dialog.fields_dict.balance_details.df.data = []; + doc.payments.forEach(pay => { + const { mode_of_payment } = pay; + dialog.fields_dict.balance_details.df.data.push({ mode_of_payment }); }); + dialog.fields_dict.balance_details.grid.refresh(); }); } }, diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index eadeb8fde8..724b60b973 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -1,36 +1,36 @@ erpnext.PointOfSale.ItemCart = class { - constructor({ wrapper, events }) { + constructor({ wrapper, events }) { this.wrapper = wrapper; this.events = events; - this.customer_info = undefined; - - this.init_component(); - } - - init_component() { - this.prepare_dom(); - this.init_child_components(); + this.customer_info = undefined; + + this.init_component(); + } + + init_component() { + this.prepare_dom(); + this.init_child_components(); this.bind_events(); this.attach_shortcuts(); - } + } - prepare_dom() { + prepare_dom() { this.wrapper.append( - `
    ` - ) + `
    ` + ) - this.$component = this.wrapper.find('.item-cart'); - } + this.$component = this.wrapper.find('.item-cart'); + } - init_child_components() { - this.init_customer_selector(); - this.init_cart_components(); - } + init_child_components() { + this.init_customer_selector(); + this.init_cart_components(); + } - init_customer_selector() { + init_customer_selector() { this.$component.append( - `
    ` - ) + `
    ` + ) this.$customer_section = this.$component.find('.customer-section'); } @@ -41,9 +41,9 @@ erpnext.PointOfSale.ItemCart = class { this.make_customer_selector(); this.customer_field.set_focus(); } - - init_cart_components() { - this.$component.append( + + init_cart_components() { + this.$component.append( `
    @@ -55,23 +55,23 @@ erpnext.PointOfSale.ItemCart = class {
    -
    ` - ); +
    ` + ); this.$cart_container = this.$component.find('.cart-container'); this.make_cart_totals_section(); this.make_cart_items_section(); - this.make_cart_numpad(); - } + this.make_cart_numpad(); + } - make_cart_items_section() { - this.$cart_header = this.$component.find('.cart-header'); - this.$cart_items_wrapper = this.$component.find('.cart-items-section'); + make_cart_items_section() { + this.$cart_header = this.$component.find('.cart-header'); + this.$cart_items_wrapper = this.$component.find('.cart-items-section'); this.make_no_items_placeholder(); - } - - make_no_items_placeholder() { + } + + make_no_items_placeholder() { this.$cart_header.addClass('d-none'); this.$cart_items_wrapper.html( `
    @@ -81,8 +81,8 @@ erpnext.PointOfSale.ItemCart = class { this.$cart_items_wrapper.addClass('mt-4 border-grey border-dashed'); } - make_cart_totals_section() { - this.$totals_section = this.$component.find('.cart-totals-section'); + make_cart_totals_section() { + this.$totals_section = this.$component.find('.cart-totals-section'); this.$totals_section.append( `
    @@ -116,9 +116,9 @@ erpnext.PointOfSale.ItemCart = class { ) this.$add_discount_elem = this.$component.find(".add-discount"); - } - - make_cart_numpad() { + } + + make_cart_numpad() { this.$numpad_section = this.$component.find('.numpad-section'); this.number_pad = new erpnext.PointOfSale.NumberPad({ @@ -155,9 +155,9 @@ erpnext.PointOfSale.ItemCart = class { Checkout
    ` ) - } - - bind_events() { + } + + bind_events() { const me = this; this.$customer_section.on('click', '.add-remove-customer', function (e) { const customer_info_is_visible = me.$cart_container.hasClass('d-none'); @@ -381,8 +381,8 @@ erpnext.PointOfSale.ItemCart = class { ` ); } - - update_customer_section() { + + update_customer_section() { const { customer, email_id='', mobile_no='', image } = this.customer_info || {}; if (customer) { @@ -403,7 +403,7 @@ erpnext.PointOfSale.ItemCart = class {
    ` ); } else { - // reset customer selector + // reset customer selector this.reset_customer_selector(); } @@ -430,9 +430,9 @@ erpnext.PointOfSale.ItemCart = class { ` } } - } - - update_totals_section(frm) { + } + + update_totals_section(frm) { if (!frm) frm = this.events.get_frm(); this.render_net_total(frm.doc.base_net_total); @@ -440,9 +440,9 @@ erpnext.PointOfSale.ItemCart = class { const taxes = frm.doc.taxes.map(t => { return { description: t.description, rate: t.rate }}) this.render_taxes(frm.doc.base_total_taxes_and_charges, taxes); - } - - render_net_total(value) { + } + + render_net_total(value) { const currency = this.events.get_frm().doc.currency; this.$totals_section.find('.net-total').html( `
    @@ -454,9 +454,9 @@ erpnext.PointOfSale.ItemCart = class { ) this.$numpad_section.find('.numpad-net-total').html(`Net Total: ${format_currency(value, currency)}`) - } - - render_grand_total(value) { + } + + render_grand_total(value) { const currency = this.events.get_frm().doc.currency; this.$totals_section.find('.grand-total').html( `
    @@ -495,20 +495,20 @@ erpnext.PointOfSale.ItemCart = class { } else { this.$totals_section.find('.taxes').html('') } - } + } - get_cart_item({ item_code, batch_no, uom }) { + get_cart_item({ item_code, batch_no, uom }) { const batch_attr = `[data-batch-no="${escape(batch_no)}"]`; const item_code_attr = `[data-item-code="${escape(item_code)}"]`; const uom_attr = `[data-uom=${escape(uom)}]`; - const item_selector = batch_no ? - `.cart-item-wrapper${batch_attr}${uom_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}`; - - return this.$cart_items_wrapper.find(item_selector); - } - - update_item_html(item, remove_item) { + const item_selector = batch_no ? + `.cart-item-wrapper${batch_attr}${uom_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}`; + + return this.$cart_items_wrapper.find(item_selector); + } + + update_item_html(item, remove_item) { const $item = this.get_cart_item(item); if (remove_item) { @@ -524,33 +524,33 @@ erpnext.PointOfSale.ItemCart = class { const no_of_cart_items = this.$cart_items_wrapper.children().length; no_of_cart_items > 0 && this.highlight_checkout_btn(no_of_cart_items > 0); - + this.update_empty_cart_section(no_of_cart_items); } - - render_cart_item(item_data, $item_to_update) { + + render_cart_item(item_data, $item_to_update) { const currency = this.events.get_frm().doc.currency; const me = this; - if (!$item_to_update.length) { - this.$cart_items_wrapper.append( - `
    -
    ` - ) - $item_to_update = this.get_cart_item(item_data); - } +
    ` + ) + $item_to_update = this.get_cart_item(item_data); + } $item_to_update.html( `
    -
    - ${item_data.item_name} -
    - ${get_description_html()} -
    - ${get_rate_discount_html()} -
    ` +
    + ${item_data.item_name} +
    + ${get_description_html()} + + ${get_rate_discount_html()} + ` ) set_dynamic_rate_header_width(); @@ -572,7 +572,7 @@ erpnext.PointOfSale.ItemCart = class { me.$cart_header.find(".rate-list-header").css("width", max_width); me.$cart_items_wrapper.find(".rate-col").css("width", max_width); } - + function get_rate_discount_html() { if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) { return ` @@ -625,7 +625,7 @@ erpnext.PointOfSale.ItemCart = class { $item_to_update.attr(`data-${selector}`, value); } - toggle_checkout_btn(show_checkout) { + toggle_checkout_btn(show_checkout) { if (show_checkout) { this.$totals_section.find('.checkout-btn').removeClass('d-none'); this.$totals_section.find('.edit-cart-btn').addClass('d-none'); @@ -635,7 +635,7 @@ erpnext.PointOfSale.ItemCart = class { } } - highlight_checkout_btn(toggle) { + highlight_checkout_btn(toggle) { const has_primary_class = this.$totals_section.find('.checkout-btn').hasClass('bg-primary'); if (toggle && !has_primary_class) { this.$totals_section.find('.checkout-btn').addClass('bg-primary text-white text-lg'); @@ -643,8 +643,8 @@ erpnext.PointOfSale.ItemCart = class { this.$totals_section.find('.checkout-btn').removeClass('bg-primary text-white text-lg'); } } - - update_empty_cart_section(no_of_cart_items) { + + update_empty_cart_section(no_of_cart_items) { const $no_item_element = this.$cart_items_wrapper.find('.no-item-wrapper'); // if cart has items and no item is present @@ -652,27 +652,27 @@ erpnext.PointOfSale.ItemCart = class { && this.$cart_items_wrapper.removeClass('mt-4 border-grey border-dashed') && this.$cart_header.removeClass('d-none'); no_of_cart_items === 0 && !$no_item_element.length && this.make_no_items_placeholder(); - } - - on_numpad_event($btn) { + } + + on_numpad_event($btn) { const current_action = $btn.attr('data-button-value'); const action_is_field_edit = ['qty', 'discount_percentage', 'rate'].includes(current_action); this.highlight_numpad_btn($btn, current_action); - const action_is_pressed_twice = this.prev_action === current_action; - const first_click_event = !this.prev_action; - const field_to_edit_changed = this.prev_action && this.prev_action != current_action; + const action_is_pressed_twice = this.prev_action === current_action; + const first_click_event = !this.prev_action; + const field_to_edit_changed = this.prev_action && this.prev_action != current_action; if (action_is_field_edit) { if (first_click_event || field_to_edit_changed) { - this.prev_action = current_action; + this.prev_action = current_action; } else if (action_is_pressed_twice) { this.prev_action = undefined; } - this.numpad_value = ''; - + this.numpad_value = ''; + } else if (current_action === 'checkout') { this.prev_action = undefined; this.toggle_item_highlight(); @@ -688,7 +688,7 @@ erpnext.PointOfSale.ItemCart = class { this.numpad_value = this.numpad_value || 0; } - const first_click_event_is_not_field_edit = !action_is_field_edit && first_click_event; + const first_click_event_is_not_field_edit = !action_is_field_edit && first_click_event; if (first_click_event_is_not_field_edit) { frappe.show_alert({ @@ -708,34 +708,34 @@ erpnext.PointOfSale.ItemCart = class { this.numpad_value = current_action; } - this.events.numpad_event(this.numpad_value, this.prev_action); - } - - highlight_numpad_btn($btn, curr_action) { - const curr_action_is_highlighted = $btn.hasClass('shadow-inner'); - const curr_action_is_action = ['qty', 'discount_percentage', 'rate', 'done'].includes(curr_action); + this.events.numpad_event(this.numpad_value, this.prev_action); + } + + highlight_numpad_btn($btn, curr_action) { + const curr_action_is_highlighted = $btn.hasClass('shadow-inner'); + const curr_action_is_action = ['qty', 'discount_percentage', 'rate', 'done'].includes(curr_action); - if (!curr_action_is_highlighted) { - $btn.addClass('shadow-inner bg-selected'); - } - if (this.prev_action === curr_action && curr_action_is_highlighted) { - // if Qty is pressed twice - $btn.removeClass('shadow-inner bg-selected'); - } - if (this.prev_action && this.prev_action !== curr_action && curr_action_is_action) { - // Order: Qty -> Rate then remove Qty highlight - const prev_btn = $(`[data-button-value='${this.prev_action}']`); - prev_btn.removeClass('shadow-inner bg-selected'); - } - if (!curr_action_is_action || curr_action === 'done') { - // if numbers are clicked - setTimeout(() => { - $btn.removeClass('shadow-inner bg-selected'); - }, 100); - } - } + if (!curr_action_is_highlighted) { + $btn.addClass('shadow-inner bg-selected'); + } + if (this.prev_action === curr_action && curr_action_is_highlighted) { + // if Qty is pressed twice + $btn.removeClass('shadow-inner bg-selected'); + } + if (this.prev_action && this.prev_action !== curr_action && curr_action_is_action) { + // Order: Qty -> Rate then remove Qty highlight + const prev_btn = $(`[data-button-value='${this.prev_action}']`); + prev_btn.removeClass('shadow-inner bg-selected'); + } + if (!curr_action_is_action || curr_action === 'done') { + // if numbers are clicked + setTimeout(() => { + $btn.removeClass('shadow-inner bg-selected'); + }, 100); + } + } - toggle_numpad(show) { + toggle_numpad(show) { if (show) { this.$totals_section.addClass('d-none'); this.$numpad_section.removeClass('d-none'); @@ -946,6 +946,6 @@ erpnext.PointOfSale.ItemCart = class { toggle_component(show) { show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none'); - } - + } + } diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index 86a1be9faf..3a5f89ba93 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -1,28 +1,28 @@ erpnext.PointOfSale.ItemDetails = class { - constructor({ wrapper, events }) { + constructor({ wrapper, events }) { this.wrapper = wrapper; - this.events = events; - this.current_item = {}; + this.events = events; + this.current_item = {}; - this.init_component(); - } + this.init_component(); + } - init_component() { - this.prepare_dom(); - this.init_child_components(); + init_component() { + this.prepare_dom(); + this.init_child_components(); this.bind_events(); this.attach_shortcuts(); - } + } - prepare_dom() { - this.wrapper.append( - `
    ` - ) + prepare_dom() { + this.wrapper.append( + `
    ` + ) - this.$component = this.wrapper.find('.item-details'); - } + this.$component = this.wrapper.find('.item-details'); + } - init_child_components() { + init_child_components() { this.$component.html( `
    @@ -49,28 +49,28 @@ erpnext.PointOfSale.ItemDetails = class { this.$item_image = this.$component.find('.item-image'); this.$form_container = this.$component.find('.form-container'); this.$dicount_section = this.$component.find('.discount-section'); - } + } - toggle_item_details_section(item) { + toggle_item_details_section(item) { const { item_code, batch_no, uom } = this.current_item; const item_code_is_same = item && item_code === item.item_code; const batch_is_same = item && batch_no == item.batch_no; const uom_is_same = item && uom === item.uom; - this.item_has_changed = !item ? false : item_code_is_same && batch_is_same && uom_is_same ? false : true; + this.item_has_changed = !item ? false : item_code_is_same && batch_is_same && uom_is_same ? false : true; - this.events.toggle_item_selector(this.item_has_changed); + this.events.toggle_item_selector(this.item_has_changed); this.toggle_component(this.item_has_changed); - + if (this.item_has_changed) { - this.doctype = item.doctype; + this.doctype = item.doctype; this.item_meta = frappe.get_meta(this.doctype); this.name = item.name; this.item_row = item; - this.currency = this.events.get_frm().doc.currency; - - this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom }; - + this.currency = this.events.get_frm().doc.currency; + + this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom }; + this.render_dom(item); this.render_discount_dom(item); this.render_form(item); @@ -102,9 +102,9 @@ erpnext.PointOfSale.ItemDetails = class { this.events.remove_item_from_cart(); } } - - render_dom(item) { - let { item_code ,item_name, description, image, price_list_rate } = item; + + render_dom(item) { + let { item_code ,item_name, description, image, price_list_rate } = item; function get_description_html() { if (description) { @@ -112,8 +112,8 @@ erpnext.PointOfSale.ItemDetails = class { return description; } return ``; - } - + } + this.$item_name.html(item_name); this.$item_description.html(get_description_html()); this.$item_price.html(format_currency(price_list_rate, this.currency)); @@ -125,9 +125,9 @@ erpnext.PointOfSale.ItemDetails = class { this.$item_image.html(frappe.get_abbr(item_code)); } - } - - render_discount_dom(item) { + } + + render_discount_dom(item) { if (item.discount_percentage) { this.$dicount_section.html( `
    @@ -141,9 +141,9 @@ erpnext.PointOfSale.ItemDetails = class { } else { this.$dicount_section.html(``) } - } + } - render_form(item) { + render_form(item) { const fields_to_display = this.get_form_fields(item); this.$form_container.html(''); @@ -157,7 +157,7 @@ erpnext.PointOfSale.ItemDetails = class { const field_meta = this.item_meta.fields.find(df => df.fieldname === fieldname); fieldname === 'discount_percentage' ? (field_meta.label = __('Discount (%)')) : ''; const me = this; - + this[`${fieldname}_control`] = frappe.ui.form.make_control({ df: { ...field_meta, @@ -174,16 +174,16 @@ erpnext.PointOfSale.ItemDetails = class { this.make_auto_serial_selection_btn(item); this.bind_custom_control_change_event(); - } + } - get_form_fields(item) { + get_form_fields(item) { const fields = ['qty', 'uom', 'rate', 'price_list_rate', 'discount_percentage', 'warehouse', 'actual_qty']; if (item.has_serial_no) fields.push('serial_no'); if (item.has_batch_no) fields.push('batch_no'); return fields; } - make_auto_serial_selection_btn(item) { + make_auto_serial_selection_btn(item) { if (item.has_serial_no) { this.$form_container.append( `
    ` @@ -203,8 +203,8 @@ erpnext.PointOfSale.ItemDetails = class { this.$form_container.find('.serial_no-control').parent().addClass('row-span-2'); } } - - bind_custom_control_change_event() { + + bind_custom_control_change_event() { const me = this; if (this.rate_control) { this.rate_control.df.onchange = function() { @@ -276,8 +276,8 @@ erpnext.PointOfSale.ItemDetails = class { }; this.batch_no_control.df.onchange = function() { me.events.set_value_in_current_cart_item('batch-no', this.value); - me.events.form_updated(me.doctype, me.name, 'batch_no', this.value); - me.current_item.batch_no = this.value; + me.events.form_updated(me.doctype, me.name, 'batch_no', this.value); + me.current_item.batch_no = this.value; } this.batch_no_control.refresh(); } @@ -289,9 +289,9 @@ erpnext.PointOfSale.ItemDetails = class { me.current_item.uom = this.value; } } - } - - async auto_update_batch_no() { + } + + async auto_update_batch_no() { if (this.serial_no_control && this.batch_no_control) { const selected_serial_nos = this.serial_no_control.get_value().split(`\n`).filter(s => s); if (!selected_serial_nos.length) return; @@ -310,9 +310,9 @@ erpnext.PointOfSale.ItemDetails = class { const batch_no = Object.keys(batch_serial_map)[0]; const batch_serial_nos = batch_serial_map[batch_no].join(`\n`); // eg. 10 selected serial no. -> 5 belongs to first batch other 5 belongs to second batch - const serial_nos_belongs_to_other_batch = selected_serial_nos.length !== batch_serial_map[batch_no].length; - - const current_batch_no = this.batch_no_control.get_value(); + const serial_nos_belongs_to_other_batch = selected_serial_nos.length !== batch_serial_map[batch_no].length; + + const current_batch_no = this.batch_no_control.get_value(); current_batch_no != batch_no && await this.batch_no_control.set_value(batch_no); if (serial_nos_belongs_to_other_batch) { @@ -326,8 +326,8 @@ erpnext.PointOfSale.ItemDetails = class { this.events.clone_new_batch_item_in_frm(batch_serial_map, this.current_item); } } - - bind_events() { + + bind_events() { this.bind_auto_serial_fetch_event(); this.bind_fields_to_numpad_fields(); @@ -345,7 +345,7 @@ erpnext.PointOfSale.ItemDetails = class { }); } - bind_fields_to_numpad_fields() { + bind_fields_to_numpad_fields() { const me = this; this.$form_container.on('click', '.input-with-feedback', function() { const fieldname = $(this).attr('data-fieldname'); @@ -355,8 +355,8 @@ erpnext.PointOfSale.ItemDetails = class { } }); } - - bind_auto_serial_fetch_event() { + + bind_auto_serial_fetch_event() { this.$form_container.on('click', '.auto-fetch-btn', () => { this.batch_no_control.set_value(''); let qty = this.qty_control.get_value(); @@ -382,7 +382,7 @@ erpnext.PointOfSale.ItemDetails = class { frappe.msgprint(`Fetched only ${records_length} available serial numbers.`); this.qty_control.set_value(records_length); } - numbers = auto_fetched_serial_numbers.join(`\n`); + numbers = auto_fetched_serial_numbers.join(`\n`); this.serial_no_control.set_value(numbers); }); }) @@ -390,5 +390,5 @@ erpnext.PointOfSale.ItemDetails = class { toggle_component(show) { show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none'); - } + } } \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index ee0c06d45d..c87b845a41 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -1,115 +1,115 @@ erpnext.PointOfSale.ItemSelector = class { - constructor({ frm, wrapper, events, pos_profile }) { + constructor({ frm, wrapper, events, pos_profile }) { this.wrapper = wrapper; this.events = events; - this.pos_profile = pos_profile; - - this.inti_component(); - } - - inti_component() { - this.prepare_dom(); - this.make_search_bar(); - this.load_items_data(); - this.bind_events(); - this.attach_shortcuts(); - } + this.pos_profile = pos_profile; + + this.inti_component(); + } + + inti_component() { + this.prepare_dom(); + this.make_search_bar(); + this.load_items_data(); + this.bind_events(); + this.attach_shortcuts(); + } - prepare_dom() { + prepare_dom() { this.wrapper.append( - `
    -
    -
    -
    -
    -
    -
    -
    ALL ITEMS
    -
    -
    -
    -
    -
    ` - ); - - this.$component = this.wrapper.find('.items-selector'); - } + `
    +
    +
    +
    +
    +
    +
    +
    ALL ITEMS
    +
    +
    +
    +
    +
    ` + ); + + this.$component = this.wrapper.find('.items-selector'); + } - async load_items_data() { - if (!this.item_group) { - const res = await frappe.db.get_value("Item Group", {lft: 1, is_group: 1}, "name"); - this.parent_item_group = res.message.name; - }; - if (!this.price_list) { - const res = await frappe.db.get_value("POS Profile", this.pos_profile, "selling_price_list"); - this.price_list = res.message.selling_price_list; - } + async load_items_data() { + if (!this.item_group) { + const res = await frappe.db.get_value("Item Group", {lft: 1, is_group: 1}, "name"); + this.parent_item_group = res.message.name; + }; + if (!this.price_list) { + const res = await frappe.db.get_value("POS Profile", this.pos_profile, "selling_price_list"); + this.price_list = res.message.selling_price_list; + } - this.get_items({}).then(({message}) => { - this.render_item_list(message.items); - }); - } + this.get_items({}).then(({message}) => { + this.render_item_list(message.items); + }); + } - get_items({start = 0, page_length = 40, search_value=''}) { - const price_list = this.events.get_frm().doc?.selling_price_list || this.price_list; - let { item_group, pos_profile } = this; + get_items({start = 0, page_length = 40, search_value=''}) { + const price_list = this.events.get_frm().doc?.selling_price_list || this.price_list; + let { item_group, pos_profile } = this; - !item_group && (item_group = this.parent_item_group); - + !item_group && (item_group = this.parent_item_group); + return frappe.call({ method: "erpnext.selling.page.point_of_sale.point_of_sale.get_items", freeze: true, - args: { start, page_length, price_list, item_group, search_value, pos_profile }, - }); + args: { start, page_length, price_list, item_group, search_value, pos_profile }, + }); } render_item_list(items) { - this.$items_container = this.$component.find('.items-container'); - this.$items_container.html(''); + this.$items_container = this.$component.find('.items-container'); + this.$items_container.html(''); - items.forEach(item => { - const item_html = this.get_item_html(item); - this.$items_container.append(item_html); - }) - } + items.forEach(item => { + const item_html = this.get_item_html(item); + this.$items_container.append(item_html); + }) + } - get_item_html(item) { - const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom } = item; - const indicator_color = actual_qty > 10 ? "green" : actual_qty !== 0 ? "orange" : "red"; + get_item_html(item) { + const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom } = item; + const indicator_color = actual_qty > 10 ? "green" : actual_qty !== 0 ? "orange" : "red"; - function get_item_image_html() { - if (item_image) { - return `
    - ${item_image} -
    ` - } else { - return `
    - ${frappe.get_abbr(item.item_name)} -
    ` - } - } + function get_item_image_html() { + if (item_image) { + return `
    + ${item_image} +
    ` + } else { + return `
    + ${frappe.get_abbr(item.item_name)} +
    ` + } + } return ( - `
    - ${get_item_image_html()} -
    -
    - - ${frappe.ellipsis(item.item_name, 18)} -
    -
    ${format_currency(item.price_list_rate, item.currency, 0) || 0}
    -
    -
    ` - ) - } + `
    + ${get_item_image_html()} +
    +
    + + ${frappe.ellipsis(item.item_name, 18)} +
    +
    ${format_currency(item.price_list_rate, item.currency, 0) || 0}
    +
    +
    ` + ) + } - make_search_bar() { - const me = this; - this.$component.find('.search-field').html(''); - this.$component.find('.item-group-field').html(''); + make_search_bar() { + const me = this; + this.$component.find('.search-field').html(''); + this.$component.find('.item-group-field').html(''); this.search_field = frappe.ui.form.make_control({ df: { @@ -119,104 +119,104 @@ erpnext.PointOfSale.ItemSelector = class { }, parent: this.$component.find('.search-field'), render_input: true, - }); + }); this.item_group_field = frappe.ui.form.make_control({ df: { label: __('Item Group'), fieldtype: 'Link', options: 'Item Group', - placeholder: __('Select item group'), - onchange: function() { - me.item_group = this.value; - !me.item_group && (me.item_group = me.parent_item_group); - me.filter_items(); - }, - get_query: function () { - return { - query: 'erpnext.selling.page.point_of_sale.point_of_sale.item_group_query', - filters: { - pos_profile: me.events.get_frm().doc?.pos_profile - } - } - }, + placeholder: __('Select item group'), + onchange: function() { + me.item_group = this.value; + !me.item_group && (me.item_group = me.parent_item_group); + me.filter_items(); + }, + get_query: function () { + return { + query: 'erpnext.selling.page.point_of_sale.point_of_sale.item_group_query', + filters: { + pos_profile: me.events.get_frm().doc?.pos_profile + } + } + }, }, - parent: this.$component.find('.item-group-field'), + parent: this.$component.find('.item-group-field'), render_input: true, - }); - this.search_field.toggle_label(false); + }); + this.search_field.toggle_label(false); this.item_group_field.toggle_label(false); } - bind_events() { - const me = this; - onScan.attachTo(document, { - onScan: (sScancode) => { - if (this.search_field && this.$component.is(':visible')) { - this.search_field.set_focus(); - $(this.search_field.$input[0]).val(sScancode).trigger("input"); - this.barcode_scanned = true; - } - } - }); + bind_events() { + const me = this; + onScan.attachTo(document, { + onScan: (sScancode) => { + if (this.search_field && this.$component.is(':visible')) { + this.search_field.set_focus(); + $(this.search_field.$input[0]).val(sScancode).trigger("input"); + this.barcode_scanned = true; + } + } + }); this.$component.on('click', '.item-wrapper', function() { const $item = $(this); const item_code = unescape($item.attr('data-item-code')); - let batch_no = unescape($item.attr('data-batch-no')); - let serial_no = unescape($item.attr('data-serial-no')); - let uom = unescape($item.attr('data-uom')); - - // escape(undefined) returns "undefined" then unescape returns "undefined" - batch_no = batch_no === "undefined" ? undefined : batch_no; - serial_no = serial_no === "undefined" ? undefined : serial_no; - uom = uom === "undefined" ? undefined : uom; + let batch_no = unescape($item.attr('data-batch-no')); + let serial_no = unescape($item.attr('data-serial-no')); + let uom = unescape($item.attr('data-uom')); + + // escape(undefined) returns "undefined" then unescape returns "undefined" + batch_no = batch_no === "undefined" ? undefined : batch_no; + serial_no = serial_no === "undefined" ? undefined : serial_no; + uom = uom === "undefined" ? undefined : uom; - me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom }}); - }) + me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom }}); + }) - this.search_field.$input.on('input', (e) => { + this.search_field.$input.on('input', (e) => { clearTimeout(this.last_search); this.last_search = setTimeout(() => { const search_term = e.target.value; this.filter_items({ search_term }); }, 300); - }); - } + }); + } - attach_shortcuts() { - frappe.ui.keys.on("ctrl+i", () => { - const selector_is_visible = this.$component.is(':visible'); - if (!selector_is_visible) return; - this.search_field.set_focus(); - }); - frappe.ui.keys.on("ctrl+g", () => { - const selector_is_visible = this.$component.is(':visible'); - if (!selector_is_visible) return; - this.item_group_field.set_focus(); - }); - // for selecting the last filtered item on search - frappe.ui.keys.on("enter", () => { - const selector_is_visible = this.$component.is(':visible'); - if (!selector_is_visible || this.search_field.get_value() === "") return; + attach_shortcuts() { + frappe.ui.keys.on("ctrl+i", () => { + const selector_is_visible = this.$component.is(':visible'); + if (!selector_is_visible) return; + this.search_field.set_focus(); + }); + frappe.ui.keys.on("ctrl+g", () => { + const selector_is_visible = this.$component.is(':visible'); + if (!selector_is_visible) return; + this.item_group_field.set_focus(); + }); + // for selecting the last filtered item on search + frappe.ui.keys.on("enter", () => { + const selector_is_visible = this.$component.is(':visible'); + if (!selector_is_visible || this.search_field.get_value() === "") return; - if (this.items.length == 1) { - this.$items_container.find(".item-wrapper").click(); - frappe.utils.play_sound("submit"); - $(this.search_field.$input[0]).val("").trigger("input"); - } else if (this.items.length == 0 && this.barcode_scanned) { - // only show alert of barcode is scanned and enter is pressed - frappe.show_alert({ - message: __("No items found. Scan barcode again."), - indicator: 'orange' - }); - frappe.utils.play_sound("error"); - this.barcode_scanned = false; - $(this.search_field.$input[0]).val("").trigger("input"); - } - }); - } - - filter_items({ search_term='' }={}) { + if (this.items.length == 1) { + this.$items_container.find(".item-wrapper").click(); + frappe.utils.play_sound("submit"); + $(this.search_field.$input[0]).val("").trigger("input"); + } else if (this.items.length == 0 && this.barcode_scanned) { + // only show alert of barcode is scanned and enter is pressed + frappe.show_alert({ + message: __("No items found. Scan barcode again."), + indicator: 'orange' + }); + frappe.utils.play_sound("error"); + this.barcode_scanned = false; + $(this.search_field.$input[0]).val("").trigger("input"); + } + }); + } + + filter_items({ search_term='' }={}) { if (search_term) { search_term = search_term.toLowerCase(); @@ -227,39 +227,39 @@ erpnext.PointOfSale.ItemSelector = class { this.items = items; this.render_item_list(items); return; - } + } } this.get_items({ search_value: search_term }) - .then(({ message }) => { - const { items, serial_no, batch_no, barcode } = message; + .then(({ message }) => { + const { items, serial_no, batch_no, barcode } = message; if (search_term && !barcode) { this.search_index[search_term] = items; } this.items = items; - this.render_item_list(items); - }); + this.render_item_list(items); + }); } - - resize_selector(minimize) { - minimize ? - this.$component.find('.search-field').removeClass('mr-8') : - this.$component.find('.search-field').addClass('mr-8'); + + resize_selector(minimize) { + minimize ? + this.$component.find('.search-field').removeClass('mr-8') : + this.$component.find('.search-field').addClass('mr-8'); - minimize ? - this.$component.find('.filter-section').addClass('flex-col') : - this.$component.find('.filter-section').removeClass('flex-col'); + minimize ? + this.$component.find('.filter-section').addClass('flex-col') : + this.$component.find('.filter-section').removeClass('flex-col'); - minimize ? - this.$component.removeClass('col-span-6').addClass('col-span-2') : - this.$component.removeClass('col-span-2').addClass('col-span-6') + minimize ? + this.$component.removeClass('col-span-6').addClass('col-span-2') : + this.$component.removeClass('col-span-2').addClass('col-span-6') - minimize ? - this.$items_container.removeClass('grid-cols-4').addClass('grid-cols-1') : - this.$items_container.removeClass('grid-cols-1').addClass('grid-cols-4') - } + minimize ? + this.$items_container.removeClass('grid-cols-4').addClass('grid-cols-1') : + this.$items_container.removeClass('grid-cols-1').addClass('grid-cols-4') + } - toggle_component(show) { + toggle_component(show) { show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none'); - } + } } \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/pos_number_pad.js b/erpnext/selling/page/point_of_sale/pos_number_pad.js index 2ffc2c0229..4b8e841805 100644 --- a/erpnext/selling/page/point_of_sale/pos_number_pad.js +++ b/erpnext/selling/page/point_of_sale/pos_number_pad.js @@ -1,49 +1,48 @@ erpnext.PointOfSale.NumberPad = class { - constructor({ wrapper, events, cols, keys, css_classes, fieldnames_map }) { - this.wrapper = wrapper; - this.events = events; - this.cols = cols; - this.keys = keys; - this.css_classes = css_classes || []; - this.fieldnames = fieldnames_map || {}; + constructor({ wrapper, events, cols, keys, css_classes, fieldnames_map }) { + this.wrapper = wrapper; + this.events = events; + this.cols = cols; + this.keys = keys; + this.css_classes = css_classes || []; + this.fieldnames = fieldnames_map || {}; - this.init_component(); - } + this.init_component(); + } - init_component() { - this.prepare_dom(); - this.bind_events(); - } + init_component() { + this.prepare_dom(); + this.bind_events(); + } - prepare_dom() { - const { cols, keys, css_classes, fieldnames } = this; + prepare_dom() { + const { cols, keys, css_classes, fieldnames } = this; - function get_keys() { - return keys.reduce((a, row, i) => { - return a + row.reduce((a2, number, j) => { - const class_to_append = css_classes && css_classes[i] ? css_classes[i][j] : ''; - const fieldname = fieldnames && fieldnames[number] ? - fieldnames[number] : - typeof number === 'string' ? frappe.scrub(number) : number; - - return a2 + `
    ${number}
    ` - }, '') - }, ''); - } + function get_keys() { + return keys.reduce((a, row, i) => { + return a + row.reduce((a2, number, j) => { + const class_to_append = css_classes && css_classes[i] ? css_classes[i][j] : ''; + const fieldname = fieldnames && fieldnames[number] ? + fieldnames[number] : typeof number === 'string' ? frappe.scrub(number) : number; - this.wrapper.html( - `
    - ${get_keys()} -
    ` - ) - } + return a2 + `
    ${number}
    ` + }, '') + }, ''); + } - bind_events() { - const me = this; - this.wrapper.on('click', '.numpad-btn', function() { - const $btn = $(this); - me.events.numpad_event($btn); - }) - } + this.wrapper.html( + `
    + ${get_keys()} +
    ` + ) + } + + bind_events() { + const me = this; + this.wrapper.on('click', '.numpad-btn', function() { + const $btn = $(this); + me.events.numpad_event($btn); + }); + } } \ No newline at end of file From 0a688005be30440ad0be86ca80911b3807281f33 Mon Sep 17 00:00:00 2001 From: marination Date: Sat, 12 Sep 2020 15:39:27 +0530 Subject: [PATCH 171/175] fix: Make sure Supplier/Customer is selected before fetching Items. --- erpnext/stock/doctype/delivery_note/delivery_note.js | 8 +++++++- .../stock/doctype/purchase_receipt/purchase_receipt.js | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 60f6a68f49..19d0bec26c 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -130,12 +130,18 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( if (this.frm.doc.docstatus===0) { this.frm.add_custom_button(__('Sales Order'), function() { + if (!me.frm.doc.customer) { + frappe.throw({ + title: __("Mandatory"), + message: __("Please Select a Customer") + }); + } erpnext.utils.map_current_doc({ method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note", source_doctype: "Sales Order", target: me.frm, setters: { - customer: me.frm.doc.customer || undefined, + customer: me.frm.doc.customer, }, get_query_filters: { docstatus: 1, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 50c18f6282..c504e23677 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -116,12 +116,18 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend if (this.frm.doc.docstatus == 0) { this.frm.add_custom_button(__('Purchase Order'), function () { + if (!me.frm.doc.supplier) { + frappe.throw({ + title: __("Mandatory"), + message: __("Please Select a Supplier") + }); + } erpnext.utils.map_current_doc({ method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt", source_doctype: "Purchase Order", target: me.frm, setters: { - supplier: me.frm.doc.supplier || undefined, + supplier: me.frm.doc.supplier, }, get_query_filters: { docstatus: 1, From 9205101ecffbfa358fcd9a0eefa2a297c3da29ce Mon Sep 17 00:00:00 2001 From: marination Date: Sat, 12 Sep 2020 16:17:13 +0530 Subject: [PATCH 172/175] fix: Make Reference fields mandatory in Quality Inspection --- .../doctype/quality_inspection/quality_inspection.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json index c951066aa8..3643174fb4 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json @@ -73,7 +73,8 @@ "fieldname": "reference_type", "fieldtype": "Select", "label": "Reference Type", - "options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry" + "options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry", + "reqd": 1 }, { "fieldname": "reference_name", @@ -84,7 +85,8 @@ "label": "Reference Name", "oldfieldname": "purchase_receipt_no", "oldfieldtype": "Link", - "options": "reference_type" + "options": "reference_type", + "reqd": 1 }, { "fieldname": "section_break_7", @@ -231,9 +233,10 @@ ], "icon": "fa fa-search", "idx": 1, + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-04-26 17:50:25.068222", + "modified": "2020-09-12 16:11:31.910508", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection", From 6db92fbb6a38b40818022c607a02ccf1f30e92f0 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 14 Sep 2020 19:54:17 +0530 Subject: [PATCH 173/175] feat: validate workflow before so/po update items (#23024) * feat: validate workflow before so/po update items * fix: incorrect workflow validation on so/po update items Co-authored-by: Marica --- erpnext/controllers/accounts_controller.py | 50 ++++++++++---- .../doctype/sales_order/test_sales_order.py | 69 +++++++++++++++++++ 2 files changed, 106 insertions(+), 13 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index d61e44b53d..5244777558 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -7,6 +7,7 @@ import json from frappe import _, throw from frappe.utils import (today, flt, cint, fmt_money, formatdate, getdate, add_days, add_months, get_last_day, nowdate, get_link_to_form) +from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied, WorkflowPermissionError from erpnext.stock.get_item_details import get_conversion_factor, get_item_details from erpnext.setup.utils import get_exchange_rate from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_account_currency @@ -1194,7 +1195,7 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna child_item.base_amount = 1 # Initiallize value will update in parent validation return child_item -def check_and_delete_children(parent, data): +def validate_and_delete_children(parent, data): deleted_children = [] updated_item_names = [d.get("docname") for d in data] for item in parent.items: @@ -1221,18 +1222,37 @@ def check_and_delete_children(parent, data): @frappe.whitelist() def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): - def check_permissions(doc, perm_type='create'): + def check_doc_permissions(doc, perm_type='create'): try: doc.check_permission(perm_type) - except: - action = "add" if perm_type == 'create' else "update" - frappe.throw(_("You do not have permissions to {} items in a Sales Order.").format(action), title=_("Insufficient Permissions")) + except frappe.PermissionError: + actions = { 'create': 'add', 'write': 'update', 'cancel': 'remove' } + + frappe.throw(_("You do not have permissions to {} items in a {}.") + .format(actions[perm_type], parent_doctype), title=_("Insufficient Permissions")) + + def validate_workflow_conditions(doc): + workflow = get_workflow_name(doc.doctype) + if not workflow: + return + + workflow_doc = frappe.get_doc("Workflow", workflow) + current_state = doc.get(workflow_doc.workflow_state_field) + roles = frappe.get_roles() + + transitions = [] + for transition in workflow_doc.transitions: + if transition.next_state == current_state and transition.allowed in roles: + if not is_transition_condition_satisfied(transition, doc): + continue + transitions.append(transition.as_dict()) + + if not transitions: + frappe.throw(_("You do not have workflow access to update this document."), title=_("Insufficient Workflow Permissions")) def get_new_child_item(item_row): - if parent_doctype == "Sales Order": - return set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row) - if parent_doctype == "Purchase Order": - return set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row) + new_child_function = set_sales_order_defaults if parent_doctype == "Sales Order" else set_purchase_order_defaults + return new_child_function(parent_doctype, parent_doctype_name, child_docname, item_row) def validate_quantity(child_item, d): if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty): @@ -1245,17 +1265,18 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation'] parent = frappe.get_doc(parent_doctype, parent_doctype_name) - - check_and_delete_children(parent, data) + + check_doc_permissions(parent, 'cancel') + validate_and_delete_children(parent, data) for d in data: new_child_flag = False if not d.get("docname"): new_child_flag = True - check_permissions(parent, 'create') + check_doc_permissions(parent, 'create') child_item = get_new_child_item(d) else: - check_permissions(parent, 'write') + check_doc_permissions(parent, 'write') child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname")) prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate")) @@ -1362,6 +1383,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.update_prevdoc_status('submit') parent.update_delivery_status() + parent.reload() + validate_workflow_conditions(parent) + parent.update_blanket_order() parent.update_billing_percentage() parent.set_status() diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index accaa9c3b3..34aaf08395 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -417,7 +417,42 @@ class TestSalesOrder(unittest.TestCase): # add new item trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 100, 'qty' : 2}]) self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) + test_user.remove_roles("Accounts User") frappe.set_user("Administrator") + + def test_update_child_qty_rate_with_workflow(self): + from frappe.model.workflow import apply_workflow + + workflow = make_sales_order_workflow() + so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1) + apply_workflow(so, 'Approve') + + frappe.set_user("Administrator") + user = 'test@example.com' + test_user = frappe.get_doc('User', user) + test_user.add_roles("Sales User", "Test Junior Approver") + frappe.set_user(user) + + # user shouldn't be able to edit since grand_total will become > 200 if qty is doubled + trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 150, 'qty' : 2, 'docname': so.items[0].name}]) + self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Sales Order', trans_item, so.name) + + frappe.set_user("Administrator") + user2 = 'test2@example.com' + test_user2 = frappe.get_doc('User', user2) + test_user2.add_roles("Sales User", "Test Approver") + frappe.set_user(user2) + + # Test Approver is allowed to edit with grand_total > 200 + update_child_qty_rate("Sales Order", trans_item, so.name) + so.reload() + self.assertEqual(so.items[0].qty, 2) + + frappe.set_user("Administrator") + test_user.remove_roles("Sales User", "Test Junior Approver", "Test Approver") + test_user2.remove_roles("Sales User", "Test Junior Approver", "Test Approver") + workflow.is_active = 0 + workflow.save() def test_update_child_qty_rate_product_bundle(self): # test Update Items with product bundle @@ -973,3 +1008,37 @@ def get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"): "reserved_qty")) test_dependencies = ["Currency Exchange"] + +def make_sales_order_workflow(): + if frappe.db.exists('Workflow', 'SO Test Workflow'): + doc = frappe.get_doc("Workflow", "SO Test Workflow") + doc.set("is_active", 1) + doc.save() + return doc + + frappe.get_doc(dict(doctype='Role', role_name='Test Junior Approver')).insert(ignore_if_duplicate=True) + frappe.get_doc(dict(doctype='Role', role_name='Test Approver')).insert(ignore_if_duplicate=True) + frappe.db.commit() + frappe.cache().hdel('roles', frappe.session.user) + + workflow = frappe.get_doc({ + "doctype": "Workflow", + "workflow_name": "SO Test Workflow", + "document_type": "Sales Order", + "workflow_state_field": "workflow_state", + "is_active": 1, + "send_email_alert": 0, + }) + workflow.append('states', dict( state = 'Pending', allow_edit = 'All' )) + workflow.append('states', dict( state = 'Approved', allow_edit = 'Test Approver', doc_status = 1 )) + workflow.append('transitions', dict( + state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Junior Approver', allow_self_approval = 1, + condition = 'doc.grand_total < 200' + )) + workflow.append('transitions', dict( + state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Approver', allow_self_approval = 1, + condition = 'doc.grand_total > 200' + )) + workflow.insert(ignore_permissions=True) + + return workflow \ No newline at end of file From fcb6ba6ec7fb245f201a22374d1c4a0c8227905e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 14 Sep 2020 21:12:36 +0530 Subject: [PATCH 174/175] fix: get_transaction_entries function arguments in Bank Statement Transaction Entry (#23051) --- .../bank_statement_transaction_entry.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py b/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py index 5b6eb9dc24..27dd8e463f 100644 --- a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py +++ b/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py @@ -55,7 +55,7 @@ class BankStatementTransactionEntry(Document): def populate_payment_entries(self): if self.bank_statement is None: return - filename = self.bank_statement.split("/")[-1] + file_url = self.bank_statement if (len(self.new_transaction_items + self.reconciled_transaction_items) > 0): frappe.throw(_("Transactions already retreived from the statement")) @@ -65,7 +65,7 @@ class BankStatementTransactionEntry(Document): if self.bank_settings: mapped_items = frappe.get_doc("Bank Statement Settings", self.bank_settings).mapped_items statement_headers = self.get_statement_headers() - transactions = get_transaction_entries(filename, statement_headers) + transactions = get_transaction_entries(file_url, statement_headers) for entry in transactions: date = entry[statement_headers["Date"]].strip() #print("Processing entry DESC:{0}-W:{1}-D:{2}-DT:{3}".format(entry["Particulars"], entry["Withdrawals"], entry["Deposits"], entry["Date"])) @@ -398,20 +398,21 @@ def get_transaction_info(headers, header_index, row): transaction[header] = "" return transaction -def get_transaction_entries(filename, headers): +def get_transaction_entries(file_url, headers): header_index = {} rows, transactions = [], [] - if (filename.lower().endswith("xlsx")): + if (file_url.lower().endswith("xlsx")): from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file - rows = read_xlsx_file_from_attached_file(file_id=filename) - elif (filename.lower().endswith("csv")): + rows = read_xlsx_file_from_attached_file(file_url=file_url) + elif (file_url.lower().endswith("csv")): from frappe.utils.csvutils import read_csv_content - _file = frappe.get_doc("File", {"file_name": filename}) + _file = frappe.get_doc("File", {"file_url": file_url}) filepath = _file.get_full_path() with open(filepath,'rb') as csvfile: rows = read_csv_content(csvfile.read()) - elif (filename.lower().endswith("xls")): + elif (file_url.lower().endswith("xls")): + filename = file_url.split("/")[-1] rows = get_rows_from_xls_file(filename) else: frappe.throw(_("Only .csv and .xlsx files are supported currently")) From 36bc0577a2a0c11fa10d669d76172cbb62a8ae90 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 15 Sep 2020 09:44:46 +0530 Subject: [PATCH 175/175] fix: Item GL entries for purchase invoice (#23027) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index b4ee7c999e..079f599706 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -405,8 +405,6 @@ class PurchaseInvoice(BuyingController): update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) def make_gl_entries(self, gl_entries=None): - if not self.grand_total: - return if not gl_entries: gl_entries = self.get_gl_entries()