feat: bank reconciliation and plaid changes (#33986)
fix: plaid link refresh: update account ids fix: plaid transactions for credit cards & add accounts on link refresh if they don't exist fix: bank reconciliation amount matching fix: bank reconciliation dialog usability feat: rewrite bank transaction reconciliation to allow multiple transactions to reconcile against vouchers before clearance fix: matching transaction amounts and race condition bug fix: ensure there is a reference number in plaid transactions and other tweaks feat: add references to Payroll Entry Bank Journal Entry feat: only clear Voucher once all Bank GLEs are allocated to Bank Transactions fix: strange type error feat: add payment method field to bank and plaid transactions and prepopulate relevant bank reconciliation new voucher fields feat: bank reconciliation - allow bank transactions to reconcile against themselves for when there are banking amendments fix: bank transaction self-reconcile bug and tidy fix: bank reconciliation datatable index update
This commit is contained in:
parent
5e48e61c66
commit
5b7d23de15
@ -118,6 +118,10 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
plaid_success(token, response) {
|
plaid_success(token, response) {
|
||||||
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
|
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.update_bank_account_ids', {
|
||||||
|
response: response,
|
||||||
|
}).then(() => {
|
||||||
|
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -155,7 +155,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render_chart: frappe.utils.debounce((frm) => {
|
render_chart(frm) {
|
||||||
frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager(
|
frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager(
|
||||||
{
|
{
|
||||||
$reconciliation_tool_cards: frm.get_field(
|
$reconciliation_tool_cards: frm.get_field(
|
||||||
@ -167,7 +167,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
|||||||
currency: frm.currency,
|
currency: frm.currency,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, 500),
|
},
|
||||||
|
|
||||||
render(frm) {
|
render(frm) {
|
||||||
if (frm.doc.bank_account) {
|
if (frm.doc.bank_account) {
|
||||||
|
@ -10,7 +10,7 @@ from frappe.model.document import Document
|
|||||||
from frappe.query_builder.custom import ConstantColumn
|
from frappe.query_builder.custom import ConstantColumn
|
||||||
from frappe.utils import cint, flt
|
from frappe.utils import cint, flt
|
||||||
|
|
||||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_paid_amount
|
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
|
||||||
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
|
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
|
||||||
get_amounts_not_reflected_in_system,
|
get_amounts_not_reflected_in_system,
|
||||||
get_entries,
|
get_entries,
|
||||||
@ -28,7 +28,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
|
|||||||
filters = []
|
filters = []
|
||||||
filters.append(["bank_account", "=", bank_account])
|
filters.append(["bank_account", "=", bank_account])
|
||||||
filters.append(["docstatus", "=", 1])
|
filters.append(["docstatus", "=", 1])
|
||||||
filters.append(["unallocated_amount", ">", 0])
|
filters.append(["unallocated_amount", ">", 0.0])
|
||||||
if to_date:
|
if to_date:
|
||||||
filters.append(["date", "<=", to_date])
|
filters.append(["date", "<=", to_date])
|
||||||
if from_date:
|
if from_date:
|
||||||
@ -58,7 +58,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_account_balance(bank_account, till_date):
|
def get_account_balance(bank_account, till_date):
|
||||||
# returns account balance till the specified date
|
# returns account balance till the specified date
|
||||||
account = frappe.get_cached_value("Bank Account", bank_account, "account")
|
account = frappe.db.get_value("Bank Account", bank_account, "account")
|
||||||
filters = frappe._dict(
|
filters = frappe._dict(
|
||||||
{"account": account, "report_date": till_date, "include_pos_transactions": 1}
|
{"account": account, "report_date": till_date, "include_pos_transactions": 1}
|
||||||
)
|
)
|
||||||
@ -66,7 +66,7 @@ def get_account_balance(bank_account, till_date):
|
|||||||
|
|
||||||
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
|
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
|
||||||
|
|
||||||
total_debit, total_credit = 0, 0
|
total_debit, total_credit = 0.0, 0.0
|
||||||
for d in data:
|
for d in data:
|
||||||
total_debit += flt(d.debit)
|
total_debit += flt(d.debit)
|
||||||
total_credit += flt(d.credit)
|
total_credit += flt(d.credit)
|
||||||
@ -131,10 +131,8 @@ def create_journal_entry_bts(
|
|||||||
fieldname=["name", "deposit", "withdrawal", "bank_account"],
|
fieldname=["name", "deposit", "withdrawal", "bank_account"],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)[0]
|
)[0]
|
||||||
company_account = frappe.get_cached_value(
|
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
|
||||||
"Bank Account", bank_transaction.bank_account, "account"
|
account_type = frappe.db.get_value("Account", second_account, "account_type")
|
||||||
)
|
|
||||||
account_type = frappe.get_cached_value("Account", second_account, "account_type")
|
|
||||||
if account_type in ["Receivable", "Payable"]:
|
if account_type in ["Receivable", "Payable"]:
|
||||||
if not (party_type and party):
|
if not (party_type and party):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
@ -147,10 +145,8 @@ def create_journal_entry_bts(
|
|||||||
accounts.append(
|
accounts.append(
|
||||||
{
|
{
|
||||||
"account": second_account,
|
"account": second_account,
|
||||||
"credit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
|
"credit_in_account_currency": bank_transaction.deposit,
|
||||||
"debit_in_account_currency": bank_transaction.withdrawal
|
"debit_in_account_currency": bank_transaction.withdrawal,
|
||||||
if bank_transaction.withdrawal > 0
|
|
||||||
else 0,
|
|
||||||
"party_type": party_type,
|
"party_type": party_type,
|
||||||
"party": party,
|
"party": party,
|
||||||
}
|
}
|
||||||
@ -160,14 +156,12 @@ def create_journal_entry_bts(
|
|||||||
{
|
{
|
||||||
"account": company_account,
|
"account": company_account,
|
||||||
"bank_account": bank_transaction.bank_account,
|
"bank_account": bank_transaction.bank_account,
|
||||||
"credit_in_account_currency": bank_transaction.withdrawal
|
"credit_in_account_currency": bank_transaction.withdrawal,
|
||||||
if bank_transaction.withdrawal > 0
|
"debit_in_account_currency": bank_transaction.deposit,
|
||||||
else 0,
|
|
||||||
"debit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
company = frappe.get_cached_value("Account", company_account, "company")
|
company = frappe.get_value("Account", company_account, "company")
|
||||||
|
|
||||||
journal_entry_dict = {
|
journal_entry_dict = {
|
||||||
"voucher_type": entry_type,
|
"voucher_type": entry_type,
|
||||||
@ -187,16 +181,22 @@ def create_journal_entry_bts(
|
|||||||
journal_entry.insert()
|
journal_entry.insert()
|
||||||
journal_entry.submit()
|
journal_entry.submit()
|
||||||
|
|
||||||
if bank_transaction.deposit > 0:
|
if bank_transaction.deposit > 0.0:
|
||||||
paid_amount = bank_transaction.deposit
|
paid_amount = bank_transaction.deposit
|
||||||
else:
|
else:
|
||||||
paid_amount = bank_transaction.withdrawal
|
paid_amount = bank_transaction.withdrawal
|
||||||
|
|
||||||
vouchers = json.dumps(
|
vouchers = json.dumps(
|
||||||
[{"payment_doctype": "Journal Entry", "payment_name": journal_entry.name, "amount": paid_amount}]
|
[
|
||||||
|
{
|
||||||
|
"payment_doctype": "Journal Entry",
|
||||||
|
"payment_name": journal_entry.name,
|
||||||
|
"amount": paid_amount,
|
||||||
|
}
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
return reconcile_vouchers(bank_transaction.name, vouchers)
|
return reconcile_vouchers(bank_transaction_name, vouchers)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@ -220,12 +220,10 @@ def create_payment_entry_bts(
|
|||||||
as_dict=True,
|
as_dict=True,
|
||||||
)[0]
|
)[0]
|
||||||
paid_amount = bank_transaction.unallocated_amount
|
paid_amount = bank_transaction.unallocated_amount
|
||||||
payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay"
|
payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay"
|
||||||
|
|
||||||
company_account = frappe.get_cached_value(
|
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
|
||||||
"Bank Account", bank_transaction.bank_account, "account"
|
company = frappe.get_value("Account", company_account, "company")
|
||||||
)
|
|
||||||
company = frappe.get_cached_value("Account", company_account, "company")
|
|
||||||
payment_entry_dict = {
|
payment_entry_dict = {
|
||||||
"company": company,
|
"company": company,
|
||||||
"payment_type": payment_type,
|
"payment_type": payment_type,
|
||||||
@ -261,9 +259,15 @@ def create_payment_entry_bts(
|
|||||||
|
|
||||||
payment_entry.submit()
|
payment_entry.submit()
|
||||||
vouchers = json.dumps(
|
vouchers = json.dumps(
|
||||||
[{"payment_doctype": "Payment Entry", "payment_name": payment_entry.name, "amount": paid_amount}]
|
[
|
||||||
|
{
|
||||||
|
"payment_doctype": "Payment Entry",
|
||||||
|
"payment_name": payment_entry.name,
|
||||||
|
"amount": paid_amount,
|
||||||
|
}
|
||||||
|
]
|
||||||
)
|
)
|
||||||
return reconcile_vouchers(bank_transaction.name, vouchers)
|
return reconcile_vouchers(bank_transaction_name, vouchers)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@ -345,59 +349,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
|
|||||||
# updated clear date of all the vouchers based on the bank transaction
|
# updated clear date of all the vouchers based on the bank transaction
|
||||||
vouchers = json.loads(vouchers)
|
vouchers = json.loads(vouchers)
|
||||||
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
|
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
|
||||||
company_account = frappe.get_cached_value("Bank Account", transaction.bank_account, "account")
|
transaction.add_payment_entries(vouchers)
|
||||||
|
|
||||||
if transaction.unallocated_amount == 0:
|
|
||||||
frappe.throw(_("This bank transaction is already fully reconciled"))
|
|
||||||
total_amount = 0
|
|
||||||
for voucher in vouchers:
|
|
||||||
voucher["payment_entry"] = frappe.get_doc(voucher["payment_doctype"], voucher["payment_name"])
|
|
||||||
total_amount += get_paid_amount(
|
|
||||||
frappe._dict(
|
|
||||||
{
|
|
||||||
"payment_document": voucher["payment_doctype"],
|
|
||||||
"payment_entry": voucher["payment_name"],
|
|
||||||
}
|
|
||||||
),
|
|
||||||
transaction.currency,
|
|
||||||
company_account,
|
|
||||||
)
|
|
||||||
|
|
||||||
if total_amount > transaction.unallocated_amount:
|
|
||||||
frappe.throw(
|
|
||||||
_(
|
|
||||||
"The sum total of amounts of all selected vouchers should be less than the unallocated amount of the bank transaction"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
account = frappe.get_cached_value("Bank Account", transaction.bank_account, "account")
|
|
||||||
|
|
||||||
for voucher in vouchers:
|
|
||||||
gl_entry = frappe.db.get_value(
|
|
||||||
"GL Entry",
|
|
||||||
dict(
|
|
||||||
account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
|
|
||||||
),
|
|
||||||
["credit_in_account_currency as credit", "debit_in_account_currency as debit"],
|
|
||||||
as_dict=1,
|
|
||||||
)
|
|
||||||
gl_amount, transaction_amount = (
|
|
||||||
(gl_entry.credit, transaction.deposit)
|
|
||||||
if gl_entry.credit > 0
|
|
||||||
else (gl_entry.debit, transaction.withdrawal)
|
|
||||||
)
|
|
||||||
allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount
|
|
||||||
|
|
||||||
transaction.append(
|
|
||||||
"payment_entries",
|
|
||||||
{
|
|
||||||
"payment_document": voucher["payment_entry"].doctype,
|
|
||||||
"payment_entry": voucher["payment_entry"].name,
|
|
||||||
"allocated_amount": allocated_amount,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
transaction.save()
|
|
||||||
transaction.update_allocations()
|
|
||||||
return frappe.get_doc("Bank Transaction", bank_transaction_name)
|
return frappe.get_doc("Bank Transaction", bank_transaction_name)
|
||||||
|
|
||||||
|
|
||||||
@ -416,9 +368,9 @@ def get_linked_payments(
|
|||||||
bank_account = frappe.db.get_values(
|
bank_account = frappe.db.get_values(
|
||||||
"Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
|
"Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
|
||||||
)[0]
|
)[0]
|
||||||
(account, company) = (bank_account.account, bank_account.company)
|
(gl_account, company) = (bank_account.account, bank_account.company)
|
||||||
matching = check_matching(
|
matching = check_matching(
|
||||||
account,
|
gl_account,
|
||||||
company,
|
company,
|
||||||
transaction,
|
transaction,
|
||||||
document_types,
|
document_types,
|
||||||
@ -428,7 +380,27 @@ def get_linked_payments(
|
|||||||
from_reference_date,
|
from_reference_date,
|
||||||
to_reference_date,
|
to_reference_date,
|
||||||
)
|
)
|
||||||
return matching
|
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(
|
def check_matching(
|
||||||
@ -442,6 +414,7 @@ def check_matching(
|
|||||||
from_reference_date,
|
from_reference_date,
|
||||||
to_reference_date,
|
to_reference_date,
|
||||||
):
|
):
|
||||||
|
exact_match = True if "exact_match" in document_types else False
|
||||||
# combine all types of vouchers
|
# combine all types of vouchers
|
||||||
subquery = get_queries(
|
subquery = get_queries(
|
||||||
bank_account,
|
bank_account,
|
||||||
@ -453,10 +426,11 @@ def check_matching(
|
|||||||
filter_by_reference_date,
|
filter_by_reference_date,
|
||||||
from_reference_date,
|
from_reference_date,
|
||||||
to_reference_date,
|
to_reference_date,
|
||||||
|
exact_match,
|
||||||
)
|
)
|
||||||
filters = {
|
filters = {
|
||||||
"amount": transaction.unallocated_amount,
|
"amount": transaction.unallocated_amount,
|
||||||
"payment_type": "Receive" if transaction.deposit > 0 else "Pay",
|
"payment_type": "Receive" if transaction.deposit > 0.0 else "Pay",
|
||||||
"reference_no": transaction.reference_number,
|
"reference_no": transaction.reference_number,
|
||||||
"party_type": transaction.party_type,
|
"party_type": transaction.party_type,
|
||||||
"party": transaction.party,
|
"party": transaction.party,
|
||||||
@ -465,7 +439,9 @@ def check_matching(
|
|||||||
|
|
||||||
matching_vouchers = []
|
matching_vouchers = []
|
||||||
|
|
||||||
matching_vouchers.extend(get_loan_vouchers(bank_account, transaction, document_types, filters))
|
matching_vouchers.extend(
|
||||||
|
get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match)
|
||||||
|
)
|
||||||
|
|
||||||
for query in subquery:
|
for query in subquery:
|
||||||
matching_vouchers.extend(
|
matching_vouchers.extend(
|
||||||
@ -487,10 +463,10 @@ def get_queries(
|
|||||||
filter_by_reference_date,
|
filter_by_reference_date,
|
||||||
from_reference_date,
|
from_reference_date,
|
||||||
to_reference_date,
|
to_reference_date,
|
||||||
|
exact_match,
|
||||||
):
|
):
|
||||||
# get queries to get matching vouchers
|
# get queries to get matching vouchers
|
||||||
amount_condition = "=" if "exact_match" in document_types else "<="
|
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
|
||||||
account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from"
|
|
||||||
queries = []
|
queries = []
|
||||||
|
|
||||||
# get matching queries from all the apps
|
# get matching queries from all the apps
|
||||||
@ -501,7 +477,7 @@ def get_queries(
|
|||||||
company,
|
company,
|
||||||
transaction,
|
transaction,
|
||||||
document_types,
|
document_types,
|
||||||
amount_condition,
|
exact_match,
|
||||||
account_from_to,
|
account_from_to,
|
||||||
from_date,
|
from_date,
|
||||||
to_date,
|
to_date,
|
||||||
@ -520,7 +496,7 @@ def get_matching_queries(
|
|||||||
company,
|
company,
|
||||||
transaction,
|
transaction,
|
||||||
document_types,
|
document_types,
|
||||||
amount_condition,
|
exact_match,
|
||||||
account_from_to,
|
account_from_to,
|
||||||
from_date,
|
from_date,
|
||||||
to_date,
|
to_date,
|
||||||
@ -530,8 +506,8 @@ def get_matching_queries(
|
|||||||
):
|
):
|
||||||
queries = []
|
queries = []
|
||||||
if "payment_entry" in document_types:
|
if "payment_entry" in document_types:
|
||||||
pe_amount_matching = get_pe_matching_query(
|
query = get_pe_matching_query(
|
||||||
amount_condition,
|
exact_match,
|
||||||
account_from_to,
|
account_from_to,
|
||||||
transaction,
|
transaction,
|
||||||
from_date,
|
from_date,
|
||||||
@ -540,11 +516,11 @@ def get_matching_queries(
|
|||||||
from_reference_date,
|
from_reference_date,
|
||||||
to_reference_date,
|
to_reference_date,
|
||||||
)
|
)
|
||||||
queries.extend([pe_amount_matching])
|
queries.append(query)
|
||||||
|
|
||||||
if "journal_entry" in document_types:
|
if "journal_entry" in document_types:
|
||||||
je_amount_matching = get_je_matching_query(
|
query = get_je_matching_query(
|
||||||
amount_condition,
|
exact_match,
|
||||||
transaction,
|
transaction,
|
||||||
from_date,
|
from_date,
|
||||||
to_date,
|
to_date,
|
||||||
@ -552,34 +528,70 @@ def get_matching_queries(
|
|||||||
from_reference_date,
|
from_reference_date,
|
||||||
to_reference_date,
|
to_reference_date,
|
||||||
)
|
)
|
||||||
queries.extend([je_amount_matching])
|
queries.append(query)
|
||||||
|
|
||||||
if transaction.deposit > 0 and "sales_invoice" in document_types:
|
if transaction.deposit > 0.0 and "sales_invoice" in document_types:
|
||||||
si_amount_matching = get_si_matching_query(amount_condition)
|
query = get_si_matching_query(exact_match)
|
||||||
queries.extend([si_amount_matching])
|
queries.append(query)
|
||||||
|
|
||||||
if transaction.withdrawal > 0:
|
if transaction.withdrawal > 0.0:
|
||||||
if "purchase_invoice" in document_types:
|
if "purchase_invoice" in document_types:
|
||||||
pi_amount_matching = get_pi_matching_query(amount_condition)
|
query = get_pi_matching_query(exact_match)
|
||||||
queries.extend([pi_amount_matching])
|
queries.append(query)
|
||||||
|
|
||||||
|
if "bank_transaction" in document_types:
|
||||||
|
query = get_bt_matching_query(exact_match, transaction)
|
||||||
|
queries.append(query)
|
||||||
|
|
||||||
return queries
|
return queries
|
||||||
|
|
||||||
|
|
||||||
def get_loan_vouchers(bank_account, transaction, document_types, filters):
|
def get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match):
|
||||||
vouchers = []
|
vouchers = []
|
||||||
amount_condition = True if "exact_match" in document_types else False
|
|
||||||
|
|
||||||
if transaction.withdrawal > 0 and "loan_disbursement" in document_types:
|
if transaction.withdrawal > 0.0 and "loan_disbursement" in document_types:
|
||||||
vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters))
|
vouchers.extend(get_ld_matching_query(bank_account, exact_match, filters))
|
||||||
|
|
||||||
if transaction.deposit > 0 and "loan_repayment" in document_types:
|
if transaction.deposit > 0.0 and "loan_repayment" in document_types:
|
||||||
vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters))
|
vouchers.extend(get_lr_matching_query(bank_account, exact_match, filters))
|
||||||
|
|
||||||
return vouchers
|
return vouchers
|
||||||
|
|
||||||
|
|
||||||
def get_ld_matching_query(bank_account, amount_condition, filters):
|
def get_bt_matching_query(exact_match, transaction):
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
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
|
||||||
|
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'}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_ld_matching_query(bank_account, exact_match, filters):
|
||||||
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
|
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
|
||||||
matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
|
matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
|
||||||
matching_party = loan_disbursement.applicant_type == filters.get(
|
matching_party = loan_disbursement.applicant_type == filters.get(
|
||||||
@ -607,17 +619,17 @@ def get_ld_matching_query(bank_account, amount_condition, filters):
|
|||||||
.where(loan_disbursement.disbursement_account == bank_account)
|
.where(loan_disbursement.disbursement_account == bank_account)
|
||||||
)
|
)
|
||||||
|
|
||||||
if amount_condition:
|
if exact_match:
|
||||||
query.where(loan_disbursement.disbursed_amount == filters.get("amount"))
|
query.where(loan_disbursement.disbursed_amount == filters.get("amount"))
|
||||||
else:
|
else:
|
||||||
query.where(loan_disbursement.disbursed_amount <= filters.get("amount"))
|
query.where(loan_disbursement.disbursed_amount > 0.0)
|
||||||
|
|
||||||
vouchers = query.run(as_list=True)
|
vouchers = query.run(as_list=True)
|
||||||
|
|
||||||
return vouchers
|
return vouchers
|
||||||
|
|
||||||
|
|
||||||
def get_lr_matching_query(bank_account, amount_condition, filters):
|
def get_lr_matching_query(bank_account, exact_match, filters):
|
||||||
loan_repayment = frappe.qb.DocType("Loan Repayment")
|
loan_repayment = frappe.qb.DocType("Loan Repayment")
|
||||||
matching_reference = loan_repayment.reference_number == filters.get("reference_number")
|
matching_reference = loan_repayment.reference_number == filters.get("reference_number")
|
||||||
matching_party = loan_repayment.applicant_type == filters.get(
|
matching_party = loan_repayment.applicant_type == filters.get(
|
||||||
@ -648,10 +660,10 @@ def get_lr_matching_query(bank_account, amount_condition, filters):
|
|||||||
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
|
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
|
||||||
query = query.where((loan_repayment.repay_from_salary == 0))
|
query = query.where((loan_repayment.repay_from_salary == 0))
|
||||||
|
|
||||||
if amount_condition:
|
if exact_match:
|
||||||
query.where(loan_repayment.amount_paid == filters.get("amount"))
|
query.where(loan_repayment.amount_paid == filters.get("amount"))
|
||||||
else:
|
else:
|
||||||
query.where(loan_repayment.amount_paid <= filters.get("amount"))
|
query.where(loan_repayment.amount_paid > 0.0)
|
||||||
|
|
||||||
vouchers = query.run()
|
vouchers = query.run()
|
||||||
|
|
||||||
@ -659,7 +671,7 @@ def get_lr_matching_query(bank_account, amount_condition, filters):
|
|||||||
|
|
||||||
|
|
||||||
def get_pe_matching_query(
|
def get_pe_matching_query(
|
||||||
amount_condition,
|
exact_match,
|
||||||
account_from_to,
|
account_from_to,
|
||||||
transaction,
|
transaction,
|
||||||
from_date,
|
from_date,
|
||||||
@ -669,7 +681,7 @@ def get_pe_matching_query(
|
|||||||
to_reference_date,
|
to_reference_date,
|
||||||
):
|
):
|
||||||
# get matching payment entries query
|
# get matching payment entries query
|
||||||
if transaction.deposit > 0:
|
if transaction.deposit > 0.0:
|
||||||
currency_field = "paid_to_account_currency as currency"
|
currency_field = "paid_to_account_currency as currency"
|
||||||
else:
|
else:
|
||||||
currency_field = "paid_from_account_currency as currency"
|
currency_field = "paid_from_account_currency as currency"
|
||||||
@ -684,7 +696,8 @@ def get_pe_matching_query(
|
|||||||
return f"""
|
return f"""
|
||||||
SELECT
|
SELECT
|
||||||
(CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
|
(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 (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,
|
+ 1 ) AS rank,
|
||||||
'Payment Entry' as doctype,
|
'Payment Entry' as doctype,
|
||||||
name,
|
name,
|
||||||
@ -698,20 +711,19 @@ def get_pe_matching_query(
|
|||||||
FROM
|
FROM
|
||||||
`tabPayment Entry`
|
`tabPayment Entry`
|
||||||
WHERE
|
WHERE
|
||||||
paid_amount {amount_condition} %(amount)s
|
docstatus = 1
|
||||||
AND docstatus = 1
|
|
||||||
AND payment_type IN (%(payment_type)s, 'Internal Transfer')
|
AND payment_type IN (%(payment_type)s, 'Internal Transfer')
|
||||||
AND ifnull(clearance_date, '') = ""
|
AND ifnull(clearance_date, '') = ""
|
||||||
AND {account_from_to} = %(bank_account)s
|
AND {account_from_to} = %(bank_account)s
|
||||||
|
AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
|
||||||
{filter_by_date}
|
{filter_by_date}
|
||||||
{filter_by_reference_no}
|
{filter_by_reference_no}
|
||||||
order by{order_by}
|
order by{order_by}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_je_matching_query(
|
def get_je_matching_query(
|
||||||
amount_condition,
|
exact_match,
|
||||||
transaction,
|
transaction,
|
||||||
from_date,
|
from_date,
|
||||||
to_date,
|
to_date,
|
||||||
@ -723,7 +735,7 @@ def get_je_matching_query(
|
|||||||
# We have mapping at the bank level
|
# We have mapping at the bank level
|
||||||
# So one bank could have both types of bank accounts like asset and liability
|
# 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
|
# 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 else "debit"
|
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}'"
|
filter_by_date = f"AND je.posting_date between '{from_date}' and '{to_date}'"
|
||||||
order_by = " je.posting_date"
|
order_by = " je.posting_date"
|
||||||
filter_by_reference_no = ""
|
filter_by_reference_no = ""
|
||||||
@ -735,26 +747,29 @@ def get_je_matching_query(
|
|||||||
return f"""
|
return f"""
|
||||||
SELECT
|
SELECT
|
||||||
(CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END
|
(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 ,
|
+ 1) AS rank ,
|
||||||
'Journal Entry' as doctype,
|
'Journal Entry' AS doctype,
|
||||||
je.name,
|
je.name,
|
||||||
jea.{cr_or_dr}_in_account_currency as paid_amount,
|
jea.{cr_or_dr}_in_account_currency AS paid_amount,
|
||||||
je.cheque_no as reference_no,
|
je.cheque_no AS reference_no,
|
||||||
je.cheque_date as reference_date,
|
je.cheque_date AS reference_date,
|
||||||
je.pay_to_recd_from as party,
|
je.pay_to_recd_from AS party,
|
||||||
jea.party_type,
|
jea.party_type,
|
||||||
je.posting_date,
|
je.posting_date,
|
||||||
jea.account_currency as currency
|
jea.account_currency AS currency
|
||||||
FROM
|
FROM
|
||||||
`tabJournal Entry Account` as jea
|
`tabJournal Entry Account` AS jea
|
||||||
JOIN
|
JOIN
|
||||||
`tabJournal Entry` as je
|
`tabJournal Entry` AS je
|
||||||
ON
|
ON
|
||||||
jea.parent = je.name
|
jea.parent = je.name
|
||||||
WHERE
|
WHERE
|
||||||
(je.clearance_date is null or je.clearance_date='0000-00-00')
|
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.account = %(bank_account)s
|
||||||
AND jea.{cr_or_dr}_in_account_currency {amount_condition} %(amount)s
|
AND jea.{cr_or_dr}_in_account_currency {'= %(amount)s' if exact_match else '> 0.0'}
|
||||||
AND je.docstatus = 1
|
AND je.docstatus = 1
|
||||||
{filter_by_date}
|
{filter_by_date}
|
||||||
{filter_by_reference_no}
|
{filter_by_reference_no}
|
||||||
@ -762,11 +777,12 @@ def get_je_matching_query(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_si_matching_query(amount_condition):
|
def get_si_matching_query(exact_match):
|
||||||
# get matchin sales invoice query
|
# get matching sales invoice query
|
||||||
return f"""
|
return f"""
|
||||||
SELECT
|
SELECT
|
||||||
( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END
|
( 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,
|
+ 1 ) AS rank,
|
||||||
'Sales Invoice' as doctype,
|
'Sales Invoice' as doctype,
|
||||||
si.name,
|
si.name,
|
||||||
@ -784,18 +800,20 @@ def get_si_matching_query(amount_condition):
|
|||||||
`tabSales Invoice` as si
|
`tabSales Invoice` as si
|
||||||
ON
|
ON
|
||||||
sip.parent = si.name
|
sip.parent = si.name
|
||||||
WHERE (sip.clearance_date is null or sip.clearance_date='0000-00-00')
|
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.account = %(bank_account)s
|
||||||
AND sip.amount {amount_condition} %(amount)s
|
AND sip.amount {'= %(amount)s' if exact_match else '> 0.0'}
|
||||||
AND si.docstatus = 1
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_pi_matching_query(amount_condition):
|
def get_pi_matching_query(exact_match):
|
||||||
# get matching purchase invoice query
|
# get matching purchase invoice query when they are also used as payment entries (is_paid)
|
||||||
return f"""
|
return f"""
|
||||||
SELECT
|
SELECT
|
||||||
( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END
|
( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END
|
||||||
|
+ CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
|
||||||
+ 1 ) AS rank,
|
+ 1 ) AS rank,
|
||||||
'Purchase Invoice' as doctype,
|
'Purchase Invoice' as doctype,
|
||||||
name,
|
name,
|
||||||
@ -809,9 +827,9 @@ def get_pi_matching_query(amount_condition):
|
|||||||
FROM
|
FROM
|
||||||
`tabPurchase Invoice`
|
`tabPurchase Invoice`
|
||||||
WHERE
|
WHERE
|
||||||
paid_amount {amount_condition} %(amount)s
|
docstatus = 1
|
||||||
AND docstatus = 1
|
|
||||||
AND is_paid = 1
|
AND is_paid = 1
|
||||||
AND ifnull(clearance_date, '') = ""
|
AND ifnull(clearance_date, '') = ""
|
||||||
AND cash_bank_account = %(bank_account)s
|
AND cash_bank_account = %(bank_account)s
|
||||||
|
AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
|
||||||
"""
|
"""
|
||||||
|
@ -12,8 +12,13 @@ frappe.ui.form.on("Bank Transaction", {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
refresh(frm) {
|
||||||
bank_account: function(frm) {
|
frm.add_custom_button(__('Unreconcile Transaction'), () => {
|
||||||
|
frm.call('remove_payment_entries')
|
||||||
|
.then( () => frm.refresh() );
|
||||||
|
});
|
||||||
|
},
|
||||||
|
bank_account: function (frm) {
|
||||||
set_bank_statement_filter(frm);
|
set_bank_statement_filter(frm);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -34,6 +39,7 @@ frappe.ui.form.on("Bank Transaction", {
|
|||||||
"Journal Entry",
|
"Journal Entry",
|
||||||
"Sales Invoice",
|
"Sales Invoice",
|
||||||
"Purchase Invoice",
|
"Purchase Invoice",
|
||||||
|
"Bank Transaction",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -49,7 +55,7 @@ const update_clearance_date = (frm, cdt, cdn) => {
|
|||||||
frappe
|
frappe
|
||||||
.xcall(
|
.xcall(
|
||||||
"erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment",
|
"erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment",
|
||||||
{ doctype: cdt, docname: cdn }
|
{ doctype: cdt, docname: cdn, bt_name: frm.doc.name }
|
||||||
)
|
)
|
||||||
.then((e) => {
|
.then((e) => {
|
||||||
if (e == "success") {
|
if (e == "success") {
|
||||||
|
@ -20,9 +20,11 @@
|
|||||||
"currency",
|
"currency",
|
||||||
"section_break_10",
|
"section_break_10",
|
||||||
"description",
|
"description",
|
||||||
"section_break_14",
|
|
||||||
"reference_number",
|
"reference_number",
|
||||||
|
"column_break_10",
|
||||||
"transaction_id",
|
"transaction_id",
|
||||||
|
"transaction_type",
|
||||||
|
"section_break_14",
|
||||||
"payment_entries",
|
"payment_entries",
|
||||||
"section_break_18",
|
"section_break_18",
|
||||||
"allocated_amount",
|
"allocated_amount",
|
||||||
@ -190,11 +192,21 @@
|
|||||||
"label": "Withdrawal",
|
"label": "Withdrawal",
|
||||||
"oldfieldname": "credit",
|
"oldfieldname": "credit",
|
||||||
"options": "currency"
|
"options": "currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_10",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "transaction_type",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Transaction Type",
|
||||||
|
"length": 50
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-03-21 19:05:04.208222",
|
"modified": "2022-05-29 18:36:50.475964",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Transaction",
|
"name": "Bank Transaction",
|
||||||
@ -248,4 +260,4 @@
|
|||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "bank_account",
|
"title_field": "bank_account",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
from functools import reduce
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
|
|
||||||
@ -18,72 +15,137 @@ class BankTransaction(StatusUpdater):
|
|||||||
self.clear_linked_payment_entries()
|
self.clear_linked_payment_entries()
|
||||||
self.set_status()
|
self.set_status()
|
||||||
|
|
||||||
|
_saving_flag = False
|
||||||
|
|
||||||
|
# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
|
||||||
def on_update_after_submit(self):
|
def on_update_after_submit(self):
|
||||||
self.update_allocations()
|
"Run on save(). Avoid recursion caused by multiple saves"
|
||||||
self.clear_linked_payment_entries()
|
if not self._saving_flag:
|
||||||
self.set_status(update=True)
|
self._saving_flag = True
|
||||||
|
self.clear_linked_payment_entries()
|
||||||
|
self.update_allocations()
|
||||||
|
self._saving_flag = False
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.clear_linked_payment_entries(for_cancel=True)
|
self.clear_linked_payment_entries(for_cancel=True)
|
||||||
self.set_status(update=True)
|
self.set_status(update=True)
|
||||||
|
|
||||||
def update_allocations(self):
|
def update_allocations(self):
|
||||||
|
"The doctype does not allow modifications after submission, so write to the db direct"
|
||||||
if self.payment_entries:
|
if self.payment_entries:
|
||||||
allocated_amount = reduce(
|
allocated_amount = sum(p.allocated_amount for p in self.payment_entries)
|
||||||
lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries]
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
allocated_amount = 0
|
allocated_amount = 0.0
|
||||||
|
|
||||||
if allocated_amount:
|
amount = abs(flt(self.withdrawal) - flt(self.deposit))
|
||||||
frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount))
|
self.db_set("allocated_amount", flt(allocated_amount))
|
||||||
frappe.db.set_value(
|
self.db_set("unallocated_amount", amount - flt(allocated_amount))
|
||||||
self.doctype,
|
self.reload()
|
||||||
self.name,
|
self.set_status(update=True)
|
||||||
"unallocated_amount",
|
|
||||||
abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount),
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
def add_payment_entries(self, vouchers):
|
||||||
frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0)
|
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
|
||||||
frappe.db.set_value(
|
if 0.0 >= self.unallocated_amount:
|
||||||
self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit))
|
frappe.throw(frappe._(f"Bank Transaction {self.name} is already fully reconciled"))
|
||||||
)
|
|
||||||
|
|
||||||
amount = self.deposit or self.withdrawal
|
added = False
|
||||||
if amount == self.allocated_amount:
|
for voucher in vouchers:
|
||||||
frappe.db.set_value(self.doctype, self.name, "status", "Reconciled")
|
# Can't add same voucher twice
|
||||||
|
found = False
|
||||||
|
for pe in self.payment_entries:
|
||||||
|
if (
|
||||||
|
pe.payment_document == voucher["payment_doctype"]
|
||||||
|
and pe.payment_entry == voucher["payment_name"]
|
||||||
|
):
|
||||||
|
found = True
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
pe = {
|
||||||
|
"payment_document": voucher["payment_doctype"],
|
||||||
|
"payment_entry": voucher["payment_name"],
|
||||||
|
"allocated_amount": 0.0, # Temporary
|
||||||
|
}
|
||||||
|
child = self.append("payment_entries", pe)
|
||||||
|
added = True
|
||||||
|
|
||||||
|
# runs on_update_after_submit
|
||||||
|
if added:
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def allocate_payment_entries(self):
|
||||||
|
"""Refactored from bank reconciliation tool.
|
||||||
|
Non-zero allocations must be amended/cleared manually
|
||||||
|
Get the bank transaction amount (b) and remove as we allocate
|
||||||
|
For each payment_entry if allocated_amount == 0:
|
||||||
|
- get the amount already allocated against all transactions (t), need latest date
|
||||||
|
- get the voucher amount (from gl) (v)
|
||||||
|
- allocate (a = v - t)
|
||||||
|
- a = 0: should already be cleared, so clear & remove payment_entry
|
||||||
|
- 0 < a <= u: allocate a & clear
|
||||||
|
- 0 < a, a > u: allocate u
|
||||||
|
- 0 > a: Error: already over-allocated
|
||||||
|
- clear means: set the latest transaction date as clearance date
|
||||||
|
"""
|
||||||
|
gl_bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account")
|
||||||
|
remaining_amount = self.unallocated_amount
|
||||||
|
for payment_entry in self.payment_entries:
|
||||||
|
if payment_entry.allocated_amount == 0.0:
|
||||||
|
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
|
||||||
|
self, payment_entry
|
||||||
|
)
|
||||||
|
|
||||||
|
if 0.0 == unallocated_amount:
|
||||||
|
if should_clear:
|
||||||
|
latest_transaction.clear_linked_payment_entry(payment_entry)
|
||||||
|
self.db_delete_payment_entry(payment_entry)
|
||||||
|
|
||||||
|
elif remaining_amount <= 0.0:
|
||||||
|
self.db_delete_payment_entry(payment_entry)
|
||||||
|
|
||||||
|
elif 0.0 < unallocated_amount and unallocated_amount <= remaining_amount:
|
||||||
|
payment_entry.db_set("allocated_amount", unallocated_amount)
|
||||||
|
remaining_amount -= unallocated_amount
|
||||||
|
if should_clear:
|
||||||
|
latest_transaction.clear_linked_payment_entry(payment_entry)
|
||||||
|
|
||||||
|
elif 0.0 < unallocated_amount and unallocated_amount > remaining_amount:
|
||||||
|
payment_entry.db_set("allocated_amount", remaining_amount)
|
||||||
|
remaining_amount = 0.0
|
||||||
|
|
||||||
|
elif 0.0 > unallocated_amount:
|
||||||
|
self.db_delete_payment_entry(payment_entry)
|
||||||
|
frappe.throw(
|
||||||
|
frappe._(f"Voucher {payment_entry.payment_entry} is over-allocated by {unallocated_amount}")
|
||||||
|
)
|
||||||
|
|
||||||
self.reload()
|
self.reload()
|
||||||
|
|
||||||
def clear_linked_payment_entries(self, for_cancel=False):
|
def db_delete_payment_entry(self, payment_entry):
|
||||||
|
frappe.db.delete("Bank Transaction Payments", {"name": payment_entry.name})
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def remove_payment_entries(self):
|
||||||
for payment_entry in self.payment_entries:
|
for payment_entry in self.payment_entries:
|
||||||
if payment_entry.payment_document == "Sales Invoice":
|
self.remove_payment_entry(payment_entry)
|
||||||
self.clear_sales_invoice(payment_entry, for_cancel=for_cancel)
|
# runs on_update_after_submit
|
||||||
elif payment_entry.payment_document in get_doctypes_for_bank_reconciliation():
|
self.save()
|
||||||
self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
|
|
||||||
|
|
||||||
def clear_simple_entry(self, payment_entry, for_cancel=False):
|
def remove_payment_entry(self, payment_entry):
|
||||||
if payment_entry.payment_document == "Payment Entry":
|
"Clear payment entry and clearance"
|
||||||
if (
|
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
|
||||||
frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type")
|
self.remove(payment_entry)
|
||||||
== "Internal Transfer"
|
|
||||||
):
|
|
||||||
if len(get_reconciled_bank_transactions(payment_entry)) < 2:
|
|
||||||
return
|
|
||||||
|
|
||||||
clearance_date = self.date if not for_cancel else None
|
def clear_linked_payment_entries(self, for_cancel=False):
|
||||||
frappe.db.set_value(
|
if for_cancel:
|
||||||
payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", clearance_date
|
for payment_entry in self.payment_entries:
|
||||||
)
|
self.clear_linked_payment_entry(payment_entry, for_cancel)
|
||||||
|
else:
|
||||||
|
self.allocate_payment_entries()
|
||||||
|
|
||||||
def clear_sales_invoice(self, payment_entry, for_cancel=False):
|
def clear_linked_payment_entry(self, payment_entry, for_cancel=False):
|
||||||
clearance_date = self.date if not for_cancel else None
|
clearance_date = None if for_cancel else self.date
|
||||||
frappe.db.set_value(
|
set_voucher_clearance(
|
||||||
"Sales Invoice Payment",
|
payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self
|
||||||
dict(parenttype=payment_entry.payment_document, parent=payment_entry.payment_entry),
|
|
||||||
"clearance_date",
|
|
||||||
clearance_date,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -93,38 +155,112 @@ def get_doctypes_for_bank_reconciliation():
|
|||||||
return frappe.get_hooks("bank_reconciliation_doctypes")
|
return frappe.get_hooks("bank_reconciliation_doctypes")
|
||||||
|
|
||||||
|
|
||||||
def get_reconciled_bank_transactions(payment_entry):
|
def get_clearance_details(transaction, payment_entry):
|
||||||
reconciled_bank_transactions = frappe.get_all(
|
"""
|
||||||
"Bank Transaction Payments",
|
There should only be one bank gle for a voucher.
|
||||||
filters={"payment_entry": payment_entry.payment_entry},
|
Could be none for a Bank Transaction.
|
||||||
fields=["parent"],
|
But if a JE, could affect two banks.
|
||||||
|
Should only clear the voucher if all bank gles are allocated.
|
||||||
|
"""
|
||||||
|
gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
|
||||||
|
gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry)
|
||||||
|
bt_allocations = get_total_allocated_amount(
|
||||||
|
payment_entry.payment_document, payment_entry.payment_entry
|
||||||
)
|
)
|
||||||
|
|
||||||
return reconciled_bank_transactions
|
unallocated_amount = min(
|
||||||
|
transaction.unallocated_amount,
|
||||||
|
get_paid_amount(payment_entry, transaction.currency, gl_bank_account),
|
||||||
|
)
|
||||||
|
unmatched_gles = len(gles)
|
||||||
|
latest_transaction = transaction
|
||||||
|
for gle in gles:
|
||||||
|
if gle["gl_account"] == gl_bank_account:
|
||||||
|
if gle["amount"] <= 0.0:
|
||||||
|
frappe.throw(
|
||||||
|
frappe._(f"Voucher {payment_entry.payment_entry} value is broken: {gle['amount']}")
|
||||||
|
)
|
||||||
|
|
||||||
|
unmatched_gles -= 1
|
||||||
|
unallocated_amount = gle["amount"]
|
||||||
|
for a in bt_allocations:
|
||||||
|
if a["gl_account"] == gle["gl_account"]:
|
||||||
|
unallocated_amount = gle["amount"] - a["total"]
|
||||||
|
if frappe.utils.getdate(transaction.date) < a["latest_date"]:
|
||||||
|
latest_transaction = frappe.get_doc("Bank Transaction", a["latest_name"])
|
||||||
|
else:
|
||||||
|
# Must be a Journal Entry affecting more than one bank
|
||||||
|
for a in bt_allocations:
|
||||||
|
if a["gl_account"] == gle["gl_account"] and a["total"] == gle["amount"]:
|
||||||
|
unmatched_gles -= 1
|
||||||
|
|
||||||
|
return unallocated_amount, unmatched_gles == 0, latest_transaction
|
||||||
|
|
||||||
|
|
||||||
def get_total_allocated_amount(payment_entry):
|
def get_related_bank_gl_entries(doctype, docname):
|
||||||
return frappe.db.sql(
|
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||||
|
result = frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
SUM(btp.allocated_amount) as allocated_amount,
|
ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount,
|
||||||
bt.name
|
gle.account AS gl_account
|
||||||
FROM
|
FROM
|
||||||
`tabBank Transaction Payments` as btp
|
`tabGL Entry` gle
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
`tabBank Transaction` bt ON bt.name=btp.parent
|
`tabAccount` ac ON ac.name=gle.account
|
||||||
WHERE
|
WHERE
|
||||||
btp.payment_document = %s
|
ac.account_type = 'Bank'
|
||||||
AND
|
AND gle.voucher_type = %(doctype)s
|
||||||
btp.payment_entry = %s
|
AND gle.voucher_no = %(docname)s
|
||||||
AND
|
AND is_cancelled = 0
|
||||||
bt.docstatus = 1""",
|
""",
|
||||||
(payment_entry.payment_document, payment_entry.payment_entry),
|
dict(doctype=doctype, docname=docname),
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_paid_amount(payment_entry, currency, bank_account):
|
def get_total_allocated_amount(doctype, docname):
|
||||||
|
"""
|
||||||
|
Gets the sum of allocations for a voucher on each bank GL account
|
||||||
|
along with the latest bank transaction name & date
|
||||||
|
NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
|
||||||
|
"""
|
||||||
|
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||||
|
result = frappe.db.sql(
|
||||||
|
"""
|
||||||
|
SELECT total, latest_name, latest_date, gl_account FROM (
|
||||||
|
SELECT
|
||||||
|
ROW_NUMBER() OVER w AS rownum,
|
||||||
|
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account) AS total,
|
||||||
|
FIRST_VALUE(bt.name) OVER w AS latest_name,
|
||||||
|
FIRST_VALUE(bt.date) OVER w AS latest_date,
|
||||||
|
ba.account AS gl_account
|
||||||
|
FROM
|
||||||
|
`tabBank Transaction Payments` btp
|
||||||
|
LEFT JOIN `tabBank Transaction` bt ON bt.name=btp.parent
|
||||||
|
LEFT JOIN `tabBank Account` ba ON ba.name=bt.bank_account
|
||||||
|
WHERE
|
||||||
|
btp.payment_document = %(doctype)s
|
||||||
|
AND btp.payment_entry = %(docname)s
|
||||||
|
AND bt.docstatus = 1
|
||||||
|
WINDOW w AS (PARTITION BY ba.account ORDER BY bt.date desc)
|
||||||
|
) temp
|
||||||
|
WHERE
|
||||||
|
rownum = 1
|
||||||
|
""",
|
||||||
|
dict(doctype=doctype, docname=docname),
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
for row in result:
|
||||||
|
# Why is this *sometimes* a byte string?
|
||||||
|
if isinstance(row["latest_name"], bytes):
|
||||||
|
row["latest_name"] = row["latest_name"].decode()
|
||||||
|
row["latest_date"] = frappe.utils.getdate(row["latest_date"])
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_paid_amount(payment_entry, currency, gl_bank_account):
|
||||||
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
|
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
|
||||||
|
|
||||||
paid_amount_field = "paid_amount"
|
paid_amount_field = "paid_amount"
|
||||||
@ -147,7 +283,7 @@ def get_paid_amount(payment_entry, currency, bank_account):
|
|||||||
elif payment_entry.payment_document == "Journal Entry":
|
elif payment_entry.payment_document == "Journal Entry":
|
||||||
return frappe.db.get_value(
|
return frappe.db.get_value(
|
||||||
"Journal Entry Account",
|
"Journal Entry Account",
|
||||||
{"parent": payment_entry.payment_entry, "account": bank_account},
|
{"parent": payment_entry.payment_entry, "account": gl_bank_account},
|
||||||
"sum(credit_in_account_currency)",
|
"sum(credit_in_account_currency)",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -166,6 +302,12 @@ def get_paid_amount(payment_entry, currency, bank_account):
|
|||||||
payment_entry.payment_document, payment_entry.payment_entry, "amount_paid"
|
payment_entry.payment_document, payment_entry.payment_entry, "amount_paid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif payment_entry.payment_document == "Bank Transaction":
|
||||||
|
dep, wth = frappe.db.get_value(
|
||||||
|
"Bank Transaction", payment_entry.payment_entry, ("deposit", "withdrawal")
|
||||||
|
)
|
||||||
|
return abs(flt(wth) - flt(dep))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
"Please reconcile {0}: {1} manually".format(
|
"Please reconcile {0}: {1} manually".format(
|
||||||
@ -174,18 +316,55 @@ def get_paid_amount(payment_entry, currency, bank_account):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
def set_voucher_clearance(doctype, docname, clearance_date, self):
|
||||||
def unclear_reference_payment(doctype, docname):
|
if doctype in [
|
||||||
if frappe.db.exists(doctype, docname):
|
"Payment Entry",
|
||||||
doc = frappe.get_doc(doctype, docname)
|
"Journal Entry",
|
||||||
if doctype == "Sales Invoice":
|
"Purchase Invoice",
|
||||||
frappe.db.set_value(
|
"Expense Claim",
|
||||||
"Sales Invoice Payment",
|
"Loan Repayment",
|
||||||
dict(parenttype=doc.payment_document, parent=doc.payment_entry),
|
"Loan Disbursement",
|
||||||
"clearance_date",
|
]:
|
||||||
None,
|
if (
|
||||||
)
|
doctype == "Payment Entry"
|
||||||
else:
|
and frappe.db.get_value("Payment Entry", docname, "payment_type") == "Internal Transfer"
|
||||||
frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None)
|
and len(get_reconciled_bank_transactions(doctype, docname)) < 2
|
||||||
|
):
|
||||||
|
return
|
||||||
|
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
|
||||||
|
|
||||||
return doc.payment_entry
|
elif doctype == "Sales Invoice":
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Sales Invoice Payment",
|
||||||
|
dict(parenttype=doctype, parent=docname),
|
||||||
|
"clearance_date",
|
||||||
|
clearance_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
elif doctype == "Bank Transaction":
|
||||||
|
# For when a second bank transaction has fixed another, e.g. refund
|
||||||
|
bt = frappe.get_doc(doctype, docname)
|
||||||
|
if clearance_date:
|
||||||
|
vouchers = [{"payment_doctype": "Bank Transaction", "payment_name": self.name}]
|
||||||
|
bt.add_payment_entries(vouchers)
|
||||||
|
else:
|
||||||
|
for pe in bt.payment_entries:
|
||||||
|
if pe.payment_document == self.doctype and pe.payment_entry == self.name:
|
||||||
|
bt.remove(pe)
|
||||||
|
bt.save()
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def get_reconciled_bank_transactions(doctype, docname):
|
||||||
|
return frappe.get_all(
|
||||||
|
"Bank Transaction Payments",
|
||||||
|
filters={"payment_document": doctype, "payment_entry": docname},
|
||||||
|
pluck="parent",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def unclear_reference_payment(doctype, docname, bt_name):
|
||||||
|
bt = frappe.get_doc("Bank Transaction", bt_name)
|
||||||
|
set_voucher_clearance(doctype, docname, None, bt)
|
||||||
|
return docname
|
||||||
|
@ -12,7 +12,7 @@ class PlaidConnector:
|
|||||||
def __init__(self, access_token=None):
|
def __init__(self, access_token=None):
|
||||||
self.access_token = access_token
|
self.access_token = access_token
|
||||||
self.settings = frappe.get_single("Plaid Settings")
|
self.settings = frappe.get_single("Plaid Settings")
|
||||||
self.products = ["auth", "transactions"]
|
self.products = ["transactions"]
|
||||||
self.client_name = frappe.local.site
|
self.client_name = frappe.local.site
|
||||||
self.client = plaid.Client(
|
self.client = plaid.Client(
|
||||||
client_id=self.settings.plaid_client_id,
|
client_id=self.settings.plaid_client_id,
|
||||||
|
@ -47,7 +47,7 @@ erpnext.integrations.plaidLink = class plaidLink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init_config() {
|
async init_config() {
|
||||||
this.product = ["auth", "transactions"];
|
this.product = ["transactions"];
|
||||||
this.plaid_env = this.frm.doc.plaid_env;
|
this.plaid_env = this.frm.doc.plaid_env;
|
||||||
this.client_name = frappe.boot.sitename;
|
this.client_name = frappe.boot.sitename;
|
||||||
this.token = await this.get_link_token();
|
this.token = await this.get_link_token();
|
||||||
|
@ -70,7 +70,8 @@ def add_bank_accounts(response, bank, company):
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
bank = json.loads(bank)
|
if isinstance(bank, str):
|
||||||
|
bank = json.loads(bank)
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
default_gl_account = get_default_bank_cash_account(company, "Bank")
|
default_gl_account = get_default_bank_cash_account(company, "Bank")
|
||||||
@ -177,16 +178,15 @@ def sync_transactions(bank, bank_account):
|
|||||||
)
|
)
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for transaction in reversed(transactions):
|
if transactions:
|
||||||
result += new_bank_transaction(transaction)
|
for transaction in reversed(transactions):
|
||||||
|
result += new_bank_transaction(transaction)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
last_transaction_date = frappe.db.get_value("Bank Transaction", result.pop(), "date")
|
last_transaction_date = frappe.db.get_value("Bank Transaction", result.pop(), "date")
|
||||||
|
|
||||||
frappe.logger().info(
|
frappe.logger().info(
|
||||||
"Plaid added {} new Bank Transactions from '{}' between {} and {}".format(
|
f"Plaid added {len(result)} new Bank Transactions from '{bank_account}' between {start_date} and {end_date}"
|
||||||
len(result), bank_account, start_date, end_date
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
@ -230,19 +230,20 @@ def new_bank_transaction(transaction):
|
|||||||
|
|
||||||
bank_account = frappe.db.get_value("Bank Account", dict(integration_id=transaction["account_id"]))
|
bank_account = frappe.db.get_value("Bank Account", dict(integration_id=transaction["account_id"]))
|
||||||
|
|
||||||
if float(transaction["amount"]) >= 0:
|
amount = float(transaction["amount"])
|
||||||
debit = 0
|
if amount >= 0.0:
|
||||||
credit = float(transaction["amount"])
|
deposit = 0.0
|
||||||
|
withdrawal = amount
|
||||||
else:
|
else:
|
||||||
debit = abs(float(transaction["amount"]))
|
deposit = abs(amount)
|
||||||
credit = 0
|
withdrawal = 0.0
|
||||||
|
|
||||||
status = "Pending" if transaction["pending"] == "True" else "Settled"
|
status = "Pending" if transaction["pending"] == "True" else "Settled"
|
||||||
|
|
||||||
tags = []
|
tags = []
|
||||||
try:
|
try:
|
||||||
tags += transaction["category"]
|
tags += transaction["category"]
|
||||||
tags += ["Plaid Cat. {}".format(transaction["category_id"])]
|
tags += [f'Plaid Cat. {transaction["category_id"]}']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -254,11 +255,18 @@ def new_bank_transaction(transaction):
|
|||||||
"date": getdate(transaction["date"]),
|
"date": getdate(transaction["date"]),
|
||||||
"status": status,
|
"status": status,
|
||||||
"bank_account": bank_account,
|
"bank_account": bank_account,
|
||||||
"deposit": debit,
|
"deposit": deposit,
|
||||||
"withdrawal": credit,
|
"withdrawal": withdrawal,
|
||||||
"currency": transaction["iso_currency_code"],
|
"currency": transaction["iso_currency_code"],
|
||||||
"transaction_id": transaction["transaction_id"],
|
"transaction_id": transaction["transaction_id"],
|
||||||
"reference_number": transaction["payment_meta"]["reference_number"],
|
"transaction_type": (
|
||||||
|
transaction["transaction_code"] or transaction["payment_meta"]["payment_method"]
|
||||||
|
),
|
||||||
|
"reference_number": (
|
||||||
|
transaction["check_number"]
|
||||||
|
or transaction["payment_meta"]["reference_number"]
|
||||||
|
or transaction["name"]
|
||||||
|
),
|
||||||
"description": transaction["name"],
|
"description": transaction["name"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -271,7 +279,7 @@ def new_bank_transaction(transaction):
|
|||||||
result.append(new_transaction.name)
|
result.append(new_transaction.name)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
frappe.throw(title=_("Bank transaction creation error"))
|
frappe.throw(_("Bank transaction creation error"))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -300,3 +308,26 @@ def enqueue_synchronization():
|
|||||||
def get_link_token_for_update(access_token):
|
def get_link_token_for_update(access_token):
|
||||||
plaid = PlaidConnector(access_token)
|
plaid = PlaidConnector(access_token)
|
||||||
return plaid.get_link_token(update_mode=True)
|
return plaid.get_link_token(update_mode=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_company(bank_account_name):
|
||||||
|
from frappe.defaults import get_user_default
|
||||||
|
|
||||||
|
company_names = frappe.db.get_all("Company", pluck="name")
|
||||||
|
if len(company_names) == 1:
|
||||||
|
return company_names[0]
|
||||||
|
if frappe.db.exists("Bank Account", bank_account_name):
|
||||||
|
return frappe.db.get_value("Bank Account", bank_account_name, "company")
|
||||||
|
company_default = get_user_default("Company")
|
||||||
|
if company_default:
|
||||||
|
return company_default
|
||||||
|
frappe.throw(_("Could not detect the Company for updating Bank Accounts"))
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def update_bank_account_ids(response):
|
||||||
|
data = json.loads(response)
|
||||||
|
institution_name = data["institution"]["name"]
|
||||||
|
bank = frappe.get_doc("Bank", institution_name).as_dict()
|
||||||
|
bank_account_name = f"{data['account']['name']} - {institution_name}"
|
||||||
|
return add_bank_accounts(response, bank, get_company(bank_account_name))
|
||||||
|
@ -125,6 +125,8 @@ class TestPlaidSettings(unittest.TestCase):
|
|||||||
"unofficial_currency_code": None,
|
"unofficial_currency_code": None,
|
||||||
"name": "INTRST PYMNT",
|
"name": "INTRST PYMNT",
|
||||||
"transaction_type": "place",
|
"transaction_type": "place",
|
||||||
|
"transaction_code": "direct debit",
|
||||||
|
"check_number": "3456789",
|
||||||
"amount": -4.22,
|
"amount": -4.22,
|
||||||
"location": {
|
"location": {
|
||||||
"city": None,
|
"city": None,
|
||||||
|
@ -182,6 +182,9 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.transactions.splice(transaction_index, 1);
|
this.transactions.splice(transaction_index, 1);
|
||||||
|
for (const [k, v] of Object.entries(this.transaction_dt_map)) {
|
||||||
|
if (v > transaction_index) this.transaction_dt_map[k] = v - 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.datatable.refresh(this.transactions, this.columns);
|
this.datatable.refresh(this.transactions, this.columns);
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
doctype: "Bank Transaction",
|
doctype: "Bank Transaction",
|
||||||
filters: { name: this.bank_transaction_name },
|
filters: { name: this.bank_transaction_name },
|
||||||
fieldname: [
|
fieldname: [
|
||||||
"date as reference_date",
|
"date",
|
||||||
"deposit",
|
"deposit",
|
||||||
"withdrawal",
|
"withdrawal",
|
||||||
"currency",
|
"currency",
|
||||||
@ -33,6 +33,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
"party",
|
"party",
|
||||||
"unallocated_amount",
|
"unallocated_amount",
|
||||||
"allocated_amount",
|
"allocated_amount",
|
||||||
|
"transaction_type",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
@ -41,11 +42,23 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
r.message.payment_entry = 1;
|
r.message.payment_entry = 1;
|
||||||
r.message.journal_entry = 1;
|
r.message.journal_entry = 1;
|
||||||
this.dialog.set_values(r.message);
|
this.dialog.set_values(r.message);
|
||||||
|
this.copy_data_to_voucher();
|
||||||
this.dialog.show();
|
this.dialog.show();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copy_data_to_voucher() {
|
||||||
|
let copied = {
|
||||||
|
reference_number: this.bank_transaction.reference_number || this.bank_transaction.description,
|
||||||
|
posting_date: this.bank_transaction.date,
|
||||||
|
reference_date: this.bank_transaction.date,
|
||||||
|
mode_of_payment: this.bank_transaction.transaction_type,
|
||||||
|
};
|
||||||
|
this.dialog.set_values(copied);
|
||||||
|
}
|
||||||
|
|
||||||
get_linked_vouchers(document_types) {
|
get_linked_vouchers(document_types) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method:
|
method:
|
||||||
@ -75,10 +88,9 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
row[1],
|
row[1],
|
||||||
row[2],
|
row[2],
|
||||||
reference_date,
|
reference_date,
|
||||||
row[8],
|
|
||||||
format_currency(row[3], row[9]),
|
format_currency(row[3], row[9]),
|
||||||
row[6],
|
|
||||||
row[4],
|
row[4],
|
||||||
|
row[6],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
this.get_dt_columns();
|
this.get_dt_columns();
|
||||||
@ -104,7 +116,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
{
|
{
|
||||||
name: __("Document Name"),
|
name: __("Document Name"),
|
||||||
editable: false,
|
editable: false,
|
||||||
width: 150,
|
width: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: __("Reference Date"),
|
name: __("Reference Date"),
|
||||||
@ -112,25 +124,19 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
width: 120,
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Posting Date",
|
name: __("Remaining"),
|
||||||
editable: false,
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: __("Amount"),
|
|
||||||
editable: false,
|
editable: false,
|
||||||
width: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: __("Party"),
|
|
||||||
editable: false,
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
name: __("Reference Number"),
|
name: __("Reference Number"),
|
||||||
editable: false,
|
editable: false,
|
||||||
width: 140,
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: __("Party"),
|
||||||
|
editable: false,
|
||||||
|
width: 100,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -224,6 +230,16 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
fieldname: "exact_match",
|
fieldname: "exact_match",
|
||||||
onchange: () => this.update_options(),
|
onchange: () => this.update_options(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldname: "column_break_5",
|
||||||
|
fieldtype: "Column Break",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Check",
|
||||||
|
label: "Bank Transaction",
|
||||||
|
fieldname: "bank_transaction",
|
||||||
|
onchange: () => this.update_options(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fieldtype: "Section Break",
|
fieldtype: "Section Break",
|
||||||
fieldname: "section_break_1",
|
fieldname: "section_break_1",
|
||||||
@ -289,7 +305,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
fieldtype: "Column Break",
|
fieldtype: "Column Break",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: "Journal Entry Type",
|
default: "Bank Entry",
|
||||||
fieldname: "journal_entry_type",
|
fieldname: "journal_entry_type",
|
||||||
fieldtype: "Select",
|
fieldtype: "Select",
|
||||||
label: "Journal Entry Type",
|
label: "Journal Entry Type",
|
||||||
@ -364,7 +380,12 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
fieldtype: "Section Break",
|
fieldtype: "Section Break",
|
||||||
fieldname: "details_section",
|
fieldname: "details_section",
|
||||||
label: "Transaction Details",
|
label: "Transaction Details",
|
||||||
collapsible: 1,
|
},
|
||||||
|
{
|
||||||
|
fieldname: "date",
|
||||||
|
fieldtype: "Date",
|
||||||
|
label: "Date",
|
||||||
|
read_only: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "deposit",
|
fieldname: "deposit",
|
||||||
@ -381,14 +402,14 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
read_only: 1,
|
read_only: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "description",
|
fieldname: "column_break_17",
|
||||||
fieldtype: "Small Text",
|
fieldtype: "Column Break",
|
||||||
label: "Description",
|
|
||||||
read_only: 1,
|
read_only: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "column_break_17",
|
fieldname: "description",
|
||||||
fieldtype: "Column Break",
|
fieldtype: "Small Text",
|
||||||
|
label: "Description",
|
||||||
read_only: 1,
|
read_only: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -398,7 +419,6 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
options: "Currency",
|
options: "Currency",
|
||||||
read_only: 1,
|
read_only: 1,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
fieldname: "unallocated_amount",
|
fieldname: "unallocated_amount",
|
||||||
fieldtype: "Currency",
|
fieldtype: "Currency",
|
||||||
@ -593,4 +613,4 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user