mirror of
https://github.com/frappe/erpnext.git
synced 2026-10-29 22:57:42 +00:00
fix(accounts): validate bank clearance updates
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user