refactor: POS Invoice merging and cancellation (#24351)

* feat: pos invoice merging with background jobs

* fix: invoice threshold for queueing job

* refactor: cancellation flow of point of sale

* feat: tests for cancellation of pos closing

Co-authored-by: Marica <maricadsouza221197@gmail.com>
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
index e79eb42..9ea616f 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
@@ -3,6 +3,7 @@
 
 frappe.ui.form.on('POS Closing Entry', {
 	onload: function(frm) {
+		frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log'];
 		frm.set_query("pos_profile", function(doc) {
 			return {
 				filters: { 'user': doc.user }
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
index 32bca3b..18d430f 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
@@ -11,6 +11,7 @@
   "column_break_3",
   "posting_date",
   "pos_opening_entry",
+  "status",
   "section_break_5",
   "company",
   "column_break_7",
@@ -184,11 +185,27 @@
    "label": "POS Opening Entry",
    "options": "POS Opening Entry",
    "reqd": 1
+  },
+  {
+   "allow_on_submit": 1,
+   "default": "Draft",
+   "fieldname": "status",
+   "fieldtype": "Select",
+   "hidden": 1,
+   "label": "Status",
+   "options": "Draft\nSubmitted\nQueued\nCancelled",
+   "print_hide": 1,
+   "read_only": 1
   }
  ],
  "is_submittable": 1,
- "links": [],
- "modified": "2020-05-29 15:03:22.226113",
+ "links": [
+  {
+   "link_doctype": "POS Invoice Merge Log",
+   "link_fieldname": "pos_closing_entry"
+  }
+ ],
+ "modified": "2021-01-12 12:21:05.388650",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "POS Closing Entry",
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
index c26e14f..edf3d5a 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
@@ -6,13 +6,12 @@
 import frappe
 import json
 from frappe import _
-from frappe.model.document import Document
-from frappe.utils import getdate, get_datetime, flt
-from collections import defaultdict
+from frappe.utils import get_datetime, flt
+from erpnext.controllers.status_updater import StatusUpdater
 from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
-from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
+from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices, unconsolidate_pos_invoices
 
-class POSClosingEntry(Document):
+class POSClosingEntry(StatusUpdater):
 	def validate(self):
 		if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
 			frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
@@ -64,17 +63,22 @@
 
 		frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True)
 
-	def on_submit(self):
-		merge_pos_invoices(self.pos_transactions)
-		opening_entry = frappe.get_doc("POS Opening Entry", self.pos_opening_entry)
-		opening_entry.pos_closing_entry = self.name
-		opening_entry.set_status()
-		opening_entry.save()
-
 	def get_payment_reconciliation_details(self):
 		currency = frappe.get_cached_value('Company', self.company,  "default_currency")
 		return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html",
 			{"data": self, "currency": currency})
+	
+	def on_submit(self):
+		consolidate_pos_invoices(closing_entry=self)
+	
+	def on_cancel(self):
+		unconsolidate_pos_invoices(closing_entry=self)
+
+	def update_opening_entry(self, for_cancel=False):
+		opening_entry = frappe.get_doc("POS Opening Entry", self.pos_opening_entry)
+		opening_entry.pos_closing_entry = self.name if not for_cancel else None
+		opening_entry.set_status()
+		opening_entry.save()
 
 @frappe.whitelist()
 @frappe.validate_and_sanitize_search_inputs
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry_list.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry_list.js
new file mode 100644
index 0000000..20fd610
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry_list.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+// License: GNU General Public License v3. See license.txt
+
+// render
+frappe.listview_settings['POS Closing Entry'] = {
+	get_indicator: function(doc) {
+		var status_color = {
+			"Draft": "red",
+			"Submitted": "blue",
+			"Queued": "orange",
+			"Cancelled": "red"
+
+		};
+		return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
+	}
+};
diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
index 8de54d5..40db09e 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
+++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
@@ -13,7 +13,6 @@
 class TestPOSClosingEntry(unittest.TestCase):
 	def test_pos_closing_entry(self):
 		test_user, pos_profile = init_user_and_profile()
-
 		opening_entry = create_opening_entry(pos_profile, test_user.name)
 
 		pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
@@ -45,6 +44,49 @@
 		frappe.set_user("Administrator")
 		frappe.db.sql("delete from `tabPOS Profile`")
 
+	def test_cancelling_of_pos_closing_entry(self):
+		test_user, pos_profile = init_user_and_profile()
+		opening_entry = create_opening_entry(pos_profile, test_user.name)
+
+		pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
+		pos_inv1.append('payments', {
+			'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3500
+		})
+		pos_inv1.submit()
+
+		pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
+		pos_inv2.append('payments', {
+			'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
+		})
+		pos_inv2.submit()
+
+		pcv_doc = make_closing_entry_from_opening(opening_entry)
+		payment = pcv_doc.payment_reconciliation[0]
+
+		self.assertEqual(payment.mode_of_payment, 'Cash')
+
+		for d in pcv_doc.payment_reconciliation:
+			if d.mode_of_payment == 'Cash':
+				d.closing_amount = 6700
+
+		pcv_doc.submit()
+
+		pos_inv1.load_from_db()
+		self.assertRaises(frappe.ValidationError, pos_inv1.cancel)
+
+		si_doc = frappe.get_doc("Sales Invoice", pos_inv1.consolidated_invoice)
+		self.assertRaises(frappe.ValidationError, si_doc.cancel)
+
+		pcv_doc.load_from_db()
+		pcv_doc.cancel()
+		si_doc.load_from_db()
+		pos_inv1.load_from_db()
+		self.assertEqual(si_doc.docstatus, 2)
+		self.assertEqual(pos_inv1.status, 'Paid')
+
+		frappe.set_user("Administrator")
+		frappe.db.sql("delete from `tabPOS Profile`")
+
 def init_user_and_profile(**args):
 	user = 'test@example.com'
 	test_user = frappe.get_doc('User', user)
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
index 6a7c4be..465f0d3 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
@@ -11,6 +11,7 @@
 
 	onload(doc) {
 		this._super();
+		this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log'];
 		if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') {
 			this.frm.script_manager.trigger("is_pos");
 			this.frm.refresh_fields();
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 7312248..9d8f848 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -6,10 +6,9 @@
 import frappe
 from frappe import _
 from frappe.model.document import Document
-from erpnext.controllers.selling_controller import SellingController
-from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate
 from erpnext.accounts.utils import get_account_currency
 from erpnext.accounts.party import get_party_account, get_due_date
+from frappe.utils import cint, flt, getdate, nowdate, get_link_to_form
 from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
 from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
 from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
@@ -58,6 +57,22 @@
 			self.apply_loyalty_points()
 		self.check_phone_payments()
 		self.set_status(update=True)
+	
+	def before_cancel(self):
+		if self.consolidated_invoice and frappe.db.get_value('Sales Invoice', self.consolidated_invoice, 'docstatus') == 1:
+			pos_closing_entry = frappe.get_all(
+				"POS Invoice Reference",
+				ignore_permissions=True,
+				filters={ 'pos_invoice': self.name },
+				pluck="parent",
+				limit=1
+			)
+			frappe.throw(
+				_('You need to cancel POS Closing Entry {} to be able to cancel this document.').format(
+					get_link_to_form("POS Closing Entry", pos_closing_entry[0])
+				),
+				title=_('Not Allowed')
+			)
 
 	def on_cancel(self):
 		# run on cancel method of selling controller
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index c179360..57a23af 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -290,7 +290,7 @@
 
 	def test_merging_into_sales_invoice_with_discount(self):
 		from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
-		from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
+		from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
 
 		frappe.db.sql("delete from `tabPOS Invoice`")
 		test_user, pos_profile = init_user_and_profile()
@@ -306,7 +306,7 @@
 		})
 		pos_inv2.submit()
 
-		merge_pos_invoices()
+		consolidate_pos_invoices()
 
 		pos_inv.load_from_db()
 		rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
@@ -315,7 +315,7 @@
 
 	def test_merging_into_sales_invoice_with_discount_and_inclusive_tax(self):
 		from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
-		from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
+		from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
 
 		frappe.db.sql("delete from `tabPOS Invoice`")
 		test_user, pos_profile = init_user_and_profile()
@@ -348,7 +348,7 @@
 		})
 		pos_inv2.submit()
 
