Merge branch 'develop' into refactor/qb/pick-list
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
index 7921fcc..c62b711 100644
--- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
@@ -6,6 +6,7 @@
  "engine": "InnoDB",
  "field_order": [
   "api_details_section",
+  "disabled",
   "service_provider",
   "api_endpoint",
   "url",
@@ -77,12 +78,18 @@
    "label": "Service Provider",
    "options": "frankfurter.app\nexchangerate.host\nCustom",
    "reqd": 1
+  },
+  {
+   "default": "0",
+   "fieldname": "disabled",
+   "fieldtype": "Check",
+   "label": "Disabled"
   }
  ],
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2022-01-10 15:51:14.521174",
+ "modified": "2023-01-09 12:19:03.955906",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Currency Exchange Settings",
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 13712ce..12c0b7a 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -334,7 +334,7 @@
 		)
 
 		# Account Currency has balance
-		dr_or_cr = "debit" if self.party_type == "Customer" else "debit"
+		dr_or_cr = "debit" if self.party_type == "Customer" else "credit"
 		reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
 
 		journal_account = frappe._dict(
diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js
index 832be23..67bd24d 100644
--- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js
+++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js
@@ -13,38 +13,24 @@
 			reqd: 1
 		},
 		{
-			fieldname: "fiscal_year",
-			label: __("Fiscal Year"),
-			fieldtype: "Link",
-			options: "Fiscal Year",
-			default: frappe.defaults.get_user_default("fiscal_year"),
-			reqd: 1,
-			on_change: function(query_report) {
-				var fiscal_year = query_report.get_values().fiscal_year;
-				if (!fiscal_year) {
-					return;
-				}
-				frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
-					var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
-					frappe.query_report.set_filter_value({
-						from_date: fy.year_start_date,
-						to_date: fy.year_end_date
-					});
-				});
-			}
+			label: __("Based On"),
+			fieldname:"based_on",
+			fieldtype: "Select",
+			options: "Creation Date\nPlanned Date\nActual Date",
+			default: "Creation Date"
 		},
 		{
 			label: __("From Posting Date"),
 			fieldname:"from_date",
 			fieldtype: "Date",
-			default: frappe.defaults.get_user_default("year_start_date"),
+			default: frappe.datetime.add_months(frappe.datetime.get_today(), -3),
 			reqd: 1
 		},
 		{
 			label: __("To Posting Date"),
 			fieldname:"to_date",
 			fieldtype: "Date",
-			default: frappe.defaults.get_user_default("year_end_date"),
+			default: frappe.datetime.get_today(),
 			reqd: 1,
 		},
 		{
diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py
index b69ad07..97f30ef 100644
--- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py
+++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py
@@ -31,6 +31,7 @@
 		"sales_order",
 		"production_item",
 		"qty",
+		"creation",
 		"produced_qty",
 		"planned_start_date",
 		"planned_end_date",
@@ -47,11 +48,17 @@
 		if filters.get(field):
 			query_filters[field] = filters.get(field)
 
-	query_filters["planned_start_date"] = (">=", filters.get("from_date"))
-	query_filters["planned_end_date"] = ("<=", filters.get("to_date"))
+	if filters.get("based_on") == "Planned Date":
+		query_filters["planned_start_date"] = (">=", filters.get("from_date"))
+		query_filters["planned_end_date"] = ("<=", filters.get("to_date"))
+	elif filters.get("based_on") == "Actual Date":
+		query_filters["actual_start_date"] = (">=", filters.get("from_date"))
+		query_filters["actual_end_date"] = ("<=", filters.get("to_date"))
+	else:
+		query_filters["creation"] = ("between", [filters.get("from_date"), filters.get("to_date")])
 
 	data = frappe.get_all(
-		"Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc"
+		"Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc", debug=1
 	)
 
 	res = []
@@ -214,6 +221,12 @@
 				"width": 90,
 			},
 			{
+				"label": _("Created On"),
+				"fieldname": "creation",
+				"fieldtype": "Date",
+				"width": 150,
+			},
+			{
 				"label": _("Planned Start Date"),
 				"fieldname": "planned_start_date",
 				"fieldtype": "Date",
diff --git a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py
index 5c4b578..cbc40bb 100644
--- a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py
+++ b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py
@@ -14,7 +14,6 @@
 		},
 		"internal_links": {
 			"Quotation": ["items", "prevdoc_docname"],
-			"Material Request": ["items", "material_request"],
 		},
 		"transactions": [
 			{
diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py
index 54bd8c3..bab57fe 100644
--- a/erpnext/setup/utils.py
+++ b/erpnext/setup/utils.py
@@ -81,6 +81,11 @@
 	if entries:
 		return flt(entries[0].exchange_rate)
 
+	if frappe.get_cached_value(
+		"Currency Exchange Settings", "Currency Exchange Settings", "disabled"
+	):
+		return 0.00
+
 	try:
 		cache = frappe.cache()
 		key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date, from_currency, to_currency)
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index cecc588..808f19e 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -4,7 +4,7 @@
 import json
 from collections import OrderedDict, defaultdict
 from itertools import groupby
-from typing import Dict, List, Set
+from typing import Dict, List
 
 import frappe
 from frappe import _
@@ -41,7 +41,9 @@
 				)
 
 	def before_submit(self):
