Merge branch 'develop' into repack-entry-stock-ageing
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index eab6d50..859146b 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -40,10 +40,14 @@
     echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres;
 fi
 
-wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
-tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
-sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
-sudo chmod o+x /usr/local/bin/wkhtmltopdf
+
+install_whktml() {
+    wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
+    tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
+    sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
+    sudo chmod o+x /usr/local/bin/wkhtmltopdf
+}
+install_whktml &
 
 cd ~/frappe-bench || exit
 
@@ -57,5 +61,5 @@
 if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
 
 bench start &> bench_run_logs.txt &
+CI=Yes bench build --app frappe &
 bench --site test_site reinstall --yes
-bench build --app frappe
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
index dbf3622..46ba27c 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
@@ -64,6 +64,7 @@
 					"account_currency",
 					(r) => {
 						frm.currency = r.account_currency;
+						frm.trigger("render_chart");
 					}
 				);
 			}
@@ -128,7 +129,7 @@
 		}
 	},
 
-	render_chart(frm) {
+	render_chart: frappe.utils.debounce((frm) => {
 		frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager(
 			{
 				$reconciliation_tool_cards: frm.get_field(
@@ -140,7 +141,7 @@
 				currency: frm.currency,
 			}
 		);
-	},
+	}, 500),
 
 	render(frm) {
 		if (frm.doc.bank_account) {
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 994b903..d05787f 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1955,7 +1955,8 @@
 
 		qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse)
 
-	update_bin_qty(row.item_code, row.warehouse, qty_dict)
+	if row.warehouse:
+		update_bin_qty(row.item_code, row.warehouse, qty_dict)
 
 def validate_and_delete_children(parent, data):
 	deleted_children = []
diff --git a/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json b/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json
index 8a8d425..0cfcf0e 100644
--- a/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json
+++ b/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json
@@ -3,7 +3,7 @@
  "allow_events_in_timeline": 0, 
  "allow_guest_to_view": 0, 
  "allow_import": 0, 
- "allow_rename": 0, 
+ "allow_rename": 1, 
  "autoname": "field:lost_reason", 
  "beta": 0, 
  "creation": "2018-12-28 14:48:51.044975", 
@@ -57,7 +57,7 @@
  "issingle": 0, 
  "istable": 0, 
  "max_attachments": 0, 
- "modified": "2018-12-28 14:49:43.336437", 
+ "modified": "2022-02-16 10:49:43.336437", 
  "modified_by": "Administrator", 
  "module": "CRM", 
  "name": "Opportunity Lost Reason", 
@@ -150,4 +150,4 @@
  "track_changes": 0, 
  "track_seen": 0, 
  "track_views": 0
-}
\ No newline at end of file
+}
diff --git a/erpnext/e_commerce/variant_selector/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py
index bb6b3ef..3107c01 100644
--- a/erpnext/e_commerce/variant_selector/item_variants_cache.py
+++ b/erpnext/e_commerce/variant_selector/item_variants_cache.py
@@ -66,26 +66,24 @@
 			)
 		]
 
-		# join with Website Item
-		item_variants_data = frappe.get_all(
-			'Item Variant Attribute',
-			{'variant_of': parent_item_code},
-			['parent', 'attribute', 'attribute_value'],
-			order_by='name',
-			as_list=1
+		# Get Variants and tehir Attributes that are not disabled
+		iva = frappe.qb.DocType("Item Variant Attribute")
+		item = frappe.qb.DocType("Item")
+		query = (
+			frappe.qb.from_(iva)
+			.join(item).on(item.name == iva.parent)
+			.select(
+				iva.parent, iva.attribute, iva.attribute_value
+			).where(
+				(iva.variant_of == parent_item_code)
+				& (item.disabled == 0)
+			).orderby(iva.name)
 		)
-
-		disabled_items = set(
-			[i.name for i in frappe.db.get_all('Item', {'disabled': 1})]
-		)
+		item_variants_data = query.run()
 
 		attribute_value_item_map = frappe._dict()
 		item_attribute_value_map = frappe._dict()
 
-		# dont consider variants that are disabled
-		# pull all other variants
-		item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items]
-
 		for row in item_variants_data:
 			item_code, attribute, attribute_value = row
 			# (attr, value) => [item1, item2]
@@ -124,4 +122,7 @@
 def enqueue_build_cache(item_code):
 	if frappe.cache().hget('item_cache_build_in_progress', item_code):
 		return
-	frappe.enqueue(build_cache, item_code=item_code, queue='long')
+	frappe.enqueue(
+		"erpnext.e_commerce.variant_selector.item_variants_cache.build_cache",
+		item_code=item_code, queue='long'
+	)
diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py
index b83961e..4d907c6 100644
--- a/erpnext/e_commerce/variant_selector/test_variant_selector.py
+++ b/erpnext/e_commerce/variant_selector/test_variant_selector.py
@@ -104,6 +104,8 @@
 		})
 
 		make_web_item_price(item_code="Test-Tshirt-Temp-S-R", price_list_rate=100)
