fix(accounts): validate bank clearance updates

This commit is contained in:
pandiyan
2026-10-26 13:05:31 +05:30
parent 4ef6cba70b
commit 1e1cd21847
@@ -8,7 +8,7 @@ from frappe.model.document import Document
from frappe.query_builder import Case
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Coalesce, Sum
from frappe.utils import flt, fmt_money, get_link_to_form, getdate
from frappe.utils import flt, fmt_money, getdate
from pypika import Order
import erpnext
@@ -41,27 +41,7 @@ class BankClearance(Document):
@frappe.whitelist()
def get_payment_entries(self):
if not (self.from_date and self.to_date):
frappe.throw(_("From Date and To Date are Mandatory"))
if not self.account:
frappe.throw(_("Account is mandatory to get payment entries"))
entries = []
# get entries from all the apps
for method_name in frappe.get_hooks("get_payment_entries_for_bank_clearance"):
entries += (
frappe.get_attr(method_name)(
self.from_date,
self.to_date,
self.account,
self.bank_account,
self.include_reconciled_entries,
self.include_pos_transactions,
)
or []
)
entries = self.get_entries_for_account()
entries = sorted(
entries,
@@ -88,56 +68,158 @@ class BankClearance(Document):
d.pop("account_currency")
row.update(d)
def get_entries_for_account(self):
if not (self.from_date and self.to_date):
frappe.throw(_("From Date and To Date are Mandatory"))
if not self.account:
frappe.throw(_("Account is mandatory to get payment entries"))
entries = []
# get entries from all the apps
for method_name in frappe.get_hooks("get_payment_entries_for_bank_clearance"):
entries += (
frappe.get_attr(method_name)(
self.from_date,
self.to_date,
self.account,
self.bank_account,
self.include_reconciled_entries,
self.include_pos_transactions,
)
or []
)
return entries
@frappe.whitelist()
def update_clearance_date(self):
payment_docs = []
for d in self.get("payment_entries"):
if d.payment_document not in payment_docs:
payment_docs.append(d.payment_document)
self.check_permission("write")
frappe.get_doc("Account", self.account).check_permission("read")
available_entries = {
(entry.payment_document, entry.payment_entry): entry for entry in self.get_entries_for_account()
}
entries_to_update = []
for row in self.get("payment_entries"):
if not (row.clearance_date or self.include_reconciled_entries):
continue
for doctype in payment_docs:
frappe.has_permission(doctype, "write", throw=True)
entry = available_entries.get((row.payment_document, row.payment_entry))
if not entry:
frappe.throw(
_("Row {0}: Voucher is not eligible for clearance with the selected filters.").format(
row.idx
),
title=_("Invalid Voucher"),
)
for d in self.get("payment_entries"):
if d.clearance_date:
if not d.payment_document:
frappe.throw(_("Row #{0}: Payment document is required to complete the transaction"))
voucher = frappe.get_doc(row.payment_document, row.payment_entry)
if not has_clearance_date_changed(voucher, row.clearance_date, self.account):
continue
if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
frappe.throw(
_("Row #{0}: For {1} Clearance date {2} cannot be before Cheque Date {3}").format(
d.idx,
get_link_to_form(d.payment_document, d.payment_entry),
d.clearance_date,
d.cheque_date,
)
)
if not row.clearance_date:
row.clearance_date = None
if d.clearance_date or self.include_reconciled_entries:
if not d.clearance_date:
d.clearance_date = None
validate_clearance_date(
voucher,
row.clearance_date,
self.account,
row_idx=row.idx,
cheque_date=entry.get("cheque_date"),
)
if d.payment_document == "Sales Invoice":
frappe.db.set_value(
"Sales Invoice Payment",
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
"clearance_date",
d.clearance_date,
)
entries_to_update.append(row)
else:
# using db_set to trigger notification
frappe.db.set_value(
d.payment_document, d.payment_entry, "clearance_date", d.clearance_date
)
clearance_date_updated = True
if clearance_date_updated:
self.get_payment_entries()
msgprint(_("Clearance Date updated"))
else:
if not entries_to_update:
msgprint(_("Clearance Date not mentioned"))
return
for d in entries_to_update:
if d.payment_document == "Sales Invoice":
frappe.db.set_value(
"Sales Invoice Payment",
{"parent": d.payment_entry, "account": self.account, "amount": ["!=", 0]},
"clearance_date",
d.clearance_date,
)
else:
frappe.db.set_value(d.payment_document, d.payment_entry, "clearance_date", d.clearance_date)
self.get_payment_entries()
msgprint(_("Clearance Date updated"))
def validate_clearance_date(voucher, clearance_date, account, row_idx=None, cheque_date=None):
"""Validate manual clearance changes using the stored voucher and bank allocations."""
voucher.check_permission("read")
voucher.check_permission("write")
label = _("Row {0}").format(row_idx) if row_idx else f"{_(voucher.doctype)} {voucher.name}"
if voucher.docstatus != 1:
frappe.throw(
_("{0}: Only submitted vouchers can be cleared.").format(label), title=_("Invalid Voucher")
)
if not has_clearance_date_changed(voucher, clearance_date, account):
return
if clearance_date:
clearance_date = getdate(clearance_date)
if clearance_date < getdate(voucher.posting_date):
frappe.throw(
_("{0}: Clearance date cannot be before posting date.").format(label),
title=_("Invalid Clearance Date"),
)
cheque_date = cheque_date or voucher.get("cheque_date") or voucher.get("reference_date")
if cheque_date and clearance_date < getdate(cheque_date):
frappe.throw(
_("{0}: Clearance date cannot be before cheque date.").format(label),
title=_("Invalid Clearance Date"),
)
transaction = frappe.qb.DocType("Bank Transaction")
payment = frappe.qb.DocType("Bank Transaction Payments")
allocated = (
frappe.qb.from_(transaction)
.inner_join(payment)
.on(payment.parent == transaction.name)
.select(transaction.name)
.where(
(transaction.docstatus == 1)
& (payment.payment_document == voucher.doctype)
& (payment.payment_entry == voucher.name)
& (payment.allocated_amount != 0)
)
.limit(1)
)
if voucher.doctype == "Sales Invoice" and account:
bank_account = frappe.qb.DocType("Bank Account")
allocated = allocated.where(
transaction.bank_account.isin(
frappe.qb.from_(bank_account).select(bank_account.name).where(bank_account.account == account)
)
)
if allocated.run():
frappe.throw(
_(
"{0}: Unreconcile the voucher from its Bank Transaction before changing the clearance date."
).format(label),
title=_("Voucher Already Reconciled"),
)
def has_clearance_date_changed(voucher, clearance_date, account):
clearance_date = getdate(clearance_date) if clearance_date else None
clearance_dates = (
[
payment.clearance_date
for payment in voucher.payments
if (not account or payment.account == account) and payment.amount != 0
]
if voucher.doctype == "Sales Invoice"
else [voucher.clearance_date]
)
return any((getdate(date) if date else None) != clearance_date for date in clearance_dates)
def get_payment_entries_for_bank_clearance(
@@ -265,10 +347,16 @@ def get_payment_entries_for_bank_clearance(
frappe.qb.from_(pi)
.inner_join(acc)
.on(pi.cash_bank_account == acc.name)
.inner_join(company)
.on(pi.company == company.name)
.select(
ConstantColumn("Purchase Invoice").as_("payment_document"),
pi.name.as_("payment_entry"),
pi.paid_amount.as_("credit"),
(
Case()
.when(acc.account_currency == company.default_currency, pi.base_paid_amount)
.else_(pi.paid_amount)
).as_("credit"),
pi.posting_date,
pi.supplier.as_("against_account"),
pi.bill_no.as_("cheque_number"),
@@ -306,11 +394,17 @@ def get_payment_entries_for_bank_clearance(
.on(si_payment.parent == si.name)
.inner_join(acc)
.on(si_payment.account == acc.name)
.inner_join(company)
.on(si.company == company.name)
.select(
ConstantColumn("Sales Invoice").as_("payment_document"),
si.name.as_("payment_entry"),
si_payment.reference_no.as_("cheque_number"),
si_payment.amount.as_("debit"),
(
Case()
.when(acc.account_currency == company.default_currency, si_payment.base_amount)
.else_(si_payment.amount)
).as_("debit"),
si.posting_date,
si.customer.as_("against_account"),
si_payment.clearance_date,