From 646e2e01d7ea1f50e043169a8a2ad0fad05bf5c2 Mon Sep 17 00:00:00 2001 From: Ishwarya Date: Sun, 15 Feb 2026 23:39:10 +0530 Subject: [PATCH] Run SQL migration tool on repo --- .../check_run/doctype/check_run/check_run.py | 4 +- check_run/overrides/payment_entry.py | 1252 ++++++++--------- 2 files changed, 627 insertions(+), 629 deletions(-) diff --git a/check_run/check_run/doctype/check_run/check_run.py b/check_run/check_run/doctype/check_run/check_run.py index 9a0e43e5..243ff7b2 100644 --- a/check_run/check_run/doctype/check_run/check_run.py +++ b/check_run/check_run/doctype/check_run/check_run.py @@ -211,7 +211,7 @@ def process_check_run(self) -> None: def _process_check_run(self, save: bool = False) -> None: frappe.defaults.set_global_default("check_run_submitting", self.name) - frappe.db.sql("SAVEPOINT process_check_run") + frappe.db.sql("SAVEPOINT process_check_run") # sql:ignore try: __transactions = self.transactions _transactions = json.loads(__transactions) @@ -233,7 +233,7 @@ def _process_check_run(self, save: bool = False) -> None: self.save() self.submit() self.create_and_attach_positive_pay() - frappe.db.sql("RELEASE SAVEPOINT process_check_run") + frappe.db.sql("RELEASE SAVEPOINT process_check_run") # sql:ignore def build_nacha_file(self, settings: CheckRunSettings) -> str: electronic_mop = frappe.get_all( diff --git a/check_run/overrides/payment_entry.py b/check_run/overrides/payment_entry.py index a60d0e28..6075bc47 100644 --- a/check_run/overrides/payment_entry.py +++ b/check_run/overrides/payment_entry.py @@ -1,627 +1,625 @@ -# Copyright (c) 2023, AgriTheory and contributors -# For license information, please see license.txt - -import base64 -import copy - -import frappe -from erpnext.accounts.doctype.payment_entry.payment_entry import ( - PaymentEntry, - add_regional_gl_entries, - get_outstanding_reference_documents, -) -from erpnext.accounts.general_ledger import ( - check_freezing_date, - make_acc_dimensions_offsetting_entry, - make_entry, - process_gl_map, - save_entries, - set_as_cancel, - validate_accounting_period, - validate_against_pcv, - validate_disabled_accounts, -) -from erpnext.accounts.utils import get_payment_ledger_entries, is_immutable_ledger_enabled -from frappe import _, safe_decode -from frappe.core.doctype.file.utils import get_local_image -from frappe.utils import flt, get_link_to_form, now -from frappe.utils.data import getdate - - -class CheckRunPaymentEntry(PaymentEntry): - def make_gl_entries(self, cancel=0, adv_adj=0): - """ - HASH: 86853224c3df3089c78b09916c1d26a63bd9751e - REPO: https://github.com/frappe/erpnext/ - PATH: erpnext/accounts/doctype/payment_entry/payment_entry.py - METHOD: make_gl_entries - - This method overrides both build_gl_map and make_gl_entries - """ - if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"): - self.setup_party_account_field() - self.set_transaction_currency_and_rate() - - voided_date = None - if self.status == "Voided": - # voided_date field is set via the dialog from the UI - voided_date = frappe.get_value(self.doctype, self.name, "voided_date") or getdate() - original_posting_date = self.posting_date - self.voided_date = self.posting_date = voided_date - - gl_entries = [] - self.add_party_gl_entries(gl_entries) - self.add_bank_gl_entries(gl_entries) - self.add_deductions_gl_entries(gl_entries) - self.add_tax_gl_entries(gl_entries) - add_regional_gl_entries(gl_entries, self) - - gl_entries = process_gl_map(gl_entries) - make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj, voided_date=voided_date) - - if self.status == "Voided": - self.posting_date = original_posting_date - - def set_status(self): - """ - HASH: 86853224c3df3089c78b09916c1d26a63bd9751e - REPO: https://github.com/frappe/erpnext/ - PATH: erpnext/accounts/doctype/payment_entry/payment_entry.py - METHOD: set_status - """ - if self.status == "Voided": - pass - elif self.docstatus == 2: - self.status = "Cancelled" - elif self.docstatus == 1: - self.status = "Submitted" - else: - self.status = "Draft" - - self.db_set("status", self.status, update_modified=True) - - # Bug Fix - def get_valid_reference_doctypes(self): - """ - HASH: 86853224c3df3089c78b09916c1d26a63bd9751e - REPO: https://github.com/frappe/erpnext/ - PATH: erpnext/accounts/doctype/payment_entry/payment_entry.py - METHOD: get_valid_reference_doctypes - """ - if self.party_type == "Customer": - return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning") - elif self.party_type == "Supplier": - return ("Purchase Order", "Purchase Invoice", "Journal Entry") - elif self.party_type == "Shareholder": - return ("Journal Entry",) - elif self.party_type == "Employee": - return ("Journal Entry", "Expense Claim") # Expense Claim - - """ - Because Check Run processes multiple payment entries in a background queue, errors generally do not include - enough data to identify the problem since there were written and remain appropriate for the context of an individual - Payment Entry. This code is copied from: - - https://github.com/frappe/erpnext/blob/version-14/erpnext/accounts/doctype/payment_entry/payment_entry.py#L164 - - https://github.com/frappe/erpnext/blob/version-14/erpnext/accounts/doctype/payment_entry/payment_entry.py#L194 - """ - - def validate_allocated_amount(self): - """ - HASH: 86853224c3df3089c78b09916c1d26a63bd9751e - REPO: https://github.com/frappe/erpnext/ - PATH: erpnext/accounts/doctype/payment_entry/payment_entry.py - METHOD: validate_allocated_amount - """ - if self.payment_type == "Internal Transfer": - return - - if self.party_type in ("Customer", "Supplier"): - self.validate_allocated_amount_with_latest_data() - else: - fail_message = _( - "{0} Row {1} / {2}: Allocated Amount of {3} cannot be greater than outstanding amount of {4}." - ) - for d in self.get("references"): - if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(d.outstanding_amount): - frappe.throw( - fail_message.format( - self.party_name, - d.idx, - get_link_to_form(d.reference_doctype, d.reference_name), - d.allocated_amount, - d.outstanding_amount, - ) - ) - - # Check for negative outstanding invoices as well - if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount): - frappe.throw( - fail_message.format( - self.party_name, - d.idx, - get_link_to_form(d.reference_doctype, d.reference_name), - d.allocated_amount, - d.outstanding_amount, - ) - ) - - def validate_allocated_amount_with_latest_data(self): - """ - HASH: 86853224c3df3089c78b09916c1d26a63bd9751e - REPO: https://github.com/frappe/erpnext/ - PATH: erpnext/accounts/doctype/payment_entry/payment_entry.py - METHOD: validate_allocated_amount_with_latest_data - """ - if self.references: - unique_vouchers = {(x.reference_doctype, x.reference_name) for x in self.references} - vouchers = [frappe._dict({"voucher_type": x[0], "voucher_no": x[1]}) for x in unique_vouchers] - latest_references = get_outstanding_reference_documents( - { - "posting_date": self.posting_date, - "company": self.company, - "party_type": self.party_type, - "payment_type": self.payment_type, - "party": self.party, - "party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to, - "get_outstanding_invoices": True, - "get_orders_to_be_billed": True, - "vouchers": vouchers, - } - ) - - # Group latest_references by (voucher_type, voucher_no) - latest_lookup = {} - for d in latest_references: - d = frappe._dict(d) - latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d - - for idx, d in enumerate(self.get("references"), start=1): - latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict() - - # If term based allocation is enabled, throw - if ( - d.payment_term is None or d.payment_term == "" - ) and self.term_based_allocation_enabled_for_reference( - d.reference_doctype, d.reference_name - ): - frappe.throw( - _( - "{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section" - ).format(frappe.bold(d.reference_name), frappe.bold(idx)) - ) - - # if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key - latest = latest.get(d.payment_term) or latest.get(None) - # The reference has already been fully paid - if not latest: - frappe.throw( - _("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name) - ) - # The reference has already been partly paid - elif ( - latest.outstanding_amount < latest.invoice_amount - and flt(d.outstanding_amount, d.precision("outstanding_amount")) - != flt(latest.outstanding_amount, d.precision("outstanding_amount")) - and d.payment_term == "" - ): - frappe.throw( - _( - "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts." - ).format(_(d.reference_doctype), d.reference_name) - ) - - fail_message = _( - "Row #{1} {0} / {2}: Allocated Amount of {3} cannot be greater than outstanding amount of {4}." - ) - - if ( - d.payment_term - and ( - (flt(d.allocated_amount)) > 0 - and latest.payment_term_outstanding - and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding)) - ) - and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name) - ): - frappe.throw( - _( - "Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}" - ).format( - d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term - ) - ) - - if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount): - frappe.throw( - fail_message.format( - self.party_name, - d.idx, - get_link_to_form(d.reference_doctype, d.reference_name), - d.allocated_amount, - d.outstanding_amount, - ) - ) - - # Check for negative outstanding invoices as well - if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount): - frappe.throw( - fail_message.format( - self.party_name, - d.idx, - get_link_to_form(d.reference_doctype, d.reference_name), - d.allocated_amount, - d.outstanding_amount, - ) - ) - - -def make_gl_entries( - gl_map, - cancel=False, - adv_adj=False, - merge_entries=True, - update_outstanding="Yes", - from_repost=False, - voided_date=None, # CUSTOM CODE -): - """ - HASH: a2b6e4a1c587ce2f7e017f39944899f76e3e2f7d - REPO: https://github.com/frappe/erpnext/ - PATH: erpnext/accounts/general_ledger.py - METHOD: make_gl_entries - """ - if gl_map: - if not cancel: - make_acc_dimensions_offsetting_entry(gl_map) - validate_accounting_period(gl_map) - validate_disabled_accounts(gl_map) - gl_map = process_gl_map(gl_map, merge_entries, from_repost=from_repost) - if gl_map and len(gl_map) > 1: - if gl_map[0].voucher_type != "Period Closing Voucher": - create_payment_ledger_entry( - gl_map, - cancel=0, - adv_adj=adv_adj, - update_outstanding=update_outstanding, - from_repost=from_repost, - voided_date=voided_date, # CUSTOM CODE - ) - save_entries(gl_map, adv_adj, update_outstanding, from_repost) - # Post GL Map process there may no be any GL Entries - elif gl_map: - frappe.throw( - _( - "Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction." - ) - ) - else: - make_reverse_gl_entries( - gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding, voided_date=voided_date - ) # CUSTOM CODE - - -def make_reverse_gl_entries( - gl_entries=None, - voucher_type=None, - voucher_no=None, - adv_adj=False, - update_outstanding="Yes", - partial_cancel=False, - voided_date=None, # CUSTOM CODE -): - """ - HASH: a2b6e4a1c587ce2f7e017f39944899f76e3e2f7d - REPO: https://github.com/frappe/erpnext/ - PATH: erpnext/accounts/general_ledger.py - METHOD: make_reverse_gl_entries - - Get original gl entries of the voucher - and make reverse gl entries by swapping debit and credit - """ - - immutable_ledger_enabled = is_immutable_ledger_enabled() - - if not gl_entries: - gl_entry = frappe.qb.DocType("GL Entry") - gl_entries = ( - frappe.qb.from_(gl_entry) - .select("*") - .where(gl_entry.voucher_type == voucher_type) - .where(gl_entry.voucher_no == voucher_no) - .where(gl_entry.is_cancelled == 0) - .for_update() - ).run(as_dict=1) - - if gl_entries: - create_payment_ledger_entry( - gl_entries, - cancel=1, - adv_adj=adv_adj, - update_outstanding=update_outstanding, - partial_cancel=partial_cancel, - voided_date=voided_date, # CUSTOM CODE - ) - validate_accounting_period(gl_entries) - check_freezing_date(gl_entries[0]["posting_date"], adv_adj) - - is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries) - validate_against_pcv(is_opening, gl_entries[0]["posting_date"], gl_entries[0]["company"]) - if partial_cancel: - # Partial cancel is only used by `Advance` in separate account feature. - # Only cancel GL entries for unlinked reference using `voucher_detail_no` - gle = frappe.qb.DocType("GL Entry") - for x in gl_entries: - query = ( - frappe.qb.update(gle) - .set(gle.modified, now()) - .set(gle.modified_by, frappe.session.user) - .where( - (gle.company == x.company) - & (gle.account == x.account) - & (gle.party_type == x.party_type) - & (gle.party == x.party) - & (gle.voucher_type == x.voucher_type) - & (gle.voucher_no == x.voucher_no) - & (gle.against_voucher_type == x.against_voucher_type) - & (gle.against_voucher == x.against_voucher) - & (gle.voucher_detail_no == x.voucher_detail_no) - ) - ) - - if not immutable_ledger_enabled: - query = query.set(gle.is_cancelled, True) - - query.run() - else: - if not immutable_ledger_enabled: - gle_names = [x.get("name") for x in gl_entries] - - # if names are available, cancel only that set of entries - if not all(gle_names): - set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"]) - else: - frappe.db.sql( - """UPDATE `tabGL Entry` SET is_cancelled = 1, - modified=%s, modified_by=%s - where name in %s and is_cancelled = 0""", - (now(), frappe.session.user, tuple(gle_names)), - ) - - for entry in gl_entries: - new_gle = copy.deepcopy(entry) - new_gle["name"] = None - debit = new_gle.get("debit", 0) - credit = new_gle.get("credit", 0) - - debit_in_account_currency = new_gle.get("debit_in_account_currency", 0) - credit_in_account_currency = new_gle.get("credit_in_account_currency", 0) - debit_in_transaction_currency = new_gle.get("debit_in_transaction_currency", 0) - credit_in_transaction_currency = new_gle.get("credit_in_transaction_currency", 0) - - new_gle["debit"] = credit - new_gle["credit"] = debit - new_gle["debit_in_account_currency"] = credit_in_account_currency - new_gle["credit_in_account_currency"] = debit_in_account_currency - new_gle["debit_in_transaction_currency"] = credit_in_transaction_currency - new_gle["credit_in_transaction_currency"] = debit_in_transaction_currency - - new_gle["remarks"] = "On cancellation of " + new_gle["voucher_no"] - new_gle["is_cancelled"] = 1 - - if immutable_ledger_enabled: - new_gle["is_cancelled"] = 0 - new_gle["posting_date"] = frappe.form_dict.get("posting_date") or getdate() - - if new_gle["debit"] or new_gle["credit"]: - make_entry(new_gle, adv_adj, "Yes") - - -def create_payment_ledger_entry( - gl_entries, - cancel=0, - adv_adj=0, - update_outstanding="Yes", - from_repost=0, - partial_cancel=False, - voided_date=None, # CUSTOM CODE -): - """ - HASH: f039bfe35a575272049534bac9aa771260691bde - REPO: https://github.com/frappe/erpnext/ - PATH: erpnext/accounts/utils.py - METHOD: create_payment_ledger_entry - """ - if gl_entries: - ple_map = get_payment_ledger_entries(gl_entries, cancel=cancel) - - for entry in ple_map: - ple = frappe.get_doc(entry) - - if cancel: - delink_original_entry(ple, partial_cancel=partial_cancel, voided_date=voided_date) - if is_immutable_ledger_enabled(): - ple.delinked = 0 - ple.posting_date = frappe.form_dict.get("posting_date") or getdate() - elif voided_date: - ple.delinked = 0 - ple.posting_date = voided_date - - ple.flags.ignore_permissions = 1 - ple.flags.adv_adj = adv_adj - ple.flags.from_repost = from_repost - ple.flags.update_outstanding = update_outstanding - ple.submit() - - -def delink_original_entry(pl_entry, partial_cancel=False, voided_date=None): # CUSTOM CODE - """ - HASH: f039bfe35a575272049534bac9aa771260691bde - REPO: https://github.com/frappe/erpnext/ - PATH: erpnext/accounts/utils.py - METHOD: delink_original_entry - """ - if not pl_entry: - return - - if pl_entry.doctype == "Advance Payment Ledger Entry": - adv = frappe.qb.DocType("Advance Payment Ledger Entry") - - ( - frappe.qb.update(adv) - .set(adv.delinked, 1) - .set(adv.event, "Cancel") - .set(adv.modified, now()) - .set(adv.modified_by, frappe.session.user) - .where(adv.voucher_type == pl_entry.voucher_type) - .where(adv.voucher_no == pl_entry.voucher_no) - .where(adv.against_voucher_type == pl_entry.against_voucher_type) - .where(adv.against_voucher_no == pl_entry.against_voucher_no) - .where(adv.event == pl_entry.event) - .run() - ) - - else: - ple = frappe.qb.DocType("Payment Ledger Entry") - query = ( - frappe.qb.update(ple) - .set(ple.modified, now()) - .set(ple.modified_by, frappe.session.user) - .where( - (ple.company == pl_entry.company) - & (ple.account_type == pl_entry.account_type) - & (ple.account == pl_entry.account) - & (ple.party_type == pl_entry.party_type) - & (ple.party == pl_entry.party) - & (ple.voucher_type == pl_entry.voucher_type) - & (ple.voucher_no == pl_entry.voucher_no) - & (ple.against_voucher_type == pl_entry.against_voucher_type) - & (ple.against_voucher_no == pl_entry.against_voucher_no) - ) - ) - - if partial_cancel: - query = query.where(ple.voucher_detail_no == pl_entry.voucher_detail_no) - - if not (is_immutable_ledger_enabled() or voided_date): # CUSTOM CODE - query = query.set(ple.delinked, True) - - query.run() - - -@frappe.whitelist() -def update_check_number(doc: PaymentEntry, method: str | None = None) -> None: - mode_of_payment_type = frappe.db.get_value("Mode of Payment", doc.mode_of_payment, "type") - if doc.bank_account and mode_of_payment_type == "Bank" and str(doc.reference_no).isdigit(): - frappe.db.set_value("Bank Account", doc.bank_account, "check_number", doc.reference_no) - - -@frappe.whitelist() -def validate_duplicate_check_number(doc: PaymentEntry, method: str | None = None) -> None: - check_run_settings = frappe.db.exists( - "Check Run Settings", {"bank_account": doc.bank_account, "pay_to_account": doc.paid_to} - ) - if not check_run_settings or not frappe.db.get_value( - "Check Run Settings", check_run_settings, "validate_unique_check_number" - ): - return - - mode_of_payment_type = frappe.db.get_value("Mode of Payment", doc.mode_of_payment, "type") - if mode_of_payment_type != "Bank" or not str(doc.reference_no).isdigit(): - return - - pe_names = frappe.get_all( - "Payment Entry", - { - "payment_type": "Pay", - "name": ["!=", doc.name], - "docstatus": ["!=", 2], - "reference_no": doc.reference_no, - }, - ) - if not pe_names: - return - - error_message = "
  • ".join([get_link_to_form("Payment Entry", p.name) for p in pe_names]) - frappe.throw( - msg=frappe._( - f"Check Number {doc.reference_no} is already set in:

    " - ), - title="Check Number already exists", - ) - - -@frappe.whitelist() -def update_outstanding_amount(doc: PaymentEntry, method: str | None = None): - paid_amount = doc.paid_amount if method == "on_submit" else 0.0 - for r in doc.get("references"): - if r.reference_doctype != "Purchase Invoice": - continue - payment_schedules = frappe.get_all( - "Payment Schedule", - {"parent": r.reference_name}, - ["name", "outstanding", "payment_term", "payment_amount"], - order_by="due_date ASC", - ) - if not payment_schedules: - continue - - payment_schedule = frappe.get_doc("Payment Schedule", payment_schedules[0]["name"]) - precision = payment_schedule.precision("outstanding") - payment_schedules = payment_schedules if method == "on_submit" else reversed(payment_schedules) - - for term in payment_schedules: - if r.payment_term and term.payment_term != r.payment_term: - continue - - if method == "on_submit": - if term.outstanding > 0.0 and paid_amount > 0.0: - if term.outstanding > paid_amount: - frappe.db.set_value( - "Payment Schedule", - term.name, - "outstanding", - flt(term.outstanding - paid_amount, precision), - ) - break - else: - paid_amount = flt(paid_amount - term.outstanding, precision) - frappe.db.set_value("Payment Schedule", term.name, "outstanding", 0) - if paid_amount <= 0.0: - break - - if method == "on_cancel": - if term.outstanding != term.payment_amount: - # if this payment term had previously been allocated against - paid_amount += flt(paid_amount + (term.payment_amount - term.outstanding), precision) - reverse = ( - flt(paid_amount + term.outstanding, precision) - if paid_amount < term.payment_amount - else term.payment_amount - ) - frappe.db.set_value("Payment Schedule", term.name, "outstanding", reverse) - if paid_amount >= doc.paid_amount: - break - - -@frappe.whitelist() -def get_image_base64_data(file_url): - file_doc = frappe.get_doc("File", {"file_url": file_url}) - if not file_doc.has_permission(permtype="read"): - return "" - image, unused_filename, extn = get_local_image(file_url) - file_content = file_doc.get_content() - return f"data:image/{extn};base64,{safe_decode(base64.b64encode(file_content).decode('utf-8'))}" - - -@frappe.whitelist() -def set_voided_date(doctype, docname, voided_date): - doc = frappe.get_doc(doctype, docname) - voided_date = getdate(voided_date) - if voided_date < doc.posting_date: - frappe.throw( - msg=_("Void As Of Date cannot be before the Payment Entry's posting date."), - title=_("Invalid Void As Of Date"), - ) - frappe.db.set_value(doctype, docname, "voided_date", voided_date, update_modified=False) +# Copyright (c) 2023, AgriTheory and contributors +# For license information, please see license.txt + +import base64 +import copy + +import frappe +from erpnext.accounts.doctype.payment_entry.payment_entry import ( + PaymentEntry, + add_regional_gl_entries, + get_outstanding_reference_documents, +) +from erpnext.accounts.general_ledger import ( + check_freezing_date, + make_acc_dimensions_offsetting_entry, + make_entry, + process_gl_map, + save_entries, + set_as_cancel, + validate_accounting_period, + validate_against_pcv, + validate_disabled_accounts, +) +from erpnext.accounts.utils import get_payment_ledger_entries, is_immutable_ledger_enabled +from frappe import _, safe_decode +from frappe.core.doctype.file.utils import get_local_image +from frappe.utils import flt, get_link_to_form, now +from frappe.utils.data import getdate + + +class CheckRunPaymentEntry(PaymentEntry): + def make_gl_entries(self, cancel=0, adv_adj=0): + """ + HASH: 86853224c3df3089c78b09916c1d26a63bd9751e + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/accounts/doctype/payment_entry/payment_entry.py + METHOD: make_gl_entries + + This method overrides both build_gl_map and make_gl_entries + """ + if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"): + self.setup_party_account_field() + self.set_transaction_currency_and_rate() + + voided_date = None + if self.status == "Voided": + # voided_date field is set via the dialog from the UI + voided_date = frappe.get_value(self.doctype, self.name, "voided_date") or getdate() + original_posting_date = self.posting_date + self.voided_date = self.posting_date = voided_date + + gl_entries = [] + self.add_party_gl_entries(gl_entries) + self.add_bank_gl_entries(gl_entries) + self.add_deductions_gl_entries(gl_entries) + self.add_tax_gl_entries(gl_entries) + add_regional_gl_entries(gl_entries, self) + + gl_entries = process_gl_map(gl_entries) + make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj, voided_date=voided_date) + + if self.status == "Voided": + self.posting_date = original_posting_date + + def set_status(self): + """ + HASH: 86853224c3df3089c78b09916c1d26a63bd9751e + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/accounts/doctype/payment_entry/payment_entry.py + METHOD: set_status + """ + if self.status == "Voided": + pass + elif self.docstatus == 2: + self.status = "Cancelled" + elif self.docstatus == 1: + self.status = "Submitted" + else: + self.status = "Draft" + + self.db_set("status", self.status, update_modified=True) + + # Bug Fix + def get_valid_reference_doctypes(self): + """ + HASH: 86853224c3df3089c78b09916c1d26a63bd9751e + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/accounts/doctype/payment_entry/payment_entry.py + METHOD: get_valid_reference_doctypes + """ + if self.party_type == "Customer": + return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning") + elif self.party_type == "Supplier": + return ("Purchase Order", "Purchase Invoice", "Journal Entry") + elif self.party_type == "Shareholder": + return ("Journal Entry",) + elif self.party_type == "Employee": + return ("Journal Entry", "Expense Claim") # Expense Claim + + """ + Because Check Run processes multiple payment entries in a background queue, errors generally do not include + enough data to identify the problem since there were written and remain appropriate for the context of an individual + Payment Entry. This code is copied from: + + https://github.com/frappe/erpnext/blob/version-14/erpnext/accounts/doctype/payment_entry/payment_entry.py#L164 + + https://github.com/frappe/erpnext/blob/version-14/erpnext/accounts/doctype/payment_entry/payment_entry.py#L194 + """ + + def validate_allocated_amount(self): + """ + HASH: 86853224c3df3089c78b09916c1d26a63bd9751e + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/accounts/doctype/payment_entry/payment_entry.py + METHOD: validate_allocated_amount + """ + if self.payment_type == "Internal Transfer": + return + + if self.party_type in ("Customer", "Supplier"): + self.validate_allocated_amount_with_latest_data() + else: + fail_message = _( + "{0} Row {1} / {2}: Allocated Amount of {3} cannot be greater than outstanding amount of {4}." + ) + for d in self.get("references"): + if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(d.outstanding_amount): + frappe.throw( + fail_message.format( + self.party_name, + d.idx, + get_link_to_form(d.reference_doctype, d.reference_name), + d.allocated_amount, + d.outstanding_amount, + ) + ) + + # Check for negative outstanding invoices as well + if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount): + frappe.throw( + fail_message.format( + self.party_name, + d.idx, + get_link_to_form(d.reference_doctype, d.reference_name), + d.allocated_amount, + d.outstanding_amount, + ) + ) + + def validate_allocated_amount_with_latest_data(self): + """ + HASH: 86853224c3df3089c78b09916c1d26a63bd9751e + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/accounts/doctype/payment_entry/payment_entry.py + METHOD: validate_allocated_amount_with_latest_data + """ + if self.references: + unique_vouchers = {(x.reference_doctype, x.reference_name) for x in self.references} + vouchers = [frappe._dict({"voucher_type": x[0], "voucher_no": x[1]}) for x in unique_vouchers] + latest_references = get_outstanding_reference_documents( + { + "posting_date": self.posting_date, + "company": self.company, + "party_type": self.party_type, + "payment_type": self.payment_type, + "party": self.party, + "party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to, + "get_outstanding_invoices": True, + "get_orders_to_be_billed": True, + "vouchers": vouchers, + } + ) + + # Group latest_references by (voucher_type, voucher_no) + latest_lookup = {} + for d in latest_references: + d = frappe._dict(d) + latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d + + for idx, d in enumerate(self.get("references"), start=1): + latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict() + + # If term based allocation is enabled, throw + if ( + d.payment_term is None or d.payment_term == "" + ) and self.term_based_allocation_enabled_for_reference( + d.reference_doctype, d.reference_name + ): + frappe.throw( + _( + "{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section" + ).format(frappe.bold(d.reference_name), frappe.bold(idx)) + ) + + # if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key + latest = latest.get(d.payment_term) or latest.get(None) + # The reference has already been fully paid + if not latest: + frappe.throw( + _("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name) + ) + # The reference has already been partly paid + elif ( + latest.outstanding_amount < latest.invoice_amount + and flt(d.outstanding_amount, d.precision("outstanding_amount")) + != flt(latest.outstanding_amount, d.precision("outstanding_amount")) + and d.payment_term == "" + ): + frappe.throw( + _( + "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts." + ).format(_(d.reference_doctype), d.reference_name) + ) + + fail_message = _( + "Row #{1} {0} / {2}: Allocated Amount of {3} cannot be greater than outstanding amount of {4}." + ) + + if ( + d.payment_term + and ( + (flt(d.allocated_amount)) > 0 + and latest.payment_term_outstanding + and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding)) + ) + and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name) + ): + frappe.throw( + _( + "Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}" + ).format( + d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term + ) + ) + + if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount): + frappe.throw( + fail_message.format( + self.party_name, + d.idx, + get_link_to_form(d.reference_doctype, d.reference_name), + d.allocated_amount, + d.outstanding_amount, + ) + ) + + # Check for negative outstanding invoices as well + if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount): + frappe.throw( + fail_message.format( + self.party_name, + d.idx, + get_link_to_form(d.reference_doctype, d.reference_name), + d.allocated_amount, + d.outstanding_amount, + ) + ) + + +def make_gl_entries( + gl_map, + cancel=False, + adv_adj=False, + merge_entries=True, + update_outstanding="Yes", + from_repost=False, + voided_date=None, # CUSTOM CODE +): + """ + HASH: a2b6e4a1c587ce2f7e017f39944899f76e3e2f7d + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/accounts/general_ledger.py + METHOD: make_gl_entries + """ + if gl_map: + if not cancel: + make_acc_dimensions_offsetting_entry(gl_map) + validate_accounting_period(gl_map) + validate_disabled_accounts(gl_map) + gl_map = process_gl_map(gl_map, merge_entries, from_repost=from_repost) + if gl_map and len(gl_map) > 1: + if gl_map[0].voucher_type != "Period Closing Voucher": + create_payment_ledger_entry( + gl_map, + cancel=0, + adv_adj=adv_adj, + update_outstanding=update_outstanding, + from_repost=from_repost, + voided_date=voided_date, # CUSTOM CODE + ) + save_entries(gl_map, adv_adj, update_outstanding, from_repost) + # Post GL Map process there may no be any GL Entries + elif gl_map: + frappe.throw( + _( + "Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction." + ) + ) + else: + make_reverse_gl_entries( + gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding, voided_date=voided_date + ) # CUSTOM CODE + + +def make_reverse_gl_entries( + gl_entries=None, + voucher_type=None, + voucher_no=None, + adv_adj=False, + update_outstanding="Yes", + partial_cancel=False, + voided_date=None, # CUSTOM CODE +): + """ + HASH: a2b6e4a1c587ce2f7e017f39944899f76e3e2f7d + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/accounts/general_ledger.py + METHOD: make_reverse_gl_entries + + Get original gl entries of the voucher + and make reverse gl entries by swapping debit and credit + """ + + immutable_ledger_enabled = is_immutable_ledger_enabled() + + if not gl_entries: + gl_entry = frappe.qb.DocType("GL Entry") + gl_entries = ( + frappe.qb.from_(gl_entry) + .select("*") + .where(gl_entry.voucher_type == voucher_type) + .where(gl_entry.voucher_no == voucher_no) + .where(gl_entry.is_cancelled == 0) + .for_update() + ).run(as_dict=1) + + if gl_entries: + create_payment_ledger_entry( + gl_entries, + cancel=1, + adv_adj=adv_adj, + update_outstanding=update_outstanding, + partial_cancel=partial_cancel, + voided_date=voided_date, # CUSTOM CODE + ) + validate_accounting_period(gl_entries) + check_freezing_date(gl_entries[0]["posting_date"], adv_adj) + + is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries) + validate_against_pcv(is_opening, gl_entries[0]["posting_date"], gl_entries[0]["company"]) + if partial_cancel: + # Partial cancel is only used by `Advance` in separate account feature. + # Only cancel GL entries for unlinked reference using `voucher_detail_no` + gle = frappe.qb.DocType("GL Entry") + for x in gl_entries: + query = ( + frappe.qb.update(gle) + .set(gle.modified, now()) + .set(gle.modified_by, frappe.session.user) + .where( + (gle.company == x.company) + & (gle.account == x.account) + & (gle.party_type == x.party_type) + & (gle.party == x.party) + & (gle.voucher_type == x.voucher_type) + & (gle.voucher_no == x.voucher_no) + & (gle.against_voucher_type == x.against_voucher_type) + & (gle.against_voucher == x.against_voucher) + & (gle.voucher_detail_no == x.voucher_detail_no) + ) + ) + + if not immutable_ledger_enabled: + query = query.set(gle.is_cancelled, True) + + query.run() + else: + if not immutable_ledger_enabled: + gle_names = [x.get("name") for x in gl_entries] + + # if names are available, cancel only that set of entries + if not all(gle_names): + set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"]) + else: + GLEntry = frappe.qb.DocType("GL Entry") + frappe.qb.update(GLEntry).set(GLEntry.is_cancelled, 1).set(GLEntry.modified, "now()").set( + GLEntry.modified_by, frappe.session.user + ).where(GLEntry.name.isin([]) & (GLEntry.is_cancelled == 0)).run() + + for entry in gl_entries: + new_gle = copy.deepcopy(entry) + new_gle["name"] = None + debit = new_gle.get("debit", 0) + credit = new_gle.get("credit", 0) + + debit_in_account_currency = new_gle.get("debit_in_account_currency", 0) + credit_in_account_currency = new_gle.get("credit_in_account_currency", 0) + debit_in_transaction_currency = new_gle.get("debit_in_transaction_currency", 0) + credit_in_transaction_currency = new_gle.get("credit_in_transaction_currency", 0) + + new_gle["debit"] = credit + new_gle["credit"] = debit + new_gle["debit_in_account_currency"] = credit_in_account_currency + new_gle["credit_in_account_currency"] = debit_in_account_currency + new_gle["debit_in_transaction_currency"] = credit_in_transaction_currency + new_gle["credit_in_transaction_currency"] = debit_in_transaction_currency + + new_gle["remarks"] = "On cancellation of " + new_gle["voucher_no"] + new_gle["is_cancelled"] = 1 + + if immutable_ledger_enabled: + new_gle["is_cancelled"] = 0 + new_gle["posting_date"] = frappe.form_dict.get("posting_date") or getdate() + + if new_gle["debit"] or new_gle["credit"]: + make_entry(new_gle, adv_adj, "Yes") + + +def create_payment_ledger_entry( + gl_entries, + cancel=0, + adv_adj=0, + update_outstanding="Yes", + from_repost=0, + partial_cancel=False, + voided_date=None, # CUSTOM CODE +): + """ + HASH: f039bfe35a575272049534bac9aa771260691bde + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/accounts/utils.py + METHOD: create_payment_ledger_entry + """ + if gl_entries: + ple_map = get_payment_ledger_entries(gl_entries, cancel=cancel) + + for entry in ple_map: + ple = frappe.get_doc(entry) + + if cancel: + delink_original_entry(ple, partial_cancel=partial_cancel, voided_date=voided_date) + if is_immutable_ledger_enabled(): + ple.delinked = 0 + ple.posting_date = frappe.form_dict.get("posting_date") or getdate() + elif voided_date: + ple.delinked = 0 + ple.posting_date = voided_date + + ple.flags.ignore_permissions = 1 + ple.flags.adv_adj = adv_adj + ple.flags.from_repost = from_repost + ple.flags.update_outstanding = update_outstanding + ple.submit() + + +def delink_original_entry(pl_entry, partial_cancel=False, voided_date=None): # CUSTOM CODE + """ + HASH: f039bfe35a575272049534bac9aa771260691bde + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/accounts/utils.py + METHOD: delink_original_entry + """ + if not pl_entry: + return + + if pl_entry.doctype == "Advance Payment Ledger Entry": + adv = frappe.qb.DocType("Advance Payment Ledger Entry") + + ( + frappe.qb.update(adv) + .set(adv.delinked, 1) + .set(adv.event, "Cancel") + .set(adv.modified, now()) + .set(adv.modified_by, frappe.session.user) + .where(adv.voucher_type == pl_entry.voucher_type) + .where(adv.voucher_no == pl_entry.voucher_no) + .where(adv.against_voucher_type == pl_entry.against_voucher_type) + .where(adv.against_voucher_no == pl_entry.against_voucher_no) + .where(adv.event == pl_entry.event) + .run() + ) + + else: + ple = frappe.qb.DocType("Payment Ledger Entry") + query = ( + frappe.qb.update(ple) + .set(ple.modified, now()) + .set(ple.modified_by, frappe.session.user) + .where( + (ple.company == pl_entry.company) + & (ple.account_type == pl_entry.account_type) + & (ple.account == pl_entry.account) + & (ple.party_type == pl_entry.party_type) + & (ple.party == pl_entry.party) + & (ple.voucher_type == pl_entry.voucher_type) + & (ple.voucher_no == pl_entry.voucher_no) + & (ple.against_voucher_type == pl_entry.against_voucher_type) + & (ple.against_voucher_no == pl_entry.against_voucher_no) + ) + ) + + if partial_cancel: + query = query.where(ple.voucher_detail_no == pl_entry.voucher_detail_no) + + if not (is_immutable_ledger_enabled() or voided_date): # CUSTOM CODE + query = query.set(ple.delinked, True) + + query.run() + + +@frappe.whitelist() +def update_check_number(doc: PaymentEntry, method: str | None = None) -> None: + mode_of_payment_type = frappe.db.get_value("Mode of Payment", doc.mode_of_payment, "type") + if doc.bank_account and mode_of_payment_type == "Bank" and str(doc.reference_no).isdigit(): + frappe.db.set_value("Bank Account", doc.bank_account, "check_number", doc.reference_no) + + +@frappe.whitelist() +def validate_duplicate_check_number(doc: PaymentEntry, method: str | None = None) -> None: + check_run_settings = frappe.db.exists( + "Check Run Settings", {"bank_account": doc.bank_account, "pay_to_account": doc.paid_to} + ) + if not check_run_settings or not frappe.db.get_value( + "Check Run Settings", check_run_settings, "validate_unique_check_number" + ): + return + + mode_of_payment_type = frappe.db.get_value("Mode of Payment", doc.mode_of_payment, "type") + if mode_of_payment_type != "Bank" or not str(doc.reference_no).isdigit(): + return + + pe_names = frappe.get_all( + "Payment Entry", + { + "payment_type": "Pay", + "name": ["!=", doc.name], + "docstatus": ["!=", 2], + "reference_no": doc.reference_no, + }, + ) + if not pe_names: + return + + error_message = "
  • ".join([get_link_to_form("Payment Entry", p.name) for p in pe_names]) + frappe.throw( + msg=frappe._( + f"Check Number {doc.reference_no} is already set in:

    " + ), + title="Check Number already exists", + ) + + +@frappe.whitelist() +def update_outstanding_amount(doc: PaymentEntry, method: str | None = None): + paid_amount = doc.paid_amount if method == "on_submit" else 0.0 + for r in doc.get("references"): + if r.reference_doctype != "Purchase Invoice": + continue + payment_schedules = frappe.get_all( + "Payment Schedule", + {"parent": r.reference_name}, + ["name", "outstanding", "payment_term", "payment_amount"], + order_by="due_date ASC", + ) + if not payment_schedules: + continue + + payment_schedule = frappe.get_doc("Payment Schedule", payment_schedules[0]["name"]) + precision = payment_schedule.precision("outstanding") + payment_schedules = payment_schedules if method == "on_submit" else reversed(payment_schedules) + + for term in payment_schedules: + if r.payment_term and term.payment_term != r.payment_term: + continue + + if method == "on_submit": + if term.outstanding > 0.0 and paid_amount > 0.0: + if term.outstanding > paid_amount: + frappe.db.set_value( + "Payment Schedule", + term.name, + "outstanding", + flt(term.outstanding - paid_amount, precision), + ) + break + else: + paid_amount = flt(paid_amount - term.outstanding, precision) + frappe.db.set_value("Payment Schedule", term.name, "outstanding", 0) + if paid_amount <= 0.0: + break + + if method == "on_cancel": + if term.outstanding != term.payment_amount: + # if this payment term had previously been allocated against + paid_amount += flt(paid_amount + (term.payment_amount - term.outstanding), precision) + reverse = ( + flt(paid_amount + term.outstanding, precision) + if paid_amount < term.payment_amount + else term.payment_amount + ) + frappe.db.set_value("Payment Schedule", term.name, "outstanding", reverse) + if paid_amount >= doc.paid_amount: + break + + +@frappe.whitelist() +def get_image_base64_data(file_url): + file_doc = frappe.get_doc("File", {"file_url": file_url}) + if not file_doc.has_permission(permtype="read"): + return "" + image, unused_filename, extn = get_local_image(file_url) + file_content = file_doc.get_content() + return f"data:image/{extn};base64,{safe_decode(base64.b64encode(file_content).decode('utf-8'))}" + + +@frappe.whitelist() +def set_voided_date(doctype, docname, voided_date): + doc = frappe.get_doc(doctype, docname) + voided_date = getdate(voided_date) + if voided_date < doc.posting_date: + frappe.throw( + msg=_("Void As Of Date cannot be before the Payment Entry's posting date."), + title=_("Invalid Void As Of Date"), + ) + frappe.db.set_value(doctype, docname, "voided_date", voided_date, update_modified=False)