+
+		frappe.local.shopping_cart_settings = None # clear cached settings values
 		next_values = get_next_attribute_and_values(
 			"Test-Tshirt-Temp",
 			selected_attributes={"Test Size": "Small", "Test Colour": "Red"}
diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
index a8119ac..f02f76e 100644
--- a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
+++ b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
@@ -13,7 +13,7 @@
 
 
 class GoCardlessSettings(Document):
-	supported_currencies = ["EUR", "DKK", "GBP", "SEK"]
+	supported_currencies = ["EUR", "DKK", "GBP", "SEK", "AUD", "NZD", "CAD", "USD"]
 
 	def validate(self):
 		self.initialize_client()
@@ -80,7 +80,7 @@
 
 	def validate_transaction_currency(self, currency):
 		if currency not in self.supported_currencies:
-			frappe.throw(_("Please select another payment method. Stripe does not support transactions in currency '{0}'").format(currency))
+			frappe.throw(_("Please select another payment method. Go Cardless does not support transactions in currency '{0}'").format(currency))
 
 	def get_payment_url(self, **kwargs):
 		return get_url("./integrations/gocardless_checkout?{0}".format(urlencode(kwargs)))
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 0a468f1..80003da 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -319,7 +319,7 @@
 
 		if self.total_produced_qty > 0:
 			self.status = "In Process"
-			if self.check_have_work_orders_completed():
+			if self.all_items_completed():
 				self.status = "Completed"
 
 		if self.status != 'Completed':
@@ -591,21 +591,32 @@
 
 			self.append("sub_assembly_items", data)
 
-	def check_have_work_orders_completed(self):
-		wo_status = frappe.db.get_list(
+	def all_items_completed(self):
+		all_items_produced = all(flt(d.planned_qty) - flt(d.produced_qty) < 0.000001
+									for d in self.po_items)
+		if not all_items_produced:
+			return False
+
+		wo_status = frappe.get_all(
 			"Work Order",
-			filters={"production_plan": self.name},
+			filters={
+				"production_plan": self.name,
+				"status": ("not in", ["Closed", "Stopped"]),
+				"docstatus": ("<", 2),
+			},
 			fields="status",
-			pluck="status"
+			pluck="status",
 		)
-		return all(s == "Completed" for s in wo_status)
+		all_work_orders_completed = all(s == "Completed" for s in wo_status)
+		return all_work_orders_completed
 
 @frappe.whitelist()
 def download_raw_materials(doc, warehouses=None):
 	if isinstance(doc, str):
 		doc = frappe._dict(json.loads(doc))
 
-	item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM',
+	item_list = [['Item Code', 'Item Name', 'Description',
+		'Stock UOM', 'Warehouse', 'Required Qty as per BOM',
 		'Projected Qty', 'Available Qty In Hand', 'Ordered Qty', 'Planned Qty',
 		'Reserved Qty for Production', 'Safety Stock', 'Required Qty']]
 
@@ -614,7 +625,8 @@
 	items = get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True)
 
 	for d in items:
-		item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('warehouse'),
+		item_list.append([d.get('item_code'), d.get('item_name'),
+			d.get('description'), d.get('stock_uom'), d.get('warehouse'),
 			d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'),
 			d.get('planned_qty'), d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')])
 
@@ -1044,4 +1056,4 @@
 def set_default_warehouses(row, default_warehouses):
 	for field in ['wip_warehouse', 'fg_warehouse']:
 		if not row.get(field):
-			row[field] = default_warehouses.get(field)
\ No newline at end of file
+			row[field] = default_warehouses.get(field)
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index afa1501..d88e10a 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -409,9 +409,6 @@
 		boms = {
 			"Assembly": {
 				"SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
-				"SubAssembly2": {"ChildPart3": {}},
-				"SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}},
-				"ChildPart5": {},
 				"ChildPart6": {},
 				"SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}},
 			},
@@ -591,6 +588,20 @@
 		pln.reload()
 		self.assertEqual(pln.po_items[0].pending_qty, 1)
 
+	def test_qty_based_status(self):
+		pp = frappe.new_doc("Production Plan")
+		pp.po_items = [
+			frappe._dict(planned_qty=5, produce_qty=4)
+		]
+		self.assertFalse(pp.all_items_completed())
+
+		pp.po_items = [
+			frappe._dict(planned_qty=5, produce_qty=10),
+			frappe._dict(planned_qty=5, produce_qty=4)
+		]
+		self.assertFalse(pp.all_items_completed())
+
+
 def create_production_plan(**args):
 	"""
 	sales_order (obj): Sales Order Doc Object
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index d104bc0..a93ceca 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -329,7 +329,6 @@
 erpnext.patches.v14_0.set_payroll_cost_centers
 erpnext.patches.v13_0.agriculture_deprecation_warning
 erpnext.patches.v13_0.hospitality_deprecation_warning
-erpnext.patches.v13_0.update_exchange_rate_settings
 erpnext.patches.v13_0.update_asset_quantity_field
 erpnext.patches.v13_0.delete_bank_reconciliation_detail
 erpnext.patches.v13_0.enable_provisional_accounting
@@ -351,4 +350,6 @@
 erpnext.patches.v13_0.shopping_cart_to_ecommerce
 erpnext.patches.v13_0.update_disbursement_account
 erpnext.patches.v13_0.update_reserved_qty_closed_wo
+erpnext.patches.v13_0.update_exchange_rate_settings
 erpnext.patches.v14_0.delete_amazon_mws_doctype
+erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
diff --git a/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py b/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py
index 9b083ca..8dec9ff 100644
--- a/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py
+++ b/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py
@@ -9,6 +9,8 @@
 		FROM `tabBin`""",as_dict=1)
 
 	for entry in bin_details:
+		if not (entry.item_code and entry.warehouse):
+			continue
 		update_bin_qty(entry.get("item_code"), entry.get("warehouse"), {
 			"indented_qty": get_indented_qty(entry.get("item_code"), entry.get("warehouse"))
 		})
diff --git a/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py b/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py
new file mode 100644
index 0000000..f097ab9
--- /dev/null
+++ b/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py
@@ -0,0 +1,36 @@
+import frappe
+
+
+def execute():
+    """
+    1. Get submitted Work Orders with MR, MR Item and SO set
+    2. Get SO Item detail from MR Item detail in WO, and set in WO
+    3. Update work_order_qty in SO
+    """
+    work_order = frappe.qb.DocType("Work Order")
+    query = (
+        frappe.qb.from_(work_order)
+        .select(
+            work_order.name, work_order.produced_qty,
+            work_order.material_request,
+            work_order.material_request_item,
+            work_order.sales_order
+        ).where(
+            (work_order.material_request.isnotnull())
+            & (work_order.material_request_item.isnotnull())
+            & (work_order.sales_order.isnotnull())
+            & (work_order.docstatus == 1)
+            & (work_order.produced_qty > 0)
+        )
+    )
+    results = query.run(as_dict=True)
+
+    for row in results:
+        so_item = frappe.get_value(
+            "Material Request Item", row.material_request_item, "sales_order_item"
+        )
+        frappe.db.set_value("Work Order", row.name, "sales_order_item", so_item)
+
+        if so_item:
+            wo = frappe.get_doc("Work Order", row.name)
+            wo.update_work_order_qty_in_so()
diff --git a/erpnext/patches/v14_0/update_opportunity_currency_fields.py b/erpnext/patches/v14_0/update_opportunity_currency_fields.py
index 1307147..75049a6 100644
--- a/erpnext/patches/v14_0/update_opportunity_currency_fields.py
+++ b/erpnext/patches/v14_0/update_opportunity_currency_fields.py
@@ -6,9 +6,6 @@
 
 
 def execute():
-	frappe.reload_doc('crm', 'doctype', 'opportunity')
-	frappe.reload_doc('crm', 'doctype', 'opportunity_item')
-
 	opportunities = frappe.db.get_list('Opportunity', filters={
 		'opportunity_amount': ['>', 0]
 	}, fields=['name', 'company', 'currency', 'opportunity_amount'])
@@ -20,15 +17,11 @@
 		if opportunity.currency != company_currency:
 			conversion_rate = get_exchange_rate(opportunity.currency, company_currency)
 			base_opportunity_amount = flt(conversion_rate) * flt(opportunity.opportunity_amount)
-			grand_total = flt(opportunity.opportunity_amount)
-			base_grand_total = flt(conversion_rate) * flt(opportunity.opportunity_amount)
 		else:
 			conversion_rate = 1
-			base_opportunity_amount = grand_total = base_grand_total = flt(opportunity.opportunity_amount)
+			base_opportunity_amount = flt(opportunity.opportunity_amount)
 
 		frappe.db.set_value('Opportunity', opportunity.name, {
 			'conversion_rate': conversion_rate,
-			'base_opportunity_amount': base_opportunity_amount,
-			'grand_total': grand_total,
-			'base_grand_total': base_grand_total
+			'base_opportunity_amount': base_opportunity_amount
 		}, update_modified=False)
diff --git a/erpnext/patches/v4_2/repost_reserved_qty.py b/erpnext/patches/v4_2/repost_reserved_qty.py
index c2ca9be..ed4b19d 100644
--- a/erpnext/patches/v4_2/repost_reserved_qty.py
+++ b/erpnext/patches/v4_2/repost_reserved_qty.py
@@ -29,9 +29,11 @@
 	""")
 
 	for item_code, warehouse in repost_for:
-			update_bin_qty(item_code, warehouse, {
-				"reserved_qty": get_reserved_qty(item_code, warehouse)
-			})
+		if not (item_code and warehouse):
+			continue
+		update_bin_qty(item_code, warehouse, {
+			"reserved_qty": get_reserved_qty(item_code, warehouse)
+		})
 
 	frappe.db.sql("""delete from tabBin
 		where exists(
diff --git a/erpnext/patches/v4_2/update_requested_and_ordered_qty.py b/erpnext/patches/v4_2/update_requested_and_ordered_qty.py
index 42b0b04..dd79410 100644
--- a/erpnext/patches/v4_2/update_requested_and_ordered_qty.py
+++ b/erpnext/patches/v4_2/update_requested_and_ordered_qty.py
@@ -14,6 +14,8 @@
 		union
 		select item_code, warehouse from `tabStock Ledger Entry`) a"""):
 			try:
+				if not (item_code and warehouse):
+					continue
 				count += 1
 				update_bin_qty(item_code, warehouse, {
 					"indented_qty": get_indented_qty(item_code, warehouse),
diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py
index 989bcd1..8b60357 100644
--- a/erpnext/projects/doctype/timesheet/test_timesheet.py
+++ b/erpnext/projects/doctype/timesheet/test_timesheet.py
@@ -151,6 +151,35 @@
 		settings.ignore_employee_time_overlap = initial_setting
 		settings.save()
 
+	def test_timesheet_not_overlapping_with_continuous_timelogs(self):
+		emp = make_employee("test_employee_6@salary.com")
+
+		update_activity_type("_Test Activity Type")
+		timesheet = frappe.new_doc("Timesheet")
+		timesheet.employee = emp
+		timesheet.append(
+			'time_logs',
+			{
+				"billable": 1,
+				"activity_type": "_Test Activity Type",
+				"from_time": now_datetime(),
+				"to_time": now_datetime() + datetime.timedelta(hours=3),
+				"company": "_Test Company"
+			}
+		)
+		timesheet.append(
+			'time_logs',
+			{
+				"billable": 1,
+				"activity_type": "_Test Activity Type",
+				"from_time": now_datetime() + datetime.timedelta(hours=3),
+				"to_time": now_datetime() + datetime.timedelta(hours=4),
+				"company": "_Test Company"
+			}
+		)
+
+		timesheet.save() # should not throw an error
+
 	def test_to_time(self):
 		emp = make_employee("test_employee_6@salary.com")
 		from_time = now_datetime()
diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py
index dd0b5f9..b44d501 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.py
+++ b/erpnext/projects/doctype/timesheet/timesheet.py
@@ -7,7 +7,7 @@
 import frappe
 from frappe import _
 from frappe.model.document import Document
-from frappe.utils import add_to_date, flt, getdate, time_diff_in_hours
+from frappe.utils import add_to_date, flt, get_datetime, getdate, time_diff_in_hours
 
 from erpnext.controllers.queries import get_match_cond
 from erpnext.hr.utils import validate_active_employee
@@ -145,7 +145,7 @@
 		if not (data.from_time and data.hours):
 			return
 
-		_to_time = add_to_date(data.from_time, hours=data.hours, as_datetime=True)
+		_to_time = get_datetime(add_to_date(data.from_time, hours=data.hours, as_datetime=True))
 		if data.to_time != _to_time:
 			data.to_time = _to_time
 
@@ -171,39 +171,54 @@
 				.format(args.idx, self.name, existing.name), OverlapError)
 
 	def get_overlap_for(self, fieldname, args, value):
-		cond = "ts.`{0}`".format(fieldname)
-		if fieldname == 'workstation':
-			cond = "tsd.`{0}`".format(fieldname)
+		timesheet = frappe.qb.DocType("Timesheet")
+		timelog = frappe.qb.DocType("Timesheet Detail")
 
-		existing = frappe.db.sql("""select ts.name as name, tsd.from_time as from_time, tsd.to_time as to_time from
-			`tabTimesheet Detail` tsd, `tabTimesheet` ts where {0}=%(val)s and tsd.parent = ts.name and
-			(
-				(%(from_time)s > tsd.from_time and %(from_time)s < tsd.to_time) or
-				(%(to_time)s > tsd.from_time and %(to_time)s < tsd.to_time) or
-				(%(from_time)s <= tsd.from_time and %(to_time)s >= tsd.to_time))
-			and tsd.name!=%(name)s
-			and ts.name!=%(parent)s
-			and ts.docstatus < 2""".format(cond),
-			{
-				"val": value,
-				"from_time": args.from_time,
-				"to_time": args.to_time,
-				"name": args.name or "No Name",
-				"parent": args.parent or "No Name"
-			}, as_dict=True)
-		# check internal overlap
-		for time_log in self.time_logs:
-			if not (time_log.from_time and time_log.to_time
-				and args.from_time and args.to_time): continue
+		from_time = get_datetime(args.from_time)
+		to_time = get_datetime(args.to_time)
 
-			if (fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and \
-				args.idx != time_log.idx and ((args.from_time > time_log.from_time and args.from_time < time_log.to_time) or
-				(args.to_time > time_log.from_time and args.to_time < time_log.to_time) or
-				(args.from_time <= time_log.from_time and args.to_time >= time_log.to_time)):
-				return self
+		existing = (
+			frappe.qb.from_(timesheet)
+				.join(timelog)
+				.on(timelog.parent == timesheet.name)
+				.select(timesheet.name.as_('name'), timelog.from_time.as_('from_time'), timelog.to_time.as_('to_time'))
+				.where(
+					(timelog.name != (args.name or "No Name"))
+					& (timesheet.name != (args.parent or "No Name"))
+					& (timesheet.docstatus < 2)
+					& (timesheet[fieldname] == value)
+					& (
+						((from_time > timelog.from_time) & (from_time < timelog.to_time))
+						| ((to_time > timelog.from_time) & (to_time < timelog.to_time))
+						| ((from_time <= timelog.from_time) & (to_time >= timelog.to_time))
+					)
+				)
+		).run(as_dict=True)
+
+		if self.check_internal_overlap(fieldname, args):
+			return self
 
 		return existing[0] if existing else None
 
+	def check_internal_overlap(self, fieldname, args):
+		for time_log in self.time_logs:
+			if not (time_log.from_time and time_log.to_time
+				and args.from_time and args.to_time):
+				continue
+
+			from_time = get_datetime(time_log.from_time)
+			to_time = get_datetime(time_log.to_time)
+			args_from_time = get_datetime(args.from_time)
+			args_to_time = get_datetime(args.to_time)
+
+			if (args.get(fieldname) == time_log.get(fieldname)) and (args.idx != time_log.idx) and (
+				(args_from_time > from_time and args_from_time < to_time)
+				or (args_to_time > from_time and args_to_time < to_time)
+				or (args_from_time <= from_time and args_to_time >= to_time)
+			):
+				return True
+		return False
+
 	def update_cost(self):
 		for data in self.time_logs:
 			if data.activity_type or data.is_billable:
diff --git a/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json b/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json
index ee04c61..90fdb83 100644
--- a/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json
+++ b/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json
@@ -14,12 +14,6 @@
   "to_time",
   "hours",
   "completed",
-  "section_break_7",
-  "completed_qty",
-  "workstation",
-  "column_break_12",
-  "operation",
-  "operation_id",
   "project_details",
   "project",
   "project_name",
@@ -84,43 +78,6 @@
    "label": "Completed"
   },
   {
-   "fieldname": "section_break_7",
-   "fieldtype": "Section Break"
-  },
-  {
-   "depends_on": "eval:parent.work_order",
-   "fieldname": "completed_qty",
-   "fieldtype": "Float",
-   "label": "Completed Qty"
-  },
-  {
-   "depends_on": "eval:parent.work_order",
-   "fieldname": "workstation",
-   "fieldtype": "Link",
-   "label": "Workstation",
-   "options": "Workstation",
-   "read_only": 1
-  },
-  {
-   "fieldname": "column_break_12",
-   "fieldtype": "Column Break"
-  },
-  {
-   "depends_on": "eval:parent.work_order",
-   "fieldname": "operation",
-   "fieldtype": "Link",
-   "label": "Operation",
-   "options": "Operation",
-   "read_only": 1
-  },
-  {
-   "depends_on": "eval:parent.work_order",
-   "fieldname": "operation_id",
-   "fieldtype": "Data",
-   "hidden": 1,
-   "label": "Operation Id"
-  },
-  {
    "fieldname": "project_details",
    "fieldtype": "Section Break"
   },
@@ -267,7 +224,7 @@
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2021-05-18 12:19:33.205940",
+ "modified": "2022-02-17 16:53:34.878798",
  "modified_by": "Administrator",
  "module": "Projects",
  "name": "Timesheet Detail",
@@ -275,5 +232,6 @@
  "permissions": [],
  "quick_entry": 1,
  "sort_field": "modified",
- "sort_order": "ASC"
+ "sort_order": "ASC",
+ "states": []
 }
\ No newline at end of file
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index aa3e2f3..136e1ed 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -2284,20 +2284,12 @@
 
 	coupon_code() {
 		var me = this;
-		if (this.frm.doc.coupon_code) {
-			frappe.run_serially([
-				() => this.frm.doc.ignore_pricing_rule=1,
-				() => me.ignore_pricing_rule(),
-				() => this.frm.doc.ignore_pricing_rule=0,
-				() => me.apply_pricing_rule(),
-				() => this.frm.save()
-			]);
-		} else {
-			frappe.run_serially([
-				() => this.frm.doc.ignore_pricing_rule=1,
-				() => me.ignore_pricing_rule()
-			]);
-		}
+		frappe.run_serially([
+			() => this.frm.doc.ignore_pricing_rule=1,
+			() => me.ignore_pricing_rule(),
+			() => this.frm.doc.ignore_pricing_rule=0,
+			() => me.apply_pricing_rule()
+		]);
 	}
 };
 
diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py
index 15d524d..d2ef6f3 100644
--- a/erpnext/regional/saudi_arabia/setup.py
+++ b/erpnext/regional/saudi_arabia/setup.py
@@ -102,7 +102,7 @@
 		]
 	}
 
-	create_custom_fields(custom_fields, update=True)
+	create_custom_fields(custom_fields, ignore_validate=True, update=True)
 
 def update_regional_tax_settings(country, company):
 	create_ksa_vat_setting(company)
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index acf048e..73c5bd2 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -6,7 +6,7 @@
 import frappe
 import frappe.permissions
 from frappe.core.doctype.user_permission.test_user_permission import create_user
-from frappe.utils import add_days, flt, getdate, nowdate
+from frappe.utils import add_days, flt, getdate, nowdate, today
 
 from erpnext.controllers.accounts_controller import update_child_qty_rate
 from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
@@ -1399,6 +1399,48 @@
 		so.load_from_db()
 		self.assertEqual(so.billing_status, 'Fully Billed')
 
+	def test_so_back_updated_from_wo_via_mr(self):
+		"SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO."
+		from erpnext.manufacturing.doctype.work_order.work_order import (
+			make_stock_entry as make_se_from_wo,
+		)
+		from erpnext.stock.doctype.material_request.material_request import raise_work_orders
+
+		so = make_sales_order(item_list=[{"item_code": "_Test FG Item","qty": 2, "rate":100}])
+
+		mr = make_material_request(so.name)
+		mr.material_request_type = "Manufacture"
+		mr.schedule_date = today()
+		mr.submit()
+
+		# WO from MR
+		wo_name = raise_work_orders(mr.name)[0]
+		wo = frappe.get_doc("Work Order", wo_name)
+		wo.wip_warehouse = "Work In Progress - _TC"
+		wo.skip_transfer = True
+
+		self.assertEqual(wo.sales_order, so.name)
+		self.assertEqual(wo.sales_order_item, so.items[0].name)
+
+		wo.submit()
+		make_stock_entry(item_code="_Test Item", # Stock RM
+			target="Work In Progress - _TC",
+			qty=4, basic_rate=100
+		)
+		make_stock_entry(item_code="_Test Item Home Desktop 100", # Stock RM
+			target="Work In Progress - _TC",
+			qty=4, basic_rate=100
+		)
+
+		se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 2))
+		se.submit() # Finish WO
+
+		mr.reload()
+		wo.reload()
+		so.reload()
+		self.assertEqual(so.items[0].work_order_qty, wo.produced_qty)
+		self.assertEqual(mr.status, "Manufactured")
+
 def automatically_fetch_payment_terms(enable=1):
 	accounts_settings = frappe.get_doc("Accounts Settings")
 	accounts_settings.automatically_fetch_payment_terms = enable
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index b9b6559..9650bc8 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -169,6 +169,29 @@
       }
     });
 
+		frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => {
+			if (!frm.doc.ignore_pricing_rule) {
+				if (frm.doc.coupon_code) {
+					frappe.run_serially([
+						() => frm.doc.ignore_pricing_rule=1,
+						() => frm.trigger('ignore_pricing_rule'),
+						() => frm.doc.ignore_pricing_rule=0,
+						() => frm.trigger('apply_pricing_rule'),
+						() => frm.save(),
+						() => this.update_totals_section(frm.doc)
+					]);
+				} else {
+					frappe.run_serially([
+						() => frm.doc.ignore_pricing_rule=1,
+						() => frm.trigger('ignore_pricing_rule'),
+						() => frm.doc.ignore_pricing_rule=0,
+						() => frm.save(),
+						() => this.update_totals_section(frm.doc)
+					]);
+				}
+			}
+		});
+
 		this.setup_listener_for_payments();
 
 		this.$payment_modes.on('click', '.shortcut', function() {
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/__init__.py b/erpnext/selling/report/payment_terms_status_for_sales_order/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/__init__.py
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js
new file mode 100644
index 0000000..0e36b3f
--- /dev/null
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js
@@ -0,0 +1,84 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+function get_filters() {
+	let filters = [
+		{
+			"fieldname":"company",
+			"label": __("Company"),
+			"fieldtype": "Link",
+			"options": "Company",
+			"default": frappe.defaults.get_user_default("Company"),
+			"reqd": 1
+		},
+		{
+			"fieldname":"period_start_date",
+			"label": __("Start Date"),
+			"fieldtype": "Date",
+			"reqd": 1,
+			"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1)
+		},
+		{
+			"fieldname":"period_end_date",
+			"label": __("End Date"),
+			"fieldtype": "Date",
+			"reqd": 1,
+			"default": frappe.datetime.get_today()
+		},
+		{
+			"fieldname":"sales_order",
+			"label": __("Sales Order"),
+			"fieldtype": "MultiSelectList",
+			"width": 100,
+			"options": "Sales Order",
+			"get_data": function(txt) {
+				return frappe.db.get_link_options("Sales Order", txt, this.filters());
+			},
+			"filters": () => {
+				return {
+					docstatus: 1,
+					payment_terms_template: ['not in', ['']],
+					company: frappe.query_report.get_filter_value("company"),
+					transaction_date: ['between', [frappe.query_report.get_filter_value("period_start_date"), frappe.query_report.get_filter_value("period_end_date")]]
+				}
+			},
+			on_change: function(){
+				frappe.query_report.refresh();
+			}
+		}
+	]
+
+	return filters;
+}
+
+frappe.query_reports["Payment Terms Status for Sales Order"] = {
+	"filters": get_filters(),
+	"formatter": function(value, row, column, data, default_formatter){
+		if(column.fieldname == 'invoices' && value) {
+			invoices = value.split(',');
+			const invoice_formatter = (prev_value, curr_value) => {
+				if(prev_value != "") {
+					return prev_value + ", " + default_formatter(curr_value, row, column, data);
+				}
+				else {
+					return default_formatter(curr_value, row, column, data);
+				}
+			}
+			return invoices.reduce(invoice_formatter, "")
+		}
+		else if (column.fieldname == 'paid_amount' && value){
+			formatted_value = default_formatter(value, row, column, data);
+			if(value > 0) {
+				formatted_value = "<span style='color:green;'>" + formatted_value + "</span>"
+			}
+			return formatted_value;
+		}
+		else if (column.fieldname == 'status' && value == 'Completed'){
+			return "<span style='color:green;'>" + default_formatter(value, row, column, data) + "</span>";
+		}
+
+		return default_formatter(value, row, column, data);
+	},
+
+};
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json
new file mode 100644
index 0000000..850fa4d
--- /dev/null
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json
@@ -0,0 +1,38 @@
+{
+ "add_total_row": 1,
+ "columns": [],
+ "creation": "2021-12-28 10:39:34.533964",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-12-30 10:42:06.058457",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "Payment Terms Status for Sales Order",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Sales Order",
+ "report_name": "Payment Terms Status for Sales Order",
+ "report_type": "Script Report",
+ "roles": [
+  {
+   "role": "Sales User"
+  },
+  {
+   "role": "Sales Manager"
+  },
+  {
+   "role": "Maintenance User"
+  },
+  {
+   "role": "Accounts User"
+  },
+  {
+   "role": "Stock User"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
new file mode 100644
index 0000000..e6a56ee
--- /dev/null
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
@@ -0,0 +1,205 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# License: MIT. See LICENSE
+
+import frappe
+from frappe import _, qb, query_builder
+from frappe.query_builder import functions
+
+
+def get_columns():
+	columns = [
+		{
+			"label": _("Sales Order"),
+			"fieldname": "name",
+			"fieldtype": "Link",
+			"options": "Sales Order",
+		},
+		{
+			"label": _("Posting Date"),
+			"fieldname": "submitted",
+			"fieldtype": "Date",
+		},
+		{
+			"label": _("Payment Term"),
+			"fieldname": "payment_term",
+			"fieldtype": "Data",
+		},
+		{
+			"label": _("Description"),
+			"fieldname": "description",
+			"fieldtype": "Data",
+		},
+		{
+			"label": _("Due Date"),
+			"fieldname": "due_date",
+			"fieldtype": "Date",
+		},
+		{
+			"label": _("Invoice Portion"),
+			"fieldname": "invoice_portion",
+			"fieldtype": "Percent",
+		},
+		{
+			"label": _("Payment Amount"),
+			"fieldname": "base_payment_amount",
+			"fieldtype": "Currency",
+			"options": "currency",
+		},
+		{
+			"label": _("Paid Amount"),
+			"fieldname": "paid_amount",
+			"fieldtype": "Currency",
+			"options": "currency",
+		},
+		{
+			"label": _("Invoices"),
+			"fieldname": "invoices",
+			"fieldtype": "Link",
+			"options": "Sales Invoice",
+		},
+		{
+			"label": _("Status"),
+			"fieldname": "status",
+			"fieldtype": "Data",
+		},
+		{
+			"label": _("Currency"),
+			"fieldname": "currency",
+			"fieldtype": "Currency",
+			"hidden": 1
+		}
+	]
+	return columns
+
+
+def get_conditions(filters):
+	"""
+	Convert filter options to conditions used in query
+	"""
+	filters = frappe._dict(filters) if filters else frappe._dict({})
+	conditions = frappe._dict({})
+
+	conditions.company = filters.company or frappe.defaults.get_user_default("company")
+	conditions.end_date = filters.period_end_date or frappe.utils.today()
+	conditions.start_date = filters.period_start_date or frappe.utils.add_months(
+		conditions.end_date, -1
+	)
+	conditions.sales_order = filters.sales_order or []
+
+	return conditions
+
+
+def get_so_with_invoices(filters):
+	"""
+	Get Sales Order with payment terms template with their associated Invoices
+	"""
+	sorders = []
+
+	so = qb.DocType("Sales Order")
+	ps = qb.DocType("Payment Schedule")
+	datediff = query_builder.CustomFunction("DATEDIFF", ["cur_date", "due_date"])
+	ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"])
+
+	conditions = get_conditions(filters)
+	query_so = (
+		qb.from_(so)
+		.join(ps)
+		.on(ps.parent == so.name)
+		.select(
+			so.name,
+			so.transaction_date.as_("submitted"),
+			ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"),
+			ps.payment_term,
+			ps.description,
+			ps.due_date,
+			ps.invoice_portion,
+			ps.base_payment_amount,
+			ps.paid_amount,
+		)
+		.where(
+			(so.docstatus == 1)
+			& (so.payment_terms_template != "NULL")
+			& (so.company == conditions.company)
+			& (so.transaction_date[conditions.start_date : conditions.end_date])
+		)
+		.orderby(so.name, so.transaction_date, ps.due_date)
+	)
+
+	if conditions.sales_order != []:
+		query_so = query_so.where(so.name.isin(conditions.sales_order))
+
+	sorders = query_so.run(as_dict=True)
+
+	invoices = []
+	if sorders != []:
+		soi = qb.DocType("Sales Order Item")
+		si = qb.DocType("Sales Invoice")
+		sii = qb.DocType("Sales Invoice Item")
+		query_inv = (
+			qb.from_(sii)
+			.right_join(si)
+			.on(si.name == sii.parent)
+			.inner_join(soi)
+			.on(soi.name == sii.so_detail)
+			.select(sii.sales_order, sii.parent.as_("invoice"), si.base_grand_total.as_("invoice_amount"))
+			.where((sii.sales_order.isin([x.name for x in sorders])) & (si.docstatus == 1))
+			.groupby(sii.parent)
+		)
+		invoices = query_inv.run(as_dict=True)
+
+	return sorders, invoices
+
+
+def set_payment_terms_statuses(sales_orders, invoices, filters):
+	"""
+	compute status for payment terms with associated sales invoice using FIFO
+	"""
+
+	for so in sales_orders:
+		so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency')
+		so.invoices = ""
+		for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]:
+			if so.base_payment_amount - so.paid_amount > 0:
+				amount = so.base_payment_amount - so.paid_amount
+				if inv.invoice_amount >= amount:
+					inv.invoice_amount -= amount
+					so.paid_amount += amount
+					so.invoices += "," + inv.invoice
+					so.status = "Completed"
+					break
+				else:
+					so.paid_amount += inv.invoice_amount
+					inv.invoice_amount = 0
+					so.invoices += "," + inv.invoice
+					so.status = "Partly Paid"
+
+	return sales_orders, invoices
+
+
+def prepare_chart(s_orders):
+	if len(set([x.name for x in s_orders])) == 1:
+		chart = {
+			"data": {
+				"labels": [term.payment_term for term in s_orders],
+				"datasets": [
+					{"name": "Payment Amount", "values": [x.base_payment_amount for x in s_orders],},
+					{"name": "Paid Amount", "values": [x.paid_amount for x in s_orders],},
+				],
+			},
+			"type": "bar",
+		}
+		return chart
+
+
+def execute(filters=None):
+	columns = get_columns()
+	sales_orders, so_invoices = get_so_with_invoices(filters)
+	sales_orders, so_invoices = set_payment_terms_statuses(sales_orders, so_invoices, filters)
+
+	prepare_chart(sales_orders)
+
+	data = sales_orders
+	message = []
+	chart = prepare_chart(sales_orders)
+
+	return columns, data, message, chart
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
new file mode 100644
index 0000000..cad41e1
--- /dev/null
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
@@ -0,0 +1,198 @@
+import datetime
+
+import frappe
+from frappe.utils import add_days
+
+from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
+from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
+from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order import (
+	execute,
+)
+from erpnext.stock.doctype.item.test_item import create_item
+from erpnext.tests.utils import ERPNextTestCase
+
+test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template"]
+
+
+class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase):
+	def create_payment_terms_template(self):
+		# create template for 50-50 payments
+		template = None
+		if frappe.db.exists("Payment Terms Template", "_Test 50-50"):
+			template = frappe.get_doc("Payment Terms Template", "_Test 50-50")
+		else:
+			template = frappe.get_doc(
+				{
+					"doctype": "Payment Terms Template",
+					"template_name": "_Test 50-50",
+					"terms": [
+						{
+							"doctype": "Payment Terms Template Detail",
+							"due_date_based_on": "Day(s) after invoice date",
+							"payment_term_name": "_Test 50% on 15 Days",
+							"description": "_Test 50-50",
+							"invoice_portion": 50,
+							"credit_days": 15,
+						},
+						{
+							"doctype": "Payment Terms Template Detail",
+							"due_date_based_on": "Day(s) after invoice date",
+							"payment_term_name": "_Test 50% on 30 Days",
+							"description": "_Test 50-50",
+							"invoice_portion": 50,
+							"credit_days": 30,
+						},
+					],
+				}
+			)
+			template.insert()
+		self.template = template
+
+	def test_payment_terms_status(self):
+		self.create_payment_terms_template()
+		item = create_item(item_code="_Test Excavator", is_stock_item=0)
+		so = make_sales_order(
+			transaction_date="2021-06-15",
+			delivery_date=add_days("2021-06-15", -30),
+			item=item.item_code,
+			qty=10,
+			rate=100000,
+			do_not_save=True,
+		)
+		so.po_no = ""
+		so.taxes_and_charges = ""
+		so.taxes = ""
+		so.payment_terms_template = self.template.name
+		so.save()
+		so.submit()
+
+		# make invoice with 60% of the total sales order value
+		sinv = make_sales_invoice(so.name)
+		sinv.taxes_and_charges = ""
+		sinv.taxes = ""
+		sinv.items[0].qty = 6
+		sinv.insert()
+		sinv.submit()
+		columns, data, message, chart = execute(
+			{
+				"company": "_Test Company",
+				"period_start_date": "2021-06-01",
+				"period_end_date": "2021-06-30",
+				"sales_order": [so.name],
+			}
+		)
+
+		expected_value = [
+			{
+				"name": so.name,
+				"submitted": datetime.date(2021, 6, 15),
+				"status": "Completed",
+				"payment_term": None,
+				"description": "_Test 50-50",
+				"due_date": datetime.date(2021, 6, 30),
+				"invoice_portion": 50.0,
+				"currency": "INR",
+				"base_payment_amount": 500000.0,
+				"paid_amount": 500000.0,
+				"invoices": ","+sinv.name,
+			},
+			{
+				"name": so.name,
+				"submitted": datetime.date(2021, 6, 15),
+				"status": "Partly Paid",
+				"payment_term": None,
+				"description": "_Test 50-50",
+				"due_date": datetime.date(2021, 7, 15),
+				"invoice_portion": 50.0,
+				"currency": "INR",
+				"base_payment_amount": 500000.0,
+				"paid_amount": 100000.0,
+				"invoices": ","+sinv.name,
+			},
+		]
+		self.assertEqual(data, expected_value)
+
+	def create_exchange_rate(self, date):
+		# make an entry in Currency Exchange list. serves as a static exchange rate
+		if frappe.db.exists({'doctype': "Currency Exchange",'date': date,'from_currency': 'USD', 'to_currency':'INR'}):
+			return
+		else:
+			doc = frappe.get_doc({
+				'doctype': "Currency Exchange",
+				'date': date,
+				'from_currency': 'USD',
+				'to_currency': frappe.get_cached_value("Company", '_Test Company','default_currency'),
+				'exchange_rate': 70,
+				'for_buying': True,
+				'for_selling': True
+			})
+			doc.insert()
+
+	def test_alternate_currency(self):
+		transaction_date = "2021-06-15"
+		self.create_payment_terms_template()
+		self.create_exchange_rate(transaction_date)
+		item = create_item(item_code="_Test Excavator", is_stock_item=0)
+		so = make_sales_order(
+			transaction_date=transaction_date,
+			currency="USD",
+			delivery_date=add_days(transaction_date, -30),
+			item=item.item_code,
+			qty=10,
+			rate=10000,
+			do_not_save=True,
+		)
+		so.po_no = ""
+		so.taxes_and_charges = ""
+		so.taxes = ""
+		so.payment_terms_template = self.template.name
+		so.save()
+		so.submit()
+
+		# make invoice with 60% of the total sales order value
+		sinv = make_sales_invoice(so.name)
+		sinv.currency = "USD"
+		sinv.taxes_and_charges = ""
+		sinv.taxes = ""
+		sinv.items[0].qty = 6
+		sinv.insert()
+		sinv.submit()
+		columns, data, message, chart = execute(
+			{
+				"company": "_Test Company",
+				"period_start_date": "2021-06-01",
+				"period_end_date": "2021-06-30",
+				"sales_order": [so.name],
+			}
+		)
+
+		# report defaults to company currency.
+		expected_value = [
+			{
+				"name": so.name,
+				"submitted": datetime.date(2021, 6, 15),
+				"status": "Completed",
+				"payment_term": None,
+				"description": "_Test 50-50",
+				"due_date": datetime.date(2021, 6, 30),
+				"invoice_portion": 50.0,
+				"currency": frappe.get_cached_value("Company", '_Test Company','default_currency'),
+				"base_payment_amount": 3500000.0,
+				"paid_amount": 3500000.0,
+				"invoices": ","+sinv.name,
+			},
+			{
+				"name": so.name,
+				"submitted": datetime.date(2021, 6, 15),
+				"status": "Partly Paid",
+				"payment_term": None,
+				"description": "_Test 50-50",
+				"due_date": datetime.date(2021, 7, 15),
+				"invoice_portion": 50.0,
+				"currency": frappe.get_cached_value("Company", '_Test Company','default_currency'),
+				"base_payment_amount": 3500000.0,
+				"paid_amount": 700000.0,
+				"invoices": ","+sinv.name,
+			},
+		]
+		self.assertEqual(data, expected_value)
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 2a30ca1..dfc0918 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -545,7 +545,7 @@
 			let selected_attributes = {};
 			me.multiple_variant_dialog.$wrapper.find('.form-column').each((i, col) => {
 				if(i===0) return;
-				let attribute_name = $(col).find('label').html();
+				let attribute_name = $(col).find('label').html().trim();
 				selected_attributes[attribute_name] = [];
 				let checked_opts = $(col).find('.checkbox input');
 				checked_opts.each((i, opt) => {
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 103e8d6..b39328f 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -533,6 +533,7 @@
 					"stock_uom": d.stock_uom,
 					"expected_delivery_date": d.schedule_date,
 					"sales_order": d.sales_order,
+					"sales_order_item": d.get("sales_order_item"),
 					"bom_no": get_item_details(d.item_code).bom_no,
 					"material_request": mr.name,
 					"material_request_item": d.name,
diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py
index 523ba12..4e472a9 100644
--- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py
+++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py
@@ -9,7 +9,7 @@
 import frappe
 from frappe import _
 from frappe.model.document import Document
-from frappe.utils import cint, floor, flt, nowdate
+from frappe.utils import cint, cstr, floor, flt, nowdate
 
 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
 from erpnext.stock.utils import get_stock_balance
@@ -142,11 +142,44 @@
 	if items_not_accomodated:
 		show_unassigned_items_message(items_not_accomodated)
 
-	items[:] = updated_table if updated_table else items # modify items table
+	if updated_table and _items_changed(items, updated_table, doctype):
+		items[:] = updated_table
+		frappe.msgprint(_("Applied putaway rules."), alert=True)
 
 	if sync and json.loads(sync): # sync with client side
 		return items
 
+def _items_changed(old, new, doctype: str) -> bool:
+	""" Check if any items changed by application of putaway rules.
+
+		If not, changing item table can have side effects since `name` items also changes.
+	"""
+	if len(old) != len(new):
+		return True
+
+	old = [frappe._dict(item) if isinstance(item, dict) else item for item in old]
+
+	if doctype == "Stock Entry":
+		compare_keys = ("item_code", "t_warehouse", "transfer_qty", "serial_no")
+		sort_key = lambda item: (item.item_code, cstr(item.t_warehouse),  # noqa
+				flt(item.transfer_qty), cstr(item.serial_no))
+	else:
+		# purchase receipt / invoice
+		compare_keys = ("item_code", "warehouse", "stock_qty", "received_qty", "serial_no")
+		sort_key = lambda item: (item.item_code, cstr(item.warehouse),  # noqa
+				flt(item.stock_qty), flt(item.received_qty), cstr(item.serial_no))
+
+	old_sorted = sorted(old, key=sort_key)
+	new_sorted = sorted(new, key=sort_key)
+
+	# Once sorted by all relevant keys both tables should align if they are same.
+	for old_item, new_item in zip(old_sorted, new_sorted):
+		for key in compare_keys:
+			if old_item.get(key) != new_item.get(key):
+				return True
+	return False
+
+
 def get_ordered_putaway_rules(item_code, company, source_warehouse=None):
 	"""Returns an ordered list of putaway rules to apply on an item."""
 	filters = {
diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
index bd4d811..ff1c19a 100644
--- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
+++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
@@ -35,6 +35,18 @@
 			new_uom.uom_name = "Bag"
 			new_uom.save()
 
+	def assertUnchangedItemsOnResave(self, doc):
+		""" Check if same items remain even after reapplication of rules.
+
+			This is required since some business logic like subcontracting
+			depends on `name` of items to be same if item isn't changed.
+		"""
+		doc.reload()
+		old_items = {d.name for d in doc.items}
+		doc.save()
+		new_items = {d.name for d in doc.items}
+		self.assertSetEqual(old_items, new_items)
+
 	def test_putaway_rules_priority(self):
 		"""Test if rule is applied by priority, irrespective of free space."""
 		rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200,
@@ -50,6 +62,8 @@
 		self.assertEqual(pr.items[1].qty, 100)
 		self.assertEqual(pr.items[1].warehouse, self.warehouse_2)
 
+		self.assertUnchangedItemsOnResave(pr)
+
 		pr.delete()
 		rule_1.delete()
 		rule_2.delete()
@@ -162,6 +176,8 @@
 		# leftover space was for 500 kg (0.5 Bag)
 		# Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned
 
+		self.assertUnchangedItemsOnResave(pr)
+
 		pr.delete()
 		rule_1.delete()
 		rule_2.delete()
@@ -196,6 +212,8 @@
 		self.assertEqual(pr.items[1].warehouse, self.warehouse_1)
 		self.assertEqual(pr.items[1].putaway_rule, rule_1.name)
 
+		self.assertUnchangedItemsOnResave(pr)
+
 		pr.delete()
 		rule_1.delete()
 
@@ -239,6 +257,8 @@
 		self.assertEqual(stock_entry_item.qty, 100) # unassigned 100 out of 200 Kg
 		self.assertEqual(stock_entry_item.putaway_rule, rule_2.name)
 
+		self.assertUnchangedItemsOnResave(stock_entry)
+
 		stock_entry.delete()
 		rule_1.delete()
 		rule_2.delete()
@@ -294,6 +314,8 @@
 		self.assertEqual(stock_entry.items[2].qty, 200)
 		self.assertEqual(stock_entry.items[2].putaway_rule, rule_2.name)
 
+		self.assertUnchangedItemsOnResave(stock_entry)
+
 		stock_entry.delete()
 		rule_1.delete()
 		rule_2.delete()
@@ -344,6 +366,8 @@
 		self.assertEqual(stock_entry.items[1].serial_no, "\n".join(serial_nos[3:]))
 		self.assertEqual(stock_entry.items[1].batch_no, "BOTTL-BATCH-1")
 
+		self.assertUnchangedItemsOnResave(stock_entry)
+
 		stock_entry.delete()
 		pr.cancel()
 		rule_1.delete()
@@ -366,6 +390,8 @@
 		self.assertEqual(stock_entry_item.qty, 100)
 		self.assertEqual(stock_entry_item.putaway_rule, rule_1.name)
 
+		self.assertUnchangedItemsOnResave(stock_entry)
+
 		stock_entry.delete()
 		rule_1.delete()
 		rule_2.delete()