Merge pull request #21273 from marination/quoted-item-report

refactor: Quoted Item Comparison Report
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js
index 16061c6..1b8b404 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js
@@ -18,6 +18,10 @@
 	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 82fc628..3bc441a 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,18 @@
    "options": "Opportunity",
    "print_hide": 1,
    "read_only": 1
+  },
+  {
+   "fieldname": "valid_till",
+   "fieldtype": "Date",
+   "label": "Valid Till"
   }
  ],
  "icon": "fa fa-shopping-cart",
  "idx": 29,
  "is_submittable": 1,
  "links": [],
- "modified": "2019-12-30 19:17:28.208693",
+ "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.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py
index 5b4356a..baf2457 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 @@
 		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 @@
 				"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 @@
 	}, 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/doctype/supplier_quotation/supplier_quotation_list.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js
index 9555439..9f4fece 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 @@
 			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 3d05612..a76ffee 100644
--- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js
+++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js
@@ -5,20 +5,18 @@
 	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,
 			default: "",
 			options: "Item",
 			label: __("Item"),
-			fieldname: "item",
+			fieldname: "item_code",
 			fieldtype: "Link",
 			get_query: () => {
 				let quote = frappe.query_report.get_filter_value('supplier_quotation');
@@ -37,8 +35,37 @@
 					}
 				}
 			}
+		},
+		{
+			fieldname: "supplier",
+			label: __("Supplier"),
+			fieldtype: "MultiSelectList",
+			get_data: function(txt) {
+				return frappe.db.get_link_options('Supplier', txt);
+			}
+		},
+		{
+			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] } }
+			}
 		}
 	],
+
 	onload: (report) => {
 		// Create a button for setting the default supplier
 		report.page.add_inner_button(__("Select Default Supplier"), () => {
@@ -102,6 +129,4 @@
 		});
 		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 5aff6ba..a33867a 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,180 @@
 # 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 frappe import _
+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)
-	return columns, data
-	
-def get_quote_list(item, qty_list):
-	out = []
-	if not item:
+	if not filters:
+		return [], []
+
+	conditions = get_conditions(filters)
+	supplier_quotation_data = get_data(filters, conditions)
+	columns = get_columns()
+
+	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):
+	if not filters.get("item_code"):
 		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)
+	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 = %(item_code)s
+			AND sqi.parent = sq.name
+			AND sqi.docstatus < 2
+			AND sq.company = %(company)s
+			AND sq.status != 'Expired'
+			{0}""".format(conditions), filters, as_dict=1)
 
-	#Add a row for each supplier
-	for root in set(suppliers):
-		supplier_currency = frappe.db.get_value("Supplier", root, "default_currency")
+	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:
 			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),
+			"uom": data.get("uom"),
+			"request_for_quotation": data.get("request_for_quotation"),
+		}
 
-		for qt in qty_list:
-			col = frappe._dict({
-				"key": str(qt.qty),
-				"label": "Qty: " + str(int(qt.qty))
-			})
-			out.append(col)
+		# map for report view of form {'supplier1':[{},{},...]}
+		supplier_wise_map[supplier].append(row)
 
-	return out
-	
-def get_columns(qty_list):
+		# 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)
+
+	chart_data = prepare_chart_data(suppliers, qty_list, supplier_qty_price_map)
+
+	return out, chart_data
+
+def prepare_chart_data(suppliers, qty_list, supplier_qty_price_map):
+	data_points_map = {}
+	qty_list.sort()
+
+	# 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 = [{
 		"fieldname": "supplier_name",
-		"label": "Supplier",
+		"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": "uom",
+		"label": _("UOM"),
+		"fieldtype": "Link",
+		"options": "UOM",
+		"width": 90
+	},
+	{
+		"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 6b198e7..ab161aa 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -308,7 +308,8 @@
 		"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.healthcare.doctype.patient_appointment.patient_appointment.update_appointment_status"
+		"erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_appointment_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 e7df472..ebbcccc 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -683,3 +683,4 @@
 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
+erpnext.patches.v12_0.set_valid_till_date_in_supplier_quotation
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 0000000..4a6e228
--- /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():
+	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""")