Merge remote-tracking branch 'upstream/develop' into payments-based-dunning
diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js
index 9909c6c..1ac909e 100644
--- a/erpnext/accounts/doctype/dunning/dunning.js
+++ b/erpnext/accounts/doctype/dunning/dunning.js
@@ -1,13 +1,14 @@
-// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
 // For license information, please see license.txt
 
 frappe.ui.form.on("Dunning", {
 	setup: function (frm) {
-		frm.set_query("sales_invoice", () => {
+		frm.set_query("sales_invoice", "overdue_payments", () => {
 			return {
 				filters: {
 					docstatus: 1,
 					company: frm.doc.company,
+					customer: frm.doc.customer,
 					outstanding_amount: [">", 0],
 					status: "Overdue"
 				},
@@ -22,14 +23,24 @@
 				}
 			};
 		});
+		frm.set_query("cost_center", () => {
+			return {
+				filters: {
+					company: frm.doc.company,
+					is_group: 0
+				}
+			};
+		});
+
+		frm.set_query("contact_person", erpnext.queries.contact_query);
+		frm.set_query("customer_address", erpnext.queries.address_query);
+		frm.set_query("company_address", erpnext.queries.company_address_query);
+
+		// cannot add rows manually, only via button "Fetch Overdue Payments"
+		frm.set_df_property("overdue_payments", "cannot_add_rows", true);
 	},
 	refresh: function (frm) {
 		frm.set_df_property("company", "read_only", frm.doc.__islocal ? 0 : 1);
-		frm.set_df_property(
-			"sales_invoice",
-			"read_only",
-			frm.doc.__islocal ? 0 : 1
-		);
 		if (frm.doc.docstatus === 1 && frm.doc.status === "Unresolved") {
 			frm.add_custom_button(__("Resolve"), () => {
 				frm.set_value("status", "Resolved");
@@ -40,42 +51,111 @@
 				__("Payment"),
 				function () {
 					frm.events.make_payment_entry(frm);
-				},__("Create")
+				}, __("Create")
 			);
 			frm.page.set_inner_btn_group_as_primary(__("Create"));
 		}
 
-		if(frm.doc.docstatus > 0) {
-			frm.add_custom_button(__('Ledger'), function() {
-				frappe.route_options = {
-					"voucher_no": frm.doc.name,
-					"from_date": frm.doc.posting_date,
-					"to_date": frm.doc.posting_date,
-					"company": frm.doc.company,
-					"show_cancelled_entries": frm.doc.docstatus === 2
-				};
-				frappe.set_route("query-report", "General Ledger");
-			}, __('View'));
+		if (frm.doc.docstatus === 0) {
+			frm.add_custom_button(__("Fetch Overdue Payments"), () => {
+				erpnext.utils.map_current_doc({
+					method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
+					source_doctype: "Sales Invoice",
+					date_field: "due_date",
+					target: frm,
+					setters: {
+						customer: frm.doc.customer || undefined,
+					},
+					get_query_filters: {
+						docstatus: 1,
+						status: "Overdue",
+						company: frm.doc.company
+					},
+					allow_child_item_selection: true,
+					child_fieldname: "payment_schedule",
+					child_columns: ["due_date", "outstanding"],
+				});
+			});
 		}
+
+		frappe.dynamic_link = { doc: frm.doc, fieldname: 'customer', doctype: 'Customer' };
+
+		frm.toggle_display("customer_name", (frm.doc.customer_name && frm.doc.customer_name !== frm.doc.customer));
 	},
-	overdue_days: function (frm) {
-		frappe.db.get_value(
-			"Dunning Type",
-			{
-				start_day: ["<", frm.doc.overdue_days],
-				end_day: [">=", frm.doc.overdue_days],
-			},
-			"dunning_type",
-			(r) => {
-				if (r) {
-					frm.set_value("dunning_type", r.dunning_type);
-				} else {
-					frm.set_value("dunning_type", "");
-					frm.set_value("rate_of_interest", "");
-					frm.set_value("dunning_fee", "");
+	// When multiple companies are set up. in case company name is changed set default company address
+	company: function (frm) {
+		if (frm.doc.company) {
+			frappe.call({
+				method: "erpnext.setup.doctype.company.company.get_default_company_address",
+				args: { name: frm.doc.company, existing_address: frm.doc.company_address || "" },
+				debounce: 2000,
+				callback: function (r) {
+					frm.set_value("company_address", r && r.message || "");
+				}
+			});
+
+			if (frm.fields_dict.currency) {
+				const company_currency = erpnext.get_currency(frm.doc.company);
+
+				if (!frm.doc.currency) {
+					frm.set_value("currency", company_currency);
+				}
+
+				if (frm.doc.currency == company_currency) {
+					frm.set_value("conversion_rate", 1.0);
 				}
 			}
-		);
+
+			const company_doc = frappe.get_doc(":Company", frm.doc.company);
+			if (company_doc.default_letter_head) {
+				if (frm.fields_dict.letter_head) {
+					frm.set_value("letter_head", company_doc.default_letter_head);
+				}
+			}
+		}
+	},
+	currency: function (frm) {
+		// this.set_dynamic_labels();
+		const company_currency = erpnext.get_currency(frm.doc.company);
+		// Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc
+		if (frm.doc.currency && frm.doc.currency !== company_currency) {
+			frappe.call({
+				method: "erpnext.setup.utils.get_exchange_rate",
+				args: {
+					transaction_date: frm.doc.posting_date,
+					from_currency: frm.doc.currency,
+					to_currency: company_currency,
+					args: "for_selling"
+				},
+				freeze: true,
+				freeze_message: __("Fetching exchange rates ..."),
+				callback: function(r) {
+					const exchange_rate = flt(r.message);
+					if (exchange_rate != frm.doc.conversion_rate) {
+						frm.set_value("conversion_rate", exchange_rate);
+					}
+				}
+			});
+		} else {
+			frm.trigger("conversion_rate");
+		}
+	},
+	customer: (frm) => {
+		erpnext.utils.get_party_details(frm);
+	},
+	conversion_rate: function (frm) {
+		if (frm.doc.currency === erpnext.get_currency(frm.doc.company)) {
+			frm.set_value("conversion_rate", 1.0);
+		}
+
+		// Make read only if Accounts Settings doesn't allow stale rates
+		frm.set_df_property("conversion_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
+	},
+	customer_address: function (frm) {
+		erpnext.utils.get_address_display(frm, "customer_address");
+	},
+	company_address: function (frm) {
+		erpnext.utils.get_address_display(frm, "company_address");
 	},
 	dunning_type: function (frm) {
 		frm.trigger("get_dunning_letter_text");
@@ -87,7 +167,7 @@
 		if (frm.doc.dunning_type) {
 			frappe.call({
 				method:
-				"erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text",
+					"erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text",
 				args: {
 					dunning_type: frm.doc.dunning_type,
 					language: frm.doc.language,
@@ -106,49 +186,62 @@
 			});
 		}
 	},
-	due_date: function (frm) {
-		frm.trigger("calculate_overdue_days");
-	},
 	posting_date: function (frm) {
 		frm.trigger("calculate_overdue_days");
 	},
 	rate_of_interest: function (frm) {
-		frm.trigger("calculate_interest_and_amount");
-	},
-	outstanding_amount: function (frm) {
-		frm.trigger("calculate_interest_and_amount");
-	},
-	interest_amount: function (frm) {
-		frm.trigger("calculate_interest_and_amount");
+		frm.trigger("calculate_interest");
 	},
 	dunning_fee: function (frm) {
-		frm.trigger("calculate_interest_and_amount");
+		frm.trigger("calculate_totals");
 	},
-	sales_invoice: function (frm) {
-		frm.trigger("calculate_overdue_days");
+	overdue_payments_add: function (frm) {
+		frm.trigger("calculate_totals");
+	},
+	overdue_payments_remove: function (frm) {
+		frm.trigger("calculate_totals");
 	},
 	calculate_overdue_days: function (frm) {
-		if (frm.doc.posting_date && frm.doc.due_date) {
-			const overdue_days = moment(frm.doc.posting_date).diff(
-				frm.doc.due_date,
-				"days"
-			);
-			frm.set_value("overdue_days", overdue_days);
-		}
+		frm.doc.overdue_payments.forEach((row) => {
+			if (frm.doc.posting_date && row.due_date) {
+				const overdue_days = moment(frm.doc.posting_date).diff(
+					row.due_date,
+					"days"
+				);
+				frappe.model.set_value(row.doctype, row.name, "overdue_days", overdue_days);
+			}
+		});
 	},
-	calculate_interest_and_amount: function (frm) {
-		const interest_per_year = frm.doc.outstanding_amount * frm.doc.rate_of_interest / 100;
-		const interest_amount = flt((interest_per_year * cint(frm.doc.overdue_days)) / 365 || 0, precision('interest_amount'));
-		const dunning_amount = flt(interest_amount + frm.doc.dunning_fee, precision('dunning_amount'));
-		const grand_total = flt(frm.doc.outstanding_amount + dunning_amount, precision('grand_total'));
-		frm.set_value("interest_amount", interest_amount);
-		frm.set_value("dunning_amount", dunning_amount);
-		frm.set_value("grand_total", grand_total);
+	calculate_interest: function (frm) {
+		frm.doc.overdue_payments.forEach((row) => {
+			const interest_per_day = frm.doc.rate_of_interest / 100 / 365;
+			const interest = flt((interest_per_day * row.overdue_days * row.outstanding), precision("interest", row));
+			frappe.model.set_value(row.doctype, row.name, "interest", interest);
+		});
+	},
+	calculate_totals: function (frm) {
+		const total_interest = frm.doc.overdue_payments
+			.reduce((prev, cur) => prev + cur.interest, 0);
+		const total_outstanding = frm.doc.overdue_payments
+			.reduce((prev, cur) => prev + cur.outstanding, 0);
+		const dunning_amount = total_interest + frm.doc.dunning_fee;
+		const base_dunning_amount = dunning_amount * frm.doc.conversion_rate;
+		const grand_total = total_outstanding + dunning_amount;
+
+		function setWithPrecison(field, value) {
+			frm.set_value(field, flt(value, precision(field)));
+		}
+
+		setWithPrecison("total_outstanding", total_outstanding);
+		setWithPrecison("total_interest", total_interest);
+		setWithPrecison("dunning_amount", dunning_amount);
+		setWithPrecison("base_dunning_amount", base_dunning_amount);
+		setWithPrecison("grand_total", grand_total);
 	},
 	make_payment_entry: function (frm) {
 		return frappe.call({
 			method:
-			"erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry",
+				"erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry",
 			args: {
 				dt: frm.doc.doctype,
 				dn: frm.doc.name,
@@ -160,3 +253,9 @@
 		});
 	},
 });
+
+frappe.ui.form.on("Overdue Payment", {
+	interest: function (frm) {
+		frm.trigger("calculate_totals");
+	}
+});
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json
index 2a32b99..b7e8aea 100644
--- a/erpnext/accounts/doctype/dunning/dunning.json
+++ b/erpnext/accounts/doctype/dunning/dunning.json
@@ -2,49 +2,60 @@
  "actions": [],
  "allow_events_in_timeline": 1,
  "autoname": "naming_series:",
+ "beta": 1,
  "creation": "2019-07-05 16:34:31.013238",
  "doctype": "DocType",
  "engine": "InnoDB",
  "field_order": [
-  "title",
   "naming_series",
-  "sales_invoice",
   "customer",
   "customer_name",
-  "outstanding_amount",
-  "currency",
-  "conversion_rate",
   "column_break_3",
   "company",
   "posting_date",
   "posting_time",
-  "due_date",
-  "overdue_days",
+  "status",
+  "section_break_9",
+  "currency",
+  "column_break_11",
+  "conversion_rate",
   "address_and_contact_section",
+  "customer_address",
   "address_display",
+  "contact_person",
   "contact_display",
+  "column_break_16",
+  "company_address",
+  "company_address_display",
   "contact_mobile",
   "contact_email",
-  "column_break_18",
-  "company_address_display",
   "section_break_6",
   "dunning_type",
-  "dunning_fee",
   "column_break_8",
   "rate_of_interest",
-  "interest_amount",
   "section_break_12",
-  "dunning_amount",
-  "grand_total",
-  "income_account",
+  "overdue_payments",
+  "section_break_28",
+  "total_interest",
+  "dunning_fee",
   "column_break_17",
-  "status",
-  "printing_setting_section",
+  "dunning_amount",
+  "base_dunning_amount",
+  "section_break_32",
+  "spacer",
+  "column_break_33",
+  "total_outstanding",
+  "grand_total",
+  "printing_settings_section",
   "language",
   "body_text",
   "column_break_22",
   "letter_head",
   "closing_text",
+  "accounting_details_section",
+  "income_account",
+  "column_break_48",
+  "cost_center",
   "amended_from"
  ],
  "fields": [
@@ -60,19 +71,11 @@
    "fieldname": "naming_series",
    "fieldtype": "Select",
    "label": "Series",
-   "options": "DUNN-.MM.-.YY.-"
+   "options": "DUNN-.MM.-.YY.-",
+   "print_hide": 1
   },
   {
-   "fieldname": "sales_invoice",
-   "fieldtype": "Link",
-   "in_list_view": 1,
-   "in_standard_filter": 1,
-   "label": "Sales Invoice",
-   "options": "Sales Invoice",
-   "reqd": 1
-  },
-  {
-   "fetch_from": "sales_invoice.customer_name",
+   "fetch_from": "customer.customer_name",
    "fieldname": "customer_name",
    "fieldtype": "Data",
    "in_list_view": 1,
@@ -80,13 +83,6 @@
    "read_only": 1
   },
   {
-   "fetch_from": "sales_invoice.outstanding_amount",
-   "fieldname": "outstanding_amount",
-   "fieldtype": "Currency",
-   "label": "Outstanding Amount",
-   "read_only": 1
-  },
-  {
    "fieldname": "column_break_3",
    "fieldtype": "Column Break"
   },
@@ -94,13 +90,8 @@
    "default": "Today",
    "fieldname": "posting_date",
    "fieldtype": "Date",
-   "label": "Date"
-  },
-  {
-   "fieldname": "overdue_days",
-   "fieldtype": "Int",
-   "label": "Overdue Days",
-   "read_only": 1
+   "label": "Date",
+   "reqd": 1
   },
   {
    "fieldname": "section_break_6",
@@ -112,16 +103,7 @@
    "in_list_view": 1,
    "in_standard_filter": 1,
    "label": "Dunning Type",
-   "options": "Dunning Type",
-   "reqd": 1
-  },
-  {
-   "default": "0",
-   "fieldname": "interest_amount",
-   "fieldtype": "Currency",
-   "label": "Interest Amount",
-   "precision": "2",
-   "read_only": 1
+   "options": "Dunning Type"
   },
   {
    "fieldname": "column_break_8",
@@ -134,6 +116,7 @@
    "fieldname": "dunning_fee",
    "fieldtype": "Currency",
    "label": "Dunning Fee",
+   "options": "currency",
    "precision": "2"
   },
   {
@@ -145,36 +128,24 @@
    "fieldtype": "Column Break"
   },
   {
-   "fieldname": "printing_setting_section",
-   "fieldtype": "Section Break",
-   "label": "Printing Setting"
-  },
-  {
    "fieldname": "language",
    "fieldtype": "Link",
    "label": "Print Language",
-   "options": "Language"
+   "options": "Language",
+   "print_hide": 1
   },
   {
    "fieldname": "letter_head",
    "fieldtype": "Link",
    "label": "Letter Head",
-   "options": "Letter Head"
+   "options": "Letter Head",
+   "print_hide": 1
   },
   {
    "fieldname": "column_break_22",
    "fieldtype": "Column Break"
   },
   {
-   "fetch_from": "sales_invoice.currency",
-   "fieldname": "currency",
-   "fieldtype": "Link",
-   "hidden": 1,
-   "label": "Currency",
-   "options": "Currency",
-   "read_only": 1
-  },
-  {
    "fieldname": "amended_from",
    "fieldtype": "Link",
    "label": "Amended From",
@@ -184,14 +155,6 @@
    "read_only": 1
   },
   {
-   "allow_on_submit": 1,
-   "default": "{customer_name}",
-   "fieldname": "title",
-   "fieldtype": "Data",
-   "hidden": 1,
-   "label": "Title"
-  },
-  {
    "fieldname": "body_text",
    "fieldtype": "Text Editor",
    "label": "Body Text"
@@ -202,13 +165,6 @@
    "label": "Closing Text"
   },
   {
-   "fetch_from": "sales_invoice.due_date",
-   "fieldname": "due_date",
-   "fieldtype": "Date",
-   "label": "Due Date",
-   "read_only": 1
-  },
-  {
    "fieldname": "posting_time",
    "fieldtype": "Time",
    "label": "Posting Time"
@@ -222,26 +178,24 @@
    "label": "Rate of Interest (%) Yearly"
   },
   {
+   "collapsible": 1,
    "fieldname": "address_and_contact_section",
    "fieldtype": "Section Break",
    "label": "Address and Contact"
   },
   {
-   "fetch_from": "sales_invoice.address_display",
    "fieldname": "address_display",
    "fieldtype": "Small Text",
    "label": "Address",
    "read_only": 1
   },
   {
-   "fetch_from": "sales_invoice.contact_display",
    "fieldname": "contact_display",
    "fieldtype": "Small Text",
    "label": "Contact",
    "read_only": 1
   },
   {
-   "fetch_from": "sales_invoice.contact_mobile",
    "fieldname": "contact_mobile",
    "fieldtype": "Small Text",
    "label": "Mobile No",
@@ -249,18 +203,12 @@
    "read_only": 1
   },
   {
-   "fieldname": "column_break_18",
-   "fieldtype": "Column Break"
-  },
-  {
-   "fetch_from": "sales_invoice.company_address_display",
    "fieldname": "company_address_display",
    "fieldtype": "Small Text",
-   "label": "Company Address",
+   "label": "Company Address Display",
    "read_only": 1
   },
   {
-   "fetch_from": "sales_invoice.contact_email",
    "fieldname": "contact_email",
    "fieldtype": "Data",
    "label": "Contact Email",
@@ -268,18 +216,18 @@
    "read_only": 1
   },
   {
-   "fetch_from": "sales_invoice.customer",
    "fieldname": "customer",
    "fieldtype": "Link",
    "label": "Customer",
    "options": "Customer",
-   "read_only": 1
+   "reqd": 1
   },
   {
    "default": "0",
    "fieldname": "grand_total",
    "fieldtype": "Currency",
    "label": "Grand Total",
+   "options": "currency",
    "precision": "2",
    "read_only": 1
   },
@@ -290,33 +238,150 @@
    "fieldtype": "Select",
    "in_standard_filter": 1,
    "label": "Status",
-   "options": "Draft\nResolved\nUnresolved\nCancelled"
-  },
-  {
-   "fieldname": "dunning_amount",
-   "fieldtype": "Currency",
-   "hidden": 1,
-   "label": "Dunning Amount",
+   "options": "Draft\nResolved\nUnresolved\nCancelled",
    "read_only": 1
   },
   {
+   "description": "For dunning fee and interest",
+   "fetch_from": "dunning_type.income_account",
    "fieldname": "income_account",
    "fieldtype": "Link",
    "label": "Income Account",
-   "options": "Account"
+   "options": "Account",
+   "print_hide": 1
   },
   {
-   "fetch_from": "sales_invoice.conversion_rate",
+   "fieldname": "overdue_payments",
+   "fieldtype": "Table",
+   "label": "Overdue Payments",
+   "options": "Overdue Payment"
+  },
+  {
+   "fieldname": "section_break_28",
+   "fieldtype": "Section Break"
+  },
+  {
+   "default": "0",
+   "fieldname": "total_interest",
+   "fieldtype": "Currency",
+   "label": "Total Interest",
+   "options": "currency",
+   "precision": "2",
+   "read_only": 1
+  },
+  {
+   "fieldname": "total_outstanding",
+   "fieldtype": "Currency",
+   "label": "Total Outstanding",
+   "options": "currency",
+   "read_only": 1
+  },
+  {
+   "fieldname": "customer_address",
+   "fieldtype": "Link",
+   "label": "Customer Address",
+   "options": "Address",
+   "print_hide": 1
+  },
+  {
+   "fieldname": "contact_person",
+   "fieldtype": "Link",
+   "label": "Contact Person",
+   "options": "Contact",
+   "print_hide": 1
+  },
+  {
+   "default": "0",
+   "fieldname": "dunning_amount",
+   "fieldtype": "Currency",
+   "label": "Dunning Amount",
+   "options": "currency",
+   "read_only": 1
+  },
+  {
+   "collapsible": 1,
+   "fieldname": "accounting_details_section",
+   "fieldtype": "Section Break",
+   "label": "Accounting Details"
+  },
+  {
+   "fetch_from": "dunning_type.cost_center",
+   "fieldname": "cost_center",
+   "fieldtype": "Link",
+   "label": "Cost Center",
+   "options": "Cost Center",
+   "print_hide": 1
+  },
+  {
+   "collapsible": 1,
+   "fieldname": "printing_settings_section",
+   "fieldtype": "Section Break",
+   "label": "Printing Settings"
+  },
+  {
+   "fieldname": "section_break_32",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "column_break_33",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "spacer",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "label": "Spacer",
+   "print_hide": 1,
+   "read_only": 1,
+   "report_hide": 1
+  },
+  {
+   "fieldname": "column_break_16",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "company_address",
+   "fieldtype": "Link",
+   "label": "Company Address",
+   "options": "Address",
+   "print_hide": 1
+  },
+  {
+   "fieldname": "section_break_9",
+   "fieldtype": "Section Break",
+   "label": "Currency"
+  },
+  {
+   "fieldname": "currency",
+   "fieldtype": "Link",
+   "label": "Currency",
+   "options": "Currency"
+  },
+  {
+   "fieldname": "column_break_11",
+   "fieldtype": "Column Break"
+  },
+  {
    "fieldname": "conversion_rate",
    "fieldtype": "Float",
-   "hidden": 1,
-   "label": "Conversion Rate",
+   "label": "Conversion Rate"
+  },
+  {
+   "default": "0",
+   "fieldname": "base_dunning_amount",
+   "fieldtype": "Currency",
+   "label": "Dunning Amount (Company Currency)",
+   "options": "Company:company:default_currency",
    "read_only": 1
+  },
+  {
+   "fieldname": "column_break_48",
+   "fieldtype": "Column Break"
   }
  ],
  "is_submittable": 1,
  "links": [],
- "modified": "2023-06-03 16:24:01.677026",
+ "modified": "2023-06-15 15:46:53.865712",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Dunning",
diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py
index b4df0a5..9d0d36b 100644
--- a/erpnext/accounts/doctype/dunning/dunning.py
+++ b/erpnext/accounts/doctype/dunning/dunning.py
@@ -1,131 +1,150 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
 # For license information, please see license.txt
+"""
+# Accounting
 
+1. Payment of outstanding invoices with dunning amount
 
+		- Debit full amount to bank
+		- Credit invoiced amount to receivables
+		- Credit dunning amount to interest and similar revenue
+
+		-> Resolves dunning automatically
+"""
 import json
 
 import frappe
-from frappe.utils import cint, flt, getdate
+from frappe import _
+from frappe.contacts.doctype.address.address import get_address_display
+from frappe.utils import getdate
 
-from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
-	get_accounting_dimensions,
-)
-from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
 from erpnext.controllers.accounts_controller import AccountsController
 
 
 class Dunning(AccountsController):
 	def validate(self):
-		self.validate_overdue_days()
-		self.validate_amount()
-		if not self.income_account:
-			self.income_account = frappe.get_cached_value("Company", self.company, "default_income_account")
+		self.validate_same_currency()
+		self.validate_overdue_payments()
+		self.validate_totals()
+		self.set_party_details()
+		self.set_dunning_level()
 
-	def validate_overdue_days(self):
-		self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0
+	def validate_same_currency(self):
+		"""
+		Throw an error if invoice currency differs from dunning currency.
+		"""
+		for row in self.overdue_payments:
+			invoice_currency = frappe.get_value("Sales Invoice", row.sales_invoice, "currency")
+			if invoice_currency != self.currency:
+				frappe.throw(
+					_(
+						"The currency of invoice {} ({}) is different from the currency of this dunning ({})."
+					).format(row.sales_invoice, invoice_currency, self.currency)
+				)
 
-	def validate_amount(self):
-		amounts = calculate_interest_and_amount(
-			self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days
+	def validate_overdue_payments(self):
+		daily_interest = self.rate_of_interest / 100 / 365
+
+		for row in self.overdue_payments:
+			row.overdue_days = (getdate(self.posting_date) - getdate(row.due_date)).days or 0
+			row.interest = row.outstanding * daily_interest * row.overdue_days
+
+	def validate_totals(self):
+		self.total_outstanding = sum(row.outstanding for row in self.overdue_payments)
+		self.total_interest = sum(row.interest for row in self.overdue_payments)
+		self.dunning_amount = self.total_interest + self.dunning_fee
+		self.base_dunning_amount = self.dunning_amount * self.conversion_rate
+		self.grand_total = self.total_outstanding + self.dunning_amount
+
+	def set_party_details(self):
+		from erpnext.accounts.party import _get_party_details
+
+		party_details = _get_party_details(
+			self.customer,
+			ignore_permissions=self.flags.ignore_permissions,
+			doctype=self.doctype,
+			company=self.company,
+			posting_date=self.get("posting_date"),
+			fetch_payment_terms_template=False,
+			party_address=self.customer_address,
+			company_address=self.get("company_address"),
 		)
-		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"))
+		for field in [
+			"customer_address",
+			"address_display",
+			"company_address",
+			"contact_person",
+			"contact_display",
+			"contact_mobile",
+		]:
+			self.set(field, party_details.get(field))
 
-	def on_submit(self):
-		self.make_gl_entries()
+		self.set("company_address_display", get_address_display(self.company_address))
 
-	def on_cancel(self):
-		if self.dunning_amount:
-			self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
-			make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
-
-	def make_gl_entries(self):
-		if not self.dunning_amount:
-			return
-		gl_entries = []
-		invoice_fields = [
-			"project",
-			"cost_center",
-			"debit_to",
-			"party_account_currency",
-			"conversion_rate",
-			"cost_center",
-		]
-		inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
-
-		accounting_dimensions = get_accounting_dimensions()
-		invoice_fields.extend(accounting_dimensions)
-
-		dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate)
-		default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
-
-		gl_entries.append(
-			self.get_gl_dict(
-				{
-					"account": inv.debit_to,
-					"party_type": "Customer",
-					"party": self.customer,
-					"due_date": self.due_date,
-					"against": self.income_account,
-					"debit": dunning_in_company_currency,
-					"debit_in_account_currency": self.dunning_amount,
-					"against_voucher": self.name,
-					"against_voucher_type": "Dunning",
-					"cost_center": inv.cost_center or default_cost_center,
-					"project": inv.project,
+	def set_dunning_level(self):
+		for row in self.overdue_payments:
+			past_dunnings = frappe.get_all(
+				"Overdue Payment",
+				filters={
+					"payment_schedule": row.payment_schedule,
+					"parent": ("!=", row.parent),
+					"docstatus": 1,
 				},
-				inv.party_account_currency,
-				item=inv,
 			)
-		)
-		gl_entries.append(
-			self.get_gl_dict(
-				{
-					"account": self.income_account,
-					"against": self.customer,
-					"credit": dunning_in_company_currency,
-					"cost_center": inv.cost_center or default_cost_center,
-					"credit_in_account_currency": self.dunning_amount,
-					"project": inv.project,
-				},
-				item=inv,
-			)
-		)
-		make_gl_entries(
-			gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False
-		)
+			row.dunning_level = len(past_dunnings) + 1
 
 
 def resolve_dunning(doc, state):
+	"""
+	Check if all payments have been made and resolve dunning, if yes. Called
+	when a Payment Entry is submitted.
+	"""
 	for reference in doc.references:
-		if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0:
-			dunnings = frappe.get_list(
-				"Dunning",
-				filters={"sales_invoice": reference.reference_name, "status": ("!=", "Resolved")},
-				ignore_permissions=True,
-			)
+		# 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" 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:
-				frappe.db.set_value("Dunning", dunning.name, "status", "Resolved")
+				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 = False if (outstanding_ps > 0 and outstanding_inv > 0) else True
+
+				dunning.status = "Resolved" if resolve else "Unresolved"
+				dunning.save()
 
 
-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,
-	}
+def get_linked_dunnings_as_per_state(sales_invoice, state):
+	dunning = frappe.qb.DocType("Dunning")
+	overdue_payment = frappe.qb.DocType("Overdue Payment")
+
+	return (
+		frappe.qb.from_(dunning)
+		.join(overdue_payment)
+		.on(overdue_payment.parent == dunning.name)
+		.select(dunning.name)
+		.where(
+			(dunning.status == state)
+			& (dunning.docstatus != 2)
+			& (overdue_payment.sales_invoice == sales_invoice)
+		)
+	).run(as_dict=True)
 
 
 @frappe.whitelist()
diff --git a/erpnext/accounts/doctype/dunning/dunning_dashboard.py b/erpnext/accounts/doctype/dunning/dunning_dashboard.py
deleted file mode 100644
index d1d4031..0000000
--- a/erpnext/accounts/doctype/dunning/dunning_dashboard.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from frappe import _
-
-
-def get_data():
-	return {
-		"fieldname": "dunning",
-		"non_standard_fieldnames": {
-			"Journal Entry": "reference_name",
-			"Payment Entry": "reference_name",
-		},
-		"transactions": [{"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]}],
-	}
diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py
index e1fd1e9..b29ace2 100644
--- a/erpnext/accounts/doctype/dunning/test_dunning.py
+++ b/erpnext/accounts/doctype/dunning/test_dunning.py
@@ -1,162 +1,197 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
 # See license.txt
-
-import unittest
-
 import frappe
+from frappe.tests.utils import FrappeTestCase
 from frappe.utils import add_days, nowdate, today
 
-from erpnext.accounts.doctype.dunning.dunning import calculate_interest_and_amount
+from erpnext import get_default_cost_center
 from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
 from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
 	unlink_payment_on_cancel_of_invoice,
 )
+from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
+	create_dunning as create_dunning_from_sales_invoice,
+)
 from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import (
 	create_sales_invoice_against_cost_center,
 )
 
+test_dependencies = ["Company", "Cost Center"]
 
-class TestDunning(unittest.TestCase):
+
+class TestDunning(FrappeTestCase):
 	@classmethod
-	def setUpClass(self):
-		create_dunning_type()
-		create_dunning_type_with_zero_interest_rate()
+	def setUpClass(cls):
+		super().setUpClass()
+		create_dunning_type("First Notice", fee=0.0, interest=0.0, is_default=1)
+		create_dunning_type("Second Notice", fee=10.0, interest=10.0, is_default=0)
 		unlink_payment_on_cancel_of_invoice()
 
 	@classmethod
-	def tearDownClass(self):
+	def tearDownClass(cls):
 		unlink_payment_on_cancel_of_invoice(0)
+		super().tearDownClass()
 
-	def test_dunning(self):
-		dunning = create_dunning()
-		amounts = calculate_interest_and_amount(
-			dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
-		)
-		self.assertEqual(round(amounts.get("interest_amount"), 2), 0.44)
-		self.assertEqual(round(amounts.get("dunning_amount"), 2), 20.44)
-		self.assertEqual(round(amounts.get("grand_total"), 2), 120.44)
+	def test_dunning_without_fees(self):
+		dunning = create_dunning(overdue_days=20)
 
-	def test_dunning_with_zero_interest_rate(self):
-		dunning = create_dunning_with_zero_interest_rate()
-		amounts = calculate_interest_and_amount(
-			dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
-		)
-		self.assertEqual(round(amounts.get("interest_amount"), 2), 0)
-		self.assertEqual(round(amounts.get("dunning_amount"), 2), 20)
-		self.assertEqual(round(amounts.get("grand_total"), 2), 120)
+		self.assertEqual(round(dunning.total_outstanding, 2), 100.00)
+		self.assertEqual(round(dunning.total_interest, 2), 0.00)
+		self.assertEqual(round(dunning.dunning_fee, 2), 0.00)
+		self.assertEqual(round(dunning.dunning_amount, 2), 0.00)
+		self.assertEqual(round(dunning.grand_total, 2), 100.00)
 
-	def test_gl_entries(self):
-		dunning = create_dunning()
-		dunning.submit()
-		gl_entries = frappe.db.sql(
-			"""select account, debit, credit
-			from `tabGL Entry` where voucher_type='Dunning' and voucher_no=%s
-			order by account asc""",
-			dunning.name,
-			as_dict=1,
-		)
-		self.assertTrue(gl_entries)
-		expected_values = dict(
-			(d[0], d) for d in [["Debtors - _TC", 20.44, 0.0], ["Sales - _TC", 0.0, 20.44]]
-		)
-		for gle in gl_entries:
-			self.assertEqual(expected_values[gle.account][0], gle.account)
-			self.assertEqual(expected_values[gle.account][1], gle.debit)
-			self.assertEqual(expected_values[gle.account][2], gle.credit)
+	def test_dunning_with_fees_and_interest(self):
+		dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC")
 
-	def test_payment_entry(self):
-		dunning = create_dunning()
+		self.assertEqual(round(dunning.total_outstanding, 2), 100.00)
+		self.assertEqual(round(dunning.total_interest, 2), 0.41)
+		self.assertEqual(round(dunning.dunning_fee, 2), 10.00)
+		self.assertEqual(round(dunning.dunning_amount, 2), 10.41)
+		self.assertEqual(round(dunning.grand_total, 2), 110.41)
+
+	def test_dunning_with_payment_entry(self):
+		dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC")
 		dunning.submit()
 		pe = get_payment_entry("Dunning", dunning.name)
 		pe.reference_no = "1"
 		pe.reference_date = nowdate()
-		pe.paid_from_account_currency = dunning.currency
-		pe.paid_to_account_currency = dunning.currency
-		pe.source_exchange_rate = 1
-		pe.target_exchange_rate = 1
 		pe.insert()
 		pe.submit()
-		si_doc = frappe.get_doc("Sales Invoice", dunning.sales_invoice)
-		self.assertEqual(si_doc.outstanding_amount, 0)
+
+		for overdue_payment in dunning.overdue_payments:
+			outstanding_amount = frappe.get_value(
+				"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
+			)
+			self.assertEqual(outstanding_amount, 0)
+
+		dunning.reload()
+		self.assertEqual(dunning.status, "Resolved")
+
+	def test_dunning_and_payment_against_partially_due_invoice(self):
+		"""
+		Create SI with first installment overdue. Check impact of Dunning and Payment Entry.
+		"""
+		create_payment_terms_template_for_dunning()
+		sales_invoice = create_sales_invoice_against_cost_center(
+			posting_date=add_days(today(), -1 * 6),
+			qty=1,
+			rate=100,
+			do_not_submit=True,
+		)
+		sales_invoice.payment_terms_template = "_Test 50-50 for Dunning"
+		sales_invoice.submit()
+		dunning = create_dunning_from_sales_invoice(sales_invoice.name)
+
+		self.assertEqual(len(dunning.overdue_payments), 1)
+		self.assertEqual(dunning.overdue_payments[0].payment_term, "_Test Payment Term 1 for Dunning")
+
+		dunning.submit()
+		pe = get_payment_entry("Dunning", dunning.name)
+		pe.reference_no, pe.reference_date = "2", nowdate()
+		pe.insert()
+		pe.submit()
+		sales_invoice.load_from_db()
+		dunning.load_from_db()
+
+		self.assertEqual(sales_invoice.status, "Partly Paid")
+		self.assertEqual(sales_invoice.payment_schedule[0].outstanding, 0)
+		self.assertEqual(dunning.status, "Resolved")
+
+		# Test impact on cancellation of PE
+		pe.cancel()
+		sales_invoice.reload()
+		dunning.reload()
+
+		self.assertEqual(sales_invoice.status, "Overdue")
+		self.assertEqual(dunning.status, "Unresolved")
 
 
-def create_dunning():
-	posting_date = add_days(today(), -20)
-	due_date = add_days(today(), -15)
+def create_dunning(overdue_days, dunning_type_name=None):
+	posting_date = add_days(today(), -1 * overdue_days)
 	sales_invoice = create_sales_invoice_against_cost_center(
-		posting_date=posting_date, due_date=due_date, status="Overdue"
+		posting_date=posting_date, qty=1, rate=100
 	)
-	dunning_type = frappe.get_doc("Dunning Type", "First Notice")
-	dunning = frappe.new_doc("Dunning")
-	dunning.sales_invoice = sales_invoice.name
-	dunning.customer_name = sales_invoice.customer_name
-	dunning.outstanding_amount = sales_invoice.outstanding_amount
-	dunning.debit_to = sales_invoice.debit_to
-	dunning.currency = sales_invoice.currency
-	dunning.company = sales_invoice.company
-	dunning.posting_date = nowdate()
-	dunning.due_date = sales_invoice.due_date
-	dunning.dunning_type = "First Notice"
-	dunning.rate_of_interest = dunning_type.rate_of_interest
-	dunning.dunning_fee = dunning_type.dunning_fee
-	dunning.save()
-	return dunning
+	dunning = create_dunning_from_sales_invoice(sales_invoice.name)
+
+	if dunning_type_name:
+		dunning_type = frappe.get_doc("Dunning Type", dunning_type_name)
+		dunning.dunning_type = dunning_type.name
+		dunning.rate_of_interest = dunning_type.rate_of_interest
+		dunning.dunning_fee = dunning_type.dunning_fee
+		dunning.income_account = dunning_type.income_account
+		dunning.cost_center = dunning_type.cost_center
+
+	return dunning.save()
 
 
-def create_dunning_with_zero_interest_rate():
-	posting_date = add_days(today(), -20)
-	due_date = add_days(today(), -15)
-	sales_invoice = create_sales_invoice_against_cost_center(
-		posting_date=posting_date, due_date=due_date, status="Overdue"
-	)
-	dunning_type = frappe.get_doc("Dunning Type", "First Notice with 0% Rate of Interest")
-	dunning = frappe.new_doc("Dunning")
-	dunning.sales_invoice = sales_invoice.name
-	dunning.customer_name = sales_invoice.customer_name
-	dunning.outstanding_amount = sales_invoice.outstanding_amount
-	dunning.debit_to = sales_invoice.debit_to
-	dunning.currency = sales_invoice.currency
-	dunning.company = sales_invoice.company
-	dunning.posting_date = nowdate()
-	dunning.due_date = sales_invoice.due_date
-	dunning.dunning_type = "First Notice with 0% Rate of Interest"
-	dunning.rate_of_interest = dunning_type.rate_of_interest
-	dunning.dunning_fee = dunning_type.dunning_fee
-	dunning.save()
-	return dunning
+def create_dunning_type(title, fee, interest, is_default):
+	company = "_Test Company"
+	if frappe.db.exists("Dunning Type", f"{title} - _TC"):
+		return
 
-
-def create_dunning_type():
 	dunning_type = frappe.new_doc("Dunning Type")
-	dunning_type.dunning_type = "First Notice"
-	dunning_type.start_day = 10
-	dunning_type.end_day = 20
-	dunning_type.dunning_fee = 20
-	dunning_type.rate_of_interest = 8
+	dunning_type.dunning_type = title
+	dunning_type.company = company
+	dunning_type.is_default = is_default
+	dunning_type.dunning_fee = fee
+	dunning_type.rate_of_interest = interest
+	dunning_type.income_account = get_income_account(company)
+	dunning_type.cost_center = get_default_cost_center(company)
 	dunning_type.append(
 		"dunning_letter_text",
 		{
 			"language": "en",
-			"body_text": "We have still not received payment for our invoice ",
+			"body_text": "We have still not received payment for our invoice",
 			"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.",
 		},
 	)
-	dunning_type.save()
+	dunning_type.insert()
 
 
-def create_dunning_type_with_zero_interest_rate():
-	dunning_type = frappe.new_doc("Dunning Type")
-	dunning_type.dunning_type = "First Notice with 0% Rate of Interest"
-	dunning_type.start_day = 10
-	dunning_type.end_day = 20
-	dunning_type.dunning_fee = 20
-	dunning_type.rate_of_interest = 0
-	dunning_type.append(
-		"dunning_letter_text",
-		{
-			"language": "en",
-			"body_text": "We have still not received payment for our invoice ",
-			"closing_text": "We kindly request that you pay the outstanding amount immediately, and late fees.",
-		},
+def get_income_account(company):
+	return (
+		frappe.get_value("Company", company, "default_income_account")
+		or frappe.get_all(
+			"Account",
+			filters={"is_group": 0, "company": company},
+			or_filters={
+				"report_type": "Profit and Loss",
+				"account_type": ("in", ("Income Account", "Temporary")),
+			},
+			limit=1,
+			pluck="name",
+		)[0]
 	)
-	dunning_type.save()
+
+
+def create_payment_terms_template_for_dunning():
+	from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_term
+
+	create_payment_term("_Test Payment Term 1 for Dunning")
+	create_payment_term("_Test Payment Term 2 for Dunning")
+
+	if not frappe.db.exists("Payment Terms Template", "_Test 50-50 for Dunning"):
+		frappe.get_doc(
+			{
+				"doctype": "Payment Terms Template",
+				"template_name": "_Test 50-50 for Dunning",
+				"allocate_payment_based_on_payment_terms": 1,
+				"terms": [
+					{
+						"doctype": "Payment Terms Template Detail",
+						"payment_term": "_Test Payment Term 1 for Dunning",
+						"invoice_portion": 50.00,
+						"credit_days_based_on": "Day(s) after invoice date",
+						"credit_days": 5,
+					},
+					{
+						"doctype": "Payment Terms Template Detail",
+						"payment_term": "_Test Payment Term 2 for Dunning",
+						"invoice_portion": 50.00,
+						"credit_days_based_on": "Day(s) after invoice date",
+						"credit_days": 10,
+					},
+				],
+			}
+		).insert()
diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.js b/erpnext/accounts/doctype/dunning_type/dunning_type.js
index 54156b4..b2c08c1 100644
--- a/erpnext/accounts/doctype/dunning_type/dunning_type.js
+++ b/erpnext/accounts/doctype/dunning_type/dunning_type.js
@@ -1,8 +1,24 @@
 // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
 // For license information, please see license.txt
 
-frappe.ui.form.on('Dunning Type', {
-	// refresh: function(frm) {
-
-	// }
+frappe.ui.form.on("Dunning Type", {
+	setup: function (frm) {
+		frm.set_query("income_account", () => {
+			return {
+				filters: {
+					root_type: "Income",
+					is_group: 0,
+					company: frm.doc.company,
+				},
+			};
+		});
+		frm.set_query("cost_center", () => {
+			return {
+				filters: {
+					is_group: 0,
+					company: frm.doc.company,
+				},
+			};
+		});
+	},
 });
diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.json b/erpnext/accounts/doctype/dunning_type/dunning_type.json
index da43664..5e39769 100644
--- a/erpnext/accounts/doctype/dunning_type/dunning_type.json
+++ b/erpnext/accounts/doctype/dunning_type/dunning_type.json
@@ -1,23 +1,26 @@
 {
  "actions": [],
  "allow_rename": 1,
- "autoname": "field:dunning_type",
+ "beta": 1,
  "creation": "2019-12-04 04:59:08.003664",
  "doctype": "DocType",
  "editable_grid": 1,
  "engine": "InnoDB",
  "field_order": [
   "dunning_type",
-  "overdue_interval_section",
-  "start_day",
-  "column_break_4",
-  "end_day",
+  "is_default",
+  "column_break_3",
+  "company",
   "section_break_6",
   "dunning_fee",
   "column_break_8",
   "rate_of_interest",
   "text_block_section",
-  "dunning_letter_text"
+  "dunning_letter_text",
+  "section_break_9",
+  "income_account",
+  "column_break_13",
+  "cost_center"
  ],
  "fields": [
   {
@@ -46,10 +49,6 @@
    "options": "Dunning Letter Text"
   },
   {
-   "fieldname": "column_break_4",
-   "fieldtype": "Column Break"
-  },
-  {
    "fieldname": "section_break_6",
    "fieldtype": "Section Break"
   },
@@ -58,32 +57,61 @@
    "fieldtype": "Column Break"
   },
   {
-   "fieldname": "overdue_interval_section",
-   "fieldtype": "Section Break",
-   "label": "Overdue Interval"
-  },
-  {
-   "fieldname": "start_day",
-   "fieldtype": "Int",
-   "label": "Start Day"
-  },
-  {
-   "fieldname": "end_day",
-   "fieldtype": "Int",
-   "label": "End Day"
-  },
-  {
    "fieldname": "rate_of_interest",
    "fieldtype": "Float",
    "in_list_view": 1,
    "label": "Rate of Interest (%) Yearly"
+  },
+  {
+   "default": "0",
+   "fieldname": "is_default",
+   "fieldtype": "Check",
+   "label": "Is Default"
+  },
+  {
+   "fieldname": "section_break_9",
+   "fieldtype": "Section Break",
+   "label": "Accounting Details"
+  },
+  {
+   "fieldname": "income_account",
+   "fieldtype": "Link",
+   "label": "Income Account",
+   "options": "Account"
+  },
+  {
+   "fieldname": "cost_center",
+   "fieldtype": "Link",
+   "label": "Cost Center",
+   "options": "Cost Center"
+  },
+  {
+   "fieldname": "column_break_3",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "company",
+   "fieldtype": "Link",
+   "label": "Company",
+   "options": "Company",
+   "reqd": 1
+  },
+  {
+   "fieldname": "column_break_13",
+   "fieldtype": "Column Break"
   }
  ],
- "links": [],
- "modified": "2020-07-15 17:14:17.835074",
+ "links": [
+  {
+   "link_doctype": "Dunning",
+   "link_fieldname": "dunning_type"
+  }
+ ],
+ "modified": "2021-11-13 00:25:35.659283",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Dunning Type",
+ "naming_rule": "By script",
  "owner": "Administrator",
  "permissions": [
   {
diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.py b/erpnext/accounts/doctype/dunning_type/dunning_type.py
index 1b9bb9c..226e159 100644
--- a/erpnext/accounts/doctype/dunning_type/dunning_type.py
+++ b/erpnext/accounts/doctype/dunning_type/dunning_type.py
@@ -2,9 +2,11 @@
 # For license information, please see license.txt
 
 
-# import frappe
+import frappe
 from frappe.model.document import Document
 
 
 class DunningType(Document):
-	pass
+	def autoname(self):
+		company_abbr = frappe.get_value("Company", self.company, "abbr")
+		self.name = f"{self.dunning_type} - {company_abbr}"
diff --git a/erpnext/accounts/doctype/dunning_type/test_records.json b/erpnext/accounts/doctype/dunning_type/test_records.json
new file mode 100644
index 0000000..7f28aab
--- /dev/null
+++ b/erpnext/accounts/doctype/dunning_type/test_records.json
@@ -0,0 +1,36 @@
+[
+    {
+        "doctype": "Dunning Type",
+        "dunning_type": "_Test First Notice",
+        "company": "_Test Company",
+        "is_default": 1,
+        "dunning_fee": 0.0,
+        "rate_of_interest": 0.0,
+        "dunning_letter_text": [
+            {
+                "language": "en",
+                "body_text": "We have still not received payment for our invoice",
+                "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees."
+            }
+        ],
+        "income_account": "Sales - _TC",
+        "cost_center": "_Test Cost Center - _TC"
+    },
+    {
+        "doctype": "Dunning Type",
+        "dunning_type": "_Test Second Notice",
+        "company": "_Test Company",
+        "is_default": 0,
+        "dunning_fee": 10.0,
+        "rate_of_interest": 10.0,
+        "dunning_letter_text": [
+            {
+                "language": "en",
+                "body_text": "We have still not received payment for our invoice",
+                "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees."
+            }
+        ],
+        "income_account": "Sales - _TC",
+        "cost_center": "_Test Cost Center - _TC"
+    }
+]
diff --git a/erpnext/accounts/doctype/overdue_payment/__init__.py b/erpnext/accounts/doctype/overdue_payment/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/overdue_payment/__init__.py
diff --git a/erpnext/accounts/doctype/overdue_payment/overdue_payment.json b/erpnext/accounts/doctype/overdue_payment/overdue_payment.json
new file mode 100644
index 0000000..99e1646
--- /dev/null
+++ b/erpnext/accounts/doctype/overdue_payment/overdue_payment.json
@@ -0,0 +1,170 @@
+{
+ "actions": [],
+ "creation": "2021-09-15 18:34:27.172906",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "sales_invoice",
+  "payment_schedule",
+  "dunning_level",
+  "payment_term",
+  "section_break_15",
+  "description",
+  "section_break_4",
+  "due_date",
+  "overdue_days",
+  "mode_of_payment",
+  "column_break_5",
+  "invoice_portion",
+  "section_break_16",
+  "payment_amount",
+  "outstanding",
+  "paid_amount",
+  "discounted_amount",
+  "interest"
+ ],
+ "fields": [
+  {
+   "columns": 2,
+   "fieldname": "payment_term",
+   "fieldtype": "Link",
+   "label": "Payment Term",
+   "options": "Payment Term",
+   "print_hide": 1,
+   "read_only": 1
+  },
+  {
+   "collapsible": 1,
+   "fieldname": "section_break_15",
+   "fieldtype": "Section Break",
+   "label": "Description"
+  },
+  {
+   "columns": 2,
+   "fetch_from": "payment_term.description",
+   "fieldname": "description",
+   "fieldtype": "Small Text",
+   "label": "Description",
+   "read_only": 1
+  },
+  {
+   "fieldname": "section_break_4",
+   "fieldtype": "Section Break"
+  },
+  {
+   "columns": 2,
+   "fieldname": "due_date",
+   "fieldtype": "Date",
+   "label": "Due Date",
+   "read_only": 1
+  },
+  {
+   "fieldname": "mode_of_payment",
+   "fieldtype": "Link",
+   "label": "Mode of Payment",
+   "options": "Mode of Payment",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_5",
+   "fieldtype": "Column Break"
+  },
+  {
+   "columns": 2,
+   "fieldname": "invoice_portion",
+   "fieldtype": "Percent",
+   "label": "Invoice Portion",
+   "read_only": 1
+  },
+  {
+   "columns": 2,
+   "fieldname": "payment_amount",
+   "fieldtype": "Currency",
+   "label": "Payment Amount",
+   "options": "currency",
+   "read_only": 1
+  },
+  {
+   "fetch_from": "payment_amount",
+   "fieldname": "outstanding",
+   "fieldtype": "Currency",
+   "in_list_view": 1,
+   "label": "Outstanding",
+   "options": "currency",
+   "read_only": 1
+  },
+  {
+   "depends_on": "paid_amount",
+   "fieldname": "paid_amount",
+   "fieldtype": "Currency",
+   "label": "Paid Amount",
+   "options": "currency"
+  },
+  {
+   "default": "0",
+   "depends_on": "discounted_amount",
+   "fieldname": "discounted_amount",
+   "fieldtype": "Currency",
+   "label": "Discounted Amount",
+   "print_hide": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "sales_invoice",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Sales Invoice",
+   "options": "Sales Invoice",
+   "read_only": 1,
+   "reqd": 1
+  },
+  {
+   "fieldname": "payment_schedule",
+   "fieldtype": "Data",
+   "label": "Payment Schedule",
+   "print_hide": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "overdue_days",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Overdue Days",
+   "read_only": 1
+  },
+  {
+   "default": "1",
+   "fieldname": "dunning_level",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "Dunning Level",
+   "read_only": 1
+  },
+  {
+   "fieldname": "section_break_16",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "interest",
+   "fieldtype": "Currency",
+   "in_list_view": 1,
+   "label": "Interest",
+   "options": "currency",
+   "read_only": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-09-23 13:48:27.898830",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Overdue Payment",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/overdue_payment/overdue_payment.py b/erpnext/accounts/doctype/overdue_payment/overdue_payment.py
new file mode 100644
index 0000000..6a543ad
--- /dev/null
+++ b/erpnext/accounts/doctype/overdue_payment/overdue_payment.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class OverduePayment(Document):
+	pass
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index c0fd63e..c85c1ae 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -2002,28 +2002,27 @@
 				pe.append("references", reference)
 		else:
 			if dt == "Dunning":
+				for overdue_payment in doc.overdue_payments:
+					pe.append(
+						"references",
+						{
+							"reference_doctype": "Sales Invoice",
+							"reference_name": overdue_payment.sales_invoice,
+							"payment_term": overdue_payment.payment_term,
+							"due_date": overdue_payment.due_date,
+							"total_amount": overdue_payment.outstanding,
+							"outstanding_amount": overdue_payment.outstanding,
+							"allocated_amount": overdue_payment.outstanding,
+						},
+					)
+
 				pe.append(
-					"references",
+					"deductions",
 					{
-						"reference_doctype": "Sales Invoice",
-						"reference_name": doc.get("sales_invoice"),
-						"bill_no": doc.get("bill_no"),
-						"due_date": doc.get("due_date"),
-						"total_amount": doc.get("outstanding_amount"),
-						"outstanding_amount": doc.get("outstanding_amount"),
-						"allocated_amount": doc.get("outstanding_amount"),
-					},
-				)
-				pe.append(
-					"references",
-					{
-						"reference_doctype": dt,
-						"reference_name": dn,
-						"bill_no": doc.get("bill_no"),
-						"due_date": doc.get("due_date"),
-						"total_amount": doc.get("dunning_amount"),
-						"outstanding_amount": doc.get("dunning_amount"),
-						"allocated_amount": doc.get("dunning_amount"),
+						"account": doc.income_account,
+						"cost_center": doc.cost_center,
+						"amount": -1 * doc.dunning_amount,
+						"description": _("Interest and/or dunning fee"),
 					},
 				)
 			else:
@@ -2117,8 +2116,10 @@
 
 def set_payment_type(dt, doc):
 	if (
-		dt == "Sales Order" or (dt in ("Sales Invoice", "Dunning") and doc.outstanding_amount > 0)
-	) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0):
+		(dt == "Sales Order" or (dt == "Sales Invoice" and doc.outstanding_amount > 0))
+		or (dt == "Purchase Invoice" and doc.outstanding_amount < 0)
+		or dt == "Dunning"
+	):
 		payment_type = "Receive"
 	else:
 		payment_type = "Pay"
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 8753ebc..4ec103c 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -142,9 +142,15 @@
 					cur_frm.events.create_invoice_discounting(cur_frm);
 				}, __('Create'));
 
-				if (doc.due_date < frappe.datetime.get_today()) {
-					cur_frm.add_custom_button(__('Dunning'), function() {
-						cur_frm.events.create_dunning(cur_frm);
+				const payment_is_overdue = doc.payment_schedule.map(
+					row => Date.parse(row.due_date) < Date.now()
+				).reduce(
+					(prev, current) => prev || current
+				);
+
+				if (payment_is_overdue) {
+					this.frm.add_custom_button(__('Dunning'), () => {
+						this.frm.events.create_dunning(this.frm);
 					}, __('Create'));
 				}
 			}
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 7ab1c89..b3212b5 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -2516,55 +2516,49 @@
 
 
 @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, "company": source.company})
+		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())
+			target.income_account = dunning_type.income_account
+			target.cost_center = dunning_type.cost_center
+			letter_text = get_dunning_letter_text(
+				dunning_type=dunning_type.name, doc=target.as_dict(), language=source.language
+			)
+
 			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,
-			)
-			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,
-		{
+		target.validate()
+
+	return get_mapped_doc(
+		from_doctype="Sales Invoice",
+		from_docname=source_name,
+		target_doc=target_doc,
+		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 and getdate(doc.due_date) < getdate(),
+			},
 		},
-		target_doc,
-		set_missing_values,
+		postprocess=postprocess_dunning,
+		ignore_permissions=ignore_permissions,
 	)
-	return doclist
 
 
 def check_if_return_invoice_linked_with_payment_entry(self):
diff --git a/erpnext/accounts/print_format/dunning_letter/dunning_letter.json b/erpnext/accounts/print_format/dunning_letter/dunning_letter.json
index a7eac70..c48e1cf 100644
--- a/erpnext/accounts/print_format/dunning_letter/dunning_letter.json
+++ b/erpnext/accounts/print_format/dunning_letter/dunning_letter.json
@@ -1,4 +1,5 @@
 {
+ "absolute_value": 0,
  "align_labels_right": 0,
  "creation": "2019-12-11 04:37:14.012805",
  "css": ".print-format th {\n    background-color: transparent !important;\n    border-bottom: 1px solid !important;\n    border-top: none !important;\n}\n.print-format .ql-editor {\n    padding-left: 0px;\n    padding-right: 0px;\n}\n\n.print-format table {\n    margin-bottom: 0px;\n    }\n.print-format .table-data tr:last-child { \n    border-bottom: 1px solid !important;\n}\n\n.print-format .table-inner tr:last-child {\n    border-bottom:none !important;\n}\n.print-format .table-inner {\n    margin: 0px 0px;\n}\n\n.print-format .table-data ul li { \n    color:#787878 !important;\n}\n\n.no-top-border {\n    border-top:none !important;\n}\n\n.table-inner td {\n    padding-left: 0px !important;    \n    padding-top: 1px !important;\n    padding-bottom: 1px !important;\n    color:#787878 !important;\n}\n\n.total {\n    background-color: lightgrey !important;\n    padding-top: 4px !important;\n    padding-bottom: 4px !important;\n}\n",
@@ -9,10 +10,10 @@
  "docstatus": 0,
  "doctype": "Print Format",
  "font": "Arial",
- "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"<div></div>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<b>{{doc.customer_name}}</b> <br />\\n{{doc.address_display}}\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<div style=\\\"text-align:left;\\\">\\n<div style=\\\"font-size:24px; text-transform:uppercase;\\\">{{_(doc.dunning_type)}}</div>\\n<div style=\\\"font-size:16px;padding-bottom:5px;\\\">{{ doc.name }}</div>\\n</div>\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldname\": \"sales_invoice\", \"print_hide\": 0, \"label\": \"Sales Invoice\"}, {\"fieldname\": \"due_date\", \"print_hide\": 0, \"label\": \"Due Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"body_text\", \"print_hide\": 0, \"label\": \"Body Text\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<table class=\\\"table table-borderless table-data\\\">\\n   <tbody>\\n        <tr>\\n            <th>{{_(\\\"Description\\\")}}</th>\\n\\t        <th style=\\\"text-align: right;\\\">{{_(\\\"Amount\\\")}}</th>\\n        </tr>\\n        <tr>\\n            <td>\\n                {{_(\\\"Outstanding Amount\\\")}}\\n             </td>\\n            <td style=\\\"text-align: right;\\\">\\n                {{doc.get_formatted(\\\"outstanding_amount\\\")}}\\n            </td>\\n        </tr>\\n        {%if doc.rate_of_interest > 0%}\\n        <tr>\\n            <td>\\n                {{_(\\\"Interest \\\")}} {{doc.rate_of_interest}}% p.a. ({{doc.overdue_days}} {{_(\\\"days\\\")}})\\n             </td>\\n            <td style=\\\"text-align: right;\\\">\\n                {{doc.get_formatted(\\\"interest_amount\\\")}}\\n            </td>\\n        </tr>\\n        {% endif %}\\n        {%if doc.dunning_fee > 0%}\\n        <tr>\\n            <td>\\n                {{_(\\\"Dunning Fee\\\")}}\\n             </td>\\n            <td style=\\\"text-align: right;\\\">\\n                {{doc.get_formatted(\\\"dunning_fee\\\")}}\\n            </td>\\n        </tr>\\n        {% endif %}\\n    </tbody>\\n</table>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"\\n<div class=\\\"row total\\\" style =\\\"margin-right: 0px;\\\">\\n\\t\\t<div class=\\\"col-xs-5\\\">\\n\\t\\t\\t<b>{{_(\\\"Grand Total\\\")}}</b></div>\\n\\t\\t<div class=\\\"col-xs-7 text-right\\\" style=\\\"padding-right: 4px;\\\">\\n\\t\\t\\t<b>{{doc.get_formatted(\\\"grand_total\\\")}}</b>\\n\\t\\t</div>\\n</div>\\n\\n\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"closing_text\", \"print_hide\": 0, \"label\": \"Closing Text\"}]",
+ "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"<div></div>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<b>{{doc.customer_name}}</b> <br />\\n{{doc.address_display}}\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<div style=\\\"text-align:left;\\\">\\n<div style=\\\"font-size:24px; text-transform:uppercase;\\\">{{_(doc.dunning_type)}}</div>\\n<div style=\\\"font-size:16px;padding-bottom:5px;\\\">{{ doc.name }}</div>\\n</div>\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"body_text\", \"print_hide\": 0, \"label\": \"Body Text\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"overdue_payments\", \"print_hide\": 0, \"label\": \"Overdue Payments\", \"visible_columns\": [{\"fieldname\": \"sales_invoice\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"dunning_level\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"due_date\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"overdue_days\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"invoice_portion\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"outstanding\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"interest\", \"print_width\": \"\", \"print_hide\": 0}]}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"total_outstanding\", \"print_hide\": 0, \"label\": \"Total Outstanding\"}, {\"fieldname\": \"dunning_fee\", \"print_hide\": 0, \"label\": \"Dunning Fee\"}, {\"fieldname\": \"total_interest\", \"print_hide\": 0, \"label\": \"Total Interest\"}, {\"fieldname\": \"grand_total\", \"print_hide\": 0, \"label\": \"Grand Total\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"closing_text\", \"print_hide\": 0, \"label\": \"Closing Text\"}]",
  "idx": 0,
  "line_breaks": 0,
- "modified": "2020-07-14 18:25:44.348207",
+ "modified": "2021-09-30 10:22:02.603871",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Dunning Letter",
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index a6d939e..68af89b 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -334,6 +334,7 @@
 			"erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status",
 			"erpnext.accounts.doctype.dunning.dunning.resolve_dunning",
 		],
+		"on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
 		"on_trash": "erpnext.regional.check_deletion_permission",
 	},
 	"Address": {
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index b3b9bc6..4536abf 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -333,4 +333,5 @@
 erpnext.patches.v14_0.cleanup_workspaces
 erpnext.patches.v15_0.remove_loan_management_module #2023-07-03
 erpnext.patches.v14_0.set_report_in_process_SOA
-erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users
\ No newline at end of file
+erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users
+erpnext.patches.v14_0.single_to_multi_dunning
diff --git a/erpnext/patches/v14_0/single_to_multi_dunning.py b/erpnext/patches/v14_0/single_to_multi_dunning.py
new file mode 100644
index 0000000..7a8e591
--- /dev/null
+++ b/erpnext/patches/v14_0/single_to_multi_dunning.py
@@ -0,0 +1,49 @@
+import frappe
+
+from erpnext.accounts.general_ledger import make_reverse_gl_entries
+
+
+def execute():
+	frappe.reload_doc("accounts", "doctype", "overdue_payment")
+	frappe.reload_doc("accounts", "doctype", "dunning")
+
+	all_dunnings = frappe.get_all("Dunning", filters={"docstatus": ("!=", 2)}, pluck="name")
+	for dunning_name in all_dunnings:
+		dunning = frappe.get_doc("Dunning", dunning_name)
+		if not dunning.sales_invoice:
+			# nothing we can do
+			continue
+
+		if dunning.overdue_payments:
+			# something's already here, doesn't need patching
+			continue
+
+		payment_schedules = frappe.get_all(
+			"Payment Schedule",
+			filters={"parent": dunning.sales_invoice},
+			fields=[
+				"parent as sales_invoice",
+				"name as payment_schedule",
+				"payment_term",
+				"due_date",
+				"invoice_portion",
+				"payment_amount",
+				# at the time of creating this dunning, the full amount was outstanding
+				"payment_amount as outstanding",
+				"'0' as paid_amount",
+				"discounted_amount",
+			],
+		)
+
+		dunning.extend("overdue_payments", payment_schedules)
+		dunning.validate()
+
+		dunning.flags.ignore_validate_update_after_submit = True
+		dunning.save()
+
+		if dunning.status != "Resolved":
+			# With the new logic, dunning amount gets recorded as additional income
+			# at time of payment. We don't want to record the dunning amount twice,
+			# so we reverse previous GL Entries that recorded the dunning amount at
+			# time of submission of the Dunning.
+			make_reverse_gl_entries(voucher_type="Dunning", voucher_no=dunning.name)