-		merge_pos_invoices()
+		consolidate_pos_invoices()
 
 		pos_inv.load_from_db()
 		rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
@@ -357,7 +357,7 @@
 
 	def test_merging_with_validate_selling_price(self):
 		from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
-		from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
+		from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
 
 		if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"):
 			frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 1)
@@ -393,7 +393,7 @@
 		})
 		pos_inv2.submit()
 
-		merge_pos_invoices()
+		consolidate_pos_invoices()
 
 		pos_inv2.load_from_db()
 		rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json
index 8f97639..da2984f 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json
@@ -7,6 +7,8 @@
  "field_order": [
   "posting_date",
   "customer",
+  "column_break_3",
+  "pos_closing_entry",
   "section_break_3",
   "pos_invoices",
   "references_section",
@@ -76,11 +78,22 @@
    "label": "Consolidated Credit Note",
    "options": "Sales Invoice",
    "read_only": 1
+  },
+  {
+   "fieldname": "column_break_3",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "pos_closing_entry",
+   "fieldtype": "Link",
+   "label": "POS Closing Entry",
+   "options": "POS Closing Entry"
   }
  ],
+ "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2020-05-29 15:08:41.317100",
+ "modified": "2020-12-01 11:53:57.267579",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "POS Invoice Merge Log",
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index 5e43cfe..5496804 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -7,8 +7,11 @@
 from frappe import _
 from frappe.model import default_fields
 from frappe.model.document import Document
