Merge pull request #26415 from GangaManoj/backport-asset-repair-refactor

refactor: Asset Repair
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 4e93fc6..0ba8507 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -717,9 +717,8 @@
 			"ignore_conversion_rate": True
 		})
 		item_doc = frappe.get_cached_doc("Item", args.get("item_code"))
-		out = frappe._dict()
-		get_price_list_rate(bom_args, item_doc, out)
-		rate = out.price_list_rate
+		price_list_data = get_price_list_rate(bom_args, item_doc)
+		rate = price_list_data.price_list_rate
 
 	return rate
 
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 69c7f5c..66e2394 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -608,6 +608,11 @@
 		target.set_missing_values()
 		target.set_stock_entry_type()
 
+		wo_allows_alternate_item = frappe.db.get_value("Work Order", target.work_order, "allow_alternative_item")
+		for item in target.items:
+			item.allow_alternative_item = int(wo_allows_alternate_item and
+					frappe.get_cached_value("Item", item.item_code, "allow_alternative_item"))
+
 	doclist = get_mapped_doc("Job Card", source_name, {
 		"Job Card": {
 			"doctype": "Stock Entry",
@@ -698,4 +703,4 @@
 		}
 	}, target_doc, set_missing_values)
 
-	return doclist
\ No newline at end of file
+	return doclist
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 6a024f2..8c27d6c 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -109,6 +109,15 @@
 		so_mr_list = [d.get(field) for d in self.get(table) if d.get(field)]
 		return so_mr_list
 
+	def get_bom_item(self):
+		"""Check if Item or if its Template has a BOM."""
+		bom_item = None
+		has_bom = frappe.db.exists({'doctype': 'BOM', 'item': self.item_code, 'docstatus': 1})
+		if not has_bom:
+			template_item = frappe.db.get_value('Item', self.item_code, ['variant_of'])
+			bom_item = "bom.item = {0}".format(frappe.db.escape(template_item)) if template_item else bom_item
+		return bom_item
+
 	def get_so_items(self):
 		# Check for empty table or empty rows
 		if not self.get("sales_orders") or not self.get_so_mr_list("sales_order", "sales_orders"):
@@ -117,16 +126,26 @@
 		so_list = self.get_so_mr_list("sales_order", "sales_orders")
 
 		item_condition = ""
-		if self.item_code:
+		bom_item = "bom.item = so_item.item_code"
+		if self.item_code and frappe.db.exists('Item', self.item_code):
+			bom_item = self.get_bom_item() or bom_item
 			item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code))
 
-		items = frappe.db.sql("""select distinct parent, item_code, warehouse,
-			(qty - work_order_qty) * conversion_factor as pending_qty, description, name
-			from `tabSales Order Item` so_item
-			where parent in (%s) and docstatus = 1 and qty > work_order_qty
-			and exists (select name from `tabBOM` bom where bom.item=so_item.item_code
-					and bom.is_active = 1) %s""" % \
-			(", ".join(["%s"] * len(so_list)), item_condition), tuple(so_list), as_dict=1)
+		items = frappe.db.sql("""
+			select
+				distinct parent, item_code, warehouse,
+				(qty - work_order_qty) * conversion_factor as pending_qty,
+				description, name
+			from
+				`tabSales Order Item` so_item
+			where
+				parent in (%s) and docstatus = 1 and qty > work_order_qty
+				and exists (select name from `tabBOM` bom where %s
+				and bom.is_active = 1) %s""" %
+			(", ".join(["%s"] * len(so_list)),
+			bom_item,
+			item_condition),
+			tuple(so_list), as_dict=1)
 
 		if self.item_code:
 			item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code))
@@ -683,6 +702,7 @@
 
 def get_sales_orders(self):
 	so_filter = item_filter = ""
+	bom_item = "bom.item = so_item.item_code"
 	if self.from_date:
 		so_filter += " and so.transaction_date >= %(from_date)s"
 	if self.to_date:
@@ -694,7 +714,8 @@
 	if self.sales_order_status:
 		so_filter += "and so.status = %(sales_order_status)s"
 
