diff --git a/erpnext/accounts/doctype/ledger_merge/__init__.py b/erpnext/accounts/doctype/ledger_merge/__init__.py new file mode 100644 index 00000000000..e69de29bb2d 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 00000000000..b2db98dbd03 --- /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 00000000000..dd816df6278 --- /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 00000000000..830ad370d70 --- /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 00000000000..f7315362b7b --- /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 00000000000..e69de29bb2d 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 00000000000..4ce55ada7fa --- /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 00000000000..30dfd657820 --- /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