-		update_sales_orders = set()
+		self.validate_picked_items()
+
+	def validate_picked_items(self):
 		for item in self.locations:
 			if self.scan_mode and item.picked_qty < item.stock_qty:
 				frappe.throw(
@@ -50,17 +52,14 @@
 					).format(item.idx, item.stock_qty - item.picked_qty, item.stock_uom),
 					title=_("Pick List Incomplete"),
 				)
-			elif not self.scan_mode and item.picked_qty == 0:
+
+			if not self.scan_mode and item.picked_qty == 0:
 				# if the user has not entered any picked qty, set it to stock_qty, before submit
 				item.picked_qty = item.stock_qty
 
-			if item.sales_order_item:
-				# update the picked_qty in SO Item
-				self.update_sales_order_item(item, item.picked_qty, item.item_code)
-				update_sales_orders.add(item.sales_order)
-
 			if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"):
 				continue
+
 			if not item.serial_no:
 				frappe.throw(
 					_("Row #{0}: {1} does not have any available serial numbers in {2}").format(
@@ -68,58 +67,96 @@
 					),
 					title=_("Serial Nos Required"),
 				)
-			if len(item.serial_no.split("\n")) == item.picked_qty:
-				continue
-			frappe.throw(
-				_(
-					"For item {0} at row {1}, count of serial numbers does not match with the picked quantity"
-				).format(frappe.bold(item.item_code), frappe.bold(item.idx)),
-				title=_("Quantity Mismatch"),
-			)
 
-		self.update_bundle_picked_qty()
-		self.update_sales_order_picking_status(update_sales_orders)
-
-	def before_cancel(self):
-		"""Deduct picked qty on cancelling pick list"""
-		updated_sales_orders = set()
-
-		for item in self.get("locations"):
-			if item.sales_order_item:
-				self.update_sales_order_item(item, -1 * item.picked_qty, item.item_code)
-				updated_sales_orders.add(item.sales_order)
-
-		self.update_bundle_picked_qty()
-		self.update_sales_order_picking_status(updated_sales_orders)
-
-	def update_sales_order_item(self, item, picked_qty, item_code):
-		item_table = "Sales Order Item" if not item.product_bundle_item else "Packed Item"
-		stock_qty_field = "stock_qty" if not item.product_bundle_item else "qty"
-
-		already_picked, actual_qty = frappe.db.get_value(
-			item_table,
-			item.sales_order_item,
-			["picked_qty", stock_qty_field],
-			for_update=True,
-		)
-
-		if self.docstatus == 1:
-			if (((already_picked + picked_qty) / actual_qty) * 100) > (
-				100 + flt(frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance"))
-			):
+			if len(item.serial_no.split("\n")) != item.picked_qty:
 				frappe.throw(
 					_(
-						"You are picking more than required quantity for {}. Check if there is any other pick list created for {}"
-					).format(item_code, item.sales_order)
+						"For item {0} at row {1}, count of serial numbers does not match with the picked quantity"
+					).format(frappe.bold(item.item_code), frappe.bold(item.idx)),
+					title=_("Quantity Mismatch"),
 				)
 
