feat: rework dunning backend
diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py
index b4df0a5..56d49df 100644
--- a/erpnext/accounts/doctype/dunning/dunning.py
+++ b/erpnext/accounts/doctype/dunning/dunning.py
@@ -15,25 +15,34 @@
 
 
 class Dunning(AccountsController):
+
 	def validate(self):
-		self.validate_overdue_days()
-		self.validate_amount()
+		self.validate_overdue_payments()
+		self.validate_totals()
+
 		if not self.income_account:
 			self.income_account = frappe.get_cached_value("Company", self.company, "default_income_account")
 
-	def validate_overdue_days(self):
-		self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0
+	def validate_overdue_payments(self):
+		for row in self.overdue_payments:
+			row.overdue_days = (getdate(self.posting_date) - getdate(row.due_date)).days or 0
+			interest_per_year = flt(row.outstanding) * flt(self.rate_of_interest) / 100
+			row.interest_amount = (interest_per_year * cint(row.overdue_days)) / 365
 
-	def validate_amount(self):
-		amounts = calculate_interest_and_amount(
-			self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days
-		)
-		if self.interest_amount != amounts.get("interest_amount"):
-			self.interest_amount = flt(amounts.get("interest_amount"), self.precision("interest_amount"))
-		if self.dunning_amount != amounts.get("dunning_amount"):
-			self.dunning_amount = flt(amounts.get("dunning_amount"), self.precision("dunning_amount"))
-		if self.grand_total != amounts.get("grand_total"):
-			self.grand_total = flt(amounts.get("grand_total"), self.precision("grand_total"))
+	def validate_totals(self):
+		total_outstanding = sum(row.outstanding for row in self.overdue_payments)
+		total_interest = sum(row.interest_amount for row in self.overdue_payments)
+		dunning_amount = flt(total_interest) + flt(self.dunning_fee)
+		grand_total = flt(total_outstanding) + flt(dunning_amount)
+
+		if self.total_outstanding != total_outstanding:
+			self.total_outstanding = flt(total_outstanding, self.precision('total_outstanding'))
+		if self.total_interest != total_interest:
+			self.total_interest = flt(total_interest, self.precision('total_interest'))
+		if self.dunning_amount != dunning_amount:
+			self.dunning_amount = flt(dunning_amount, self.precision('dunning_amount'))
+		if self.grand_total != grand_total:
+			self.grand_total = flt(grand_total, self.precision('grand_total'))
 
 	def on_submit(self):
 		self.make_gl_entries()
@@ -113,20 +122,6 @@
 				frappe.db.set_value("Dunning", dunning.name, "status", "Resolved")
 
 
-def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
-	interest_amount = 0
-	grand_total = flt(outstanding_amount) + flt(dunning_fee)
-	if rate_of_interest:
-		interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100
-		interest_amount = (interest_per_year * cint(overdue_days)) / 365
-		grand_total += flt(interest_amount)
-	dunning_amount = flt(interest_amount) + flt(dunning_fee)
-	return {
-		"interest_amount": interest_amount,
-		"grand_total": grand_total,
-		"dunning_amount": dunning_amount,
-	}
-
 
 @frappe.whitelist()
 def get_dunning_letter_text(dunning_type, doc, language=None):
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 2075d57..0aa6eab 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -2510,55 +2510,58 @@
 
 
 @frappe.whitelist()
-def create_dunning(source_name, target_doc=None):
+def create_dunning(source_name, target_doc=None, ignore_permissions=False):
 	from frappe.model.mapper import get_mapped_doc
 
-	from erpnext.accounts.doctype.dunning.dunning import (
-		calculate_interest_and_amount,
-		get_dunning_letter_text,
-	)
+	def postprocess_dunning(source, target):
+		from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text
 
-	def set_missing_values(source, target):
-		target.sales_invoice = source_name
-		target.outstanding_amount = source.outstanding_amount
-		overdue_days = (getdate(target.posting_date) - getdate(source.due_date)).days
-		target.overdue_days = overdue_days
-		if frappe.db.exists(
-			"Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]}
-		):
-			dunning_type = frappe.get_doc(
-				"Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]}
-			)
+		dunning_type = frappe.db.exists('Dunning Type', {'is_default': 1})
+		if dunning_type:
+			dunning_type = frappe.get_doc("Dunning Type", dunning_type)
 			target.dunning_type = dunning_type.name
 			target.rate_of_interest = dunning_type.rate_of_interest
 			target.dunning_fee = dunning_type.dunning_fee
-			letter_text = get_dunning_letter_text(dunning_type=dunning_type.name, doc=target.as_dict())
-			if letter_text:
-				target.body_text = letter_text.get("body_text")
-				target.closing_text = letter_text.get("closing_text")
-				target.language = letter_text.get("language")
-			amounts = calculate_interest_and_amount(
-				target.outstanding_amount,
-				target.rate_of_interest,
-				target.dunning_fee,
-				target.overdue_days,
+			letter_text = get_dunning_letter_text(
+				dunning_type=dunning_type.name,
+				doc=target.as_dict(),
+				language=source.language
 			)
-			target.interest_amount = amounts.get("interest_amount")
-			target.dunning_amount = amounts.get("dunning_amount")
-			target.grand_total = amounts.get("grand_total")
 
-	doclist = get_mapped_doc(
-		"Sales Invoice",
-		source_name,
-		{
+			if letter_text:
+				target.body_text = letter_text.get('body_text')
+				target.closing_text = letter_text.get('closing_text')
+				target.language = letter_text.get('language')
+
+	def postprocess_overdue_payment(source, target, source_parent):
+		target.overdue_days = (getdate(nowdate()) - getdate(source.due_date)).days
+
+	return get_mapped_doc(
+		from_doctype="Sales Invoice",
+		from_docname=source_name,
+		table_maps={
 			"Sales Invoice": {
 				"doctype": "Dunning",
+				"field_map": {
+					"customer_address": "customer_address",
+					"parent": "sales_invoice"
+				},
+			},
+			"Payment Schedule": {
+				"doctype": "Overdue Payment",
+				"field_map": {
+					"name": "payment_schedule",
+					"parent": "sales_invoice"
+				},
+				"condition": lambda doc: doc.outstanding > 0,
+				"postprocess": postprocess_overdue_payment
 			}
 		},
-		target_doc,
-		set_missing_values,
+		target_doc=target_doc,
+		postprocess=postprocess_dunning,
+		ignore_permissions=ignore_permissions
 	)
-	return doclist
+
 
 
 def check_if_return_invoice_linked_with_payment_entry(self):