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:
Richard Case 2023-02-19 06:20:17 +00:00 committed by GitHub
parent 5e48e61c66
commit 5b7d23de15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 554 additions and 279 deletions

View File

@ -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' });
});
} }
}; };

View File

@ -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) {

View File

@ -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'}
""" """

View File

@ -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") {

View File

@ -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
} }

View File

@ -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

View File

@ -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,

View File

@ -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();

View File

@ -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))

View File

@ -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,

View File

@ -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);

View File

@ -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 {
} }
} }
}; };