feat: add dunning resolution for credit notes and update hooks

This commit is contained in:
Karm Soni
2025-08-08 18:17:19 +05:30
parent 4378be45e4
commit b304c1d079
3 changed files with 104 additions and 1 deletions

View File

@@ -11,6 +11,7 @@
-> Resolves dunning automatically
"""
import json
import frappe
@@ -202,6 +203,46 @@ def resolve_dunning(doc, state):
dunning.save()
def resolve_dunning_for_credit_note(doc, state):
"""
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"):
return
if not doc.get("return_against"):
return
original_invoice = doc.return_against
if doc.docstatus == 1:
state = "Unresolved"
elif doc.docstatus == 2:
state = "Resolved"
else:
return
dunnings = get_linked_dunnings_as_per_state(original_invoice, 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 and doc.docstatus == 1) else "Unresolved"
if dunning.status != new_status:
dunning.status = new_status
dunning.save()
def get_linked_dunnings_as_per_state(sales_invoice, state):
dunning = frappe.qb.DocType("Dunning")
overdue_payment = frappe.qb.DocType("Overdue Payment")

View File

@@ -139,6 +139,64 @@ class TestDunning(IntegrationTestCase):
self.assertEqual(sales_invoice.status, "Overdue")
self.assertEqual(dunning.status, "Unresolved")
def test_dunning_resolution_with_credit_note(self):
"""
Test that dunning is resolved when a credit note is issued against the original invoice.
"""
sales_invoice = create_sales_invoice_against_cost_center(
posting_date=add_days(today(), -10), qty=1, rate=100
)
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
dunning.submit()
self.assertEqual(dunning.status, "Unresolved")
credit_note = frappe.copy_doc(sales_invoice)
credit_note.is_return = 1
credit_note.return_against = sales_invoice.name
credit_note.update_outstanding_for_self = 0
for item in credit_note.items:
item.qty = -item.qty
credit_note.save()
credit_note.submit()
dunning.reload()
self.assertEqual(dunning.status, "Resolved")
credit_note.cancel()
dunning.reload()
self.assertEqual(dunning.status, "Unresolved")
def test_dunning_not_affected_by_standalone_credit_note(self):
"""
Test that dunning is NOT resolved when a credit note has update_outstanding_for_self checked.
"""
sales_invoice = create_sales_invoice_against_cost_center(
posting_date=add_days(today(), -10), qty=1, rate=100
)
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
dunning.submit()
self.assertEqual(dunning.status, "Unresolved")
credit_note = frappe.copy_doc(sales_invoice)
credit_note.is_return = 1
credit_note.return_against = sales_invoice.name
credit_note.update_outstanding_for_self = 1
for item in credit_note.items:
item.qty = -item.qty
credit_note.save()
credit_note = frappe.get_doc("Sales Invoice", credit_note.name)
credit_note.submit()
dunning.reload()
self.assertEqual(dunning.status, "Unresolved")
def create_dunning(overdue_days, dunning_type_name=None):
posting_date = add_days(today(), -1 * overdue_days)

View File

@@ -359,8 +359,12 @@ 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",
],
"on_cancel": [
"erpnext.regional.italy.utils.sales_invoice_on_cancel",
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning_for_credit_note",
],
"on_cancel": ["erpnext.regional.italy.utils.sales_invoice_on_cancel"],
"on_trash": "erpnext.regional.check_deletion_permission",
},
"Purchase Invoice": {