From 8179804405f6955ba35487a08516ce9486f9be3c Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 14 Apr 2020 23:05:11 +0530 Subject: [PATCH 01/82] refactor: Quoted Item Comparison Report - Refactored Report View - Added Chart - Added 'valid_till' date in Supplier Quotation - Added patch to set valid_till and daily job to set expiry status --- .../supplier_quotation/supplier_quotation.js | 4 + .../supplier_quotation.json | 13 +- .../supplier_quotation/supplier_quotation.py | 16 +- .../quoted_item_comparison.js | 91 ++++++++-- .../quoted_item_comparison.py | 161 ++++++++++-------- erpnext/hooks.py | 3 +- erpnext/patches.txt | 1 + ...t_valid_till_date_in_supplier_quotation.py | 8 + 8 files changed, 207 insertions(+), 90 deletions(-) create mode 100644 erpnext/patches/v12_0/set_valid_till_date_in_supplier_quotation.py diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js index 16061c61ba..1b8b40459f 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js @@ -18,6 +18,10 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext refresh: function() { var me = this; this._super(); + + if (this.frm.doc.__islocal && !this.frm.doc.valid_till) { + this.frm.set_value('valid_till', frappe.datetime.add_months(this.frm.doc.transaction_date, 1)); + } if (this.frm.doc.docstatus === 1) { cur_frm.add_custom_button(__("Purchase Order"), this.make_purchase_order, __('Create')); diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index 82fc6285bc..6964e783ee 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -13,9 +13,10 @@ "supplier", "supplier_name", "column_break1", - "transaction_date", - "amended_from", "company", + "transaction_date", + "valid_till", + "amended_from", "address_section", "supplier_address", "contact_person", @@ -791,13 +792,19 @@ "options": "Opportunity", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "valid_till", + "fieldtype": "Date", + "label": "Valid Till", + "reqd": 1 } ], "icon": "fa fa-shopping-cart", "idx": 29, "is_submittable": 1, "links": [], - "modified": "2019-12-30 19:17:28.208693", + "modified": "2020-04-14 22:43:32.248415", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index 5b4356a747..baf245735a 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import flt, nowdate, add_days +from frappe.utils import flt, nowdate, add_days, getdate from frappe.model.mapper import get_mapped_doc from erpnext.controllers.buying_controller import BuyingController @@ -28,6 +28,7 @@ class SupplierQuotation(BuyingController): validate_for_items(self) self.validate_with_previous_doc() self.validate_uom_is_integer("uom", "qty") + self.validate_valid_till() def on_submit(self): frappe.db.set(self, "status", "Submitted") @@ -52,6 +53,11 @@ class SupplierQuotation(BuyingController): "is_child_table": True } }) + + def validate_valid_till(self): + if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date): + frappe.throw(_("Valid till Date cannot be before Transaction Date")) + def update_rfq_supplier_status(self, include_me): rfq_list = set([]) for item in self.items: @@ -158,3 +164,11 @@ def make_quotation(source_name, target_doc=None): }, target_doc) return doclist + +def set_expired_status(): + frappe.db.sql(""" + UPDATE + `tabSupplier Quotation` SET `status` = 'Expired' + WHERE + `status` not in ('Cancelled', 'Stopped') AND `valid_till` < %s + """, (nowdate())) \ No newline at end of file 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 3d05612c9e..f331beb49e 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js @@ -5,13 +5,11 @@ frappe.query_reports["Quoted Item Comparison"] = { filters: [ { fieldtype: "Link", - label: __("Supplier Quotation"), - options: "Supplier Quotation", - fieldname: "supplier_quotation", - default: "", - get_query: () => { - return { filters: { "docstatus": ["<", 2] } } - } + label: __("Company"), + options: "Company", + fieldname: "company", + default: frappe.defaults.get_user_default("Company"), + "reqd": 1 }, { reqd: 1, @@ -37,8 +35,83 @@ frappe.query_reports["Quoted Item Comparison"] = { } } } + }, + { + fieldtype: "Link", + label: __("Supplier Quotation"), + options: "Supplier Quotation", + fieldname: "supplier_quotation", + default: "", + get_query: () => { + return { filters: { "docstatus": ["<", 2] } } + } + }, + { + fieldtype: "Link", + label: __("Request for Quotation"), + options: "Request for Quotation", + fieldname: "request_for_quotation", + default: "", + get_query: () => { + return { filters: { "docstatus": ["<", 2] } } + } } ], + + prepare_chart_data: (result) => { + let supplier_wise_map = {}, data_points_map = {}; + let qty_list = result.map(res=> res.qty); + qty_list = new Set(qty_list); + + // create supplier wise map like in Report + for(let res of result){ + if(!(res.supplier in supplier_wise_map)){ + supplier_wise_map[res.supplier]= {}; + } + supplier_wise_map[res.supplier][res.qty] = res.price; + } + + // create datapoints for each qty + for(let supplier of Object.keys(supplier_wise_map)) { + let row = supplier_wise_map[supplier]; + for(let qty of qty_list){ + if(!data_points_map[qty]){ + data_points_map[qty] = [] + } + if(row[qty]){ + data_points_map[qty].push(row[qty]); + } + else{ + data_points_map[qty].push(null); + } + } + } + + let dataset = []; + qty_list.forEach((qty) => { + let datapoints = { + 'name': 'Price for Qty ' + qty, + 'values': data_points_map[qty] + } + dataset.push(datapoints); + + }); + return dataset; + }, + + get_chart_data: function (columns, result) { + let suppliers = result.filter(d => d.supplier_name).map(res => res.supplier_name); + let dataset = frappe.query_reports["Quoted Item Comparison"].prepare_chart_data(result); + + return { + data: { + labels: suppliers, + datasets: dataset + }, + type: 'bar' + } + }, + onload: (report) => { // Create a button for setting the default supplier report.page.add_inner_button(__("Select Default Supplier"), () => { @@ -102,6 +175,4 @@ frappe.query_reports["Quoted Item Comparison"] = { }); dialog.show(); } -} - - +} \ No newline at end of file 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 5aff6bacae..bb1067a05a 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py @@ -2,103 +2,114 @@ # For license information, please see license.txt from __future__ import unicode_literals -from erpnext.setup.utils import get_exchange_rate -from frappe.utils import flt, cint import frappe +from frappe.utils import flt, cint +from collections import defaultdict +from erpnext.setup.utils import get_exchange_rate def execute(filters=None): - qty_list = get_quantity_list(filters.item) - data = get_quote_list(filters.item, qty_list) - columns = get_columns(qty_list) + conditions = get_conditions(filters) + data = get_data(filters, conditions) + columns = get_columns() return columns, data - -def get_quote_list(item, qty_list): - out = [] + +def get_data(filters, conditions): + out, suppliers = [], [] + item = filters.get("item") + if not item: return [] - suppliers = [] - price_data = [] company_currency = frappe.db.get_default("currency") - float_precision = cint(frappe.db.get_default("float_precision")) or 2 - # Get the list of suppliers - for root in frappe.db.sql("""select parent, qty, rate from `tabSupplier Quotation Item` - where item_code=%s and docstatus < 2""", item, as_dict=1): - for splr in frappe.db.sql("""select supplier from `tabSupplier Quotation` - where name =%s and docstatus < 2""", root.parent, as_dict=1): - ip = frappe._dict({ - "supplier": splr.supplier, - "qty": root.qty, - "parent": root.parent, - "rate": root.rate - }) - price_data.append(ip) - suppliers.append(splr.supplier) + float_precision = cint(frappe.db.get_default("float_precision")) or 2 - #Add a row for each supplier - for root in set(suppliers): - supplier_currency = frappe.db.get_value("Supplier", root, "default_currency") + supplier_quotation_data = frappe.db.sql("""SELECT + sqi.parent, sqi.qty, sqi.rate, sqi.uom, sqi.request_for_quotation, + sq.supplier + FROM + `tabSupplier Quotation Item` sqi, + `tabSupplier Quotation` sq + WHERE + sqi.item_code = '{0}' + AND sqi.parent = sq.name + AND sqi.docstatus < 2 + AND sq.company = '{1}' + AND sq.status != 'Expired' + {2}""".format(item, filters.get("company"), conditions), as_dict=1) + + supplier_wise_map = defaultdict(list) + + for data in supplier_quotation_data: + supplier_currency = frappe.db.get_value("Supplier", data.get("supplier"), "default_currency") if supplier_currency: exchange_rate = get_exchange_rate(supplier_currency, company_currency) else: exchange_rate = 1 - row = frappe._dict({ - "supplier_name": root - }) - for col in qty_list: - # Get the quantity for this row - for item_price in price_data: - if str(item_price.qty) == col.key and item_price.supplier == root: - row[col.key] = flt(item_price.rate * exchange_rate, float_precision) - row[col.key + "QUOTE"] = item_price.parent - break - else: - row[col.key] = "" - row[col.key + "QUOTE"] = "" - out.append(row) - - return out - -def get_quantity_list(item): - out = [] - - if item: - qty_list = frappe.db.sql("""select distinct qty from `tabSupplier Quotation Item` - where ifnull(item_code,'')=%s and docstatus < 2 order by qty""", item, as_dict=1) + row = { + "quotation": data.get("parent"), + "qty": data.get("qty"), + "price": flt(data.get("rate") * exchange_rate, float_precision), + "request_for_quotation": data.get("request_for_quotation"), + "supplier": data.get("supplier") # used for chart generation + } - for qt in qty_list: - col = frappe._dict({ - "key": str(qt.qty), - "label": "Qty: " + str(int(qt.qty)) - }) - out.append(col) + supplier_wise_map[data.supplier].append(row) + suppliers.append(data.supplier) + + suppliers = set(suppliers) + + for supplier in suppliers: + supplier_wise_map[supplier][0].update({"supplier_name": supplier}) + for entry in supplier_wise_map[supplier]: + out.append(entry) return out - -def get_columns(qty_list): + +def get_conditions(filters): + conditions = "" + + if filters.get("request_for_quotation"): + conditions += " AND sqi.request_for_quotation = '{0}' ".format(filters.get("request_for_quotation")) + + return conditions + + +def get_columns(): columns = [{ "fieldname": "supplier_name", "label": "Supplier", "fieldtype": "Link", "options": "Supplier", "width": 200 - }] - - for qty in qty_list: - columns.append({ - "fieldname": qty.key, - "label": qty.label, - "fieldtype": "Currency", - "options": "currency", - "width": 80 - }) - columns.append({ - "fieldname": qty.key + "QUOTE", - "label": "Quotation", - "fieldtype": "Link", - "options": "Supplier Quotation", - "width": 90 - }) + }, + { + "fieldname": "quotation", + "label": "Supplier Quotation", + "fieldtype": "Link", + "options": "Supplier Quotation", + "width": 200 + }, + { + "fieldname": "qty", + "label": "Quantity", + "fieldtype": "Float", + "width": 80 + }, + { + "fieldname": "price", + "label": "Price", + "fieldtype": "Currency", + "options": "Company:company:default_currency", + "width": 110 + }, + { + "fieldname": "request_for_quotation", + "label": "Request for Quotation", + "fieldtype": "Link", + "options": "Request for Quotation", + "width": 200 + } + ] return columns \ No newline at end of file diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 6199cb2264..a1f47b8f9c 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -304,7 +304,8 @@ scheduler_events = { "erpnext.support.doctype.service_level_agreement.service_level_agreement.check_agreement_status", "erpnext.crm.doctype.email_campaign.email_campaign.send_email_to_leads_or_contacts", "erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status", - "erpnext.selling.doctype.quotation.quotation.set_expired_status" + "erpnext.selling.doctype.quotation.quotation.set_expired_status", + "erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status" ], "daily_long": [ "erpnext.setup.doctype.email_digest.email_digest.send", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9b5e5d02fb..a2a393b96a 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -666,3 +666,4 @@ erpnext.patches.v12_0.recalculate_requested_qty_in_bin erpnext.patches.v12_0.set_total_batch_quantity erpnext.patches.v12_0.rename_mws_settings_fields erpnext.patches.v12_0.set_updated_purpose_in_pick_list +erpnext.patches.v12_0.set_valid_till_date_in_supplier_quotation \ No newline at end of file diff --git a/erpnext/patches/v12_0/set_valid_till_date_in_supplier_quotation.py b/erpnext/patches/v12_0/set_valid_till_date_in_supplier_quotation.py new file mode 100644 index 0000000000..0f24ec6756 --- /dev/null +++ b/erpnext/patches/v12_0/set_valid_till_date_in_supplier_quotation.py @@ -0,0 +1,8 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + reload_doc("buying", "doctype", "suppplier_quotation") + frappe.db.sql("""UPDATE `tabSupplier Quotation` + SET valid_till = DATE_ADD(transaction_date , INTERVAL 1 MONTH) + WHERE docstatus < 2""") \ No newline at end of file From 8890b6044d27381a0e9b83614fce6a7db53093f3 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 15 Apr 2020 12:18:10 +0530 Subject: [PATCH 02/82] fix: Patch fix, Travis fix and cleanup - Added UOM column in Report - Removed mandatory on `valid_till` - Added list view indicator for Expired status in Supplier Quotation - Sorted Labels in Chart and syntax cleanup - Made labels Translatable - Fixed patch --- .../supplier_quotation.json | 5 ++-- .../supplier_quotation_list.js | 2 ++ .../quoted_item_comparison.js | 24 +++++++++---------- .../quoted_item_comparison.py | 19 +++++++++++---- ...t_valid_till_date_in_supplier_quotation.py | 2 +- 5 files changed, 31 insertions(+), 21 deletions(-) diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index 6964e783ee..3bc441af6d 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -796,15 +796,14 @@ { "fieldname": "valid_till", "fieldtype": "Date", - "label": "Valid Till", - "reqd": 1 + "label": "Valid Till" } ], "icon": "fa fa-shopping-cart", "idx": 29, "is_submittable": 1, "links": [], - "modified": "2020-04-14 22:43:32.248415", + "modified": "2020-04-15 11:44:52.958022", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js index 95554397bb..9f4fecea86 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js @@ -5,6 +5,8 @@ frappe.listview_settings['Supplier Quotation'] = { return [__("Ordered"), "green", "status,=,Ordered"]; } else if(doc.status==="Rejected") { return [__("Lost"), "darkgrey", "status,=,Lost"]; + } else if(doc.status==="Expired") { + return [__("Expired"), "darkgrey", "status,=,Expired"]; } } }; 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 f331beb49e..fe4abd8c9c 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js @@ -60,28 +60,29 @@ frappe.query_reports["Quoted Item Comparison"] = { prepare_chart_data: (result) => { let supplier_wise_map = {}, data_points_map = {}; - let qty_list = result.map(res=> res.qty); + let qty_list = result.map(res => res.qty); + qty_list.sort(); qty_list = new Set(qty_list); // create supplier wise map like in Report - for(let res of result){ - if(!(res.supplier in supplier_wise_map)){ - supplier_wise_map[res.supplier]= {}; + for (let res of result) { + if (!(res.supplier in supplier_wise_map)) { + supplier_wise_map[res.supplier] = {}; } supplier_wise_map[res.supplier][res.qty] = res.price; } // create datapoints for each qty - for(let supplier of Object.keys(supplier_wise_map)) { + for (let supplier of Object.keys(supplier_wise_map)) { let row = supplier_wise_map[supplier]; - for(let qty of qty_list){ - if(!data_points_map[qty]){ - data_points_map[qty] = [] + for (let qty of qty_list) { + if (!data_points_map[qty]) { + data_points_map[qty] = []; } - if(row[qty]){ + if (row[qty]) { data_points_map[qty].push(row[qty]); } - else{ + else { data_points_map[qty].push(null); } } @@ -90,11 +91,10 @@ frappe.query_reports["Quoted Item Comparison"] = { let dataset = []; qty_list.forEach((qty) => { let datapoints = { - 'name': 'Price for Qty ' + qty, + 'name': __('Price for Qty ') + qty, 'values': data_points_map[qty] } dataset.push(datapoints); - }); return dataset; }, 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 bb1067a05a..fd7a731198 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe from frappe.utils import flt, cint +from frappe import _ from collections import defaultdict from erpnext.setup.utils import get_exchange_rate @@ -50,6 +51,7 @@ def get_data(filters, conditions): "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"), "supplier": data.get("supplier") # used for chart generation } @@ -78,34 +80,41 @@ def get_conditions(filters): def get_columns(): columns = [{ "fieldname": "supplier_name", - "label": "Supplier", + "label": _("Supplier"), "fieldtype": "Link", "options": "Supplier", "width": 200 }, { "fieldname": "quotation", - "label": "Supplier Quotation", + "label": _("Supplier Quotation"), "fieldtype": "Link", "options": "Supplier Quotation", "width": 200 }, { "fieldname": "qty", - "label": "Quantity", + "label": _("Quantity"), "fieldtype": "Float", "width": 80 }, { "fieldname": "price", - "label": "Price", + "label": _("Price"), "fieldtype": "Currency", "options": "Company:company:default_currency", "width": 110 }, + { + "fieldname": "uom", + "label": _("UOM"), + "fieldtype": "Link", + "options": "UOM", + "width": 90 + }, { "fieldname": "request_for_quotation", - "label": "Request for Quotation", + "label": _("Request for Quotation"), "fieldtype": "Link", "options": "Request for Quotation", "width": 200 diff --git a/erpnext/patches/v12_0/set_valid_till_date_in_supplier_quotation.py b/erpnext/patches/v12_0/set_valid_till_date_in_supplier_quotation.py index 0f24ec6756..befa46c31d 100644 --- a/erpnext/patches/v12_0/set_valid_till_date_in_supplier_quotation.py +++ b/erpnext/patches/v12_0/set_valid_till_date_in_supplier_quotation.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import frappe def execute(): - reload_doc("buying", "doctype", "suppplier_quotation") + frappe.reload_doc("buying", "doctype", "suppplier_quotation") frappe.db.sql("""UPDATE `tabSupplier Quotation` SET valid_till = DATE_ADD(transaction_date , INTERVAL 1 MONTH) WHERE docstatus < 2""") \ No newline at end of file From 10d9974e7721257fa1b1b87a6cb0506db9b01c0b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 30 Apr 2020 16:28:52 +0530 Subject: [PATCH 03/82] chore: add total row in sales analytics report --- .../sales_analytics/sales_analytics.json | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.json b/erpnext/selling/report/sales_analytics/sales_analytics.json index 71932610a6..cbc5999248 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.json +++ b/erpnext/selling/report/sales_analytics/sales_analytics.json @@ -1,31 +1,31 @@ { - "add_total_row": 0, - "creation": "2018-09-21 12:46:29.451048", - "disable_prepared_report": 0, - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "modified": "2019-05-24 05:37:02.866139", - "modified_by": "Administrator", - "module": "Selling", - "name": "Sales Analytics", - "owner": "Administrator", - "prepared_report": 0, - "ref_doctype": "Sales Order", - "report_name": "Sales Analytics", - "report_type": "Script Report", + "add_total_row": 1, + "creation": "2018-09-21 12:46:29.451048", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-04-30 16:27:53.112907", + "modified_by": "Administrator", + "module": "Selling", + "name": "Sales Analytics", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Sales Order", + "report_name": "Sales Analytics", + "report_type": "Script Report", "roles": [ { "role": "Stock User" - }, + }, { "role": "Maintenance User" - }, + }, { "role": "Accounts User" - }, + }, { "role": "Sales Manager" } From dfa60e0ed8f428a4bd789a6055b645eab7c57ff8 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 30 Apr 2020 20:06:30 +0530 Subject: [PATCH 04/82] chore: calculate total row month-wise in sales analytics --- .../report/sales_analytics/sales_analytics.json | 4 ++-- .../report/sales_analytics/sales_analytics.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.json b/erpnext/selling/report/sales_analytics/sales_analytics.json index cbc5999248..bf9edd6cd4 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.json +++ b/erpnext/selling/report/sales_analytics/sales_analytics.json @@ -1,5 +1,5 @@ { - "add_total_row": 1, + "add_total_row": 0, "creation": "2018-09-21 12:46:29.451048", "disable_prepared_report": 0, "disabled": 0, @@ -7,7 +7,7 @@ "doctype": "Report", "idx": 0, "is_standard": "Yes", - "modified": "2020-04-30 16:27:53.112907", + "modified": "2020-04-30 19:49:02.303320", "modified_by": "Administrator", "module": "Selling", "name": "Sales Analytics", diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index f1726ab8bf..3fc4633ae3 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -194,6 +194,9 @@ class Analytics(object): def get_rows(self): self.data = [] self.get_periodic_data() + total_row = { + "entity": "Total", + } for entity, period_data in iteritems(self.entity_periodic_data): row = { @@ -207,6 +210,9 @@ class Analytics(object): row[scrub(period)] = amount total += amount + if not total_row.get(scrub(period)): total_row[scrub(period)] = 0 + total_row[scrub(period)] += amount + row["total"] = total if self.filters.tree_type == "Item": @@ -214,9 +220,14 @@ class Analytics(object): self.data.append(row) + self.data.append(total_row) + def get_rows_by_group(self): self.get_periodic_data() out = [] + total_row = { + "entity": "Total", + } for d in reversed(self.group_entries): row = { @@ -232,8 +243,14 @@ class Analytics(object): self.entity_periodic_data.setdefault(d.parent, frappe._dict()).setdefault(period, 0.0) self.entity_periodic_data[d.parent][period] += amount total += amount + + if not total_row.get(scrub(period)): total_row[scrub(period)] = 0 + total_row[scrub(period)] += amount + row["total"] = total out = [row] + out + + out.append(total_row) self.data = out def get_periodic_data(self): From 971170c59025cfebd38c7bb7a2bd9633064625d8 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 2 May 2020 19:39:03 +0530 Subject: [PATCH 05/82] fix: incorrect total in sales analytics for customer/item group --- erpnext/selling/report/sales_analytics/sales_analytics.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index 3fc4633ae3..c0c11339ca 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -225,9 +225,6 @@ class Analytics(object): def get_rows_by_group(self): self.get_periodic_data() out = [] - total_row = { - "entity": "Total", - } for d in reversed(self.group_entries): row = { @@ -245,12 +242,10 @@ class Analytics(object): total += amount if not total_row.get(scrub(period)): total_row[scrub(period)] = 0 - total_row[scrub(period)] += amount row["total"] = total out = [row] + out - out.append(total_row) self.data = out def get_periodic_data(self): From 826b39559db854df6a6c22447b7aacdb379055bc Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 2 May 2020 19:50:45 +0530 Subject: [PATCH 06/82] fix: review fixes --- erpnext/selling/report/sales_analytics/sales_analytics.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index c0c11339ca..97d9322918 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -241,8 +241,6 @@ class Analytics(object): self.entity_periodic_data[d.parent][period] += amount total += amount - if not total_row.get(scrub(period)): total_row[scrub(period)] = 0 - row["total"] = total out = [row] + out From ab9bb08c4b100fc4abf2361968c9706cb03781da Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sun, 3 May 2020 14:53:13 +0530 Subject: [PATCH 07/82] fix: test --- .../report/sales_analytics/test_analytics.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/erpnext/selling/report/sales_analytics/test_analytics.py b/erpnext/selling/report/sales_analytics/test_analytics.py index 4d81a1e4dd..0a5198ccc5 100644 --- a/erpnext/selling/report/sales_analytics/test_analytics.py +++ b/erpnext/selling/report/sales_analytics/test_analytics.py @@ -83,6 +83,22 @@ class TestAnalytics(unittest.TestCase): "feb_2018": 0.0, "mar_2018": 0.0, "total": 3000.0 + }, + { + "entity": "Total", + "entity_name": "", + "apr_2017": 0.0, + "may_2017": 0.0, + "jun_2017": 2000.0, + "jul_2017": 1000.0, + "aug_2017": 0.0, + "sep_2017": 1500.0, + "oct_2017": 1000.0, + "nov_2017": 0.0, + "dec_2017": 0.0, + "jan_2018": 0.0, + "feb_2018": 0.0, + "mar_2018": 0.0 } ] result = sorted(report[1], key=lambda k: k['entity']) From 52cc6ad456940798be40923e216898e1b3a6082e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 4 May 2020 20:24:00 +0530 Subject: [PATCH 08/82] fix: test --- .../report/sales_analytics/test_analytics.py | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/erpnext/selling/report/sales_analytics/test_analytics.py b/erpnext/selling/report/sales_analytics/test_analytics.py index 0a5198ccc5..7e8501dcc1 100644 --- a/erpnext/selling/report/sales_analytics/test_analytics.py +++ b/erpnext/selling/report/sales_analytics/test_analytics.py @@ -33,6 +33,21 @@ class TestAnalytics(unittest.TestCase): report = execute(filters) expected_data = [ + { + 'entity': 'Total', + 'apr_2017': 0.0, + 'may_2017': 0.0, + 'jun_2017': 2000.0, + 'jul_2017': 1000.0, + 'aug_2017': 0.0, + 'sep_2017': 1500.0, + 'oct_2017': 1000.0, + 'nov_2017': 0.0, + 'dec_2017': 0.0, + 'jan_2018': 0.0, + 'feb_2018': 2000.0, + 'mar_2018': 0.0 + }, { "entity": "_Test Customer 1", "entity_name": "_Test Customer 1", @@ -83,22 +98,6 @@ class TestAnalytics(unittest.TestCase): "feb_2018": 0.0, "mar_2018": 0.0, "total": 3000.0 - }, - { - "entity": "Total", - "entity_name": "", - "apr_2017": 0.0, - "may_2017": 0.0, - "jun_2017": 2000.0, - "jul_2017": 1000.0, - "aug_2017": 0.0, - "sep_2017": 1500.0, - "oct_2017": 1000.0, - "nov_2017": 0.0, - "dec_2017": 0.0, - "jan_2018": 0.0, - "feb_2018": 0.0, - "mar_2018": 0.0 } ] result = sorted(report[1], key=lambda k: k['entity']) @@ -150,6 +149,21 @@ class TestAnalytics(unittest.TestCase): report = execute(filters) expected_data = [ + { + 'entity': 'Total', + 'apr_2017': 0.0, + 'may_2017': 0.0, + 'jun_2017': 20.0, + 'jul_2017': 10.0, + 'aug_2017': 0.0, + 'sep_2017': 15.0, + 'oct_2017': 10.0, + 'nov_2017': 0.0, + 'dec_2017': 0.0, + 'jan_2018': 0.0, + 'feb_2018': 20.0, + 'mar_2018': 0.0 + }, { "entity": "_Test Customer 1", "entity_name": "_Test Customer 1", From d097eaef6304a247f7f7681e79acd38bc5ce3349 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Tue, 5 May 2020 15:57:49 +0530 Subject: [PATCH 09/82] Appending Email and Phone in Child Table --- erpnext/selling/doctype/customer/customer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 50e719f02e..d0db6d62a0 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -164,6 +164,8 @@ class Customer(TransactionBase): contact.phone = lead.phone contact.mobile_no = lead.mobile_no contact.is_primary_contact = 1 + contact.append('email_ids', dict(email_id=lead.email_id, is_primary=1)) + contact.append('phone_nos', dict(phone=lead.mobile_no, is_primary_mobile_no=1)) contact.append('links', dict(link_doctype='Customer', link_name=self.name)) contact.flags.ignore_permissions = self.flags.ignore_permissions contact.autoname() From 6e95d248e35abeebeae65c65fa788afa1e1bc925 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 5 May 2020 16:15:07 +0530 Subject: [PATCH 10/82] fix: work order operation completed qty --- erpnext/manufacturing/doctype/job_card/job_card.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index e9627a5514..e43b98aee1 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -206,30 +206,31 @@ 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" 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, "operation": self.operation}) + "workstation": self.workstation, field: self.get(field)}) if data and len(data) > 0: for_quantity = data[0].completed_qty time_in_mins = data[0].time_in_mins - if for_quantity: + if self.get(field): time_data = frappe.db.sql(""" SELECT min(from_time) as start_time, max(to_time) as end_time 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.operation = %s and jc.docstatus = 1 - """, (self.work_order, self.workstation, self.operation), as_dict=1) + 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) 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.workstation == self.workstation and data.operation == self.operation: + if data.get(work_order_field) == self.get(field) and data.workstation == self.workstation: 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 d078883cd436ae878f6b38607f1f3db693f62d04 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Wed, 6 May 2020 01:13:51 +0530 Subject: [PATCH 11/82] fixing callback urls --- .../linkedin_settings/linkedin_settings.js | 2 ++ .../linkedin_settings/linkedin_settings.py | 6 +++--- .../twitter_settings/twitter_settings.js | 2 ++ .../twitter_settings/twitter_settings.py | 19 +++++++++++-------- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js index 50b98e9ce1..263005ef6c 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js @@ -62,6 +62,8 @@ frappe.ui.form.on('LinkedIn Settings', { callback : function(r) { window.location.href = r.message; } + }).fail(function() { + frappe.dom.unfreeze(); }); } }, diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py index 5df35df3dd..bdde9eed37 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py @@ -15,7 +15,7 @@ class LinkedInSettings(Document): params = urlencode({ "response_type":"code", "client_id": self.consumer_key, - "redirect_uri": get_site_url(frappe.local.site) + "/?cmd=erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback", + "redirect_uri": get_site_url(frappe.local.site) + "/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?", "scope": "r_emailaddress w_organization_social r_basicprofile r_liteprofile r_organization_social rw_organization_admin w_member_social" }) @@ -30,7 +30,7 @@ class LinkedInSettings(Document): "code": code, "client_id": self.consumer_key, "client_secret": self.get_password(fieldname="consumer_secret"), - "redirect_uri": get_site_url(frappe.local.site) + "/?cmd=erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback", + "redirect_uri": get_site_url(frappe.local.site) + "/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?", } headers = { "Content-Type": "application/x-www-form-urlencoded" @@ -154,7 +154,7 @@ class LinkedInSettings(Document): return response -@frappe.whitelist() +@frappe.whitelist(allow_guest=True) def callback(code=None, error=None, error_description=None): if not error: linkedin_settings = frappe.get_doc("LinkedIn Settings") diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.js b/erpnext/crm/doctype/twitter_settings/twitter_settings.js index b55946a8bd..f6f431ca5c 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.js +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.js @@ -47,6 +47,8 @@ frappe.ui.form.on('Twitter Settings', { callback : function(r) { window.location.href = r.message; } + }).fail(function() { + frappe.dom.unfreeze(); }); } }, diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py index 64f53b5eb0..7616b4c027 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.py +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py @@ -12,13 +12,12 @@ from tweepy.error import TweepError class TwitterSettings(Document): def get_authorize_url(self): - callback_url = "{0}/?cmd=erpnext.crm.doctype.twitter_settings.twitter_settings.callback".format(frappe.utils.get_url()) + callback_url = "{0}/api/method/erpnext.crm.doctype.twitter_settings.twitter_settings.callback?".format(frappe.utils.get_url()) auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url) - try: redirect_url = auth.get_authorization_url() return redirect_url - except: + except tweepy.TweepError as e: frappe.msgprint(_("Error! Failed to get request token.")) frappe.throw(_('Invalid {0} or {1}').format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key"))) @@ -91,8 +90,12 @@ class TwitterSettings(Document): frappe.db.commit() frappe.throw(content["message"],title="Twitter Error {0} {1}".format(e.response.status_code, e.response.reason)) -@frappe.whitelist() -def callback(oauth_token, oauth_verifier): - twitter_settings = frappe.get_single("Twitter Settings") - twitter_settings.get_access_token(oauth_token,oauth_verifier) - frappe.db.commit() +@frappe.whitelist(allow_guest=True) +def callback(oauth_token = None, oauth_verifier = None): + if oauth_token and oauth_verifier: + twitter_settings = frappe.get_single("Twitter Settings") + twitter_settings.get_access_token(oauth_token,oauth_verifier) + frappe.db.commit() + else: + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url_to_form("Twitter Settings","Twitter Settings") From 3e0c51879e8a17a8be8ce622e9fbbce93eeab2b8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Wed, 6 May 2020 10:46:13 +0530 Subject: [PATCH 12/82] fix: Set Purchase Receipt and Delivery Note detail patch (#21607) * fix: Set Purchase Receipt and Delievry Note detail patch * fix: Reload purchase receipt item --- .../v12_0/set_purchase_receipt_delivery_note_detail.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py b/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py index f5bd8c3aa2..6f843cdabd 100644 --- a/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py +++ b/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py @@ -3,6 +3,10 @@ import frappe from collections import defaultdict def execute(): + + frappe.reload_doc('stock', 'doctype', 'delivery_note_item', force=True) + frappe.reload_doc('stock', 'doctype', 'purchase_receipt_item', force=True) + def map_rows(doc_row, return_doc_row, detail_field, doctype): """Map rows after identifying similar ones.""" From b2103f547073b8441cfbaec4861c4fab74a187d2 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 6 May 2020 15:18:43 +0530 Subject: [PATCH 13/82] feat: remove old fixtures --- .../account_balance_timeline.py | 2 +- erpnext/patches.txt | 1 - .../patches/v12_0/add_default_dashboards.py | 10 -- .../setup_wizard/data/dashboard_charts.py | 133 ------------------ .../operations/install_fixtures.py | 23 --- 5 files changed, 1 insertion(+), 168 deletions(-) delete mode 100644 erpnext/patches/v12_0/add_default_dashboards.py delete mode 100644 erpnext/setup/setup_wizard/data/dashboard_charts.py diff --git a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py index c3e2f7db12..5decccb486 100644 --- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py +++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py @@ -6,7 +6,7 @@ import frappe, json from frappe import _ from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate, get_link_to_form from erpnext.accounts.report.general_ledger.general_ledger import execute -from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan +from frappe.utils.dashboard import cache_source, get_from_date_from_timespan from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_period_ending from frappe.utils.nestedset import get_descendants_of diff --git a/erpnext/patches.txt b/erpnext/patches.txt index ba17b67de1..15235f15a4 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -630,7 +630,6 @@ execute:frappe.reload_doc('desk', 'doctype', 'dashboard') 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.add_default_dashboards # 2020-04-05 erpnext.patches.v12_0.remove_bank_remittance_custom_fields erpnext.patches.v12_0.generate_leave_ledger_entries execute:frappe.delete_doc_if_exists("Report", "Loan Repayment") diff --git a/erpnext/patches/v12_0/add_default_dashboards.py b/erpnext/patches/v12_0/add_default_dashboards.py deleted file mode 100644 index 2a91e1b932..0000000000 --- a/erpnext/patches/v12_0/add_default_dashboards.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2019, Frappe and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe -from erpnext.setup.setup_wizard.operations.install_fixtures import add_dashboards - -def execute(): - frappe.reload_doc("desk", "doctype", "number_card_link") - frappe.reload_doc("healthcare", "doctype", "patient_appointment") - add_dashboards() diff --git a/erpnext/setup/setup_wizard/data/dashboard_charts.py b/erpnext/setup/setup_wizard/data/dashboard_charts.py deleted file mode 100644 index b182dfc103..0000000000 --- a/erpnext/setup/setup_wizard/data/dashboard_charts.py +++ /dev/null @@ -1,133 +0,0 @@ -from __future__ import unicode_literals -from frappe import _ -import frappe -import json - -def get_company_for_dashboards(): - company = frappe.defaults.get_defaults().company - if company: - return company - else: - company_list = frappe.get_list("Company") - if company_list: - return company_list[0].name - return None - -def get_default_dashboards(): - company = frappe.get_doc("Company", get_company_for_dashboards()) - income_account = company.default_income_account or get_account("Income Account", company.name) - expense_account = company.default_expense_account or get_account("Expense Account", company.name) - bank_account = company.default_bank_account or get_account("Bank", company.name) - - return { - "Dashboards": [ - { - "doctype": "Dashboard", - "dashboard_name": "Accounts", - "charts": [ - { "chart": "Outgoing Bills (Sales Invoice)" }, - { "chart": "Incoming Bills (Purchase Invoice)" }, - { "chart": "Bank Balance" }, - { "chart": "Income" }, - { "chart": "Expenses" }, - { "chart": "Patient Appointments" } - ] - } - ], - "Charts": [ - { - "doctype": "Dashboard Chart", - "time_interval": "Quarterly", - "chart_name": "Income", - "timespan": "Last Year", - "color": None, - "filters_json": json.dumps({"company": company.name, "account": income_account}), - "source": "Account Balance Timeline", - "chart_type": "Custom", - "timeseries": 1, - "owner": "Administrator", - "type": "Line", - "width": "Half" - }, - { - "doctype": "Dashboard Chart", - "time_interval": "Quarterly", - "chart_name": "Expenses", - "timespan": "Last Year", - "color": None, - "filters_json": json.dumps({"company": company.name, "account": expense_account}), - "source": "Account Balance Timeline", - "chart_type": "Custom", - "timeseries": 1, - "owner": "Administrator", - "type": "Line", - "width": "Half" - }, - { - "doctype": "Dashboard Chart", - "time_interval": "Quarterly", - "chart_name": "Bank Balance", - "timespan": "Last Year", - "color": "#ffb868", - "filters_json": json.dumps({"company": company.name, "account": bank_account}), - "source": "Account Balance Timeline", - "chart_type": "Custom", - "timeseries": 1, - "owner": "Administrator", - "type": "Line", - "width": "Half" - }, - { - "doctype": "Dashboard Chart", - "time_interval": "Monthly", - "chart_name": "Incoming Bills (Purchase Invoice)", - "timespan": "Last Year", - "color": "#a83333", - "value_based_on": "base_grand_total", - "filters_json": json.dumps({}), - "chart_type": "Sum", - "timeseries": 1, - "based_on": "posting_date", - "owner": "Administrator", - "document_type": "Purchase Invoice", - "type": "Bar", - "width": "Half" - }, - { - "doctype": "Dashboard Chart", - "time_interval": "Monthly", - "chart_name": "Outgoing Bills (Sales Invoice)", - "timespan": "Last Year", - "color": "#7b933d", - "value_based_on": "base_grand_total", - "filters_json": json.dumps({}), - "chart_type": "Sum", - "timeseries": 1, - "based_on": "posting_date", - "owner": "Administrator", - "document_type": "Sales Invoice", - "type": "Bar", - "width": "Half" - }, - { - "doctype": "Dashboard Chart", - "time_interval": "Daily", - "chart_name": "Patient Appointments", - "timespan": "Last Month", - "color": "#77ecca", - "filters_json": json.dumps({}), - "chart_type": "Count", - "timeseries": 1, - "based_on": "appointment_datetime", - "owner": "Administrator", - "document_type": "Patient Appointment", - "type": "Line", - "width": "Half" - } - ] - } - -def get_account(account_type, company): - accounts = frappe.get_list("Account", filters={"account_type": account_type, "company": company}) - if accounts: - return accounts[0].name diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 3be6f44832..8bb0a0529d 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -485,8 +485,6 @@ def install_defaults(args=None): # bank account same as a CoA entry pass - add_dashboards() - # Now, with fixtures out of the way, onto concrete stuff records = [ @@ -504,27 +502,6 @@ def install_defaults(args=None): make_records(records) -def add_dashboards(): - from erpnext.setup.setup_wizard.data.dashboard_charts import get_company_for_dashboards - - if not get_company_for_dashboards(): - return - - from erpnext.setup.setup_wizard.data.dashboard_charts import get_default_dashboards - from frappe.modules.import_file import import_file_by_path - - dashboard_data = get_default_dashboards() - - # create account balance timeline before creating dashbaord charts - doctype = "dashboard_chart_source" - docname = "account_balance_timeline" - folder = os.path.dirname(frappe.get_module("erpnext.accounts").__file__) - doc_path = os.path.join(folder, doctype, docname, docname) + ".json" - import_file_by_path(doc_path, force=0, for_sync=True) - - make_records(dashboard_data["Charts"]) - make_records(dashboard_data["Dashboards"]) - def get_fy_details(fy_start_date, fy_end_date): start_year = getdate(fy_start_date).year From 28436d2bc104a7b01dd9f3311ed9ff1a93aebc9a Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 6 May 2020 15:19:10 +0530 Subject: [PATCH 14/82] feat: added dashboard fixtures for accounts --- erpnext/accounts/dashboard_fixtures.py | 119 +++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 erpnext/accounts/dashboard_fixtures.py diff --git a/erpnext/accounts/dashboard_fixtures.py b/erpnext/accounts/dashboard_fixtures.py new file mode 100644 index 0000000000..30eb5e3115 --- /dev/null +++ b/erpnext/accounts/dashboard_fixtures.py @@ -0,0 +1,119 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +import json + + +def get_data(): + return frappe._dict({ + "dashboards": get_dashboards(), + "charts": get_charts(), + }) + +def get_dashboards(): + return [{ + "name": "Accounts", + "dashboard_name": "Accounts", + "charts": [ + { "chart": "Outgoing Bills (Sales Invoice)" }, + { "chart": "Incoming Bills (Purchase Invoice)" }, + { "chart": "Bank Balance" }, + { "chart": "Income" }, + { "chart": "Expenses" } + ] + }] + +def get_charts(): + company = frappe.get_doc("Company", get_company_for_dashboards()) + income_account = company.default_income_account or get_account("Income Account", company.name) + expense_account = company.default_expense_account or get_account("Expense Account", company.name) + bank_account = company.default_bank_account or get_account("Bank", company.name) + + return [ + { + "doctype": "Dashboard Chart", + "time_interval": "Quarterly", + "name": "Income", + "chart_name": "Income", + "timespan": "Last Year", + "color": None, + "filters_json": json.dumps({"company": company.name, "account": income_account}), + "source": "Account Balance Timeline", + "chart_type": "Custom", + "timeseries": 1, + "owner": "Administrator", + "type": "Line" + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Quarterly", + "name": "Expenses", + "chart_name": "Expenses", + "timespan": "Last Year", + "color": None, + "filters_json": json.dumps({"company": company.name, "account": expense_account}), + "source": "Account Balance Timeline", + "chart_type": "Custom", + "timeseries": 1, + "owner": "Administrator", + "type": "Line" + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Quarterly", + "name": "Bank Balance", + "chart_name": "Bank Balance", + "timespan": "Last Year", + "color": "#ffb868", + "filters_json": json.dumps({"company": company.name, "account": bank_account}), + "source": "Account Balance Timeline", + "chart_type": "Custom", + "timeseries": 1, + "owner": "Administrator", + "type": "Line" + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Monthly", + "name": "Incoming Bills (Purchase Invoice)", + "chart_name": "Incoming Bills (Purchase Invoice)", + "timespan": "Last Year", + "color": "#a83333", + "value_based_on": "base_grand_total", + "filters_json": json.dumps({}), + "chart_type": "Sum", + "timeseries": 1, + "based_on": "posting_date", + "owner": "Administrator", + "document_type": "Purchase Invoice", + "type": "Bar" + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Monthly", + "name": "Outgoing Bills (Sales Invoice)", + "chart_name": "Outgoing Bills (Sales Invoice)", + "timespan": "Last Year", + "color": "#7b933d", + "value_based_on": "base_grand_total", + "filters_json": json.dumps({}), + "chart_type": "Sum", + "timeseries": 1, + "based_on": "posting_date", + "owner": "Administrator", + "document_type": "Sales Invoice", + "type": "Bar" + } + ] + + +def get_company_for_dashboards(): + company = frappe.defaults.get_defaults().company + if company: + return company + else: + company_list = frappe.get_list("Company") + if company_list: + return company_list[0].name + return None \ No newline at end of file From 8cdcb965c562b470dd2f5fe6d781361b8e4bd09d Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 6 May 2020 15:19:22 +0530 Subject: [PATCH 15/82] feat: added dashboard fixtures for health care --- erpnext/healthcare/dashboard_fixtures.py | 40 ++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 erpnext/healthcare/dashboard_fixtures.py diff --git a/erpnext/healthcare/dashboard_fixtures.py b/erpnext/healthcare/dashboard_fixtures.py new file mode 100644 index 0000000000..856222762b --- /dev/null +++ b/erpnext/healthcare/dashboard_fixtures.py @@ -0,0 +1,40 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +import json + + +def get_data(): + return frappe._dict({ + "dashboards": get_dashboards(), + "charts": get_charts(), + }) + +def get_dashboards(): + return [{ + "name": "Healthcare", + "dashboard_name": "Healthcare", + "charts": [ + { "chart": "Patient Appointments" } + ] + }] + +def get_charts(): + return [ + { + "doctype": "Dashboard Chart", + "time_interval": "Daily", + "chart_name": "Patient Appointments", + "timespan": "Last Month", + "color": "#77ecca", + "filters_json": json.dumps({}), + "chart_type": "Count", + "timeseries": 1, + "based_on": "appointment_datetime", + "owner": "Administrator", + "document_type": "Patient Appointment", + "type": "Line", + "width": "Half" + } + ] From 2596c8bbe72f25d1f10ed51c422d802b1f29598c Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Wed, 6 May 2020 17:06:53 +0530 Subject: [PATCH 16/82] fix: upload attendance (#21620) --- erpnext/hr/doctype/upload_attendance/upload_attendance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/upload_attendance/upload_attendance.py b/erpnext/hr/doctype/upload_attendance/upload_attendance.py index f75bb4155e..61faea1871 100644 --- a/erpnext/hr/doctype/upload_attendance/upload_attendance.py +++ b/erpnext/hr/doctype/upload_attendance/upload_attendance.py @@ -145,7 +145,7 @@ def import_attendances(rows): def remove_holidays(rows): rows = [ row for row in rows if row[4] != "Holiday"] - return + return rows from frappe.modules import scrub From d50502ca40beb2b6985ceb6f674976d1e0ed3585 Mon Sep 17 00:00:00 2001 From: Prssanna Desai Date: Wed, 6 May 2020 17:36:48 +0530 Subject: [PATCH 17/82] fix: fix get_employee_details query (#21623) --- .../report/monthly_attendance_sheet/monthly_attendance_sheet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py index d98ed1b414..82ed27715f 100644 --- a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py +++ b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py @@ -215,7 +215,7 @@ def get_conditions(filters): def get_employee_details(group_by, company): emp_map = {} query = """select name, employee_name, designation, department, branch, company, - holiday_list from `tabEmployee` where company = '%s' """ % frappe.db.escape(company) + holiday_list from `tabEmployee` where company = %s """ % frappe.db.escape(company) if group_by: group_by = group_by.lower() From b821f22b7cb83c8002a2709e6e5c8c6df87cfc53 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 6 May 2020 18:12:23 +0530 Subject: [PATCH 18/82] fix(minor): Reverse GL fix --- erpnext/accounts/general_ledger.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index fb1a4f4dba..bfe35ab006 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -297,7 +297,8 @@ def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None, fields = ["*"], filters = { "voucher_type": voucher_type, - "voucher_no": voucher_no + "voucher_no": voucher_no, + "is_cancelled": 0 }) if gl_entries: From 41616d4dd8aa3a723046aa0e07fdaf037c46b363 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 6 May 2020 20:21:05 +0530 Subject: [PATCH 19/82] feat: add name to fixture --- erpnext/healthcare/dashboard_fixtures.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/healthcare/dashboard_fixtures.py b/erpnext/healthcare/dashboard_fixtures.py index 856222762b..fc3d62f2d8 100644 --- a/erpnext/healthcare/dashboard_fixtures.py +++ b/erpnext/healthcare/dashboard_fixtures.py @@ -25,6 +25,7 @@ def get_charts(): { "doctype": "Dashboard Chart", "time_interval": "Daily", + "name": "Patient Appointments", "chart_name": "Patient Appointments", "timespan": "Last Month", "color": "#77ecca", From 9de26f664831bd0939012477c0cc5cb427c4d1f6 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 7 May 2020 12:06:47 +0530 Subject: [PATCH 20/82] fix: list index out of range (#21613) --- erpnext/stock/doctype/batch/batch.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 9b7249e66b..a091ac7fae 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -7,7 +7,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.model.naming import make_autoname, revert_series_if_last -from frappe.utils import flt, cint +from frappe.utils import flt, cint, get_link_to_form from frappe.utils.jinja import render_template from frappe.utils.data import add_days from six import string_types @@ -124,7 +124,7 @@ class Batch(Document): if has_expiry_date and not self.expiry_date: frappe.throw(msg=_("Please set {0} for Batched Item {1}, which is used to set {2} on Submit.") \ .format(frappe.bold("Shelf Life in Days"), - frappe.utils.get_link_to_form("Item", self.item), + get_link_to_form("Item", self.item), frappe.bold("Batch Expiry Date")), title=_("Expiry Date Mandatory")) @@ -264,16 +264,20 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None): def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos cond = '' - if serial_no: + if serial_no and frappe.get_cached_value('Item', item_code, 'has_batch_no'): + serial_nos = get_serial_nos(serial_no) batch = frappe.get_all("Serial No", fields = ["distinct batch_no"], filters= { "item_code": item_code, "warehouse": warehouse, - "name": ("in", get_serial_nos(serial_no)) + "name": ("in", serial_nos) } ) + if not batch: + validate_serial_no_with_batch(serial_nos, item_code) + if batch and len(batch) > 1: return [] @@ -288,4 +292,15 @@ def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL) {0} group by batch_id order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC - """.format(cond), (item_code, warehouse), as_dict=True) \ No newline at end of file + """.format(cond), (item_code, warehouse), as_dict=True) + +def validate_serial_no_with_batch(serial_nos, item_code): + if frappe.get_cached_value("Serial No", serial_nos[0], "item_code") != item_code: + frappe.throw(_("The serial no {0} does not belong to item {1}") + .format(get_link_to_form("Serial No", serial_nos[0]), get_link_to_form("Item", item_code))) + + serial_no_link = ','.join([get_link_to_form("Serial No", sn) for sn in serial_nos]) + + message = "Serial Nos" if len(serial_nos) > 1 else "Serial No" + frappe.throw(_("There is no batch found against the {0}: {1}") + .format(message, serial_no_link)) \ No newline at end of file From c759e1790023890aeb75f5efc3f11907175678b4 Mon Sep 17 00:00:00 2001 From: sahil28297 <37302950+sahil28297@users.noreply.github.com> Date: Thu, 7 May 2020 12:12:13 +0530 Subject: [PATCH 21/82] fix(item): patch to rename duplicate item_code values to name (#21622) Co-authored-by: Mangesh-Khairnar --- erpnext/patches.txt | 1 + .../patches/v11_0/rename_duplicate_item_code_values.py | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 erpnext/patches/v11_0/rename_duplicate_item_code_values.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index ba17b67de1..ce0e4ac471 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -495,6 +495,7 @@ erpnext.patches.v10_0.rename_offer_letter_to_job_offer execute:frappe.delete_doc('DocType', 'Production Planning Tool', ignore_missing=True) erpnext.patches.v10_0.migrate_daily_work_summary_settings_to_daily_work_summary_group # 24-12-2018 erpnext.patches.v10_0.add_default_cash_flow_mappers +erpnext.patches.v11_0.rename_duplicate_item_code_values erpnext.patches.v11_0.make_quality_inspection_template erpnext.patches.v10_0.update_status_for_multiple_source_in_po erpnext.patches.v10_0.set_auto_created_serial_no_in_stock_entry diff --git a/erpnext/patches/v11_0/rename_duplicate_item_code_values.py b/erpnext/patches/v11_0/rename_duplicate_item_code_values.py new file mode 100644 index 0000000000..00ab562c35 --- /dev/null +++ b/erpnext/patches/v11_0/rename_duplicate_item_code_values.py @@ -0,0 +1,8 @@ +import frappe + +def execute(): + items = [] + items = frappe.db.sql("""select item_code from `tabItem` group by item_code having count(*) > 1""", as_dict=True) + if items: + for item in items: + frappe.db.sql("""update `tabItem` set item_code=name where item_code = %s""", (item.item_code)) From e16498590377487967ee122ac6fc5590fdf5d85b Mon Sep 17 00:00:00 2001 From: SDLyu Date: Thu, 7 May 2020 16:49:29 +0800 Subject: [PATCH 22/82] Create taiwan.html --- erpnext/regional/address_template/templates/taiwan.html | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 erpnext/regional/address_template/templates/taiwan.html diff --git a/erpnext/regional/address_template/templates/taiwan.html b/erpnext/regional/address_template/templates/taiwan.html new file mode 100644 index 0000000000..43051afc77 --- /dev/null +++ b/erpnext/regional/address_template/templates/taiwan.html @@ -0,0 +1,4 @@ +{{ country }}
{% if pincode %}{{ pincode }}
{% endif -%}{{ county }}{{ city }}{{ address_line1 }}
{% if address_line2 %}{{ address_line2 }}{% endif -%} +{% if phone %}Phone: {{ phone }}
{% endif -%} +{% if fax %}Fax: {{ fax }}
{% endif -%} +{% if email_id %}Email: {{ email_id }}
{% endif -%} From b2eb4ee0f98b4cce7f4f32edb6cdcd9b19c7c08a Mon Sep 17 00:00:00 2001 From: SDLyu Date: Thu, 7 May 2020 17:09:17 +0800 Subject: [PATCH 23/82] Change
position Change
position for better format --- erpnext/regional/address_template/templates/taiwan.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/address_template/templates/taiwan.html b/erpnext/regional/address_template/templates/taiwan.html index 43051afc77..1715bea48a 100644 --- a/erpnext/regional/address_template/templates/taiwan.html +++ b/erpnext/regional/address_template/templates/taiwan.html @@ -1,4 +1,4 @@ -{{ country }}
{% if pincode %}{{ pincode }}
{% endif -%}{{ county }}{{ city }}{{ address_line1 }}
{% if address_line2 %}{{ address_line2 }}{% endif -%} -{% if phone %}Phone: {{ phone }}
{% endif -%} -{% if fax %}Fax: {{ fax }}
{% endif -%} -{% if email_id %}Email: {{ email_id }}
{% endif -%} +{{ country }}
{% if pincode %}{{ pincode }}
{% endif -%}{{ county }}{{ city }}{{ address_line1 }}{% if address_line2 %}{{ address_line2 }}{% endif -%} +{% if phone %}
Phone: {{ phone }}{% endif -%} +{% if fax %}
Fax: {{ fax }}{% endif -%} +{% if email_id %}
Email: {{ email_id }}{% endif -%} From 52189cba861e0d88cf9847f53bd5c125f683f824 Mon Sep 17 00:00:00 2001 From: Kevin Chan Date: Thu, 7 May 2020 17:49:32 +0800 Subject: [PATCH 24/82] style: Improve formatting This commit improves indentations and makes sql queries more readable. --- .../budget_variance_report.py | 418 ++++++++++++------ 1 file changed, 284 insertions(+), 134 deletions(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index 39e218bfad..f286a45757 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -2,187 +2,337 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals +from six import iteritems + import frappe from frappe import _ from frappe.utils import flt from frappe.utils import formatdate + from erpnext.controllers.trends import get_period_date_ranges, get_period_month_ranges -from six import iteritems -from pprint import pprint + def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} - columns = get_columns(filters) - if filters.get("budget_against_filter"): - dimensions = filters.get("budget_against_filter") - else: - dimensions = get_cost_centers(filters) + columns = get_columns(filters) - period_month_ranges = get_period_month_ranges(filters["period"], filters["from_fiscal_year"]) - cam_map = get_dimension_account_month_map(filters) + if filters.get("budget_against_filter"): + dimensions = filters.get("budget_against_filter") + else: + dimensions = get_cost_centers(filters) - data = [] - for dimension in dimensions: - dimension_items = cam_map.get(dimension) - if dimension_items: - for account, monthwise_data in iteritems(dimension_items): - row = [dimension, account] - totals = [0, 0, 0] - for year in get_fiscal_years(filters): - last_total = 0 - for relevant_months in period_month_ranges: - period_data = [0, 0, 0] - for month in relevant_months: - if monthwise_data.get(year[0]): - month_data = monthwise_data.get(year[0]).get(month, {}) - for i, fieldname in enumerate(["target", "actual", "variance"]): - value = flt(month_data.get(fieldname)) - period_data[i] += value - totals[i] += value + period_month_ranges = get_period_month_ranges( + filters["period"], filters["from_fiscal_year"] + ) - period_data[0] += last_total + cam_map = get_dimension_account_month_map(filters) - if(filters.get("show_cumulative")): - last_total = period_data[0] - period_data[1] + data = [] + for dimension in dimensions: + dimension_items = cam_map.get(dimension) + if dimension_items: + for account, monthwise_data in iteritems(dimension_items): + row = [dimension, account] + totals = [0, 0, 0] + for year in get_fiscal_years(filters): + last_total = 0 + for relevant_months in period_month_ranges: + period_data = [0, 0, 0] + for month in relevant_months: + if monthwise_data.get(year[0]): + month_data = monthwise_data.get(year[0]).get(month, {}) + for i, fieldname in enumerate( + ["target", "actual", "variance"] + ): + value = flt(month_data.get(fieldname)) + period_data[i] += value + totals[i] += value - period_data[2] = period_data[0] - period_data[1] - row += period_data - totals[2] = totals[0] - totals[1] - if filters["period"] != "Yearly" : - row += totals - data.append(row) + period_data[0] += last_total + + if filters.get("show_cumulative"): + last_total = period_data[0] - period_data[1] + + period_data[2] = period_data[0] - period_data[1] + row += period_data + totals[2] = totals[0] - totals[1] + if filters["period"] != "Yearly": + row += totals + data.append(row) + + return columns, data - return columns, data def get_columns(filters): - columns = [_(filters.get("budget_against")) + ":Link/%s:150"%(filters.get("budget_against")), _("Account") + ":Link/Account:150"] + columns = [ + _(filters.get("budget_against")) + + ":Link/%s:150" % (filters.get("budget_against")), + _("Account") + ":Link/Account:150", + ] - group_months = False if filters["period"] == "Monthly" else True + group_months = False if filters["period"] == "Monthly" else True - fiscal_year = get_fiscal_years(filters) + fiscal_year = get_fiscal_years(filters) - for year in fiscal_year: - for from_date, to_date in get_period_date_ranges(filters["period"], year[0]): - if filters["period"] == "Yearly": - labels = [_("Budget") + " " + str(year[0]), _("Actual ") + " " + str(year[0]), _("Variance ") + " " + str(year[0])] - for label in labels: - columns.append(label+":Float:150") - else: - for label in [_("Budget") + " (%s)" + " " + str(year[0]), _("Actual") + " (%s)" + " " + str(year[0]), _("Variance") + " (%s)" + " " + str(year[0])]: - if group_months: - label = label % (formatdate(from_date, format_string="MMM") + "-" + formatdate(to_date, format_string="MMM")) - else: - label = label % formatdate(from_date, format_string="MMM") + for year in fiscal_year: + for from_date, to_date in get_period_date_ranges(filters["period"], year[0]): + if filters["period"] == "Yearly": + labels = [ + _("Budget") + " " + str(year[0]), + _("Actual ") + " " + str(year[0]), + _("Variance ") + " " + str(year[0]), + ] + for label in labels: + columns.append(label + ":Float:150") + else: + for label in [ + _("Budget") + " (%s)" + " " + str(year[0]), + _("Actual") + " (%s)" + " " + str(year[0]), + _("Variance") + " (%s)" + " " + str(year[0]), + ]: + if group_months: + label = label % ( + formatdate(from_date, format_string="MMM") + + "-" + + formatdate(to_date, format_string="MMM") + ) + else: + label = label % formatdate(from_date, format_string="MMM") - columns.append(label+":Float:150") + columns.append(label + ":Float:150") + + if filters["period"] != "Yearly": + return columns + [ + _("Total Budget") + ":Float:150", + _("Total Actual") + ":Float:150", + _("Total Variance") + ":Float:150", + ] + else: + return columns - if filters["period"] != "Yearly" : - return columns + [_("Total Budget") + ":Float:150", _("Total Actual") + ":Float:150", - _("Total Variance") + ":Float:150"] - else: - return columns def get_cost_centers(filters): - cond = "and 1=1" - if filters.get("budget_against") == "Cost Center": - cond = "order by lft" + order_by = "" + if filters.get("budget_against") == "Cost Center": + order_by = "order by lft" - if filters.get("budget_against") in ["Cost Center", "Project"]: - return frappe.db.sql_list("""select name from `tab{tab}` where company=%s - {cond}""".format(tab=filters.get("budget_against"), cond=cond), filters.get("company")) - else: - return frappe.db.sql_list("""select name from `tab{tab}`""".format(tab=filters.get("budget_against"))) #nosec + if filters.get("budget_against") in ["Cost Center", "Project"]: + return frappe.db.sql_list( + """ + select + name + from + `tab{tab}` + where + company = %s + {order_by}; + """.format( + tab=filters.get("budget_against"), order_by=order_by + ), + filters.get("company"), + ) + else: + return frappe.db.sql_list( + """ + select + name + from + `tab{tab}` + """.format( + tab=filters.get("budget_against") + ) + ) # nosec -#Get dimension & target details + +# Get dimension & target details def get_dimension_target_details(filters): - cond = "" - if filters.get("budget_against_filter"): - cond += " and b.{budget_against} in (%s)".format(budget_against = \ - frappe.scrub(filters.get('budget_against'))) % ', '.join(['%s']* len(filters.get('budget_against_filter'))) + budget_against = frappe.scrub(filters.get("budget_against")) - return frappe.db.sql(""" - select b.{budget_against} as budget_against, b.monthly_distribution, ba.account, ba.budget_amount,b.fiscal_year - from `tabBudget` b, `tabBudget Account` ba - where b.name=ba.parent and b.docstatus = 1 and b.fiscal_year between %s and %s - and b.budget_against = %s and b.company=%s {cond} order by b.fiscal_year - """.format(budget_against=filters.get("budget_against").replace(" ", "_").lower(), cond=cond), - tuple([filters.from_fiscal_year,filters.to_fiscal_year,filters.budget_against, filters.company] + filters.get('budget_against_filter')), - as_dict=True) + cond = "" + if filters.get("budget_against_filter"): + cond += """ + and + b.{budget_against} in ( + %s + ) + """.format( + budget_against=budget_against + ) % ", ".join( + ["%s"] * len(filters.get("budget_against_filter")) + ) + + return frappe.db.sql( + """ + select + b.{budget_against} as name + from + `tabBudget` b + where + b.docstatus = 1 + and b.fiscal_year between %s and %s + and b.budget_against = %s + and b.company = %s + {cond} + order by + b.fiscal_year + """.format( + budget_against=budget_against, cond=cond + ), + tuple( + [ + filters.from_fiscal_year, + filters.to_fiscal_year, + filters.budget_against, + filters.company, + ] + + filters.get("budget_against_filter") + ), + as_dict=True, + ) -#Get target distribution details of accounts of cost center +# Get target distribution details of accounts of cost center def get_target_distribution_details(filters): - target_details = {} - for d in frappe.db.sql("""select md.name, mdp.month, mdp.percentage_allocation - from `tabMonthly Distribution Percentage` mdp, `tabMonthly Distribution` md - where mdp.parent=md.name and md.fiscal_year between %s and %s order by md.fiscal_year""",(filters.from_fiscal_year, filters.to_fiscal_year), as_dict=1): - target_details.setdefault(d.name, {}).setdefault(d.month, flt(d.percentage_allocation)) + target_details = {} + for d in frappe.db.sql( + """ + select + md.name, + mdp.month, + mdp.percentage_allocation + from + `tabMonthly Distribution Percentage` mdp, + `tabMonthly Distribution` md + where + mdp.parent = md.name + and md.fiscal_year between %s + and %s + order by md.fiscal_year + """, + (filters.from_fiscal_year, filters.to_fiscal_year), + as_dict=1, + ): + target_details.setdefault(d.name, {}).setdefault( + d.month, flt(d.percentage_allocation) + ) - return target_details + return target_details -#Get actual details from gl entry + +# Get actual details from gl entry def get_actual_details(name, filters): - cond = "1=1" - budget_against=filters.get("budget_against").replace(" ", "_").lower() + budget_against = frappe.scrub(filters.get("budget_against")) - if filters.get("budget_against") == "Cost Center": - cc_lft, cc_rgt = frappe.db.get_value("Cost Center", name, ["lft", "rgt"]) - cond = "lft>='{lft}' and rgt<='{rgt}'".format(lft = cc_lft, rgt=cc_rgt) + cond = "" + if filters.get("budget_against") == "Cost Center": + cc_lft, cc_rgt = frappe.db.get_value("Cost Center", name, ["lft", "rgt"]) + cond = """ + and lft >= '{lft}' + and rgt <= '{rgt}' + """.format( + lft=cc_lft, rgt=cc_rgt + ) - ac_details = frappe.db.sql("""select gl.account, gl.debit, gl.credit,gl.fiscal_year, - MONTHNAME(gl.posting_date) as month_name, b.{budget_against} as budget_against - from `tabGL Entry` gl, `tabBudget Account` ba, `tabBudget` b - where - b.name = ba.parent - and b.docstatus = 1 - and ba.account=gl.account - and b.{budget_against} = gl.{budget_against} - and gl.fiscal_year between %s and %s - and b.{budget_against}=%s - and exists(select name from `tab{tab}` where name=gl.{budget_against} and {cond}) group by gl.name order by gl.fiscal_year - """.format(tab = filters.budget_against, budget_against = budget_against, cond = cond,from_year=filters.from_fiscal_year,to_year=filters.to_fiscal_year), - (filters.from_fiscal_year, filters.to_fiscal_year, name), as_dict=1) + ac_details = frappe.db.sql( + """ + select + gl.account, + gl.debit, + gl.credit, + gl.fiscal_year, + MONTHNAME(gl.posting_date) as month_name, + b.{budget_against} as budget_against + from + `tabGL Entry` gl, + `tabBudget Account` ba, + `tabBudget` b + where + b.name = ba.parent + and b.docstatus = 1 + and ba.account=gl.account + and b.{budget_against} = gl.{budget_against} + and gl.fiscal_year between %s and %s + and b.{budget_against} = %s + and exists( + select + name + from + `tab{tab}` + where + name = gl.{budget_against} + {cond} + ) + group by + gl.name + order by gl.fiscal_year + """.format( + tab=filters.budget_against, budget_against=budget_against, cond=cond + ), + (filters.from_fiscal_year, filters.to_fiscal_year, name), + as_dict=1, + ) - cc_actual_details = {} - for d in ac_details: - cc_actual_details.setdefault(d.account, []).append(d) + cc_actual_details = {} + for d in ac_details: + cc_actual_details.setdefault(d.account, []).append(d) + + return cc_actual_details - return cc_actual_details def get_dimension_account_month_map(filters): - import datetime - dimension_target_details = get_dimension_target_details(filters) - tdd = get_target_distribution_details(filters) + import datetime - cam_map = {} + dimension_target_details = get_dimension_target_details(filters) + tdd = get_target_distribution_details(filters) - for ccd in dimension_target_details: - actual_details = get_actual_details(ccd.budget_against, filters) + cam_map = {} - for month_id in range(1, 13): - month = datetime.date(2013, month_id, 1).strftime('%B') - cam_map.setdefault(ccd.budget_against, {}).setdefault(ccd.account, {}).setdefault(ccd.fiscal_year,{})\ - .setdefault(month, frappe._dict({ - "target": 0.0, "actual": 0.0 - })) + for ccd in dimension_target_details: + actual_details = get_actual_details(ccd.budget_against, filters) - tav_dict = cam_map[ccd.budget_against][ccd.account][ccd.fiscal_year][month] - month_percentage = tdd.get(ccd.monthly_distribution, {}).get(month, 0) \ - if ccd.monthly_distribution else 100.0/12 + for month_id in range(1, 13): + month = datetime.date(2013, month_id, 1).strftime("%B") + cam_map.setdefault(ccd.budget_against, {}).setdefault( + ccd.account, {} + ).setdefault(ccd.fiscal_year, {}).setdefault( + month, frappe._dict({"target": 0.0, "actual": 0.0}) + ) - tav_dict.target = flt(ccd.budget_amount) * month_percentage / 100 + tav_dict = cam_map[ccd.budget_against][ccd.account][ccd.fiscal_year][month] + month_percentage = ( + tdd.get(ccd.monthly_distribution, {}).get(month, 0) + if ccd.monthly_distribution + else 100.0 / 12 + ) - for ad in actual_details.get(ccd.account, []): - if ad.month_name == month: - tav_dict.actual += flt(ad.debit) - flt(ad.credit) + tav_dict.target = flt(ccd.budget_amount) * month_percentage / 100 + + for ad in actual_details.get(ccd.account, []): + if ad.month_name == month: + tav_dict.actual += flt(ad.debit) - flt(ad.credit) + + return cam_map - return cam_map def get_fiscal_years(filters): - fiscal_year = frappe.db.sql("""select name from `tabFiscal Year` where - name between %(from_fiscal_year)s and %(to_fiscal_year)s""", - {'from_fiscal_year': filters["from_fiscal_year"], 'to_fiscal_year': filters["to_fiscal_year"]}) + fiscal_year = frappe.db.sql( + """ + select + name + from + `tabFiscal Year` + where + name between %(from_fiscal_year)s and %(to_fiscal_year)s + order by + year + """, + { + "from_fiscal_year": filters["from_fiscal_year"], + "to_fiscal_year": filters["to_fiscal_year"], + }, + ) - return fiscal_year + return fiscal_year From dd340c15de2daec6e21dcc5c35db362bee1d419c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 7 May 2020 15:35:54 +0530 Subject: [PATCH 25/82] fix: Loan Security Unpledge fixes --- erpnext/loan_management/doctype/loan/loan.py | 3 +- .../loan_management/doctype/loan/test_loan.py | 12 +- .../doctype/loan_repayment/loan_repayment.py | 9 +- .../loan_security_shortfall.py | 2 +- .../loan_security_unpledge.js | 8 +- .../loan_security_unpledge.py | 138 +++++++++--------- .../doctype/unpledge/unpledge.json | 13 +- 7 files changed, 92 insertions(+), 93 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index c550d4952d..76e10e5ddd 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -248,8 +248,7 @@ def create_loan_security_unpledge(loan, applicant_type, applicant, company, as_d for loan_security in loan_security_pledge_details: unpledge_request.append('securities', { "loan_security": loan_security.loan_security, - "qty": loan_security.qty, - "against_pledge": loan_security.parent + "qty": loan_security.qty }) if as_dict: diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 77a1fcc574..364e2ffecf 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -15,6 +15,7 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_ 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_security_unpledge.loan_security_unpledge import get_pledged_security_qty class TestLoan(unittest.TestCase): def setUp(self): @@ -152,7 +153,7 @@ class TestLoan(unittest.TestCase): repayment_entry.save() repayment_entry.submit() - penalty_amount = (accrued_interest_amount * 5 * 25) / (100 * days_in_year(get_datetime(first_date).year)) + penalty_amount = (accrued_interest_amount * 4 * 25) / (100 * days_in_year(get_datetime(first_date).year)) self.assertEquals(flt(repayment_entry.penalty_amount, 2), flt(penalty_amount, 2)) amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', @@ -305,7 +306,7 @@ 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() @@ -319,13 +320,12 @@ class TestLoan(unittest.TestCase): unpledge_request.submit() unpledge_request.status = 'Approved' unpledge_request.save() - - loan_security_pledge.load_from_db() loan.load_from_db() + pledged_qty = get_pledged_security_qty(loan.name) + self.assertEqual(loan.status, 'Closed') - for security in loan_security_pledge.securities: - self.assertEquals(security.qty, 0) + self.assertEquals(sum(pledged_qty.values()), 0) def create_loan_accounts(): diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 452c836819..2ab668a0e1 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -264,6 +264,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): penalty_amount = 0 payable_principal_amount = 0 final_due_date = '' + due_date = '' for entry in accrued_interest_entries: # Loan repayment due date is one day after the loan interest is accrued @@ -272,7 +273,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)) + 1 + 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 @@ -290,9 +291,9 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable - if payment_type == "Loan Closure" and not payable_principal_amount: - if final_due_date: - pending_days = date_diff(posting_date, final_due_date) + if payment_type == "Loan Closure": + if due_date: + pending_days = date_diff(posting_date, due_date) + 1 else: pending_days = date_diff(posting_date, against_loan_doc.disbursement_date) + 1 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 8ca6e3e908..308c4385d3 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 @@ -69,7 +69,7 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): loan_security_map[loan.name]['security_value'] += current_loan_security_amount - (current_loan_security_amount * loan.haircut/100) for loan, value in iteritems(loan_security_map): - if (value["security_value"]/value["loan_amount"]) < ltv_ratio: + if (value["loan_amount"]/value['security_value'] * 100) > ltv_ratio: create_loan_security_shortfall(loan, value, process_loan_security_shortfall) def create_loan_security_shortfall(loan, value, process_loan_security_shortfall): diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js index 72c5f38cf3..8223206277 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js @@ -4,10 +4,8 @@ frappe.ui.form.on('Loan Security Unpledge', { refresh: function(frm) { - frm.set_query("against_pledge", "securities", () => { - return { - filters : [["status", "in", ["Pledged", "Partially Pledged"]]] - }; - }); + if (frm.doc.docstatus == 1 && frm.doc.status == 'Approved') { + frm.set_df_property('status', 'read_only', 1); + } } }); 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 b2bb22a3ce..5e9d82aa91 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 @@ -8,12 +8,13 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import get_datetime, flt import json +from six import iteritems from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price class LoanSecurityUnpledge(Document): def validate(self): - self.validate_pledges() self.validate_duplicate_securities() + self.validate_unpledge_qty() def on_cancel(self): self.update_loan_security_pledge(cancel=1) @@ -23,80 +24,52 @@ class LoanSecurityUnpledge(Document): def validate_duplicate_securities(self): security_list = [] for d in self.securities: - security = [d.loan_security, d.against_pledge] - if security not in security_list: - security_list.append(security) + if d.loan_security not in security_list: + security_list.append(d.loan_security) else: - frappe.throw(_("Row {0}: Loan Security {1} against Loan Security Pledge {2} added multiple times").format( - d.idx, frappe.bold(d.loan_security), frappe.bold(d.against_pledge))) + frappe.throw(_("Row {0}: Loan Security {1} added multiple times").format( + d.idx, frappe.bold(d.loan_security))) - def validate_pledges(self): - pledge_qty_map = self.get_pledge_details() - loan = frappe.get_doc("Loan", self.loan) + def validate_unpledge_qty(self): + pledge_qty_map = get_pledged_security_qty(self.loan) - remaining_qty = 0 - unpledge_value = 0 + ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type", + fields=["name", "loan_to_value_ratio"], as_list=1)) + + loan_security_price_map = frappe._dict(frappe.get_all("Loan Security Price", + fields=["loan_security", "loan_security_price"], + filters = { + "valid_from": ("<=", get_datetime()), + "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 + security_value = 0 for security in self.securities: - pledged_qty = pledge_qty_map.get((security.against_pledge, security.loan_security), 0) - if not pledged_qty: - frappe.throw(_("Zero qty of {0} pledged against loan {1}").format(frappe.bold(security.loan_security), - frappe.bold(self.loan))) + pledged_qty = pledge_qty_map.get(security.loan_security) - unpledge_qty = pledged_qty - security.qty - security_price = security.qty * get_loan_security_price(security.loan_security) + 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, + frappe.bold(security.loan_security), frappe.bold(self.loan))) - if unpledge_qty < 0: - frappe.throw(_("""Row {0}: Cannot unpledge more than {1} qty of {2} against - Loan Security Pledge {3}""").format(security.idx, frappe.bold(pledged_qty), - frappe.bold(security.loan_security), frappe.bold(security.against_pledge))) + qty_after_unpledge = pledged_qty - security.qty + ltv_ratio = ltv_ratio_map.get(security.loan_security_type) - remaining_qty += unpledge_qty - unpledge_value += security_price - flt(security_price * security.haircut/100) + security_value += qty_after_unpledge * loan_security_price_map.get(security.loan_security) - if unpledge_value > loan.total_principal_paid: - frappe.throw(_("Cannot Unpledge, loan security value is greater than the repaid amount")) + if not security_value and pending_principal_amount > 0: + frappe.throw("Cannot Unpledge, loan to value ratio is breaching") - def get_pledge_details(self): - pledge_qty_map = {} - - pledge_details = frappe.db.sql(""" - SELECT p.parent, p.loan_security, p.qty FROM - `tabLoan Security Pledge` lsp, - `tabPledge` p - WHERE - p.parent = lsp.name - AND lsp.loan = %s - AND lsp.docstatus = 1 - AND lsp.status in ('Pledged', 'Partially Pledged') - """, (self.loan), as_dict=1) - - for pledge in pledge_details: - pledge_qty_map.setdefault((pledge.parent, pledge.loan_security), pledge.qty) - - return pledge_qty_map + if security_value and (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.update_loan_security_pledge() self.update_loan_status() - - def update_loan_security_pledge(self, cancel=0): - if cancel: - new_qty = 'p.qty + u.qty' - else: - new_qty = 'p.qty - u.qty' - - frappe.db.sql(""" - UPDATE - `tabPledge` p, `tabUnpledge` u, `tabLoan Security Pledge` lsp, `tabLoan Security Unpledge` lsu - SET p.qty = {new_qty} - WHERE - lsp.loan = %s - AND p.parent = u.against_pledge - AND p.parent = lsp.name - AND lsp.docstatus = 1 - AND p.loan_security = u.loan_security""".format(new_qty=new_qty),(self.loan)) + self.db_set('unpledge_time', get_datetime()) def update_loan_status(self, cancel=0): if cancel: @@ -104,10 +77,45 @@ class LoanSecurityUnpledge(Document): if loan_status == 'Closed': frappe.db.set_value('Loan', self.loan, 'status', 'Loan Closure Requested') else: - pledge_qty = frappe.db.sql("""SELECT SUM(c.qty) - FROM `tabLoan Security Pledge` p, `tabPledge` c - WHERE p.loan = %s AND c.parent = p.name""", (self.loan))[0][0] + pledged_qty = 0 + current_pledges = get_pledged_security_qty(self.loan) - if not pledge_qty: + for security, qty in iteritems(current_pledges): + pledged_qty += qty + + if not pledged_qty: frappe.db.set_value('Loan', self.loan, 'status', 'Closed') +@frappe.whitelist() +def get_pledged_security_qty(loan): + + current_pledges = {} + + unpledges = frappe._dict(frappe.db.sql(""" + SELECT u.loan_security, sum(u.qty) as qty + FROM `tabLoan Security Unpledge` up, `tabUnpledge` u + WHERE up.loan = %s + AND u.parent = up.name + AND up.status = 'Approved' + GROUP BY u.loan_security + """, (loan))) + + pledges = frappe._dict(frappe.db.sql(""" + SELECT p.loan_security, sum(p.qty) as qty + FROM `tabLoan Security Pledge` lp, `tabPledge`p + WHERE lp.loan = %s + AND p.parent = lp.name + AND lp.status = 'Pledged' + GROUP BY p.loan_security + """, (loan))) + + for security, qty in iteritems(pledges): + current_pledges.setdefault(security, qty) + current_pledges[security] -= unpledges.get(security, 0.0) + + return current_pledges + + + + + diff --git a/erpnext/loan_management/doctype/unpledge/unpledge.json b/erpnext/loan_management/doctype/unpledge/unpledge.json index 9e6277d5f8..ee192d7377 100644 --- a/erpnext/loan_management/doctype/unpledge/unpledge.json +++ b/erpnext/loan_management/doctype/unpledge/unpledge.json @@ -1,11 +1,11 @@ { + "actions": [], "creation": "2019-09-21 13:22:19.793797", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ "loan_security", - "against_pledge", "loan_security_type", "loan_security_code", "haircut", @@ -54,14 +54,6 @@ "label": "Quantity", "reqd": 1 }, - { - "fieldname": "against_pledge", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Against Pledge", - "options": "Loan Security Pledge", - "reqd": 1 - }, { "fetch_from": "loan_security.haircut", "fieldname": "haircut", @@ -71,7 +63,8 @@ } ], "istable": 1, - "modified": "2019-10-02 12:48:18.588236", + "links": [], + "modified": "2020-05-06 10:50:18.448552", "modified_by": "Administrator", "module": "Loan Management", "name": "Unpledge", From fc0e45d79c2d07c2313264d019489fa901f6c6dc Mon Sep 17 00:00:00 2001 From: Myuddin khatri Date: Thu, 7 May 2020 15:11:39 +0530 Subject: [PATCH 26/82] solved merge conflicts --- erpnext/crm/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/utils.py b/erpnext/crm/utils.py index 38bf79e5fc..95b19ec21e 100644 --- a/erpnext/crm/utils.py +++ b/erpnext/crm/utils.py @@ -19,6 +19,5 @@ def update_lead_phone_numbers(contact, method): mobile_no = primary_mobile_nos[0] lead = frappe.get_doc("Lead", contact_lead) - lead.phone = phone - lead.mobile_no = mobile_no - lead.save() + lead.db_set("phone", phone) + lead.db_set("mobile_no", mobile_no) From b3ee6314f9ef7937b96bf9c346c13161c9ca5226 Mon Sep 17 00:00:00 2001 From: Kevin Chan Date: Thu, 7 May 2020 18:39:47 +0800 Subject: [PATCH 27/82] fix: Fix Budget Variance Report This commit fixes a bug in Budget Variance Report where it combines the actual expense amounts across different fiscal years. This was fixed by updating the function and queries for computing the actual expense amounts. --- .../budget_variance_report.py | 172 +++++++++++++----- 1 file changed, 127 insertions(+), 45 deletions(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index f286a45757..1b110b0aac 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -2,12 +2,12 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals +import datetime from six import iteritems import frappe from frappe import _ -from frappe.utils import flt -from frappe.utils import formatdate +from frappe.utils import flt, formatdate from erpnext.controllers.trends import get_period_date_ranges, get_period_month_ranges @@ -126,8 +126,8 @@ def get_cost_centers(filters): from `tab{tab}` where - company = %s - {order_by}; + company=%s + {order_by} """.format( tab=filters.get("budget_against"), order_by=order_by ), @@ -153,11 +153,11 @@ def get_dimension_target_details(filters): cond = "" if filters.get("budget_against_filter"): cond += """ - and - b.{budget_against} in ( - %s - ) - """.format( + and + b.{budget_against} in ( + %s + ) + """.format( budget_against=budget_against ) % ", ".join( ["%s"] * len(filters.get("budget_against_filter")) @@ -165,7 +165,7 @@ def get_dimension_target_details(filters): return frappe.db.sql( """ - select + select distinct b.{budget_against} as name from `tabBudget` b @@ -198,19 +198,19 @@ def get_target_distribution_details(filters): target_details = {} for d in frappe.db.sql( """ - select - md.name, - mdp.month, - mdp.percentage_allocation - from - `tabMonthly Distribution Percentage` mdp, - `tabMonthly Distribution` md - where - mdp.parent = md.name - and md.fiscal_year between %s - and %s - order by md.fiscal_year - """, + select + md.name, + mdp.month, + mdp.percentage_allocation + from + `tabMonthly Distribution Percentage` mdp, + `tabMonthly Distribution` md + where + mdp.parent = md.name + and md.fiscal_year between %s + and %s + order by md.fiscal_year + """, (filters.from_fiscal_year, filters.to_fiscal_year), as_dict=1, ): @@ -229,8 +229,8 @@ def get_actual_details(name, filters): if filters.get("budget_against") == "Cost Center": cc_lft, cc_rgt = frappe.db.get_value("Cost Center", name, ["lft", "rgt"]) cond = """ - and lft >= '{lft}' - and rgt <= '{rgt}' + and lft >= \'{lft}\' + and rgt <= \'{rgt}\' """.format( lft=cc_lft, rgt=cc_rgt ) @@ -282,37 +282,44 @@ def get_actual_details(name, filters): def get_dimension_account_month_map(filters): - import datetime - dimension_target_details = get_dimension_target_details(filters) tdd = get_target_distribution_details(filters) cam_map = {} for ccd in dimension_target_details: - actual_details = get_actual_details(ccd.budget_against, filters) + accounts = get_accounts(ccd.name, filters) + actual_details = get_actual_details(ccd.name, filters) - for month_id in range(1, 13): - month = datetime.date(2013, month_id, 1).strftime("%B") - cam_map.setdefault(ccd.budget_against, {}).setdefault( - ccd.account, {} - ).setdefault(ccd.fiscal_year, {}).setdefault( - month, frappe._dict({"target": 0.0, "actual": 0.0}) - ) + for year in get_fiscal_years(filters): + year = year[0] + monthly_distribution = get_monthly_distribution(ccd.name, year, filters) + for month_id in range(1, 13): + month = datetime.date(2013, month_id, 1).strftime( + "%B" + ) # Get month string - tav_dict = cam_map[ccd.budget_against][ccd.account][ccd.fiscal_year][month] - month_percentage = ( - tdd.get(ccd.monthly_distribution, {}).get(month, 0) - if ccd.monthly_distribution - else 100.0 / 12 - ) + for account in accounts: + account = account[0] + cam_map.setdefault(ccd.name, {}).setdefault(account, {}).setdefault( + year, {} + ).setdefault(month, frappe._dict({"target": 0.0, "actual": 0.0})) - tav_dict.target = flt(ccd.budget_amount) * month_percentage / 100 + tav_dict = cam_map[ccd.name][account][year][month] - for ad in actual_details.get(ccd.account, []): - if ad.month_name == month: - tav_dict.actual += flt(ad.debit) - flt(ad.credit) + month_percentage = ( + tdd.get(monthly_distribution, {}).get(month, 0) + if monthly_distribution + else 100.0 / 12 + ) + budget_amount = get_budget_amount(ccd.name, year, account, filters) + + tav_dict.target = flt(budget_amount) * month_percentage / 100 + + for ad in actual_details.get(account, []): + if ad.month_name == month and ad.fiscal_year == year: + tav_dict.actual += flt(ad.debit) - flt(ad.credit) return cam_map @@ -336,3 +343,78 @@ def get_fiscal_years(filters): ) return fiscal_year + + +def get_accounts(name, filters): + budget_against = frappe.scrub(filters.get("budget_against")) + + accounts = frappe.db.sql( + """ + select + distinct(ba.account) + from + `tabBudget Account` ba + join + `tabBudget` b + on b.name = ba.parent + where + b.docstatus = 1 + and b.fiscal_year between %s and %s + and b.{budget_against} = %s + order by + ba.account + """.format( + budget_against=budget_against + ), + (filters.from_fiscal_year, filters.to_fiscal_year, name), + ) + + return accounts + + +def get_monthly_distribution(name, year, filters): + budget_against = frappe.scrub(filters.get("budget_against")) + + monthly_distribution = frappe.db.sql( + """ + select + monthly_distribution + from + `tabBudget` + where + docstatus = 1 + and {budget_against} = %s + and fiscal_year = %s + """.format( + budget_against=budget_against + ), + (name, year), + ) + + return monthly_distribution[0][0] if monthly_distribution else None + + +def get_budget_amount(name, year, account, filters): + budget_against = frappe.scrub(filters.get("budget_against")) + + budget_amount = frappe.db.sql( + """ + select + ba.budget_amount + from + `tabBudget Account` ba + join + `tabBudget` b + on b.name = ba.parent + where + b.docstatus = 1 + and b.{budget_against} = %s + and b.fiscal_year = %s + and ba.account = %s + """.format( + budget_against=budget_against + ), + (name, year, account), + ) + + return budget_amount[0][0] if budget_amount else 0 From 2ba89ffd488b835bd288102c1fceed09f4bed83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=BCrker=20Tunal=C4=B1?= Date: Thu, 7 May 2020 12:38:37 +0300 Subject: [PATCH 28/82] fix: Job Card submitted qty Update Operation Status function in work order was throwing exception without checking the "Overproduction Percentage For Work Order" setting. To submit Job Card qty for more than the Work Order's "To Manufacture Qty" we need to apply this fix. (cherry picked from commit 9bd6d119c405f2ace6c93d53647af140c6543542) --- erpnext/manufacturing/doctype/work_order/work_order.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 84bfab2f1d..8301f30d83 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -421,6 +421,9 @@ class WorkOrder(Document): return holidays[holiday_list] def update_operation_status(self): + allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order")) + max_allowed_qty_for_wo = flt(self.qty) + (allowance_percentage/100 * flt(self.qty)) + for d in self.get("operations"): if not d.completed_qty: d.status = "Pending" @@ -428,6 +431,8 @@ class WorkOrder(Document): d.status = "Work in Progress" elif flt(d.completed_qty) == flt(self.qty): d.status = "Completed" + elif flt(d.completed_qty) <= max_allowed_qty_for_wo: + d.status = "Completed" else: frappe.throw(_("Completed Qty can not be greater than 'Qty to Manufacture'")) From b1385f676a60fe13201399e7051d4c7b6cfcf878 Mon Sep 17 00:00:00 2001 From: Sahil Khan Date: Thu, 7 May 2020 16:47:41 +0530 Subject: [PATCH 29/82] fix(lead): strip lead_name before splitting --- erpnext/crm/doctype/lead/lead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 74b358219d..ec7d14d6ae 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -153,7 +153,7 @@ class Lead(SellingController): if not self.lead_name: self.set_lead_name() - names = self.lead_name.split(" ") + names = self.lead_name.strip().split(" ") if len(names) > 1: first_name, last_name = names[0], " ".join(names[1:]) else: From 31a747b98ad4220d6659523f9a53b5bb81ba9252 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 7 May 2020 17:38:00 +0530 Subject: [PATCH 30/82] fix: Query enhancement, cleanup, added extra filter - Query changes as requested - Moved chart generation from js to py - Added Supplier Multiselect filter --- .../quoted_item_comparison.js | 64 ++--------- .../quoted_item_comparison.py | 101 ++++++++++++++---- 2 files changed, 88 insertions(+), 77 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 fe4abd8c9c..a76ffeec2e 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js @@ -16,7 +16,7 @@ frappe.query_reports["Quoted Item Comparison"] = { default: "", options: "Item", label: __("Item"), - fieldname: "item", + fieldname: "item_code", fieldtype: "Link", get_query: () => { let quote = frappe.query_report.get_filter_value('supplier_quotation'); @@ -36,6 +36,14 @@ frappe.query_reports["Quoted Item Comparison"] = { } } }, + { + fieldname: "supplier", + label: __("Supplier"), + fieldtype: "MultiSelectList", + get_data: function(txt) { + return frappe.db.get_link_options('Supplier', txt); + } + }, { fieldtype: "Link", label: __("Supplier Quotation"), @@ -58,60 +66,6 @@ frappe.query_reports["Quoted Item Comparison"] = { } ], - prepare_chart_data: (result) => { - let supplier_wise_map = {}, data_points_map = {}; - let qty_list = result.map(res => res.qty); - qty_list.sort(); - qty_list = new Set(qty_list); - - // create supplier wise map like in Report - for (let res of result) { - if (!(res.supplier in supplier_wise_map)) { - supplier_wise_map[res.supplier] = {}; - } - supplier_wise_map[res.supplier][res.qty] = res.price; - } - - // create datapoints for each qty - for (let supplier of Object.keys(supplier_wise_map)) { - let row = supplier_wise_map[supplier]; - for (let qty of qty_list) { - if (!data_points_map[qty]) { - data_points_map[qty] = []; - } - if (row[qty]) { - data_points_map[qty].push(row[qty]); - } - else { - data_points_map[qty].push(null); - } - } - } - - let dataset = []; - qty_list.forEach((qty) => { - let datapoints = { - 'name': __('Price for Qty ') + qty, - 'values': data_points_map[qty] - } - dataset.push(datapoints); - }); - return dataset; - }, - - get_chart_data: function (columns, result) { - let suppliers = result.filter(d => d.supplier_name).map(res => res.supplier_name); - let dataset = frappe.query_reports["Quoted Item Comparison"].prepare_chart_data(result); - - return { - data: { - labels: suppliers, - datasets: dataset - }, - type: 'bar' - } - }, - onload: (report) => { // Create a button for setting the default supplier report.page.add_inner_button(__("Select Default Supplier"), () => { 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 fd7a731198..a33867a525 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py @@ -9,21 +9,33 @@ from collections import defaultdict from erpnext.setup.utils import get_exchange_rate def execute(filters=None): + if not filters: + return [], [] + conditions = get_conditions(filters) - data = get_data(filters, conditions) + supplier_quotation_data = get_data(filters, conditions) columns = get_columns() - return columns, data + + data, chart_data = prepare_data(supplier_quotation_data) + + return columns, data, None, chart_data + +def get_conditions(filters): + conditions = "" + if filters.get("supplier_quotation"): + conditions += " AND sqi.parent = %(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" + return conditions def get_data(filters, conditions): - out, suppliers = [], [] - item = filters.get("item") - - if not item: + if not filters.get("item_code"): return [] - company_currency = frappe.db.get_default("currency") - float_precision = cint(frappe.db.get_default("float_precision")) or 2 - supplier_quotation_data = frappe.db.sql("""SELECT sqi.parent, sqi.qty, sqi.rate, sqi.uom, sqi.request_for_quotation, sq.supplier @@ -31,17 +43,27 @@ def get_data(filters, conditions): `tabSupplier Quotation Item` sqi, `tabSupplier Quotation` sq WHERE - sqi.item_code = '{0}' + sqi.item_code = %(item_code)s AND sqi.parent = sq.name AND sqi.docstatus < 2 - AND sq.company = '{1}' + AND sq.company = %(company)s AND sq.status != 'Expired' - {2}""".format(item, filters.get("company"), conditions), as_dict=1) + {0}""".format(conditions), filters, as_dict=1) + return supplier_quotation_data + +def prepare_data(supplier_quotation_data): + out, suppliers, qty_list = [], [], [] supplier_wise_map = defaultdict(list) + supplier_qty_price_map = {} + + company_currency = frappe.db.get_default("currency") + float_precision = cint(frappe.db.get_default("float_precision")) or 2 for data in supplier_quotation_data: + supplier = data.get("supplier") supplier_currency = frappe.db.get_value("Supplier", data.get("supplier"), "default_currency") + if supplier_currency: exchange_rate = get_exchange_rate(supplier_currency, company_currency) else: @@ -53,29 +75,64 @@ def get_data(filters, conditions): "price": flt(data.get("rate") * exchange_rate, float_precision), "uom": data.get("uom"), "request_for_quotation": data.get("request_for_quotation"), - "supplier": data.get("supplier") # used for chart generation } - supplier_wise_map[data.supplier].append(row) - suppliers.append(data.supplier) + # map for report view of form {'supplier1':[{},{},...]} + supplier_wise_map[supplier].append(row) - suppliers = set(suppliers) + # 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"] + suppliers.append(supplier) + qty_list.append(data.get("qty")) + + suppliers = list(set(suppliers)) + qty_list = list(set(qty_list)) + + # final data format for report view for supplier in suppliers: supplier_wise_map[supplier][0].update({"supplier_name": supplier}) for entry in supplier_wise_map[supplier]: out.append(entry) - return out + chart_data = prepare_chart_data(suppliers, qty_list, supplier_qty_price_map) -def get_conditions(filters): - conditions = "" + return out, chart_data - if filters.get("request_for_quotation"): - conditions += " AND sqi.request_for_quotation = '{0}' ".format(filters.get("request_for_quotation")) +def prepare_chart_data(suppliers, qty_list, supplier_qty_price_map): + data_points_map = {} + qty_list.sort() - return conditions + # create qty wise values map of the form {'qty1':[value1, value2]} + for supplier in suppliers: + entry = supplier_qty_price_map[supplier] + for qty in qty_list: + if not qty in data_points_map: + data_points_map[qty] = [] + if qty in entry: + data_points_map[qty].append(entry[qty]) + else: + data_points_map[qty].append(None) + dataset = [] + for qty in qty_list: + datapoints = { + "name": _("Price for Qty ") + str(qty), + "values": data_points_map[qty] + } + dataset.append(datapoints) + + chart_data = { + "data": { + "labels": suppliers, + "datasets": dataset + }, + "type": "bar" + } + + return chart_data def get_columns(): columns = [{ From ebf3b78b420acc40156bec117f080952b8137366 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Thu, 7 May 2020 19:20:16 +0530 Subject: [PATCH 31/82] renaming LMS to Learning Management System --- .../education_settings/education_settings.json | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/erpnext/education/doctype/education_settings/education_settings.json b/erpnext/education/doctype/education_settings/education_settings.json index 967a030fd2..0e548dbf77 100644 --- a/erpnext/education/doctype/education_settings/education_settings.json +++ b/erpnext/education/doctype/education_settings/education_settings.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2017-04-05 13:33:04.519313", "doctype": "DocType", "editable_grid": 1, @@ -42,12 +43,14 @@ "fieldtype": "Column Break" }, { + "default": "0", "description": "For Batch based Student Group, the Student Batch will be validated for every Student from the Program Enrollment.", "fieldname": "validate_batch", "fieldtype": "Check", "label": "Validate Batch for Students in Student Group" }, { + "default": "0", "description": "For Course based Student Group, the Course will be validated for every Student from the enrolled Courses in Program Enrollment.", "fieldname": "validate_course", "fieldtype": "Check", @@ -74,13 +77,13 @@ { "fieldname": "web_academy_settings_section", "fieldtype": "Section Break", - "label": "LMS Settings" + "label": "Learning Management System Settings" }, { "depends_on": "eval: doc.enable_lms", "fieldname": "portal_title", "fieldtype": "Data", - "label": "LMS Title" + "label": "Learning Management System Title" }, { "depends_on": "eval: doc.enable_lms", @@ -89,9 +92,10 @@ "label": "Description" }, { + "default": "0", "fieldname": "enable_lms", "fieldtype": "Check", - "label": "Enable LMS" + "label": "Enable Learning Management System" }, { "default": "0", @@ -102,7 +106,8 @@ } ], "issingle": 1, - "modified": "2019-05-13 18:36:13.127563", + "links": [], + "modified": "2020-05-07 19:18:10.639356", "modified_by": "Administrator", "module": "Education", "name": "Education Settings", @@ -141,4 +146,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file From 383a94d7302d0f8da179f0cdc0bf2c0b76c3bd44 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 7 May 2020 23:32:53 +0530 Subject: [PATCH 32/82] fix: delete old appointment analytics tree grid report --- erpnext/patches.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index ce0e4ac471..525593327e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -680,3 +680,4 @@ erpnext.patches.v12_0.fix_quotation_expired_status erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry erpnext.patches.v12_0.retain_permission_rules_for_video_doctype erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive +execute:frappe.delete_doc_if_exists("Page", "appointment-analytic") From 8b526977234881cb3315af16b5f7ca5fc864fb31 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Fri, 8 May 2020 02:23:28 +0530 Subject: [PATCH 33/82] Payment Order not allowing to create Payment Entry --- erpnext/accounts/doctype/payment_order/payment_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_order/payment_order.py b/erpnext/accounts/doctype/payment_order/payment_order.py index 3f3174a69b..7ecdc41d03 100644 --- a/erpnext/accounts/doctype/payment_order/payment_order.py +++ b/erpnext/accounts/doctype/payment_order/payment_order.py @@ -80,7 +80,7 @@ def make_journal_entry(doc, supplier, mode_of_payment=None): paid_amt += d.amount je.append('accounts', { - 'account': doc.references[0].account, + 'account': doc.account, 'credit_in_account_currency': paid_amt }) From 5b1a0fe00ebb9af0c91894dfee5622b04a29e7d7 Mon Sep 17 00:00:00 2001 From: sahil28297 <37302950+sahil28297@users.noreply.github.com> Date: Fri, 8 May 2020 13:06:50 +0530 Subject: [PATCH 34/82] fix: bump erpnext develop to version 13-dev (#21651) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 786b9cfd16..38d8a62f07 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '12.0.0-dev' +__version__ = '13.0.0-dev' def get_default_company(user=None): '''Get default company for user''' From 7d4ccdf24006f70bef35b3a4882773df47d989d2 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 8 May 2020 13:27:25 +0530 Subject: [PATCH 35/82] fix: wrong fieldname branch_code in add_fetch --- .../doctype/payment_request/payment_request.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 97ae5ffde3..7508683c08 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "naming_series:", "creation": "2015-12-15 22:23:24.745065", "doctype": "DocType", @@ -210,13 +211,14 @@ "label": "IBAN" }, { - "fetch_from": "bank_account.branch_code", + "fetch_from": "bank.branch_code", + "fetch_if_empty": 1, "fieldname": "branch_code", "fieldtype": "Read Only", "label": "Branch Code" }, { - "fetch_from": "bank_account.swift_number", + "fetch_from": "bank.swift_number", "fieldname": "swift_number", "fieldtype": "Read Only", "label": "SWIFT Number" @@ -348,7 +350,8 @@ } ], "is_submittable": 1, - "modified": "2020-03-28 16:07:31.960798", + "links": [], + "modified": "2020-05-08 10:23:02.815237", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", From e29283630b5b36e7d2eecbeff29726826d5eaba2 Mon Sep 17 00:00:00 2001 From: Kevin Chan Date: Fri, 8 May 2020 17:45:08 +0800 Subject: [PATCH 36/82] fix: Zero threshold in Tax Withholding Category This fixes a bug in Tax Withholding Category where if it has a threshold of 0, it doesn't apply to Purchase Invoices. The bug was fixed by updating the condition. --- .../tax_withholding_category/tax_withholding_category.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index dd6b4fdc60..818e56633a 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -162,8 +162,7 @@ def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_detai debit_note_amount = get_debit_note_amount(suppliers, year_start_date, year_end_date) supplier_credit_amount -= debit_note_amount - if ((tax_details.get('threshold', 0) and supplier_credit_amount >= tax_details.threshold) - or (tax_details.get('cumulative_threshold', 0) and supplier_credit_amount >= tax_details.cumulative_threshold)): + if supplier_credit_amount >= tax_details.get('threshold', 0) or supplier_credit_amount >= tax_details.get('cumulative_threshold', 0): if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, tds_deducted, net_total, ldc.certificate_limit): @@ -225,4 +224,4 @@ def is_valid_certificate(valid_from, valid_upto, posting_date, deducted_amount, certificate_limit > deducted_amount): valid = True - return valid \ No newline at end of file + return valid From 9d81f97fe677d0ac92064fc9a4e37bf4f5d4a6a0 Mon Sep 17 00:00:00 2001 From: Kevin Chan Date: Fri, 8 May 2020 18:01:18 +0800 Subject: [PATCH 37/82] fix: Tax Withholding Category Description default This commit fixes a bug that happens when a Purchase Invoice uses a Tax Withholding Category without a category_name. The category_name is used as the tax description which is a required field. The bug was fixed by using the Tax Withholding Category's name as the description if the category_name is empty. --- .../tax_withholding_category/tax_withholding_category.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index dd6b4fdc60..e904a681f6 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -58,7 +58,7 @@ def get_tax_withholding_details(tax_withholding_category, fiscal_year, company): "rate": tax_rate_detail.tax_withholding_rate, "threshold": tax_rate_detail.single_threshold, "cumulative_threshold": tax_rate_detail.cumulative_threshold, - "description": tax_withholding.category_name + "description": tax_withholding.category_name.strip() if tax_withholding.category_name.strip() else tax_withholding_category }) def get_tax_withholding_rates(tax_withholding, fiscal_year): @@ -225,4 +225,4 @@ def is_valid_certificate(valid_from, valid_upto, posting_date, deducted_amount, certificate_limit > deducted_amount): valid = True - return valid \ No newline at end of file + return valid From 074ea2f882640fce6848e3c69754d6584002489b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 8 May 2020 15:33:35 +0530 Subject: [PATCH 38/82] fix: local variable 'lft' referenced before assignment --- erpnext/stock/doctype/warehouse/warehouse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index eb4867d2cf..cd86be3115 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -177,7 +177,7 @@ def convert_to_group_or_ledger(): return frappe.get_doc("Warehouse", args.docname).convert_to_group_or_ledger() def get_child_warehouses(warehouse): - lft, rgt = frappe.get_cached_value("Warehouse", warehouse, [lft, rgt]) + lft, rgt = frappe.get_cached_value("Warehouse", warehouse, ["lft", "rgt"]) return frappe.db.sql_list("""select name from `tabWarehouse` where lft >= %s and rgt <= %s""", (lft, rgt)) From 65123d2adcff62ff01f99b043f278b1bfdbf3f89 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 8 May 2020 16:30:46 +0530 Subject: [PATCH 39/82] fix: Item Barcode stays the same after updation. --- erpnext/stock/doctype/item/item.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index c62b3ab583..4cc50bba9e 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -572,6 +572,13 @@ class Item(WebsiteGenerator): frappe.throw(_("Barcode {0} is not a valid {1} code").format( item_barcode.barcode, item_barcode.barcode_type), InvalidBarcode) + if item_barcode.barcode != item_barcode.name: + # if barcode is getting updated , the row name has to reset. + # Delete previous old row doc and re-enter row as if new to reset name in db. + item_barcode.set("__islocal", True) + item_barcode.name = None + frappe.delete_doc("Item Barcode", item_barcode.name) + def validate_warehouse_for_reorder(self): '''Validate Reorder level table for duplicate and conditional mandatory''' warehouse = [] From be6eb201b71150202f3c63fa2c632a5a9594cd95 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Fri, 8 May 2020 17:33:21 +0530 Subject: [PATCH 40/82] Appending Email and Phone in Child Table --- erpnext/selling/doctype/customer/customer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index d0db6d62a0..3d172ac7a2 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -164,9 +164,11 @@ class Customer(TransactionBase): contact.phone = lead.phone contact.mobile_no = lead.mobile_no contact.is_primary_contact = 1 - contact.append('email_ids', dict(email_id=lead.email_id, is_primary=1)) - contact.append('phone_nos', dict(phone=lead.mobile_no, is_primary_mobile_no=1)) contact.append('links', dict(link_doctype='Customer', link_name=self.name)) + if lead.email_id: + contact.append('email_ids', dict(email_id=lead.email_id, is_primary=1)) + if lead.mobile_no: + contact.append('phone_nos', dict(phone=lead.mobile_no, is_primary_mobile_no=1)) contact.flags.ignore_permissions = self.flags.ignore_permissions contact.autoname() if not frappe.db.exists("Contact", contact.name): From 8d9f86f78113de8dce8b4b8b193c7ab23430a143 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 8 May 2020 19:45:57 +0530 Subject: [PATCH 41/82] fix: Gross and net profit report fix --- .../gross_and_net_profit_report.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py index 1c458107d4..fb07472b22 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py @@ -9,8 +9,8 @@ from erpnext.accounts.report.financial_statements import (get_period_list, get_c import copy def execute(filters=None): - period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year, - filters.periodicity, filters.accumulated_values, filters.company) + period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year, '', '', + 'Fiscal Year', filters.periodicity, filters.accumulated_values, filters.company) columns, data = [], [] From 3392e6b80c9c8105e4ebca00d0c6e164f12baa84 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 8 May 2020 19:53:09 +0530 Subject: [PATCH 42/82] fix: User filters instead of hardcoded values --- .../gross_and_net_profit_report.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py index fb07472b22..714e48d279 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py @@ -9,8 +9,8 @@ from erpnext.accounts.report.financial_statements import (get_period_list, get_c import copy def execute(filters=None): - period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year, '', '', - 'Fiscal Year', filters.periodicity, filters.accumulated_values, filters.company) + period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year, filters.period_start_date, + filters.period_end_date, filters.filter_based_on, filters.periodicity, filters.accumulated_values, filters.company) columns, data = [], [] From bc058558052a9d3af7f151347f7af018aa403002 Mon Sep 17 00:00:00 2001 From: Kevin Chan Date: Sat, 9 May 2020 16:02:11 +0800 Subject: [PATCH 43/82] fix: Simplify get_dimension_account_month_map This commit updates get_dimension_account_month_map to no longer show the actual expense when there is no budget. This also removes the other functions and queries related to it. Spaces are also converted to tabs. --- .../budget_variance_report.py | 500 ++++++++---------- 1 file changed, 209 insertions(+), 291 deletions(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index 1b110b0aac..4d5892a913 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -13,164 +13,165 @@ from erpnext.controllers.trends import get_period_date_ranges, get_period_month_ def execute(filters=None): - if not filters: - filters = {} + if not filters: + filters = {} - columns = get_columns(filters) + columns = get_columns(filters) + if filters.get("budget_against_filter"): + dimensions = filters.get("budget_against_filter") + else: + dimensions = get_cost_centers(filters) - if filters.get("budget_against_filter"): - dimensions = filters.get("budget_against_filter") - else: - dimensions = get_cost_centers(filters) + period_month_ranges = get_period_month_ranges( + filters["period"], filters["from_fiscal_year"] + ) + cam_map = get_dimension_account_month_map(filters) - period_month_ranges = get_period_month_ranges( - filters["period"], filters["from_fiscal_year"] - ) + data = [] + for dimension in dimensions: + dimension_items = cam_map.get(dimension) + if dimension_items: + for account, monthwise_data in iteritems(dimension_items): + row = [dimension, account] + totals = [0, 0, 0] + for year in get_fiscal_years(filters): + last_total = 0 + for relevant_months in period_month_ranges: + period_data = [0, 0, 0] + for month in relevant_months: + if monthwise_data.get(year[0]): + month_data = monthwise_data.get(year[0]).get(month, {}) + for i, fieldname in enumerate( + ["target", "actual", "variance"] + ): + value = flt(month_data.get(fieldname)) + period_data[i] += value + totals[i] += value - cam_map = get_dimension_account_month_map(filters) + period_data[0] += last_total - data = [] - for dimension in dimensions: - dimension_items = cam_map.get(dimension) - if dimension_items: - for account, monthwise_data in iteritems(dimension_items): - row = [dimension, account] - totals = [0, 0, 0] - for year in get_fiscal_years(filters): - last_total = 0 - for relevant_months in period_month_ranges: - period_data = [0, 0, 0] - for month in relevant_months: - if monthwise_data.get(year[0]): - month_data = monthwise_data.get(year[0]).get(month, {}) - for i, fieldname in enumerate( - ["target", "actual", "variance"] - ): - value = flt(month_data.get(fieldname)) - period_data[i] += value - totals[i] += value + if filters.get("show_cumulative"): + last_total = period_data[0] - period_data[1] - period_data[0] += last_total + period_data[2] = period_data[0] - period_data[1] + row += period_data + totals[2] = totals[0] - totals[1] + if filters["period"] != "Yearly": + row += totals + data.append(row) - if filters.get("show_cumulative"): - last_total = period_data[0] - period_data[1] - - period_data[2] = period_data[0] - period_data[1] - row += period_data - totals[2] = totals[0] - totals[1] - if filters["period"] != "Yearly": - row += totals - data.append(row) - - return columns, data + return columns, data def get_columns(filters): - columns = [ - _(filters.get("budget_against")) - + ":Link/%s:150" % (filters.get("budget_against")), - _("Account") + ":Link/Account:150", - ] + columns = [ + _(filters.get("budget_against")) + + ":Link/%s:150" % (filters.get("budget_against")), + _("Account") + ":Link/Account:150", + ] - group_months = False if filters["period"] == "Monthly" else True + group_months = False if filters["period"] == "Monthly" else True - fiscal_year = get_fiscal_years(filters) + fiscal_year = get_fiscal_years(filters) - for year in fiscal_year: - for from_date, to_date in get_period_date_ranges(filters["period"], year[0]): - if filters["period"] == "Yearly": - labels = [ - _("Budget") + " " + str(year[0]), - _("Actual ") + " " + str(year[0]), - _("Variance ") + " " + str(year[0]), - ] - for label in labels: - columns.append(label + ":Float:150") - else: - for label in [ - _("Budget") + " (%s)" + " " + str(year[0]), - _("Actual") + " (%s)" + " " + str(year[0]), - _("Variance") + " (%s)" + " " + str(year[0]), - ]: - if group_months: - label = label % ( - formatdate(from_date, format_string="MMM") - + "-" - + formatdate(to_date, format_string="MMM") - ) - else: - label = label % formatdate(from_date, format_string="MMM") + for year in fiscal_year: + for from_date, to_date in get_period_date_ranges(filters["period"], year[0]): + if filters["period"] == "Yearly": + labels = [ + _("Budget") + " " + str(year[0]), + _("Actual ") + " " + str(year[0]), + _("Variance ") + " " + str(year[0]), + ] + for label in labels: + columns.append(label + ":Float:150") + else: + for label in [ + _("Budget") + " (%s)" + " " + str(year[0]), + _("Actual") + " (%s)" + " " + str(year[0]), + _("Variance") + " (%s)" + " " + str(year[0]), + ]: + if group_months: + label = label % ( + formatdate(from_date, format_string="MMM") + + "-" + + formatdate(to_date, format_string="MMM") + ) + else: + label = label % formatdate(from_date, format_string="MMM") - columns.append(label + ":Float:150") + columns.append(label + ":Float:150") - if filters["period"] != "Yearly": - return columns + [ - _("Total Budget") + ":Float:150", - _("Total Actual") + ":Float:150", - _("Total Variance") + ":Float:150", - ] - else: - return columns + if filters["period"] != "Yearly": + return columns + [ + _("Total Budget") + ":Float:150", + _("Total Actual") + ":Float:150", + _("Total Variance") + ":Float:150", + ] + else: + return columns def get_cost_centers(filters): - order_by = "" - if filters.get("budget_against") == "Cost Center": - order_by = "order by lft" + order_by = "" + if filters.get("budget_against") == "Cost Center": + order_by = "order by lft" - if filters.get("budget_against") in ["Cost Center", "Project"]: - return frappe.db.sql_list( - """ + if filters.get("budget_against") in ["Cost Center", "Project"]: + return frappe.db.sql_list( + """ select name from `tab{tab}` where - company=%s + company = %s {order_by} """.format( - tab=filters.get("budget_against"), order_by=order_by - ), - filters.get("company"), - ) - else: - return frappe.db.sql_list( - """ + tab=filters.get("budget_against"), order_by=order_by + ), + filters.get("company"), + ) + else: + return frappe.db.sql_list( + """ select name from `tab{tab}` """.format( - tab=filters.get("budget_against") - ) - ) # nosec + tab=filters.get("budget_against") + ) + ) # nosec # Get dimension & target details def get_dimension_target_details(filters): - budget_against = frappe.scrub(filters.get("budget_against")) + budget_against = frappe.scrub(filters.get("budget_against")) + cond = "" + if filters.get("budget_against_filter"): + cond += """ + and + b.{budget_against} in ( + %s + ) + """.format( + budget_against=budget_against + ) % ", ".join(["%s"] * len(filters.get("budget_against_filter"))) - cond = "" - if filters.get("budget_against_filter"): - cond += """ - and - b.{budget_against} in ( - %s - ) - """.format( - budget_against=budget_against - ) % ", ".join( - ["%s"] * len(filters.get("budget_against_filter")) - ) - - return frappe.db.sql( - """ - select distinct - b.{budget_against} as name + return frappe.db.sql( + """ + select + b.{budget_against} as budget_against, + b.monthly_distribution, + ba.account, + ba.budget_amount, + b.fiscal_year from - `tabBudget` b + `tabBudget` b, + `tabBudget Account` ba where - b.docstatus = 1 + b.name = ba.parent + and b.docstatus = 1 and b.fiscal_year between %s and %s and b.budget_against = %s and b.company = %s @@ -178,65 +179,66 @@ def get_dimension_target_details(filters): order by b.fiscal_year """.format( - budget_against=budget_against, cond=cond - ), - tuple( - [ - filters.from_fiscal_year, - filters.to_fiscal_year, - filters.budget_against, - filters.company, - ] - + filters.get("budget_against_filter") - ), - as_dict=True, - ) + budget_against=budget_against, + cond=cond, + ), + tuple( + [ + filters.from_fiscal_year, + filters.to_fiscal_year, + filters.budget_against, + filters.company, + ] + + filters.get("budget_against_filter") + ), + as_dict=True, + ) # Get target distribution details of accounts of cost center def get_target_distribution_details(filters): - target_details = {} - for d in frappe.db.sql( - """ - select - md.name, - mdp.month, - mdp.percentage_allocation - from - `tabMonthly Distribution Percentage` mdp, - `tabMonthly Distribution` md - where - mdp.parent = md.name - and md.fiscal_year between %s - and %s - order by md.fiscal_year - """, - (filters.from_fiscal_year, filters.to_fiscal_year), - as_dict=1, - ): - target_details.setdefault(d.name, {}).setdefault( - d.month, flt(d.percentage_allocation) - ) + target_details = {} + for d in frappe.db.sql( + """ + select + md.name, + mdp.month, + mdp.percentage_allocation + from + `tabMonthly Distribution Percentage` mdp, + `tabMonthly Distribution` md + where + mdp.parent = md.name + and md.fiscal_year between %s and %s + order by + md.fiscal_year + """, + (filters.from_fiscal_year, filters.to_fiscal_year), + as_dict=1, + ): + target_details.setdefault(d.name, {}).setdefault( + d.month, flt(d.percentage_allocation) + ) - return target_details + return target_details # Get actual details from gl entry def get_actual_details(name, filters): - budget_against = frappe.scrub(filters.get("budget_against")) + budget_against = frappe.scrub(filters.get("budget_against")) - cond = "" - if filters.get("budget_against") == "Cost Center": - cc_lft, cc_rgt = frappe.db.get_value("Cost Center", name, ["lft", "rgt"]) - cond = """ - and lft >= \'{lft}\' - and rgt <= \'{rgt}\' + cond = "" + if filters.get("budget_against") == "Cost Center": + cc_lft, cc_rgt = frappe.db.get_value("Cost Center", name, ["lft", "rgt"]) + cond = """ + and lft >= "{lft}" + and rgt <= "{rgt}" """.format( - lft=cc_lft, rgt=cc_rgt - ) + lft=cc_lft, rgt=cc_rgt + ) - ac_details = frappe.db.sql( - """ + ac_details = frappe.db.sql( + """ select gl.account, gl.debit, @@ -268,65 +270,56 @@ def get_actual_details(name, filters): gl.name order by gl.fiscal_year """.format( - tab=filters.budget_against, budget_against=budget_against, cond=cond - ), - (filters.from_fiscal_year, filters.to_fiscal_year, name), - as_dict=1, - ) + tab=filters.budget_against, budget_against=budget_against, cond=cond + ), + (filters.from_fiscal_year, filters.to_fiscal_year, name), + as_dict=1, + ) - cc_actual_details = {} - for d in ac_details: - cc_actual_details.setdefault(d.account, []).append(d) + cc_actual_details = {} + for d in ac_details: + cc_actual_details.setdefault(d.account, []).append(d) - return cc_actual_details + return cc_actual_details def get_dimension_account_month_map(filters): - dimension_target_details = get_dimension_target_details(filters) - tdd = get_target_distribution_details(filters) + dimension_target_details = get_dimension_target_details(filters) + tdd = get_target_distribution_details(filters) - cam_map = {} + cam_map = {} - for ccd in dimension_target_details: - accounts = get_accounts(ccd.name, filters) - actual_details = get_actual_details(ccd.name, filters) + for ccd in dimension_target_details: + actual_details = get_actual_details(ccd.budget_against, filters) - for year in get_fiscal_years(filters): - year = year[0] - monthly_distribution = get_monthly_distribution(ccd.name, year, filters) - for month_id in range(1, 13): - month = datetime.date(2013, month_id, 1).strftime( - "%B" - ) # Get month string + for month_id in range(1, 13): + month = datetime.date(2013, month_id, 1).strftime("%B") + cam_map.setdefault(ccd.budget_against, {}).setdefault( + ccd.account, {} + ).setdefault(ccd.fiscal_year, {}).setdefault( + month, frappe._dict({"target": 0.0, "actual": 0.0}) + ) - for account in accounts: - account = account[0] - cam_map.setdefault(ccd.name, {}).setdefault(account, {}).setdefault( - year, {} - ).setdefault(month, frappe._dict({"target": 0.0, "actual": 0.0})) + tav_dict = cam_map[ccd.budget_against][ccd.account][ccd.fiscal_year][month] + month_percentage = ( + tdd.get(ccd.monthly_distribution, {}).get(month, 0) + if ccd.monthly_distribution + else 100.0 / 12 + ) - tav_dict = cam_map[ccd.name][account][year][month] + tav_dict.target = flt(ccd.budget_amount) * month_percentage / 100 - month_percentage = ( - tdd.get(monthly_distribution, {}).get(month, 0) - if monthly_distribution - else 100.0 / 12 - ) + for ad in actual_details.get(ccd.account, []): + if ad.month_name == month and ad.fiscal_year == ccd.fiscal_year: + tav_dict.actual += flt(ad.debit) - flt(ad.credit) - budget_amount = get_budget_amount(ccd.name, year, account, filters) - - tav_dict.target = flt(budget_amount) * month_percentage / 100 - - for ad in actual_details.get(account, []): - if ad.month_name == month and ad.fiscal_year == year: - tav_dict.actual += flt(ad.debit) - flt(ad.credit) - return cam_map + return cam_map def get_fiscal_years(filters): - fiscal_year = frappe.db.sql( - """ + fiscal_year = frappe.db.sql( + """ select name from @@ -336,85 +329,10 @@ def get_fiscal_years(filters): order by year """, - { - "from_fiscal_year": filters["from_fiscal_year"], - "to_fiscal_year": filters["to_fiscal_year"], - }, - ) + { + "from_fiscal_year": filters["from_fiscal_year"], + "to_fiscal_year": filters["to_fiscal_year"], + }, + ) - return fiscal_year - - -def get_accounts(name, filters): - budget_against = frappe.scrub(filters.get("budget_against")) - - accounts = frappe.db.sql( - """ - select - distinct(ba.account) - from - `tabBudget Account` ba - join - `tabBudget` b - on b.name = ba.parent - where - b.docstatus = 1 - and b.fiscal_year between %s and %s - and b.{budget_against} = %s - order by - ba.account - """.format( - budget_against=budget_against - ), - (filters.from_fiscal_year, filters.to_fiscal_year, name), - ) - - return accounts - - -def get_monthly_distribution(name, year, filters): - budget_against = frappe.scrub(filters.get("budget_against")) - - monthly_distribution = frappe.db.sql( - """ - select - monthly_distribution - from - `tabBudget` - where - docstatus = 1 - and {budget_against} = %s - and fiscal_year = %s - """.format( - budget_against=budget_against - ), - (name, year), - ) - - return monthly_distribution[0][0] if monthly_distribution else None - - -def get_budget_amount(name, year, account, filters): - budget_against = frappe.scrub(filters.get("budget_against")) - - budget_amount = frappe.db.sql( - """ - select - ba.budget_amount - from - `tabBudget Account` ba - join - `tabBudget` b - on b.name = ba.parent - where - b.docstatus = 1 - and b.{budget_against} = %s - and b.fiscal_year = %s - and ba.account = %s - """.format( - budget_against=budget_against - ), - (name, year, account), - ) - - return budget_amount[0][0] if budget_amount else 0 + return fiscal_year From aadf264b497677a6d962f3086f5d7fabc6528f17 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Sat, 9 May 2020 16:15:51 +0530 Subject: [PATCH 44/82] Assessment Plan not getting created --- .../assessment_plan/assessment_plan.json | 702 ++---------------- 1 file changed, 54 insertions(+), 648 deletions(-) diff --git a/erpnext/education/doctype/assessment_plan/assessment_plan.json b/erpnext/education/doctype/assessment_plan/assessment_plan.json index bc3946440c..95ed853c21 100644 --- a/erpnext/education/doctype/assessment_plan/assessment_plan.json +++ b/erpnext/education/doctype/assessment_plan/assessment_plan.json @@ -1,790 +1,207 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, + "actions": [], "allow_import": 1, - "allow_rename": 0, "autoname": "EDU-ASP-.YYYY.-.#####", - "beta": 0, "creation": "2015-11-12 16:34:34.658092", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "Setup", - "editable_grid": 0, "engine": "InnoDB", + "field_order": [ + "student_group", + "assessment_name", + "assessment_group", + "grading_scale", + "column_break_2", + "course", + "program", + "academic_year", + "academic_term", + "section_break_5", + "schedule_date", + "room", + "examiner", + "examiner_name", + "column_break_4", + "from_time", + "to_time", + "supervisor", + "supervisor_name", + "section_break_20", + "maximum_assessment_score", + "assessment_criteria", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "student_group", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, "in_list_view": 1, "in_standard_filter": 1, "label": "Student Group", - "length": 0, - "no_copy": 0, "options": "Student Group", - "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 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "assessment_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": "Assessment 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 + "label": "Assessment Name" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "assessment_group", "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": 1, "label": "Assessment Group", - "length": 0, - "no_copy": 0, "options": "Assessment Group", - "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 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "course.default_grading_scale", + "fetch_if_empty": 1, "fieldname": "grading_scale", "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": 1, "label": "Grading Scale", - "length": 0, - "no_copy": 0, "options": "Grading Scale", - "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 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_2", - "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 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "student_group.course", + "fetch_if_empty": 1, "fieldname": "course", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, - "in_list_view": 0, "in_standard_filter": 1, "label": "Course", - "length": 0, - "no_copy": 0, "options": "Course", - "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 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "student_group.program", "fieldname": "program", "fieldtype": "Link", - "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": "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": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Program" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "student_group.academic_year", "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 + "options": "Academic Year" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "student_group.academic_term", "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 + "options": "Academic Term" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": "", - "columns": 0, - "depends_on": "", "fieldname": "section_break_5", "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": "Schedule", - "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 + "label": "Schedule" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Today", "fieldname": "schedule_date", "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Schedule Date", - "length": 0, "no_copy": 1, - "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 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "room", "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": "Room", - "length": 0, - "no_copy": 0, - "options": "Room", - "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 + "options": "Room" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "examiner", "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": "Examiner", - "length": 0, - "no_copy": 0, - "options": "Instructor", - "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 + "options": "Instructor" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "examiner.instructor_name", "fieldname": "examiner_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": "Examiner Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "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 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_4", - "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 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "from_time", "fieldtype": "Time", - "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": "From Time", - "length": 0, "no_copy": 1, - "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 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "to_time", "fieldtype": "Time", - "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": "To Time", - "length": 0, "no_copy": 1, - "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 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "supervisor", "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": "Supervisor", - "length": 0, - "no_copy": 0, - "options": "Instructor", - "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 + "options": "Instructor" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "supervisor.instructor_name", "fieldname": "supervisor_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": "Supervisor Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "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 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "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": "Evaluate", - "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 + "label": "Evaluate" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "maximum_assessment_score", "fieldtype": "Float", - "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": "Maximum Assessment Score", - "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 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "assessment_criteria", "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": "Assessment Criteria", - "length": 0, - "no_copy": 0, "options": "Assessment Plan Criteria", - "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 + "reqd": 1 }, { - "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": "Assessment Plan", - "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 + "read_only": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2018-08-30 00:48:03.475522", + "links": [], + "modified": "2020-05-09 14:56:26.746988", "modified_by": "Administrator", "module": "Education", "name": "Assessment Plan", - "name_case": "", "owner": "Administrator", "permissions": [ { @@ -794,28 +211,17 @@ "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Academics User", - "set_user_permissions": 0, "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": 0, "sort_field": "modified", "sort_order": "DESC", - "title_field": "assessment_name", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + "title_field": "assessment_name" } \ No newline at end of file From 65d3ac814d053cd1c191c8345bf8226fde4d75a7 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Sat, 9 May 2020 17:39:59 +0530 Subject: [PATCH 45/82] fix: Add accidentally removed function --- erpnext/accounts/dashboard_fixtures.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/dashboard_fixtures.py b/erpnext/accounts/dashboard_fixtures.py index 30eb5e3115..a106f70dd0 100644 --- a/erpnext/accounts/dashboard_fixtures.py +++ b/erpnext/accounts/dashboard_fixtures.py @@ -107,6 +107,10 @@ def get_charts(): } ] +def get_account(account_type, company): + accounts = frappe.get_list("Account", filters={"account_type": account_type, "company": company}) + if accounts: + return accounts[0].name def get_company_for_dashboards(): company = frappe.defaults.get_defaults().company From d543830a59d1a87bf6de83244303347ac168e769 Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Sat, 9 May 2020 21:56:53 +0530 Subject: [PATCH 46/82] feat(Selling): Added Territory wise treeview to 'Customer Acquistion and Loyalty' report --- .../customer_acquisition_and_loyalty.js | 20 +- .../customer_acquisition_and_loyalty.py | 239 ++++++++++++++---- erpnext/setup/doctype/territory/territory.py | 5 +- 3 files changed, 212 insertions(+), 52 deletions(-) diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js index a854fa9969..654614b4bb 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js @@ -3,6 +3,13 @@ frappe.query_reports["Customer Acquisition and Loyalty"] = { "filters": [ + { + "fieldname":"view_type", + "label": __("View Type"), + "fieldtype": "Select", + "default": "Time Series", + "options": ["Time Series", "Territory Tree"] + }, { "fieldname":"company", "label": __("Company"), @@ -24,6 +31,13 @@ frappe.query_reports["Customer Acquisition and Loyalty"] = { "fieldtype": "Date", "default": frappe.defaults.get_user_default("year_end_date"), "reqd": 1 - }, - ] -} + } + ], + 'formatter': function(value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + if (data && data.bold) { + value = value.bold(); + } + return value + } +} \ No newline at end of file diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index aa57665a81..f2033f1fff 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -2,65 +2,210 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals +import calendar import frappe from frappe import _ -from frappe.utils import getdate, cint, cstr -import calendar +from frappe.utils import cint, cstr def execute(filters=None): - # key yyyy-mm - new_customers_in = {} - repeat_customers_in = {} - customers = [] - company_condition = "" + common_columns = [ + { + 'label': _('New Customers'), + 'fieldname': 'new_customers', + 'fieldtype': 'Int', + 'default': 0, + 'width': 100 + }, + { + 'label': _('Repeat Customers'), + 'fieldname': 'repeat_customers', + 'fieldtype': 'Int', + 'default': 0, + 'width': 100 + }, + { + 'label': _('Total'), + 'fieldname': 'total', + 'fieldtype': 'Int', + 'default': 0, + 'width': 100 + }, + { + 'label': _('New Customer Revenue'), + 'fieldname': 'new_customer_revenue', + 'fieldtype': 'Currency', + 'default': 0.0, + 'width': 150 + }, + { + 'label': _('Repeat Customer Revenue'), + 'fieldname': 'repeat_customer_revenue', + 'fieldtype': 'Currency', + 'default': 0.0, + 'width': 150 + }, + { + 'label': _('Total Revenue'), + 'fieldname': 'total_revenue', + 'fieldtype': 'Currency', + 'default': 0.0, + 'width': 150 + } + ] + if filters.get('view_type') == 'Territory Tree': + return get_data_by_territory(filters, common_columns) + else: + return get_data_by_time(filters, common_columns) - if filters.get("company"): - company_condition = ' and company=%(company)s' +def get_data_by_time(filters, common_columns): + # key yyyy-mm + columns = [ + { + 'label': 'Year', + 'fieldname': 'year', + 'fieldtype': 'Data', + 'width': 100 + }, + { + 'label': 'Month', + 'fieldname': 'month', + 'fieldtype': 'Data', + 'width': 100 + }, + ] + columns += common_columns - for si in frappe.db.sql("""select posting_date, customer, base_grand_total from `tabSales Invoice` - where docstatus=1 and posting_date <= %(to_date)s - {company_condition} order by posting_date""".format(company_condition=company_condition), - filters, as_dict=1): + customers_in = get_customer_stats(filters) - key = si.posting_date.strftime("%Y-%m") - if not si.customer in customers: - new_customers_in.setdefault(key, [0, 0.0]) - new_customers_in[key][0] += 1 - new_customers_in[key][1] += si.base_grand_total - customers.append(si.customer) - else: - repeat_customers_in.setdefault(key, [0, 0.0]) - repeat_customers_in[key][0] += 1 - repeat_customers_in[key][1] += si.base_grand_total + # time series + from_year, from_month, temp = filters.get('from_date').split('-') + to_year, to_month, temp = filters.get('to_date').split('-') - # time series - from_year, from_month, temp = filters.get("from_date").split("-") - to_year, to_month, temp = filters.get("to_date").split("-") + from_year, from_month, to_year, to_month = \ + cint(from_year), cint(from_month), cint(to_year), cint(to_month) - from_year, from_month, to_year, to_month = \ - cint(from_year), cint(from_month), cint(to_year), cint(to_month) + out = [] + for year in range(from_year, to_year+1): + for month in range(from_month if year==from_year else 1, (to_month+1) if year==to_year else 13): + key = '{year}-{month:02d}'.format(year=year, month=month) + data = customers_in.get(key) + new = data['new'] if data else [0, 0.0] + repeat = data['repeat'] if data else [0, 0.0] + out.append({ + 'year': cstr(year), + 'month': calendar.month_name[month], + 'new_customers': new[0], + 'repeat_customers': repeat[0], + 'total': new[0] + repeat[0], + 'new_customer_revenue': new[1], + 'repeat_customer_revenue': repeat[1], + 'total_revenue': new[1] + repeat[1] + }) + return columns, out - out = [] - for year in range(from_year, to_year+1): - for month in range(from_month if year==from_year else 1, (to_month+1) if year==to_year else 13): - key = "{year}-{month:02d}".format(year=year, month=month) +def get_data_by_territory(filters, common_columns): + columns = [{ + 'label': 'Territory', + 'fieldname': 'territory', + 'fieldtype': 'Link', + 'options': 'Territory', + 'width': 150 + }] + columns += common_columns - new = new_customers_in.get(key, [0,0.0]) - repeat = repeat_customers_in.get(key, [0,0.0]) + customers_in = get_customer_stats(filters, tree_view=True) - out.append([cstr(year), calendar.month_name[month], - new[0], repeat[0], new[0] + repeat[0], - new[1], repeat[1], new[1] + repeat[1]]) + territory_dict = {} + for t in frappe.db.sql('''SELECT name, lft, parent_territory, is_group FROM `tabTerritory` ORDER BY lft''', as_dict=1): + territory_dict.update({ + t.name: { + 'parent': t.parent_territory, + 'is_group': t.is_group + } + }) - return [ - _("Year") + "::100", - _("Month") + "::100", - _("New Customers") + ":Int:100", - _("Repeat Customers") + ":Int:100", - _("Total") + ":Int:100", - _("New Customer Revenue") + ":Currency:150", - _("Repeat Customer Revenue") + ":Currency:150", - _("Total Revenue") + ":Currency:150" - ], out + depth_map = frappe._dict() + for name, info in territory_dict.items(): + default = depth_map.get(info['parent']) + 1 if info['parent'] else 0 + depth_map.setdefault(name, default) + data = [] + for name, indent in depth_map.items(): + condition = customers_in.get(name) + new = customers_in[name]['new'] if condition else [0, 0.0] + repeat = customers_in[name]['repeat'] if condition else [0, 0.0] + temp = { + 'territory': name, + 'indent': indent, + 'new_customers': new[0], + 'repeat_customers': repeat[0], + 'total': new[0] + repeat[0], + 'new_customer_revenue': new[1], + 'repeat_customer_revenue': repeat[1], + 'total_revenue': new[1] + repeat[1], + 'bold': 0 if condition else 1 + } + data.append(temp) + node_list = [x for x in territory_dict.keys() if territory_dict[x]['is_group'] == 0] + root_node = [x for x in territory_dict.keys() if territory_dict[x]['parent'] is None][0] + for node in node_list: + data = update_groups(node, data, root_node, territory_dict) + + for group in [x for x in territory_dict.keys() if territory_dict[x]['parent'] == root_node]: + group_data = [x for x in data if x['territory'] == group][0] + root_data = [x for x in data if x['territory'] == root_node][0] + for key in group_data.keys(): + if key not in ['indent', 'territory', 'bold']: + root_data[key] += group_data[key] + + return columns, data, None, None, None, 1 + +def update_groups(node, data, root_node, territory_dict): + ''' Adds values of child territories to parent node except root ''' + parent_node = territory_dict[node]['parent'] + if parent_node != root_node and parent_node: + node_data = [x for x in data if x['territory'] == node][0] + parent_data = [x for x in data if x['territory'] == parent_node][0] + for key in parent_data.keys(): + if key not in ['indent', 'territory', 'bold']: + parent_data[key] += node_data[key] + return update_groups(parent_node, data, root_node, territory_dict) + else: + return data + +def get_customer_stats(filters, tree_view=False): + ''' Calculates number of new and repeated customers ''' + company_condition = '' + if filters.get('company'): + company_condition = ' and company=%(company)s' + + customers = [] + customers_in = {} + new_customers_in = {} + repeat_customers_in = {} + + for si in frappe.db.sql('''select territory, posting_date, customer, base_grand_total from `tabSales Invoice` + where docstatus=1 and posting_date <= %(to_date)s and posting_date >= %(from_date)s + {company_condition} order by posting_date'''.format(company_condition=company_condition), + filters, as_dict=1): + if tree_view: + key = si.territory + else: + key = si.posting_date.strftime('%Y-%m') + if not si.customer in customers: + new_customers_in.setdefault(key, [0, 0.0]) + new_customers_in[key][0] += 1 + new_customers_in[key][1] += si.base_grand_total + customers.append(si.customer) + else: + repeat_customers_in.setdefault(key, [0, 0.0]) + repeat_customers_in[key][0] += 1 + repeat_customers_in[key][1] += si.base_grand_total + customers_in.update({ + key: { + 'new': new_customers_in[key] if new_customers_in.get(key) else [0, 0.0], + 'repeat': repeat_customers_in[key] if repeat_customers_in.get(key) else [0, 0.0], + } + }) + return customers_in diff --git a/erpnext/setup/doctype/territory/territory.py b/erpnext/setup/doctype/territory/territory.py index 095bd1c179..4f2ab70b2c 100644 --- a/erpnext/setup/doctype/territory/territory.py +++ b/erpnext/setup/doctype/territory/territory.py @@ -3,8 +3,6 @@ from __future__ import unicode_literals import frappe - - from frappe.utils import flt from frappe import _ @@ -14,6 +12,9 @@ class Territory(NestedSet): nsm_parent_field = 'parent_territory' def validate(self): + if frappe.db.sql("SELECT COUNT(name) FROM `tabTerritory` WHERE parent IS NULL")[0][0] > 1: + frappe.throw('Only one Root Territory is allowed, please select a Parent Territory!') + for d in self.get('targets') or []: if not flt(d.target_qty) and not flt(d.target_amount): frappe.throw(_("Either target qty or target amount is mandatory")) From b59f2780ebf974779b4b0236e67e3f1bbee7781a Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Sat, 9 May 2020 22:09:02 +0530 Subject: [PATCH 47/82] fix: renamed view types, added default --- .../customer_acquisition_and_loyalty.js | 7 ++++--- .../customer_acquisition_and_loyalty.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js index 654614b4bb..c24d2e2bdd 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js @@ -4,11 +4,12 @@ frappe.query_reports["Customer Acquisition and Loyalty"] = { "filters": [ { - "fieldname":"view_type", + "fieldname": "view_type", "label": __("View Type"), "fieldtype": "Select", - "default": "Time Series", - "options": ["Time Series", "Territory Tree"] + "options": ["Monthly", "Territory Wise"], + "default": "Monthly", + "reqd": 1 }, { "fieldname":"company", diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index f2033f1fff..d8cc763ed9 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -52,7 +52,7 @@ def execute(filters=None): 'width': 150 } ] - if filters.get('view_type') == 'Territory Tree': + if filters.get('view_type') == 'Territory Wise': return get_data_by_territory(filters, common_columns) else: return get_data_by_time(filters, common_columns) From 7cbc902d90a3f43b985c9c12abbf0ab8caf82dc2 Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Sat, 9 May 2020 22:35:28 +0530 Subject: [PATCH 48/82] removed validation for root node in territory, codacy recommended changed --- .../customer_acquisition_and_loyalty.js | 2 +- .../customer_acquisition_and_loyalty.py | 4 ++-- erpnext/setup/doctype/territory/territory.py | 2 -- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js index c24d2e2bdd..d93ffb7266 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js @@ -39,6 +39,6 @@ frappe.query_reports["Customer Acquisition and Loyalty"] = { if (data && data.bold) { value = value.bold(); } - return value + return value; } } \ No newline at end of file diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index d8cc763ed9..0121a8267f 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -162,7 +162,7 @@ def get_data_by_territory(filters, common_columns): return columns, data, None, None, None, 1 def update_groups(node, data, root_node, territory_dict): - ''' Adds values of child territories to parent node except root ''' + ''' Adds values of child territories to parent node except root. ''' parent_node = territory_dict[node]['parent'] if parent_node != root_node and parent_node: node_data = [x for x in data if x['territory'] == node][0] @@ -175,7 +175,7 @@ def update_groups(node, data, root_node, territory_dict): return data def get_customer_stats(filters, tree_view=False): - ''' Calculates number of new and repeated customers ''' + ''' Calculates number of new and repeated customers. ''' company_condition = '' if filters.get('company'): company_condition = ' and company=%(company)s' diff --git a/erpnext/setup/doctype/territory/territory.py b/erpnext/setup/doctype/territory/territory.py index 4f2ab70b2c..808b5386ab 100644 --- a/erpnext/setup/doctype/territory/territory.py +++ b/erpnext/setup/doctype/territory/territory.py @@ -12,8 +12,6 @@ class Territory(NestedSet): nsm_parent_field = 'parent_territory' def validate(self): - if frappe.db.sql("SELECT COUNT(name) FROM `tabTerritory` WHERE parent IS NULL")[0][0] > 1: - frappe.throw('Only one Root Territory is allowed, please select a Parent Territory!') for d in self.get('targets') or []: if not flt(d.target_qty) and not flt(d.target_amount): From 5ec5584319e18341e839d354bd251b9b7a382ca7 Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Sun, 10 May 2020 00:24:43 +0530 Subject: [PATCH 49/82] In treeview, bold only for root territory, looks cleaner --- .../customer_acquisition_and_loyalty.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index 0121a8267f..b7bb021056 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -143,7 +143,7 @@ def get_data_by_territory(filters, common_columns): 'new_customer_revenue': new[1], 'repeat_customer_revenue': repeat[1], 'total_revenue': new[1] + repeat[1], - 'bold': 0 if condition else 1 + 'bold': 0 if indent else 1 } data.append(temp) node_list = [x for x in territory_dict.keys() if territory_dict[x]['is_group'] == 0] From 500dff63e764d7a679747546f15f1ee671a2fdc8 Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Sun, 10 May 2020 14:53:59 +0530 Subject: [PATCH 50/82] fix: adjusted width of colums to see full column names, also fixes #21556 --- .../customer_acquisition_and_loyalty.py | 12 ++++---- .../territory_wise_sales.py | 30 +++++++++++-------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index b7bb021056..6e3f397fd6 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -14,21 +14,21 @@ def execute(filters=None): 'fieldname': 'new_customers', 'fieldtype': 'Int', 'default': 0, - 'width': 100 + 'width': 150 }, { 'label': _('Repeat Customers'), 'fieldname': 'repeat_customers', 'fieldtype': 'Int', 'default': 0, - 'width': 100 + 'width': 150 }, { 'label': _('Total'), 'fieldname': 'total', 'fieldtype': 'Int', 'default': 0, - 'width': 100 + 'width': 150 }, { 'label': _('New Customer Revenue'), @@ -52,10 +52,10 @@ def execute(filters=None): 'width': 150 } ] - if filters.get('view_type') == 'Territory Wise': - return get_data_by_territory(filters, common_columns) - else: + if filters.get('view_type') == 'Monthly': return get_data_by_time(filters, common_columns) + else: + return get_data_by_territory(filters, common_columns) def get_data_by_time(filters, common_columns): # key yyyy-mm diff --git a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py index f2db478686..e883500170 100644 --- a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py +++ b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py @@ -20,31 +20,36 @@ def get_columns(): "label": _("Territory"), "fieldname": "territory", "fieldtype": "Link", - "options": "Territory" + "options": "Territory", + "width": 150 }, { "label": _("Opportunity Amount"), "fieldname": "opportunity_amount", "fieldtype": "Currency", - "options": currency + "options": currency, + "width": 150 }, { "label": _("Quotation Amount"), "fieldname": "quotation_amount", "fieldtype": "Currency", - "options": currency + "options": currency, + "width": 150 }, { "label": _("Order Amount"), "fieldname": "order_amount", "fieldtype": "Currency", - "options": currency + "options": currency, + "width": 150 }, { "label": _("Billing Amount"), "fieldname": "billing_amount", "fieldtype": "Currency", - "options": currency + "options": currency, + "width": 150 } ] @@ -62,8 +67,7 @@ def get_data(filters=None): territory_opportunities = list(filter(lambda x: x.territory == territory.name, opportunities)) t_opportunity_names = [] if territory_opportunities: - t_opportunity_names = [t.name for t in territory_opportunities] - + t_opportunity_names = [t.name for t in territory_opportunities] territory_quotations = [] if t_opportunity_names and quotations: territory_quotations = list(filter(lambda x: x.opportunity in t_opportunity_names, quotations)) @@ -76,7 +80,7 @@ def get_data(filters=None): list(filter(lambda x: x.quotation in t_quotation_names, sales_orders)) t_order_names = [] if territory_orders: - t_order_names = [t.name for t in territory_orders] + t_order_names = [t.name for t in territory_orders] territory_invoices = list(filter(lambda x: x.sales_order in t_order_names, sales_invoices)) if t_order_names and sales_invoices else [] @@ -96,12 +100,12 @@ def get_opportunities(filters): if filters.get('transaction_date'): conditions = " WHERE transaction_date between {0} and {1}".format( - frappe.db.escape(filters['transaction_date'][0]), + frappe.db.escape(filters['transaction_date'][0]), frappe.db.escape(filters['transaction_date'][1])) - + if filters.company: if conditions: - conditions += " AND" + conditions += " AND" else: conditions += " WHERE" conditions += " company = %(company)s" @@ -115,7 +119,7 @@ def get_opportunities(filters): def get_quotations(opportunities): if not opportunities: return [] - + opportunity_names = [o.name for o in opportunities] return frappe.db.sql(""" @@ -155,5 +159,5 @@ def _get_total(doclist, amount_field="base_grand_total"): total = 0 for doc in doclist: total += doc.get(amount_field, 0) - + return total From b81bd5f70c13e269a6a28bbd28022978d1d2a46f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 10 May 2020 17:24:11 +0530 Subject: [PATCH 51/82] fix: Formatting fixes --- .../budget_variance_report.py | 69 +++++-------------- 1 file changed, 19 insertions(+), 50 deletions(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index 4d5892a913..49c1d0f2cc 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -22,9 +22,7 @@ def execute(filters=None): else: dimensions = get_cost_centers(filters) - period_month_ranges = get_period_month_ranges( - filters["period"], filters["from_fiscal_year"] - ) + period_month_ranges = get_period_month_ranges(filters["period"], filters["from_fiscal_year"]) cam_map = get_dimension_account_month_map(filters) data = [] @@ -41,9 +39,7 @@ def execute(filters=None): for month in relevant_months: if monthwise_data.get(year[0]): month_data = monthwise_data.get(year[0]).get(month, {}) - for i, fieldname in enumerate( - ["target", "actual", "variance"] - ): + for i, fieldname in enumerate(["target", "actual", "variance"]): value = flt(month_data.get(fieldname)) period_data[i] += value totals[i] += value @@ -67,7 +63,7 @@ def get_columns(filters): columns = [ _(filters.get("budget_against")) + ":Link/%s:150" % (filters.get("budget_against")), - _("Account") + ":Link/Account:150", + _("Account") + ":Link/Account:150" ] group_months = False if filters["period"] == "Monthly" else True @@ -80,7 +76,7 @@ def get_columns(filters): labels = [ _("Budget") + " " + str(year[0]), _("Actual ") + " " + str(year[0]), - _("Variance ") + " " + str(year[0]), + _("Variance ") + " " + str(year[0]) ] for label in labels: columns.append(label + ":Float:150") @@ -88,7 +84,7 @@ def get_columns(filters): for label in [ _("Budget") + " (%s)" + " " + str(year[0]), _("Actual") + " (%s)" + " " + str(year[0]), - _("Variance") + " (%s)" + " " + str(year[0]), + _("Variance") + " (%s)" + " " + str(year[0]) ]: if group_months: label = label % ( @@ -105,7 +101,7 @@ def get_columns(filters): return columns + [ _("Total Budget") + ":Float:150", _("Total Actual") + ":Float:150", - _("Total Variance") + ":Float:150", + _("Total Variance") + ":Float:150" ] else: return columns @@ -126,11 +122,8 @@ def get_cost_centers(filters): where company = %s {order_by} - """.format( - tab=filters.get("budget_against"), order_by=order_by - ), - filters.get("company"), - ) + """.format(tab=filters.get("budget_against"), order_by=order_by), + filters.get("company")) else: return frappe.db.sql_list( """ @@ -138,10 +131,7 @@ def get_cost_centers(filters): name from `tab{tab}` - """.format( - tab=filters.get("budget_against") - ) - ) # nosec + """.format(tab=filters.get("budget_against"))) # nosec # Get dimension & target details @@ -149,14 +139,8 @@ def get_dimension_target_details(filters): budget_against = frappe.scrub(filters.get("budget_against")) cond = "" if filters.get("budget_against_filter"): - cond += """ - and - b.{budget_against} in ( - %s - ) - """.format( - budget_against=budget_against - ) % ", ".join(["%s"] * len(filters.get("budget_against_filter"))) + cond += """ and b.{budget_against} in (%s)""".format( + budget_against=budget_against) % ", ".join(["%s"] * len(filters.get("budget_against_filter"))) return frappe.db.sql( """ @@ -190,9 +174,7 @@ def get_dimension_target_details(filters): filters.company, ] + filters.get("budget_against_filter") - ), - as_dict=True, - ) + ), as_dict=True) # Get target distribution details of accounts of cost center @@ -213,29 +195,24 @@ def get_target_distribution_details(filters): order by md.fiscal_year """, - (filters.from_fiscal_year, filters.to_fiscal_year), - as_dict=1, - ): + (filters.from_fiscal_year, filters.to_fiscal_year), as_dict=1): target_details.setdefault(d.name, {}).setdefault( d.month, flt(d.percentage_allocation) ) return target_details - # Get actual details from gl entry def get_actual_details(name, filters): budget_against = frappe.scrub(filters.get("budget_against")) - cond = "" + if filters.get("budget_against") == "Cost Center": cc_lft, cc_rgt = frappe.db.get_value("Cost Center", name, ["lft", "rgt"]) cond = """ and lft >= "{lft}" and rgt <= "{rgt}" - """.format( - lft=cc_lft, rgt=cc_rgt - ) + """.format(lft=cc_lft, rgt=cc_rgt) ac_details = frappe.db.sql( """ @@ -269,12 +246,8 @@ def get_actual_details(name, filters): group by gl.name order by gl.fiscal_year - """.format( - tab=filters.budget_against, budget_against=budget_against, cond=cond - ), - (filters.from_fiscal_year, filters.to_fiscal_year, name), - as_dict=1, - ) + """.format(tab=filters.budget_against, budget_against=budget_against, cond=cond), + (filters.from_fiscal_year, filters.to_fiscal_year, name), as_dict=1) cc_actual_details = {} for d in ac_details: @@ -282,7 +255,6 @@ def get_actual_details(name, filters): return cc_actual_details - def get_dimension_account_month_map(filters): dimension_target_details = get_dimension_target_details(filters) tdd = get_target_distribution_details(filters) @@ -326,13 +298,10 @@ def get_fiscal_years(filters): `tabFiscal Year` where name between %(from_fiscal_year)s and %(to_fiscal_year)s - order by - year """, { "from_fiscal_year": filters["from_fiscal_year"], - "to_fiscal_year": filters["to_fiscal_year"], - }, - ) + "to_fiscal_year": filters["to_fiscal_year"] + }) return fiscal_year From 3a914a7cd74abf5445e1d9d45b84c3b307229911 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 11 May 2020 11:44:45 +0530 Subject: [PATCH 52/82] fix: '<' not supported between instances of 'str' and 'int' --- 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 ddf4ec0393..10f2555475 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -181,7 +181,7 @@ class StockEntry(StockController): stock_items = self.get_stock_items() serialized_items = self.get_serialized_items() for item in self.get("items"): - if item.qty and item.qty < 0: + if flt(item.qty) and flt(item.qty) < 0: frappe.throw(_("Row {0}: The item {1}, quantity must be positive number") .format(item.idx, frappe.bold(item.item_code))) From 87776c335beb693c471608fa318a4997171f2dbd Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Mon, 11 May 2020 15:18:40 +0530 Subject: [PATCH 53/82] code improvements --- .../customer_acquisition_and_loyalty.py | 45 ++++++++----------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index 6e3f397fd6..38fbd60008 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -14,42 +14,42 @@ def execute(filters=None): 'fieldname': 'new_customers', 'fieldtype': 'Int', 'default': 0, - 'width': 150 + 'width': 125 }, { 'label': _('Repeat Customers'), 'fieldname': 'repeat_customers', 'fieldtype': 'Int', 'default': 0, - 'width': 150 + 'width': 125 }, { 'label': _('Total'), 'fieldname': 'total', 'fieldtype': 'Int', 'default': 0, - 'width': 150 + 'width': 100 }, { 'label': _('New Customer Revenue'), 'fieldname': 'new_customer_revenue', 'fieldtype': 'Currency', 'default': 0.0, - 'width': 150 + 'width': 175 }, { 'label': _('Repeat Customer Revenue'), 'fieldname': 'repeat_customer_revenue', 'fieldtype': 'Currency', 'default': 0.0, - 'width': 150 + 'width': 175 }, { 'label': _('Total Revenue'), 'fieldname': 'total_revenue', 'fieldtype': 'Currency', 'default': 0.0, - 'width': 150 + 'width': 175 } ] if filters.get('view_type') == 'Monthly': @@ -148,13 +148,13 @@ def get_data_by_territory(filters, common_columns): data.append(temp) node_list = [x for x in territory_dict.keys() if territory_dict[x]['is_group'] == 0] root_node = [x for x in territory_dict.keys() if territory_dict[x]['parent'] is None][0] + root_data = [x for x in data if x['territory'] == root_node][0] for node in node_list: data = update_groups(node, data, root_node, territory_dict) for group in [x for x in territory_dict.keys() if territory_dict[x]['parent'] == root_node]: group_data = [x for x in data if x['territory'] == group][0] - root_data = [x for x in data if x['territory'] == root_node][0] for key in group_data.keys(): if key not in ['indent', 'territory', 'bold']: root_data[key] += group_data[key] @@ -162,7 +162,7 @@ def get_data_by_territory(filters, common_columns): return columns, data, None, None, None, 1 def update_groups(node, data, root_node, territory_dict): - ''' Adds values of child territories to parent node except root. ''' + """ Adds values of child territories to parent node except root. """ parent_node = territory_dict[node]['parent'] if parent_node != root_node and parent_node: node_data = [x for x in data if x['territory'] == node][0] @@ -175,37 +175,28 @@ def update_groups(node, data, root_node, territory_dict): return data def get_customer_stats(filters, tree_view=False): - ''' Calculates number of new and repeated customers. ''' + """ Calculates number of new and repeated customers. """ company_condition = '' if filters.get('company'): company_condition = ' and company=%(company)s' customers = [] customers_in = {} - new_customers_in = {} - repeat_customers_in = {} for si in frappe.db.sql('''select territory, posting_date, customer, base_grand_total from `tabSales Invoice` where docstatus=1 and posting_date <= %(to_date)s and posting_date >= %(from_date)s {company_condition} order by posting_date'''.format(company_condition=company_condition), filters, as_dict=1): - if tree_view: - key = si.territory - else: - key = si.posting_date.strftime('%Y-%m') + + key = si.territory if tree_view else si.posting_date.strftime('%Y-%m') + customers_in.setdefault(key, {'new': [0, 0.0], 'repeat': [0, 0.0]}) + if not si.customer in customers: - new_customers_in.setdefault(key, [0, 0.0]) - new_customers_in[key][0] += 1 - new_customers_in[key][1] += si.base_grand_total + customers_in[key]['new'][0] += 1 + customers_in[key]['new'][1] += si.base_grand_total customers.append(si.customer) else: - repeat_customers_in.setdefault(key, [0, 0.0]) - repeat_customers_in[key][0] += 1 - repeat_customers_in[key][1] += si.base_grand_total - customers_in.update({ - key: { - 'new': new_customers_in[key] if new_customers_in.get(key) else [0, 0.0], - 'repeat': repeat_customers_in[key] if repeat_customers_in.get(key) else [0, 0.0], - } - }) + customers_in[key]['repeat'][0] += 1 + customers_in[key]['repeat'][1] += si.base_grand_total + return customers_in From 6e5952fa6952c3cae1a860c8825f4ed13e65dac1 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 11 May 2020 16:11:43 +0530 Subject: [PATCH 54/82] fix: Changed label for payroll working days based on field --- erpnext/hr/doctype/hr_settings/hr_settings.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index 9161ed822a..ebf8723be6 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -13,12 +13,12 @@ "stop_birthday_reminders", "expense_approver_mandatory_in_expense_claim", "payroll_settings", - "payroll_based_on", - "max_working_hours_against_timesheet", + "payroll_based_on", + "max_working_hours_against_timesheet", "include_holidays_in_total_working_days", "disable_rounded_total", "column_break_11", - "daily_wages_fraction_for_half_day", + "daily_wages_fraction_for_half_day", "email_salary_slip_to_employee", "encrypt_salary_slips_in_emails", "password_policy", @@ -191,7 +191,7 @@ "default": "Leave", "fieldname": "payroll_based_on", "fieldtype": "Select", - "label": "Calculate Working Days in Payroll based on", + "label": "Calculate Payroll Working Days Based On", "options": "Leave\nAttendance" }, { @@ -206,7 +206,7 @@ "idx": 1, "issingle": 1, "links": [], - "modified": "2020-04-13 21:20:59.382394", + "modified": "2020-05-11 13:02:51.274347", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", From 85a89812a42a8d8e5f80dbec89c77e5fe5e82988 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 11 May 2020 19:23:18 +0530 Subject: [PATCH 55/82] fix: Run income-tax-slab patch only if slab already exists in payroll period (#21684) --- ...ve_tax_slabs_from_payroll_period_to_income_tax_slab.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py index 179be2cfde..ec94cd01d1 100644 --- a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py +++ b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py @@ -7,7 +7,7 @@ import frappe from frappe.model.utils.rename_field import rename_field def execute(): - if not frappe.db.table_exists("Payroll Period"): + if not (frappe.db.table_exists("Payroll Period") and frappe.db.table_exists("Taxable Salary Slab")): return for doctype in ("income_tax_slab", "salary_structure_assignment", "employee_other_income", "income_tax_slab_other_charges"): @@ -60,6 +60,9 @@ def execute(): """, (income_tax_slab.name, company.name, period.start_date)) # move other incomes to separate document + if not frappe.db.table_exists("Employee Tax Exemption Proof Submission"): + return + migrated = [] proofs = frappe.get_all("Employee Tax Exemption Proof Submission", filters = {'docstatus': 1}, @@ -79,6 +82,9 @@ def execute(): except: pass + if not frappe.db.table_exists("Employee Tax Exemption Declaration"): + return + declerations = frappe.get_all("Employee Tax Exemption Declaration", filters = {'docstatus': 1}, fields =['payroll_period', 'employee', 'company', 'income_from_other_sources'] From 28c9468dad126985ee062d9d155449124196e1e7 Mon Sep 17 00:00:00 2001 From: sahil28297 <37302950+sahil28297@users.noreply.github.com> Date: Mon, 11 May 2020 19:28:10 +0530 Subject: [PATCH 56/82] fix(patch): use translated string while setting notification template (#21679) --- erpnext/patches/v11_0/set_default_email_template_in_hr.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v11_0/set_default_email_template_in_hr.py b/erpnext/patches/v11_0/set_default_email_template_in_hr.py index 14954fbeb3..4622376109 100644 --- a/erpnext/patches/v11_0/set_default_email_template_in_hr.py +++ b/erpnext/patches/v11_0/set_default_email_template_in_hr.py @@ -1,8 +1,9 @@ from __future__ import unicode_literals +from frappe import _ import frappe def execute(): hr_settings = frappe.get_single("HR Settings") - hr_settings.leave_approval_notification_template = "Leave Approval Notification" - hr_settings.leave_status_notification_template = "Leave Status Notification" - hr_settings.save() \ No newline at end of file + hr_settings.leave_approval_notification_template = _("Leave Approval Notification") + hr_settings.leave_status_notification_template = _("Leave Status Notification") + hr_settings.save() From 30f26b4457e0633266c33ab19d0ab0096db89490 Mon Sep 17 00:00:00 2001 From: Chinmay Pai Date: Mon, 11 May 2020 19:54:46 +0530 Subject: [PATCH 57/82] fix: do not setup charts if not setup_complete or no company found (#21670) * fix: do not setup charts if not setup_complete or no company found Signed-off-by: Chinmay D. Pai * chore: remove check for setup_complete moved the check for setup_complete to frappe Signed-off-by: Chinmay D. Pai * chore: return an empty dict from get_data if company not set Signed-off-by: Chinmay D. Pai --- erpnext/accounts/dashboard_fixtures.py | 176 +++++++++++++------------ 1 file changed, 90 insertions(+), 86 deletions(-) diff --git a/erpnext/accounts/dashboard_fixtures.py b/erpnext/accounts/dashboard_fixtures.py index a106f70dd0..cdd166134f 100644 --- a/erpnext/accounts/dashboard_fixtures.py +++ b/erpnext/accounts/dashboard_fixtures.py @@ -1,15 +1,22 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +from erpnext import get_default_company import frappe import json def get_data(): - return frappe._dict({ - "dashboards": get_dashboards(), - "charts": get_charts(), + data = frappe._dict({ + "dashboards": [], + "charts": [] }) + company = get_company_for_dashboards() + if company: + company_doc = frappe.get_doc("Company", company) + data.dashboards = get_dashboards() + data.charts = get_charts(company_doc) + return data def get_dashboards(): return [{ @@ -24,88 +31,87 @@ def get_dashboards(): ] }] -def get_charts(): - company = frappe.get_doc("Company", get_company_for_dashboards()) +def get_charts(company): income_account = company.default_income_account or get_account("Income Account", company.name) expense_account = company.default_expense_account or get_account("Expense Account", company.name) bank_account = company.default_bank_account or get_account("Bank", company.name) return [ - { - "doctype": "Dashboard Chart", - "time_interval": "Quarterly", - "name": "Income", - "chart_name": "Income", - "timespan": "Last Year", - "color": None, - "filters_json": json.dumps({"company": company.name, "account": income_account}), - "source": "Account Balance Timeline", - "chart_type": "Custom", - "timeseries": 1, - "owner": "Administrator", - "type": "Line" - }, - { - "doctype": "Dashboard Chart", - "time_interval": "Quarterly", - "name": "Expenses", - "chart_name": "Expenses", - "timespan": "Last Year", - "color": None, - "filters_json": json.dumps({"company": company.name, "account": expense_account}), - "source": "Account Balance Timeline", - "chart_type": "Custom", - "timeseries": 1, - "owner": "Administrator", - "type": "Line" - }, - { - "doctype": "Dashboard Chart", - "time_interval": "Quarterly", - "name": "Bank Balance", - "chart_name": "Bank Balance", - "timespan": "Last Year", - "color": "#ffb868", - "filters_json": json.dumps({"company": company.name, "account": bank_account}), - "source": "Account Balance Timeline", - "chart_type": "Custom", - "timeseries": 1, - "owner": "Administrator", - "type": "Line" - }, - { - "doctype": "Dashboard Chart", - "time_interval": "Monthly", - "name": "Incoming Bills (Purchase Invoice)", - "chart_name": "Incoming Bills (Purchase Invoice)", - "timespan": "Last Year", - "color": "#a83333", - "value_based_on": "base_grand_total", - "filters_json": json.dumps({}), - "chart_type": "Sum", - "timeseries": 1, - "based_on": "posting_date", - "owner": "Administrator", - "document_type": "Purchase Invoice", - "type": "Bar" - }, - { - "doctype": "Dashboard Chart", - "time_interval": "Monthly", - "name": "Outgoing Bills (Sales Invoice)", - "chart_name": "Outgoing Bills (Sales Invoice)", - "timespan": "Last Year", - "color": "#7b933d", - "value_based_on": "base_grand_total", - "filters_json": json.dumps({}), - "chart_type": "Sum", - "timeseries": 1, - "based_on": "posting_date", - "owner": "Administrator", - "document_type": "Sales Invoice", - "type": "Bar" - } - ] + { + "doctype": "Dashboard Chart", + "time_interval": "Quarterly", + "name": "Income", + "chart_name": "Income", + "timespan": "Last Year", + "color": None, + "filters_json": json.dumps({"company": company.name, "account": income_account}), + "source": "Account Balance Timeline", + "chart_type": "Custom", + "timeseries": 1, + "owner": "Administrator", + "type": "Line" + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Quarterly", + "name": "Expenses", + "chart_name": "Expenses", + "timespan": "Last Year", + "color": None, + "filters_json": json.dumps({"company": company.name, "account": expense_account}), + "source": "Account Balance Timeline", + "chart_type": "Custom", + "timeseries": 1, + "owner": "Administrator", + "type": "Line" + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Quarterly", + "name": "Bank Balance", + "chart_name": "Bank Balance", + "timespan": "Last Year", + "color": "#ffb868", + "filters_json": json.dumps({"company": company.name, "account": bank_account}), + "source": "Account Balance Timeline", + "chart_type": "Custom", + "timeseries": 1, + "owner": "Administrator", + "type": "Line" + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Monthly", + "name": "Incoming Bills (Purchase Invoice)", + "chart_name": "Incoming Bills (Purchase Invoice)", + "timespan": "Last Year", + "color": "#a83333", + "value_based_on": "base_grand_total", + "filters_json": json.dumps({}), + "chart_type": "Sum", + "timeseries": 1, + "based_on": "posting_date", + "owner": "Administrator", + "document_type": "Purchase Invoice", + "type": "Bar" + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Monthly", + "name": "Outgoing Bills (Sales Invoice)", + "chart_name": "Outgoing Bills (Sales Invoice)", + "timespan": "Last Year", + "color": "#7b933d", + "value_based_on": "base_grand_total", + "filters_json": json.dumps({}), + "chart_type": "Sum", + "timeseries": 1, + "based_on": "posting_date", + "owner": "Administrator", + "document_type": "Sales Invoice", + "type": "Bar" + } + ] def get_account(account_type, company): accounts = frappe.get_list("Account", filters={"account_type": account_type, "company": company}) @@ -113,11 +119,9 @@ def get_account(account_type, company): return accounts[0].name def get_company_for_dashboards(): - company = frappe.defaults.get_defaults().company - if company: - return company - else: + company = get_default_company() + if not company: company_list = frappe.get_list("Company") if company_list: - return company_list[0].name - return None \ No newline at end of file + company = company_list[0].name + return company From b52e40037abe4453934cc8673b3aa36bb35aaf59 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 11 May 2020 20:01:57 +0530 Subject: [PATCH 58/82] fix: Remove domain restriction from Location doctype (#21659) --- erpnext/assets/doctype/location/location.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/location/location.json b/erpnext/assets/doctype/location/location.json index 6a35130f30..f56fd05d98 100644 --- a/erpnext/assets/doctype/location/location.json +++ b/erpnext/assets/doctype/location/location.json @@ -141,7 +141,7 @@ ], "is_tree": 1, "links": [], - "modified": "2020-03-18 18:00:08.885805", + "modified": "2020-05-08 16:11:11.375701", "modified_by": "Administrator", "module": "Assets", "name": "Location", @@ -221,7 +221,6 @@ } ], "quick_entry": 1, - "restrict_to_domain": "Agriculture", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", From 97715f2877320486353a17662ea5f57f8eaf4f8c Mon Sep 17 00:00:00 2001 From: Marica Date: Mon, 11 May 2020 20:45:37 +0530 Subject: [PATCH 59/82] fix: Message for missing valuation rate (#21686) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- erpnext/stock/stock_ledger.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 10f2555475..62c9eb1eb2 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -470,7 +470,7 @@ class StockEntry(StockController): "qty": item.s_warehouse and -1*flt(item.transfer_qty) or flt(item.transfer_qty), "serial_no": item.serial_no, "voucher_type": self.doctype, - "voucher_no": item.name, + "voucher_no": self.name, "company": self.company, "allow_zero_valuation": item.allow_zero_valuation_rate, }) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index b4cb8cadb4..e1b3730f2f 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -548,7 +548,16 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, if not allow_zero_rate and not valuation_rate and raise_error_if_no_rate \ and cint(erpnext.is_perpetual_inventory_enabled(company)): frappe.local.message_log = [] - frappe.throw(_("Valuation rate not found for the Item {0}, which is required to do accounting entries for {1} {2}. If the item is transacting as a zero valuation rate item in the {1}, please mention that in the {1} Item table. Otherwise, please create an incoming stock transaction for the item or mention valuation rate in the Item record, and then try submiting / cancelling this entry.") - .format(item_code, voucher_type, voucher_no)) + form_link = frappe.utils.get_link_to_form("Item", item_code) + + message = _("Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}.").format(form_link, voucher_type, voucher_no) + message += "

" + _(" Here are the options to proceed:") + solutions = "
  • " + _("If the item is transacting as a Zero Valuation Rate item in this entry, please enable 'Allow Zero Valuation Rate' in the {0} Item table.").format(voucher_type) + "
  • " + solutions += "
  • " + _("If not, you can Cancel / Submit this entry ") + _("{0}").format(frappe.bold("after")) + _(" performing either one below:") + "
  • " + sub_solutions = "
    • " + _("Create an incoming stock transaction for the Item.") + "
    • " + sub_solutions += "
    • " + _("Mention Valuation Rate in the Item master.") + "
    " + msg = message + solutions + sub_solutions + "" + + frappe.throw(msg=msg, title=_("Valuation Rate Missing")) return valuation_rate From 498f6583210e730207e922823c5e6d87579c55b8 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 11 May 2020 20:47:08 +0530 Subject: [PATCH 60/82] refactor: rename getting started to home (#21674) * feat: rename getting started to home * feat: added patch for renaming --- erpnext/patches.txt | 1 + .../getting_started.json => home/home.json} | 14 ++++---------- 2 files changed, 5 insertions(+), 10 deletions(-) rename erpnext/setup/desk_page/{getting_started/getting_started.json => home/home.json} (97%) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 3f90d36916..f72172474c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -680,3 +680,4 @@ erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry erpnext.patches.v12_0.retain_permission_rules_for_video_doctype erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive execute:frappe.delete_doc_if_exists("Page", "appointment-analytic") +execute:frappe.rename_doc("Desk Page", "Getting Started", "Home", force=True) diff --git a/erpnext/setup/desk_page/getting_started/getting_started.json b/erpnext/setup/desk_page/home/home.json similarity index 97% rename from erpnext/setup/desk_page/getting_started/getting_started.json rename to erpnext/setup/desk_page/home/home.json index 63d8984c40..63cd5c5cec 100644 --- a/erpnext/setup/desk_page/getting_started/getting_started.json +++ b/erpnext/setup/desk_page/home/home.json @@ -47,26 +47,20 @@ } ], "category": "Modules", - "charts": [ - { - "chart_name": "Bank Balance", - "label": "Bank Balance" - } - ], + "charts": [], "creation": "2020-01-23 13:46:38.833076", "developer_mode_only": 0, "disable_user_customization": 0, "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, - "icon": "", "idx": 0, "is_standard": 1, - "label": "Getting Started", - "modified": "2020-04-01 11:30:19.763099", + "label": "Home", + "modified": "2020-05-11 10:20:37.358701", "modified_by": "Administrator", "module": "Setup", - "name": "Getting Started", + "name": "Home", "owner": "Administrator", "pin_to_bottom": 0, "pin_to_top": 1, From 734bfcdfc2878ea9e69d90f83a02a30912cecb26 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 12 May 2020 14:11:33 +0530 Subject: [PATCH 61/82] fix: purchase inv shows overdue for fraction of outstanding --- .../purchase_invoice/purchase_invoice.py | 34 +++++++++++++++++++ erpnext/controllers/status_updater.py | 11 ------ 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 3aa24df16d..5b16eb4640 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1021,6 +1021,40 @@ class PurchaseInvoice(BuyingController): # calculate totals again after applying TDS self.calculate_taxes_and_totals() + + def set_status(self, update=False, status=None, update_modified=True): + if self.is_new(): + if self.get('amended_from'): + self.status = 'Draft' + return + + precision = self.precision("outstanding_amount") + outstanding_amount = flt(self.outstanding_amount, precision) + due_date = getdate(self.due_date) + nowdate = getdate() + + if not status: + if self.docstatus == 2: + status = "Cancelled" + elif self.docstatus == 1: + elif outstanding_amount > 0 and due_date < nowdate: + self.status = "Overdue" + elif outstanding_amount > 0 and due_date >= nowdate: + self.status = "Unpaid" + #Check if outstanding amount is 0 due to debit note issued against invoice + elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}): + self.status = "Debit Note Issued" + elif self.is_return == 1: + self.status = "Return" + elif outstanding_amount<=0: + self.status = "Paid" + else: + self.status = "Submitted" + else: + self.status = "Draft" + + if update: + self.db_set('status', self.status, update_modified = update_modified) def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index de76e45cd1..b465a106f0 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -69,17 +69,6 @@ status_map = { ["Cancelled", "eval:self.docstatus==2"], ["Closed", "eval:self.status=='Closed'"], ], - "Purchase Invoice": [ - ["Draft", None], - ["Submitted", "eval:self.docstatus==1"], - ["Paid", "eval:self.outstanding_amount==0 and self.docstatus==1"], - ["Return", "eval:self.is_return==1 and self.docstatus==1"], - ["Debit Note Issued", - "eval:self.outstanding_amount <= 0 and self.docstatus==1 and self.is_return==0 and get_value('Purchase Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1})"], - ["Unpaid", "eval:self.outstanding_amount > 0 and getdate(self.due_date) >= getdate(nowdate()) and self.docstatus==1"], - ["Overdue", "eval:self.outstanding_amount > 0 and getdate(self.due_date) < getdate(nowdate()) and self.docstatus==1"], - ["Cancelled", "eval:self.docstatus==2"], - ], "Material Request": [ ["Draft", None], ["Stopped", "eval:self.status == 'Stopped'"], From 411b12590648e602565829d6e5f26b57ce56b62c Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Tue, 12 May 2020 15:25:35 +0530 Subject: [PATCH 62/82] new parent updating logic, made requested changes --- .../customer_acquisition_and_loyalty.py | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index 38fbd60008..88bd9c135d 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -61,13 +61,13 @@ def get_data_by_time(filters, common_columns): # key yyyy-mm columns = [ { - 'label': 'Year', + 'label': _('Year'), 'fieldname': 'year', 'fieldtype': 'Data', 'width': 100 }, { - 'label': 'Month', + 'label': _('Month'), 'fieldname': 'month', 'fieldtype': 'Data', 'width': 100 @@ -136,6 +136,7 @@ def get_data_by_territory(filters, common_columns): repeat = customers_in[name]['repeat'] if condition else [0, 0.0] temp = { 'territory': name, + 'parent_territory': territory_dict[name]['parent'], 'indent': indent, 'new_customers': new[0], 'repeat_customers': repeat[0], @@ -146,34 +147,18 @@ def get_data_by_territory(filters, common_columns): 'bold': 0 if indent else 1 } data.append(temp) - node_list = [x for x in territory_dict.keys() if territory_dict[x]['is_group'] == 0] - root_node = [x for x in territory_dict.keys() if territory_dict[x]['parent'] is None][0] - root_data = [x for x in data if x['territory'] == root_node][0] - for node in node_list: - data = update_groups(node, data, root_node, territory_dict) + loop_data = sorted(data, key=lambda k: k['indent'], reverse=True) - for group in [x for x in territory_dict.keys() if territory_dict[x]['parent'] == root_node]: - group_data = [x for x in data if x['territory'] == group][0] - for key in group_data.keys(): - if key not in ['indent', 'territory', 'bold']: - root_data[key] += group_data[key] + for ld in loop_data: + if ld['parent_territory']: + parent_data = [x for x in data if x['territory'] == ld['parent_territory']][0] + for key in parent_data.keys(): + if key not in ['indent', 'territory', 'parent_territory', 'bold']: + parent_data[key] += ld[key] return columns, data, None, None, None, 1 -def update_groups(node, data, root_node, territory_dict): - """ Adds values of child territories to parent node except root. """ - parent_node = territory_dict[node]['parent'] - if parent_node != root_node and parent_node: - node_data = [x for x in data if x['territory'] == node][0] - parent_data = [x for x in data if x['territory'] == parent_node][0] - for key in parent_data.keys(): - if key not in ['indent', 'territory', 'bold']: - parent_data[key] += node_data[key] - return update_groups(parent_node, data, root_node, territory_dict) - else: - return data - def get_customer_stats(filters, tree_view=False): """ Calculates number of new and repeated customers. """ company_condition = '' From 673e704bb5daefbc3c128bafee2f7051eb60d7cb Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Tue, 12 May 2020 19:09:27 +0530 Subject: [PATCH 63/82] typo in error message in loan_security_pledge.py --- .../doctype/loan_security_pledge/loan_security_pledge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py index f97e5965a5..961c05c9c1 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py @@ -38,7 +38,7 @@ class LoanSecurityPledge(Document): for pledge in self.securities: if not pledge.qty and not pledge.amount: - frappe.throw(_("Qty or Amount is mandatroy for loan security")) + frappe.throw(_("Qty or Amount is mandatory for loan security!")) if not (self.loan_application and pledge.loan_security_price): pledge.loan_security_price = get_loan_security_price(pledge.loan_security) From 4a9fd9ef6d5e34eb6f04deb0423c93f33f0b3028 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Wed, 13 May 2020 16:11:22 +0530 Subject: [PATCH 64/82] fix: error log title for failing bank transactions --- .../doctype/plaid_settings/plaid_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index b4a5bd11a0..a7062239c3 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -209,7 +209,7 @@ def new_bank_transaction(transaction): result.append(new_transaction.name) except Exception: - frappe.throw(frappe.get_traceback()) + frappe.throw(title=_('Bank transaction creation error')) return result From 1aaedd68b9d7213804a6e244d4783674cc8865d2 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Wed, 13 May 2020 17:48:11 +0530 Subject: [PATCH 65/82] Twitter and LinkedIn Auth fix --- .../linkedin_settings/linkedin_settings.py | 4 +-- .../twitter_settings/twitter_settings.json | 34 +++++++++---------- .../twitter_settings/twitter_settings.py | 14 ++++---- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py index bdde9eed37..377e061fdf 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py @@ -15,7 +15,7 @@ class LinkedInSettings(Document): params = urlencode({ "response_type":"code", "client_id": self.consumer_key, - "redirect_uri": get_site_url(frappe.local.site) + "/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?", + "redirect_uri": "{0}/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?".format(frappe.utils.get_url()), "scope": "r_emailaddress w_organization_social r_basicprofile r_liteprofile r_organization_social rw_organization_admin w_member_social" }) @@ -30,7 +30,7 @@ class LinkedInSettings(Document): "code": code, "client_id": self.consumer_key, "client_secret": self.get_password(fieldname="consumer_secret"), - "redirect_uri": get_site_url(frappe.local.site) + "/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?", + "redirect_uri": "{0}/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?".format(frappe.utils.get_url()), } headers = { "Content-Type": "application/x-www-form-urlencoded" diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.json b/erpnext/crm/doctype/twitter_settings/twitter_settings.json index f92e7f0495..36776e5c20 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.json +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.json @@ -11,8 +11,8 @@ "consumer_key", "column_break_5", "consumer_secret", - "oauth_token", - "oauth_secret", + "access_token", + "access_token_secret", "session_status" ], "fields": [ @@ -41,20 +41,6 @@ "label": "API Secret Key", "reqd": 1 }, - { - "fieldname": "oauth_token", - "fieldtype": "Data", - "hidden": 1, - "label": "OAuth Token", - "read_only": 1 - }, - { - "fieldname": "oauth_secret", - "fieldtype": "Password", - "hidden": 1, - "label": "OAuth Token Secret", - "read_only": 1 - }, { "fieldname": "column_break_5", "fieldtype": "Column Break" @@ -72,12 +58,26 @@ "label": "Session Status", "options": "Expired\nActive", "read_only": 1 + }, + { + "fieldname": "access_token", + "fieldtype": "Data", + "hidden": 1, + "label": "Access Token", + "read_only": 1 + }, + { + "fieldname": "access_token_secret", + "fieldtype": "Data", + "hidden": 1, + "label": "Access Token Secret", + "read_only": 1 } ], "image_field": "profile_pic", "issingle": 1, "links": [], - "modified": "2020-04-21 22:06:43.726798", + "modified": "2020-05-13 17:50:47.934776", "modified_by": "Administrator", "module": "CRM", "name": "Twitter Settings", diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py index 7616b4c027..976a23dfc7 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.py +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py @@ -31,13 +31,13 @@ class TwitterSettings(Document): try: auth.get_access_token(oauth_verifier) - api = self.get_api() + api = self.get_api(auth.access_token, auth.access_token_secret) user = api.me() profile_pic = (user._json["profile_image_url"]).replace("_normal","") frappe.db.set_value(self.doctype, self.name, { - "oauth_token" : auth.access_token, - "oauth_secret" : auth.access_token_secret, + "access_token" : auth.access_token, + "access_token_secret" : auth.access_token_secret, "account_name" : user._json["screen_name"], "profile_pic" : profile_pic, "session_status" : "Active" @@ -49,11 +49,11 @@ class TwitterSettings(Document): frappe.msgprint(_("Error! Failed to get access token.")) frappe.throw(_('Invalid Consumer Key or Consumer Secret Key')) - def get_api(self): + def get_api(self, access_token, access_token_secret): # authentication of consumer key and secret auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) # authentication of access token and secret - auth.set_access_token(self.oauth_token, self.get_password(fieldname="oauth_secret")) + auth.set_access_token(access_token, access_token_secret) return tweepy.API(auth) @@ -67,13 +67,13 @@ class TwitterSettings(Document): def upload_image(self, media): media = get_file_path(media) - api = self.get_api() + api = self.get_api(self.access_token, self.access_token_secret) media = api.media_upload(media) return media.media_id def send_tweet(self, text, media_id=None): - api = self.get_api() + api = self.get_api(self.access_token, self.access_token_secret) try: if media_id: response = api.update_status(status = text, media_ids = [media_id]) From bd7e5358857b66f5025e38ab3cf82d589dec6506 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Wed, 13 May 2020 19:48:42 +0530 Subject: [PATCH 66/82] Fix: Set General Ledger 'Group By' filter as 'Group by Voucher(Consolidated)' when opened from Invoice (#21673) * fix for issue #21419 * changing group by filter to default Group by Voucher (Consolidated) --- erpnext/accounts/report/general_ledger/general_ledger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 1188beaa0f..2aecd6b717 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -53,7 +53,7 @@ frappe.query_reports["General Ledger"] = { "label": __("Voucher No"), "fieldtype": "Data", on_change: function() { - frappe.query_report.set_filter_value('group_by', ""); + frappe.query_report.set_filter_value('group_by', "Group by Voucher (Consolidated)"); } }, { From 13096cdbfefd58f16f7e009a48343a5601f41221 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 13 May 2020 22:02:56 +0530 Subject: [PATCH 67/82] fix: invalid conditional statement --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 5b16eb4640..265969db1f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1037,7 +1037,7 @@ class PurchaseInvoice(BuyingController): if self.docstatus == 2: status = "Cancelled" elif self.docstatus == 1: - elif outstanding_amount > 0 and due_date < nowdate: + if outstanding_amount > 0 and due_date < nowdate: self.status = "Overdue" elif outstanding_amount > 0 and due_date >= nowdate: self.status = "Unpaid" From dde39c3d1aa323072de5703b8c30eabe07b3960c Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Thu, 14 May 2020 12:09:36 +0530 Subject: [PATCH 68/82] chore: Drop Python2 support (#21704) * chore: Drop Python2 support * test: Fix test redundancy by removing countries 3 countries seems ennough to test coa template feature --- .travis.yml | 17 ++--------------- erpnext/setup/doctype/company/test_company.py | 4 +--- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index 213445b806..77d427e5a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ -dist: trusty - language: python +dist: trusty git: depth: 1 @@ -14,21 +13,10 @@ addons: jobs: include: - - name: "Python 2.7 Server Side Test" - python: 2.7 - script: bench --site test_site run-tests --app erpnext --coverage - - name: "Python 3.6 Server Side Test" python: 3.6 script: bench --site test_site run-tests --app erpnext --coverage - - name: "Python 2.7 Patch Test" - python: 2.7 - before_script: - - wget http://build.erpnext.com/20171108_190013_955977f8_database.sql.gz - - bench --site test_site --force restore ~/frappe-bench/20171108_190013_955977f8_database.sql.gz - script: bench --site test_site migrate - - name: "Python 3.6 Patch Test" python: 3.6 before_script: @@ -40,8 +28,7 @@ install: - cd ~ - nvm install 10 - - git clone https://github.com/frappe/bench --depth 1 - - pip install -e ./bench + - pip install frappe-bench - git clone https://github.com/frappe/frappe --branch $TRAVIS_BRANCH --depth 1 - bench init --skip-assets --frappe-path ~/frappe --python $(which python) frappe-bench diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py index b37cc17ba9..29f6c3731d 100644 --- a/erpnext/setup/doctype/company/test_company.py +++ b/erpnext/setup/doctype/company/test_company.py @@ -47,9 +47,7 @@ class TestCompany(unittest.TestCase): frappe.delete_doc("Company", "COA from Existing Company") def test_coa_based_on_country_template(self): - countries = ["India", "Brazil", "United Arab Emirates", "Canada", "Germany", "France", - "Guatemala", "Indonesia", "Italy", "Mexico", "Nicaragua", "Netherlands", "Singapore", - "Brazil", "Argentina", "Hungary", "Taiwan"] + countries = ["Canada", "Germany", "France"] for country in countries: templates = get_charts_for_country(country) From 10df3d5081c54d5ee70bdf69c81c3d70acb35169 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 14 May 2020 17:15:16 +0530 Subject: [PATCH 69/82] fix: Get basic and hra component from db, not from cache --- erpnext/regional/india/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 33098587c2..732780a0a3 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -251,8 +251,7 @@ def get_tax_template_for_sez(party_details, master_doctype, company, party_type) def calculate_annual_eligible_hra_exemption(doc): - basic_component = frappe.get_cached_value('Company', doc.company, "basic_component") - hra_component = frappe.get_cached_value('Company', doc.company, "hra_component") + basic_component, hra_component = frappe.db.get_value('Company', doc.company, ["basic_component", "hra_component"]) if not (basic_component and hra_component): frappe.throw(_("Please mention Basic and HRA component in Company")) annual_exemption, monthly_exemption, hra_amount = 0, 0, 0 From 200f80c3d368884000ebd8b5942e1e9ea28d0301 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 14 May 2020 17:18:29 +0530 Subject: [PATCH 70/82] fix: Added submit permission in employee other income --- .../employee_other_income.json | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/erpnext/hr/doctype/employee_other_income/employee_other_income.json b/erpnext/hr/doctype/employee_other_income/employee_other_income.json index 2dd6c10988..8abfe1e93a 100644 --- a/erpnext/hr/doctype/employee_other_income/employee_other_income.json +++ b/erpnext/hr/doctype/employee_other_income/employee_other_income.json @@ -76,25 +76,15 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-03-19 18:06:45.361830", + "modified": "2020-05-14 17:17:38.883126", "modified_by": "Administrator", "module": "HR", "name": "Employee Other Income", "owner": "Administrator", "permissions": [ { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - }, - { + "amend": 1, + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -104,9 +94,12 @@ "report": 1, "role": "HR Manager", "share": 1, + "submit": 1, "write": 1 }, { + "amend": 1, + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -116,9 +109,12 @@ "report": 1, "role": "HR User", "share": 1, + "submit": 1, "write": 1 }, { + "amend": 1, + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -128,6 +124,7 @@ "report": 1, "role": "Employee", "share": 1, + "submit": 1, "write": 1 } ], From 39cb749f955f0fe40dc2289cdbc32bcb7f9ba939 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 14 May 2020 18:25:58 +0530 Subject: [PATCH 71/82] fix: add heatmap_year parameter to get --- .../account_balance_timeline/account_balance_timeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py index 5decccb486..39bf4b053a 100644 --- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py +++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py @@ -14,7 +14,7 @@ from frappe.utils.nestedset import get_descendants_of @frappe.whitelist() @cache_source def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None, - to_date = None, timespan = None, time_interval = None): + to_date = None, timespan = None, time_interval = None, heatmap_year = None): if chart_name: chart = frappe.get_doc('Dashboard Chart', chart_name) else: From 41b47a68b36a377d040784b3125e6a2b62d931a9 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 15 May 2020 04:24:36 +0530 Subject: [PATCH 72/82] fix: item price not fetching when customer is unset in item price (#21488) * fix: item price not fetching when customer is unset in item price * fix: item price of selling type has hidden supplier value * fix: remove test variable * fix: test * patch: invalid customer/supplier based on item price type * fix: invalid query * fix: patch Co-authored-by: Marica --- erpnext/patches.txt | 1 + ...stomer_supplier_based_on_type_of_item_price.py | 15 +++++++++++++++ erpnext/stock/doctype/item_price/item_price.py | 7 +++++++ erpnext/stock/get_item_details.py | 2 +- 4 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f72172474c..0edadcc66d 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -681,3 +681,4 @@ erpnext.patches.v12_0.retain_permission_rules_for_video_doctype erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive execute:frappe.delete_doc_if_exists("Page", "appointment-analytic") execute:frappe.rename_doc("Desk Page", "Getting Started", "Home", force=True) +erpnext.patches.v12_0.unset_customer_supplier_based_on_type_of_item_price diff --git a/erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py b/erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py new file mode 100644 index 0000000000..60aec05466 --- /dev/null +++ b/erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + invalid_selling_item_price = frappe.db.sql( + """SELECT name FROM `tabItem Price` WHERE selling = 1 and buying = 0 and (supplier IS NOT NULL or supplier = '')""" + ) + invalid_buying_item_price = frappe.db.sql( + """SELECT name FROM `tabItem Price` WHERE selling = 0 and buying = 1 and (customer IS NOT NULL or customer = '')""" + ) + docs_to_modify = invalid_buying_item_price + invalid_selling_item_price + for d in docs_to_modify: + # saving the doc will auto reset invalid customer/supplier field + doc = frappe.get_doc("Item Price", d[0]) + doc.save() \ No newline at end of file diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index 957c41546b..8e39eb5037 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -69,3 +69,10 @@ class ItemPrice(Document): self.reference = self.customer if self.buying: self.reference = self.supplier + + if self.selling and not self.buying: + # if only selling then remove supplier + self.supplier = None + if self.buying and not self.selling: + # if only buying then remove customer + self.customer = None diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index d50712aee7..11b6403419 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -630,7 +630,7 @@ def get_item_price(args, item_code, ignore_party=False): elif args.get("supplier"): conditions += " and supplier=%(supplier)s" else: - conditions += " and (customer is null or customer = '') and (supplier is null or supplier = '')" + conditions += "and (customer is null or customer = '') and (supplier is null or supplier = '')" if args.get('transaction_date'): conditions += """ and %(transaction_date)s between From c649468f37db8fb941f940eeae7c63807729840a Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 15 May 2020 11:35:41 +0530 Subject: [PATCH 73/82] Fixed typo --- .../v12_0/set_valid_till_date_in_supplier_quotation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/patches/v12_0/set_valid_till_date_in_supplier_quotation.py b/erpnext/patches/v12_0/set_valid_till_date_in_supplier_quotation.py index befa46c31d..4a6e228856 100644 --- a/erpnext/patches/v12_0/set_valid_till_date_in_supplier_quotation.py +++ b/erpnext/patches/v12_0/set_valid_till_date_in_supplier_quotation.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import frappe def execute(): - frappe.reload_doc("buying", "doctype", "suppplier_quotation") + frappe.reload_doc("buying", "doctype", "supplier_quotation") frappe.db.sql("""UPDATE `tabSupplier Quotation` SET valid_till = DATE_ADD(transaction_date , INTERVAL 1 MONTH) - WHERE docstatus < 2""") \ No newline at end of file + WHERE docstatus < 2""") From f984bee5f96723c0bd8d6370c042f7ae4f4f9d47 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 15 May 2020 11:55:42 +0530 Subject: [PATCH 74/82] fix: duplicate leave expiry creation (#21505) * fix: validate existing ledger entries to avoid duplicates * patch: remove duplicate ledger entries created * fix: consider only submitted ledger entries * fix: delete duplicate leaves from the ledger * fix: check if duplicate ledger entry exists * chore: formatting changes Co-authored-by: Nabin Hait --- .../leave_application/leave_application.py | 6 +-- .../leave_ledger_entry/leave_ledger_entry.py | 50 +++++++++++-------- erpnext/patches.txt | 1 + .../remove_duplicate_leave_ledger_entries.py | 44 ++++++++++++++++ 4 files changed, 78 insertions(+), 23 deletions(-) create mode 100644 erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 47b1bb7684..d2620bec91 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -549,7 +549,7 @@ def get_remaining_leaves(allocation, leaves_taken, date, expiry): return _get_remaining_leaves(total_leaves, allocation.to_date) -def get_leaves_for_period(employee, leave_type, from_date, to_date): +def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_expired_leaves=False): leave_entries = get_leave_entries(employee, leave_type, from_date, to_date) leave_days = 0 @@ -559,8 +559,8 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date): if inclusive_period and leave_entry.transaction_type == 'Leave Encashment': leave_days += leave_entry.leaves - elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' \ - and leave_entry.is_expired and not skip_expiry_leaves(leave_entry, to_date): + elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' and leave_entry.is_expired \ + and (do_not_skip_expired_leaves or not skip_expiry_leaves(leave_entry, to_date)): leave_days += leave_entry.leaves elif leave_entry.transaction_type == 'Leave Application': diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py index 9ed58c9e59..63559c4f5a 100644 --- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py @@ -88,32 +88,40 @@ def get_previous_expiry_ledger_entry(ledger): }, fieldname=['name']) def process_expired_allocation(): - ''' Check if a carry forwarded allocation has expired and create a expiry ledger entry ''' + ''' Check if a carry forwarded allocation has expired and create a expiry ledger entry + Case 1: carry forwarded expiry period is set for the leave type, + create a separate leave expiry entry against each entry of carry forwarded and non carry forwarded leaves + Case 2: leave type has no specific expiry period for carry forwarded leaves + and there is no carry forwarded leave allocation, create a single expiry against the remaining leaves. + ''' # fetch leave type records that has carry forwarded leaves expiry leave_type_records = frappe.db.get_values("Leave Type", filters={ 'expire_carry_forwarded_leaves_after_days': (">", 0) }, fieldname=['name']) - leave_type = [record[0] for record in leave_type_records] + leave_type = [record[0] for record in leave_type_records] or [''] - expired_allocation = frappe.db.sql_list("""SELECT name - FROM `tabLeave Ledger Entry` - WHERE - `transaction_type`='Leave Allocation' - AND `is_expired`=1""") - - expire_allocation = frappe.get_all("Leave Ledger Entry", - fields=['leaves', 'to_date', 'employee', 'leave_type', 'is_carry_forward', 'transaction_name as name', 'transaction_type'], - filters={ - 'to_date': ("<", today()), - 'transaction_type': 'Leave Allocation', - 'transaction_name': ('not in', expired_allocation) - }, - or_filters={ - 'is_carry_forward': 0, - 'leave_type': ('in', leave_type) - }) + # fetch non expired leave ledger entry of transaction_type allocation + expire_allocation = frappe.db.sql(""" + SELECT + leaves, to_date, employee, leave_type, + is_carry_forward, transaction_name as name, transaction_type + FROM `tabLeave Ledger Entry` l + WHERE (NOT EXISTS + (SELECT name + FROM `tabLeave Ledger Entry` + WHERE + transaction_name = l.transaction_name + AND transaction_type = 'Leave Allocation' + AND name<>l.name + AND docstatus = 1 + AND ( + is_carry_forward=l.is_carry_forward + OR (is_carry_forward = 0 AND leave_type not in %s) + ))) + AND transaction_type = 'Leave Allocation' + AND to_date < %s""", (leave_type, today()), as_dict=1) if expire_allocation: create_expiry_ledger_entry(expire_allocation) @@ -133,6 +141,7 @@ def get_remaining_leaves(allocation): 'employee': allocation.employee, 'leave_type': allocation.leave_type, 'to_date': ('<=', allocation.to_date), + 'docstatus': 1 }, fieldname=['SUM(leaves)']) @frappe.whitelist() @@ -159,7 +168,8 @@ def expire_allocation(allocation, expiry_date=None): def expire_carried_forward_allocation(allocation): ''' Expires remaining leaves in the on carried forward allocation ''' from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period - leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type, allocation.from_date, allocation.to_date) + leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type, + allocation.from_date, allocation.to_date, do_not_skip_expired_leaves=True) leaves = flt(allocation.leaves) + flt(leaves_taken) # allow expired leaves entry to be created diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0edadcc66d..274728151a 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -678,6 +678,7 @@ erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 erpnext.patches.v12_0.fix_quotation_expired_status erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry erpnext.patches.v12_0.retain_permission_rules_for_video_doctype +erpnext.patches.v12_0.remove_duplicate_leave_ledger_entries erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive execute:frappe.delete_doc_if_exists("Page", "appointment-analytic") execute:frappe.rename_doc("Desk Page", "Getting Started", "Home", force=True) diff --git a/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py b/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py new file mode 100644 index 0000000000..98a2fcf27e --- /dev/null +++ b/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py @@ -0,0 +1,44 @@ +# Copyright (c) 2018, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + """Delete duplicate leave ledger entries of type allocation created.""" + if not frappe.db.a_row_exists("Leave Ledger Entry"): + return + + duplicate_records_list = get_duplicate_records() + delete_duplicate_ledger_entries(duplicate_records_list) + +def get_duplicate_records(): + """Fetch all but one duplicate records from the list of expired leave allocation.""" + return frappe.db.sql_list(""" + WITH duplicate_records AS + (SELECT + name, transaction_name, is_carry_forward, + ROW_NUMBER() over(partition by transaction_name order by creation)as row + FROM `tabLeave Ledger Entry` l + WHERE (EXISTS + (SELECT name + FROM `tabLeave Ledger Entry` + WHERE + transaction_name = l.transaction_name + AND transaction_type = 'Leave Allocation' + AND name <> l.name + AND employee = l.employee + AND docstatus = 1 + AND leave_type = l.leave_type + AND is_carry_forward=l.is_carry_forward + AND to_date = l.to_date + AND from_date = l.from_date + AND is_expired = 1 + ))) + SELECT name FROM duplicate_records WHERE row > 1 + """) + +def delete_duplicate_ledger_entries(duplicate_records_list): + """Delete duplicate leave ledger entries.""" + if duplicate_records_list: + frappe.db.sql(''' DELETE FROM `tabLeave Ledger Entry` WHERE name in {0}'''.format(tuple(duplicate_records_list))) #nosec \ No newline at end of file From 200af04657edac47a96ef873347c66d93b7d7078 Mon Sep 17 00:00:00 2001 From: Rohan Date: Fri, 15 May 2020 12:10:34 +0530 Subject: [PATCH 75/82] format: better error messages for invalid coupon codes (develop) (#21599) * format: better error messages for invalid coupon codes * fix: remove unnecessary docstatus check --- .../accounts/doctype/pricing_rule/utils.py | 30 +++++++------ erpnext/shopping_cart/cart.py | 44 ++++++++++--------- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index b358f56671..cb05481df5 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -4,13 +4,19 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe, copy, json -from frappe import throw, _ + +import copy +import json + from six import string_types -from frappe.utils import flt, cint, get_datetime, get_link_to_form, today + +import frappe from erpnext.setup.doctype.item_group.item_group import get_child_item_groups from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses from erpnext.stock.get_item_details import get_conversion_factor +from frappe import _, throw +from frappe.utils import cint, flt, get_datetime, get_link_to_form, getdate, today + class MultiplePricingRuleConflict(frappe.ValidationError): pass @@ -502,18 +508,16 @@ def get_pricing_rule_items(pr_doc): return list(set(apply_on_data)) def validate_coupon_code(coupon_name): - from frappe.utils import today,getdate - coupon=frappe.get_doc("Coupon Code",coupon_name) + coupon = frappe.get_doc("Coupon Code", coupon_name) + if coupon.valid_from: - if coupon.valid_from > getdate(today()) : - frappe.throw(_("Sorry,coupon code validity has not started")) + if coupon.valid_from > getdate(today()): + frappe.throw(_("Sorry, this coupon code's validity has not started")) elif coupon.valid_upto: - if coupon.valid_upto < getdate(today()) : - frappe.throw(_("Sorry,coupon code validity has expired")) - elif coupon.used>=coupon.maximum_use: - frappe.throw(_("Sorry,coupon code are exhausted")) - else: - return + if coupon.valid_upto < getdate(today()): + frappe.throw(_("Sorry, this coupon code's validity has expired")) + elif coupon.used >= coupon.maximum_use: + frappe.throw(_("Sorry, this coupon code is no longer valid")) def update_coupon_code_count(coupon_name,transaction_type): coupon=frappe.get_doc("Coupon Code",coupon_name) diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py index e11e1bb5dc..4ac546e82c 100644 --- a/erpnext/shopping_cart/cart.py +++ b/erpnext/shopping_cart/cart.py @@ -541,27 +541,31 @@ def show_terms(doc): return doc.tc_name @frappe.whitelist(allow_guest=True) -def apply_coupon_code(applied_code,applied_referral_sales_partner): +def apply_coupon_code(applied_code, applied_referral_sales_partner): quotation = True - if applied_code: - coupon_list=frappe.get_all('Coupon Code', filters={"docstatus": ("<", "2"), 'coupon_code':applied_code }, fields=['name']) - if coupon_list: - coupon_name=coupon_list[0].name - from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code - validate_coupon_code(coupon_name) - quotation = _get_cart_quotation() - quotation.coupon_code=coupon_name + + if not applied_code: + frappe.throw(_("Please enter a coupon code")) + + coupon_list = frappe.get_all('Coupon Code', filters={'coupon_code': applied_code}) + if not coupon_list: + frappe.throw(_("Please enter a valid coupon code")) + + coupon_name = coupon_list[0].name + + from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code + validate_coupon_code(coupon_name) + quotation = _get_cart_quotation() + quotation.coupon_code = coupon_name + quotation.flags.ignore_permissions = True + quotation.save() + + if applied_referral_sales_partner: + sales_partner_list = frappe.get_all('Sales Partner', filters={'referral_code': applied_referral_sales_partner}) + if sales_partner_list: + sales_partner_name = sales_partner_list[0].name + quotation.referral_sales_partner = sales_partner_name quotation.flags.ignore_permissions = True quotation.save() - if applied_referral_sales_partner: - sales_partner_list=frappe.get_all('Sales Partner', filters={'docstatus': 0, 'referral_code':applied_referral_sales_partner }, fields=['name']) - if sales_partner_list: - sales_partner_name=sales_partner_list[0].name - quotation.referral_sales_partner=sales_partner_name - quotation.flags.ignore_permissions = True - quotation.save() - else: - frappe.throw(_("Please enter valid coupon code !!")) - else: - frappe.throw(_("Please enter coupon code !!")) + return quotation From 7d61c03af41271082bd872c7edbfdb3ac3b478ae Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Fri, 15 May 2020 12:58:48 +0530 Subject: [PATCH 76/82] fix: Add missing dimensions in GL entries (#21689) * fix: Add misssing dimensions in GL entries * fix: Add project filter in trial balance report * fix: Use current dimensions instead of dimensions from asset --- erpnext/accounts/deferred_revenue.py | 12 ++++----- .../accounting_dimension.py | 6 ++--- .../invoice_discounting.py | 14 ++++++++--- .../doctype/payment_entry/payment_entry.py | 10 ++++---- .../purchase_invoice/purchase_invoice.py | 13 +++++----- .../doctype/sales_invoice/sales_invoice.py | 25 +++++++++---------- .../report/trial_balance/trial_balance.js | 12 ++++++--- .../report/trial_balance/trial_balance.py | 8 ++++++ erpnext/assets/doctype/asset/asset.py | 12 ++++----- erpnext/education/doctype/fees/fees.py | 6 +++-- .../hr/doctype/expense_claim/expense_claim.py | 13 +++++----- .../expense_claim_detail.json | 17 ++++++++++--- .../expense_taxes_and_charges.json | 17 ++++++++++--- erpnext/patches.txt | 2 +- ...counting_dimensions_in_missing_doctypes.py | 3 ++- 15 files changed, 107 insertions(+), 63 deletions(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index b0210e5fd4..b57e6783ce 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -185,7 +185,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): total_days, total_booking_days, account_currency) make_gl_entries(doc, credit_account, debit_account, against, - amount, base_amount, end_date, project, account_currency, item.cost_center, item.name, deferred_process) + amount, base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process) # Returned in case of any errors because it tries to submit the same record again and again in case of errors if frappe.flags.deferred_accounting_error: @@ -222,7 +222,7 @@ def process_deferred_accounting(posting_date=today()): doc.submit() def make_gl_entries(doc, credit_account, debit_account, against, - amount, base_amount, posting_date, project, account_currency, cost_center, voucher_detail_no, deferred_process=None): + amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None): # GL Entry for crediting the amount in the deferred expense from erpnext.accounts.general_ledger import make_gl_entries @@ -236,12 +236,12 @@ def make_gl_entries(doc, credit_account, debit_account, against, "credit": base_amount, "credit_in_account_currency": amount, "cost_center": cost_center, - "voucher_detail_no": voucher_detail_no, + "voucher_detail_no": item.name, 'posting_date': posting_date, 'project': project, 'against_voucher_type': 'Process Deferred Accounting', 'against_voucher': deferred_process - }, account_currency) + }, account_currency, item=item) ) # GL Entry to debit the amount from the expense gl_entries.append( @@ -251,12 +251,12 @@ def make_gl_entries(doc, credit_account, debit_account, against, "debit": base_amount, "debit_in_account_currency": amount, "cost_center": cost_center, - "voucher_detail_no": voucher_detail_no, + "voucher_detail_no": item.name, 'posting_date': posting_date, 'project': project, 'against_voucher_type': 'Process Deferred Accounting', 'against_voucher': deferred_process - }, account_currency) + }, account_currency, item=item) ) if gl_entries: diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 7a85bfb26b..894ec5bdec 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -162,9 +162,9 @@ def toggle_disabling(doc): def get_doctypes_with_dimensions(): doclist = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset", - "Expense Claim", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", "Sales Invoice Item", "Purchase Invoice Item", - "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item", "Purchase Receipt Item", - "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule", + "Expense Claim", "Expense Claim Detail", "Expense Taxes and Charges", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", + "Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item", + "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule", "Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation", "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription", "Subscription Plan"] diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py index 39fc203d53..594b4d4a22 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py @@ -8,6 +8,7 @@ from frappe import _ from frappe.utils import flt, getdate, nowdate, add_days from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.general_ledger import make_gl_entries +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions class InvoiceDiscounting(AccountsController): def validate(self): @@ -81,10 +82,15 @@ class InvoiceDiscounting(AccountsController): def make_gl_entries(self): company_currency = frappe.get_cached_value('Company', self.company, "default_currency") + gl_entries = [] + invoice_fields = ["debit_to", "party_account_currency", "conversion_rate", "cost_center"] + accounting_dimensions = get_accounting_dimensions() + + invoice_fields.extend(accounting_dimensions) + for d in self.invoices: - inv = frappe.db.get_value("Sales Invoice", d.sales_invoice, - ["debit_to", "party_account_currency", "conversion_rate", "cost_center"], as_dict=1) + inv = frappe.db.get_value("Sales Invoice", d.sales_invoice, invoice_fields, as_dict=1) if d.outstanding_amount: outstanding_in_company_currency = flt(d.outstanding_amount * inv.conversion_rate, @@ -102,7 +108,7 @@ class InvoiceDiscounting(AccountsController): "cost_center": inv.cost_center, "against_voucher": d.sales_invoice, "against_voucher_type": "Sales Invoice" - }, inv.party_account_currency)) + }, inv.party_account_currency, item=inv)) gl_entries.append(self.get_gl_dict({ "account": self.accounts_receivable_credit, @@ -115,7 +121,7 @@ class InvoiceDiscounting(AccountsController): "cost_center": inv.cost_center, "against_voucher": d.sales_invoice, "against_voucher_type": "Sales Invoice" - }, ar_credit_account_currency)) + }, ar_credit_account_currency, item=inv)) make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding='No') diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 83c670eace..22df5be1b9 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -86,7 +86,7 @@ class PaymentEntry(AccountsController): self.update_payment_schedule(cancel=1) self.set_payment_req_status() self.set_status() - + def set_payment_req_status(self): from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status update_payment_req_status(self, None) @@ -280,7 +280,7 @@ class PaymentEntry(AccountsController): outstanding_amount, is_return = frappe.get_cached_value(d.reference_doctype, d.reference_name, ["outstanding_amount", "is_return"]) if outstanding_amount <= 0 and not is_return: no_oustanding_refs.setdefault(d.reference_doctype, []).append(d) - + for k, v in no_oustanding_refs.items(): frappe.msgprint(_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.

    \ If this is undesirable please cancel the corresponding Payment Entry.") @@ -506,7 +506,7 @@ class PaymentEntry(AccountsController): "against": against_account, "account_currency": self.party_account_currency, "cost_center": self.cost_center - }) + }, item=self) dr_or_cr = "credit" if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit" @@ -550,7 +550,7 @@ class PaymentEntry(AccountsController): "credit_in_account_currency": self.paid_amount, "credit": self.base_paid_amount, "cost_center": self.cost_center - }) + }, item=self) ) if self.payment_type in ("Receive", "Internal Transfer"): gl_entries.append( @@ -561,7 +561,7 @@ class PaymentEntry(AccountsController): "debit_in_account_currency": self.received_amount, "debit": self.base_received_amount, "cost_center": self.cost_center - }) + }, item=self) ) def add_deductions_gl_entries(self, gl_entries): diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 3aa24df16d..cf4e158bba 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -460,7 +460,7 @@ class PurchaseInvoice(BuyingController): "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center - }, self.party_account_currency) + }, self.party_account_currency, item=self) ) def make_item_gl_entries(self, gl_entries): @@ -841,7 +841,7 @@ class PurchaseInvoice(BuyingController): "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center - }, self.party_account_currency) + }, self.party_account_currency, item=self) ) gl_entries.append( @@ -852,7 +852,7 @@ class PurchaseInvoice(BuyingController): "credit_in_account_currency": self.base_paid_amount \ if bank_account_currency==self.company_currency else self.paid_amount, "cost_center": self.cost_center - }, bank_account_currency) + }, bank_account_currency, item=self) ) def make_write_off_gl_entry(self, gl_entries): @@ -873,7 +873,7 @@ class PurchaseInvoice(BuyingController): "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center - }, self.party_account_currency) + }, self.party_account_currency, item=self) ) gl_entries.append( self.get_gl_dict({ @@ -883,7 +883,7 @@ class PurchaseInvoice(BuyingController): "credit_in_account_currency": self.base_write_off_amount \ if write_off_account_currency==self.company_currency else self.write_off_amount, "cost_center": self.cost_center or self.write_off_cost_center - }) + }, item=self) ) def make_gle_for_rounding_adjustment(self, gl_entries): @@ -902,8 +902,7 @@ class PurchaseInvoice(BuyingController): "debit_in_account_currency": self.rounding_adjustment, "debit": self.base_rounding_adjustment, "cost_center": self.cost_center or round_off_cost_center, - } - )) + }, item=self)) def on_cancel(self): super(PurchaseInvoice, self).on_cancel() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 3b0fade0e5..05b85dabd4 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -791,7 +791,7 @@ class SalesInvoice(SellingController): "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center - }, self.party_account_currency) + }, self.party_account_currency, item=self) ) def make_tax_gl_entries(self, gl_entries): @@ -808,7 +808,7 @@ class SalesInvoice(SellingController): tax.precision("base_tax_amount_after_discount_amount")) if account_currency==self.company_currency else flt(tax.tax_amount_after_discount_amount, tax.precision("tax_amount_after_discount_amount"))), "cost_center": tax.cost_center - }, account_currency) + }, account_currency, item=tax) ) def make_item_gl_entries(self, gl_entries): @@ -828,7 +828,7 @@ class SalesInvoice(SellingController): for gle in fixed_asset_gl_entries: gle["against"] = self.customer - gl_entries.append(self.get_gl_dict(gle)) + gl_entries.append(self.get_gl_dict(gle, item=item)) asset.db_set("disposal_date", self.posting_date) asset.set_status("Sold" if self.docstatus==1 else None) @@ -866,7 +866,7 @@ class SalesInvoice(SellingController): "against_voucher": self.return_against if cint(self.is_return) else self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center - }) + }, item=self) ) gl_entries.append( self.get_gl_dict({ @@ -875,7 +875,7 @@ class SalesInvoice(SellingController): "against": self.customer, "debit": self.loyalty_amount, "remark": "Loyalty Points redeemed by the customer" - }) + }, item=self) ) def make_pos_gl_entries(self, gl_entries): @@ -896,7 +896,7 @@ class SalesInvoice(SellingController): "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center - }, self.party_account_currency) + }, self.party_account_currency, item=self) ) payment_mode_account_currency = get_account_currency(payment_mode.account) @@ -909,7 +909,7 @@ class SalesInvoice(SellingController): if payment_mode_account_currency==self.company_currency \ else payment_mode.amount, "cost_center": self.cost_center - }, payment_mode_account_currency) + }, payment_mode_account_currency, item=self) ) def make_gle_for_change_amount(self, gl_entries): @@ -927,7 +927,7 @@ class SalesInvoice(SellingController): "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center - }, self.party_account_currency) + }, self.party_account_currency, item=self) ) gl_entries.append( @@ -936,7 +936,7 @@ class SalesInvoice(SellingController): "against": self.customer, "credit": self.base_change_amount, "cost_center": self.cost_center - }) + }, item=self) ) else: frappe.throw(_("Select change amount account"), title="Mandatory Field") @@ -960,7 +960,7 @@ class SalesInvoice(SellingController): "against_voucher": self.return_against if cint(self.is_return) else self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center - }, self.party_account_currency) + }, self.party_account_currency, item=self) ) gl_entries.append( self.get_gl_dict({ @@ -971,7 +971,7 @@ class SalesInvoice(SellingController): self.precision("base_write_off_amount")) if write_off_account_currency==self.company_currency else flt(self.write_off_amount, self.precision("write_off_amount"))), "cost_center": self.cost_center or self.write_off_cost_center or default_cost_center - }, write_off_account_currency) + }, write_off_account_currency, item=self) ) def make_gle_for_rounding_adjustment(self, gl_entries): @@ -988,8 +988,7 @@ class SalesInvoice(SellingController): "credit": flt(self.base_rounding_adjustment, self.precision("base_rounding_adjustment")), "cost_center": self.cost_center or round_off_cost_center, - } - )) + }, item=self)) def update_billing_status_in_dn(self, update_modified=True): updated_delivery_notes = [] diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js index 622bab6946..07752e1e62 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.js +++ b/erpnext/accounts/report/trial_balance/trial_balance.js @@ -46,7 +46,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "default": frappe.defaults.get_user_default("year_end_date"), }, { - "fieldname":"cost_center", + "fieldname": "cost_center", "label": __("Cost Center"), "fieldtype": "Link", "options": "Cost Center", @@ -61,7 +61,13 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { } }, { - "fieldname":"finance_book", + "fieldname": "project", + "label": __("Project"), + "fieldtype": "Link", + "options": "Project" + }, + { + "fieldname": "finance_book", "label": __("Finance Book"), "fieldtype": "Link", "options": "Finance Book", @@ -97,7 +103,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { } erpnext.dimension_filters.forEach((dimension) => { - frappe.query_reports["Trial Balance"].filters.splice(5, 0 ,{ + frappe.query_reports["Trial Balance"].filters.splice(6, 0 ,{ "fieldname": dimension["fieldname"], "label": __(dimension["label"]), "fieldtype": "Link", diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index d78324157a..8bd4399e60 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -69,6 +69,10 @@ def get_data(filters): gl_entries_by_account = {} opening_balances = get_opening_balances(filters) + + #add filter inside list so that the query in financial_statements.py doesn't break + filters.project = [filters.project] + set_gl_entries_by_account(filters.company, filters.from_date, filters.to_date, min_lft, max_rgt, filters, gl_entries_by_account, ignore_closing_entries=not flt(filters.with_period_closing_entry)) @@ -102,6 +106,9 @@ def get_rootwise_opening_balances(filters, report_type): additional_conditions += """ and cost_center in (select name from `tabCost Center` where lft >= %s and rgt <= %s)""" % (lft, rgt) + if filters.project: + additional_conditions += " and project = %(project)s" + if filters.finance_book: fb_conditions = " AND finance_book = %(finance_book)s" if filters.include_default_book_entries: @@ -116,6 +123,7 @@ def get_rootwise_opening_balances(filters, report_type): "from_date": filters.from_date, "report_type": report_type, "year_start_date": filters.year_start_date, + "project": filters.project, "finance_book": filters.finance_book, "company_fb": frappe.db.get_value("Company", filters.company, 'default_finance_book') } diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index a3200d5644..505ba4c6b6 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -125,7 +125,7 @@ class Asset(AccountsController): if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date): frappe.throw(_("Available-for-use Date should be after purchase date")) - + def validate_gross_and_purchase_amount(self): if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_receipt_amount: frappe.throw(_("Gross Purchase Amount should be {} to purchase amount of one single Asset. {}\ @@ -455,7 +455,7 @@ class Asset(AccountsController): for d in self.get('finance_books'): if d.finance_book == self.default_finance_book: return cint(d.idx) - 1 - + def validate_make_gl_entry(self): purchase_document = self.get_purchase_document() asset_bought_with_invoice = purchase_document == self.purchase_invoice @@ -487,14 +487,14 @@ class Asset(AccountsController): purchase_document = self.purchase_invoice if asset_bought_with_invoice else self.purchase_receipt return purchase_document - + def get_asset_accounts(self): fixed_asset_account = get_asset_category_account('fixed_asset_account', asset=self.name, asset_category = self.asset_category, company = self.company) cwip_account = get_asset_account("capital_work_in_progress_account", self.name, self.asset_category, self.company) - + return fixed_asset_account, cwip_account def make_gl_entries(self): @@ -513,7 +513,7 @@ class Asset(AccountsController): "credit": self.purchase_receipt_amount, "credit_in_account_currency": self.purchase_receipt_amount, "cost_center": self.cost_center - })) + }, item=self)) gl_entries.append(self.get_gl_dict({ "account": fixed_asset_account, @@ -523,7 +523,7 @@ class Asset(AccountsController): "debit": self.purchase_receipt_amount, "debit_in_account_currency": self.purchase_receipt_amount, "cost_center": self.cost_center - })) + }, item=self)) if gl_entries: from erpnext.accounts.general_ledger import make_gl_entries diff --git a/erpnext/education/doctype/fees/fees.py b/erpnext/education/doctype/fees/fees.py index f0d60faed6..25d67d2d5f 100644 --- a/erpnext/education/doctype/fees/fees.py +++ b/erpnext/education/doctype/fees/fees.py @@ -98,14 +98,16 @@ class Fees(AccountsController): "debit_in_account_currency": self.grand_total, "against_voucher": self.name, "against_voucher_type": self.doctype - }) + }, item=self) + fee_gl_entry = self.get_gl_dict({ "account": self.income_account, "against": self.student, "credit": self.grand_total, "credit_in_account_currency": self.grand_total, "cost_center": self.cost_center - }) + }, item=self) + from erpnext.accounts.general_ledger import make_gl_entries make_gl_entries([student_gl_entries, fee_gl_entry], cancel=(self.docstatus == 2), update_outstanding="Yes", merge_entries=False) diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index ac1bfa1a39..ea469b82c9 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -116,8 +116,9 @@ class ExpenseClaim(AccountsController): "party_type": "Employee", "party": self.employee, "against_voucher_type": self.doctype, - "against_voucher": self.name - }) + "against_voucher": self.name, + "cost_center": self.cost_center + }, item=self) ) # expense entries @@ -129,7 +130,7 @@ class ExpenseClaim(AccountsController): "debit_in_account_currency": data.sanctioned_amount, "against": self.employee, "cost_center": data.cost_center - }) + }, item=data) ) for data in self.advances: @@ -157,7 +158,7 @@ class ExpenseClaim(AccountsController): "credit": self.grand_total, "credit_in_account_currency": self.grand_total, "against": self.employee - }) + }, item=self) ) gl_entry.append( @@ -170,7 +171,7 @@ class ExpenseClaim(AccountsController): "debit_in_account_currency": self.grand_total, "against_voucher": self.name, "against_voucher_type": self.doctype, - }) + }, item=self) ) return gl_entry @@ -187,7 +188,7 @@ class ExpenseClaim(AccountsController): "cost_center": self.cost_center, "against_voucher_type": self.doctype, "against_voucher": self.name - }) + }, item=tax) ) def validate_account_details(self): diff --git a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json index 16e9eef917..3cce50e090 100644 --- a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json +++ b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json @@ -13,9 +13,11 @@ "description", "section_break_6", "amount", - "cost_center", "column_break_8", - "sanctioned_amount" + "sanctioned_amount", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break" ], "fields": [ { @@ -104,12 +106,21 @@ "fieldtype": "Link", "label": "Cost Center", "options": "Cost Center" + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2019-12-11 13:42:33.233432", + "modified": "2020-05-11 18:54:35.601592", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim Detail", diff --git a/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json index d68caf1cc1..885e3eed97 100644 --- a/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json +++ b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json @@ -8,14 +8,16 @@ "engine": "InnoDB", "field_order": [ "account_head", - "cost_center", "rate", "col_break1", "description", "section_break_6", "tax_amount", "column_break_8", - "total" + "total", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break" ], "fields": [ { @@ -91,11 +93,20 @@ { "fieldname": "column_break_8", "fieldtype": "Column Break" + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" } ], "istable": 1, "links": [], - "modified": "2020-03-11 13:25:06.721917", + "modified": "2020-05-11 19:01:26.611758", "modified_by": "Administrator", "module": "HR", "name": "Expense Taxes and Charges", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 274728151a..e7df472272 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -623,7 +623,7 @@ erpnext.patches.v11_1.update_default_supplier_in_item_defaults erpnext.patches.v12_0.update_due_date_in_gle erpnext.patches.v12_0.add_default_buying_selling_terms_in_company erpnext.patches.v12_0.update_ewaybill_field_position -erpnext.patches.v12_0.create_accounting_dimensions_in_missing_doctypes +erpnext.patches.v12_0.create_accounting_dimensions_in_missing_doctypes #2020-05-11 erpnext.patches.v11_1.set_status_for_material_request_type_manufacture erpnext.patches.v12_0.move_plaid_settings_to_doctype execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_link') diff --git a/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py b/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py index b71ea66594..657decfed2 100644 --- a/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py +++ b/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py @@ -20,7 +20,8 @@ def execute(): else: insert_after_field = 'accounting_dimensions_section' - for doctype in ["Subscription Plan", "Subscription", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item"]: + for doctype in ["Subscription Plan", "Subscription", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", + "Expense Claim Detail", "Expense Taxes and Charges"]: field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname}) From 940856cc16bca544963473afea0d42644de2cf01 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 15 May 2020 14:30:24 +0530 Subject: [PATCH 77/82] fix: add tests for set_status --- .../doctype/purchase_invoice/purchase_invoice.py | 2 +- .../purchase_invoice/test_purchase_invoice.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 265969db1f..baf2ba9fd4 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1042,7 +1042,7 @@ class PurchaseInvoice(BuyingController): elif outstanding_amount > 0 and due_date >= nowdate: self.status = "Unpaid" #Check if outstanding amount is 0 due to debit note issued against invoice - elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}): + elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Purchase Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}): self.status = "Debit Note Issued" elif self.is_return == 1: self.status = "Return" diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index e41ad42846..6170005061 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -86,6 +86,8 @@ class TestPurchaseInvoice(unittest.TestCase): pe.submit() pi_doc = frappe.get_doc('Purchase Invoice', pi_doc.name) + pi_doc.load_from_db() + self.assertTrue(pi_doc.status, "Paid") self.assertRaises(frappe.LinkExistsError, pi_doc.cancel) unlink_payment_on_cancel_of_invoice() @@ -203,7 +205,9 @@ class TestPurchaseInvoice(unittest.TestCase): pi.insert() pi.submit() + pi.load_from_db() + self.assertTrue(pi.status, "Unpaid") self.check_gle_for_pi(pi.name) def check_gle_for_pi(self, pi): @@ -234,6 +238,9 @@ class TestPurchaseInvoice(unittest.TestCase): pi = frappe.copy_doc(test_records[0]) pi.insert() + pi.load_from_db() + + self.assertTrue(pi.status, "Draft") pi.naming_series = 'TEST-' self.assertRaises(frappe.CannotChangeConstantError, pi.save) @@ -248,6 +255,8 @@ class TestPurchaseInvoice(unittest.TestCase): pi.get("taxes").pop(1) pi.insert() pi.submit() + pi.load_from_db() + self.assertTrue(pi.status, "Unpaid") gl_entries = frappe.db.sql("""select account, debit, credit from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s @@ -599,6 +608,11 @@ class TestPurchaseInvoice(unittest.TestCase): # return entry pi1 = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2, rate=50, update_stock=1) + pi.load_from_db() + self.assertTrue(pi.status, "Debit Note Issued") + pi1.load_from_db() + self.assertTrue(pi1.status, "Return") + actual_qty_2 = get_qty_after_transaction() self.assertEqual(actual_qty_1 - 2, actual_qty_2) @@ -771,6 +785,8 @@ class TestPurchaseInvoice(unittest.TestCase): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import get_outstanding_amount pi = make_purchase_invoice(item_code = "_Test Item", qty = (5 * -1), rate=500, is_return = 1) + pi.load_from_db() + self.assertTrue(pi.status, "Return") outstanding_amount = get_outstanding_amount(pi.doctype, pi.name, "Creditors - _TC", pi.supplier, "Supplier") From 03d165ff79291a3b4c104f398277fbbd43d8cb84 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 15 May 2020 16:39:02 +0530 Subject: [PATCH 78/82] fix: update remark on submitting payment entry --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 83c670eace..7e3b1349f2 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -451,8 +451,6 @@ class PaymentEntry(AccountsController): frappe.throw(_("Reference No and Reference Date is mandatory for Bank transaction")) def set_remarks(self): - if self.remarks: return - if self.payment_type=="Internal Transfer": remarks = [_("Amount {0} {1} transferred from {2} to {3}") .format(self.paid_from_account_currency, self.paid_amount, self.paid_from, self.paid_to)] From 7be71c88f21b93f9058a1dbe96fa79509a05ccac Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Fri, 15 May 2020 19:23:41 +0530 Subject: [PATCH 79/82] fix: Better validation message for group accounts (#21725) --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index efab5801e8..291aff3f5a 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -112,8 +112,8 @@ class GLEntry(Document): from tabAccount where name=%s""", self.account, as_dict=1)[0] if ret.is_group==1: - frappe.throw(_("{0} {1}: Account {2} cannot be a Group") - .format(self.voucher_type, self.voucher_no, self.account)) + frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in + transactions''').format(self.voucher_type, self.voucher_no, self.account)) if ret.docstatus==2: frappe.throw(_("{0} {1}: Account {2} is inactive") From 5cfbdf4bcbb96ec891f114e891e42d6948dfb694 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 15 May 2020 19:36:23 +0530 Subject: [PATCH 80/82] fix: user not able to view product (#21740) --- erpnext/shopping_cart/product_info.py | 8 +++++--- erpnext/stock/doctype/item/item.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/shopping_cart/product_info.py b/erpnext/shopping_cart/product_info.py index a7da09cb80..21ee335125 100644 --- a/erpnext/shopping_cart/product_info.py +++ b/erpnext/shopping_cart/product_info.py @@ -10,14 +10,16 @@ from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings from erpnext.utilities.product import get_price, get_qty_in_stock, get_non_stock_item_status @frappe.whitelist(allow_guest=True) -def get_product_info_for_website(item_code): +def get_product_info_for_website(item_code, skip_quotation_creation=False): """get product price / stock info for website""" cart_settings = get_shopping_cart_settings() if not cart_settings.enabled: return frappe._dict() - cart_quotation = _get_cart_quotation() + cart_quotation = frappe._dict() + if not skip_quotation_creation: + cart_quotation = _get_cart_quotation() price = get_price( item_code, @@ -51,7 +53,7 @@ def get_product_info_for_website(item_code): def set_product_info_for_website(item): """set product price uom for website""" - product_info = get_product_info_for_website(item.item_code) + product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True) if product_info: item.update(product_info) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 4cc50bba9e..7a1c1279ea 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -467,7 +467,7 @@ class Item(WebsiteGenerator): def set_shopping_cart_data(self, context): from erpnext.shopping_cart.product_info import get_product_info_for_website - context.shopping_cart = get_product_info_for_website(self.name) + context.shopping_cart = get_product_info_for_website(self.name, skip_quotation_creation=True) def add_default_uom_in_conversion_factor_table(self): uom_conv_list = [d.uom for d in self.get("uoms")] From 49dbbdc4cb2a7b8c385cca7b92684c7ab6c5e606 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 16 May 2020 04:59:43 +0530 Subject: [PATCH 81/82] fix: promotional scheme not able to savce --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 19f571fb30..4d9053a55b 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -99,7 +99,7 @@ class PricingRule(Document): self.same_item = 1 def validate_max_discount(self): - if self.rate_or_discount == "Discount Percentage" and self.items: + if self.rate_or_discount == "Discount Percentage" and self.get("items"): for d in self.items: max_discount = frappe.get_cached_value("Item", d.item_code, "max_discount") if max_discount and flt(self.discount_percentage) > flt(max_discount): From 2a0e3e3515ed4b25cbbe771ec39d5472aa9d630e Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sat, 16 May 2020 18:07:03 +0530 Subject: [PATCH 82/82] fix: Remove strip --- .../tax_withholding_category/tax_withholding_category.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index e904a681f6..93bec686ce 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -58,7 +58,7 @@ def get_tax_withholding_details(tax_withholding_category, fiscal_year, company): "rate": tax_rate_detail.tax_withholding_rate, "threshold": tax_rate_detail.single_threshold, "cumulative_threshold": tax_rate_detail.cumulative_threshold, - "description": tax_withholding.category_name.strip() if tax_withholding.category_name.strip() else tax_withholding_category + "description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category }) def get_tax_withholding_rates(tax_withholding, fiscal_year):