diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2c9a60c7c4c..0d6cc58343e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,23 +16,23 @@ repos:
- id: check-merge-conflict
- id: check-ast
- - repo: https://github.com/pre-commit/mirrors-eslint
- rev: v8.44.0
- hooks:
- - id: eslint
- types_or: [javascript]
- args: ['--quiet']
- # Ignore any files that might contain jinja / bundles
- exclude: |
- (?x)^(
- erpnext/public/dist/.*|
- cypress/.*|
- .*node_modules.*|
- .*boilerplate.*|
- erpnext/public/js/controllers/.*|
- erpnext/templates/pages/order.js|
- erpnext/templates/includes/.*
- )$
+ # - repo: https://github.com/pre-commit/mirrors-eslint
+ # rev: v8.44.0
+ # hooks:
+ # - id: eslint
+ # types_or: [javascript]
+ # args: ['--quiet']
+ # # Ignore any files that might contain jinja / bundles
+ # exclude: |
+ # (?x)^(
+ # erpnext/public/dist/.*|
+ # cypress/.*|
+ # .*node_modules.*|
+ # .*boilerplate.*|
+ # erpnext/public/js/controllers/.*|
+ # erpnext/templates/pages/order.js|
+ # erpnext/templates/includes/.*
+ # )$
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool_beta/__init__.py b/erpnext/accounts/doctype/bank_reconciliation_tool_beta/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.js b/erpnext/accounts/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.js
new file mode 100644
index 00000000000..0d12a006a66
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.js
@@ -0,0 +1,238 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Bank Reconciliation Tool Beta", {
+ setup: function (frm) {
+ frm.set_query("bank_account", function () {
+ return {
+ filters: {
+ company: frm.doc.company,
+ "is_company_account": 1
+ },
+ };
+ });
+
+ frm.set_query("party_type", function () {
+ return {
+ filters: {
+ name: [
+ "in", Object.keys(frappe.boot.party_account_types),
+ ],
+ },
+ };
+ });
+
+ },
+
+ onload: function (frm) {
+ // Set default filter dates
+ let today = frappe.datetime.get_today()
+ frm.doc.bank_statement_from_date = frappe.datetime.add_months(today, -1);
+ frm.doc.bank_statement_to_date = today;
+ },
+
+ filter_by_reference_date: function (frm) {
+ if (frm.doc.filter_by_reference_date) {
+ frm.set_value("bank_statement_from_date", "");
+ frm.set_value("bank_statement_to_date", "");
+ } else {
+ frm.set_value("from_reference_date", "");
+ frm.set_value("to_reference_date", "");
+ }
+ },
+
+ refresh: function(frm) {
+ frm.disable_save();
+ frm.fields_dict["filters_section"].collapse(false);
+
+ frm.add_custom_button(__("Get Bank Transactions"), function() {
+ if (!frm.doc.bank_account) {
+ frappe.throw(
+ {
+ message: __("Please set the 'Bank Account' filter"),
+ title: __("Filter Required")
+ }
+ );
+ }
+
+ frm.events.add_upload_statement_button(frm);
+ frm.events.build_reconciliation_area(frm);
+ });
+ frm.change_custom_button_type("Get Bank Transactions", null, "primary");
+
+ frm.add_custom_button(__("Auto Reconcile"), function() {
+ frappe.call({
+ method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.auto_reconcile_vouchers",
+ args: {
+ bank_account: frm.doc.bank_account,
+ from_date: frm.doc.bank_statement_from_date,
+ to_date: frm.doc.bank_statement_to_date,
+ filter_by_reference_date: frm.doc.filter_by_reference_date,
+ from_reference_date: frm.doc.from_reference_date,
+ to_reference_date: frm.doc.to_reference_date,
+ },
+ callback: function(r) {
+ if (!r.exc) frm.refresh();
+ }
+ });
+ });
+
+ frm.$reconciliation_area = frm.get_field("reconciliation_action_area").$wrapper;
+ frm.events.setup_empty_state(frm);
+
+ frm.events.build_reconciliation_area(frm);
+ },
+
+ add_upload_statement_button: function(frm) {
+ frm.remove_custom_button(__("Upload a Bank Statement"));
+ frm.add_custom_button(
+ __("Upload a Bank Statement"),
+ () => frm.events.route_to_bank_statement_import(frm),
+ );
+ },
+
+ setup_empty_state: function(frm) {
+ frm.$reconciliation_area.empty();
+ let empty_area = frm.$reconciliation_area.append(`
+
+
+ ${__("Set Filters and Get Bank Transactions")}
+
+
${__("Or")}
+
+ `).find(".bank-reco-beta-empty-state");
+
+ frappe.utils.add_custom_button(
+ __("Upload a Bank Statement"),
+ () => frm.events.route_to_bank_statement_import(frm),
+ "",
+ __("Upload a Bank Statement"),
+ "btn-primary",
+ $(empty_area),
+ )
+ },
+
+ route_to_bank_statement_import(frm) {
+ frappe.open_in_new_tab = true;
+
+ if (!frm.doc.bank_account || !frm.doc.company) {
+ frappe.new_doc("Bank Statement Import");
+ return;
+ }
+
+ // Route to saved Import Record in new tab
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_statement_import.bank_statement_import.upload_bank_statement",
+ args: {
+ dt: frm.doc.doctype,
+ dn: frm.doc.name,
+ company: frm.doc.company,
+ bank_account: frm.doc.bank_account,
+ },
+ callback: function (r) {
+ if (!r.exc) {
+ var doc = frappe.model.sync(r.message);
+ frappe.open_in_new_tab = true;
+ frappe.set_route("Form", doc[0].doctype, doc[0].name);
+ }
+ },
+ })
+ },
+
+ bank_account: function (frm) {
+ frappe.db.get_value(
+ "Bank Account",
+ frm.doc.bank_account,
+ "account",
+ (r) => {
+ frappe.db.get_value(
+ "Account",
+ r.account,
+ "account_currency",
+ (r) => {
+ frm.doc.account_currency = r.account_currency;
+ frm.trigger("bank_statement_from_date");
+ frm.trigger("bank_statement_to_date");
+ }
+ );
+ }
+ );
+
+ },
+
+ bank_statement_from_date: function (frm) {
+ frm.trigger("get_account_opening_balance");
+ },
+
+ bank_statement_to_date: function (frm) {
+ frm.trigger("get_account_closing_balance");
+ frm.trigger("render_summary");
+ },
+
+ bank_statement_closing_balance: function (frm) {
+ frm.trigger("render_summary");
+ },
+
+ get_account_opening_balance(frm) {
+ if (frm.doc.bank_account && frm.doc.bank_statement_from_date) {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
+ args: {
+ bank_account: frm.doc.bank_account,
+ till_date: frm.doc.bank_statement_from_date,
+ },
+ callback: (response) => {
+ frm.set_value("account_opening_balance", response.message);
+ },
+ });
+ }
+ },
+
+ get_account_closing_balance(frm) {
+ if (frm.doc.bank_account && frm.doc.bank_statement_to_date) {
+ return frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
+ args: {
+ bank_account: frm.doc.bank_account,
+ till_date: frm.doc.bank_statement_to_date,
+ },
+ callback: (response) => {
+ frm.cleared_balance = response.message;
+ },
+ });
+ }
+ },
+
+ render_summary: function(frm) {
+ frm.get_field("reconciliation_tool_cards").$wrapper.empty();
+
+ frappe.require("bank-reconciliation-tool-beta.bundle.js", () => {
+ let difference = flt(frm.doc.bank_statement_closing_balance) - flt(frm.cleared_balance);
+ let difference_color = difference >= 0 ? "text-success" : "text-danger";
+
+ frm.summary_card = new erpnext.accounts.bank_reconciliation.SummaryCard({
+ $wrapper: frm.get_field("reconciliation_tool_cards").$wrapper,
+ values: {
+ "Bank Closing Balance": [frm.doc.bank_statement_closing_balance],
+ "ERP Closing Balance": [frm.cleared_balance],
+ "Difference": [difference, difference_color]
+ },
+ currency: frm.doc.account_currency,
+ })
+ });
+ },
+
+ build_reconciliation_area: function(frm) {
+ if (!frm.doc.bank_account) return;
+
+ frappe.require("bank-reconciliation-tool-beta.bundle.js", () =>
+ frm.panel_manager = new erpnext.accounts.bank_reconciliation.PanelManager({
+ doc: frm.doc,
+ $wrapper: frm.$reconciliation_area,
+ })
+ );
+ },
+});
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.json b/erpnext/accounts/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.json
new file mode 100644
index 00000000000..0fe074eb5ae
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.json
@@ -0,0 +1,142 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-08-24 15:15:48.714131",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "filters_section",
+ "company",
+ "bank_account",
+ "account_currency",
+ "column_break_oojl",
+ "account_opening_balance",
+ "bank_statement_closing_balance",
+ "column_break_sdit",
+ "bank_statement_from_date",
+ "bank_statement_to_date",
+ "from_reference_date",
+ "to_reference_date",
+ "filter_by_reference_date",
+ "section_break_dyil",
+ "reconciliation_tool_cards",
+ "reconciliation_action_area"
+ ],
+ "fields": [
+ {
+ "collapsible": 1,
+ "fieldname": "filters_section",
+ "fieldtype": "Section Break",
+ "label": "Filters"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ },
+ {
+ "fieldname": "bank_account",
+ "fieldtype": "Link",
+ "label": "Bank Account",
+ "options": "Bank Account"
+ },
+ {
+ "fieldname": "account_currency",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Account Currency",
+ "options": "Currency"
+ },
+ {
+ "fieldname": "column_break_oojl",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_sdit",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "account_opening_balance",
+ "fieldtype": "Currency",
+ "label": "Account Opening Balance",
+ "options": "account_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "bank_statement_closing_balance",
+ "fieldtype": "Currency",
+ "label": "Closing Balance",
+ "options": "account_currency"
+ },
+ {
+ "depends_on": "eval: doc.bank_account && !doc.filter_by_reference_date",
+ "fieldname": "bank_statement_from_date",
+ "fieldtype": "Date",
+ "label": "From Date"
+ },
+ {
+ "depends_on": "eval: doc.bank_account && !doc.filter_by_reference_date",
+ "fieldname": "bank_statement_to_date",
+ "fieldtype": "Date",
+ "label": "To Date"
+ },
+ {
+ "depends_on": "eval:doc.filter_by_reference_date",
+ "fieldname": "from_reference_date",
+ "fieldtype": "Date",
+ "label": "From Reference Date"
+ },
+ {
+ "depends_on": "eval:doc.filter_by_reference_date",
+ "fieldname": "to_reference_date",
+ "fieldtype": "Date",
+ "label": "To Reference Date"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.bank_account",
+ "fieldname": "filter_by_reference_date",
+ "fieldtype": "Check",
+ "label": "Filter by Reference Date"
+ },
+ {
+ "fieldname": "section_break_dyil",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "reconciliation_tool_cards",
+ "fieldtype": "HTML"
+ },
+ {
+ "fieldname": "reconciliation_action_area",
+ "fieldtype": "HTML"
+ }
+ ],
+ "hide_toolbar": 1,
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2023-08-24 15:29:55.140942",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Bank Reconciliation Tool Beta",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.py b/erpnext/accounts/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.py
new file mode 100644
index 00000000000..e9d526cdf1b
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.py
@@ -0,0 +1,838 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+import json
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.query_builder.custom import ConstantColumn
+from frappe.utils import cint
+from pypika.terms import Parameter, PseudoColumn
+
+from erpnext import get_default_cost_center
+from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
+ reconcile_vouchers,
+)
+from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
+from erpnext.accounts.utils import get_account_currency
+
+
+class BankReconciliationToolBeta(Document):
+ pass
+
+
+@frappe.whitelist()
+def get_bank_transactions(bank_account, from_date=None, to_date=None, order_by="date asc"):
+ # returns bank transactions for a bank account
+ filters = []
+ filters.append(["bank_account", "=", bank_account])
+ filters.append(["docstatus", "=", 1])
+ filters.append(["unallocated_amount", ">", 0.0])
+ if to_date:
+ filters.append(["date", "<=", to_date])
+ if from_date:
+ filters.append(["date", ">=", from_date])
+ transactions = frappe.get_all(
+ "Bank Transaction",
+ fields=[
+ "date",
+ "deposit",
+ "withdrawal",
+ "currency",
+ "description",
+ "name",
+ "bank_account",
+ "company",
+ "unallocated_amount",
+ "reference_number",
+ "party_type",
+ "party",
+ "bank_party_name",
+ "bank_party_account_number",
+ "bank_party_iban",
+ ],
+ filters=filters,
+ order_by=order_by,
+ )
+ return transactions
+
+
+@frappe.whitelist()
+def create_journal_entry_bts(
+ bank_transaction_name,
+ reference_number=None,
+ reference_date=None,
+ posting_date=None,
+ entry_type=None,
+ second_account=None,
+ mode_of_payment=None,
+ party_type=None,
+ party=None,
+ allow_edit=None,
+):
+ # Create a new journal entry based on the bank transaction
+ bank_transaction = frappe.db.get_values(
+ "Bank Transaction",
+ bank_transaction_name,
+ fieldname=["name", "deposit", "withdrawal", "bank_account"],
+ as_dict=True,
+ )[0]
+ company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
+ account_type = frappe.db.get_value("Account", second_account, "account_type")
+ if account_type in ["Receivable", "Payable"]:
+ if not (party_type and party):
+ frappe.throw(
+ _("Party Type and Party is required for Receivable / Payable account {0}").format(
+ second_account
+ )
+ )
+
+ company = frappe.get_value("Account", company_account, "company")
+
+ accounts = []
+ # Multi Currency?
+ accounts.append(
+ {
+ "account": second_account,
+ "credit_in_account_currency": bank_transaction.deposit,
+ "debit_in_account_currency": bank_transaction.withdrawal,
+ "party_type": party_type,
+ "party": party,
+ "cost_center": get_default_cost_center(company),
+ }
+ )
+
+ accounts.append(
+ {
+ "account": company_account,
+ "bank_account": bank_transaction.bank_account,
+ "credit_in_account_currency": bank_transaction.withdrawal,
+ "debit_in_account_currency": bank_transaction.deposit,
+ "cost_center": get_default_cost_center(company),
+ }
+ )
+
+ journal_entry_dict = {
+ "voucher_type": entry_type,
+ "company": company,
+ "posting_date": posting_date,
+ "cheque_date": reference_date,
+ "cheque_no": reference_number,
+ "mode_of_payment": mode_of_payment,
+ }
+ journal_entry = frappe.new_doc("Journal Entry")
+ journal_entry.update(journal_entry_dict)
+ journal_entry.set("accounts", accounts)
+ journal_entry.insert()
+
+ if allow_edit:
+ return journal_entry # Return saved document
+
+ journal_entry.submit()
+
+ if bank_transaction.deposit > 0.0:
+ paid_amount = bank_transaction.deposit
+ else:
+ paid_amount = bank_transaction.withdrawal
+
+ vouchers = json.dumps(
+ [
+ {
+ "payment_doctype": "Journal Entry",
+ "payment_name": journal_entry.name,
+ "amount": paid_amount,
+ }
+ ]
+ )
+
+ return reconcile_vouchers(bank_transaction_name, vouchers)
+
+
+@frappe.whitelist()
+def create_payment_entry_bts(
+ bank_transaction_name,
+ reference_number=None,
+ reference_date=None,
+ party_type=None,
+ party=None,
+ posting_date=None,
+ mode_of_payment=None,
+ project=None,
+ cost_center=None,
+ allow_edit=None,
+):
+ # Create a new payment entry based on the bank transaction
+ bank_transaction = frappe.db.get_values(
+ "Bank Transaction",
+ bank_transaction_name,
+ fieldname=["name", "unallocated_amount", "deposit", "bank_account"],
+ as_dict=True,
+ )[0]
+ paid_amount = bank_transaction.unallocated_amount
+ payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay"
+
+ company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
+ company = frappe.get_value("Account", company_account, "company")
+ payment_entry_dict = {
+ "company": company,
+ "payment_type": payment_type,
+ "reference_no": reference_number,
+ "reference_date": reference_date,
+ "party_type": party_type,
+ "party": party,
+ "posting_date": posting_date,
+ "paid_amount": paid_amount,
+ "received_amount": paid_amount,
+ }
+ payment_entry = frappe.new_doc("Payment Entry")
+
+ payment_entry.update(payment_entry_dict)
+
+ if mode_of_payment:
+ payment_entry.mode_of_payment = mode_of_payment
+ if project:
+ payment_entry.project = project
+ if cost_center:
+ payment_entry.cost_center = cost_center
+ if payment_type == "Receive":
+ payment_entry.paid_to = company_account
+ else:
+ payment_entry.paid_from = company_account
+
+ payment_entry.validate()
+ payment_entry.insert()
+
+ if allow_edit:
+ return payment_entry # Return saved document
+
+ payment_entry.submit()
+ vouchers = json.dumps(
+ [
+ {
+ "payment_doctype": "Payment Entry",
+ "payment_name": payment_entry.name,
+ "amount": paid_amount,
+ }
+ ]
+ )
+ return reconcile_vouchers(bank_transaction_name, vouchers)
+
+
+@frappe.whitelist()
+def get_linked_payments(
+ bank_transaction_name,
+ document_types=None,
+ from_date=None,
+ to_date=None,
+ filter_by_reference_date=None,
+ from_reference_date=None,
+ to_reference_date=None,
+):
+ # get all matching payments for a bank transaction
+ transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
+ bank_account = frappe.db.get_values(
+ "Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
+ )[0]
+ (gl_account, company) = (bank_account.account, bank_account.company)
+ matching = check_matching(
+ gl_account,
+ company,
+ transaction,
+ document_types,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+ )
+ return subtract_allocations(gl_account, matching)
+
+
+def subtract_allocations(gl_account, vouchers):
+ "Look up & subtract any existing Bank Transaction allocations"
+ copied = []
+ for voucher in vouchers:
+ rows = get_total_allocated_amount(voucher[1], voucher[2])
+ amount = None
+ for row in rows:
+ if row["gl_account"] == gl_account:
+ amount = row["total"]
+ break
+
+ if amount:
+ l = list(voucher)
+ l[3] -= amount
+ copied.append(tuple(l))
+ else:
+ copied.append(voucher)
+ return copied
+
+
+def check_matching(
+ bank_account,
+ company,
+ transaction,
+ document_types,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+):
+ # combine all types of vouchers
+ subquery = get_queries(
+ bank_account,
+ company,
+ transaction,
+ document_types,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+ )
+ filters = {
+ "amount": transaction.unallocated_amount,
+ "payment_type": "Receive" if transaction.deposit > 0.0 else "Pay",
+ "reference_no": transaction.reference_number,
+ "party_type": transaction.party_type,
+ "party": transaction.party,
+ "bank_account": bank_account,
+ }
+
+ matching_vouchers = []
+ matching_vouchers.extend(get_loan_vouchers(bank_account, transaction, document_types, filters))
+
+ for query in subquery:
+ matching_vouchers.extend(
+ frappe.db.sql(
+ query,
+ filters,
+ as_dict=1,
+ )
+ )
+ return (
+ sorted(matching_vouchers, key=lambda x: x["rank"], reverse=True) if matching_vouchers else []
+ )
+
+
+def get_queries(
+ bank_account,
+ company,
+ transaction,
+ document_types,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+):
+ # get queries to get matching vouchers
+ account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
+ exact_match = "exact_match" in document_types
+ queries = []
+
+ # get matching queries from all the apps (except erpnext, to override)
+ for method_name in frappe.get_hooks("get_matching_queries")[1:]:
+ queries.extend(
+ frappe.get_attr(method_name)(
+ bank_account,
+ company,
+ transaction,
+ document_types,
+ exact_match,
+ account_from_to,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+ )
+ or []
+ )
+
+ return queries
+
+
+def get_matching_queries(
+ bank_account,
+ company,
+ transaction,
+ document_types,
+ exact_match,
+ account_from_to,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+):
+ queries = []
+ exact_party_match = "exact_party_match" in document_types
+ currency = get_account_currency(bank_account)
+
+ if "payment_entry" in document_types:
+ query = get_pe_matching_query(
+ exact_match,
+ account_from_to,
+ transaction,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+ exact_party_match,
+ )
+ queries.append(query)
+
+ if "journal_entry" in document_types:
+ query = get_je_matching_query(
+ exact_match,
+ transaction,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+ )
+ queries.append(query)
+
+ if transaction.deposit > 0.0 and "sales_invoice" in document_types:
+ if "unpaid_invoices" in document_types:
+ query = get_unpaid_si_matching_query(exact_match, exact_party_match, currency)
+ queries.append(query)
+ else:
+ query = get_si_matching_query(exact_match, exact_party_match)
+ queries.append(query)
+
+ if transaction.withdrawal > 0.0 and "purchase_invoice" in document_types:
+ if "unpaid_invoices" in document_types:
+ query = get_unpaid_pi_matching_query(exact_match, exact_party_match, currency)
+ queries.append(query)
+ else:
+ query = get_pi_matching_query(exact_match, exact_party_match)
+ queries.append(query)
+
+ if "bank_transaction" in document_types:
+ query = get_bt_matching_query(exact_match, transaction, exact_party_match)
+ queries.append(query)
+
+ return queries
+
+
+def get_loan_vouchers(bank_account, transaction, document_types, filters):
+ vouchers = []
+ exact_match = "exact_match" in document_types
+
+ if transaction.withdrawal > 0.0 and "loan_disbursement" in document_types:
+ vouchers.extend(get_ld_matching_query(bank_account, exact_match, filters))
+
+ if transaction.deposit > 0.0 and "loan_repayment" in document_types:
+ vouchers.extend(get_lr_matching_query(bank_account, exact_match, filters))
+
+ return vouchers
+
+
+def get_bt_matching_query(exact_match, transaction, exact_party_match):
+ # get matching bank transaction query
+ # find bank transactions in the same bank account with opposite sign
+ # same bank account must have same company and currency
+ field = "deposit" if transaction.withdrawal > 0.0 else "withdrawal"
+ filter_by_party = (
+ "AND party_type = %(party_type)s AND party = %(party)s" if exact_party_match else ""
+ )
+
+ return f"""
+ SELECT
+ (
+ CASE WHEN reference_number = %(reference_no)s THEN 1 ELSE 0 END
+ + CASE WHEN {field} = %(amount)s THEN 1 ELSE 0 END
+ + CASE WHEN ( party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
+ + CASE WHEN unallocated_amount = %(amount)s THEN 1 ELSE 0 END
+ + 1
+ ) AS rank,
+ 'Bank Transaction' AS doctype,
+ name,
+ unallocated_amount AS paid_amount,
+ reference_number AS reference_no,
+ date AS reference_date,
+ party,
+ party_type,
+ date AS posting_date,
+ currency,
+ (
+ CASE WHEN reference_number = %(reference_no)s THEN 1 ELSE 0 END
+ ) as reference_number_match,
+ (
+ CASE WHEN {field} = %(amount)s THEN 1 ELSE 0 END
+ ) as amount_match,
+ (
+ CASE WHEN ( party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
+ ) as party_match,
+ (
+ CASE WHEN unallocated_amount = %(amount)s THEN 1 ELSE 0 END
+ ) as unallocated_amount_match
+ FROM
+ `tabBank Transaction`
+ WHERE
+ status != 'Reconciled'
+ AND name != '{transaction.name}'
+ AND bank_account = '{transaction.bank_account}'
+ AND {field} {'= %(amount)s' if exact_match else '> 0.0'}
+ {filter_by_party}
+ """
+
+
+def get_ld_matching_query(bank_account, exact_match, filters):
+ loan_disbursement = frappe.qb.DocType("Loan Disbursement")
+ matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
+ matching_party = loan_disbursement.applicant_type == filters.get(
+ "party_type"
+ ) and loan_disbursement.applicant == filters.get("party")
+
+ rank = frappe.qb.terms.Case().when(matching_reference, 1).else_(0)
+
+ rank1 = frappe.qb.terms.Case().when(matching_party, 1).else_(0)
+
+ query = (
+ frappe.qb.from_(loan_disbursement)
+ .select(
+ rank + rank1 + 1,
+ ConstantColumn("Loan Disbursement").as_("doctype"),
+ loan_disbursement.name,
+ loan_disbursement.disbursed_amount.as_("paid_amount"),
+ loan_disbursement.reference_number.as_("reference_no"),
+ loan_disbursement.reference_date,
+ loan_disbursement.applicant.as_("party"),
+ loan_disbursement.applicant_type.as_("party_type"),
+ loan_disbursement.disbursement_date.as_("posting_date"),
+ "".as_("currency"),
+ rank.as_("reference_number_match"),
+ rank1.as_("party_match"),
+ )
+ .where(loan_disbursement.docstatus == 1)
+ .where(loan_disbursement.clearance_date.isnull())
+ .where(loan_disbursement.disbursement_account == bank_account)
+ )
+
+ if exact_match:
+ query.where(loan_disbursement.disbursed_amount == filters.get("amount"))
+ else:
+ query.where(loan_disbursement.disbursed_amount > 0.0)
+
+ vouchers = query.run(as_list=True)
+
+ return vouchers
+
+
+def get_lr_matching_query(bank_account, exact_match, filters):
+ loan_repayment = frappe.qb.DocType("Loan Repayment")
+ matching_reference = loan_repayment.reference_number == filters.get("reference_number")
+ matching_party = loan_repayment.applicant_type == filters.get(
+ "party_type"
+ ) and loan_repayment.applicant == filters.get("party")
+
+ rank = frappe.qb.terms.Case().when(matching_reference, 1).else_(0)
+
+ rank1 = frappe.qb.terms.Case().when(matching_party, 1).else_(0)
+
+ query = (
+ frappe.qb.from_(loan_repayment)
+ .select(
+ rank + rank1 + 1,
+ ConstantColumn("Loan Repayment").as_("doctype"),
+ loan_repayment.name,
+ loan_repayment.amount_paid.as_("paid_amount"),
+ loan_repayment.reference_number.as_("reference_no"),
+ loan_repayment.reference_date,
+ loan_repayment.applicant.as_("party"),
+ loan_repayment.applicant_type.as_("party_type"),
+ loan_repayment.posting_date,
+ "".as_("currency"),
+ rank.as_("reference_number_match"),
+ rank1.as_("party_match"),
+ )
+ .where(loan_repayment.docstatus == 1)
+ .where(loan_repayment.clearance_date.isnull())
+ .where(loan_repayment.payment_account == bank_account)
+ )
+
+ if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
+ query = query.where((loan_repayment.repay_from_salary == 0))
+
+ if exact_match:
+ query.where(loan_repayment.amount_paid == filters.get("amount"))
+ else:
+ query.where(loan_repayment.amount_paid > 0.0)
+
+ vouchers = query.run()
+
+ return vouchers
+
+
+def get_pe_matching_query(
+ exact_match,
+ account_from_to,
+ transaction,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+ exact_party_match,
+):
+ # get matching payment entries query
+ to_from = "to" if transaction.deposit > 0.0 else "from"
+ currency_field = f"paid_{to_from}_account_currency as currency"
+ filter_by_date = f"AND posting_date between '{from_date}' and '{to_date}'"
+ order_by = " posting_date"
+ filter_by_reference_no = ""
+
+ if cint(filter_by_reference_date):
+ filter_by_date = f"AND reference_date between '{from_reference_date}' and '{to_reference_date}'"
+ order_by = " reference_date"
+
+ if frappe.flags.auto_reconcile_vouchers == True:
+ filter_by_reference_no = f"AND reference_no = '{transaction.reference_number}'"
+
+ filter_by_party = (
+ "AND (party_type = %(party_type)s AND party = %(party)s )" if exact_party_match else ""
+ )
+
+ return f"""
+ SELECT
+ (CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
+ + CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
+ + CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
+ + 1 ) AS rank,
+ 'Payment Entry' as doctype,
+ name,
+ paid_amount,
+ reference_no,
+ reference_date,
+ party,
+ party_type,
+ posting_date,
+ {currency_field},
+ (CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END) AS reference_number_match,
+ (CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END) AS party_match,
+ (CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END) AS amount_match
+ FROM
+ `tabPayment Entry`
+ WHERE
+ docstatus = 1
+ AND payment_type IN (%(payment_type)s, 'Internal Transfer')
+ AND ifnull(clearance_date, '') = ""
+ AND {account_from_to} = %(bank_account)s
+ AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
+ {filter_by_date}
+ {filter_by_reference_no}
+ {filter_by_party}
+ order by{order_by}
+ """
+
+
+def get_je_matching_query(
+ exact_match,
+ transaction,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+):
+ # get matching journal entry query
+ # We have mapping at the bank level
+ # So one bank could have both types of bank accounts like asset and liability
+ # So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
+ cr_or_dr = "credit" if transaction.withdrawal > 0.0 else "debit"
+ filter_by_date = f"AND je.posting_date between '{from_date}' and '{to_date}'"
+ order_by = " je.posting_date"
+ filter_by_reference_no = ""
+ if cint(filter_by_reference_date):
+ filter_by_date = f"AND je.cheque_date between '{from_reference_date}' and '{to_reference_date}'"
+ order_by = " je.cheque_date"
+ if frappe.flags.auto_reconcile_vouchers == True:
+ filter_by_reference_no = f"AND je.cheque_no = '{transaction.reference_number}'"
+ return f"""
+ SELECT
+ (CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END
+ + CASE WHEN jea.{cr_or_dr}_in_account_currency = %(amount)s THEN 1 ELSE 0 END
+ + 1) AS rank ,
+ 'Journal Entry' AS doctype,
+ je.name,
+ jea.{cr_or_dr}_in_account_currency AS paid_amount,
+ je.cheque_no AS reference_no,
+ je.cheque_date AS reference_date,
+ je.pay_to_recd_from AS party,
+ jea.party_type,
+ je.posting_date,
+ jea.account_currency AS currency,
+ (CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END) AS reference_number_match,
+ (CASE WHEN jea.{cr_or_dr}_in_account_currency = %(amount)s THEN 1 ELSE 0 END) AS amount_match
+ FROM
+ `tabJournal Entry Account` AS jea
+ JOIN
+ `tabJournal Entry` AS je
+ ON
+ jea.parent = je.name
+ WHERE
+ je.docstatus = 1
+ AND je.voucher_type NOT IN ('Opening Entry')
+ AND (je.clearance_date IS NULL OR je.clearance_date='0000-00-00')
+ AND jea.account = %(bank_account)s
+ AND jea.{cr_or_dr}_in_account_currency {'= %(amount)s' if exact_match else '> 0.0'}
+ AND je.docstatus = 1
+ {filter_by_date}
+ {filter_by_reference_no}
+ order by {order_by}
+ """
+
+
+def get_si_matching_query(exact_match, exact_party_match):
+ # get matching paid sales invoice query
+ filter_by_party = " AND si.customer = %(party)s" if exact_party_match else ""
+
+ return f"""
+ SELECT
+ ( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END
+ + CASE WHEN sip.amount = %(amount)s THEN 1 ELSE 0 END
+ + 1 ) AS rank,
+ 'Sales Invoice' as doctype,
+ si.name,
+ sip.amount as paid_amount,
+ si.name as reference_no,
+ '' as reference_date,
+ si.customer as party,
+ 'Customer' as party_type,
+ si.posting_date,
+ si.currency,
+ (CASE WHEN si.customer=%(party)s THEN 1 ELSE 0 END) AS party_match,
+ (CASE WHEN sip.amount = %(amount)s THEN 1 ELSE 0 END) AS amount_match
+ FROM
+ `tabSales Invoice Payment` as sip
+ JOIN
+ `tabSales Invoice` as si
+ ON
+ sip.parent = si.name
+ WHERE
+ si.docstatus = 1
+ AND (sip.clearance_date is null or sip.clearance_date='0000-00-00')
+ AND sip.account = %(bank_account)s
+ AND sip.amount {'= %(amount)s' if exact_match else '> 0.0'}
+ {filter_by_party}
+ """
+
+
+def get_unpaid_si_matching_query(exact_match, exact_party_match, currency):
+ sales_invoice = frappe.qb.DocType("Sales Invoice")
+
+ party_match = (
+ frappe.qb.terms.Case().when(sales_invoice.customer == Parameter("%(party)s"), 1).else_(0)
+ )
+ amount_match = (
+ frappe.qb.terms.Case().when(sales_invoice.grand_total == Parameter("%(amount)s"), 1).else_(0)
+ )
+
+ query = (
+ frappe.qb.from_(sales_invoice)
+ .select(
+ (party_match + amount_match + 1).as_("rank"),
+ PseudoColumn("'Sales Invoice' as doctype"),
+ sales_invoice.name.as_("name"),
+ sales_invoice.outstanding_amount.as_("paid_amount"),
+ sales_invoice.name.as_("reference_no"),
+ PseudoColumn("'' as reference_date"),
+ sales_invoice.customer.as_("party"),
+ PseudoColumn("'Customer' as party_type"),
+ sales_invoice.posting_date,
+ sales_invoice.currency,
+ party_match.as_("party_match"),
+ amount_match.as_("amount_match"),
+ )
+ .where(sales_invoice.docstatus == 1)
+ .where(sales_invoice.is_return == 0)
+ .where(sales_invoice.outstanding_amount > 0.0)
+ .where(sales_invoice.currency == currency)
+ )
+
+ if exact_match:
+ query = query.where(sales_invoice.grand_total == Parameter("%(amount)s"))
+
+ if exact_party_match:
+ query = query.where(sales_invoice.customer == Parameter("%(party)s"))
+
+ return str(query)
+
+
+def get_pi_matching_query(exact_match, exact_party_match):
+ # get matching purchase invoice query when they are also used as payment entries (is_paid)
+ filter_by_party = "AND supplier = %(party)s" if exact_party_match else ""
+
+ return f"""
+ SELECT
+ ( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END
+ + CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
+ + 1 ) AS rank,
+ 'Purchase Invoice' as doctype,
+ name,
+ paid_amount,
+ name as reference_no,
+ '' as reference_date,
+ supplier as party,
+ 'Supplier' as party_type,
+ posting_date,
+ currency,
+ (CASE WHEN supplier=%(party)s THEN 1 ELSE 0 END) AS party_match,
+ (CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END) AS amount_match
+ FROM
+ `tabPurchase Invoice`
+ WHERE
+ docstatus = 1
+ AND is_paid = 1
+ AND ifnull(clearance_date, '') = ""
+ AND cash_bank_account = %(bank_account)s
+ AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
+ {filter_by_party}
+ """
+
+
+def get_unpaid_pi_matching_query(exact_match, exact_party_match, currency):
+ purchase_invoice = frappe.qb.DocType("Purchase Invoice")
+
+ party_match = (
+ frappe.qb.terms.Case().when(purchase_invoice.supplier == Parameter("%(party)s"), 1).else_(0)
+ )
+ amount_match = (
+ frappe.qb.terms.Case().when(purchase_invoice.grand_total == Parameter("%(amount)s"), 1).else_(0)
+ )
+
+ query = (
+ frappe.qb.from_(purchase_invoice)
+ .select(
+ (party_match + amount_match + 1).as_("rank"),
+ PseudoColumn("'Purchase Invoice' as doctype"),
+ purchase_invoice.name.as_("name"),
+ purchase_invoice.outstanding_amount.as_("paid_amount"),
+ purchase_invoice.name.as_("reference_no"),
+ PseudoColumn("'' as reference_date"),
+ purchase_invoice.supplier.as_("party"),
+ PseudoColumn("'Supplier' as party_type"),
+ purchase_invoice.posting_date,
+ purchase_invoice.currency,
+ party_match.as_("party_match"),
+ amount_match.as_("amount_match"),
+ )
+ .where(purchase_invoice.docstatus == 1)
+ .where(purchase_invoice.is_return == 0)
+ .where(purchase_invoice.outstanding_amount > 0.0)
+ .where(purchase_invoice.currency == currency)
+ )
+
+ if exact_match:
+ query = query.where(purchase_invoice.grand_total == Parameter("%(amount)s"))
+
+ if exact_party_match:
+ query = query.where(purchase_invoice.supplier == Parameter("%(party)s"))
+
+ return str(query)
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool_beta/test_bank_reconciliation_tool_beta.py b/erpnext/accounts/doctype/bank_reconciliation_tool_beta/test_bank_reconciliation_tool_beta.py
new file mode 100644
index 00000000000..e398a23a88c
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool_beta/test_bank_reconciliation_tool_beta.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestBankReconciliationToolBeta(FrappeTestCase):
+ pass
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 6a475624127..fffe323a07c 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -4,6 +4,7 @@
import frappe
from frappe.utils import flt
+from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.controllers.status_updater import StatusUpdater
@@ -63,18 +64,40 @@ class BankTransaction(StatusUpdater):
found = True
if not found:
+ payment_doctype, payment_name = voucher["payment_doctype"], voucher["payment_name"]
+
+ if self.is_unpaid_invoice(payment_doctype, payment_name):
+ # Make Payment Entry against the unpaid invoice, link PE to Bank Transaction
+ payment_name = self.make_pe_against_invoice(payment_doctype, payment_name)
+ payment_doctype = "Payment Entry" # Change doctype to PE
+
pe = {
- "payment_document": voucher["payment_doctype"],
- "payment_entry": voucher["payment_name"],
+ "payment_document": payment_doctype,
+ "payment_entry": payment_name,
"allocated_amount": 0.0, # Temporary
}
- child = self.append("payment_entries", pe)
+ self.append("payment_entries", pe)
added = True
# runs on_update_after_submit
if added:
self.save()
+ def is_unpaid_invoice(self, payment_doctype, payment_name):
+ is_invoice = payment_doctype in ("Sales Invoice", "Purchase Invoice")
+ if not is_invoice:
+ return False
+
+ # Check if the invoice is unpaid
+ return flt(frappe.db.get_value(payment_doctype, payment_name, "outstanding_amount")) > 0
+
+ def make_pe_against_invoice(self, payment_doctype, payment_name):
+ bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account")
+ payment_entry = get_payment_entry(payment_doctype, payment_name, bank_account=bank_account)
+ payment_entry.reference_no = self.reference_number or payment_name
+ payment_entry.submit()
+ return payment_entry.name
+
def allocate_payment_entries(self):
"""Refactored from bank reconciliation tool.
Non-zero allocations must be amended/cleared manually
diff --git a/erpnext/public/js/bank-reconciliation-tool-beta.bundle.js b/erpnext/public/js/bank-reconciliation-tool-beta.bundle.js
new file mode 100644
index 00000000000..005e8f9fe42
--- /dev/null
+++ b/erpnext/public/js/bank-reconciliation-tool-beta.bundle.js
@@ -0,0 +1,3 @@
+import "./bank_reconciliation_tool_beta/panel_manager";
+import "./bank_reconciliation_tool_beta/actions_panel";
+import "./bank_reconciliation_tool_beta/summary_number_card";
\ No newline at end of file
diff --git a/erpnext/public/js/bank_reconciliation_tool_beta/actions_panel.js b/erpnext/public/js/bank_reconciliation_tool_beta/actions_panel.js
new file mode 100644
index 00000000000..2dc7eb8657f
--- /dev/null
+++ b/erpnext/public/js/bank_reconciliation_tool_beta/actions_panel.js
@@ -0,0 +1,1012 @@
+frappe.provide("erpnext.accounts.bank_reconciliation");
+
+erpnext.accounts.bank_reconciliation.ActionsPanel = class ActionsPanel {
+ constructor(opts) {
+ Object.assign(this, opts);
+ this.make();
+ }
+
+ make() {
+ this.init_actions_container();
+ this.render_tabs();
+
+ // Default to last selected tab
+ this.$actions_container.find("#" + this.panel_manager.actions_tab).trigger("click");
+ }
+
+ init_actions_container() {
+ if (this.$wrapper.find(".actions-panel").length > 0) {
+ this.$actions_container = this.$wrapper.find(".actions-panel");
+ this.$actions_container.empty();
+ } else {
+ this.$actions_container = this.$wrapper.append(`
+
+ `).find(".actions-panel");
+ }
+
+ this.$actions_container.append(`
+
+
+
+ `);
+ }
+
+ render_tabs() {
+ this.tabs_list_ul = this.$actions_container.find(".form-tabs");
+ this.$tab_content = this.$actions_container.find(".tab-content");
+
+ ["Details", "Match Voucher", "Create Voucher"].forEach(tab => {
+ let tab_name = frappe.scrub(tab);
+ this.add_tab(tab_name, tab);
+
+ let $tab_link = this.tabs_list_ul.find(`#${tab_name}-tab`);
+ $tab_link.on("click", () => {
+ if (tab == "Details") {
+ this.details_section();
+ } else if (tab == "Match Voucher") {
+ this.render_match_section();
+ } else {
+ this.create_section();
+ }
+ });
+ });
+ }
+
+ add_tab(tab_name, tab) {
+ this.tabs_list_ul.append(`
+
+
+ ${__(tab)}
+
+
+ `);
+ }
+
+ details_section() {
+ this.$tab_content.empty();
+ this.panel_manager.actions_tab = "details-tab";
+
+ this.details_field_group = new frappe.ui.FieldGroup({
+ fields: this.get_detail_tab_fields(),
+ body: this.$tab_content,
+ card_layout: true,
+ });
+ this.details_field_group.make();
+ }
+
+ create_section() {
+ this.$tab_content.empty();
+ this.panel_manager.actions_tab = "create_voucher-tab";
+
+ this.create_field_group = new frappe.ui.FieldGroup({
+ fields: this.get_create_tab_fields(),
+ body: this.$tab_content,
+ card_layout: true,
+ });
+ this.create_field_group.make();
+ }
+
+ async render_match_section() {
+ this.$tab_content.empty();
+ this.panel_manager.actions_tab = "match_voucher-tab";
+
+ this.match_field_group = new frappe.ui.FieldGroup({
+ fields: this.get_match_tab_fields(),
+ body: this.$tab_content,
+ card_layout: true,
+ });
+ this.match_field_group.make()
+
+ this.summary_empty_state();
+ await this.populate_matching_vouchers();
+ }
+
+ summary_empty_state() {
+ let summary_field = this.match_field_group.get_field("transaction_amount_summary").$wrapper;
+ summary_field.append(
+ `
+
`
+ );
+ }
+
+ async populate_matching_vouchers() {
+ let filter_fields = this.match_field_group.get_values();
+ let document_types = Object.keys(filter_fields).filter(field => filter_fields[field] === 1);
+
+ this.update_filters_in_state(document_types);
+
+ let vouchers = await this.get_matching_vouchers(document_types);
+ this.render_data_table(vouchers);
+
+ let transaction_amount = this.transaction.withdrawal || this.transaction.deposit;
+ this.render_transaction_amount_summary(
+ flt(transaction_amount),
+ flt(this.transaction.unallocated_amount),
+ this.transaction.currency,
+ );
+ }
+
+ update_filters_in_state(document_types) {
+ Object.keys(this.panel_manager.actions_filters).map((key) => {
+ let value = document_types.includes(key) ? 1 : 0;
+ this.panel_manager.actions_filters[key] = value;
+ })
+ }
+
+ render_data_table(vouchers) {
+ this.summary_data = {};
+ this.match_params = {};
+ let table_data = vouchers.map((row) => {
+ this.match_params[row.name] = {
+ "Reference No": row.reference_number_match || 0,
+ "Party": row.party_match || 0,
+ "Transaction Amount": row.amount_match || 0,
+ "Unallocated Amount": row.unallocated_amount_match || 0,
+ }
+ return [
+ this.help_button(row.name),
+ row.doctype,
+ row.reference_date || row.posting_date, // Reference Date
+ format_currency(row.paid_amount, row.currency),
+ row.reference_no || '',
+ row.party || '',
+ row.name
+ ];
+ });
+
+ const datatable_options = {
+ columns: this.get_data_table_columns(),
+ data: table_data,
+ dynamicRowHeight: true,
+ checkboxColumn: true,
+ inlineFilters: true,
+ };
+
+
+ this.actions_table = new frappe.DataTable(
+ this.match_field_group.get_field("vouchers").$wrapper[0],
+ datatable_options
+ );
+
+ // Highlight first row
+ this.actions_table.style.setStyle(
+ ".dt-cell[data-row-index='0']", {backgroundColor: '#F4FAEE'}
+ );
+
+ this.bind_row_check_event();
+ this.bind_help_button();
+ }
+
+ help_button(voucher_name) {
+ return `
+
+
+
+ `;
+ }
+
+ bind_row_check_event() {
+ // Resistant to row removal on being out of view in datatable
+ $(this.actions_table.bodyScrollable).on("click", ".dt-cell__content input", (e) => {
+ let idx = $(e.currentTarget).closest(".dt-cell").data().rowIndex;
+ let voucher_row = this.actions_table.getRows()[idx];
+
+ this.check_data_table_row(voucher_row)
+ })
+ }
+
+ bind_help_button() {
+ var me = this;
+ $(this.actions_table.bodyScrollable).on("mouseenter", ".match-reasons-btn", (e) => {
+ let $btn = $(e.currentTarget);
+ let voucher_name = $btn.data().name;
+ $btn.popover({
+ trigger: "manual",
+ placement: "top",
+ html: true,
+ content: () => {
+ return `
+
+
+ ${me.get_match_reasons(voucher_name)}
+
+ `;
+
+ }
+ });
+ $btn.popover("toggle");
+ });
+
+ $(this.actions_table.bodyScrollable).on("mouseleave", ".match-reasons-btn", (e) => {
+ let $btn = $(e.currentTarget);
+ $btn.popover("toggle");
+ });
+ }
+
+ get_match_reasons(voucher_name) {
+ let reasons = this.match_params[voucher_name], html = "";
+ for (let key in reasons) {
+ if (reasons[key]) {
+ html += `${__(key)}
`;
+ }
+ }
+ return html || __("No Specific Match Reasons");
+
+ }
+
+ check_data_table_row(row) {
+ if (!row) return;
+
+ let id = row[1].content;
+ let value = this.get_amount_from_row(row);
+
+ // If `id` in summary_data, remove it (row was unchecked), else add it
+ if (id in this.summary_data) {
+ delete this.summary_data[id];
+ } else {
+ this.summary_data[id] = value;
+ }
+
+ // Total of selected row amounts in summary_data
+ let total_allocated = Object.values(this.summary_data).reduce(
+ (a, b) => a + b, 0
+ );
+
+ // Deduct allocated amount from transaction's unallocated amount
+ // to show the final effect on reconciling
+ let transaction_amount = this.transaction.withdrawal || this.transaction.deposit;
+ let unallocated = flt(this.transaction.unallocated_amount) - flt(total_allocated);
+
+ this.render_transaction_amount_summary(
+ flt(transaction_amount), unallocated, this.transaction.currency,
+ );
+ }
+
+ render_transaction_amount_summary(total_amount, unallocated_amount, currency) {
+ let summary_field = this.match_field_group.get_field("transaction_amount_summary").$wrapper;
+ summary_field.empty();
+
+ let allocated_amount = flt(total_amount) - flt(unallocated_amount);
+
+ new erpnext.accounts.bank_reconciliation.SummaryCard({
+ $wrapper: summary_field,
+ values: {
+ "Amount": [total_amount],
+ "Allocated Amount": [allocated_amount],
+ "To Allocate": [
+ unallocated_amount,
+ (unallocated_amount < 0 ? "text-danger" : unallocated_amount > 0 ? "text-blue" : "text-success")
+ ]
+ },
+ currency: currency,
+ wrapper_class: "reconciliation-summary"
+ });
+ }
+
+ async get_matching_vouchers(document_types) {
+ let vouchers = await frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool_beta.bank_reconciliation_tool_beta.get_linked_payments",
+ args: {
+ bank_transaction_name: this.transaction.name,
+ document_types: document_types,
+ from_date: this.doc.bank_statement_from_date,
+ to_date: this.doc.bank_statement_to_date,
+ filter_by_reference_date: this.doc.filter_by_reference_date,
+ from_reference_date: this.doc.from_reference_date,
+ to_reference_date: this.doc.to_reference_date
+ },
+ }).then(result => result.message);
+ return vouchers || [];
+ }
+
+ update_bank_transaction() {
+ var me = this;
+ const reference_number = this.details_field_group.get_value("reference_number");
+ const party = this.details_field_group.get_value("party");
+ const party_type = this.details_field_group.get_value("party_type");
+
+ let diff = ["reference_number", "party", "party_type"].some(field => {
+ return me.details_field_group.get_value(field) !== me.transaction[field];
+ });
+ if (!diff) {
+ frappe.show_alert({message: __("No changes to update"), indicator: "yellow"});
+ return;
+ }
+
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.update_bank_transaction",
+ args: {
+ bank_transaction_name: me.transaction.name,
+ reference_number: reference_number,
+ party_type: party_type,
+ party: party,
+ },
+ freeze: true,
+ freeze_message: __("Updating ..."),
+ callback: (response) => {
+ if (response.exc) {
+ frappe.show_alert(__("Failed to update {0}", [me.transaction.name]));
+ return;
+ }
+
+ // Update transaction
+ me.panel_manager.refresh_transaction(
+ null, reference_number, party_type, party
+ );
+
+ frappe.show_alert(
+ __("Bank Transaction {0} updated", [me.transaction.name])
+ );
+ },
+ });
+ }
+
+ reconcile_selected_vouchers() {
+ var me = this;
+ let selected_vouchers = [];
+ let selected_map = this.actions_table.rowmanager.checkMap;
+ let voucher_rows = this.actions_table.getRows();
+
+ selected_map.forEach((value, idx) => {
+ if (value === 1) {
+ let row = voucher_rows[idx];
+ selected_vouchers.push({
+ payment_doctype: row[3].content,
+ payment_name: row[8].content,
+ amount: this.get_amount_from_row(row),
+ });
+ }
+ });
+
+ if (!selected_vouchers.length > 0) {
+ frappe.show_alert({
+ message: __("Please select at least one voucher to reconcile"),
+ indicator: "red"
+ });
+ return;
+ }
+
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.reconcile_vouchers",
+ args: {
+ bank_transaction_name: this.transaction.name,
+ vouchers: selected_vouchers,
+ },
+ freeze: true,
+ freeze_message: __("Reconciling ..."),
+ callback: (response) => {
+ if (response.exc) {
+ frappe.show_alert({
+ message: __("Failed to reconcile {0}", [this.transaction.name]),
+ indicator: "red"
+ });
+ return;
+ }
+
+ me.after_transaction_reconcile(response.message, false);
+ },
+ });
+ }
+
+ create_voucher() {
+ var me = this;
+ let values = this.create_field_group.get_values();
+ let document_type = values.document_type;
+
+ // Create new voucher and delete or refresh current BT row depending on reconciliation
+ this.create_voucher_bts(
+ null,
+ (message) => me.after_transaction_reconcile(message, true, document_type)
+ )
+ }
+
+ edit_in_full_page() {
+ this.create_voucher_bts(true, (message) => {
+ const doc = frappe.model.sync(message);
+ frappe.open_in_new_tab = true;
+ frappe.set_route("Form", doc[0].doctype, doc[0].name);
+ });
+ }
+
+ create_voucher_bts(allow_edit=false, success_callback) {
+ // Create PE or JV and run `success_callback`
+ let values = this.create_field_group.get_values();
+ let document_type = values.document_type;
+ let method = "erpnext.accounts.doctype.bank_reconciliation_tool_beta.bank_reconciliation_tool_beta";
+ let args = {
+ bank_transaction_name: this.transaction.name,
+ reference_number: values.reference_number,
+ reference_date: values.reference_date,
+ party_type: values.party_type,
+ party: values.party,
+ posting_date: values.posting_date,
+ mode_of_payment: values.mode_of_payment,
+ allow_edit: allow_edit
+ };
+
+ if (document_type === "Payment Entry") {
+ method = method + ".create_payment_entry_bts";
+ args = {
+ ...args,
+ project: values.project,
+ cost_center: values.cost_center
+ }
+ } else {
+ method = method + ".create_journal_entry_bts";
+ args = {
+ ...args,
+ entry_type: values.journal_entry_type,
+ second_account: values.second_account,
+ }
+ }
+
+ frappe.call({
+ method: method,
+ args: args,
+ callback: (response) => {
+ if (response.exc) {
+ frappe.show_alert({
+ message: __("Failed to create {0} against {1}", [document_type, this.transaction.name]),
+ indicator: "red"
+ });
+ return;
+ } else if (response.message) {
+ success_callback(response.message);
+ }
+ }
+ })
+
+ }
+
+ after_transaction_reconcile(message, with_new_voucher=false, document_type) {
+ // Actions after a transaction is matched with a voucher
+ // `with_new_voucher`: If a new voucher was created and reconciled with the transaction
+ let doc = message;
+ let unallocated_amount = flt(doc.unallocated_amount);
+ if (unallocated_amount > 0) {
+ // if partial update this.transaction, re-click on list row
+ frappe.show_alert({
+ message: __(
+ "Bank Transaction {0} Partially {1}",
+ [this.transaction.name, with_new_voucher ? "Reconciled" : "Matched"]
+ ),
+ indicator: "blue"
+ });
+ this.panel_manager.refresh_transaction(unallocated_amount);
+ } else {
+ let alert_string = __("Bank Transaction {0} Matched", [this.transaction.name])
+ if (with_new_voucher) {
+ alert_string = __("Bank Transaction {0} reconciled with a new {1}", [this.transaction.name, document_type]);
+ }
+ frappe.show_alert({message: alert_string, indicator: "green"});
+ this.panel_manager.move_to_next_transaction();
+ }
+ }
+
+ get_amount_from_row(row) {
+ let value = row[5].content;
+ return flt(value.split(" ") ? value.split(" ")[1] : 0);
+ }
+
+ get_match_tab_fields() {
+ const filters_state = this.panel_manager.actions_filters;
+ return [
+ {
+ label: __("Payment Entry"),
+ fieldname: "payment_entry",
+ fieldtype: "Check",
+ default: filters_state.payment_entry,
+ onchange: () => {
+ this.populate_matching_vouchers();
+ }
+ },
+ {
+ label: __("Journal Entry"),
+ fieldname: "journal_entry",
+ fieldtype: "Check",
+ default: filters_state.journal_entry,
+ onchange: () => {
+ this.populate_matching_vouchers();
+ }
+ },
+ {
+ fieldtype: "Column Break"
+ },
+ {
+ label: __("Purchase Invoice"),
+ fieldname: "purchase_invoice",
+ fieldtype: "Check",
+ default: filters_state.purchase_invoice,
+ onchange: () => {
+ let value = this.match_field_group.get_value("purchase_invoice");
+ this.match_field_group.get_field("unpaid_invoices").df.hidden = !value;
+ this.match_field_group.refresh();
+
+ this.populate_matching_vouchers();
+ }
+ },
+ {
+ label: __("Sales Invoice"),
+ fieldname: "sales_invoice",
+ fieldtype: "Check",
+ default: filters_state.sales_invoice,
+ onchange: () => {
+ let value = this.match_field_group.get_value("sales_invoice");
+ this.match_field_group.get_field("unpaid_invoices").df.hidden = !value;
+ this.match_field_group.refresh();
+
+ this.populate_matching_vouchers();
+ }
+ },
+ {
+ fieldtype: "Column Break"
+ },
+ {
+ label: __("Loan Repayment"),
+ fieldname: "loan_repayment",
+ fieldtype: "Check",
+ default: filters_state.loan_repayment,
+ onchange: () => {
+ this.populate_matching_vouchers();
+ }
+ },
+ {
+ label: __("Loan Disbursement"),
+ fieldname: "loan_disbursement",
+ fieldtype: "Check",
+ default: filters_state.loan_disbursement,
+ onchange: () => {
+ this.populate_matching_vouchers();
+ }
+ },
+ {
+ fieldtype: "Column Break"
+ },
+ {
+ label: __("Expense Claim"),
+ fieldname: "expense_claim",
+ fieldtype: "Check",
+ default: filters_state.expense_claim,
+ onchange: () => {
+ this.populate_matching_vouchers();
+ }
+ },
+ {
+ label: __("Bank Transaction"),
+ fieldname: "bank_transaction",
+ fieldtype: "Check",
+ default: filters_state.bank_transaction,
+ onchange: () => {
+ this.populate_matching_vouchers();
+ }
+ },
+ {
+ fieldtype: "Section Break"
+ },
+ {
+ label: __("Show Exact Amount"),
+ fieldname: "exact_match",
+ fieldtype: "Check",
+ default: filters_state.exact_match,
+ onchange: () => {
+ this.populate_matching_vouchers();
+ }
+ },
+ {
+ fieldtype: "Column Break"
+ },
+ {
+ label: __("Show Exact Party"),
+ fieldname: "exact_party_match",
+ fieldtype: "Check",
+ default: filters_state.exact_party_match,
+ onchange: () => {
+ this.populate_matching_vouchers();
+ },
+ read_only: !Boolean(this.transaction.party_type && this.transaction.party)
+ },
+ {
+ fieldtype: "Column Break"
+ },
+ {
+ label: __("Unpaid Invoices"),
+ fieldname: "unpaid_invoices",
+ fieldtype: "Check",
+ default: filters_state.unpaid_invoices,
+ onchange: () => {
+ this.populate_matching_vouchers();
+ },
+ hidden: (filters_state.sales_invoice || filters_state.purchase_invoice) ? 0 : 1
+ },
+ {
+ fieldtype: "Column Break"
+ },
+ {
+ fieldtype: "Section Break"
+ },
+ {
+ fieldname: "transaction_amount_summary",
+ fieldtype: "HTML",
+ },
+ {
+ fieldname: "vouchers",
+ fieldtype: "HTML",
+ },
+ {
+ fieldtype: "Section Break",
+ fieldname: "section_break_reconcile",
+ hide_border: 1,
+ },
+ {
+ label: __("Hidden field for alignment"),
+ fieldname: "hidden_field_2",
+ fieldtype: "Data",
+ hidden: 1
+ },
+ {
+ fieldtype: "Column Break"
+ },
+ {
+ label: __("Reconcile"),
+ fieldname: "bt_reconcile",
+ fieldtype: "Button",
+ primary: true,
+ click: () => {
+ this.reconcile_selected_vouchers();
+ }
+ }
+ ];
+ }
+
+ get_detail_tab_fields() {
+ return [
+ {
+ label: __("ID"),
+ fieldname: "name",
+ fieldtype: "Link",
+ options: "Bank Transaction",
+ default: this.transaction.name,
+ read_only: 1,
+ },
+ {
+ label: __("Date"),
+ fieldname: "date",
+ fieldtype: "Date",
+ default: this.transaction.date,
+ read_only: 1,
+ },
+ {
+ label: __("Deposit"),
+ fieldname: "deposit",
+ fieldtype: "Currency",
+ default: this.transaction.deposit,
+ read_only: 1,
+ },
+ {
+ label: __("Withdrawal"),
+ fieldname: "withdrawal",
+ fieldtype: "Currency",
+ default: this.transaction.withdrawal,
+ read_only: 1,
+ },
+ {
+ fieldtype: "Column Break"
+ },
+ {
+ label: __("Description"),
+ fieldname: "description",
+ fieldtype: "Small Text",
+ default: this.transaction.description,
+ read_only: 1,
+ },
+ {
+ label: __("To Allocate"),
+ fieldname: "unallocated_amount",
+ fieldtype: "Currency",
+ options: "account_currency",
+ default: this.transaction.unallocated_amount,
+ read_only: 1,
+ },
+ {
+ label: __("Currency"),
+ fieldname: "account_currency",
+ fieldtype: "Link",
+ options: "Currency",
+ read_only: 1,
+ default: this.transaction.currency,
+ hidden: 1,
+ },
+ {
+ label: __("Account Holder"),
+ fieldname: "account",
+ fieldtype: "Data",
+ default: this.transaction.bank_party_name,
+ read_only: 1,
+ hidden: this.transaction.bank_party_name ? 0 : 1,
+ },
+ {
+ label: __("Party Account Number"),
+ fieldname: "account_number",
+ fieldtype: "Data",
+ default: this.transaction.bank_party_account_number,
+ read_only: 1,
+ hidden: this.transaction.bank_party_account_number ? 0 : 1,
+ },
+ {
+ label: __("Party IBAN"),
+ fieldname: "iban",
+ fieldtype: "Data",
+ default: this.transaction.bank_party_iban,
+ read_only: 1,
+ hidden: this.transaction.bank_party_iban ? 0 : 1,
+ },
+ {
+ label: __("Update"),
+ fieldtype: "Section Break",
+ fieldname: "update_section",
+ },
+ {
+ label: __("Reference Number"),
+ fieldname: "reference_number",
+ fieldtype: "Data",
+ default: this.transaction.reference_number,
+ },
+ {
+ fieldtype: "Column Break"
+ },
+ {
+ label: __("Party Type"),
+ fieldname: "party_type",
+ fieldtype: "Link",
+ options: "DocType",
+ get_query: function () {
+ return {
+ filters: {
+ name: [
+ "in", Object.keys(frappe.boot.party_account_types),
+ ],
+ },
+ };
+ },
+ onchange: () => {
+ let value = this.details_field_group.get_value("party_type");
+ this.details_field_group.get_field("party").df.options = value;
+ },
+ default: this.transaction.party_type || null,
+ },
+ {
+ label: __("Party"),
+ fieldname: "party",
+ fieldtype: "Link",
+ default: this.transaction.party,
+ options: this.transaction.party_type || null,
+ },
+ {
+ fieldtype: "Section Break"
+ },
+ {
+ label: __("Hidden field for alignment"),
+ fieldname: "hidden_field",
+ fieldtype: "Data",
+ hidden: 1
+ },
+ {
+ fieldtype: "Column Break"
+ },
+ {
+ label: __("Submit"),
+ fieldname: "submit_transaction",
+ fieldtype: "Button",
+ primary: true,
+ click: () => this.update_bank_transaction(),
+ }
+ ];
+ }
+
+ get_create_tab_fields() {
+ let party_type = this.transaction.party_type || (flt(this.transaction.withdrawal) > 0 ? "Supplier" : "Customer");
+ return [
+ {
+ label: __("Document Type"),
+ fieldname: "document_type",
+ fieldtype: "Select",
+ options: `Payment Entry\nJournal Entry`,
+ default: "Payment Entry",
+ onchange: () => {
+ let value = this.create_field_group.get_value("document_type");
+ let fields = this.create_field_group;
+
+ fields.get_field("project").df.hidden = value === "Journal Entry";
+ fields.get_field("cost_center").df.hidden = value === "Journal Entry";
+
+ fields.get_field("journal_entry_type").df.hidden = value === "Payment Entry";
+ fields.get_field("journal_entry_type").df.reqd = value === "Journal Entry";
+ fields.get_field("second_account").df.hidden = value === "Payment Entry";
+ fields.get_field("second_account").df.reqd = value === "Journal Entry";
+
+ this.create_field_group.refresh();
+ }
+ },
+ {
+ fieldtype: "Section Break",
+ fieldname: "details",
+ label: "Details",
+ },
+ {
+ fieldname: "reference_number",
+ fieldtype: "Data",
+ label: __("Reference Number"),
+ default: this.transaction.reference_number || this.transaction.description,
+ },
+ {
+ fieldname: "posting_date",
+ fieldtype: "Date",
+ label: __("Posting Date"),
+ reqd: 1,
+ default: this.transaction.date,
+ },
+ {
+ fieldname: "reference_date",
+ fieldtype: "Date",
+ label: __("Cheque/Reference Date"),
+ reqd: 1,
+ default: this.transaction.date,
+ },
+ {
+ fieldname: "mode_of_payment",
+ fieldtype: "Link",
+ label: __("Mode of Payment"),
+ options: "Mode of Payment",
+ },
+ {
+ fieldname: "edit_in_full_page",
+ fieldtype: "Button",
+ label: __("Edit in Full Page"),
+ click: () => {
+ this.edit_in_full_page();
+ },
+ },
+ {
+ fieldname: "column_break_7",
+ fieldtype: "Column Break",
+ },
+ {
+ label: __("Journal Entry Type"),
+ fieldname: "journal_entry_type",
+ fieldtype: "Select",
+ options:
+ `Bank Entry\nJournal Entry\nInter Company Journal Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nDeferred Revenue\nDeferred Expense`,
+ default: "Bank Entry",
+ hidden: 1,
+ },
+ {
+ fieldname: "second_account",
+ fieldtype: "Link",
+ label: "Account",
+ options: "Account",
+ get_query: () => {
+ return {
+ filters: {
+ is_group: 0,
+ company: this.doc.company,
+ },
+ };
+ },
+ hidden: 1,
+ },
+ {
+ fieldname: "party_type",
+ fieldtype: "Link",
+ label: "Party Type",
+ options: "DocType",
+ reqd: 1,
+ default: party_type,
+ get_query: function () {
+ return {
+ filters: {
+ name: [
+ "in",
+ Object.keys(frappe.boot.party_account_types),
+ ],
+ },
+ };
+ },
+ onchange: () => {
+ let value = this.create_field_group.get_value("party_type");
+ this.create_field_group.get_field("party").df.options = value;
+ }
+ },
+ {
+ fieldname: "party",
+ fieldtype: "Link",
+ label: "Party",
+ default: this.transaction.party,
+ options: party_type,
+ reqd: 1,
+ },
+ {
+ fieldname: "project",
+ fieldtype: "Link",
+ label: "Project",
+ options: "Project",
+ },
+ {
+ fieldname: "cost_center",
+ fieldtype: "Link",
+ label: "Cost Center",
+ options: "Cost Center",
+ },
+ {
+ fieldtype: "Section Break"
+ },
+ {
+ label: __("Hidden field for alignment"),
+ fieldname: "hidden_field",
+ fieldtype: "Data",
+ hidden: 1
+ },
+ {
+ fieldtype: "Column Break"
+ },
+ {
+ label: __("Create"),
+ fieldtype: "Button",
+ primary: true,
+ click: () => this.create_voucher(),
+ }
+ ];
+ }
+
+ get_data_table_columns() {
+ return [
+ {
+ name: __("Reason"),
+ editable: false,
+ width: 50,
+ },
+ {
+ name: __("Document Type"),
+ editable: false,
+ width: 100,
+ },
+ {
+ name: __("Reference Date"),
+ editable: false,
+ width: 120,
+ },
+ {
+ name: __("Remaining"),
+ editable: false,
+ width: 100,
+ },
+ {
+ name: __("Reference Number"),
+ editable: false,
+ width: 200,
+ },
+ {
+ name: __("Party"),
+ editable: false,
+ width: 100,
+ },
+ {
+ name: __("Document Name"),
+ editable: false,
+ width: 100,
+ },
+ ];
+ }
+}
\ No newline at end of file
diff --git a/erpnext/public/js/bank_reconciliation_tool_beta/panel_manager.js b/erpnext/public/js/bank_reconciliation_tool_beta/panel_manager.js
new file mode 100644
index 00000000000..bb762e6e1a0
--- /dev/null
+++ b/erpnext/public/js/bank_reconciliation_tool_beta/panel_manager.js
@@ -0,0 +1,247 @@
+frappe.provide("erpnext.accounts.bank_reconciliation");
+
+erpnext.accounts.bank_reconciliation.PanelManager = class PanelManager {
+ constructor(opts) {
+ Object.assign(this, opts);
+ this.make();
+ }
+
+ make() {
+ this.init_panels();
+ }
+
+ async init_panels() {
+ this.transactions = await this.get_bank_transactions();
+
+ this.$wrapper.empty();
+ this.$panel_wrapper = this.$wrapper.append(`
+
+ `).find(".panel-container");
+
+ this.render_panels()
+ }
+
+ async get_bank_transactions() {
+ let transactions = await frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool_beta.bank_reconciliation_tool_beta.get_bank_transactions",
+ args: {
+ bank_account: this.doc.bank_account,
+ from_date: this.doc.bank_statement_from_date,
+ to_date: this.doc.bank_statement_to_date,
+ order_by: this.order || "date asc",
+ },
+ freeze: true,
+ freeze_message: __("Fetching Bank Transactions"),
+ }).then(response => response.message);
+ return transactions;
+ }
+
+ render_panels() {
+ this.set_actions_panel_default_states();
+
+ if (!this.transactions || !this.transactions.length) {
+ this.render_no_transactions();
+ } else {
+ this.render_list_panel();
+
+ let first_transaction = this.transactions[0];
+ this.$list_container.find("#" + first_transaction.name).click();
+ }
+ }
+
+ set_actions_panel_default_states() {
+ // Init actions panel states to store for persistent views
+ this.actions_tab = "match_voucher-tab";
+ this.actions_filters = {
+ payment_entry: 1,
+ journal_entry: 1,
+ purchase_invoice: 0,
+ sales_invoice: 0,
+ loan_repayment: 0,
+ loan_disbursement: 0,
+ expense_claim: 0,
+ bank_transaction: 0,
+ exact_match: 0,
+ exact_party_match: 0,
+ unpaid_invoices: 0
+ }
+ }
+
+ render_no_transactions() {
+ this.$panel_wrapper.empty();
+ this.$panel_wrapper.append(`
+
+

+
${__("No Transactions found for the current filters.")}
+
+ `);
+ }
+
+ render_list_panel() {
+ this.$panel_wrapper.append(`
+
+ `);
+
+ this.render_sort_area();
+ this.render_transactions_list();
+ }
+
+ render_actions_panel() {
+ this.actions_panel = new erpnext.accounts.bank_reconciliation.ActionsPanel({
+ $wrapper: this.$panel_wrapper,
+ transaction: this.active_transaction,
+ doc: this.doc,
+ panel_manager: this
+ });
+ }
+
+ render_sort_area() {
+ this.$sort_area = this.$panel_wrapper.find(".sort-by");
+ this.$sort_area.append(`
+ ${__("Sort By")}
+
+ `);
+
+ var me = this;
+ new frappe.ui.SortSelector({
+ parent: me.$sort_area.find(".sort-by-selector"),
+ args: {
+ sort_by: me.order_by || "date",
+ sort_order: me.order_direction || "asc",
+ options: [
+ {fieldname: "date", label: __("Date")},
+ {fieldname: "withdrawal", label: __("Withdrawal")},
+ {fieldname: "deposit", label: __("Deposit")},
+ {fieldname: "unallocated_amount", label: __("Unallocated Amount")}
+ ]
+ },
+ change: function(sort_by, sort_order) {
+ // Globally set the order used in the re-rendering of the list
+ me.order_by = (sort_by || me.order_by || "date");
+ me.order_direction = (sort_order || me.order_direction || "asc");
+ me.order = me.order_by + " " + me.order_direction;
+
+ // Re-render the list
+ me.init_panels();
+ }
+ });
+ }
+
+ render_transactions_list() {
+ this.$list_container = this.$panel_wrapper.find(".list-container");
+
+ this.transactions.map(transaction => {
+ let amount = transaction.deposit || transaction.withdrawal;
+ let symbol = transaction.withdrawal ? "-" : "+";
+
+ let $row = this.$list_container.append(`
+
+
+
+
+ ${frappe.format(transaction.date, {fieldtype: "Date"})}
+
+
+
+
+ ${symbol} ${format_currency(amount, transaction.currency)}
+
+
+
+
+
+
+
+ ${transaction.bank_party_name}
+
+
+
+ ${transaction.description}
+
+
+
+ ${transaction.reference_number}
+
+
+ `).find("#" + transaction.name);
+
+ $row.on("click", () => {
+ $row.addClass("active").siblings().removeClass("active");
+
+ // this.transaction's objects get updated, we want the latest values
+ this.active_transaction = this.transactions.find(({name}) => name === transaction.name);
+ this.render_actions_panel();
+ })
+ })
+ }
+
+ refresh_transaction(updated_amount=null, reference_number=null, party_type=null, party=null) {
+ // Update the transaction object's unallocated_amount **OR** other details
+ let id = this.active_transaction.name;
+ let current_index = this.transactions.findIndex(({name}) => name === id);
+
+ let $current_transaction = this.$list_container.find("#" + id);
+ let transaction = this.transactions[current_index];
+
+ if (updated_amount) {
+ // update amount is > 0 always [src: `after_transaction_reconcile()`]
+ this.transactions[current_index]["unallocated_amount"] = updated_amount;
+ } else {
+ this.transactions[current_index] = {
+ ...transaction,
+ reference_number: reference_number,
+ party_type: party_type,
+ party: party
+ };
+ // Update Reference Number in List
+ $current_transaction.find(".reference").removeClass("hide");
+ $current_transaction.find(".reference-value").text(reference_number || "--");
+ }
+
+ $current_transaction.click();
+ }
+
+ move_to_next_transaction() {
+ let id = this.active_transaction.name;
+ let $current_transaction = this.$list_container.find("#" + id);
+ let current_index = this.transactions.findIndex(({name}) => name === id);
+
+ let next_transaction = this.transactions[current_index + 1];
+ let previous_transaction = this.transactions[current_index - 1];
+
+ if (next_transaction) {
+ this.active_transaction = next_transaction;
+ let $next_transaction = $current_transaction.next();
+ $next_transaction.click();
+ } else if (previous_transaction) {
+ this.active_transaction = previous_transaction;
+ let $previous_transaction = $current_transaction.prev();
+ $previous_transaction.click();
+ }
+
+ this.transactions.splice(current_index, 1);
+ $current_transaction.remove();
+
+ if (!next_transaction && !previous_transaction) {
+ this.active_transaction = null;
+ this.render_no_transactions();
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/erpnext/public/js/bank_reconciliation_tool_beta/summary_number_card.js b/erpnext/public/js/bank_reconciliation_tool_beta/summary_number_card.js
new file mode 100644
index 00000000000..6a161f0895b
--- /dev/null
+++ b/erpnext/public/js/bank_reconciliation_tool_beta/summary_number_card.js
@@ -0,0 +1,50 @@
+frappe.provide("erpnext.accounts.bank_reconciliation");
+
+erpnext.accounts.bank_reconciliation.SummaryCard = class SummaryCard {
+ // {
+ // $wrapper : $wrapper,
+ // values: {
+ // "Amount": [120, "text-blue"],
+ // "Unallocated Amount": [200]
+ // },
+ // wrapper_class: "custom-style",
+ // currency: "USD"
+ // }
+
+ constructor(opts) {
+ Object.assign(this, opts);
+ this.make();
+ }
+
+ make() {
+ this.$wrapper.empty();
+ let $container = null;
+
+ if (this.$wrapper.find(".report-summary").length > 0) {
+ $container = this.$wrapper.find(".report-summary");
+ $container.empty();
+ } else {
+ $container = this.$wrapper.append(
+ ``
+ ).find(".report-summary");
+ }
+
+ Object.keys(this.values).map((key) => {
+ let values = this.values[key];
+ let data = {
+ value: values[0],
+ label: __(key),
+ datatype: "Currency",
+ currency: this.currency,
+ }
+
+ let number_card = frappe.utils.build_summary_item(data);
+ $container.append(number_card);
+
+ if (values.length > 1) {
+ let $text = number_card.find(".summary-value");
+ $text.addClass(values[1]);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/erpnext/public/scss/bank_reconciliation_tool_beta.scss b/erpnext/public/scss/bank_reconciliation_tool_beta.scss
new file mode 100644
index 00000000000..93c37e52351
--- /dev/null
+++ b/erpnext/public/scss/bank_reconciliation_tool_beta.scss
@@ -0,0 +1,166 @@
+.p-10 {
+ padding: 10px;
+}
+
+.list-panel {
+ display: flex;
+ flex-direction: column;
+ width: 40%;
+ height: 100vh;
+ border: 1px solid var(--gray-200);
+
+ > .sort-by {
+ display:flex;
+ justify-content: flex-start;
+ align-items: center;
+ border-bottom: 1px solid var(--gray-200);
+ cursor: pointer;
+
+ > .sort-by-title {
+ padding: 10px 0 10px 10px;
+ color: var(--text-muted);
+ }
+ }
+
+ > .list-container {
+ height: -webkit-fill-available;
+ overflow-y: scroll;
+
+ > .transaction-row {
+ cursor: pointer;
+ border-bottom: 1px solid var(--gray-200);
+
+ &.active {
+ border-left: 6px solid var(--blue-500);
+ }
+
+ > div {
+ padding: 4px 10px;
+
+ > .bt-label {
+ color: var(--gray-500);
+ }
+ }
+
+ * .reference-value, * .account-holder-value {
+ font-weight: 600;
+ }
+
+ }
+ }
+}
+
+.bt-amount-contianer {
+ text-align: end;
+
+ > .bt-amount {
+ font-size: var(--text-base);
+ }
+}
+
+.actions-panel {
+ display: flex;
+ flex-direction: column;
+ width: 60%;
+ height: 100vh;
+ border: 1px solid var(--gray-200);
+ overflow-y: scroll;
+
+ > .tab-content {
+ height: -webkit-fill-available;
+
+ * .frappe-control[data-fieldname="submit_transaction"],
+ * .btn-primary[data-fieldname="bt_reconcile"],
+ * .btn-primary[data-fieldname="create"] {
+ float: right;
+ }
+
+ * .dt-scrollable {
+ height: calc(100vh - 550px) !important;
+ }
+
+ * .dt-toast {
+ display: none !important;
+ }
+ }
+}
+
+.nav-actions-link {
+ display: block;
+ padding: var(--padding-md) 0;
+ margin: 0 var(--margin-md);
+ color: var(--text-muted);
+
+ &.active {
+ font-weight: 600;
+ border-bottom: 1px solid var(--primary);
+ color: var(--text-color);
+ }
+
+ &:hover {
+ text-decoration: none;
+ }
+}
+
+.report-summary {
+ margin: .5rem 0 calc(var(--margin-sm) + 1rem) 0 !important;
+}
+
+.reconciliation-summary {
+ gap: 0 !important;
+
+ > .summary-item {
+ > .summary-label {
+ font-size: var(--text-base);
+ }
+
+ > .summary-value {
+ font-weight: 600;
+ font-size: 16px;
+ }
+
+ }
+}
+
+.text-blue {
+ color: var(--blue-500) !important;
+}
+
+.bank-reco-beta-empty-state {
+ display: flex;
+ flex-direction: column;
+ min-height: 30vh;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+ font-size: 14px;
+ color: var(--gray-600);
+
+ > .btn-primary {
+ padding: 0.5rem 1rem !important;
+ }
+}
+
+.no-transactions {
+ display: flex;
+ flex-direction: column;
+ min-height: 30vh;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+ font-size: 14px;
+ width: 100%;
+ color: var(--gray-600);
+
+ > img {
+ margin-bottom: var(--margin-md);
+ max-height: 70px;
+ }
+}
+
+.match-popover-header {
+ font-size: var(--text-base);
+ color: var(--primary);
+ font-weight: 500;
+ margin-bottom: .5rem;
+}
diff --git a/erpnext/public/scss/erpnext.bundle.scss b/erpnext/public/scss/erpnext.bundle.scss
index d3313c7cee2..a74efa660de 100644
--- a/erpnext/public/scss/erpnext.bundle.scss
+++ b/erpnext/public/scss/erpnext.bundle.scss
@@ -1,3 +1,4 @@
@import "./erpnext";
@import "./call_popup";
@import "./point-of-sale";
+@import "./bank_reconciliation_tool_beta";