diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 48f12897045..379c7e96e66 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -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") diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index 9c0885a5bd0..c8b5a631864 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -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) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 806955d6e7c..5006f143377 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -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": {