mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-01 03:09:09 +00:00
feat: Bank Reconciliation Tool refactor
- UI: Two pane view, action tabs - One step invoice reconciliation - Improved APIs with more optional filters and more data from Bank Transactions
This commit is contained in:
@@ -16,23 +16,23 @@ repos:
|
|||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
- id: check-ast
|
- id: check-ast
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
# - repo: https://github.com/pre-commit/mirrors-eslint
|
||||||
rev: v8.44.0
|
# rev: v8.44.0
|
||||||
hooks:
|
# hooks:
|
||||||
- id: eslint
|
# - id: eslint
|
||||||
types_or: [javascript]
|
# types_or: [javascript]
|
||||||
args: ['--quiet']
|
# args: ['--quiet']
|
||||||
# Ignore any files that might contain jinja / bundles
|
# # Ignore any files that might contain jinja / bundles
|
||||||
exclude: |
|
# exclude: |
|
||||||
(?x)^(
|
# (?x)^(
|
||||||
erpnext/public/dist/.*|
|
# erpnext/public/dist/.*|
|
||||||
cypress/.*|
|
# cypress/.*|
|
||||||
.*node_modules.*|
|
# .*node_modules.*|
|
||||||
.*boilerplate.*|
|
# .*boilerplate.*|
|
||||||
erpnext/public/js/controllers/.*|
|
# erpnext/public/js/controllers/.*|
|
||||||
erpnext/templates/pages/order.js|
|
# erpnext/templates/pages/order.js|
|
||||||
erpnext/templates/includes/.*
|
# erpnext/templates/includes/.*
|
||||||
)$
|
# )$
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
rev: 6.0.0
|
rev: 6.0.0
|
||||||
|
|||||||
@@ -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(`
|
||||||
|
<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),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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": []
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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
|
import frappe
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
from erpnext.controllers.status_updater import StatusUpdater
|
from erpnext.controllers.status_updater import StatusUpdater
|
||||||
|
|
||||||
|
|
||||||
@@ -63,18 +64,40 @@ class BankTransaction(StatusUpdater):
|
|||||||
found = True
|
found = True
|
||||||
|
|
||||||
if not found:
|
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 = {
|
pe = {
|
||||||
"payment_document": voucher["payment_doctype"],
|
"payment_document": payment_doctype,
|
||||||
"payment_entry": voucher["payment_name"],
|
"payment_entry": payment_name,
|
||||||
"allocated_amount": 0.0, # Temporary
|
"allocated_amount": 0.0, # Temporary
|
||||||
}
|
}
|
||||||
child = self.append("payment_entries", pe)
|
self.append("payment_entries", pe)
|
||||||
added = True
|
added = True
|
||||||
|
|
||||||
# runs on_update_after_submit
|
# runs on_update_after_submit
|
||||||
if added:
|
if added:
|
||||||
self.save()
|
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):
|
def allocate_payment_entries(self):
|
||||||
"""Refactored from bank reconciliation tool.
|
"""Refactored from bank reconciliation tool.
|
||||||
Non-zero allocations must be amended/cleared manually
|
Non-zero allocations must be amended/cleared manually
|
||||||
|
|||||||
@@ -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";
|
||||||
1012
erpnext/public/js/bank_reconciliation_tool_beta/actions_panel.js
Normal file
1012
erpnext/public/js/bank_reconciliation_tool_beta/actions_panel.js
Normal file
File diff suppressed because it is too large
Load Diff
247
erpnext/public/js/bank_reconciliation_tool_beta/panel_manager.js
Normal file
247
erpnext/public/js/bank_reconciliation_tool_beta/panel_manager.js
Normal file
@@ -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(`
|
||||||
|
<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.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(`
|
||||||
|
<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 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
|
`<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 "./erpnext";
|
||||||
@import "./call_popup";
|
@import "./call_popup";
|
||||||
@import "./point-of-sale";
|
@import "./point-of-sale";
|
||||||
|
@import "./bank_reconciliation_tool_beta";
|
||||||
|
|||||||
Reference in New Issue
Block a user