Merge pull request #28911 from nextchamp-saqib/fix-ksa-pf

fix(ksa): enable ksa pos invoice print format
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index 1dae87f..467d4a1 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -23,6 +23,7 @@
 	get_accounting_dimensions,
 )
 from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
+from erpnext.accounts.party import get_party_account_currency
 
 
 class Subscription(Document):
@@ -355,6 +356,9 @@
 			if frappe.db.get_value('Supplier', self.party, 'tax_withholding_category'):
 				invoice.apply_tds = 1
 
+		### Add party currency to invoice
+		invoice.currency = get_party_account_currency(self.party_type, self.party, self.company)
+
 		## Add dimensions in invoice for subscription:
 		accounting_dimensions = get_accounting_dimensions()
 
diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py
index 9dd370b..6f67bc5 100644
--- a/erpnext/accounts/doctype/subscription/test_subscription.py
+++ b/erpnext/accounts/doctype/subscription/test_subscription.py
@@ -60,15 +60,38 @@
 		plan.billing_interval_count = 3
 		plan.insert()
 
+	if not frappe.db.exists('Subscription Plan', '_Test Plan Multicurrency'):
+		plan = frappe.new_doc('Subscription Plan')
+		plan.plan_name = '_Test Plan Multicurrency'
+		plan.item = '_Test Non Stock Item'
+		plan.price_determination = "Fixed Rate"
+		plan.cost = 50
+		plan.currency = 'USD'
+		plan.billing_interval = 'Month'
+		plan.billing_interval_count = 1
+		plan.insert()
+
+def create_parties():
 	if not frappe.db.exists('Supplier', '_Test Supplier'):
 		supplier = frappe.new_doc('Supplier')
 		supplier.supplier_name = '_Test Supplier'
 		supplier.supplier_group = 'All Supplier Groups'
 		supplier.insert()
 
+	if not frappe.db.exists('Customer', '_Test Subscription Customer'):
+		customer = frappe.new_doc('Customer')
+		customer.customer_name = '_Test Subscription Customer'
+		customer.billing_currency = 'USD'
+		customer.append('accounts', {
+			'company': '_Test Company',
+			'account': '_Test Receivable USD - _TC'
+		})
+		customer.insert()
+
 class TestSubscription(unittest.TestCase):
 	def setUp(self):
 		create_plan()
+		create_parties()
 
 	def test_create_subscription_with_trial_with_correct_period(self):
 		subscription = frappe.new_doc('Subscription')
@@ -637,3 +660,22 @@
 
 		subscription.process()
 		self.assertEqual(len(subscription.invoices), 1)
+
+	def test_multicurrency_subscription(self):
+		subscription = frappe.new_doc('Subscription')
+		subscription.party_type = 'Customer'
+		subscription.party = '_Test Subscription Customer'
+		subscription.generate_invoice_at_period_start = 1
+		subscription.company = '_Test Company'
+		# select subscription start date as '2018-01-15'
+		subscription.start_date = '2018-01-01'
+		subscription.append('plans', {'plan': '_Test Plan Multicurrency', 'qty': 1})
+		subscription.save()
+
+		subscription.process()
+		self.assertEqual(len(subscription.invoices), 1)
+		self.assertEqual(subscription.status, 'Unpaid')
+
+		# Check the currency of the created invoice
+		currency = frappe.db.get_value('Sales Invoice', subscription.invoices[0].invoice, 'currency')
+		self.assertEqual(currency, 'USD')
\ No newline at end of file
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
index 37ea3fd..5017126 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
+++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
@@ -56,9 +56,14 @@
 
 		ms.submit()
 		s_id = ms.get_pending_data(data_type = "id", item_name = i.item_name, s_date = expected_dates[1])
-		test = make_maintenance_visit(source_name = ms.name, item_name = "_Test Item", s_id = s_id)
+
+		# Check if item is mapped in visit.
+		test_map_visit = make_maintenance_visit(source_name = ms.name, item_name = "_Test Item", s_id = s_id)
+		self.assertEqual(len(test_map_visit.purposes), 1)
+		self.assertEqual(test_map_visit.purposes[0].item_name, "_Test Item")
+
 		visit = frappe.new_doc('Maintenance Visit')
-		visit = test
+		visit = test_map_visit
 		visit.maintenance_schedule = ms.name
 		visit.maintenance_schedule_detail = s_id
 		visit.completion_status = "Partially Completed"
diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
index 6f6ca61..d2197a6 100644
--- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
+++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
@@ -47,7 +47,7 @@
 			frm.set_value({ status: 'Draft' });
 		}
 		if (frm.doc.__islocal) {
-			frm.clear_table("purposes");
+			frm.doc.maintenance_type == 'Unscheduled' && frm.clear_table("purposes");
 			frm.set_value({ mntc_date: frappe.datetime.get_today() });
 		}
 	},
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index dac7b36..d85b8a6 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -76,6 +76,7 @@
 			frm.trigger("prepare_timer_buttons");
 		}
 
