Merge pull request #29865 from deepeshgarg007/loan_bank_reco

feat: Bank Reconciliation for loan documents
This commit is contained in:
Deepesh Garg 2022-02-22 20:43:00 +05:30 committed by GitHub
commit bd80e04aa7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 374 additions and 47 deletions

View File

@ -7,6 +7,7 @@ import json
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt
from erpnext import get_company_currency
@ -275,6 +276,10 @@ def check_matching(bank_account, company, transaction, document_types):
}
matching_vouchers = []
matching_vouchers.extend(get_loan_vouchers(bank_account, transaction,
document_types, filters))
for query in subquery:
matching_vouchers.extend(
frappe.db.sql(query, filters,)
@ -311,6 +316,114 @@ def get_queries(bank_account, company, transaction, document_types):
return queries
def get_loan_vouchers(bank_account, transaction, document_types, filters):
vouchers = []
amount_condition = True if "exact_match" in document_types else False
if transaction.withdrawal > 0 and "loan_disbursement" in document_types:
vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters))
if transaction.deposit > 0 and "loan_repayment" in document_types:
vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters))
return vouchers
def get_ld_matching_query(bank_account, amount_condition, filters):
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
matching_party = loan_disbursement.applicant_type == filters.get("party_type") and \
loan_disbursement.applicant == filters.get("party")
rank = (
frappe.qb.terms.Case()
.when(matching_reference, 1)
.else_(0)
)
rank1 = (
frappe.qb.terms.Case()
.when(matching_party, 1)
.else_(0)
)
query = frappe.qb.from_(loan_disbursement).select(
rank + rank1 + 1,
ConstantColumn("Loan Disbursement").as_("doctype"),
loan_disbursement.name,
loan_disbursement.disbursed_amount,
loan_disbursement.reference_number,
loan_disbursement.reference_date,
loan_disbursement.applicant_type,
loan_disbursement.disbursement_date
).where(
loan_disbursement.docstatus == 1
).where(
loan_disbursement.clearance_date.isnull()
).where(
loan_disbursement.disbursement_account == bank_account
)
if amount_condition:
query.where(
loan_disbursement.disbursed_amount == filters.get('amount')
)
else:
query.where(
loan_disbursement.disbursed_amount <= filters.get('amount')
)
vouchers = query.run(as_list=True)
return vouchers
def get_lr_matching_query(bank_account, amount_condition, filters):
loan_repayment = frappe.qb.DocType("Loan Repayment")
matching_reference = loan_repayment.reference_number == filters.get("reference_number")
matching_party = loan_repayment.applicant_type == filters.get("party_type") and \
loan_repayment.applicant == filters.get("party")
rank = (
frappe.qb.terms.Case()
.when(matching_reference, 1)
.else_(0)
)
rank1 = (
frappe.qb.terms.Case()
.when(matching_party, 1)
.else_(0)
)
query = frappe.qb.from_(loan_repayment).select(
rank + rank1 + 1,
ConstantColumn("Loan Repayment").as_("doctype"),
loan_repayment.name,
loan_repayment.amount_paid,
loan_repayment.reference_number,
loan_repayment.reference_date,
loan_repayment.applicant_type,
loan_repayment.posting_date
).where(
loan_repayment.docstatus == 1
).where(
loan_repayment.clearance_date.isnull()
).where(
loan_repayment.payment_account == bank_account
)
if amount_condition:
query.where(
loan_repayment.amount_paid == filters.get('amount')
)
else:
query.where(
loan_repayment.amount_paid <= filters.get('amount')
)
vouchers = query.run()
return vouchers
def get_pe_matching_query(amount_condition, account_from_to, transaction):
# get matching payment entries query
if transaction.deposit > 0:
@ -348,7 +461,6 @@ def get_je_matching_query(amount_condition, transaction):
# We have mapping at the bank level
# So one bank could have both types of bank accounts like asset and liability
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
company_account = frappe.get_value("Bank Account", transaction.bank_account, "account")
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
return f"""

View File

@ -49,7 +49,8 @@ class BankTransaction(StatusUpdater):
def clear_linked_payment_entries(self, for_cancel=False):
for payment_entry in self.payment_entries:
if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]:
if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim", "Loan Repayment",
"Loan Disbursement"]:
self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
elif payment_entry.payment_document == "Sales Invoice":
@ -116,11 +117,18 @@ def get_paid_amount(payment_entry, currency, bank_account):
payment_entry.payment_entry, paid_amount_field)
elif payment_entry.payment_document == "Journal Entry":
return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, "sum(credit_in_account_currency)")
return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account},
"sum(credit_in_account_currency)")
elif payment_entry.payment_document == "Expense Claim":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed")
elif payment_entry.payment_document == "Loan Disbursement":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "disbursed_amount")
elif payment_entry.payment_document == "Loan Repayment":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "amount_paid")
else:
frappe.throw("Please reconcile {0}: {1} manually".format(payment_entry.payment_document, payment_entry.payment_entry))

View File

@ -4,7 +4,12 @@
import frappe
from frappe import _
from frappe.utils import flt, getdate, nowdate
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Sum
from frappe.utils import flt, getdate
from pypika import CustomFunction
from erpnext.accounts.utils import get_balance_on
def execute(filters=None):
@ -18,7 +23,6 @@ def execute(filters=None):
data = get_entries(filters)
from erpnext.accounts.utils import get_balance_on
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
total_debit, total_credit = 0,0
@ -118,7 +122,21 @@ def get_columns():
]
def get_entries(filters):
journal_entries = frappe.db.sql("""
journal_entries = get_journal_entries(filters)
payment_entries = get_payment_entries(filters)
loan_entries = get_loan_entries(filters)
pos_entries = []
if filters.include_pos_transactions:
pos_entries = get_pos_entries(filters)
return sorted(list(payment_entries)+list(journal_entries+list(pos_entries) + list(loan_entries)),
key=lambda k: getdate(k['posting_date']))
def get_journal_entries(filters):
return frappe.db.sql("""
select "Journal Entry" as payment_document, jv.posting_date,
jv.name as payment_entry, jvd.debit_in_account_currency as debit,
jvd.credit_in_account_currency as credit, jvd.against_account,
@ -130,7 +148,8 @@ def get_entries(filters):
and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s
and ifnull(jv.is_opening, 'No') = 'No'""", filters, as_dict=1)
payment_entries = frappe.db.sql("""
def get_payment_entries(filters):
return frappe.db.sql("""
select
"Payment Entry" as payment_document, name as payment_entry,
reference_no, reference_date as ref_date,
@ -145,9 +164,8 @@ def get_entries(filters):
and ifnull(clearance_date, '4000-01-01') > %(report_date)s
""", filters, as_dict=1)
pos_entries = []
if filters.include_pos_transactions:
pos_entries = frappe.db.sql("""
def get_pos_entries(filters):
return frappe.db.sql("""
select
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
si.posting_date, si.debit_to as against_account, sip.clearance_date,
@ -161,8 +179,42 @@ def get_entries(filters):
si.posting_date ASC, si.name DESC
""", filters, as_dict=1)
return sorted(list(payment_entries)+list(journal_entries+list(pos_entries)),
key=lambda k: k['posting_date'] or getdate(nowdate()))
def get_loan_entries(filters):
loan_docs = []
for doctype in ["Loan Disbursement", "Loan Repayment"]:
loan_doc = frappe.qb.DocType(doctype)
ifnull = CustomFunction('IFNULL', ['value', 'default'])
if doctype == "Loan Disbursement":
amount_field = (loan_doc.disbursed_amount).as_("credit")
posting_date = (loan_doc.disbursement_date).as_("posting_date")
account = loan_doc.disbursement_account
else:
amount_field = (loan_doc.amount_paid).as_("debit")
posting_date = (loan_doc.posting_date).as_("posting_date")
account = loan_doc.payment_account
entries = frappe.qb.from_(loan_doc).select(
ConstantColumn(doctype).as_("payment_document"),
(loan_doc.name).as_("payment_entry"),
(loan_doc.reference_number).as_("reference_no"),
(loan_doc.reference_date).as_("ref_date"),
amount_field,
posting_date,
).where(
loan_doc.docstatus == 1
).where(
account == filters.get('account')
).where(
posting_date <= getdate(filters.get('report_date'))
).where(
ifnull(loan_doc.clearance_date, '4000-01-01') > getdate(filters.get('report_date'))
).run(as_dict=1)
loan_docs.extend(entries)
return loan_docs
def get_amounts_not_reflected_in_system(filters):
je_amount = frappe.db.sql("""
@ -182,7 +234,40 @@ def get_amounts_not_reflected_in_system(filters):
pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0
return je_amount + pe_amount
loan_amount = get_loan_amount(filters)
return je_amount + pe_amount + loan_amount
def get_loan_amount(filters):
total_amount = 0
for doctype in ["Loan Disbursement", "Loan Repayment"]:
loan_doc = frappe.qb.DocType(doctype)
ifnull = CustomFunction('IFNULL', ['value', 'default'])
if doctype == "Loan Disbursement":
amount_field = Sum(loan_doc.disbursed_amount)
posting_date = (loan_doc.disbursement_date).as_("posting_date")
account = loan_doc.disbursement_account
else:
amount_field = Sum(loan_doc.amount_paid)
posting_date = (loan_doc.posting_date).as_("posting_date")
account = loan_doc.payment_account
amount = frappe.qb.from_(loan_doc).select(
amount_field
).where(
loan_doc.docstatus == 1
).where(
account == filters.get('account')
).where(
posting_date > getdate(filters.get('report_date'))
).where(
ifnull(loan_doc.clearance_date, '4000-01-01') <= getdate(filters.get('report_date'))
).run()[0][0]
total_amount += flt(amount)
return amount
def get_balance_row(label, amount, account_currency):
if amount > 0:

View File

@ -14,11 +14,15 @@
"applicant",
"section_break_7",
"disbursement_date",
"clearance_date",
"column_break_8",
"disbursed_amount",
"accounting_dimensions_section",
"cost_center",
"customer_details_section",
"accounting_details",
"disbursement_account",
"column_break_16",
"loan_account",
"bank_account",
"disbursement_references_section",
"reference_date",
@ -106,11 +110,6 @@
"fieldtype": "Section Break",
"label": "Disbursement Details"
},
{
"fieldname": "customer_details_section",
"fieldtype": "Section Break",
"label": "Customer Details"
},
{
"fetch_from": "against_loan.applicant_type",
"fieldname": "applicant_type",
@ -149,15 +148,48 @@
"fieldname": "reference_number",
"fieldtype": "Data",
"label": "Reference Number"
},
{
"fieldname": "clearance_date",
"fieldtype": "Date",
"label": "Clearance Date",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "accounting_details",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fetch_from": "against_loan.disbursement_account",
"fieldname": "disbursement_account",
"fieldtype": "Link",
"label": "Disbursement Account",
"options": "Account",
"read_only": 1
},
{
"fieldname": "column_break_16",
"fieldtype": "Column Break"
},
{
"fetch_from": "against_loan.loan_account",
"fieldname": "loan_account",
"fieldtype": "Link",
"label": "Loan Account",
"options": "Account",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-04-19 18:09:32.175355",
"modified": "2022-02-17 18:23:44.157598",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Disbursement",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@ -194,5 +226,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -42,9 +42,6 @@ class LoanDisbursement(AccountsController):
if not self.posting_date:
self.posting_date = self.disbursement_date or nowdate()
if not self.bank_account and self.applicant_type == "Customer":
self.bank_account = frappe.db.get_value("Customer", self.applicant, "default_bank_account")
def validate_disbursal_amount(self):
possible_disbursal_amount = get_disbursal_amount(self.against_loan)
@ -117,12 +114,11 @@ class LoanDisbursement(AccountsController):
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
loan_details = frappe.get_doc("Loan", self.against_loan)
gle_map.append(
self.get_gl_dict({
"account": loan_details.loan_account,
"against": loan_details.disbursement_account,
"account": self.loan_account,
"against": self.disbursement_account,
"debit": self.disbursed_amount,
"debit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan",
@ -137,8 +133,8 @@ class LoanDisbursement(AccountsController):
gle_map.append(
self.get_gl_dict({
"account": loan_details.disbursement_account,
"against": loan_details.loan_account,
"account": self.disbursement_account,
"against": self.loan_account,
"credit": self.disbursed_amount,
"credit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan",

View File

@ -1,7 +1,7 @@
{
"actions": [],
"autoname": "LM-REP-.####",
"creation": "2019-09-03 14:44:39.977266",
"creation": "2022-01-25 10:30:02.767941",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
@ -13,6 +13,7 @@
"column_break_3",
"company",
"posting_date",
"clearance_date",
"rate_of_interest",
"payroll_payable_account",
"is_term_loan",
@ -37,7 +38,12 @@
"total_penalty_paid",
"total_interest_paid",
"repayment_details",
"amended_from"
"amended_from",
"accounting_details_section",
"payment_account",
"penalty_income_account",
"column_break_36",
"loan_account"
],
"fields": [
{
@ -260,12 +266,52 @@
"fieldname": "repay_from_salary",
"fieldtype": "Check",
"label": "Repay From Salary"
},
{
"fieldname": "clearance_date",
"fieldtype": "Date",
"label": "Clearance Date",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "accounting_details_section",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fetch_from": "against_loan.payment_account",
"fieldname": "payment_account",
"fieldtype": "Link",
"label": "Repayment Account",
"options": "Account",
"read_only": 1
},
{
"fieldname": "column_break_36",
"fieldtype": "Column Break"
},
{
"fetch_from": "against_loan.loan_account",
"fieldname": "loan_account",
"fieldtype": "Link",
"label": "Loan Account",
"options": "Account",
"read_only": 1
},
{
"fetch_from": "against_loan.penalty_income_account",
"fieldname": "penalty_income_account",
"fieldtype": "Link",
"hidden": 1,
"label": "Penalty Income Account",
"options": "Account"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-01-06 01:51:06.707782",
"modified": "2022-02-18 19:10:07.742298",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Repayment",

View File

@ -310,7 +310,6 @@ class LoanRepayment(AccountsController):
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
loan_details = frappe.get_doc("Loan", self.against_loan)
if self.shortfall_amount and self.amount_paid > self.shortfall_amount:
remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(self.shortfall_amount,
@ -323,13 +322,13 @@ class LoanRepayment(AccountsController):
if self.repay_from_salary:
payment_account = self.payroll_payable_account
else:
payment_account = loan_details.payment_account
payment_account = self.payment_account
if self.total_penalty_paid:
gle_map.append(
self.get_gl_dict({
"account": loan_details.loan_account,
"against": loan_details.payment_account,
"account": self.loan_account,
"against": payment_account,
"debit": self.total_penalty_paid,
"debit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
@ -344,8 +343,8 @@ class LoanRepayment(AccountsController):
gle_map.append(
self.get_gl_dict({
"account": loan_details.penalty_income_account,
"against": loan_details.loan_account,
"account": self.penalty_income_account,
"against": self.loan_account,
"credit": self.total_penalty_paid,
"credit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
@ -359,8 +358,7 @@ class LoanRepayment(AccountsController):
gle_map.append(
self.get_gl_dict({
"account": payment_account,
"against": loan_details.loan_account + ", " + loan_details.interest_income_account
+ ", " + loan_details.penalty_income_account,
"against": self.loan_account + ", " + self.penalty_income_account,
"debit": self.amount_paid,
"debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
@ -368,16 +366,16 @@ class LoanRepayment(AccountsController):
"remarks": remarks,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date),
"party_type": loan_details.applicant_type if self.repay_from_salary else '',
"party": loan_details.applicant if self.repay_from_salary else ''
"party_type": self.applicant_type if self.repay_from_salary else '',
"party": self.applicant if self.repay_from_salary else ''
})
)
gle_map.append(
self.get_gl_dict({
"account": loan_details.loan_account,
"party_type": loan_details.applicant_type,
"party": loan_details.applicant,
"account": self.loan_account,
"party_type": self.applicant_type,
"party": self.applicant,
"against": payment_account,
"credit": self.amount_paid,
"credit_in_account_currency": self.amount_paid,

View File

@ -353,4 +353,5 @@ erpnext.patches.v13_0.update_reserved_qty_closed_wo
erpnext.patches.v13_0.update_exchange_rate_settings
erpnext.patches.v14_0.delete_amazon_mws_doctype
erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
erpnext.patches.v13_0.update_accounts_in_loan_docs
erpnext.patches.v14_0.update_batch_valuation_flag

View File

@ -0,0 +1,37 @@
import frappe
def execute():
ld = frappe.qb.DocType("Loan Disbursement").as_("ld")
lr = frappe.qb.DocType("Loan Repayment").as_("lr")
loan = frappe.qb.DocType("Loan")
frappe.qb.update(
ld
).inner_join(
loan
).on(
loan.name == ld.against_loan
).set(
ld.disbursement_account, loan.disbursement_account
).set(
ld.loan_account, loan.loan_account
).where(
ld.docstatus < 2
).run()
frappe.qb.update(
lr
).inner_join(
loan
).on(
loan.name == lr.against_loan
).set(
lr.payment_account, loan.payment_account
).set(
lr.loan_account, loan.loan_account
).set(
lr.penalty_income_account, loan.penalty_income_account
).where(
lr.docstatus < 2
).run()

View File

@ -181,6 +181,12 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
fieldname: "journal_entry",
onchange: () => this.update_options(),
},
{
fieldtype: "Check",
label: "Loan Repayment",
fieldname: "loan_repayment",
onchange: () => this.update_options(),
},
{
fieldname: "column_break_5",
fieldtype: "Column Break",
@ -191,13 +197,18 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
fieldname: "sales_invoice",
onchange: () => this.update_options(),
},
{
fieldtype: "Check",
label: "Purchase Invoice",
fieldname: "purchase_invoice",
onchange: () => this.update_options(),
},
{
fieldtype: "Check",
label: "Show Only Exact Amount",
fieldname: "exact_match",
onchange: () => this.update_options(),
},
{
fieldname: "column_break_5",
fieldtype: "Column Break",
@ -210,8 +221,8 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
},
{
fieldtype: "Check",
label: "Show Only Exact Amount",
fieldname: "exact_match",
label: "Loan Disbursement",
fieldname: "loan_disbursement",
onchange: () => this.update_options(),
},
{