-	if self.item_code:
+	if self.item_code and frappe.db.exists('Item', self.item_code):
+		bom_item = self.get_bom_item() or bom_item
 		item_filter += " and so_item.item_code = %(item)s"
 
 	open_so = frappe.db.sql("""
@@ -704,13 +725,13 @@
 			and so.docstatus = 1 and so.status not in ("Stopped", "Closed")
 			and so.company = %(company)s
 			and so_item.qty > so_item.work_order_qty {0} {1}
-			and (exists (select name from `tabBOM` bom where bom.item=so_item.item_code
+			and (exists (select name from `tabBOM` bom where {2}
 					and bom.is_active = 1)
 				or exists (select name from `tabPacked Item` pi
 					where pi.parent = so.name and pi.parent_item = so_item.item_code
 						and exists (select name from `tabBOM` bom where bom.item=pi.item_code
 							and bom.is_active = 1)))
-		""".format(so_filter, item_filter), {
+		""".format(so_filter, item_filter, bom_item), {
 			"from_date": self.from_date,
 			"to_date": self.to_date,
 			"customer": self.customer,
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 93e6d7a..af8de8e 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -11,6 +11,7 @@
 from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
 from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
 from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests, get_warehouse_list
+from erpnext.controllers.item_variant import create_variant
 
 class TestProductionPlan(unittest.TestCase):
 	def setUp(self):
@@ -271,6 +272,60 @@
 
 		self.assertEqual(warehouses, expected_warehouses)
 
+	def test_get_sales_order_with_variant(self):
+		if not frappe.db.exists('Item', {"item_code": 'PIV'}):
+			item = create_item('PIV', valuation_rate = 100)
+			variant_settings = {
+				"attributes": [
+					{
+						"attribute": "Colour"
+					},
+				],
+				"has_variants": 1
+			}
+			item.update(variant_settings)
+			item.save()
+			parent_bom = make_bom(item = 'PIV', raw_materials = ['PIV'])
+		if not frappe.db.exists('BOM', {"item": 'PIV'}):
+			parent_bom = make_bom(item = 'PIV', raw_materials = ['PIV'])
+		else:
+			parent_bom = frappe.get_doc('BOM', {"item": 'PIV'})
+
+		if not frappe.db.exists('Item', {"item_code": 'PIV-RED'}):
+			variant = create_variant("PIV", {"Colour": "Red"})
+			variant.save()
+			variant_bom = make_bom(item = variant.item_code, raw_materials = [variant.item_code])
+		else:
+			variant = frappe.get_doc('Item', 'PIV-RED')
+		if not frappe.db.exists('BOM', {"item": 'PIV-RED'}):
+			variant_bom = make_bom(item = variant.item_code, raw_materials = [variant.item_code])
+
+		"""Testing when item variant has a BOM"""
+		so = make_sales_order(item_code="PIV-RED", qty=5)
+		pln = frappe.new_doc('Production Plan')
+		pln.company = so.company
+		pln.get_items_from = 'Sales Order'
+		pln.item_code = 'PIV-RED'
+		pln.get_open_sales_orders()
+		self.assertEqual(pln.sales_orders[0].sales_order, so.name)
+		pln.get_so_items()
+		self.assertEqual(pln.po_items[0].item_code, 'PIV-RED')
+		self.assertEqual(pln.po_items[0].bom_no, variant_bom.name)
+		so.cancel()
+		frappe.delete_doc('Sales Order', so.name)
+		variant_bom.cancel()
+		frappe.delete_doc('BOM', variant_bom.name)
+
+		"""Testing when item variant doesn't have a BOM"""
+		so = make_sales_order(item_code="PIV-RED", qty=5)
+		pln.get_open_sales_orders()
+		self.assertEqual(pln.sales_orders[0].sales_order, so.name)
+		pln.po_items = []
+		pln.get_so_items()
+		self.assertEqual(pln.po_items[0].item_code, 'PIV-RED')
+		self.assertEqual(pln.po_items[0].bom_no, parent_bom.name)
+
+		frappe.db.rollback()
 
 def create_production_plan(**args):
 	args = frappe._dict(args)
diff --git a/erpnext/regional/address_template/templates/france.html b/erpnext/regional/address_template/templates/france.html
new file mode 100644
index 0000000..752331e
--- /dev/null
+++ b/erpnext/regional/address_template/templates/france.html
@@ -0,0 +1,5 @@
+{% if address_line1 %}{{ address_line1 }}{% endif -%}
+{% if address_line2 %}<br>{{ address_line2 }}{% endif -%}
+{% if pincode %}<br>{{ pincode }}{% endif -%}
+{% if city %} {{ city }}{% endif -%}
+{% if country %}<br>{{ country }}{% endif -%}
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index cbd272d..a85a022 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -269,11 +269,14 @@
 		batch2 = create_batch('_Test Batch Price Item', 300, 1)
 		batch3 = create_batch('_Test Batch Price Item', 400, 0)
 
+		company = "_Test Company with perpetual inventory"
+		currency = frappe.get_cached_value("Company",  company,  "default_currency")
+
 		args = frappe._dict({
 			"item_code": "_Test Batch Price Item",
-			"company": "_Test Company with perpetual inventory",
+			"company": company,
 			"price_list": "_Test Price List",
-			"currency": "_Test Currency",
+			"currency": currency,
 			"doctype": "Sales Invoice",
 			"conversion_rate": 1,
 			"price_list_currency": "_Test Currency",
@@ -333,4 +336,4 @@
 	except frappe.DuplicateEntryError:
 		batch = frappe.get_doc("Batch", args.batch_id)
 
-	return batch
\ No newline at end of file
+	return batch
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index c7467a5..9ec44d2 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -83,14 +83,17 @@
 
 		make_test_objects("Item Price")
 
+		company = "_Test Company"
+		currency = frappe.get_cached_value("Company",  company,  "default_currency")
+
 		details = get_item_details({
 			"item_code": "_Test Item",
-			"company": "_Test Company",
+			"company": company,
 			"price_list": "_Test Price List",
-			"currency": "_Test Currency",
+			"currency": currency,
 			"doctype": "Sales Order",
 			"conversion_rate": 1,
-			"price_list_currency": "_Test Currency",
+			"price_list_currency": currency,
 			"plc_conversion_rate": 1,
 			"order_type": "Sales",
 			"customer": "_Test Customer",
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 2eb8bfd..ca6e61f 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -23,9 +23,7 @@
 
 	def test_reverse_purchase_receipt_sle(self):
 
-		frappe.db.set_value('UOM', '_Test UOM', 'must_be_whole_number', 0)
-
-		pr = make_purchase_receipt(qty=0.5)
+		pr = make_purchase_receipt(qty=0.5, item_code="_Test Item Home Desktop 200")
 
 		sl_entry = frappe.db.get_all("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
 			"voucher_no": pr.name}, ['actual_qty'])
@@ -41,8 +39,6 @@
 		self.assertEqual(len(sl_entry_cancelled), 2)
 		self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5)
 
-		frappe.db.set_value('UOM', '_Test UOM', 'must_be_whole_number', 1)
-
 	def test_make_purchase_invoice(self):
 		if not frappe.db.exists('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice'):
 			frappe.get_doc({
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 2ed7a04..be8508a 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -74,8 +74,7 @@
 
 	update_party_blanket_order(args, out)
 
-	
-	get_price_list_rate(args, item, out)
+	out.update(get_price_list_rate(args, item))
 
 	if args.customer and cint(args.is_pos):
 		out.update(get_pos_profile_item_details(args.company, args, update_data=True))
@@ -638,7 +637,10 @@
 		or item_group.get("default_supplier")
 		or brand.get("default_supplier"))
 
-def get_price_list_rate(args, item_doc, out):
+def get_price_list_rate(args, item_doc, out=None):
+	if out is None:
+		out = frappe._dict()
+
 	meta = frappe.get_meta(args.parenttype or args.doctype)
 
 	if meta.get_field("currency") or args.get('currency'):
@@ -651,17 +653,17 @@
 		if meta.get_field("currency"):
 			validate_conversion_rate(args, meta)
 
-		price_list_rate = get_price_list_rate_for(args, item_doc.name) or 0
+		price_list_rate = get_price_list_rate_for(args, item_doc.name)
 
 		# variant
-		if not price_list_rate and item_doc.variant_of:
+		if price_list_rate is None and item_doc.variant_of:
 			price_list_rate = get_price_list_rate_for(args, item_doc.variant_of)
 
 		# insert in database
-		if not price_list_rate:
+		if price_list_rate is None:
 			if args.price_list and args.rate:
 				insert_item_price(args)
-			return {}
+			return out
 
 		out.price_list_rate = flt(price_list_rate) * flt(args.plc_conversion_rate) \
 			/ flt(args.conversion_rate)
@@ -671,6 +673,8 @@
 			out.update(get_last_purchase_details(item_doc.name,
 				args.name, args.conversion_rate))
 
+	return out
+
 def insert_item_price(args):
 	"""Insert Item Price if Price List and Price List Rate are specified and currency is the same"""
 	if frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency \
@@ -1073,9 +1077,8 @@
 		}
 
 def apply_price_list_on_item(args):
-	item_details = frappe._dict()
 	item_doc = frappe.get_doc("Item", args.item_code)
-	get_price_list_rate(args, item_doc, item_details)
+	item_details = get_price_list_rate(args, item_doc)
 
 	item_details.update(get_pricing_rule_for_item(args, item_details.price_list_rate))