+from frappe.utils import flt, getdate, nowdate
+from frappe.utils.background_jobs import enqueue
 from frappe.model.mapper import map_doc, map_child_doc
-from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate
+from frappe.utils.scheduler import is_scheduler_inactive
+from frappe.core.page.background_jobs.background_jobs import get_info
 
 from six import iteritems
 
@@ -61,7 +64,13 @@
 
 		self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
 
-		self.update_pos_invoices(sales_invoice, credit_note)
+		self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
+
+	def on_cancel(self):
+		pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
+
+		self.update_pos_invoices(pos_invoice_docs)
+		self.cancel_linked_invoices()
 
 	def process_merging_into_sales_invoice(self, data):
 		sales_invoice = self.get_new_sales_invoice()
@@ -160,17 +169,21 @@
 
 		return sales_invoice
 	
-	def update_pos_invoices(self, sales_invoice, credit_note):
-		for d in self.pos_invoices:
-			doc = frappe.get_doc('POS Invoice', d.pos_invoice)
-			if not doc.is_return:
-				doc.update({'consolidated_invoice': sales_invoice})
-			else:
-				doc.update({'consolidated_invoice': credit_note})
+	def update_pos_invoices(self, invoice_docs, sales_invoice='', credit_note=''):
+		for doc in invoice_docs:
+			doc.load_from_db()
+			doc.update({ 'consolidated_invoice': None if self.docstatus==2 else (credit_note if doc.is_return else sales_invoice) })
 			doc.set_status(update=True)
 			doc.save()
 
