feat: Dunning (#22559)
* feat: Dunning
* fix: Replaces spaces with tab
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
diff --git a/erpnext/accounts/doctype/dunning/__init__.py b/erpnext/accounts/doctype/dunning/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/dunning/__init__.py
diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js
new file mode 100644
index 0000000..c563368
--- /dev/null
+++ b/erpnext/accounts/doctype/dunning/dunning.js
@@ -0,0 +1,149 @@
+// Copyright (c) 2020, 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", () => {
+ return {
+ filters: {
+ docstatus: 1,
+ company: frm.doc.company,
+ outstanding_amount: [">", 0],
+ status: "Overdue"
+ },
+ };
+ });
+ frm.set_query("income_account", () => {
+ return {
+ filters: {
+ company: frm.doc.company,
+ root_type: "Income",
+ is_group: 0
+ }
+ };
+ });
+ },
+ 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");
+ });
+ }
+ if (frm.doc.docstatus === 1 && frm.doc.status !== "Resolved") {
+ frm.add_custom_button(
+ __("Payment"),
+ function () {
+ frm.events.make_payment_entry(frm);
+ },__("Create")
+ );
+ frm.page.set_inner_btn_group_as_primary(__("Create"));
+ }
+ },
+ 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", "");
+ }
+ }
+ );
+ },
+ dunning_type: function (frm) {
+ frm.trigger("get_dunning_letter_text");
+ },
+ language: function (frm) {
+ frm.trigger("get_dunning_letter_text");
+ },
+ get_dunning_letter_text: function (frm) {
+ if (frm.doc.dunning_type) {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text",
+ args: {
+ dunning_type: frm.doc.dunning_type,
+ language: frm.doc.language,
+ doc: frm.doc,
+ },
+ callback: function (r) {
+ if (r.message) {
+ frm.set_value("body_text", r.message.body_text);
+ frm.set_value("closing_text", r.message.closing_text);
+ frm.set_value("language", r.message.language);
+ } else {
+ frm.set_value("body_text", "");
+ frm.set_value("closing_text", "");
+ }
+ },
+ });
+ }
+ },
+ 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");
+ },
+ dunning_fee: function (frm) {
+ frm.trigger("calculate_interest_and_amount");
+ },
+ sales_invoice: function (frm) {
+ frm.trigger("calculate_overdue_days");
+ },
+ 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);
+ }
+ },
+ calculate_interest_and_amount: function (frm) {
+ const interest_per_year = frm.doc.outstanding_amount * frm.doc.rate_of_interest / 100;
+ const interest_amount = interest_per_year / 365 * frm.doc.overdue_days || 0;
+ const dunning_amount = interest_amount + frm.doc.dunning_fee;
+ const grand_total = frm.doc.outstanding_amount + dunning_amount;
+ frm.set_value("interest_amount", interest_amount);
+ frm.set_value("dunning_amount", dunning_amount);
+ frm.set_value("grand_total", grand_total);
+ },
+ make_payment_entry: function (frm) {
+ return frappe.call({
+ method:
+ "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry",
+ args: {
+ dt: frm.doc.doctype,
+ dn: frm.doc.name,
+ },
+ callback: function (r) {
+ var doc = frappe.model.sync(r.message);
+ frappe.set_route("Form", doc[0].doctype, doc[0].name);
+ },
+ });
+ },
+});
diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json
new file mode 100644
index 0000000..b3eddf5
--- /dev/null
+++ b/erpnext/accounts/doctype/dunning/dunning.json
@@ -0,0 +1,370 @@
+{
+ "actions": [],
+ "allow_events_in_timeline": 1,
+ "autoname": "naming_series:",
+ "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",
+ "address_and_contact_section",
+ "address_display",
+ "contact_display",
+ "contact_mobile",
+ "contact_email",
+ "column_break_18",
+ "company_address_display",
+ "section_break_6",
+ "dunning_type",
+ "interest_amount",
+ "column_break_8",
+ "rate_of_interest",
+ "dunning_fee",
+ "section_break_12",
+ "dunning_amount",
+ "grand_total",
+ "income_account",
+ "column_break_17",
+ "status",
+ "printing_setting_section",
+ "language",
+ "body_text",
+ "column_break_22",
+ "letter_head",
+ "closing_text",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "default": "DUNN-.MM.-.YY.-",
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Series",
+ "options": "DUNN-.MM.-.YY.-"
+ },
+ {
+ "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",
+ "fieldname": "customer_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Customer Name",
+ "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"
+ },
+ {
+ "default": "Today",
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "label": "Date"
+ },
+ {
+ "fieldname": "overdue_days",
+ "fieldtype": "Int",
+ "label": "Overdue Days",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "dunning_type",
+ "fieldtype": "Link",
+ "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
+ },
+ {
+ "fieldname": "column_break_8",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fetch_from": "dunning_type.dunning_fee",
+ "fetch_if_empty": 1,
+ "fieldname": "dunning_fee",
+ "fieldtype": "Currency",
+ "label": "Dunning Fee",
+ "precision": "2"
+ },
+ {
+ "fieldname": "section_break_12",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_17",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "printing_setting_section",
+ "fieldtype": "Section Break",
+ "label": "Printing Setting"
+ },
+ {
+ "fieldname": "language",
+ "fieldtype": "Link",
+ "label": "Print Language",
+ "options": "Language"
+ },
+ {
+ "fieldname": "letter_head",
+ "fieldtype": "Link",
+ "label": "Letter Head",
+ "options": "Letter Head"
+ },
+ {
+ "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",
+ "no_copy": 1,
+ "options": "Dunning",
+ "print_hide": 1,
+ "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"
+ },
+ {
+ "fieldname": "closing_text",
+ "fieldtype": "Text Editor",
+ "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"
+ },
+ {
+ "default": "0",
+ "fetch_from": "dunning_type.interest_rate",
+ "fetch_if_empty": 1,
+ "fieldname": "rate_of_interest",
+ "fieldtype": "Float",
+ "label": "Rate of Interest (%) Yearly"
+ },
+ {
+ "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",
+ "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",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "sales_invoice.contact_email",
+ "fieldname": "contact_email",
+ "fieldtype": "Data",
+ "label": "Contact Email",
+ "options": "Email",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "sales_invoice.customer",
+ "fieldname": "customer",
+ "fieldtype": "Link",
+ "label": "Customer",
+ "options": "Customer",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "grand_total",
+ "fieldtype": "Currency",
+ "label": "Grand Total",
+ "precision": "2",
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "default": "Unresolved",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_standard_filter": 1,
+ "label": "Status",
+ "options": "Draft\nResolved\nUnresolved\nCancelled"
+ },
+ {
+ "fieldname": "dunning_amount",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Dunning Amount",
+ "read_only": 1
+ },
+ {
+ "fieldname": "income_account",
+ "fieldtype": "Link",
+ "label": "Income Account",
+ "options": "Account"
+ },
+ {
+ "fetch_from": "sales_invoice.conversion_rate",
+ "fieldname": "conversion_rate",
+ "fieldtype": "Float",
+ "hidden": 1,
+ "label": "Conversion Rate",
+ "read_only": 1
+ }
+ ],
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-07-21 18:20:23.512151",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Dunning",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "ASC",
+ "title_field": "customer_name",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py
new file mode 100644
index 0000000..0be6a48
--- /dev/null
+++ b/erpnext/accounts/doctype/dunning/dunning.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import json
+from six import string_types
+from frappe.utils import getdate, get_datetime, rounded, flt
+from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year
+from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
+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.db.get_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_amount(self):
+ amounts = calculate_interest_and_amount(
+ self.posting_date, self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days)
+ if self.interest_amount != amounts.get('interest_amount'):
+ self.interest_amount = amounts.get('interest_amount')
+ if self.dunning_amount != amounts.get('dunning_amount'):
+ self.dunning_amount = amounts.get('dunning_amount')
+ if self.grand_total != amounts.get('grand_total'):
+ self.grand_total = amounts.get('grand_total')
+
+ def on_submit(self):
+ self.make_gl_entries()
+
+ def on_cancel(self):
+ if self.dunning_amount:
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock 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
+ }, 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)
+
+
+def resolve_dunning(doc, state):
+ 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')})
+
+ for dunning in dunnings:
+ frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved')
+
+def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
+ interest_amount = 0
+ if rate_of_interest:
+ interest_per_year = rounded(flt(outstanding_amount) * flt(rate_of_interest))/100
+ interest_amount = (
+ interest_per_year / days_in_year(get_datetime(posting_date).year)) * int(overdue_days)
+ grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee)
+ 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):
+ if isinstance(doc, string_types):
+ doc = json.loads(doc)
+ if language:
+ filters = {'parent': dunning_type, 'language': language}
+ else:
+ filters = {'parent': dunning_type, 'is_default_language': 1}
+ letter_text = frappe.db.get_value('Dunning Letter Text', filters,
+ ['body_text', 'closing_text', 'language'], as_dict=1)
+ if letter_text:
+ return {
+ 'body_text': frappe.render_template(letter_text.body_text, doc),
+ 'closing_text': frappe.render_template(letter_text.closing_text, doc),
+ 'language': letter_text.language
+ }
diff --git a/erpnext/accounts/doctype/dunning/dunning_list.js b/erpnext/accounts/doctype/dunning/dunning_list.js
new file mode 100644
index 0000000..8dc0a8c
--- /dev/null
+++ b/erpnext/accounts/doctype/dunning/dunning_list.js
@@ -0,0 +1,9 @@
+frappe.listview_settings["Dunning"] = {
+ get_indicator: function (doc) {
+ if (doc.status === "Resolved") {
+ return [__("Resolved"), "green", "status,=,Resolved"];
+ } else {
+ return [__("Unresolved"), "red", "status,=,Unresolved"];
+ }
+ },
+};
diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py
new file mode 100644
index 0000000..cb18309
--- /dev/null
+++ b/erpnext/accounts/doctype/dunning/test_dunning.py
@@ -0,0 +1,100 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+from frappe.utils import add_days, today, nowdate
+from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice_against_cost_center
+from erpnext.accounts.doctype.dunning.dunning import calculate_interest_and_amount
+from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
+
+
+class TestDunning(unittest.TestCase):
+ @classmethod
+ def setUpClass(self):
+ create_dunning_type()
+ unlink_payment_on_cancel_of_invoice()
+
+ @classmethod
+ def tearDownClass(self):
+ unlink_payment_on_cancel_of_invoice(0)
+
+ def test_dunning(self):
+ dunning = create_dunning()
+ amounts = calculate_interest_and_amount(
+ dunning.posting_date, 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_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.assertEquals(expected_values[gle.account][0], gle.account)
+ self.assertEquals(expected_values[gle.account][1], gle.debit)
+ self.assertEquals(expected_values[gle.account][2], gle.credit)
+
+ def test_payment_entry(self):
+ dunning = create_dunning()
+ 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)
+
+
+def create_dunning():
+ 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')
+ 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
+
+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.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, including interest and late fees.'
+ }
+ )
+ dunning_type.save()
diff --git a/erpnext/accounts/doctype/dunning_letter_text/__init__.py b/erpnext/accounts/doctype/dunning_letter_text/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/dunning_letter_text/__init__.py
diff --git a/erpnext/accounts/doctype/dunning_letter_text/dunning_letter_text.json b/erpnext/accounts/doctype/dunning_letter_text/dunning_letter_text.json
new file mode 100644
index 0000000..5ede3a1
--- /dev/null
+++ b/erpnext/accounts/doctype/dunning_letter_text/dunning_letter_text.json
@@ -0,0 +1,70 @@
+{
+ "actions": [],
+ "creation": "2019-12-06 04:25:40.215625",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "language",
+ "is_default_language",
+ "section_break_4",
+ "body_text",
+ "closing_text",
+ "section_break_7",
+ "body_and_closing_text_help"
+ ],
+ "fields": [
+ {
+ "fieldname": "language",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Language",
+ "options": "Language"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_default_language",
+ "fieldtype": "Check",
+ "label": "Is Default Language"
+ },
+ {
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break"
+ },
+ {
+ "description": "Letter or Email Body Text",
+ "fieldname": "body_text",
+ "fieldtype": "Text Editor",
+ "in_list_view": 1,
+ "label": "Body Text"
+ },
+ {
+ "description": "Letter or Email Closing Text",
+ "fieldname": "closing_text",
+ "fieldtype": "Text Editor",
+ "in_list_view": 1,
+ "label": "Closing Text"
+ },
+ {
+ "fieldname": "section_break_7",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "body_and_closing_text_help",
+ "fieldtype": "HTML",
+ "label": "Body and Closing Text Help",
+ "options": "<h4>Body Text and Closing Text Example</h4>\n\n<div>We have noticed that you have not yet paid invoice {{sales_invoice}} for {{frappe.db.get_value(\"Currency\", currency, \"symbol\")}} {{outstanding_amount}}. This is a friendly reminder that the invoice was due on {{due_date}}. Please pay the amount due immediately to avoid any further dunning cost.</div>\n\n<h4>How to get fieldnames</h4>\n\n<p>The fieldnames you can use in your template are the fields in the document. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Sales Invoice)</p>\n\n<h4>Templating</h4>\n\n<p>Templates are compiled using the Jinja Templating Language. To learn more about Jinja, <a class=\"strong\" href=\"http://jinja.pocoo.org/docs/dev/templates/\">read this documentation.</a></p>"
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-07-14 18:02:35.988958",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Dunning Letter Text",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/dunning_letter_text/dunning_letter_text.py b/erpnext/accounts/doctype/dunning_letter_text/dunning_letter_text.py
new file mode 100644
index 0000000..426497b
--- /dev/null
+++ b/erpnext/accounts/doctype/dunning_letter_text/dunning_letter_text.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class DunningLetterText(Document):
+ pass
diff --git a/erpnext/accounts/doctype/dunning_type/__init__.py b/erpnext/accounts/doctype/dunning_type/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/dunning_type/__init__.py
diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.js b/erpnext/accounts/doctype/dunning_type/dunning_type.js
new file mode 100644
index 0000000..54156b4
--- /dev/null
+++ b/erpnext/accounts/doctype/dunning_type/dunning_type.js
@@ -0,0 +1,8 @@
+// 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) {
+
+ // }
+});
diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.json b/erpnext/accounts/doctype/dunning_type/dunning_type.json
new file mode 100644
index 0000000..da43664
--- /dev/null
+++ b/erpnext/accounts/doctype/dunning_type/dunning_type.json
@@ -0,0 +1,129 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:dunning_type",
+ "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",
+ "section_break_6",
+ "dunning_fee",
+ "column_break_8",
+ "rate_of_interest",
+ "text_block_section",
+ "dunning_letter_text"
+ ],
+ "fields": [
+ {
+ "fieldname": "dunning_type",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Dunning Type",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "dunning_fee",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Dunning Fee"
+ },
+ {
+ "description": "This section allows the user to set the Body and Closing text of the Dunning Letter for the Dunning Type based on language, which can be used in Print.",
+ "fieldname": "text_block_section",
+ "fieldtype": "Section Break",
+ "label": "Dunning Letter"
+ },
+ {
+ "fieldname": "dunning_letter_text",
+ "fieldtype": "Table",
+ "options": "Dunning Letter Text"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_8",
+ "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"
+ }
+ ],
+ "links": [],
+ "modified": "2020-07-15 17:14:17.835074",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Dunning Type",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Administrator",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.py b/erpnext/accounts/doctype/dunning_type/dunning_type.py
new file mode 100644
index 0000000..8708748
--- /dev/null
+++ b/erpnext/accounts/doctype/dunning_type/dunning_type.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class DunningType(Document):
+ pass
diff --git a/erpnext/accounts/doctype/dunning_type/test_dunning_type.py b/erpnext/accounts/doctype/dunning_type/test_dunning_type.py
new file mode 100644
index 0000000..b2fb26f
--- /dev/null
+++ b/erpnext/accounts/doctype/dunning_type/test_dunning_type.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestDunningType(unittest.TestCase):
+ pass
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 42c9fde..4bbf63b 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -90,7 +90,7 @@
frm.set_query("reference_doctype", "references", function() {
if (frm.doc.party_type=="Customer") {
- var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry"];
+ var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
} else if (frm.doc.party_type=="Supplier") {
var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"];
} else if (frm.doc.party_type=="Employee") {
@@ -125,7 +125,7 @@
const child = locals[cdt][cdn];
const filters = {"docstatus": 1, "company": doc.company};
const party_type_doctypes = ['Sales Invoice', 'Sales Order', 'Purchase Invoice',
- 'Purchase Order', 'Expense Claim', 'Fees'];
+ 'Purchase Order', 'Expense Claim', 'Fees', 'Dunning'];
if (in_list(party_type_doctypes, child.reference_doctype)) {
filters[doc.party_type.toLowerCase()] = doc.party;
@@ -863,10 +863,10 @@
}
if(frm.doc.party_type=="Customer" &&
- !in_list(["Sales Order", "Sales Invoice", "Journal Entry"], row.reference_doctype)
+ !in_list(["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"], row.reference_doctype)
) {
frappe.model.set_value(row.doctype, row.name, "reference_doctype", null);
- frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Sales Order, Sales Invoice or Journal Entry", [row.idx]));
+ frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Sales Order, Sales Invoice, Journal Entry or Dunning", [row.idx]));
return false;
}
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 1cecab7..f9db14b 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -199,8 +199,8 @@
def validate_account_type(self, account, account_types):
account_type = frappe.db.get_value("Account", account, "account_type")
- if account_type not in account_types:
- frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types)))
+ # if account_type not in account_types:
+ # frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types)))
def set_exchange_rate(self):
if self.paid_from and not self.source_exchange_rate:
@@ -223,7 +223,7 @@
if self.party_type == "Student":
valid_reference_doctypes = ("Fees")
elif self.party_type == "Customer":
- valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry")
+ valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning")
elif self.party_type == "Supplier":
valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry")
elif self.party_type == "Employee":
@@ -897,6 +897,10 @@
total_amount = ref_doc.get("grand_total")
exchange_rate = 1
outstanding_amount = ref_doc.get("outstanding_amount")
+ if reference_doctype == "Dunning":
+ total_amount = ref_doc.get("dunning_amount")
+ exchange_rate = 1
+ outstanding_amount = ref_doc.get("dunning_amount")
elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1:
total_amount = ref_doc.get("total_amount")
if ref_doc.multi_currency:
@@ -951,7 +955,7 @@
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0:
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
- if dt in ("Sales Invoice", "Sales Order"):
+ if dt in ("Sales Invoice", "Sales Order", "Dunning"):
party_type = "Customer"
elif dt in ("Purchase Invoice", "Purchase Order"):
party_type = "Supplier"
@@ -980,7 +984,7 @@
party_account_currency = doc.get("party_account_currency") or get_account_currency(party_account)
# payment type
- if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees") and doc.outstanding_amount > 0)) \
+ if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \
or (dt=="Purchase Invoice" and doc.outstanding_amount < 0):
payment_type = "Receive"
else:
@@ -1006,6 +1010,9 @@
elif dt == "Fees":
grand_total = doc.grand_total
outstanding_amount = doc.outstanding_amount
+ elif dt == "Dunning":
+ grand_total = doc.grand_total
+ outstanding_amount = doc.grand_total
else:
if party_account_currency == doc.company_currency:
grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)
@@ -1075,15 +1082,35 @@
for reference in get_reference_as_per_payment_terms(doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount):
pe.append('references', reference)
else:
- pe.append("references", {
- 'reference_doctype': dt,
- 'reference_name': dn,
- "bill_no": doc.get("bill_no"),
- "due_date": doc.get("due_date"),
- 'total_amount': grand_total,
- 'outstanding_amount': outstanding_amount,
- 'allocated_amount': outstanding_amount
- })
+ if dt == "Dunning":
+ pe.append("references", {
+ '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')
+ })
+ else:
+ pe.append("references", {
+ 'reference_doctype': dt,
+ 'reference_name': dn,
+ "bill_no": doc.get("bill_no"),
+ "due_date": doc.get("due_date"),
+ 'total_amount': grand_total,
+ 'outstanding_amount': outstanding_amount,
+ 'allocated_amount': outstanding_amount
+ })
pe.setup_party_account_field()
pe.set_missing_values()
@@ -1172,4 +1199,4 @@
}, target_doc, set_missing_values)
- return doclist
+ return doclist
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index df0c3d2..061ce1c 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -96,6 +96,12 @@
cur_frm.add_custom_button(__('Invoice Discounting'), function() {
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);
+ }, __('Create'));
+ }
}
if (doc.docstatus === 1) {
@@ -824,6 +830,12 @@
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting",
frm: frm
});
+ },
+ create_dunning: function(frm) {
+ frappe.model.open_mapped_doc({
+ method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
+ frm: frm
+ });
}
})
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index bab5208..8984348 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -1602,3 +1602,37 @@
})
return invoice_discounting
+
+@frappe.whitelist()
+def create_dunning(source_name, target_doc=None):
+ from frappe.model.mapper import get_mapped_doc
+ from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text, calculate_interest_and_amount
+ 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]})
+ 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.posting_date, 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, {
+ "Sales Invoice": {
+ "doctype": "Dunning",
+ }
+ }, target_doc, set_missing_values)
+ return doclist
\ No newline at end of file
diff --git a/erpnext/accounts/print_format/dunning_letter/__init__.py b/erpnext/accounts/print_format/dunning_letter/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/print_format/dunning_letter/__init__.py
diff --git a/erpnext/accounts/print_format/dunning_letter/dunning_letter.json b/erpnext/accounts/print_format/dunning_letter/dunning_letter.json
new file mode 100644
index 0000000..a7eac70
--- /dev/null
+++ b/erpnext/accounts/print_format/dunning_letter/dunning_letter.json
@@ -0,0 +1,25 @@
+{
+ "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",
+ "custom_format": 0,
+ "default_print_language": "en",
+ "disabled": 0,
+ "doc_type": "Dunning",
+ "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\"}]",
+ "idx": 0,
+ "line_breaks": 0,
+ "modified": "2020-07-14 18:25:44.348207",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Dunning Letter",
+ "owner": "Administrator",
+ "print_format_builder": 0,
+ "print_format_type": "Jinja",
+ "raw_printing": 0,
+ "show_section_headings": 0,
+ "standard": "Yes"
+}
\ No newline at end of file
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index e8dda20..95a836f 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -249,7 +249,7 @@
"validate": "erpnext.regional.india.utils.update_grand_total_for_rcm"
},
"Payment Entry": {
- "on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status"],
+ "on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
"on_trash": "erpnext.regional.check_deletion_permission"
},
'Address': {
@@ -552,4 +552,4 @@
{'doctype': 'Hotel Room Package', 'index': 3},
{'doctype': 'Hotel Room Type', 'index': 4}
]
-}
+}
\ No newline at end of file