Merge pull request #29137 from ruthra-kumar/payment_terms_report

feat: Payment Terms Status report
diff --git a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py
index 49e3351..7908c35 100644
--- a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py
+++ b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py
@@ -49,7 +49,7 @@
 				"min_grade":0.0,"name":"Very Poor",
 				"prevent_rfqs":1,
 				"notify_supplier":0,
-				"doctype":"Supplier Scorecard Standing",
+				"doctype":"Supplier Scorecard Scoring Standing",
 				"max_grade":30.0,
 				"prevent_pos":1,
 				"warn_pos":0,
@@ -65,7 +65,7 @@
 				"name":"Poor",
 				"prevent_rfqs":1,
 				"notify_supplier":0,
-				"doctype":"Supplier Scorecard Standing",
+				"doctype":"Supplier Scorecard Scoring Standing",
 				"max_grade":50.0,
 				"prevent_pos":0,
 				"warn_pos":0,
@@ -81,7 +81,7 @@
 				"name":"Average",
 				"prevent_rfqs":0,
 				"notify_supplier":0,
-				"doctype":"Supplier Scorecard Standing",
+				"doctype":"Supplier Scorecard Scoring Standing",
 				"max_grade":80.0,
 				"prevent_pos":0,
 				"warn_pos":0,
@@ -97,7 +97,7 @@
 				"name":"Excellent",
 				"prevent_rfqs":0,
 				"notify_supplier":0,
-				"doctype":"Supplier Scorecard Standing",
+				"doctype":"Supplier Scorecard Scoring Standing",
 				"max_grade":100.0,
 				"prevent_pos":0,
 				"warn_pos":0,
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/patches.txt b/erpnext/patches.txt
index d104bc0..c26451a 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -352,3 +352,4 @@
 erpnext.patches.v13_0.update_disbursement_account
 erpnext.patches.v13_0.update_reserved_qty_closed_wo
 erpnext.patches.v14_0.delete_amazon_mws_doctype
+erpnext.patches.v14_0.set_work_order_qty_in_so_from_mr
diff --git a/erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py b/erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py
new file mode 100644
index 0000000..f097ab9
--- /dev/null
+++ b/erpnext/patches/v14_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/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index f83053e..daa0f89 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -6,6 +6,7 @@
 import unittest
 
 import frappe
+from frappe.model.document import Document
 from frappe.utils import (
 	add_days,
 	add_months,
@@ -687,20 +688,25 @@
 
 def make_salary_component(salary_components, test_tax, company_list=None):
 	for salary_component in salary_components:
-		if not frappe.db.exists('Salary Component', salary_component["salary_component"]):
-			if test_tax:
-				if salary_component["type"] == "Earning":
-					salary_component["is_tax_applicable"] = 1
-				elif salary_component["salary_component"] == "TDS":
-					salary_component["variable_based_on_taxable_salary"] = 1
-					salary_component["amount_based_on_formula"] = 0
-					salary_component["amount"] = 0
-					salary_component["formula"] = ""
-					salary_component["condition"] = ""
-			salary_component["doctype"] = "Salary Component"
-			salary_component["salary_component_abbr"] = salary_component["abbr"]
-			frappe.get_doc(salary_component).insert()
-		get_salary_component_account(salary_component["salary_component"], company_list)
+		if frappe.db.exists('Salary Component', salary_component["salary_component"]):
+			continue
+
+		if test_tax:
+			if salary_component["type"] == "Earning":
+				salary_component["is_tax_applicable"] = 1
+			elif salary_component["salary_component"] == "TDS":
+				salary_component["variable_based_on_taxable_salary"] = 1
+				salary_component["amount_based_on_formula"] = 0
+				salary_component["amount"] = 0
+				salary_component["formula"] = ""
+				salary_component["condition"] = ""
+
+		salary_component["salary_component_abbr"] = salary_component["abbr"]
+		doc = frappe.new_doc("Salary Component")
+		doc.update(salary_component)
+		doc.insert()
+
+		get_salary_component_account(doc, company_list)
 
 def get_salary_component_account(sal_comp, company_list=None):
 	company = erpnext.get_default_company()
@@ -708,7 +714,9 @@
 	if company_list and company not in company_list:
 		company_list.append(company)
 
-	sal_comp = frappe.get_doc("Salary Component", sal_comp)
+	if not isinstance(sal_comp, Document):
+		sal_comp = frappe.get_doc("Salary Component", sal_comp)
+
 	if not sal_comp.get("accounts"):
 		for d in company_list:
 			company_abbr = frappe.get_cached_value('Company', d, 'abbr')
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/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,