-def get_all_invoices():
+	def cancel_linked_invoices(self):
+		for si_name in [self.consolidated_invoice, self.consolidated_credit_note]:
+			if not si_name: continue
+			si = frappe.get_doc('Sales Invoice', si_name)
+			si.flags.ignore_validate = True
+			si.cancel()
+
+def get_all_unconsolidated_invoices():
 	filters = {
 		'consolidated_invoice': [ 'in', [ '', None ]],
 		'status': ['not in', ['Consolidated']],
@@ -181,7 +194,7 @@
 	
 	return pos_invoices
 
-def get_invoices_customer_map(pos_invoices):
+def get_invoice_customer_map(pos_invoices):
 	# pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Custoemr 2' : [{}] }
 	pos_invoice_customer_map = {}
 	for invoice in pos_invoices:
@@ -191,20 +204,82 @@
 	
 	return pos_invoice_customer_map
 
-def merge_pos_invoices(pos_invoices=[]):
-	if not pos_invoices:
-		pos_invoices = get_all_invoices()
-	
-	pos_invoice_map = get_invoices_customer_map(pos_invoices)
-	create_merge_logs(pos_invoice_map)
+def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
+	invoices = pos_invoices or closing_entry.get('pos_transactions') or get_all_unconsolidated_invoices()
+	invoice_by_customer = get_invoice_customer_map(invoices)
 
-def create_merge_logs(pos_invoice_customer_map):
-	for customer, invoices in iteritems(pos_invoice_customer_map):
+	if len(invoices) >= 5 and closing_entry:
+		enqueue_job(create_merge_logs, invoice_by_customer, closing_entry)
+		closing_entry.set_status(update=True, status='Queued')
+	else:
+		create_merge_logs(invoice_by_customer, closing_entry)
+
+def unconsolidate_pos_invoices(closing_entry):
+	merge_logs = frappe.get_all(
+		'POS Invoice Merge Log',
+		filters={ 'pos_closing_entry': closing_entry.name },
+		pluck='name'
+	)
+
+	if len(merge_logs) >= 5:
+		enqueue_job(cancel_merge_logs, merge_logs, closing_entry)
+		closing_entry.set_status(update=True, status='Queued')
+	else:
+		cancel_merge_logs(merge_logs, closing_entry)
+
+def create_merge_logs(invoice_by_customer, closing_entry={}):
+	for customer, invoices in iteritems(invoice_by_customer):
 		merge_log = frappe.new_doc('POS Invoice Merge Log')
 		merge_log.posting_date = getdate(nowdate())
 		merge_log.customer = customer
+		merge_log.pos_closing_entry = closing_entry.get('name', None)
 
 		merge_log.set('pos_invoices', invoices)
 		merge_log.save(ignore_permissions=True)
 		merge_log.submit()
+	
+	if closing_entry:
+		closing_entry.set_status(update=True, status='Submitted')
+		closing_entry.update_opening_entry()
 
+def cancel_merge_logs(merge_logs, closing_entry={}):
+	for log in merge_logs:
+		merge_log = frappe.get_doc('POS Invoice Merge Log', log)
+		merge_log.flags.ignore_permissions = True
+		merge_log.cancel()
+
+	if closing_entry:
+		closing_entry.set_status(update=True, status='Cancelled')
+		closing_entry.update_opening_entry(for_cancel=True)
+
+def enqueue_job(job, invoice_by_customer, closing_entry):
+	check_scheduler_status()
+
+	job_name = closing_entry.get("name")
+	if not job_already_enqueued(job_name):
+		enqueue(
+			job,
+			queue="long",
+			timeout=10000,
+			event="processing_merge_logs",
+			job_name=job_name,
+			closing_entry=closing_entry,
+			invoice_by_customer=invoice_by_customer,
+			now=frappe.conf.developer_mode or frappe.flags.in_test
+		)
+
+		if job == create_merge_logs:
+			msg = _('POS Invoices will be consolidated in a background process')
+		else:
+			msg = _('POS Invoices will be unconsolidated in a background process')
+
+		frappe.msgprint(msg, alert=1)
+
+def check_scheduler_status():
+	if is_scheduler_inactive() and not frappe.flags.in_test:
+		frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive"))
+
+def job_already_enqueued(job_name):
+	enqueued_jobs = [d.get("job_name") for d in get_info()]
+	if job_name in enqueued_jobs:
+		return True
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
index 0f34272..db046c9 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
@@ -7,7 +7,7 @@
 import unittest
 from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
 from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
-from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
+from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
 from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
 
 class TestPOSInvoiceMergeLog(unittest.TestCase):
@@ -34,7 +34,7 @@
 		})
 		pos_inv3.submit()
 
-		merge_pos_invoices()
+		consolidate_pos_invoices()
 
 		pos_inv.load_from_db()
 		self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
@@ -79,7 +79,7 @@
 		pos_inv_cn.paid_amount = -300
 		pos_inv_cn.submit()
 
-		merge_pos_invoices()
+		consolidate_pos_invoices()
 
 		pos_inv.load_from_db()
 		self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py
index acac1c4..cb5b3a5 100644
--- a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py
+++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py
@@ -6,7 +6,6 @@
 import frappe
 from frappe import _
 from frappe.utils import cint, get_link_to_form
-from frappe.model.document import Document
 from erpnext.controllers.status_updater import StatusUpdater
 
 class POSOpeningEntry(StatusUpdater):
diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry_list.js b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry_list.js
index 6c26ded..1ad3c91 100644
--- a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry_list.js
+++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry_list.js
@@ -5,7 +5,7 @@
 frappe.listview_settings['POS Opening Entry'] = {
 	get_indicator: function(doc) {
 		var status_color = {
-			"Draft": "grey",
+			"Draft": "red",
 			"Open": "orange",
 			"Closed": "green",
 			"Cancelled": "red"
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index f2a62cd..5bef9e2 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -20,6 +20,7 @@
 		var me = this;
 		this._super();
 
+		this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice'];
 		if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
 			// show debit_to in print format
 			this.frm.set_df_property("debit_to", "print_hide", 0);
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 447cee4..018bc7e 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -1987,8 +1987,15 @@
  "icon": "fa fa-file-text",
  "idx": 181,
  "is_submittable": 1,
- "links": [],
- "modified": "2020-12-25 22:57:32.555067",
+ "links": [
+  {
+   "custom": 1,
+   "group": "Reference",
+   "link_doctype": "POS Invoice",
+   "link_fieldname": "consolidated_invoice"
+  }
+ ],
+ "modified": "2021-01-12 12:16:15.192520",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index f45e076..c39bd39 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -236,7 +236,25 @@
 		if len(self.payments) == 0 and self.is_pos:
 			frappe.throw(_("At least one mode of payment is required for POS invoice."))
 
+	def check_if_consolidated_invoice(self):
+		# since POS Invoice extends Sales Invoice, we explicitly check if doctype is Sales Invoice
+		if self.doctype == "Sales Invoice" and self.is_consolidated:
+			invoice_or_credit_note = "consolidated_credit_note" if self.is_return else "consolidated_invoice"
+			pos_closing_entry = frappe.get_all(
+				"POS Invoice Merge Log",
+				filters={ invoice_or_credit_note: self.name },
+				pluck="pos_closing_entry"
+			)
+			if pos_closing_entry:
+				msg = _("To cancel a {} you need to cancel the POS Closing Entry {}. ").format(
+					frappe.bold("Consolidated Sales Invoice"),
+					get_link_to_form("POS Closing Entry", pos_closing_entry[0])
+				)
+				frappe.throw(msg, title=_("Not Allowed"))
+
 	def before_cancel(self):
+		self.check_if_consolidated_invoice()
+
 		super(SalesInvoice, self).before_cancel()
 		self.update_time_sheet(None)
 
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index 8c05134..0987d09 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -93,6 +93,12 @@
 		["Open", "eval:self.docstatus == 1 and not self.pos_closing_entry"],
 		["Closed", "eval:self.docstatus == 1 and self.pos_closing_entry"],
 		["Cancelled", "eval:self.docstatus == 2"],
+	],
+	"POS Closing Entry": [
+		["Draft", None],
+		["Submitted", "eval:self.docstatus == 1"],
+		["Queued", "eval:self.status == 'Queued'"],
+		["Cancelled", "eval:self.docstatus == 2"],
 	]
 }
 
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 6e28865..0c0b307 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -741,6 +741,7 @@
 erpnext.patches.v13_0.update_custom_fields_for_shopify
 erpnext.patches.v13_0.updates_for_multi_currency_payroll
 erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy
+erpnext.patches.v13_0.update_pos_closing_entry_in_merge_log
 erpnext.patches.v13_0.add_po_to_global_search
 erpnext.patches.v13_0.update_returned_qty_in_pr_dn
 erpnext.patches.v13_0.create_uae_pos_invoice_fields
diff --git a/erpnext/patches/v13_0/update_pos_closing_entry_in_merge_log.py b/erpnext/patches/v13_0/update_pos_closing_entry_in_merge_log.py
new file mode 100644
index 0000000..42bca7c
--- /dev/null
+++ b/erpnext/patches/v13_0/update_pos_closing_entry_in_merge_log.py
@@ -0,0 +1,25 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+	frappe.reload_doc("accounts", "doctype", "POS Invoice Merge Log")
+	frappe.reload_doc("accounts", "doctype", "POS Closing Entry")
+	if frappe.db.count('POS Invoice Merge Log'):
+		frappe.db.sql('''
+			UPDATE
+				`tabPOS Invoice Merge Log` log, `tabPOS Invoice Reference` log_ref
+			SET
+				log.pos_closing_entry = (
+					SELECT clo_ref.parent FROM `tabPOS Invoice Reference` clo_ref
+					WHERE clo_ref.pos_invoice = log_ref.pos_invoice
+					AND clo_ref.parenttype = 'POS Closing Entry'
+				)
+			WHERE
+				log_ref.parent = log.name
+		''')
+
+		frappe.db.sql('''UPDATE `tabPOS Closing Entry` SET status = 'Submitted' where docstatus = 1''')
+		frappe.db.sql('''UPDATE `tabPOS Closing Entry` SET status = 'Cancelled' where docstatus = 2''')