From 8179804405f6955ba35487a08516ce9486f9be3c Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 14 Apr 2020 23:05:11 +0530 Subject: [PATCH 01/12] 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/12] 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 31a747b98ad4220d6659523f9a53b5bb81ba9252 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 7 May 2020 17:38:00 +0530 Subject: [PATCH 03/12] 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 734bfcdfc2878ea9e69d90f83a02a30912cecb26 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 12 May 2020 14:11:33 +0530 Subject: [PATCH 04/12] 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 13096cdbfefd58f16f7e009a48343a5601f41221 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 13 May 2020 22:02:56 +0530 Subject: [PATCH 05/12] 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 10df3d5081c54d5ee70bdf69c81c3d70acb35169 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 14 May 2020 17:15:16 +0530 Subject: [PATCH 06/12] 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 07/12] 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 c649468f37db8fb941f940eeae7c63807729840a Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 15 May 2020 11:35:41 +0530 Subject: [PATCH 08/12] 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 940856cc16bca544963473afea0d42644de2cf01 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 15 May 2020 14:30:24 +0530 Subject: [PATCH 09/12] 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 10/12] 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 11/12] 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 12/12] 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")]