Compare commits
6 Commits
gle_indexi
...
bank-reco-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25ca234c5a | ||
|
|
7a9a867079 | ||
|
|
5133c57e22 | ||
|
|
1cac2e0d12 | ||
|
|
0b22bdefaa | ||
|
|
64b2d2ba52 |
@@ -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_beta.bank_reconciliation_tool_beta.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),
|
||||
);
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setup_empty_state: function(frm) {
|
||||
frm.$reconciliation_area.empty();
|
||||
let empty_area = frm.$reconciliation_area.append(`
|
||||
<div class="bank-reco-beta-empty-state">
|
||||
<p>
|
||||
${__("Set Filters and Get Bank Transactions")}
|
||||
</p>
|
||||
<p>${__("Or")}</p>
|
||||
</div>
|
||||
`).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),
|
||||
)
|
||||
},
|
||||
|
||||
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,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"depends_on": "bank_account",
|
||||
"fieldname": "account_opening_balance",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Account Opening Balance",
|
||||
"options": "account_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "bank_account",
|
||||
"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-31 16:18:36.244114",
|
||||
"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": []
|
||||
}
|
||||
@@ -0,0 +1,859 @@
|
||||
# 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, flt
|
||||
from pypika.terms import Parameter
|
||||
|
||||
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 auto_reconcile_vouchers(
|
||||
bank_account,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
filter_by_reference_date=None,
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
):
|
||||
# Auto reconcile vouchers with matching reference numbers
|
||||
frappe.flags.auto_reconcile_vouchers = True
|
||||
reconciled, partially_reconciled = set(), set()
|
||||
|
||||
bank_transactions = get_bank_transactions(bank_account, from_date, to_date)
|
||||
for transaction in bank_transactions:
|
||||
linked_payments = get_linked_payments(
|
||||
transaction.name,
|
||||
["payment_entry", "journal_entry"],
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
)
|
||||
|
||||
if not linked_payments:
|
||||
continue
|
||||
|
||||
vouchers = list(
|
||||
map(
|
||||
lambda entry: {
|
||||
"payment_doctype": entry.get("doctype"),
|
||||
"payment_name": entry.get("name"),
|
||||
"amount": entry.get("paid_amount"),
|
||||
},
|
||||
linked_payments,
|
||||
)
|
||||
)
|
||||
|
||||
unallocated_before = transaction.unallocated_amount
|
||||
transaction = reconcile_vouchers(transaction.name, json.dumps(vouchers))
|
||||
|
||||
if transaction.status == "Reconciled":
|
||||
reconciled.add(transaction.name)
|
||||
elif flt(unallocated_before) != flt(transaction.unallocated_amount):
|
||||
partially_reconciled.add(transaction.name) # Partially reconciled
|
||||
|
||||
alert_message, indicator = "", "blue"
|
||||
if not partially_reconciled and not reconciled:
|
||||
alert_message = _("No matches occurred via auto reconciliation")
|
||||
|
||||
if reconciled:
|
||||
alert_message += _("{0} Transaction(s) Reconciled").format(len(reconciled))
|
||||
alert_message += "<br>"
|
||||
indicator = "green"
|
||||
|
||||
if partially_reconciled:
|
||||
alert_message += _("{0} {1} Partially Reconciled").format(
|
||||
len(partially_reconciled),
|
||||
_("Transactions") if len(partially_reconciled) > 1 else _("Transaction"),
|
||||
)
|
||||
indicator = "green"
|
||||
|
||||
frappe.msgprint(title=_("Auto Reconciliation"), msg=alert_message, indicator=indicator)
|
||||
frappe.flags.auto_reconcile_vouchers = False
|
||||
return reconciled, partially_reconciled
|
||||
|
||||
|
||||
@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)
|
||||
gl_account, company = frappe.db.get_value(
|
||||
"Bank Account", transaction.bank_account, ["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.get("doctype"), voucher.get("name"))
|
||||
amount = None
|
||||
for row in rows:
|
||||
if row["gl_account"] == gl_account:
|
||||
amount = row["total"]
|
||||
break
|
||||
|
||||
if amount:
|
||||
voucher["paid_amount"] -= amount
|
||||
|
||||
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
|
||||
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_voucher_methods = frappe.get_hooks("get_matching_vouchers_for_bank_reconciliation")
|
||||
matching_voucher_methods.append(
|
||||
"erpnext.accounts.doctype.bank_reconciliation_tool_beta.bank_reconciliation_tool_beta.get_matching_vouchers_for_bank_reconciliation"
|
||||
)
|
||||
|
||||
for method_name in matching_voucher_methods[1:]:
|
||||
# get matching vouchers from all the apps (except erpnext, to override)
|
||||
matching_vouchers.extend(
|
||||
frappe.get_attr(method_name)(
|
||||
bank_account,
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
filters,
|
||||
)
|
||||
)
|
||||
|
||||
if not matching_vouchers:
|
||||
return []
|
||||
|
||||
for voucher in matching_vouchers:
|
||||
# higher rank if voucher name is in bank transaction
|
||||
if voucher["name"] in transaction.description:
|
||||
voucher["rank"] += 1
|
||||
voucher["name_in_desc_match"] = 1
|
||||
|
||||
return sorted(matching_vouchers, key=lambda x: x["rank"], reverse=True)
|
||||
|
||||
|
||||
def get_matching_vouchers_for_bank_reconciliation(
|
||||
bank_account,
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
filters,
|
||||
):
|
||||
# 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)
|
||||
matching_queries_method = frappe.get_hooks("get_matching_queries")
|
||||
matching_queries_method.append(
|
||||
"erpnext.accounts.doctype.bank_reconciliation_tool_beta.bank_reconciliation_tool_beta.get_matching_queries"
|
||||
)
|
||||
|
||||
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 []
|
||||
)
|
||||
|
||||
vouchers = []
|
||||
for query in queries:
|
||||
vouchers.extend(
|
||||
frappe.db.sql(
|
||||
query,
|
||||
filters,
|
||||
as_dict=1,
|
||||
)
|
||||
)
|
||||
|
||||
return vouchers
|
||||
|
||||
|
||||
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, currency)
|
||||
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, currency)
|
||||
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_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
|
||||
bt = frappe.qb.DocType("Bank Transaction")
|
||||
field = "deposit" if transaction.withdrawal > 0.0 else "withdrawal"
|
||||
|
||||
ref_rank = (
|
||||
frappe.qb.terms.Case().when(bt.reference_number == transaction.reference_number, 1).else_(0)
|
||||
)
|
||||
unallocated_rank = (
|
||||
frappe.qb.terms.Case().when(bt.unallocated_amount == transaction.unallocated_amount, 1).else_(0)
|
||||
)
|
||||
|
||||
amount_equality = getattr(bt, field) == transaction.unallocated_amount
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
|
||||
party_condition = (
|
||||
(bt.party_type == transaction.party_type)
|
||||
& (bt.party == transaction.party)
|
||||
& bt.party.isnotnull()
|
||||
)
|
||||
party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
|
||||
amount_condition = amount_equality if exact_match else getattr(bt, field) > 0.0
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(bt)
|
||||
.select(
|
||||
(ref_rank + amount_rank + party_rank + unallocated_rank + 1).as_("rank"),
|
||||
ConstantColumn("Bank Transaction").as_("doctype"),
|
||||
bt.name,
|
||||
bt.unallocated_amount.as_("paid_amount"),
|
||||
bt.reference_number.as_("reference_no"),
|
||||
bt.date.as_("reference_date"),
|
||||
bt.party,
|
||||
bt.party_type,
|
||||
bt.date.as_("posting_date"),
|
||||
bt.currency,
|
||||
ref_rank.as_("reference_number_match"),
|
||||
amount_rank.as_("amount_match"),
|
||||
party_rank.as_("party_match"),
|
||||
unallocated_rank.as_("unallocated_amount_match"),
|
||||
)
|
||||
.where(bt.status != "Reconciled")
|
||||
.where(bt.name != transaction.name)
|
||||
.where(bt.bank_account == transaction.bank_account)
|
||||
.where(amount_condition)
|
||||
.where(bt.docstatus == 1)
|
||||
)
|
||||
|
||||
if exact_party_match:
|
||||
query = query.where(party_condition)
|
||||
|
||||
return str(query)
|
||||
|
||||
|
||||
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,
|
||||
):
|
||||
to_from = "to" if transaction.deposit > 0.0 else "from"
|
||||
currency_field = f"paid_{to_from}_account_currency"
|
||||
payment_type = "Receive" if transaction.deposit > 0.0 else "Pay"
|
||||
pe = frappe.qb.DocType("Payment Entry")
|
||||
|
||||
ref_condition = pe.reference_no == transaction.reference_number
|
||||
ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
|
||||
|
||||
amount_equality = pe.paid_amount == transaction.unallocated_amount
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
amount_condition = amount_equality if exact_match else pe.paid_amount > 0.0
|
||||
|
||||
party_condition = (
|
||||
(pe.party_type == transaction.party_type)
|
||||
& (pe.party == transaction.party)
|
||||
& pe.party.isnotnull()
|
||||
)
|
||||
party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
|
||||
|
||||
filter_by_date = pe.posting_date.between(from_date, to_date)
|
||||
if cint(filter_by_reference_date):
|
||||
filter_by_date = pe.reference_date.between(from_reference_date, to_reference_date)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(pe)
|
||||
.select(
|
||||
(ref_rank + amount_rank + party_rank + 1).as_("rank"),
|
||||
ConstantColumn("Payment Entry").as_("doctype"),
|
||||
pe.name,
|
||||
pe.paid_amount,
|
||||
pe.reference_no,
|
||||
pe.reference_date,
|
||||
pe.party,
|
||||
pe.party_type,
|
||||
pe.posting_date,
|
||||
getattr(pe, currency_field).as_("currency"),
|
||||
ref_rank.as_("reference_number_match"),
|
||||
amount_rank.as_("amount_match"),
|
||||
party_rank.as_("party_match"),
|
||||
)
|
||||
.where(pe.docstatus == 1)
|
||||
.where(pe.payment_type.isin([payment_type, "Internal Transfer"]))
|
||||
.where(pe.clearance_date.isnull())
|
||||
.where(getattr(pe, account_from_to) == Parameter("%(bank_account)s"))
|
||||
.where(amount_condition)
|
||||
.where(filter_by_date)
|
||||
.orderby(pe.reference_date if cint(filter_by_reference_date) else pe.posting_date)
|
||||
)
|
||||
|
||||
if frappe.flags.auto_reconcile_vouchers == True:
|
||||
query = query.where(ref_condition)
|
||||
if exact_party_match:
|
||||
query = query.where(party_condition)
|
||||
|
||||
return str(query)
|
||||
|
||||
|
||||
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"
|
||||
je = frappe.qb.DocType("Journal Entry")
|
||||
jea = frappe.qb.DocType("Journal Entry Account")
|
||||
|
||||
ref_condition = je.cheque_no == transaction.reference_number
|
||||
ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
|
||||
|
||||
amount_field = f"{cr_or_dr}_in_account_currency"
|
||||
amount_equality = getattr(jea, amount_field) == transaction.unallocated_amount
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
|
||||
filter_by_date = je.posting_date.between(from_date, to_date)
|
||||
if cint(filter_by_reference_date):
|
||||
filter_by_date = je.cheque_date.between(from_reference_date, to_reference_date)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(jea)
|
||||
.join(je)
|
||||
.on(jea.parent == je.name)
|
||||
.select(
|
||||
(ref_rank + amount_rank + 1).as_("rank"),
|
||||
ConstantColumn("Journal Entry").as_("doctype"),
|
||||
je.name,
|
||||
getattr(jea, amount_field).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"),
|
||||
ref_rank.as_("reference_number_match"),
|
||||
amount_rank.as_("amount_match"),
|
||||
)
|
||||
.where(je.docstatus == 1)
|
||||
.where(je.voucher_type != "Opening Entry")
|
||||
.where(je.clearance_date.isnull())
|
||||
.where(jea.account == Parameter("%(bank_account)s"))
|
||||
.where(amount_equality if exact_match else getattr(jea, amount_field) > 0.0)
|
||||
.where(je.docstatus == 1)
|
||||
.where(filter_by_date)
|
||||
.orderby(je.cheque_date if cint(filter_by_reference_date) else je.posting_date)
|
||||
)
|
||||
|
||||
if frappe.flags.auto_reconcile_vouchers == True:
|
||||
query = query.where(ref_condition)
|
||||
|
||||
return str(query)
|
||||
|
||||
|
||||
def get_si_matching_query(exact_match, exact_party_match, currency):
|
||||
# get matching paid sales invoice query
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
sip = frappe.qb.DocType("Sales Invoice Payment")
|
||||
|
||||
amount_equality = sip.amount == Parameter("%(amount)s")
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
amount_condition = amount_equality if exact_match else sip.amount > 0.0
|
||||
|
||||
party_condition = si.customer == Parameter("%(party)s")
|
||||
party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(sip)
|
||||
.join(si)
|
||||
.on(sip.parent == si.name)
|
||||
.select(
|
||||
(party_rank + amount_rank + 1).as_("rank"),
|
||||
ConstantColumn("Sales Invoice").as_("doctype"),
|
||||
si.name,
|
||||
sip.amount.as_("paid_amount"),
|
||||
si.name.as_("reference_no"),
|
||||
ConstantColumn("").as_("reference_date"),
|
||||
si.customer.as_("party"),
|
||||
ConstantColumn("Customer").as_("party_type"),
|
||||
si.posting_date,
|
||||
si.currency,
|
||||
party_rank.as_("party_match"),
|
||||
amount_rank.as_("amount_match"),
|
||||
)
|
||||
.where(si.docstatus == 1)
|
||||
.where(sip.clearance_date.isnull())
|
||||
.where(sip.account == Parameter("%(bank_account)s"))
|
||||
.where(amount_condition)
|
||||
.where(si.currency == currency)
|
||||
)
|
||||
|
||||
if exact_party_match:
|
||||
query = query.where(party_condition)
|
||||
|
||||
return str(query)
|
||||
|
||||
|
||||
def get_unpaid_si_matching_query(exact_match, exact_party_match, currency):
|
||||
sales_invoice = frappe.qb.DocType("Sales Invoice")
|
||||
|
||||
party_condition = sales_invoice.customer == Parameter("%(party)s")
|
||||
party_match = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
|
||||
|
||||
grand_total_condition = sales_invoice.grand_total == Parameter("%(amount)s")
|
||||
amount_match = frappe.qb.terms.Case().when(grand_total_condition, 1).else_(0)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(sales_invoice)
|
||||
.select(
|
||||
(party_match + amount_match + 1).as_("rank"),
|
||||
ConstantColumn("Sales Invoice").as_("doctype"),
|
||||
sales_invoice.name.as_("name"),
|
||||
sales_invoice.outstanding_amount.as_("paid_amount"),
|
||||
sales_invoice.name.as_("reference_no"),
|
||||
ConstantColumn("").as_("reference_date"),
|
||||
sales_invoice.customer.as_("party"),
|
||||
ConstantColumn("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(grand_total_condition)
|
||||
|
||||
if exact_party_match:
|
||||
query = query.where(party_condition)
|
||||
|
||||
return str(query)
|
||||
|
||||
|
||||
def get_pi_matching_query(exact_match, exact_party_match, currency):
|
||||
# get matching purchase invoice query when they are also used as payment entries (is_paid)
|
||||
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
|
||||
|
||||
amount_equality = purchase_invoice.paid_amount == Parameter("%(amount)s")
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
amount_condition = amount_equality if exact_match else purchase_invoice.paid_amount > 0.0
|
||||
|
||||
party_condition = purchase_invoice.supplier == Parameter("%(party)s")
|
||||
party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(purchase_invoice)
|
||||
.select(
|
||||
(party_rank + amount_rank + 1).as_("rank"),
|
||||
ConstantColumn("Purchase Invoice").as_("doctype"),
|
||||
purchase_invoice.name,
|
||||
purchase_invoice.paid_amount,
|
||||
purchase_invoice.name.as_("reference_no"),
|
||||
ConstantColumn("").as_("reference_date"),
|
||||
purchase_invoice.supplier.as_("party"),
|
||||
ConstantColumn("Supplier").as_("party_type"),
|
||||
purchase_invoice.posting_date,
|
||||
purchase_invoice.currency,
|
||||
party_rank.as_("party_match"),
|
||||
amount_rank.as_("amount_match"),
|
||||
)
|
||||
.where(purchase_invoice.docstatus == 1)
|
||||
.where(purchase_invoice.is_paid == 1)
|
||||
.where(purchase_invoice.clearance_date.isnull())
|
||||
.where(purchase_invoice.cash_bank_account == Parameter("%(bank_account)s"))
|
||||
.where(amount_condition)
|
||||
.where(purchase_invoice.currency == currency)
|
||||
)
|
||||
|
||||
if exact_party_match:
|
||||
query = query.where(party_condition)
|
||||
|
||||
return str(query)
|
||||
|
||||
|
||||
def get_unpaid_pi_matching_query(exact_match, exact_party_match, currency):
|
||||
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
|
||||
|
||||
party_condition = purchase_invoice.supplier == Parameter("%(party)s")
|
||||
party_match = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
|
||||
|
||||
grand_total_condition = purchase_invoice.grand_total == Parameter("%(amount)s")
|
||||
amount_match = frappe.qb.terms.Case().when(grand_total_condition, 1).else_(0)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(purchase_invoice)
|
||||
.select(
|
||||
(party_match + amount_match + 1).as_("rank"),
|
||||
ConstantColumn("Purchase Invoice").as_("doctype"),
|
||||
purchase_invoice.name.as_("name"),
|
||||
purchase_invoice.outstanding_amount.as_("paid_amount"),
|
||||
purchase_invoice.name.as_("reference_no"),
|
||||
ConstantColumn("").as_("reference_date"),
|
||||
purchase_invoice.supplier.as_("party"),
|
||||
ConstantColumn("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(grand_total_condition)
|
||||
if exact_party_match:
|
||||
query = query.where(party_condition)
|
||||
|
||||
return str(query)
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import "./bank_reconciliation_tool_beta/panel_manager";
|
||||
import "./bank_reconciliation_tool_beta/summary_number_card";
|
||||
|
||||
import "./bank_reconciliation_tool_beta/actions_panel/actions_panel_manager";
|
||||
import "./bank_reconciliation_tool_beta/actions_panel/details_tab";
|
||||
import "./bank_reconciliation_tool_beta/actions_panel/match_tab";
|
||||
import "./bank_reconciliation_tool_beta/actions_panel/create_tab";
|
||||
@@ -0,0 +1,111 @@
|
||||
frappe.provide("erpnext.accounts.bank_reconciliation");
|
||||
|
||||
erpnext.accounts.bank_reconciliation.ActionsPanelManager = class ActionsPanelManager {
|
||||
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(`
|
||||
<div class="actions-panel"></div>
|
||||
`).find(".actions-panel");
|
||||
}
|
||||
|
||||
this.$actions_container.append(`
|
||||
<div class="form-tabs-list">
|
||||
<ul class="nav form-tabs" role="tablist" aria-label="Action Tabs">
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tab-content p-10"></div>
|
||||
`);
|
||||
}
|
||||
|
||||
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", () => {
|
||||
this.$tab_content.empty();
|
||||
|
||||
if (tab == "Details") {
|
||||
new erpnext.accounts.bank_reconciliation.DetailsTab({
|
||||
actions_panel: this,
|
||||
transaction: this.transaction,
|
||||
panel_manager: this.panel_manager,
|
||||
});
|
||||
} else if (tab == "Match Voucher") {
|
||||
new erpnext.accounts.bank_reconciliation.MatchTab({
|
||||
actions_panel: this,
|
||||
transaction: this.transaction,
|
||||
panel_manager: this.panel_manager,
|
||||
doc: this.doc,
|
||||
});
|
||||
} else {
|
||||
new erpnext.accounts.bank_reconciliation.CreateTab({
|
||||
actions_panel: this,
|
||||
transaction: this.transaction,
|
||||
panel_manager: this.panel_manager,
|
||||
company: this.doc.company,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
add_tab(tab_name, tab) {
|
||||
this.tabs_list_ul.append(`
|
||||
<li class="nav-item">
|
||||
<a class="nav-actions-link"
|
||||
id="${tab_name}-tab" data-toggle="tab"
|
||||
href="#" role="tab" aria-controls="${tab}"
|
||||
>
|
||||
${__(tab)}
|
||||
</a>
|
||||
</li>
|
||||
`);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
frappe.provide("erpnext.accounts.bank_reconciliation");
|
||||
|
||||
erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab {
|
||||
constructor(opts) {
|
||||
Object.assign(this, opts);
|
||||
this.make();
|
||||
}
|
||||
|
||||
make() {
|
||||
this.panel_manager.actions_tab = "create_voucher-tab";
|
||||
|
||||
this.create_field_group = new frappe.ui.FieldGroup({
|
||||
fields: this.get_create_tab_fields(),
|
||||
body: this.actions_panel.$tab_content,
|
||||
card_layout: true,
|
||||
});
|
||||
this.create_field_group.make();
|
||||
}
|
||||
|
||||
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.actions_panel.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);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
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("journal_entry_type").df.reqd = value === "Journal 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.slice(0, 140),
|
||||
},
|
||||
{
|
||||
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",
|
||||
depends_on: "eval: doc.document_type == 'Journal Entry'",
|
||||
},
|
||||
{
|
||||
fieldname: "second_account",
|
||||
fieldtype: "Link",
|
||||
label: "Account",
|
||||
options: "Account",
|
||||
get_query: () => {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
company: this.company,
|
||||
},
|
||||
};
|
||||
},
|
||||
depends_on: "eval: doc.document_type == 'Journal Entry'",
|
||||
},
|
||||
{
|
||||
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",
|
||||
depends_on: "eval: doc.document_type == 'Payment Entry'",
|
||||
},
|
||||
{
|
||||
fieldname: "cost_center",
|
||||
fieldtype: "Link",
|
||||
label: "Cost Center",
|
||||
options: "Cost Center",
|
||||
depends_on: "eval: doc.document_type == 'Payment Entry'",
|
||||
},
|
||||
{
|
||||
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(),
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
frappe.provide("erpnext.accounts.bank_reconciliation");
|
||||
|
||||
erpnext.accounts.bank_reconciliation.DetailsTab = class DetailsTab {
|
||||
constructor(opts) {
|
||||
$.extend(this, opts);
|
||||
this.make();
|
||||
}
|
||||
|
||||
make() {
|
||||
this.panel_manager.actions_tab = "details-tab";
|
||||
|
||||
this.details_field_group = new frappe.ui.FieldGroup({
|
||||
fields: this.get_detail_tab_fields(),
|
||||
body: this.actions_panel.$tab_content,
|
||||
card_layout: true,
|
||||
});
|
||||
this.details_field_group.make();
|
||||
}
|
||||
|
||||
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])
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
frappe.provide("erpnext.accounts.bank_reconciliation");
|
||||
|
||||
erpnext.accounts.bank_reconciliation.MatchTab = class MatchTab {
|
||||
constructor(opts) {
|
||||
$.extend(this, opts);
|
||||
this.make();
|
||||
}
|
||||
|
||||
async make() {
|
||||
this.panel_manager.actions_tab = "match_voucher-tab";
|
||||
|
||||
this.match_field_group = new frappe.ui.FieldGroup({
|
||||
fields: this.get_match_tab_fields(),
|
||||
body: this.actions_panel.$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(
|
||||
`<div class="report-summary reconciliation-summary" style="height: 90px;">
|
||||
</div>`
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
})
|
||||
}
|
||||
|
||||
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 || [];
|
||||
}
|
||||
|
||||
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,
|
||||
"Name in Description": row.name_in_desc_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 `
|
||||
<div class="w-100" style="text-align: center;">
|
||||
<button class="btn btn-default btn-xs match-reasons-btn" data-name=${voucher_name}>
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-help"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<div>
|
||||
<div class="match-popover-header">${__("Match Reasons")}</div>
|
||||
${me.get_match_reasons(voucher_name)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
}
|
||||
});
|
||||
$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 += `<div class="muted">${__(key)}</div>`;
|
||||
}
|
||||
}
|
||||
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"
|
||||
});
|
||||
}
|
||||
|
||||
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.actions_panel.after_transaction_reconcile(response.message, false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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: () => {
|
||||
this.populate_matching_vouchers();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: __("Sales Invoice"),
|
||||
fieldname: "sales_invoice",
|
||||
fieldtype: "Check",
|
||||
default: filters_state.sales_invoice,
|
||||
onchange: () => {
|
||||
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();
|
||||
},
|
||||
depends_on: "eval: doc.sales_invoice || doc.purchase_invoice",
|
||||
},
|
||||
{
|
||||
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_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,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
get_amount_from_row(row) {
|
||||
let value = row[5].content;
|
||||
return flt(value.split(" ") ? value.split(" ")[1] : 0);
|
||||
}
|
||||
}
|
||||
248
erpnext/public/js/bank_reconciliation_tool_beta/panel_manager.js
Normal file
248
erpnext/public/js/bank_reconciliation_tool_beta/panel_manager.js
Normal file
@@ -0,0 +1,248 @@
|
||||
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(`
|
||||
<div class="panel-container d-flex"></div>
|
||||
`).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(`
|
||||
<div class="no-transactions">
|
||||
<img src="/assets/frappe/images/ui-states/list-empty-state.svg" alt="Empty State">
|
||||
<p>${__("No Transactions found for the current filters.")}</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
render_list_panel() {
|
||||
this.$panel_wrapper.append(`
|
||||
<div class="list-panel">
|
||||
<div class="sort-by"></div>
|
||||
<div class="list-container"></div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
this.render_sort_area();
|
||||
this.render_transactions_list();
|
||||
}
|
||||
|
||||
render_actions_panel() {
|
||||
this.actions_panel = new erpnext.accounts.bank_reconciliation.ActionsPanelManager({
|
||||
$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(`
|
||||
<div class="sort-by-title"> ${__("Sort By")} </div>
|
||||
<div class="sort-by-selector p-10"></div>
|
||||
`);
|
||||
|
||||
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(`
|
||||
<div id="${transaction.name}" class="transaction-row p-10">
|
||||
<!-- Date & Amount -->
|
||||
<div class="d-flex">
|
||||
<div class="w-50">
|
||||
<span title="${__("Date")}">${frappe.format(transaction.date, {fieldtype: "Date"})}</span>
|
||||
</div>
|
||||
|
||||
<div class="w-50 bt-amount-contianer">
|
||||
<span
|
||||
title="${__("Amount")}"
|
||||
class="bt-amount ${transaction.withdrawal ? 'text-danger' : 'text-success'}"
|
||||
>
|
||||
<b>${symbol} ${format_currency(amount, transaction.currency)}</b>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Description, Reference, Party -->
|
||||
<div
|
||||
title="${__("Account Holder")}"
|
||||
class="account-holder ${transaction.bank_party_name ? '' : 'hide'}"
|
||||
>
|
||||
<span class="account-holder-value">${transaction.bank_party_name}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
title="${__("Description")}"
|
||||
class="description ${transaction.description ? '' : 'hide'}"
|
||||
>
|
||||
<span class="description-value">${transaction.description}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
title="${__("Reference")}"
|
||||
class="reference ${transaction.reference_number ? '' : 'hide'}"
|
||||
>
|
||||
<span class="reference-value">${transaction.reference_number}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).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 & view'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() {
|
||||
// Remove the current transaction from the list and move to the next/previous one
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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(
|
||||
`<div class="report-summary ${this.wrapper_class || ""}"></div>`
|
||||
).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]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
166
erpnext/public/scss/bank_reconciliation_tool_beta.scss
Normal file
166
erpnext/public/scss/bank_reconciliation_tool_beta.scss
Normal file
@@ -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;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
@import "./erpnext";
|
||||
@import "./call_popup";
|
||||
@import "./point-of-sale";
|
||||
@import "./bank_reconciliation_tool_beta";
|
||||
|
||||
Reference in New Issue
Block a user