mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-01 11:19:09 +00:00
feat: add dunning resolution for credit notes and update hooks
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
-> Resolves dunning automatically
|
-> Resolves dunning automatically
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
@@ -202,6 +203,46 @@ def resolve_dunning(doc, state):
|
|||||||
dunning.save()
|
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):
|
def get_linked_dunnings_as_per_state(sales_invoice, state):
|
||||||
dunning = frappe.qb.DocType("Dunning")
|
dunning = frappe.qb.DocType("Dunning")
|
||||||
overdue_payment = frappe.qb.DocType("Overdue Payment")
|
overdue_payment = frappe.qb.DocType("Overdue Payment")
|
||||||
|
|||||||
@@ -139,6 +139,64 @@ 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):
|
||||||
|
"""
|
||||||
|
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):
|
def create_dunning(overdue_days, dunning_type_name=None):
|
||||||
posting_date = add_days(today(), -1 * overdue_days)
|
posting_date = add_days(today(), -1 * overdue_days)
|
||||||
|
|||||||
@@ -359,8 +359,12 @@ 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",
|
||||||
|
],
|
||||||
|
"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",
|
"on_trash": "erpnext.regional.check_deletion_permission",
|
||||||
},
|
},
|
||||||
"Purchase Invoice": {
|
"Purchase Invoice": {
|
||||||
|
|||||||
Reference in New Issue
Block a user