-		frappe.db.set_value(item_table, item.sales_order_item, "picked_qty", already_picked + picked_qty)
+	def on_submit(self):
+		self.update_bundle_picked_qty()
+		self.update_reference_qty()
+		self.update_sales_order_picking_status()
 
-	@staticmethod
-	def update_sales_order_picking_status(sales_orders: Set[str]) -> None:
+	def on_cancel(self):
+		self.update_bundle_picked_qty()
+		self.update_reference_qty()
+		self.update_sales_order_picking_status()
+
+	def update_reference_qty(self):
+		packed_items = []
+		so_items = []
+
+		for item in self.locations:
+			if item.product_bundle_item:
+				packed_items.append(item.sales_order_item)
+			elif item.sales_order_item:
+				so_items.append(item.sales_order_item)
+
+		if packed_items:
+			self.update_packed_items_qty(packed_items)
+
+		if so_items:
+			self.update_sales_order_item_qty(so_items)
+
+	def update_packed_items_qty(self, packed_items):
+		picked_items = get_picked_items_qty(packed_items)
+		self.validate_picked_qty(picked_items)
+
+		picked_qty = frappe._dict()
+		for d in picked_items:
+			picked_qty[d.sales_order_item] = d.picked_qty
+
+		for packed_item in packed_items:
+			frappe.db.set_value(
+				"Packed Item",
+				packed_item,
+				"picked_qty",
+				flt(picked_qty.get(packed_item)),
+				update_modified=False,
+			)
+
+	def update_sales_order_item_qty(self, so_items):
+		picked_items = get_picked_items_qty(so_items)
+		self.validate_picked_qty(picked_items)
+
+		picked_qty = frappe._dict()
+		for d in picked_items:
+			picked_qty[d.sales_order_item] = d.picked_qty
+
+		for so_item in so_items:
+			frappe.db.set_value(
+				"Sales Order Item",
+				so_item,
+				"picked_qty",
+				flt(picked_qty.get(so_item)),
+				update_modified=False,
+			)
+
+	def update_sales_order_picking_status(self) -> None:
+		sales_orders = []
+		for row in self.locations:
+			if row.sales_order and row.sales_order not in sales_orders:
+				sales_orders.append(row.sales_order)
+
 		for sales_order in sales_orders:
-			if sales_order:
-				frappe.get_doc("Sales Order", sales_order, for_update=True).update_picking_status()
+			frappe.get_doc("Sales Order", sales_order, for_update=True).update_picking_status()
+
+	def validate_picked_qty(self, data):
+		over_delivery_receipt_allowance = 100 + flt(
+			frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance")
+		)
+
+		for row in data:
+			if (row.picked_qty / row.stock_qty) * 100 > over_delivery_receipt_allowance:
+				frappe.throw(
+					_(
+						f"You are picking more than required quantity for the item {row.item_code}. Check if there is any other pick list created for the sales order {row.sales_order}."
+					)
+				)
 
 	@frappe.whitelist()
 	def set_item_locations(self, save=False):
@@ -309,6 +346,31 @@
 		return int(flt(min(possible_bundles), precision or 6))
 
 
+def get_picked_items_qty(items) -> List[Dict]:
+	return frappe.db.sql(
+		f"""
+		SELECT
+			sales_order_item,
+			item_code,
+			sales_order,
+			SUM(stock_qty) AS stock_qty,
+			SUM(picked_qty) AS picked_qty
+		FROM
+			`tabPick List Item`
+		WHERE
+			sales_order_item IN (
+				{", ".join(frappe.db.escape(d) for d in items)}
+			)
+			AND docstatus = 1
+		GROUP BY
+			sales_order_item,
+			sales_order
+		FOR UPDATE
+	""",
+		as_dict=1,
+	)
+
+
 def validate_item_locations(pick_list):
 	if not pick_list.locations:
 		frappe.throw(_("Add items in the Item Locations table"))