Merge branch 'develop' into ledger-merger
diff --git a/erpnext/accounts/doctype/ledger_merge/__init__.py b/erpnext/accounts/doctype/ledger_merge/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_merge/__init__.py
diff --git a/erpnext/accounts/doctype/ledger_merge/ledger_merge.js b/erpnext/accounts/doctype/ledger_merge/ledger_merge.js
new file mode 100644
index 0000000..b2db98d
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_merge/ledger_merge.js
@@ -0,0 +1,128 @@
+// Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Ledger Merge', {
+	setup: function(frm) {
+		frappe.realtime.on('ledger_merge_refresh', ({ ledger_merge }) => {
+			if (ledger_merge !== frm.doc.name) return;
+			frappe.model.clear_doc(frm.doc.doctype, frm.doc.name);
+			frappe.model.with_doc(frm.doc.doctype, frm.doc.name).then(() => {
+				frm.refresh();
+			});
+		});
+
+		frappe.realtime.on('ledger_merge_progress', data => {
+			if (data.ledger_merge !== frm.doc.name) return;
+			let message = __('Merging {0} of {1}', [data.current, data.total]);
+			let percent = Math.floor((data.current * 100) / data.total);
+			frm.dashboard.show_progress(__('Merge Progress'), percent, message);
+			frm.page.set_indicator(__('In Progress'), 'orange');
+		});
+
+		frm.set_query("account", function(doc) {
+			if (!doc.company) frappe.throw(__('Please set Company'));
+			if (!doc.root_type) frappe.throw(__('Please set Root Type'));
+			return {
+				filters: {
+					root_type: doc.root_type,
+					company: doc.company
+				}
+			};
+		});
+
+		frm.set_query('account', 'merge_accounts', function(doc) {
+			if (!doc.company) frappe.throw(__('Please set Company'));
+			if (!doc.root_type) frappe.throw(__('Please set Root Type'));
+			if (!doc.account) frappe.throw(__('Please set Account'));
+			let acc = [doc.account];
+			frm.doc.merge_accounts.forEach((row) => {
+				acc.push(row.account);
+			});
+			return {
+				filters: {
+					is_group: doc.is_group,
+					root_type: doc.root_type,
+					name: ["not in", acc],
+					company: doc.company
+				}
+			};
+		});
+	},
+
+	refresh: function(frm) {
+		frm.page.hide_icon_group();
+		frm.trigger('set_merge_status');
+		frm.trigger('update_primary_action');
+	},
+
+	after_save: function(frm) {
+		setTimeout(() => {
+			frm.trigger('update_primary_action');
+		}, 500);
+	},
+
+	update_primary_action: function(frm) {
+		if (frm.is_dirty()) {
+			frm.enable_save();
+			return;
+		}
+		frm.disable_save();
+		if (frm.doc.status !== 'Success') {
+			if (!frm.is_new()) {
+				let label = frm.doc.status === 'Pending' ? __('Start Merge') : __('Retry');
+				frm.page.set_primary_action(label, () => frm.events.start_merge(frm));
+			} else {
+				frm.page.set_primary_action(__('Save'), () => frm.save());
+			}
+		}
+	},
+
+	start_merge: function(frm) {
+		frm.call({
+			method: 'form_start_merge',
+			args: { docname: frm.doc.name },
+			btn: frm.page.btn_primary
+		}).then(r => {
+			if (r.message === true) {
+				frm.disable_save();
+			}
+		});
+	},
+
+	set_merge_status: function(frm) {
+		if (frm.doc.status == "Pending") return;
+		let successful_records = 0;
+		frm.doc.merge_accounts.forEach((row) => {
+			if (row.merged) successful_records += 1;
+		});
+		let message_args = [successful_records, frm.doc.merge_accounts.length];
+		frm.dashboard.set_headline(__('Successfully merged {0} out of {1}.', message_args));
+	},
+
+	root_type: function(frm) {
+		frm.set_value('account', '');
+		frm.set_value('merge_accounts', []);
+	},
+
+	company: function(frm) {
+		frm.set_value('account', '');
+		frm.set_value('merge_accounts', []);
+	}
+});
+
+frappe.ui.form.on('Ledger Merge Accounts', {
+	merge_accounts_add: function(frm) {
+		frm.trigger('update_primary_action');
+	},
+
+	merge_accounts_remove: function(frm) {
+		frm.trigger('update_primary_action');
+	},
+
+	account: function(frm, cdt, cdn) {
+		let row = frappe.get_doc(cdt, cdn);
+		row.account_name = row.account;
+		frm.refresh_field('merge_accounts');
+		frm.trigger('update_primary_action');
+	}
+});
diff --git a/erpnext/accounts/doctype/ledger_merge/ledger_merge.json b/erpnext/accounts/doctype/ledger_merge/ledger_merge.json
new file mode 100644
index 0000000..dd816df
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_merge/ledger_merge.json
@@ -0,0 +1,130 @@
+{
+ "actions": [],
+ "autoname": "format:{account_name} merger on {creation}",
+ "creation": "2021-12-09 15:38:04.556584",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "section_break_1",
+  "root_type",
+  "account",
+  "account_name",
+  "column_break_3",
+  "company",
+  "status",
+  "is_group",
+  "section_break_5",
+  "merge_accounts"
+ ],
+ "fields": [
+  {
+   "depends_on": "root_type",
+   "fieldname": "account",
+   "fieldtype": "Link",
+   "label": "Account",
+   "options": "Account",
+   "reqd": 1,
+   "set_only_once": 1
+  },
+  {
+   "fieldname": "section_break_1",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "column_break_3",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "merge_accounts",
+   "fieldtype": "Table",
+   "label": "Accounts to Merge",
+   "options": "Ledger Merge Accounts",
+   "reqd": 1
+  },
+  {
+   "depends_on": "account",
+   "fieldname": "section_break_5",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "company",
+   "fieldtype": "Link",
+   "label": "Company",
+   "options": "Company",
+   "reqd": 1,
+   "set_only_once": 1
+  },
+  {
+   "fieldname": "status",
+   "fieldtype": "Select",
+   "in_list_view": 1,
+   "label": "Status",
+   "options": "Pending\nSuccess\nPartial Success\nError",
+   "read_only": 1
+  },
+  {
+   "fieldname": "root_type",
+   "fieldtype": "Select",
+   "label": "Root Type",
+   "options": "\nAsset\nLiability\nIncome\nExpense\nEquity",
+   "reqd": 1,
+   "set_only_once": 1
+  },
+  {
+   "depends_on": "account",
+   "fetch_from": "account.account_name",
+   "fetch_if_empty": 1,
+   "fieldname": "account_name",
+   "fieldtype": "Data",
+   "label": "Account Name",
+   "read_only": 1,
+   "reqd": 1
+  },
+  {
+   "default": "0",
+   "depends_on": "account",
+   "fetch_from": "account.is_group",
+   "fieldname": "is_group",
+   "fieldtype": "Check",
+   "label": "Is Group",
+   "read_only": 1
+  }
+ ],
+ "hide_toolbar": 1,
+ "links": [],
+ "modified": "2021-12-12 21:34:55.155146",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Ledger Merge",
+ "naming_rule": "Expression",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  },
+  {
+   "create": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Accounts Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py
new file mode 100644
index 0000000..830ad37
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py
@@ -0,0 +1,76 @@
+# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+
+from erpnext.accounts.doctype.account.account import merge_account
+
+
+class LedgerMerge(Document):
+	def start_merge(self):
+		from frappe.core.page.background_jobs.background_jobs import get_info
+		from frappe.utils.background_jobs import enqueue
+		from frappe.utils.scheduler import is_scheduler_inactive
+
+		if is_scheduler_inactive() and not frappe.flags.in_test:
+			frappe.throw(
+				_("Scheduler is inactive. Cannot merge accounts."), title=_("Scheduler Inactive")
+			)
+
+		enqueued_jobs = [d.get("job_name") for d in get_info()]
+
+		if self.name not in enqueued_jobs:
+			enqueue(
+				start_merge,
+				queue="default",
+				timeout=6000,
+				event="ledger_merge",
+				job_name=self.name,
+				docname=self.name,
+				now=frappe.conf.developer_mode or frappe.flags.in_test,
+			)
+			return True
+
+		return False
+
+@frappe.whitelist()
+def form_start_merge(docname):
+	return frappe.get_doc("Ledger Merge", docname).start_merge()
+
+def start_merge(docname):
+	ledger_merge = frappe.get_doc("Ledger Merge", docname)
+	successful_merges = 0
+	total = len(ledger_merge.merge_accounts)
+	for row in ledger_merge.merge_accounts:
+		if not row.merged:
+			try:
+				merge_account(
+					row.account,
+					ledger_merge.account,
+					ledger_merge.is_group,
+					ledger_merge.root_type,
+					ledger_merge.company
+				)
+				row.db_set('merged', 1)
+				frappe.db.commit()
+				successful_merges += 1
+				frappe.publish_realtime("ledger_merge_progress", {
+						"ledger_merge": ledger_merge.name,
+						"current": successful_merges,
+						"total": total
+					}
+				)
+			except Exception:
+				frappe.db.rollback()
+				frappe.log_error(title=ledger_merge.name)
+			finally:
+				if successful_merges == total:
+					ledger_merge.db_set('status', 'Success')
+				elif successful_merges > 0:
+					ledger_merge.db_set('status', 'Partial Success')
+				else:
+					ledger_merge.db_set('status', 'Error')
+
+	frappe.publish_realtime("ledger_merge_refresh", {"ledger_merge": ledger_merge.name})
diff --git a/erpnext/accounts/doctype/ledger_merge/test_ledger_merge.py b/erpnext/accounts/doctype/ledger_merge/test_ledger_merge.py
new file mode 100644
index 0000000..f731536
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_merge/test_ledger_merge.py
@@ -0,0 +1,118 @@
+# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import unittest
+
+import frappe
+
+from erpnext.accounts.doctype.ledger_merge.ledger_merge import start_merge
+
+
+class TestLedgerMerge(unittest.TestCase):
+	def test_merge_success(self):
+		if not frappe.db.exists("Account", "Indirect Expenses - _TC"):
+			acc = frappe.new_doc("Account")
+			acc.account_name = "Indirect Expenses"
+			acc.is_group = 1
+			acc.parent_account = "Expenses - _TC"
+			acc.company = "_Test Company"
+			acc.insert()
+		if not frappe.db.exists("Account", "Indirect Test Expenses - _TC"):
+			acc = frappe.new_doc("Account")
+			acc.account_name = "Indirect Test Expenses"
+			acc.is_group = 1
+			acc.parent_account = "Expenses - _TC"
+			acc.company = "_Test Company"
+			acc.insert()
+		if not frappe.db.exists("Account", "Administrative Test Expenses - _TC"):
+			acc = frappe.new_doc("Account")
+			acc.account_name = "Administrative Test Expenses"
+			acc.parent_account = "Indirect Test Expenses - _TC"
+			acc.company = "_Test Company"
+			acc.insert()
+
+		doc = frappe.get_doc({
+			"doctype": "Ledger Merge",
+			"company": "_Test Company",
+			"root_type": frappe.db.get_value("Account", "Indirect Test Expenses - _TC", "root_type"),
+			"account": "Indirect Expenses - _TC",
+			"merge_accounts": [
+				{
+					"account": "Indirect Test Expenses - _TC",
+					"account_name": "Indirect Expenses"
+				}
+			]
+		}).insert(ignore_permissions=True)
+
+		parent = frappe.db.get_value("Account", "Administrative Test Expenses - _TC", "parent_account")
+		self.assertEqual(parent, "Indirect Test Expenses - _TC")
+
+		start_merge(doc.name)
+
+		parent = frappe.db.get_value("Account", "Administrative Test Expenses - _TC", "parent_account")
+		self.assertEqual(parent, "Indirect Expenses - _TC")
+
+		self.assertFalse(frappe.db.exists("Account", "Indirect Test Expenses - _TC"))
+
+	def test_partial_merge_success(self):
+		if not frappe.db.exists("Account", "Indirect Income - _TC"):
+			acc = frappe.new_doc("Account")
+			acc.account_name = "Indirect Income"
+			acc.is_group = 1
+			acc.parent_account = "Income - _TC"
+			acc.company = "_Test Company"
+			acc.insert()
+		if not frappe.db.exists("Account", "Indirect Test Income - _TC"):
+			acc = frappe.new_doc("Account")
+			acc.account_name = "Indirect Test Income"
+			acc.is_group = 1
+			acc.parent_account = "Income - _TC"
+			acc.company = "_Test Company"
+			acc.insert()
+		if not frappe.db.exists("Account", "Administrative Test Income - _TC"):
+			acc = frappe.new_doc("Account")
+			acc.account_name = "Administrative Test Income"
+			acc.parent_account = "Indirect Test Income - _TC"
+			acc.company = "_Test Company"
+			acc.insert()
+
+		doc = frappe.get_doc({
+			"doctype": "Ledger Merge",
+			"company": "_Test Company",
+			"root_type": frappe.db.get_value("Account", "Indirect Income - _TC", "root_type"),
+			"account": "Indirect Income - _TC",
+			"merge_accounts": [
+				{
+					"account": "Indirect Test Income - _TC",
+					"account_name": "Indirect Test Income"
+				},
+				{
+					"account": "Administrative Test Income - _TC",
+					"account_name": "Administrative Test Income"
+				}
+			]
+		}).insert(ignore_permissions=True)
+
+		parent = frappe.db.get_value("Account", "Administrative Test Income - _TC", "parent_account")
+		self.assertEqual(parent, "Indirect Test Income - _TC")
+
+		start_merge(doc.name)
+
+		parent = frappe.db.get_value("Account", "Administrative Test Income - _TC", "parent_account")
+		self.assertEqual(parent, "Indirect Income - _TC")
+
+		self.assertFalse(frappe.db.exists("Account", "Indirect Test Income - _TC"))
+		self.assertTrue(frappe.db.exists("Account", "Administrative Test Income - _TC"))
+
+	def tearDown(self):
+		for entry in frappe.db.get_all("Ledger Merge"):
+			frappe.delete_doc("Ledger Merge", entry.name)
+
+		test_accounts = [
+			"Indirect Test Expenses - _TC",
+			"Administrative Test Expenses - _TC",
+			"Indirect Test Income - _TC",
+			"Administrative Test Income - _TC"
+		]
+		for account in test_accounts:
+			frappe.delete_doc_if_exists("Account", account)
diff --git a/erpnext/accounts/doctype/ledger_merge_accounts/__init__.py b/erpnext/accounts/doctype/ledger_merge_accounts/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_merge_accounts/__init__.py
diff --git a/erpnext/accounts/doctype/ledger_merge_accounts/ledger_merge_accounts.json b/erpnext/accounts/doctype/ledger_merge_accounts/ledger_merge_accounts.json
new file mode 100644
index 0000000..4ce55ad
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_merge_accounts/ledger_merge_accounts.json
@@ -0,0 +1,52 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2021-12-09 15:44:58.033398",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "account",
+  "account_name",
+  "merged"
+ ],
+ "fields": [
+  {
+   "columns": 4,
+   "fieldname": "account",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Account",
+   "options": "Account",
+   "reqd": 1
+  },
+  {
+   "columns": 2,
+   "default": "0",
+   "fieldname": "merged",
+   "fieldtype": "Check",
+   "in_list_view": 1,
+   "label": "Merged",
+   "read_only": 1
+  },
+  {
+   "columns": 4,
+   "fieldname": "account_name",
+   "fieldtype": "Data",
+   "label": "Account Name",
+   "read_only": 1,
+   "reqd": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-12-10 15:27:24.477139",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Ledger Merge Accounts",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/ledger_merge_accounts/ledger_merge_accounts.py b/erpnext/accounts/doctype/ledger_merge_accounts/ledger_merge_accounts.py
new file mode 100644
index 0000000..30dfd65
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_merge_accounts/ledger_merge_accounts.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class LedgerMergeAccounts(Document):
+	pass