+		frm.trigger("setup_quality_inspection");
 		if (frm.doc.work_order) {
 			frappe.db.get_value('Work Order', frm.doc.work_order,
 				'transfer_material_against').then((r) => {
@@ -86,6 +87,22 @@
 		}
 	},
 
+	setup_quality_inspection: function(frm) {
+		let quality_inspection_field = frm.get_docfield("quality_inspection");
+		quality_inspection_field.get_route_options_for_new_doc = function(frm) {
+			return  {
+				"inspection_type": "In Process",
+				"reference_type": "Job Card",
+				"reference_name": frm.doc.name,
+				"item_code": frm.doc.production_item,
+				"item_name": frm.doc.item_name,
+				"item_serial_no": frm.doc.serial_no,
+				"batch_no": frm.doc.batch_no,
+				"quality_inspection_template": frm.doc.quality_inspection_template,
+			};
+		};
+	},
+
 	setup_corrective_job_card: function(frm) {
 		frm.add_custom_button(__('Corrective Job Card'), () => {
 			let operations = frm.doc.sub_operations.map(d => d.sub_operation).concat(frm.doc.operation);
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json
index 6528199..5a071f1 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.json
+++ b/erpnext/manufacturing/doctype/job_card/job_card.json
@@ -19,6 +19,7 @@
   "serial_no",
   "column_break_12",
   "wip_warehouse",
+  "quality_inspection_template",
   "quality_inspection",
   "project",
   "batch_no",
@@ -408,11 +409,18 @@
    "no_copy": 1,
    "options": "Job Card Scrap Item",
    "print_hide": 1
+  },
+  {
+   "fetch_from": "operation.quality_inspection_template",
+   "fieldname": "quality_inspection_template",
+   "fieldtype": "Link",
+   "label": "Quality Inspection Template",
+   "options": "Quality Inspection Template"
   }
  ],
  "is_submittable": 1,
  "links": [],
- "modified": "2021-11-12 10:15:03.572401",
+ "modified": "2021-11-24 19:17:40.879235",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Job Card",
diff --git a/erpnext/manufacturing/doctype/operation/operation.json b/erpnext/manufacturing/doctype/operation/operation.json
index 10a97ed..753552c 100644
--- a/erpnext/manufacturing/doctype/operation/operation.json
+++ b/erpnext/manufacturing/doctype/operation/operation.json
@@ -13,6 +13,7 @@
   "is_corrective_operation",
   "job_card_section",
   "create_job_card_based_on_batch_size",
+  "quality_inspection_template",
   "column_break_6",
   "batch_size",
   "sub_operations_section",
@@ -92,15 +93,22 @@
    "fieldname": "is_corrective_operation",
    "fieldtype": "Check",
    "label": "Is Corrective Operation"
+  },
+  {
+   "fieldname": "quality_inspection_template",
+   "fieldtype": "Link",
+   "label": "Quality Inspection Template",
+   "options": "Quality Inspection Template"
   }
  ],
  "icon": "fa fa-wrench",
  "index_web_pages_for_search": 1,
  "links": [],
