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
when a Payment Entry is submitted.
Resolve / unresolve Dunning based on whether all payments have been made.
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:
# Consider partial and full payments:
# Submitting full payment: outstanding_amount will be 0
# 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" or not reference.allocated_amount:
continue
if reference.reference_doctype == "Sales Invoice" and (
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()
_update_linked_dunnings(reference.reference_name, to_resolve=is_submitted)
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.
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
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:
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)
invoices.add(overdue_payment.sales_invoice)
if overdue_payment.payment_schedule:
payment_schedule_ids.add(overdue_payment.payment_schedule)
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:
dunning.status = new_status

View File

@@ -139,7 +139,7 @@ class TestDunning(IntegrationTestCase):
self.assertEqual(sales_invoice.status, "Overdue")
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.
"""

View File

@@ -359,11 +359,11 @@ doc_events = {
"on_submit": [
"erpnext.regional.create_transaction_log",
"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": [
"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",
},
@@ -376,9 +376,9 @@ doc_events = {
"Payment Entry": {
"on_submit": [
"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",
},
"Address": {