refactor: commonify and improve perf

This commit is contained in:
Sagar Vora
2025-08-14 17:44:32 +05:30
committed by Karm Soni
parent 6c644dd5d2
commit fe2d0ea43b
3 changed files with 70 additions and 52 deletions

View File

@@ -164,70 +164,88 @@ class Dunning(AccountsController):
] ]
def resolve_dunning(doc, state): def resolve_dunnings(doc, method=None):
""" """
Check if all payments have been made and resolve dunning, if yes. Called Resolve / unresolve Dunning based on whether all payments have been made.
when a Payment Entry is submitted. Called when a Payment Entry / Credit Note is submitted / cancelled.
""" """
match doc.doctype:
case "Payment Entry":
return resolve_dunnings_from_payment_entry(doc)
case "Sales Invoice":
return resolve_dunnings_from_credit_note(doc)
def resolve_dunnings_from_payment_entry(doc):
is_submitted = doc.docstatus == 1
for reference in doc.references: for reference in doc.references:
# Consider partial and full payments: if reference.reference_doctype != "Sales Invoice" or not reference.allocated_amount:
# Submitting full payment: outstanding_amount will be 0 continue
# Submitting 1st partial payment: outstanding_amount will be the pending installment
# Cancelling full payment: outstanding_amount will revert to total amount
# Cancelling last partial payment: outstanding_amount will revert to pending amount
submit_condition = reference.outstanding_amount < reference.total_amount
cancel_condition = reference.outstanding_amount <= reference.total_amount
if reference.reference_doctype == "Sales Invoice" and ( _update_linked_dunnings(reference.reference_name, to_resolve=is_submitted)
submit_condition if doc.docstatus == 1 else cancel_condition
):
state = "Resolved" if doc.docstatus == 2 else "Unresolved"
dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state)
for dunning in dunnings:
resolve = True
dunning = frappe.get_doc("Dunning", dunning.get("name"))
for overdue_payment in dunning.overdue_payments:
outstanding_inv = frappe.get_value(
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
)
outstanding_ps = frappe.get_value(
"Payment Schedule", overdue_payment.payment_schedule, "outstanding"
)
resolve = resolve and (False if (outstanding_ps > 0 and outstanding_inv > 0) else True)
new_status = "Resolved" if resolve else "Unresolved"
if dunning.status != new_status:
dunning.status = new_status
dunning.save()
def resolve_dunning_for_credit_note(doc, state): def resolve_dunnings_from_credit_note(doc):
""" """
Check if dunning should be resolved when a credit note is issued against a Sales Invoice. Check if dunning should be resolved when a credit note is issued against a Sales Invoice.
Only process if update_outstanding_for_self is False (credit note is being applied against the original invoice). Only process if update_outstanding_for_self is False (credit note is being applied against the original invoice).
""" """
if not doc.is_return or doc.get("update_outstanding_for_self") or not doc.get("return_against"): if not doc.is_return or doc.update_outstanding_for_self or not doc.return_against:
return return
state = "Resolved" if doc.docstatus == 2 else "Unresolved" _update_linked_dunnings(doc.return_against, to_resolve=doc.docstatus == 1)
dunnings = get_linked_dunnings_as_per_state(doc.return_against, state)
def _update_linked_dunnings(sales_invoice: str, to_resolve: bool = True):
state = "Unresolved" if to_resolve else "Resolved"
dunnings = get_linked_dunnings_as_per_state(sales_invoice, state)
if not dunnings:
return
dunnings = [frappe.get_doc("Dunning", dunning.name) for dunning in dunnings]
invoices = set()
payment_schedule_ids = set()
for dunning in dunnings: for dunning in dunnings:
resolve = True
dunning = frappe.get_doc("Dunning", dunning.get("name"))
for overdue_payment in dunning.overdue_payments: for overdue_payment in dunning.overdue_payments:
outstanding_inv = frappe.get_value( invoices.add(overdue_payment.sales_invoice)
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount" if overdue_payment.payment_schedule:
) payment_schedule_ids.add(overdue_payment.payment_schedule)
outstanding_ps = frappe.get_value(
"Payment Schedule", overdue_payment.payment_schedule, "outstanding"
)
resolve = resolve and (False if (outstanding_ps > 0 and outstanding_inv > 0) else True)
new_status = "Resolved" if (resolve and doc.docstatus == 1) else "Unresolved" invoice_outstanding_amounts = dict(
frappe.get_all(
"Sales Invoice",
filters={"name": ["in", list(invoices)]},
fields=["name", "outstanding_amount"],
as_list=True,
)
)
ps_outstanding_amounts = (
dict(
frappe.get_all(
"Payment Schedule",
filters={"name": ["in", list(payment_schedule_ids)]},
fields=["name", "outstanding"],
as_list=True,
)
)
if payment_schedule_ids
else {}
)
for dunning in dunnings:
has_outstanding = False
for overdue_payment in dunning.overdue_payments:
invoice_outstanding = invoice_outstanding_amounts[overdue_payment.sales_invoice]
ps_outstanding = ps_outstanding_amounts.get(overdue_payment.payment_schedule, 0)
has_outstanding = invoice_outstanding > 0 and ps_outstanding > 0
if has_outstanding:
break
new_status = "Resolved" if (not has_outstanding and to_resolve) else "Unresolved"
if dunning.status != new_status: if dunning.status != new_status:
dunning.status = new_status dunning.status = new_status

View File

@@ -139,7 +139,7 @@ class TestDunning(IntegrationTestCase):
self.assertEqual(sales_invoice.status, "Overdue") self.assertEqual(sales_invoice.status, "Overdue")
self.assertEqual(dunning.status, "Unresolved") self.assertEqual(dunning.status, "Unresolved")
def test_dunning_resolution_with_credit_note(self): def test_dunning_resolution_from_credit_note(self):
""" """
Test that dunning is resolved when a credit note is issued against the original invoice. Test that dunning is resolved when a credit note is issued against the original invoice.
""" """

View File

@@ -359,11 +359,11 @@ doc_events = {
"on_submit": [ "on_submit": [
"erpnext.regional.create_transaction_log", "erpnext.regional.create_transaction_log",
"erpnext.regional.italy.utils.sales_invoice_on_submit", "erpnext.regional.italy.utils.sales_invoice_on_submit",
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning_for_credit_note", "erpnext.accounts.doctype.dunning.dunning.resolve_dunnings",
], ],
"on_cancel": [ "on_cancel": [
"erpnext.regional.italy.utils.sales_invoice_on_cancel", "erpnext.regional.italy.utils.sales_invoice_on_cancel",
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning_for_credit_note", "erpnext.accounts.doctype.dunning.dunning.resolve_dunnings",
], ],
"on_trash": "erpnext.regional.check_deletion_permission", "on_trash": "erpnext.regional.check_deletion_permission",
}, },
@@ -376,9 +376,9 @@ doc_events = {
"Payment Entry": { "Payment Entry": {
"on_submit": [ "on_submit": [
"erpnext.regional.create_transaction_log", "erpnext.regional.create_transaction_log",
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning", "erpnext.accounts.doctype.dunning.dunning.resolve_dunnings",
], ],
"on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"], "on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunnings"],
"on_trash": "erpnext.regional.check_deletion_permission", "on_trash": "erpnext.regional.check_deletion_permission",
}, },
"Address": { "Address": {