- "modified": "2021-01-12 15:09:23.593338",
+ "modified": "2021-11-24 19:15:24.357187",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Operation",
+ "naming_rule": "Set by user",
  "owner": "Administrator",
  "permissions": [
   {
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
index d08dc3e..eea2879 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
@@ -59,7 +59,7 @@
 	},
 
 	item_code: function(frm) {
-		if (frm.doc.item_code) {
+		if (frm.doc.item_code && !frm.doc.quality_inspection_template) {
 			return frm.call({
 				method: "get_quality_inspection_template",
 				doc: frm.doc,
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
index 913ee15..4e3b80a 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
@@ -18,6 +18,15 @@
 		if not self.readings and self.item_code:
 			self.get_item_specification_details()
 
+		if self.inspection_type=="In Process" and self.reference_type=="Job Card":
+			item_qi_template = frappe.db.get_value("Item", self.item_code, 'quality_inspection_template')
+			parameters = get_template_details(item_qi_template)
+			for reading in self.readings:
+				for d in parameters:
+					if reading.specification == d.specification:
+						reading.update(d)
+						reading.status = "Accepted"
+
 		if self.readings:
 			self.inspect_and_set_status()
 
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index c0b89fd..3c7b26b 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -167,7 +167,7 @@
 			sle.company, sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference,
 			sle.item_code as name, sle.voucher_no, sle.stock_value, sle.batch_no
 		from
-			`tabStock Ledger Entry` sle force index (posting_sort_index)
+			`tabStock Ledger Entry` sle
 		where sle.docstatus < 2 %s %s
 		and is_cancelled = 0
 		order by sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty""" % #nosec
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/__init__.py b/erpnext/stock/report/stock_ledger_invariant_check/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/report/stock_ledger_invariant_check/__init__.py
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js
new file mode 100644
index 0000000..c484516
--- /dev/null
+++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js
@@ -0,0 +1,43 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+const DIFFERNCE_FIELD_NAMES = [
+	"difference_in_qty",
+	"fifo_qty_diff",
+	"fifo_value_diff",
+	"fifo_valuation_diff",
+	"valuation_diff",
+	"fifo_difference_diff"
+];
+
+frappe.query_reports["Stock Ledger Invariant Check"] = {
+	"filters": [
+		{
+			"fieldname": "item_code",
+			"fieldtype": "Link",
+			"label": "Item",
+			"mandatory": 1,
+			"options": "Item",
+			get_query: function() {
+				return {
+					filters: {is_stock_item: 1, has_serial_no: 0}
+				}
+			}
+		},
+		{
+			"fieldname": "warehouse",
+			"fieldtype": "Link",
+			"label": "Warehouse",
+			"mandatory": 1,
+			"options": "Warehouse",
+		}
+	],
+	formatter (value, row, column, data, default_formatter) {
+		value = default_formatter(value, row, column, data);
+		if (DIFFERNCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) {
+			value = "<span style='color:red'>" + value + "</span>";
+		}
+		return value;
+	},
+};
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.json b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.json
new file mode 100644
index 0000000..d28fe0f
--- /dev/null
+++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.json
@@ -0,0 +1,26 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-12-16 06:31:23.290916",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-12-16 09:55:58.341764",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Stock Ledger Invariant Check",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "Stock Ledger Invariant Check",
+ "report_type": "Script Report",
+ "roles": [
+  {
+   "role": "System Manager"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
new file mode 100644
index 0000000..ca47a1e
--- /dev/null
+++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
@@ -0,0 +1,236 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# License: GNU GPL v3. See LICENSE
+
+import json
+
+import frappe
+
+SLE_FIELDS = (
+	"name",
+	"posting_date",
+	"posting_time",
+	"creation",
+	"voucher_type",
+	"voucher_no",
+	"actual_qty",
+	"qty_after_transaction",
+	"incoming_rate",
+	"outgoing_rate",
+	"stock_queue",
+	"batch_no",
+	"stock_value",
+	"stock_value_difference",
+	"valuation_rate",
+)
+
+
+def execute(filters=None):
+	columns = get_columns()
+	data = get_data(filters)
+	return columns, data
+
+
+def get_data(filters):
+	sles = get_stock_ledger_entries(filters)
+	return add_invariant_check_fields(sles)
+
+
+def get_stock_ledger_entries(filters):
+	return frappe.get_all(
+		"Stock Ledger Entry",
+		fields=SLE_FIELDS,
+		filters={
+			"item_code": filters.item_code,
+			"warehouse": filters.warehouse,
+			"is_cancelled": 0
+		},
+		order_by="timestamp(posting_date, posting_time), creation",
+	)
+
+
+def add_invariant_check_fields(sles):
+	balance_qty = 0.0
+	for idx, sle in enumerate(sles):
+		queue = json.loads(sle.stock_queue)
+
+		fifo_qty = 0.0
+		fifo_value = 0.0
+		for qty, rate in queue:
+			fifo_qty += qty
+			fifo_value += qty * rate
+
+		balance_qty += sle.actual_qty
+		if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
+			balance_qty = sle.qty_after_transaction
+
+		sle.fifo_queue_qty = fifo_qty
+		sle.fifo_stock_value = fifo_value
+		sle.fifo_valuation_rate = fifo_value / fifo_qty if fifo_qty else None
+		sle.balance_value_by_qty = (
+			sle.stock_value / sle.qty_after_transaction if sle.qty_after_transaction else None
+		)
+		sle.expected_qty_after_transaction = balance_qty
+
+		# set difference fields
+		sle.difference_in_qty = sle.qty_after_transaction - sle.expected_qty_after_transaction
+		sle.fifo_qty_diff = sle.qty_after_transaction - fifo_qty
+		sle.fifo_value_diff = sle.stock_value - fifo_value
+		sle.fifo_valuation_diff = (
+			sle.valuation_rate - sle.fifo_valuation_rate if sle.fifo_valuation_rate else None
+		)
+		sle.valuation_diff = (
+			sle.valuation_rate - sle.balance_value_by_qty if sle.balance_value_by_qty else None
+		)
+
+		if idx > 0:
+			sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value
+			sle.fifo_difference_diff = sle.fifo_stock_diff - sle.stock_value_difference
+
+	return sles
+
+
+def get_columns():
+	return [
+		{
+			"fieldname": "name",
+			"fieldtype": "Link",
+			"label": "Stock Ledger Entry",
+			"options": "Stock Ledger Entry",
+		},
+		{
+			"fieldname": "posting_date",
+			"fieldtype": "Date",
+			"label": "Posting Date",
+		},
+		{
+			"fieldname": "posting_time",
+			"fieldtype": "Time",
+			"label": "Posting Time",
+		},
+		{
+			"fieldname": "creation",
+			"fieldtype": "Datetime",
+			"label": "Creation",
+		},
+		{
+			"fieldname": "voucher_type",
+			"fieldtype": "Link",
+			"label": "Voucher Type",
+			"options": "DocType",
+		},
+		{
+			"fieldname": "voucher_no",
+			"fieldtype": "Dynamic Link",
+			"label": "Voucher No",
+			"options": "voucher_type",
+		},
+		{
+			"fieldname": "batch_no",
+			"fieldtype": "Link",
+			"label": "Batch",
+			"options": "Batch",
+		},
+		{
+			"fieldname": "actual_qty",
+			"fieldtype": "Float",
+			"label": "Qty Change",
+		},
+		{
+			"fieldname": "incoming_rate",
+			"fieldtype": "Float",
+			"label": "Incoming Rate",
+		},
+		{
+			"fieldname": "outgoing_rate",
+			"fieldtype": "Float",
+			"label": "Outgoing Rate",
+		},
+		{
+			"fieldname": "qty_after_transaction",
+			"fieldtype": "Float",
+			"label": "(A) Qty After Transaction",
+		},
+		{
+			"fieldname": "expected_qty_after_transaction",
+			"fieldtype": "Float",
+			"label": "(B) Expected Qty After Transaction",
+		},
+		{
+			"fieldname": "difference_in_qty",
+			"fieldtype": "Float",
+			"label": "A - B",
+		},
+		{
+			"fieldname": "stock_queue",
+			"fieldtype": "Data",
+			"label": "FIFO Queue",
+		},
+
+		{
+			"fieldname": "fifo_queue_qty",
+			"fieldtype": "Float",
+			"label": "(C) Total qty in queue",
+		},
+		{
+			"fieldname": "fifo_qty_diff",
+			"fieldtype": "Float",
+			"label": "A - C",
+		},
+		{
+			"fieldname": "stock_value",
+			"fieldtype": "Float",
+			"label": "(D) Balance Stock Value",
+		},
+		{
+			"fieldname": "fifo_stock_value",
+			"fieldtype": "Float",
+			"label": "(E) Balance Stock Value in Queue",
+		},
+		{
+			"fieldname": "fifo_value_diff",
+			"fieldtype": "Float",
+			"label": "D - E",
+		},
+
+		{
+			"fieldname": "stock_value_difference",
+			"fieldtype": "Float",
+			"label": "(F) Stock Value Difference",
+		},
+		{
+			"fieldname": "fifo_stock_diff",
+			"fieldtype": "Float",
+			"label": "(G) Stock Value difference (FIFO queue)",
+		},
+		{
+			"fieldname": "fifo_difference_diff",
+			"fieldtype": "Float",
+			"label": "F - G",
+		},
+		{
+			"fieldname": "valuation_rate",
+			"fieldtype": "Float",
+			"label": "(H) Valuation Rate",
+		},
+		{
+			"fieldname": "fifo_valuation_rate",
+			"fieldtype": "Float",
+			"label": "(I) Valuation Rate as per FIFO",
+		},
+
+		{
+			"fieldname": "fifo_valuation_diff",
+			"fieldtype": "Float",
+			"label": "H - I",
+		},
+		{
+			"fieldname": "balance_value_by_qty",
+			"fieldtype": "Float",
+			"label": "(J) Valuation = Value (D) ÷ Qty (A)",
+		},
+		{
+			"fieldname": "valuation_diff",
+			"fieldtype": "Float",
+			"label": "H - J",
+		},
+	]
diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py
index d7fb5b2..1dcf863 100644
--- a/erpnext/stock/report/test_reports.py
+++ b/erpnext/stock/report/test_reports.py
@@ -41,6 +41,12 @@
 	("Total Stock Summary", {"group_by": "warehouse",}),
 	("Batch Item Expiry Status", {}),
 	("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
+	("Stock Ledger Invariant Check",
+		{
+			"warehouse": "_Test Warehouse - _TC",
+			"item": "_Test Item"
+		}
+	),
 ]
 
 OPTIONAL_FILTERS = {