From e5b57ec965101a6183b3f8f2d74b2645cb1ecdbe Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 15 Sep 2021 19:15:31 +0200 Subject: [PATCH 01/60] feat: Overdue Payments table --- .../doctype/overdue_payments/__init__.py | 0 .../overdue_payments/overdue_payments.json | 171 ++++++++++++++++++ .../overdue_payments/overdue_payments.py | 8 + 3 files changed, 179 insertions(+) create mode 100644 erpnext/accounts/doctype/overdue_payments/__init__.py create mode 100644 erpnext/accounts/doctype/overdue_payments/overdue_payments.json create mode 100644 erpnext/accounts/doctype/overdue_payments/overdue_payments.py diff --git a/erpnext/accounts/doctype/overdue_payments/__init__.py b/erpnext/accounts/doctype/overdue_payments/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/overdue_payments/overdue_payments.json b/erpnext/accounts/doctype/overdue_payments/overdue_payments.json new file mode 100644 index 0000000000..57104c186c --- /dev/null +++ b/erpnext/accounts/doctype/overdue_payments/overdue_payments.json @@ -0,0 +1,171 @@ +{ + "actions": [], + "creation": "2021-09-15 18:34:27.172906", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "sales_invoice", + "payment_schedule", + "payment_term", + "section_break_15", + "description", + "section_break_4", + "due_date", + "overdue_days", + "mode_of_payment", + "column_break_5", + "invoice_portion", + "section_break_9", + "payment_amount", + "outstanding", + "paid_amount", + "discounted_amount", + "column_break_3", + "base_payment_amount", + "interest_amount" + ], + "fields": [ + { + "columns": 2, + "fieldname": "payment_term", + "fieldtype": "Link", + "in_list_view": 1, + "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 + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break" + }, + { + "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", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "base_payment_amount", + "fieldtype": "Currency", + "label": "Payment Amount (Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "sales_invoice", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Sales Invoice", + "options": "Sales Invoice", + "reqd": 1 + }, + { + "fieldname": "payment_schedule", + "fieldtype": "Data", + "label": "Payment Schedule", + "read_only": 1 + }, + { + "fieldname": "overdue_days", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Overdue Days", + "read_only": 1 + }, + { + "fieldname": "interest_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Interest Amount", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-09-15 19:04:54.082880", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Overdue Payments", + "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_payments/overdue_payments.py b/erpnext/accounts/doctype/overdue_payments/overdue_payments.py new file mode 100644 index 0000000000..844f8ecdbd --- /dev/null +++ b/erpnext/accounts/doctype/overdue_payments/overdue_payments.py @@ -0,0 +1,8 @@ +# 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 OverduePayments(Document): + pass From e7705327f003858b99215210869dbc1c24eff0b2 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 15 Sep 2021 19:15:53 +0200 Subject: [PATCH 02/60] feat: filter invoices --- erpnext/accounts/doctype/dunning/dunning.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 9909c6c2ab..2f997ba02e 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -3,11 +3,12 @@ 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" }, From 487c6018bfe6514972f4788584f2d6c83b2ce2b8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 15 Sep 2021 19:16:09 +0200 Subject: [PATCH 03/60] feat: restructure dunning doctype --- erpnext/accounts/doctype/dunning/dunning.json | 108 +++++++++--------- 1 file changed, 53 insertions(+), 55 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index 2a32b99f42..a0ddf04b6c 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -8,20 +8,19 @@ "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", "address_and_contact_section", + "customer_address", "address_display", + "contact_person", "contact_display", "contact_mobile", "contact_email", @@ -29,16 +28,17 @@ "company_address_display", "section_break_6", "dunning_type", - "dunning_fee", "column_break_8", "rate_of_interest", - "interest_amount", "section_break_12", - "dunning_amount", + "overdue_payments", + "section_break_28", + "column_break_17", + "total_interest", + "total_outstanding", + "dunning_fee", "grand_total", "income_account", - "column_break_17", - "status", "printing_setting_section", "language", "body_text", @@ -62,15 +62,6 @@ "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", @@ -79,13 +70,6 @@ "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" @@ -94,13 +78,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", @@ -115,14 +94,6 @@ "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" @@ -134,6 +105,7 @@ "fieldname": "dunning_fee", "fieldtype": "Currency", "label": "Dunning Fee", + "options": "currency", "precision": "2" }, { @@ -201,13 +173,6 @@ "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", @@ -222,6 +187,7 @@ "label": "Rate of Interest (%) Yearly" }, { + "collapsible": 1, "fieldname": "address_and_contact_section", "fieldtype": "Section Break", "label": "Address and Contact" @@ -273,13 +239,14 @@ "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 }, @@ -292,13 +259,6 @@ "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", @@ -312,6 +272,44 @@ "hidden": 1, "label": "Conversion Rate", "read_only": 1 + }, + { + "fieldname": "overdue_payments", + "fieldtype": "Table", + "label": "Overdue Payments", + "options": "Overdue Payments" + }, + { + "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" + }, + { + "fieldname": "contact_person", + "fieldtype": "Link", + "label": "Contact Person", + "options": "Contact" } ], "is_submittable": 1, From 86a8b0b30f6ac29fed1b3a635e4b5103e008f628 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 16 Sep 2021 14:09:32 +0200 Subject: [PATCH 04/60] refactor: doctype naming Overdue Payments -> Overdue Payment --- erpnext/accounts/doctype/dunning/dunning.json | 2 +- .../doctype/{overdue_payments => overdue_payment}/__init__.py | 0 .../overdue_payment.json} | 2 +- .../overdue_payments.py => overdue_payment/overdue_payment.py} | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename erpnext/accounts/doctype/{overdue_payments => overdue_payment}/__init__.py (100%) rename erpnext/accounts/doctype/{overdue_payments/overdue_payments.json => overdue_payment/overdue_payment.json} (99%) rename erpnext/accounts/doctype/{overdue_payments/overdue_payments.py => overdue_payment/overdue_payment.py} (84%) diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index a0ddf04b6c..b609a5ce14 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -277,7 +277,7 @@ "fieldname": "overdue_payments", "fieldtype": "Table", "label": "Overdue Payments", - "options": "Overdue Payments" + "options": "Overdue Payment" }, { "fieldname": "section_break_28", diff --git a/erpnext/accounts/doctype/overdue_payments/__init__.py b/erpnext/accounts/doctype/overdue_payment/__init__.py similarity index 100% rename from erpnext/accounts/doctype/overdue_payments/__init__.py rename to erpnext/accounts/doctype/overdue_payment/__init__.py diff --git a/erpnext/accounts/doctype/overdue_payments/overdue_payments.json b/erpnext/accounts/doctype/overdue_payment/overdue_payment.json similarity index 99% rename from erpnext/accounts/doctype/overdue_payments/overdue_payments.json rename to erpnext/accounts/doctype/overdue_payment/overdue_payment.json index 57104c186c..e5253bd12f 100644 --- a/erpnext/accounts/doctype/overdue_payments/overdue_payments.json +++ b/erpnext/accounts/doctype/overdue_payment/overdue_payment.json @@ -161,7 +161,7 @@ "modified": "2021-09-15 19:04:54.082880", "modified_by": "Administrator", "module": "Accounts", - "name": "Overdue Payments", + "name": "Overdue Payment", "owner": "Administrator", "permissions": [], "quick_entry": 1, diff --git a/erpnext/accounts/doctype/overdue_payments/overdue_payments.py b/erpnext/accounts/doctype/overdue_payment/overdue_payment.py similarity index 84% rename from erpnext/accounts/doctype/overdue_payments/overdue_payments.py rename to erpnext/accounts/doctype/overdue_payment/overdue_payment.py index 844f8ecdbd..e3820d74e0 100644 --- a/erpnext/accounts/doctype/overdue_payments/overdue_payments.py +++ b/erpnext/accounts/doctype/overdue_payment/overdue_payment.py @@ -4,5 +4,5 @@ # import frappe from frappe.model.document import Document -class OverduePayments(Document): +class OverduePayment(Document): pass From 8976e94a1d697a2a9a8930a3fe9274a0443dc176 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 16 Sep 2021 16:38:36 +0200 Subject: [PATCH 05/60] feat: rework doctypes --- erpnext/accounts/doctype/dunning/dunning.json | 80 ++++++++++++++----- .../doctype/dunning_type/dunning_type.json | 33 +++----- 2 files changed, 67 insertions(+), 46 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index b609a5ce14..a0e3c150fd 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -6,7 +6,6 @@ "doctype": "DocType", "engine": "InnoDB", "field_order": [ - "title", "naming_series", "customer", "customer_name", @@ -33,18 +32,24 @@ "section_break_12", "overdue_payments", "section_break_28", - "column_break_17", "total_interest", - "total_outstanding", "dunning_fee", + "column_break_17", + "dunning_amount", + "section_break_32", + "spacer", + "column_break_33", + "total_outstanding", "grand_total", - "income_account", - "printing_setting_section", + "printing_settings_section", "language", "body_text", "column_break_22", "letter_head", "closing_text", + "accounting_details_section", + "cost_center", + "income_account", "amended_from" ], "fields": [ @@ -60,7 +65,8 @@ "fieldname": "naming_series", "fieldtype": "Select", "label": "Series", - "options": "DUNN-.MM.-.YY.-" + "options": "DUNN-.MM.-.YY.-", + "print_hide": 1 }, { "fetch_from": "sales_invoice.customer_name", @@ -91,8 +97,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Dunning Type", - "options": "Dunning Type", - "reqd": 1 + "options": "Dunning Type" }, { "fieldname": "column_break_8", @@ -116,11 +121,6 @@ "fieldname": "column_break_17", "fieldtype": "Column Break" }, - { - "fieldname": "printing_setting_section", - "fieldtype": "Section Break", - "label": "Printing Setting" - }, { "fieldname": "language", "fieldtype": "Link", @@ -155,14 +155,6 @@ "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", @@ -260,10 +252,12 @@ "options": "Draft\nResolved\nUnresolved\nCancelled" }, { + "description": "For dunning fee and interest", "fieldname": "income_account", "fieldtype": "Link", "label": "Income Account", - "options": "Account" + "options": "Account", + "print_hide": 1 }, { "fetch_from": "sales_invoice.conversion_rate", @@ -310,6 +304,48 @@ "fieldtype": "Link", "label": "Contact Person", "options": "Contact" + }, + { + "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" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "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 } ], "is_submittable": 1, diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.json b/erpnext/accounts/doctype/dunning_type/dunning_type.json index da43664472..ca33ce58a9 100644 --- a/erpnext/accounts/doctype/dunning_type/dunning_type.json +++ b/erpnext/accounts/doctype/dunning_type/dunning_type.json @@ -8,10 +8,7 @@ "engine": "InnoDB", "field_order": [ "dunning_type", - "overdue_interval_section", - "start_day", - "column_break_4", - "end_day", + "is_default", "section_break_6", "dunning_fee", "column_break_8", @@ -45,10 +42,6 @@ "fieldtype": "Table", "options": "Dunning Letter Text" }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, { "fieldname": "section_break_6", "fieldtype": "Section Break" @@ -57,33 +50,25 @@ "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" + }, + { + "default": "0", + "fieldname": "is_default", + "fieldtype": "Check", + "label": "Is Default" } ], "links": [], - "modified": "2020-07-15 17:14:17.835074", + "modified": "2021-09-16 15:00:02.610605", "modified_by": "Administrator", "module": "Accounts", "name": "Dunning Type", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { From 2ee919220a44dc0390162b46e2b539e3cbc991d2 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 16 Sep 2021 16:42:22 +0200 Subject: [PATCH 06/60] feat: rework dunning frontend --- erpnext/accounts/doctype/dunning/dunning.js | 109 +++++++++++--------- 1 file changed, 58 insertions(+), 51 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 2f997ba02e..5158cc2b7f 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -23,14 +23,12 @@ frappe.ui.form.on("Dunning", { } }; }); + + // 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"); @@ -58,25 +56,27 @@ frappe.ui.form.on("Dunning", { frappe.set_route("query-report", "General Ledger"); }, __('View')); } - }, - 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", ""); - } - } - ); + + if(frm.doc.docstatus === 0) { + frm.add_custom_button(__("Fetch Overdue Payments"), function() { + erpnext.utils.map_current_doc({ + method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning", + source_doctype: "Sales Invoice", + 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_fielname: "payment_schedule", + child_columns: ["due_date", "outstanding"] + }); + }); + } }, dunning_type: function (frm) { frm.trigger("get_dunning_letter_text"); @@ -107,42 +107,43 @@ frappe.ui.form.on("Dunning", { }); } }, - 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_amount"); }, dunning_fee: function (frm) { - frm.trigger("calculate_interest_and_amount"); - }, - sales_invoice: function (frm) { - frm.trigger("calculate_overdue_days"); + 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); + calculate_interest_amount: function (frm) { + frm.doc.overdue_payments.forEach((row) => { + const interest_per_year = row.outstanding * frm.doc.rate_of_interest / 100; + const interest_amount = flt((interest_per_year * cint(row.overdue_days)) / 365 || 0, precision("interest_amount")); + frappe.model.set_value(row.doctype, row.name, "interest_amount", interest_amount); + }); + }, + calculate_totals: function (frm) { + const total_interest = frm.doc.overdue_payments + .reduce((prev, cur) => prev + cur.interest_amount, 0); + const total_outstanding = frm.doc.overdue_payments + .reduce((prev, cur) => prev + cur.outstanding, 0); + const dunning_amount = flt(total_interest + frm.doc.dunning_fee, precision('dunning_amount')); + const grand_total = flt(total_outstanding + dunning_amount, precision('grand_total')); + + frm.set_value("total_outstanding", total_outstanding); + frm.set_value("total_interest", total_interest); frm.set_value("dunning_amount", dunning_amount); frm.set_value("grand_total", grand_total); }, @@ -161,3 +162,9 @@ frappe.ui.form.on("Dunning", { }); }, }); + +frappe.ui.form.on("Overdue Payment", { + interest_amount: function(frm, cdt, cdn) { + frm.trigger("calculate_totals"); + } +}); \ No newline at end of file From 2d0dadd9acc370b9559f8d3e70578b6aa29cdf0d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 16 Sep 2021 16:42:51 +0200 Subject: [PATCH 07/60] feat: rework dunning backend --- erpnext/accounts/doctype/dunning/dunning.py | 51 ++++++------- .../doctype/sales_invoice/sales_invoice.py | 75 ++++++++++--------- 2 files changed, 62 insertions(+), 64 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index b4df0a5270..56d49df4be 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -15,25 +15,34 @@ from erpnext.controllers.accounts_controller import AccountsController class Dunning(AccountsController): + def validate(self): - self.validate_overdue_days() - self.validate_amount() + self.validate_overdue_payments() + self.validate_totals() + if not self.income_account: self.income_account = frappe.get_cached_value("Company", self.company, "default_income_account") - def validate_overdue_days(self): - self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0 + def validate_overdue_payments(self): + for row in self.overdue_payments: + row.overdue_days = (getdate(self.posting_date) - getdate(row.due_date)).days or 0 + interest_per_year = flt(row.outstanding) * flt(self.rate_of_interest) / 100 + row.interest_amount = (interest_per_year * cint(row.overdue_days)) / 365 - def validate_amount(self): - amounts = calculate_interest_and_amount( - self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days - ) - if self.interest_amount != amounts.get("interest_amount"): - self.interest_amount = flt(amounts.get("interest_amount"), self.precision("interest_amount")) - if self.dunning_amount != amounts.get("dunning_amount"): - self.dunning_amount = flt(amounts.get("dunning_amount"), self.precision("dunning_amount")) - if self.grand_total != amounts.get("grand_total"): - self.grand_total = flt(amounts.get("grand_total"), self.precision("grand_total")) + def validate_totals(self): + total_outstanding = sum(row.outstanding for row in self.overdue_payments) + total_interest = sum(row.interest_amount for row in self.overdue_payments) + dunning_amount = flt(total_interest) + flt(self.dunning_fee) + grand_total = flt(total_outstanding) + flt(dunning_amount) + + if self.total_outstanding != total_outstanding: + self.total_outstanding = flt(total_outstanding, self.precision('total_outstanding')) + if self.total_interest != total_interest: + self.total_interest = flt(total_interest, self.precision('total_interest')) + if self.dunning_amount != dunning_amount: + self.dunning_amount = flt(dunning_amount, self.precision('dunning_amount')) + if self.grand_total != grand_total: + self.grand_total = flt(grand_total, self.precision('grand_total')) def on_submit(self): self.make_gl_entries() @@ -113,20 +122,6 @@ def resolve_dunning(doc, state): frappe.db.set_value("Dunning", dunning.name, "status", "Resolved") -def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days): - interest_amount = 0 - grand_total = flt(outstanding_amount) + flt(dunning_fee) - if rate_of_interest: - interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100 - interest_amount = (interest_per_year * cint(overdue_days)) / 365 - grand_total += flt(interest_amount) - dunning_amount = flt(interest_amount) + flt(dunning_fee) - return { - "interest_amount": interest_amount, - "grand_total": grand_total, - "dunning_amount": dunning_amount, - } - @frappe.whitelist() def get_dunning_letter_text(dunning_type, doc, language=None): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 2075d57a35..0aa6eab862 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2510,55 +2510,58 @@ def get_mode_of_payment_info(mode_of_payment, company): @frappe.whitelist() -def create_dunning(source_name, target_doc=None): +def create_dunning(source_name, target_doc=None, ignore_permissions=False): from frappe.model.mapper import get_mapped_doc - from erpnext.accounts.doctype.dunning.dunning import ( - calculate_interest_and_amount, - get_dunning_letter_text, - ) + def postprocess_dunning(source, target): + from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text - def set_missing_values(source, target): - target.sales_invoice = source_name - target.outstanding_amount = source.outstanding_amount - overdue_days = (getdate(target.posting_date) - getdate(source.due_date)).days - target.overdue_days = overdue_days - if frappe.db.exists( - "Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]} - ): - dunning_type = frappe.get_doc( - "Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]} - ) + dunning_type = frappe.db.exists('Dunning Type', {'is_default': 1}) + if dunning_type: + dunning_type = frappe.get_doc("Dunning Type", dunning_type) target.dunning_type = dunning_type.name target.rate_of_interest = dunning_type.rate_of_interest target.dunning_fee = dunning_type.dunning_fee - letter_text = get_dunning_letter_text(dunning_type=dunning_type.name, doc=target.as_dict()) - if letter_text: - target.body_text = letter_text.get("body_text") - target.closing_text = letter_text.get("closing_text") - target.language = letter_text.get("language") - amounts = calculate_interest_and_amount( - target.outstanding_amount, - target.rate_of_interest, - target.dunning_fee, - target.overdue_days, + letter_text = get_dunning_letter_text( + dunning_type=dunning_type.name, + doc=target.as_dict(), + language=source.language ) - target.interest_amount = amounts.get("interest_amount") - target.dunning_amount = amounts.get("dunning_amount") - target.grand_total = amounts.get("grand_total") - doclist = get_mapped_doc( - "Sales Invoice", - source_name, - { + if letter_text: + target.body_text = letter_text.get('body_text') + target.closing_text = letter_text.get('closing_text') + target.language = letter_text.get('language') + + def postprocess_overdue_payment(source, target, source_parent): + target.overdue_days = (getdate(nowdate()) - getdate(source.due_date)).days + + return get_mapped_doc( + from_doctype="Sales Invoice", + from_docname=source_name, + table_maps={ "Sales Invoice": { "doctype": "Dunning", + "field_map": { + "customer_address": "customer_address", + "parent": "sales_invoice" + }, + }, + "Payment Schedule": { + "doctype": "Overdue Payment", + "field_map": { + "name": "payment_schedule", + "parent": "sales_invoice" + }, + "condition": lambda doc: doc.outstanding > 0, + "postprocess": postprocess_overdue_payment } }, - target_doc, - set_missing_values, + target_doc=target_doc, + postprocess=postprocess_dunning, + ignore_permissions=ignore_permissions ) - return doclist + def check_if_return_invoice_linked_with_payment_entry(self): From 4f51dfe4c53d83d53fc80b8929bf2c35713111df Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 16 Sep 2021 17:21:25 +0200 Subject: [PATCH 08/60] refactor: remove unnecessary code --- erpnext/accounts/doctype/dunning/dunning.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 5158cc2b7f..7bc79e78fb 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -71,9 +71,6 @@ frappe.ui.form.on("Dunning", { status: "Overdue", company: frm.doc.company }, - allow_child_item_selection: true, - child_fielname: "payment_schedule", - child_columns: ["due_date", "outstanding"] }); }); } From db47e1b69c5a2e35633c2017555a92271bd3bf76 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 16 Sep 2021 17:21:52 +0200 Subject: [PATCH 09/60] feat: address and contact queries --- erpnext/accounts/doctype/dunning/dunning.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 7bc79e78fb..73ed9c4261 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -24,6 +24,9 @@ frappe.ui.form.on("Dunning", { }; }); + frm.set_query('contact_person', erpnext.queries.contact_query); + frm.set_query('customer_address', erpnext.queries.address_query); + // cannot add rows manually, only via button "Fetch Overdue Payments" frm.set_df_property("overdue_payments", "cannot_add_rows", true); }, From b186f8e9d7b12ef599ba96db275c7733dcc0f504 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 16 Sep 2021 17:22:25 +0200 Subject: [PATCH 10/60] feat: address display --- erpnext/accounts/doctype/dunning/dunning.js | 6 ++++++ erpnext/accounts/doctype/dunning/dunning.json | 21 ++++++++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 73ed9c4261..5777583dee 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -78,6 +78,12 @@ frappe.ui.form.on("Dunning", { }); } }, + 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"); }, diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index a0e3c150fd..85c73a8a74 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -21,10 +21,11 @@ "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", "column_break_8", @@ -206,15 +207,11 @@ "options": "Phone", "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 }, { @@ -346,6 +343,16 @@ "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" } ], "is_submittable": 1, From b07620aacf8fe4e003e8f78bc445b161f27cefc0 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 16 Sep 2021 17:22:46 +0200 Subject: [PATCH 11/60] feat: child table triggers calculation of totals --- erpnext/accounts/doctype/dunning/dunning.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 5777583dee..ede5cf44b1 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -122,6 +122,12 @@ frappe.ui.form.on("Dunning", { dunning_fee: function (frm) { frm.trigger("calculate_totals"); }, + overdue_payments_add: function(frm) { + frm.trigger("calculate_totals"); + }, + overdue_payments_remove: function (frm) { + frm.trigger("calculate_totals"); + }, calculate_overdue_days: function (frm) { frm.doc.overdue_payments.forEach((row) => { if (frm.doc.posting_date && row.due_date) { From 9016baddcaaeb89c7bc4246bd28aef2a40a5b819 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Sep 2021 11:43:09 +0200 Subject: [PATCH 12/60] feat: company address query + style --- erpnext/accounts/doctype/dunning/dunning.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index ede5cf44b1..2ddfe80e61 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -24,8 +24,9 @@ frappe.ui.form.on("Dunning", { }; }); - frm.set_query('contact_person', erpnext.queries.contact_query); - frm.set_query('customer_address', erpnext.queries.address_query); + 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); @@ -48,7 +49,7 @@ frappe.ui.form.on("Dunning", { } if(frm.doc.docstatus > 0) { - frm.add_custom_button(__('Ledger'), function() { + frm.add_custom_button(__("Ledger"), function() { frappe.route_options = { "voucher_no": frm.doc.name, "from_date": frm.doc.posting_date, @@ -57,7 +58,7 @@ frappe.ui.form.on("Dunning", { "show_cancelled_entries": frm.doc.docstatus === 2 }; frappe.set_route("query-report", "General Ledger"); - }, __('View')); + }, __("View")); } if(frm.doc.docstatus === 0) { @@ -151,8 +152,8 @@ frappe.ui.form.on("Dunning", { .reduce((prev, cur) => prev + cur.interest_amount, 0); const total_outstanding = frm.doc.overdue_payments .reduce((prev, cur) => prev + cur.outstanding, 0); - const dunning_amount = flt(total_interest + frm.doc.dunning_fee, precision('dunning_amount')); - const grand_total = flt(total_outstanding + dunning_amount, precision('grand_total')); + const dunning_amount = flt(total_interest + frm.doc.dunning_fee, precision("dunning_amount")); + const grand_total = flt(total_outstanding + dunning_amount, precision("grand_total")); frm.set_value("total_outstanding", total_outstanding); frm.set_value("total_interest", total_interest); From 938f7d2266bde259c40c889014a0139c31e3138f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Sep 2021 11:58:24 +0200 Subject: [PATCH 13/60] reafctor: validate instead of postprocess --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 0aa6eab862..f240fe9b19 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2533,8 +2533,7 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): target.closing_text = letter_text.get('closing_text') target.language = letter_text.get('language') - def postprocess_overdue_payment(source, target, source_parent): - target.overdue_days = (getdate(nowdate()) - getdate(source.due_date)).days + target.validate() return get_mapped_doc( from_doctype="Sales Invoice", @@ -2553,8 +2552,7 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): "name": "payment_schedule", "parent": "sales_invoice" }, - "condition": lambda doc: doc.outstanding > 0, - "postprocess": postprocess_overdue_payment + "condition": lambda doc: doc.outstanding > 0 } }, target_doc=target_doc, From 043066a2c8deabd1df2525e86f6558b6566528dd Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Sep 2021 12:03:02 +0200 Subject: [PATCH 14/60] style: use double quotes --- erpnext/accounts/doctype/dunning/dunning.py | 10 +++++----- .../accounts/doctype/sales_invoice/sales_invoice.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 56d49df4be..79de03cc71 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -21,7 +21,7 @@ class Dunning(AccountsController): self.validate_totals() if not self.income_account: - self.income_account = frappe.get_cached_value("Company", self.company, "default_income_account") + self.income_account = frappe.db.get_value("Company", self.company, "default_income_account") def validate_overdue_payments(self): for row in self.overdue_payments: @@ -36,13 +36,13 @@ class Dunning(AccountsController): grand_total = flt(total_outstanding) + flt(dunning_amount) if self.total_outstanding != total_outstanding: - self.total_outstanding = flt(total_outstanding, self.precision('total_outstanding')) + self.total_outstanding = flt(total_outstanding, self.precision("total_outstanding")) if self.total_interest != total_interest: - self.total_interest = flt(total_interest, self.precision('total_interest')) + self.total_interest = flt(total_interest, self.precision("total_interest")) if self.dunning_amount != dunning_amount: - self.dunning_amount = flt(dunning_amount, self.precision('dunning_amount')) + self.dunning_amount = flt(dunning_amount, self.precision("dunning_amount")) if self.grand_total != grand_total: - self.grand_total = flt(grand_total, self.precision('grand_total')) + self.grand_total = flt(grand_total, self.precision("grand_total")) def on_submit(self): self.make_gl_entries() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index f240fe9b19..05f8638794 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2516,7 +2516,7 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): def postprocess_dunning(source, target): from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text - dunning_type = frappe.db.exists('Dunning Type', {'is_default': 1}) + dunning_type = frappe.db.exists("Dunning Type", {"is_default": 1}) if dunning_type: dunning_type = frappe.get_doc("Dunning Type", dunning_type) target.dunning_type = dunning_type.name @@ -2529,9 +2529,9 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): ) 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') + target.body_text = letter_text.get("body_text") + target.closing_text = letter_text.get("closing_text") + target.language = letter_text.get("language") target.validate() From 676ed6b881fcd02f10cb7e207e46119189ed55ca Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Sep 2021 12:16:23 +0200 Subject: [PATCH 15/60] feat: hide fields in print --- erpnext/accounts/doctype/dunning/dunning.json | 18 ++++++++++++------ .../overdue_payment/overdue_payment.json | 5 ++++- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index 85c73a8a74..2f880d115d 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -126,13 +126,15 @@ "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", @@ -294,13 +296,15 @@ "fieldname": "customer_address", "fieldtype": "Link", "label": "Customer Address", - "options": "Address" + "options": "Address", + "print_hide": 1 }, { "fieldname": "contact_person", "fieldtype": "Link", "label": "Contact Person", - "options": "Contact" + "options": "Contact", + "print_hide": 1 }, { "fieldname": "dunning_amount", @@ -319,7 +323,8 @@ "fieldname": "cost_center", "fieldtype": "Link", "label": "Cost Center", - "options": "Cost Center" + "options": "Cost Center", + "print_hide": 1 }, { "collapsible": 1, @@ -352,7 +357,8 @@ "fieldname": "company_address", "fieldtype": "Link", "label": "Company Address", - "options": "Address" + "options": "Address", + "print_hide": 1 } ], "is_submittable": 1, diff --git a/erpnext/accounts/doctype/overdue_payment/overdue_payment.json b/erpnext/accounts/doctype/overdue_payment/overdue_payment.json index e5253bd12f..bc351d835a 100644 --- a/erpnext/accounts/doctype/overdue_payment/overdue_payment.json +++ b/erpnext/accounts/doctype/overdue_payment/overdue_payment.json @@ -113,6 +113,7 @@ "fieldname": "discounted_amount", "fieldtype": "Currency", "label": "Discounted Amount", + "print_hide": 1, "read_only": 1 }, { @@ -124,6 +125,7 @@ "fieldtype": "Currency", "label": "Payment Amount (Company Currency)", "options": "Company:company:default_currency", + "print_hide": 1, "read_only": 1 }, { @@ -138,6 +140,7 @@ "fieldname": "payment_schedule", "fieldtype": "Data", "label": "Payment Schedule", + "print_hide": 1, "read_only": 1 }, { @@ -158,7 +161,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-15 19:04:54.082880", + "modified": "2021-09-17 12:10:42.278923", "modified_by": "Administrator", "module": "Accounts", "name": "Overdue Payment", From f143fe7dccc940c6d5b73d4eb3369089d4bfc1dc Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Sep 2021 20:34:09 +0200 Subject: [PATCH 16/60] refactor: tests --- .../accounts/doctype/dunning/test_dunning.py | 133 +++++++----------- 1 file changed, 50 insertions(+), 83 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index e1fd1e984f..956b1cfdbe 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -6,7 +6,7 @@ import unittest import frappe from frappe.utils import add_days, nowdate, today -from erpnext.accounts.doctype.dunning.dunning import calculate_interest_and_amount +from erpnext.accounts.doctype.sales_invoice.sales_invoice import create_dunning as create_dunning_from_sales_invoice 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, @@ -19,34 +19,34 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import ( class TestDunning(unittest.TestCase): @classmethod def setUpClass(self): - create_dunning_type() - create_dunning_type_with_zero_interest_rate() + 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): unlink_payment_on_cancel_of_invoice(0) - 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_first_dunning(self): + dunning = create_first_dunning() - 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_second_dunning(self): + dunning = create_second_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_gl_entries(self): - dunning = create_dunning() + dunning = create_second_dunning() dunning.submit() gl_entries = frappe.db.sql( """select account, debit, credit @@ -56,16 +56,17 @@ class TestDunning(unittest.TestCase): 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]] - ) + expected_values = dict((d[0], d) for d in [ + ['Debtors - _TC', 10.41, 0.0], + ['Sales - _TC', 0.0, 10.41] + ]) 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_payment_entry(self): - dunning = create_dunning() + dunning = create_second_dunning() dunning.submit() pe = get_payment_entry("Dunning", dunning.name) pe.reference_no = "1" @@ -80,83 +81,49 @@ class TestDunning(unittest.TestCase): self.assertEqual(si_doc.outstanding_amount, 0) -def create_dunning(): +def create_first_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 + posting_date=posting_date, due_date=due_date, qty=1, rate=100) + dunning = create_dunning_from_sales_invoice(sales_invoice.name) dunning.save() + return dunning -def create_dunning_with_zero_interest_rate(): +def create_second_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 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" + posting_date=posting_date, due_date=due_date, qty=1, rate=100) + dunning = create_dunning_from_sales_invoice(sales_invoice.name) + dunning_type = frappe.get_doc("Dunning Type", "Second Notice") + + dunning.dunning_type = dunning_type.name dunning.rate_of_interest = dunning_type.rate_of_interest dunning.dunning_fee = dunning_type.dunning_fee dunning.save() + return dunning -def create_dunning_type(): +def create_dunning_type(title, fee, interest, is_default): + existing = frappe.db.exists("Dunning Type", title) + if existing: + return frappe.get_doc("Dunning Type", existing) + 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.is_default = is_default + dunning_type.dunning_fee = fee + dunning_type.rate_of_interest = interest dunning_type.append( - "dunning_letter_text", - { + "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() - - -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.", - }, + "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() + return dunning_type From 24e7a218392111ca3bbed85412adf935f9cd4496 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Sep 2021 20:34:28 +0200 Subject: [PATCH 17/60] refactor: remove redndant argument --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 05f8638794..f8f7c3666a 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2555,7 +2555,6 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): "condition": lambda doc: doc.outstanding > 0 } }, - target_doc=target_doc, postprocess=postprocess_dunning, ignore_permissions=ignore_permissions ) From df840cca75d3350c54230c6d67734f9b9e707073 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 23 Sep 2021 20:04:34 +0200 Subject: [PATCH 18/60] refactor: validate totals --- erpnext/accounts/doctype/dunning/dunning.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 79de03cc71..e4b502a166 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -30,19 +30,10 @@ class Dunning(AccountsController): row.interest_amount = (interest_per_year * cint(row.overdue_days)) / 365 def validate_totals(self): - total_outstanding = sum(row.outstanding for row in self.overdue_payments) - total_interest = sum(row.interest_amount for row in self.overdue_payments) - dunning_amount = flt(total_interest) + flt(self.dunning_fee) - grand_total = flt(total_outstanding) + flt(dunning_amount) - - if self.total_outstanding != total_outstanding: - self.total_outstanding = flt(total_outstanding, self.precision("total_outstanding")) - if self.total_interest != total_interest: - self.total_interest = flt(total_interest, self.precision("total_interest")) - if self.dunning_amount != dunning_amount: - self.dunning_amount = flt(dunning_amount, self.precision("dunning_amount")) - if self.grand_total != grand_total: - self.grand_total = flt(grand_total, self.precision("grand_total")) + 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.grand_total = self.total_outstanding + self.dunning_amount def on_submit(self): self.make_gl_entries() From be5fb94837a8fb894c2e649d18d33c4674545474 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 23 Sep 2021 20:09:53 +0200 Subject: [PATCH 19/60] feat: currency section , debit_to, base_dunning_amount --- erpnext/accounts/doctype/dunning/dunning.js | 117 +++++++++++++++++- erpnext/accounts/doctype/dunning/dunning.json | 65 +++++++--- 2 files changed, 156 insertions(+), 26 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 2ddfe80e61..45fcc4356d 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -23,7 +23,15 @@ frappe.ui.form.on("Dunning", { } }; }); - + frm.set_query("debit_to", () => { + return { + filters: { + "account_type": "Receivable", + "is_group": 0, + "company": frm.doc.company + } + } + }); 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); @@ -43,13 +51,13 @@ frappe.ui.form.on("Dunning", { __("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() { + 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, @@ -61,8 +69,8 @@ frappe.ui.form.on("Dunning", { }, __("View")); } - if(frm.doc.docstatus === 0) { - frm.add_custom_button(__("Fetch Overdue Payments"), function() { + if (frm.doc.docstatus === 0) { + frm.add_custom_button(__("Fetch Overdue Payments"), function () { erpnext.utils.map_current_doc({ method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning", source_doctype: "Sales Invoice", @@ -78,6 +86,103 @@ frappe.ui.form.on("Dunning", { }); }); } + + 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)); + }, + // 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) { + if (r.message) { + frm.set_value("company_address", r.message) + } + else { + frm.set_value("company_address", "") + } + } + }); + + if (frm.fields_dict.currency) { + var 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); + } + } + + var 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); + } + } + } + frm.trigger("set_debit_to"); + }, + set_debit_to: function(frm) { + if (frm.doc.customer && frm.doc.company) { + return frappe.call({ + method: "erpnext.accounts.party.get_party_account", + args: { + company: frm.doc.company, + party_type: "Customer", + party: frm.doc.customer, + currency: erpnext.get_currency(frm.doc.company) + }, + callback: function (r) { + if (!r.exc && r.message) { + frm.set_value("debit_to", r.message); + } + } + }); + } + }, + customer: function (frm) { + frm.trigger("set_debit_to"); + }, + currency: function (frm) { + // this.set_dynamic_labels(); + var 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: transaction_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"); + } + }, + 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"); diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index 2f880d115d..1dd05b77fa 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -9,13 +9,15 @@ "naming_series", "customer", "customer_name", - "currency", - "conversion_rate", "column_break_3", "company", "posting_date", "posting_time", "status", + "section_break_9", + "currency", + "column_break_11", + "conversion_rate", "address_and_contact_section", "customer_address", "address_display", @@ -37,6 +39,7 @@ "dunning_fee", "column_break_17", "dunning_amount", + "base_dunning_amount", "section_break_32", "spacer", "column_break_33", @@ -51,6 +54,7 @@ "accounting_details_section", "cost_center", "income_account", + "debit_to", "amended_from" ], "fields": [ @@ -140,15 +144,6 @@ "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", @@ -248,7 +243,8 @@ "fieldtype": "Select", "in_standard_filter": 1, "label": "Status", - "options": "Draft\nResolved\nUnresolved\nCancelled" + "options": "Draft\nResolved\nUnresolved\nCancelled", + "read_only": 1 }, { "description": "For dunning fee and interest", @@ -258,14 +254,6 @@ "options": "Account", "print_hide": 1 }, - { - "fetch_from": "sales_invoice.conversion_rate", - "fieldname": "conversion_rate", - "fieldtype": "Float", - "hidden": 1, - "label": "Conversion Rate", - "read_only": 1 - }, { "fieldname": "overdue_payments", "fieldtype": "Table", @@ -307,6 +295,7 @@ "print_hide": 1 }, { + "default": "0", "fieldname": "dunning_amount", "fieldtype": "Currency", "label": "Dunning Amount", @@ -359,6 +348,42 @@ "label": "Company Address", "options": "Address", "print_hide": 1 + }, + { + "fieldname": "debit_to", + "fieldtype": "Link", + "label": "Debit To", + "options": "Account", + "print_hide": 1, + "reqd": 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", + "label": "Conversion Rate" + }, + { + "default": "0", + "fieldname": "base_dunning_amount", + "fieldtype": "Currency", + "label": "Dunning Amount (Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 } ], "is_submittable": 1, From bc40f3f425804595c50121aac2f9422dd014d0c1 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 23 Sep 2021 20:13:52 +0200 Subject: [PATCH 20/60] refactor: rename interest_amount to interest, dunning_level --- erpnext/accounts/doctype/dunning/dunning.js | 20 ++++----- erpnext/accounts/doctype/dunning/dunning.py | 5 ++- .../overdue_payment/overdue_payment.json | 44 +++++++++---------- 3 files changed, 33 insertions(+), 36 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 45fcc4356d..98462b89db 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -200,7 +200,7 @@ frappe.ui.form.on("Dunning", { 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, @@ -223,12 +223,12 @@ frappe.ui.form.on("Dunning", { frm.trigger("calculate_overdue_days"); }, rate_of_interest: function (frm) { - frm.trigger("calculate_interest_amount"); + frm.trigger("calculate_interest"); }, dunning_fee: function (frm) { frm.trigger("calculate_totals"); }, - overdue_payments_add: function(frm) { + overdue_payments_add: function (frm) { frm.trigger("calculate_totals"); }, overdue_payments_remove: function (frm) { @@ -245,16 +245,16 @@ frappe.ui.form.on("Dunning", { } }); }, - calculate_interest_amount: function (frm) { + calculate_interest: function (frm) { frm.doc.overdue_payments.forEach((row) => { - const interest_per_year = row.outstanding * frm.doc.rate_of_interest / 100; - const interest_amount = flt((interest_per_year * cint(row.overdue_days)) / 365 || 0, precision("interest_amount")); - frappe.model.set_value(row.doctype, row.name, "interest_amount", interest_amount); + const interest_per_day = frm.doc.rate_of_interest / 100 / 365; + const interest = flt((interest_per_day * row.outstanding * cint(row.overdue_days)) / 365 || 0, precision("interest")); + 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_amount, 0); + .reduce((prev, cur) => prev + cur.interest, 0); const total_outstanding = frm.doc.overdue_payments .reduce((prev, cur) => prev + cur.outstanding, 0); const dunning_amount = flt(total_interest + frm.doc.dunning_fee, precision("dunning_amount")); @@ -268,7 +268,7 @@ frappe.ui.form.on("Dunning", { 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, @@ -282,7 +282,7 @@ frappe.ui.form.on("Dunning", { }); frappe.ui.form.on("Overdue Payment", { - interest_amount: function(frm, cdt, cdn) { + interest: function (frm, cdt, cdn) { frm.trigger("calculate_totals"); } }); \ No newline at end of file diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index e4b502a166..f1283ae06f 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -24,10 +24,11 @@ class Dunning(AccountsController): self.income_account = frappe.db.get_value("Company", self.company, "default_income_account") 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 - interest_per_year = flt(row.outstanding) * flt(self.rate_of_interest) / 100 - row.interest_amount = (interest_per_year * cint(row.overdue_days)) / 365 + 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) diff --git a/erpnext/accounts/doctype/overdue_payment/overdue_payment.json b/erpnext/accounts/doctype/overdue_payment/overdue_payment.json index bc351d835a..99e16469d0 100644 --- a/erpnext/accounts/doctype/overdue_payment/overdue_payment.json +++ b/erpnext/accounts/doctype/overdue_payment/overdue_payment.json @@ -7,6 +7,7 @@ "field_order": [ "sales_invoice", "payment_schedule", + "dunning_level", "payment_term", "section_break_15", "description", @@ -16,21 +17,18 @@ "mode_of_payment", "column_break_5", "invoice_portion", - "section_break_9", + "section_break_16", "payment_amount", "outstanding", "paid_amount", "discounted_amount", - "column_break_3", - "base_payment_amount", - "interest_amount" + "interest" ], "fields": [ { "columns": 2, "fieldname": "payment_term", "fieldtype": "Link", - "in_list_view": 1, "label": "Payment Term", "options": "Payment Term", "print_hide": 1, @@ -79,10 +77,6 @@ "label": "Invoice Portion", "read_only": 1 }, - { - "fieldname": "section_break_9", - "fieldtype": "Section Break" - }, { "columns": 2, "fieldname": "payment_amount", @@ -116,24 +110,13 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "column_break_3", - "fieldtype": "Column Break" - }, - { - "fieldname": "base_payment_amount", - "fieldtype": "Currency", - "label": "Payment Amount (Company Currency)", - "options": "Company:company:default_currency", - "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 }, { @@ -151,17 +134,30 @@ "read_only": 1 }, { - "fieldname": "interest_amount", + "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 Amount", + "label": "Interest", + "options": "currency", "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-17 12:10:42.278923", + "modified": "2021-09-23 13:48:27.898830", "modified_by": "Administrator", "module": "Accounts", "name": "Overdue Payment", From 3895c03ba9305e02806272c7793430559d1d699f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 23 Sep 2021 20:14:45 +0200 Subject: [PATCH 21/60] feat: change make_gl_entries to work with new data structure --- erpnext/accounts/doctype/dunning/dunning.py | 62 ++++++--------------- 1 file changed, 18 insertions(+), 44 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index f1283ae06f..5194090743 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -5,11 +5,8 @@ import json import frappe -from frappe.utils import cint, flt, getdate +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 @@ -47,57 +44,34 @@ class Dunning(AccountsController): 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) + cost_center = self.cost_center or frappe.get_cached_value("Company", self.company, "cost_center") - 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, + make_gl_entries( + [ + self.get_gl_dict({ + "account": self.debit_to, "party_type": "Customer", "party": self.customer, "due_date": self.due_date, "against": self.income_account, - "debit": dunning_in_company_currency, + "debit": self.dunning_amount, "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( - { + "cost_center": cost_center + }), + 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 + "credit": self.dunning_amount, + "cost_center": cost_center, + "credit_in_account_currency": self.dunning_amount + }) + ], + cancel=(self.docstatus == 2), + update_outstanding="No", + merge_entries=False ) From 603117eb6bcd4319fc371f562bec0e96f2fbddbb Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 30 Sep 2021 16:23:18 +0200 Subject: [PATCH 22/60] feat: change print format to reflect doctype changes --- .../accounts/print_format/dunning_letter/dunning_letter.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/print_format/dunning_letter/dunning_letter.json b/erpnext/accounts/print_format/dunning_letter/dunning_letter.json index a7eac70b65..c48e1cf35b 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\": \"
\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"{{doc.customer_name}}
\\n{{doc.address_display}}\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"
\\n
{{_(doc.dunning_type)}}
\\n
{{ doc.name }}
\\n
\"}, {\"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\": \"\\n \\n \\n \\n\\t \\n \\n \\n \\n \\n \\n {%if doc.rate_of_interest > 0%}\\n \\n \\n \\n \\n {% endif %}\\n {%if doc.dunning_fee > 0%}\\n \\n \\n \\n \\n {% endif %}\\n \\n
{{_(\\\"Description\\\")}}{{_(\\\"Amount\\\")}}
\\n {{_(\\\"Outstanding Amount\\\")}}\\n \\n {{doc.get_formatted(\\\"outstanding_amount\\\")}}\\n
\\n {{_(\\\"Interest \\\")}} {{doc.rate_of_interest}}% p.a. ({{doc.overdue_days}} {{_(\\\"days\\\")}})\\n \\n {{doc.get_formatted(\\\"interest_amount\\\")}}\\n
\\n {{_(\\\"Dunning Fee\\\")}}\\n \\n {{doc.get_formatted(\\\"dunning_fee\\\")}}\\n
\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"\\n
\\n\\t\\t
\\n\\t\\t\\t{{_(\\\"Grand Total\\\")}}
\\n\\t\\t
\\n\\t\\t\\t{{doc.get_formatted(\\\"grand_total\\\")}}\\n\\t\\t
\\n
\\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\": \"
\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"{{doc.customer_name}}
\\n{{doc.address_display}}\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"
\\n
{{_(doc.dunning_type)}}
\\n
{{ doc.name }}
\\n
\"}, {\"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", From 16a23d9f0f69ce532ea406dee2b8421b9803c456 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 30 Sep 2021 17:37:35 +0200 Subject: [PATCH 23/60] refactor: dunning --- erpnext/accounts/doctype/dunning/dunning.js | 35 ++++---- erpnext/accounts/doctype/dunning/dunning.py | 80 +++++++++---------- .../doctype/payment_entry/payment_entry.py | 36 +++------ 3 files changed, 65 insertions(+), 86 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 98462b89db..5cee711950 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -56,19 +56,6 @@ frappe.ui.form.on("Dunning", { 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"), function () { erpnext.utils.map_current_doc({ @@ -248,22 +235,29 @@ frappe.ui.form.on("Dunning", { 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.outstanding * cint(row.overdue_days)) / 365 || 0, precision("interest")); + const interest = flt((interest_per_day * row.overdue_days * row.outstanding), precision("interest")); frappe.model.set_value(row.doctype, row.name, "interest", interest); }); }, calculate_totals: function (frm) { + debugger; 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 = flt(total_interest + frm.doc.dunning_fee, precision("dunning_amount")); - const grand_total = flt(total_outstanding + dunning_amount, precision("grand_total")); + 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; - frm.set_value("total_outstanding", total_outstanding); - frm.set_value("total_interest", total_interest); - frm.set_value("dunning_amount", dunning_amount); - frm.set_value("grand_total", grand_total); + 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({ @@ -283,6 +277,7 @@ frappe.ui.form.on("Dunning", { frappe.ui.form.on("Overdue Payment", { interest: function (frm, cdt, cdn) { + debugger; frm.trigger("calculate_totals"); } }); \ No newline at end of file diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 5194090743..ec116f3061 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -1,24 +1,44 @@ # Copyright (c) 2020, 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 +""" +from __future__ import unicode_literals import json import frappe + +from frappe import _ from frappe.utils import getdate -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_same_currency() self.validate_overdue_payments() self.validate_totals() + self.set_dunning_level() - if not self.income_account: - self.income_account = frappe.db.get_value("Company", self.company, "default_income_account") + 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_overdue_payments(self): daily_interest = self.rate_of_interest / 100 / 365 @@ -31,51 +51,25 @@ class Dunning(AccountsController): 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 on_submit(self): - self.make_gl_entries() - - 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 - - cost_center = self.cost_center or frappe.get_cached_value("Company", self.company, "cost_center") - - make_gl_entries( - [ - self.get_gl_dict({ - "account": self.debit_to, - "party_type": "Customer", - "party": self.customer, - "due_date": self.due_date, - "against": self.income_account, - "debit": self.dunning_amount, - "debit_in_account_currency": self.dunning_amount, - "against_voucher": self.name, - "against_voucher_type": "Dunning", - "cost_center": cost_center - }), - self.get_gl_dict({ - "account": self.income_account, - "against": self.customer, - "credit": self.dunning_amount, - "cost_center": cost_center, - "credit_in_account_currency": self.dunning_amount - }) - ], - cancel=(self.docstatus == 2), - update_outstanding="No", - merge_entries=False - ) + 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 + } + ) + row.dunning_level = len(past_dunnings) + 1 def resolve_dunning(doc, state): + """ + Todo: refactor + """ for reference in doc.references: if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0: dunnings = frappe.get_list( diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index b6d3e5a30e..397e998f0b 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1849,30 +1849,20 @@ def get_payment_entry( pe.append("references", reference) else: if dt == "Dunning": - pe.append( - "references", - { + for overdue_payment in doc.overdue_payments: + 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"), - }, - ) + "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("deductions", { + "amount": doc.dunning_amount + }) else: pe.append( "references", From ff7ec977e6d75ff72d629ff3dabf5f6de0b2868f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 5 Oct 2021 18:06:13 +0200 Subject: [PATCH 24/60] feat: more info for payment deductions --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 397e998f0b..5793ecfe9a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1861,7 +1861,10 @@ def get_payment_entry( }) pe.append("deductions", { - "amount": doc.dunning_amount + "account": doc.income_account, + "cost_center": doc.cost_center, + "amount": doc.dunning_amount, + "description": _("Interest and/or dunning fee") }) else: pe.append( From 6b6f4dd017790ef47384c984f5ada4ae7c9634dd Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 5 Oct 2021 18:18:23 +0200 Subject: [PATCH 25/60] refactor: run pre-commit --- erpnext/accounts/doctype/dunning/dunning.py | 1 - erpnext/accounts/doctype/dunning/test_dunning.py | 4 +++- erpnext/accounts/doctype/overdue_payment/overdue_payment.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index ec116f3061..81ec408344 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -16,7 +16,6 @@ from __future__ import unicode_literals import json import frappe - from frappe import _ from frappe.utils import getdate diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index 956b1cfdbe..499a03b591 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -6,11 +6,13 @@ import unittest import frappe from frappe.utils import add_days, nowdate, today -from erpnext.accounts.doctype.sales_invoice.sales_invoice import create_dunning as create_dunning_from_sales_invoice 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, ) diff --git a/erpnext/accounts/doctype/overdue_payment/overdue_payment.py b/erpnext/accounts/doctype/overdue_payment/overdue_payment.py index e3820d74e0..6a543ad467 100644 --- a/erpnext/accounts/doctype/overdue_payment/overdue_payment.py +++ b/erpnext/accounts/doctype/overdue_payment/overdue_payment.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class OverduePayment(Document): pass From 270040303ce490a0156078927168616e1662e8ec Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 5 Oct 2021 18:24:03 +0200 Subject: [PATCH 26/60] refactor: make sider happy --- erpnext/accounts/doctype/dunning/dunning.js | 23 +++++++++------------ 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 5cee711950..e5a5e1f8a4 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -30,7 +30,7 @@ frappe.ui.form.on("Dunning", { "is_group": 0, "company": frm.doc.company } - } + }; }); frm.set_query("contact_person", erpnext.queries.contact_query); frm.set_query("customer_address", erpnext.queries.address_query); @@ -74,7 +74,7 @@ frappe.ui.form.on("Dunning", { }); } - frappe.dynamic_link = { doc: frm.doc, fieldname: 'customer', doctype: 'Customer' } + 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)); }, @@ -87,10 +87,9 @@ frappe.ui.form.on("Dunning", { debounce: 2000, callback: function (r) { if (r.message) { - frm.set_value("company_address", r.message) - } - else { - frm.set_value("company_address", "") + frm.set_value("company_address", r.message); + } else { + frm.set_value("company_address", ""); } } }); @@ -141,11 +140,11 @@ frappe.ui.form.on("Dunning", { // this.set_dynamic_labels(); var 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) { + if (frm.doc.currency && frm.doc.currency !== company_currency) { frappe.call({ method: "erpnext.setup.utils.get_exchange_rate", args: { - transaction_date: transaction_date, + transaction_date: frm.doc.posting_date, from_currency: frm.doc.currency, to_currency: company_currency, args: "for_selling" @@ -154,7 +153,7 @@ frappe.ui.form.on("Dunning", { freeze_message: __("Fetching exchange rates ..."), callback: function(r) { const exchange_rate = flt(r.message); - if(exchange_rate != frm.doc.conversion_rate) { + if (exchange_rate != frm.doc.conversion_rate) { frm.set_value("conversion_rate", exchange_rate); } } @@ -164,7 +163,7 @@ frappe.ui.form.on("Dunning", { } }, conversion_rate: function (frm) { - if(frm.doc.currency === erpnext.get_currency(frm.doc.company)) { + if (frm.doc.currency === erpnext.get_currency(frm.doc.company)) { frm.set_value("conversion_rate", 1.0); } @@ -240,7 +239,6 @@ frappe.ui.form.on("Dunning", { }); }, calculate_totals: function (frm) { - debugger; const total_interest = frm.doc.overdue_payments .reduce((prev, cur) => prev + cur.interest, 0); const total_outstanding = frm.doc.overdue_payments @@ -276,8 +274,7 @@ frappe.ui.form.on("Dunning", { }); frappe.ui.form.on("Overdue Payment", { - interest: function (frm, cdt, cdn) { - debugger; + interest: function (frm) { frm.trigger("calculate_totals"); } }); \ No newline at end of file From ac8b6bba5ce8f8bddfe8094f1bb22b1c028a1d47 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 7 Oct 2021 13:04:09 +0200 Subject: [PATCH 27/60] feat: resolve dunning on payment entry --- erpnext/accounts/doctype/dunning/dunning.py | 26 +++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 81ec408344..941a91df5f 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -67,18 +67,30 @@ class Dunning(AccountsController): def resolve_dunning(doc, state): """ - Todo: refactor + 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, + unresolved_dunnings = frappe.get_all("Dunning", + filters={ + "sales_invoice": reference.reference_name, + "status": ("!=", "Resolved") + }, + pluck="name" ) - for dunning in dunnings: - frappe.db.set_value("Dunning", dunning.name, "status", "Resolved") + for dunning_name in unresolved_dunnings: + resolve = True + dunning = frappe.get_doc("Dunning", dunning_name) + for overdue_payment in dunning.overdue_payments: + outstanding = frappe.get_value("Payment Schedule", overdue_payment.payment_schedule, "outstanding") + if outstanding >= 0: + resolve = False + + if resolve: + dunning.status = "Resolved" + dunning.save() From c142d8995200e8e4d76ca36d1e4409e2d4abdd0d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 7 Oct 2021 13:08:52 +0200 Subject: [PATCH 28/60] tests: remove obsolete test --- .../accounts/doctype/dunning/test_dunning.py | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index 499a03b591..f8acc6c025 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -47,26 +47,6 @@ class TestDunning(unittest.TestCase): self.assertEqual(round(dunning.dunning_amount, 2), 10.41) self.assertEqual(round(dunning.grand_total, 2), 110.41) - def test_gl_entries(self): - dunning = create_second_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', 10.41, 0.0], - ['Sales - _TC', 0.0, 10.41] - ]) - 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_payment_entry(self): dunning = create_second_dunning() dunning.submit() From fd7be5da99a134a8dec426fd25a1e3fc503cf77a Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 7 Oct 2021 13:44:33 +0200 Subject: [PATCH 29/60] feat: remove obsolete "debit_to" field --- erpnext/accounts/doctype/dunning/dunning.js | 32 +------------------ erpnext/accounts/doctype/dunning/dunning.json | 9 ------ 2 files changed, 1 insertion(+), 40 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index e5a5e1f8a4..99b408a7a1 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -23,15 +23,7 @@ frappe.ui.form.on("Dunning", { } }; }); - frm.set_query("debit_to", () => { - return { - filters: { - "account_type": "Receivable", - "is_group": 0, - "company": frm.doc.company - } - }; - }); + 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); @@ -113,28 +105,6 @@ frappe.ui.form.on("Dunning", { } } } - frm.trigger("set_debit_to"); - }, - set_debit_to: function(frm) { - if (frm.doc.customer && frm.doc.company) { - return frappe.call({ - method: "erpnext.accounts.party.get_party_account", - args: { - company: frm.doc.company, - party_type: "Customer", - party: frm.doc.customer, - currency: erpnext.get_currency(frm.doc.company) - }, - callback: function (r) { - if (!r.exc && r.message) { - frm.set_value("debit_to", r.message); - } - } - }); - } - }, - customer: function (frm) { - frm.trigger("set_debit_to"); }, currency: function (frm) { // this.set_dynamic_labels(); diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index 1dd05b77fa..fc2ccc7e5d 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -54,7 +54,6 @@ "accounting_details_section", "cost_center", "income_account", - "debit_to", "amended_from" ], "fields": [ @@ -349,14 +348,6 @@ "options": "Address", "print_hide": 1 }, - { - "fieldname": "debit_to", - "fieldtype": "Link", - "label": "Debit To", - "options": "Account", - "print_hide": 1, - "reqd": 1 - }, { "fieldname": "section_break_9", "fieldtype": "Section Break", From 0990011e743119f251b99714ce546b6a96a24b05 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 7 Oct 2021 19:05:35 +0200 Subject: [PATCH 30/60] feat: add patch for dunning --- erpnext/patches.txt | 1 + .../patches/v14_0/single_to_multi_dunning.py | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 erpnext/patches/v14_0/single_to_multi_dunning.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 18bd10f45f..03ef5de06e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -339,3 +339,4 @@ execute:frappe.delete_doc('DocType', 'Cash Flow Mapper', ignore_missing=True) execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Template', ignore_missing=True) execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Accounts', ignore_missing=True) erpnext.patches.v14_0.cleanup_workspaces +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 0000000000..40fba041ef --- /dev/null +++ b/erpnext/patches/v14_0/single_to_multi_dunning.py @@ -0,0 +1,46 @@ +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", 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) From 1250e56dd6fb5132385fa4e6c74276b436a02f23 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 12 Oct 2021 17:30:11 +0200 Subject: [PATCH 31/60] feat: add Dunning to Dunning Type's dashboard --- .../accounts/doctype/dunning_type/dunning_type.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.json b/erpnext/accounts/doctype/dunning_type/dunning_type.json index ca33ce58a9..b80a8b6666 100644 --- a/erpnext/accounts/doctype/dunning_type/dunning_type.json +++ b/erpnext/accounts/doctype/dunning_type/dunning_type.json @@ -63,8 +63,14 @@ "label": "Is Default" } ], - "links": [], - "modified": "2021-09-16 15:00:02.610605", + "links": [ + { + "link_doctype": "Dunning", + "link_fieldname": "dunning_type" + } + ], + "migration_hash": "3a2c71ceb1a15469ffe1eca6053656a0", + "modified": "2021-10-12 17:26:48.080519", "modified_by": "Administrator", "module": "Accounts", "name": "Dunning Type", From 24f400b12363e1804ea7e7dacfb522a849ccf247 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 12 Oct 2021 17:30:46 +0200 Subject: [PATCH 32/60] feat: remove Dunning dashboard as there are no incoming links --- .../accounts/doctype/dunning/dunning_dashboard.py | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 erpnext/accounts/doctype/dunning/dunning_dashboard.py diff --git a/erpnext/accounts/doctype/dunning/dunning_dashboard.py b/erpnext/accounts/doctype/dunning/dunning_dashboard.py deleted file mode 100644 index d1d4031410..0000000000 --- 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"]}], - } From c17ccb455d1507351ff89cf5d410a23d74901e27 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 12 Oct 2021 17:48:54 +0200 Subject: [PATCH 33/60] refactor: run pre-commit --- erpnext/patches/v14_0/single_to_multi_dunning.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v14_0/single_to_multi_dunning.py b/erpnext/patches/v14_0/single_to_multi_dunning.py index 40fba041ef..af83ef7096 100644 --- a/erpnext/patches/v14_0/single_to_multi_dunning.py +++ b/erpnext/patches/v14_0/single_to_multi_dunning.py @@ -1,6 +1,8 @@ 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") @@ -39,7 +41,7 @@ def execute(): dunning.save() if dunning.status != "Resolved": - # With the new logic, dunning amount gets recorded as additional income + # 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. From 9eeaac0c3efa480b1d45651f905756eb8a053b69 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 12 Oct 2021 18:35:02 +0200 Subject: [PATCH 34/60] feat: remove dunning as possible reference from payment entry --- .../doctype/payment_entry/payment_entry.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 5793ecfe9a..5da89a39db 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -386,7 +386,7 @@ class PaymentEntry(AccountsController): def get_valid_reference_doctypes(self): if self.party_type == "Customer": - return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning") + return ("Sales Order", "Sales Invoice", "Journal Entry") elif self.party_type == "Supplier": return ("Purchase Order", "Purchase Invoice", "Journal Entry") elif self.party_type == "Shareholder": @@ -1693,11 +1693,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre ref_doc.company ) - if reference_doctype == "Dunning": - total_amount = outstanding_amount = ref_doc.get("dunning_amount") - exchange_rate = 1 - - elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1: + if reference_doctype == "Journal Entry" and ref_doc.docstatus == 1: total_amount = ref_doc.get("total_amount") if ref_doc.multi_currency: exchange_rate = get_exchange_rate( @@ -1930,7 +1926,7 @@ def get_bank_cash_account(doc, bank_account): def set_party_type(dt): - if dt in ("Sales Invoice", "Sales Order", "Dunning"): + if dt in ("Sales Invoice", "Sales Order"): party_type = "Customer" elif dt in ("Purchase Invoice", "Purchase Order"): party_type = "Supplier" @@ -1957,7 +1953,7 @@ def set_party_account_currency(dt, party_account, doc): def set_payment_type(dt, doc): if ( - dt == "Sales Order" or (dt in ("Sales Invoice", "Dunning") 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): payment_type = "Receive" else: @@ -1975,9 +1971,6 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre else: grand_total = doc.rounded_total or 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.get("base_grand_total")) From 8652331d1c8c9a5f3d3d923b9be1b9e8ea1d5afe Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 21 Oct 2021 19:10:13 +0200 Subject: [PATCH 35/60] Revert "feat: remove dunning as possible reference from payment entry" This reverts commit b774d8d0e3c1e5a53b3422591b3f2d52ca959645. --- .../doctype/payment_entry/payment_entry.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 5da89a39db..5793ecfe9a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -386,7 +386,7 @@ class PaymentEntry(AccountsController): def get_valid_reference_doctypes(self): if self.party_type == "Customer": - return ("Sales Order", "Sales Invoice", "Journal Entry") + return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning") elif self.party_type == "Supplier": return ("Purchase Order", "Purchase Invoice", "Journal Entry") elif self.party_type == "Shareholder": @@ -1693,7 +1693,11 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre ref_doc.company ) - if reference_doctype == "Journal Entry" and ref_doc.docstatus == 1: + if reference_doctype == "Dunning": + total_amount = outstanding_amount = ref_doc.get("dunning_amount") + exchange_rate = 1 + + elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1: total_amount = ref_doc.get("total_amount") if ref_doc.multi_currency: exchange_rate = get_exchange_rate( @@ -1926,7 +1930,7 @@ def get_bank_cash_account(doc, bank_account): def set_party_type(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" @@ -1953,7 +1957,7 @@ def set_party_account_currency(dt, party_account, doc): def set_payment_type(dt, doc): if ( - dt == "Sales Order" or (dt == "Sales Invoice" and doc.outstanding_amount > 0) + dt == "Sales Order" or (dt in ("Sales Invoice", "Dunning") and doc.outstanding_amount > 0) ) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0): payment_type = "Receive" else: @@ -1971,6 +1975,9 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre else: grand_total = doc.rounded_total or 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.get("base_grand_total")) From e37f98267bb4691ef108fa81546335e741f56639 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 21 Oct 2021 20:53:26 +0200 Subject: [PATCH 36/60] fix: resolve dunning --- erpnext/accounts/doctype/dunning/dunning.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 941a91df5f..0a55ff5f5f 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -84,8 +84,9 @@ def resolve_dunning(doc, state): resolve = True dunning = frappe.get_doc("Dunning", dunning_name) for overdue_payment in dunning.overdue_payments: - outstanding = frappe.get_value("Payment Schedule", overdue_payment.payment_schedule, "outstanding") - if outstanding >= 0: + 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") + if outstanding_ps > 0 and outstanding_inv > 0: resolve = False if resolve: From 84459c719662e0cd04255f5d8273e65eb6c6bb5b Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 21 Oct 2021 20:55:22 +0200 Subject: [PATCH 37/60] fix: create payment entry --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 5793ecfe9a..090308f6fd 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1863,7 +1863,7 @@ def get_payment_entry( pe.append("deductions", { "account": doc.income_account, "cost_center": doc.cost_center, - "amount": doc.dunning_amount, + "amount": -1 * doc.dunning_amount, "description": _("Interest and/or dunning fee") }) else: @@ -1957,8 +1957,8 @@ def set_party_account_currency(dt, party_account, doc): 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" From d55c59f2985e8ef5dbaca91fc67e89af1112efe9 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 21 Oct 2021 20:57:23 +0200 Subject: [PATCH 38/60] test: make failing tests work --- .../accounts/doctype/dunning/test_dunning.py | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index f8acc6c025..b114fcec39 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -53,38 +53,43 @@ class TestDunning(unittest.TestCase): 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 create_first_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, qty=1, rate=100) + posting_date=posting_date, qty=1, rate=100 + ) dunning = create_dunning_from_sales_invoice(sales_invoice.name) + dunning.income_account = "Interest Income Account - _TC" dunning.save() return dunning def create_second_dunning(): - posting_date = add_days(today(), -20) - due_date = add_days(today(), -15) + posting_date = add_days(today(), -15) sales_invoice = create_sales_invoice_against_cost_center( - posting_date=posting_date, due_date=due_date, qty=1, rate=100) + posting_date=posting_date, qty=1, rate=100 + ) dunning = create_dunning_from_sales_invoice(sales_invoice.name) dunning_type = frappe.get_doc("Dunning Type", "Second Notice") 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 = "Interest Income Account - _TC" dunning.save() return dunning @@ -101,11 +106,12 @@ def create_dunning_type(title, fee, interest, is_default): dunning_type.dunning_fee = fee dunning_type.rate_of_interest = interest dunning_type.append( - "dunning_letter_text", { + "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." - } + "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.", + }, ) dunning_type.save() return dunning_type From 0a06241e7c005f9a595f5a93b5f69dbe58cc54ab Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 22 Oct 2021 12:05:45 +0200 Subject: [PATCH 39/60] test: refactor, fix missing income account --- .../accounts/doctype/dunning/test_dunning.py | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index b114fcec39..4048f2a846 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -30,7 +30,7 @@ class TestDunning(unittest.TestCase): unlink_payment_on_cancel_of_invoice(0) def test_first_dunning(self): - dunning = create_first_dunning() + dunning = create_dunning(overdue_days=20) self.assertEqual(round(dunning.total_outstanding, 2), 100.00) self.assertEqual(round(dunning.total_interest, 2), 0.00) @@ -39,7 +39,7 @@ class TestDunning(unittest.TestCase): self.assertEqual(round(dunning.grand_total, 2), 100.00) def test_second_dunning(self): - dunning = create_second_dunning() + dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice") self.assertEqual(round(dunning.total_outstanding, 2), 100.00) self.assertEqual(round(dunning.total_interest, 2), 0.41) @@ -48,7 +48,7 @@ class TestDunning(unittest.TestCase): self.assertEqual(round(dunning.grand_total, 2), 110.41) def test_payment_entry(self): - dunning = create_second_dunning() + dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice") dunning.submit() pe = get_payment_entry("Dunning", dunning.name) pe.reference_no = "1" @@ -66,30 +66,20 @@ class TestDunning(unittest.TestCase): self.assertEqual(dunning.status, "Resolved") -def create_first_dunning(): - posting_date = add_days(today(), -20) +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, qty=1, rate=100 ) dunning = create_dunning_from_sales_invoice(sales_invoice.name) - dunning.income_account = "Interest Income Account - _TC" - dunning.save() - return dunning + 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 - -def create_second_dunning(): - posting_date = add_days(today(), -15) - sales_invoice = create_sales_invoice_against_cost_center( - posting_date=posting_date, qty=1, rate=100 - ) - dunning = create_dunning_from_sales_invoice(sales_invoice.name) - dunning_type = frappe.get_doc("Dunning Type", "Second Notice") - - 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 = "Interest Income Account - _TC" + dunning.income_account = get_income_account(dunning.company) dunning.save() return dunning @@ -115,3 +105,16 @@ def create_dunning_type(title, fee, interest, is_default): ) dunning_type.save() return dunning_type + + +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] From 8bfe8657596168d9ebf921bbbd21b7a6d81fa37f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 12 Nov 2021 23:24:08 +0100 Subject: [PATCH 40/60] fix: ignore cancelled dunnings --- erpnext/accounts/doctype/dunning/dunning.py | 3 ++- erpnext/patches/v14_0/single_to_multi_dunning.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 0a55ff5f5f..719f3698dc 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -75,7 +75,8 @@ def resolve_dunning(doc, state): unresolved_dunnings = frappe.get_all("Dunning", filters={ "sales_invoice": reference.reference_name, - "status": ("!=", "Resolved") + "status": ("!=", "Resolved"), + "docstatus": ("!=", 2), }, pluck="name" ) diff --git a/erpnext/patches/v14_0/single_to_multi_dunning.py b/erpnext/patches/v14_0/single_to_multi_dunning.py index af83ef7096..90966aa4cb 100644 --- a/erpnext/patches/v14_0/single_to_multi_dunning.py +++ b/erpnext/patches/v14_0/single_to_multi_dunning.py @@ -7,7 +7,7 @@ def execute(): frappe.reload_doc("accounts", "doctype", "overdue_payment") frappe.reload_doc("accounts", "doctype", "dunning") - all_dunnings = frappe.get_all("Dunning", pluck="name") + 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: From 60b6afb470bd750d6cbac0e04a5f39c312a27765 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 13 Nov 2021 01:39:22 +0100 Subject: [PATCH 41/60] fix: fetch overdue payments --- erpnext/accounts/doctype/dunning/dunning.js | 6 +++++- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 99b408a7a1..c2b91690dc 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -49,10 +49,11 @@ frappe.ui.form.on("Dunning", { } if (frm.doc.docstatus === 0) { - frm.add_custom_button(__("Fetch Overdue Payments"), function () { + 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, @@ -62,6 +63,9 @@ frappe.ui.form.on("Dunning", { status: "Overdue", company: frm.doc.company }, + allow_child_item_selection: true, + child_fielname: "payment_schedule", + child_columns: ["due_date", "outstanding"], }); }); } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index f8f7c3666a..3cce388e92 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2516,7 +2516,7 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): def postprocess_dunning(source, target): from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text - dunning_type = frappe.db.exists("Dunning Type", {"is_default": 1}) + 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 @@ -2538,6 +2538,7 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): return get_mapped_doc( from_doctype="Sales Invoice", from_docname=source_name, + target_doc=target_doc, table_maps={ "Sales Invoice": { "doctype": "Dunning", From 28dfbdda9375603bf53224c435535784d9e0fe13 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 13 Nov 2021 01:42:06 +0100 Subject: [PATCH 42/60] feat: fetch income account and cost center from dunning type --- erpnext/accounts/doctype/dunning/dunning.js | 8 ++++ erpnext/accounts/doctype/dunning/dunning.json | 10 +++- .../doctype/dunning_type/dunning_type.js | 24 ++++++++-- .../doctype/dunning_type/dunning_type.json | 47 +++++++++++++++++-- .../doctype/dunning_type/dunning_type.py | 6 ++- 5 files changed, 83 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index c2b91690dc..03553f775c 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -23,6 +23,14 @@ frappe.ui.form.on("Dunning", { } }; }); + 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); diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index fc2ccc7e5d..20e843c922 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -2,6 +2,7 @@ "actions": [], "allow_events_in_timeline": 1, "autoname": "naming_series:", + "beta": 1, "creation": "2019-07-05 16:34:31.013238", "doctype": "DocType", "engine": "InnoDB", @@ -52,8 +53,9 @@ "letter_head", "closing_text", "accounting_details_section", - "cost_center", "income_account", + "column_break_48", + "cost_center", "amended_from" ], "fields": [ @@ -247,6 +249,7 @@ }, { "description": "For dunning fee and interest", + "fetch_from": "dunning_type.income_account", "fieldname": "income_account", "fieldtype": "Link", "label": "Income Account", @@ -308,6 +311,7 @@ "label": "Accounting Details" }, { + "fetch_from": "dunning_type.cost_center", "fieldname": "cost_center", "fieldtype": "Link", "label": "Cost Center", @@ -375,6 +379,10 @@ "label": "Dunning Amount (Company Currency)", "options": "Company:company:default_currency", "read_only": 1 + }, + { + "fieldname": "column_break_48", + "fieldtype": "Column Break" } ], "is_submittable": 1, diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.js b/erpnext/accounts/doctype/dunning_type/dunning_type.js index 54156b488d..b2c08c1c7f 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 b80a8b6666..5e39769735 100644 --- a/erpnext/accounts/doctype/dunning_type/dunning_type.json +++ b/erpnext/accounts/doctype/dunning_type/dunning_type.json @@ -1,7 +1,7 @@ { "actions": [], "allow_rename": 1, - "autoname": "field:dunning_type", + "beta": 1, "creation": "2019-12-04 04:59:08.003664", "doctype": "DocType", "editable_grid": 1, @@ -9,12 +9,18 @@ "field_order": [ "dunning_type", "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": [ { @@ -61,6 +67,38 @@ "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": [ @@ -69,12 +107,11 @@ "link_fieldname": "dunning_type" } ], - "migration_hash": "3a2c71ceb1a15469ffe1eca6053656a0", - "modified": "2021-10-12 17:26:48.080519", + "modified": "2021-11-13 00:25:35.659283", "modified_by": "Administrator", "module": "Accounts", "name": "Dunning Type", - "naming_rule": "By fieldname", + "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 1b9bb9c032..b053eb51d6 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 = self.dunning_type + " - " + company_abbr From d790710ae73f3ff9c52141c02f645646daf07f6e Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Mon, 29 Nov 2021 12:11:30 +0100 Subject: [PATCH 43/60] refactor: apply suggestions from code review Co-authored-by: Himanshu --- erpnext/accounts/doctype/dunning/dunning.js | 12 ++++-------- .../accounts/doctype/dunning_type/dunning_type.py | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 03553f775c..8930fcc6cb 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -90,16 +90,12 @@ frappe.ui.form.on("Dunning", { args: { name: frm.doc.company, existing_address: frm.doc.company_address || "" }, debounce: 2000, callback: function (r) { - if (r.message) { - frm.set_value("company_address", r.message); - } else { - frm.set_value("company_address", ""); - } + frm.set_value("company_address", r && r.message || ""); } }); if (frm.fields_dict.currency) { - var company_currency = erpnext.get_currency(frm.doc.company); + const company_currency = erpnext.get_currency(frm.doc.company); if (!frm.doc.currency) { frm.set_value("currency", company_currency); @@ -110,7 +106,7 @@ frappe.ui.form.on("Dunning", { } } - var company_doc = frappe.get_doc(":Company", frm.doc.company); + 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); @@ -120,7 +116,7 @@ frappe.ui.form.on("Dunning", { }, currency: function (frm) { // this.set_dynamic_labels(); - var company_currency = erpnext.get_currency(frm.doc.company); + 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({ diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.py b/erpnext/accounts/doctype/dunning_type/dunning_type.py index b053eb51d6..226e159a3b 100644 --- a/erpnext/accounts/doctype/dunning_type/dunning_type.py +++ b/erpnext/accounts/doctype/dunning_type/dunning_type.py @@ -9,4 +9,4 @@ from frappe.model.document import Document class DunningType(Document): def autoname(self): company_abbr = frappe.get_value("Company", self.company, "abbr") - self.name = self.dunning_type + " - " + company_abbr + self.name = f"{self.dunning_type} - {company_abbr}" From 028d19f32dca25dc8fb111082f82482d082eec18 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 28 Dec 2021 19:31:18 +0100 Subject: [PATCH 44/60] test: link Dunning Type to COmpany --- erpnext/accounts/doctype/dunning/test_dunning.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index 4048f2a846..925b7e5e55 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -92,6 +92,7 @@ def create_dunning_type(title, fee, interest, is_default): dunning_type = frappe.new_doc("Dunning Type") dunning_type.dunning_type = title + dunning_type.company = "_Test Company" dunning_type.is_default = is_default dunning_type.dunning_fee = fee dunning_type.rate_of_interest = interest From fd6d86eefc37d9447fb0c32cc13fa94a75e963ab Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 28 Dec 2021 23:50:05 +0100 Subject: [PATCH 45/60] fix: show "Create Dunning" when any scheduled payment is overdue --- .../accounts/doctype/sales_invoice/sales_invoice.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 8cb29505eb..6b0c2ee76f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -138,8 +138,14 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e cur_frm.events.create_invoice_discounting(cur_frm); }, __('Create')); - if (doc.due_date < frappe.datetime.get_today()) { - cur_frm.add_custom_button(__('Dunning'), function() { + 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) { + cur_frm.add_custom_button(__('Dunning'), function () { cur_frm.events.create_dunning(cur_frm); }, __('Create')); } From 88f67e47862883a1084d137a8a150d9dcf0ad9e7 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 28 Dec 2021 23:51:32 +0100 Subject: [PATCH 46/60] fix: set income account and cost center --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 3cce388e92..d1494b7f7c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2522,6 +2522,8 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): target.dunning_type = dunning_type.name target.rate_of_interest = dunning_type.rate_of_interest target.dunning_fee = dunning_type.dunning_fee + 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(), From ccefe96665b651794fec79ce2ba4251563dd9cfc Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 29 Dec 2021 00:09:52 +0100 Subject: [PATCH 47/60] fix: map only overdue payments --- .../doctype/sales_invoice/sales_invoice.py | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index d1494b7f7c..7b741495ea 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -622,9 +622,7 @@ class SalesInvoice(SellingController): return if not self.account_for_change_amount: - self.account_for_change_amount = frappe.get_cached_value( - "Company", self.company, "default_cash_account" - ) + self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account') from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details @@ -1909,17 +1907,17 @@ def get_bank_cash_account(mode_of_payment, company): @frappe.whitelist() def make_maintenance_schedule(source_name, target_doc=None): - doclist = get_mapped_doc( - "Sales Invoice", - source_name, - { - "Sales Invoice": {"doctype": "Maintenance Schedule", "validation": {"docstatus": ["=", 1]}}, - "Sales Invoice Item": { - "doctype": "Maintenance Schedule Item", - }, + doclist = get_mapped_doc("Sales Invoice", source_name, { + "Sales Invoice": { + "doctype": "Maintenance Schedule", + "validation": { + "docstatus": ["=", 1] + } }, - target_doc, - ) + "Sales Invoice Item": { + "doctype": "Maintenance Schedule Item", + }, + }, target_doc) return doclist @@ -2555,7 +2553,7 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): "name": "payment_schedule", "parent": "sales_invoice" }, - "condition": lambda doc: doc.outstanding > 0 + "condition": lambda doc: doc.outstanding > 0 and getdate(doc.due_date) < getdate(), } }, postprocess=postprocess_dunning, From 4911c3b5b74f10115237528512154f5fd1d96053 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 29 Dec 2021 00:13:23 +0100 Subject: [PATCH 48/60] fix: precision for interst --- erpnext/accounts/doctype/dunning/dunning.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 8930fcc6cb..a99b44ff1e 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -212,7 +212,7 @@ frappe.ui.form.on("Dunning", { 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")); + const interest = flt((interest_per_day * row.overdue_days * row.outstanding), precision("interest", row)); frappe.model.set_value(row.doctype, row.name, "interest", interest); }); }, From 04aaadcb3951453c488a662040b58e79e98e840e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 3 Jan 2022 11:27:47 +0100 Subject: [PATCH 49/60] style: sider issues --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 7b741495ea..b2cd4a6d08 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -622,7 +622,7 @@ class SalesInvoice(SellingController): return if not self.account_for_change_amount: - self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account') + self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account') from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details @@ -1907,7 +1907,7 @@ def get_bank_cash_account(mode_of_payment, company): @frappe.whitelist() def make_maintenance_schedule(source_name, target_doc=None): - doclist = get_mapped_doc("Sales Invoice", source_name, { + doclist = get_mapped_doc("Sales Invoice", source_name, { "Sales Invoice": { "doctype": "Maintenance Schedule", "validation": { From 315df7b2cf6261fb4656a8634026937d0e1007d8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 3 Jan 2022 12:46:46 +0100 Subject: [PATCH 50/60] test: fix dunning test --- .../accounts/doctype/dunning/test_dunning.py | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index 925b7e5e55..129ca32d3a 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -6,6 +6,7 @@ import unittest import frappe from frappe.utils import add_days, nowdate, today +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, @@ -17,16 +18,19 @@ 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): @classmethod - def setUpClass(self): + def setUpClass(cls): 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() + frappe.db.commit() @classmethod - def tearDownClass(self): + def tearDownClass(cls): unlink_payment_on_cancel_of_invoice(0) def test_first_dunning(self): @@ -39,7 +43,7 @@ class TestDunning(unittest.TestCase): self.assertEqual(round(dunning.grand_total, 2), 100.00) def test_second_dunning(self): - dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice") + dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC") self.assertEqual(round(dunning.total_outstanding, 2), 100.00) self.assertEqual(round(dunning.total_interest, 2), 0.41) @@ -48,7 +52,7 @@ class TestDunning(unittest.TestCase): self.assertEqual(round(dunning.grand_total, 2), 110.41) def test_payment_entry(self): - dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice") + 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" @@ -78,24 +82,25 @@ def create_dunning(overdue_days, dunning_type_name=None): 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 - dunning.income_account = get_income_account(dunning.company) - dunning.save() - - return dunning + return dunning.save() def create_dunning_type(title, fee, interest, is_default): - existing = frappe.db.exists("Dunning Type", title) - if existing: - return frappe.get_doc("Dunning Type", existing) + company = "_Test Company" + if frappe.db.exists("Dunning Type", f"{title} - _TC"): + return dunning_type = frappe.new_doc("Dunning Type") dunning_type.dunning_type = title - dunning_type.company = "_Test Company" + 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", { @@ -104,8 +109,7 @@ def create_dunning_type(title, fee, interest, is_default): "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.", }, ) - dunning_type.save() - return dunning_type + dunning_type.insert() def get_income_account(company): From 15816c8afd0ec35adb5eaf4fad07b0c43db3713f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 3 Jan 2022 12:47:29 +0100 Subject: [PATCH 51/60] test: test records for dunning type --- .../doctype/dunning_type/test_records.json | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 erpnext/accounts/doctype/dunning_type/test_records.json 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 0000000000..cb589bf9ca --- /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" + }, + { + "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" + } +] From 18495ed624c86d9f4cd0a75a87bedb70a0e74a04 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 3 Jan 2022 13:20:50 +0100 Subject: [PATCH 52/60] fix: semgrep issues --- erpnext/accounts/doctype/dunning/test_dunning.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index 129ca32d3a..6125bd26c6 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -27,7 +27,6 @@ class TestDunning(unittest.TestCase): 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() - frappe.db.commit() @classmethod def tearDownClass(cls): From 772f6ffd212d564df1fa3b6f858e642ed9eb0d5b Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 14 Jun 2023 16:48:18 +0530 Subject: [PATCH 53/60] fix: Linter and incorrect cost center in test records --- erpnext/accounts/doctype/dunning/dunning.py | 28 +++++++----- .../accounts/doctype/dunning/test_dunning.py | 23 +++++----- .../doctype/dunning_type/test_records.json | 4 +- .../doctype/payment_entry/payment_entry.py | 42 ++++++++++-------- .../doctype/sales_invoice/sales_invoice.py | 43 ++++++++----------- .../patches/v14_0/single_to_multi_dunning.py | 7 +-- 6 files changed, 80 insertions(+), 67 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 719f3698dc..e0d75d3b47 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -23,7 +23,6 @@ from erpnext.controllers.accounts_controller import AccountsController class Dunning(AccountsController): - def validate(self): self.validate_same_currency() self.validate_overdue_payments() @@ -37,7 +36,11 @@ class Dunning(AccountsController): 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)) + frappe.throw( + _( + "The currency of invoice {} ({}) is different from the currency of this dunning ({})." + ).format(row.sales_invoice, invoice_currency, self.currency) + ) def validate_overdue_payments(self): daily_interest = self.rate_of_interest / 100 / 365 @@ -55,12 +58,13 @@ class Dunning(AccountsController): def set_dunning_level(self): for row in self.overdue_payments: - past_dunnings = frappe.get_all("Overdue Payment", + past_dunnings = frappe.get_all( + "Overdue Payment", filters={ "payment_schedule": row.payment_schedule, "parent": ("!=", row.parent), - "docstatus": 1 - } + "docstatus": 1, + }, ) row.dunning_level = len(past_dunnings) + 1 @@ -72,21 +76,26 @@ def resolve_dunning(doc, state): """ for reference in doc.references: if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0: - unresolved_dunnings = frappe.get_all("Dunning", + unresolved_dunnings = frappe.get_all( + "Dunning", filters={ "sales_invoice": reference.reference_name, "status": ("!=", "Resolved"), "docstatus": ("!=", 2), }, - pluck="name" + pluck="name", ) for dunning_name in unresolved_dunnings: resolve = True dunning = frappe.get_doc("Dunning", dunning_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") + 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" + ) if outstanding_ps > 0 and outstanding_inv > 0: resolve = False @@ -95,7 +104,6 @@ def resolve_dunning(doc, state): dunning.save() - @frappe.whitelist() def get_dunning_letter_text(dunning_type, doc, language=None): if isinstance(doc, str): diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index 6125bd26c6..be8c533d8d 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -112,13 +112,16 @@ def create_dunning_type(title, fee, interest, is_default): 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] + 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] + ) diff --git a/erpnext/accounts/doctype/dunning_type/test_records.json b/erpnext/accounts/doctype/dunning_type/test_records.json index cb589bf9ca..7f28aab873 100644 --- a/erpnext/accounts/doctype/dunning_type/test_records.json +++ b/erpnext/accounts/doctype/dunning_type/test_records.json @@ -14,7 +14,7 @@ } ], "income_account": "Sales - _TC", - "cost_center": "_Test Cost Center" + "cost_center": "_Test Cost Center - _TC" }, { "doctype": "Dunning Type", @@ -31,6 +31,6 @@ } ], "income_account": "Sales - _TC", - "cost_center": "_Test Cost Center" + "cost_center": "_Test Cost Center - _TC" } ] diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 090308f6fd..2bd703f4bc 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1850,22 +1850,28 @@ def get_payment_entry( 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", + { + "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("deductions", { - "account": doc.income_account, - "cost_center": doc.cost_center, - "amount": -1 * doc.dunning_amount, - "description": _("Interest and/or dunning fee") - }) + pe.append( + "deductions", + { + "account": doc.income_account, + "cost_center": doc.cost_center, + "amount": -1 * doc.dunning_amount, + "description": _("Interest and/or dunning fee"), + }, + ) else: pe.append( "references", @@ -1957,8 +1963,10 @@ def set_party_account_currency(dt, party_account, doc): def set_payment_type(dt, doc): if ( - dt == "Sales Order" or (dt == "Sales Invoice" and doc.outstanding_amount > 0) - ) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0) or dt == "Dunning": + (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.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index b2cd4a6d08..e3a159ba58 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -622,7 +622,9 @@ class SalesInvoice(SellingController): return if not self.account_for_change_amount: - self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account') + self.account_for_change_amount = frappe.get_cached_value( + "Company", self.company, "default_cash_account" + ) from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details @@ -1907,17 +1909,17 @@ def get_bank_cash_account(mode_of_payment, company): @frappe.whitelist() def make_maintenance_schedule(source_name, target_doc=None): - doclist = get_mapped_doc("Sales Invoice", source_name, { - "Sales Invoice": { - "doctype": "Maintenance Schedule", - "validation": { - "docstatus": ["=", 1] - } + doclist = get_mapped_doc( + "Sales Invoice", + source_name, + { + "Sales Invoice": {"doctype": "Maintenance Schedule", "validation": {"docstatus": ["=", 1]}}, + "Sales Invoice Item": { + "doctype": "Maintenance Schedule Item", + }, }, - "Sales Invoice Item": { - "doctype": "Maintenance Schedule Item", - }, - }, target_doc) + target_doc, + ) return doclist @@ -2523,9 +2525,7 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): 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 + dunning_type=dunning_type.name, doc=target.as_dict(), language=source.language ) if letter_text: @@ -2542,26 +2542,19 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): table_maps={ "Sales Invoice": { "doctype": "Dunning", - "field_map": { - "customer_address": "customer_address", - "parent": "sales_invoice" - }, + "field_map": {"customer_address": "customer_address", "parent": "sales_invoice"}, }, "Payment Schedule": { "doctype": "Overdue Payment", - "field_map": { - "name": "payment_schedule", - "parent": "sales_invoice" - }, + "field_map": {"name": "payment_schedule", "parent": "sales_invoice"}, "condition": lambda doc: doc.outstanding > 0 and getdate(doc.due_date) < getdate(), - } + }, }, postprocess=postprocess_dunning, - ignore_permissions=ignore_permissions + ignore_permissions=ignore_permissions, ) - def check_if_return_invoice_linked_with_payment_entry(self): # If a Return invoice is linked with payment entry along with other invoices, # the cancellation of the Return causes allocated amount to be greater than paid diff --git a/erpnext/patches/v14_0/single_to_multi_dunning.py b/erpnext/patches/v14_0/single_to_multi_dunning.py index 90966aa4cb..7a8e591798 100644 --- a/erpnext/patches/v14_0/single_to_multi_dunning.py +++ b/erpnext/patches/v14_0/single_to_multi_dunning.py @@ -18,7 +18,8 @@ def execute(): # something's already here, doesn't need patching continue - payment_schedules = frappe.get_all("Payment Schedule", + payment_schedules = frappe.get_all( + "Payment Schedule", filters={"parent": dunning.sales_invoice}, fields=[ "parent as sales_invoice", @@ -30,8 +31,8 @@ def execute(): # at the time of creating this dunning, the full amount was outstanding "payment_amount as outstanding", "'0' as paid_amount", - "discounted_amount" - ] + "discounted_amount", + ], ) dunning.extend("overdue_payments", payment_schedules) From 4673aa412e0e2aec1bc82df4b7264f6fd6f3c680 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 15 Jun 2023 15:47:18 +0530 Subject: [PATCH 54/60] fix: Broken pop-up and references to non-existent field - `child_fieldname` misspelled causing broken pop up to fetch overdue payments - `sales_invoice` referenced in dunning fields, which has been removed - Fetch `customer_name` from `customer` link field --- erpnext/accounts/doctype/dunning/dunning.js | 2 +- erpnext/accounts/doctype/dunning/dunning.json | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index a99b44ff1e..8171bb93ef 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -72,7 +72,7 @@ frappe.ui.form.on("Dunning", { company: frm.doc.company }, allow_child_item_selection: true, - child_fielname: "payment_schedule", + child_fieldname: "payment_schedule", child_columns: ["due_date", "outstanding"], }); }); diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index 20e843c922..b7e8aeaaaf 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -75,7 +75,7 @@ "print_hide": 1 }, { - "fetch_from": "sales_invoice.customer_name", + "fetch_from": "customer.customer_name", "fieldname": "customer_name", "fieldtype": "Data", "in_list_view": 1, @@ -184,21 +184,18 @@ "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", @@ -206,14 +203,12 @@ "read_only": 1 }, { - "fetch_from": "sales_invoice.company_address_display", "fieldname": "company_address_display", "fieldtype": "Small Text", "label": "Company Address Display", "read_only": 1 }, { - "fetch_from": "sales_invoice.contact_email", "fieldname": "contact_email", "fieldtype": "Data", "label": "Contact Email", @@ -221,7 +216,6 @@ "read_only": 1 }, { - "fetch_from": "sales_invoice.customer", "fieldname": "customer", "fieldtype": "Link", "label": "Customer", @@ -387,7 +381,7 @@ ], "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", From 254bab33da379d223751149414921145a631981e Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 15 Jun 2023 19:00:24 +0530 Subject: [PATCH 55/60] fix: Consider installments/partial payments while back updating Dunning - Also use data from Overdue Payment table and not just Dunning parent document --- erpnext/accounts/doctype/dunning/dunning.py | 33 ++++++++++++++------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index e0d75d3b47..1daaf0682a 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -75,16 +75,12 @@ def resolve_dunning(doc, state): when a Payment Entry is submitted. """ for reference in doc.references: - if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0: - unresolved_dunnings = frappe.get_all( - "Dunning", - filters={ - "sales_invoice": reference.reference_name, - "status": ("!=", "Resolved"), - "docstatus": ("!=", 2), - }, - pluck="name", - ) + # Consider partial and full payments + if ( + reference.reference_doctype == "Sales Invoice" + and reference.outstanding_amount < reference.total_amount + ): + unresolved_dunnings = get_unresolved_dunnings(reference.reference_name) for dunning_name in unresolved_dunnings: resolve = True @@ -104,6 +100,23 @@ def resolve_dunning(doc, state): dunning.save() +def get_unresolved_dunnings(sales_invoice): + 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 != "Resolved") + & (dunning.docstatus != 2) + & (overdue_payment.sales_invoice == sales_invoice) + ) + ).run(as_dict=True) + + @frappe.whitelist() def get_dunning_letter_text(dunning_type, doc, language=None): if isinstance(doc, str): From c32113918ea92038aae94461fd61e6bcc8ade626 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 15 Jun 2023 20:04:54 +0530 Subject: [PATCH 56/60] fix: Updation of dunning on PE cancellation --- erpnext/accounts/doctype/dunning/dunning.py | 35 ++++++++++++--------- erpnext/hooks.py | 1 + 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 1daaf0682a..1447ac03f0 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -75,16 +75,23 @@ def resolve_dunning(doc, state): when a Payment Entry is submitted. """ for reference in doc.references: - # Consider partial and full payments - if ( - reference.reference_doctype == "Sales Invoice" - and reference.outstanding_amount < reference.total_amount - ): - unresolved_dunnings = get_unresolved_dunnings(reference.reference_name) + # 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 - for dunning_name in unresolved_dunnings: + 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: resolve = True - dunning = frappe.get_doc("Dunning", dunning_name) + 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" @@ -92,15 +99,13 @@ def resolve_dunning(doc, state): outstanding_ps = frappe.get_value( "Payment Schedule", overdue_payment.payment_schedule, "outstanding" ) - if outstanding_ps > 0 and outstanding_inv > 0: - resolve = False + resolve = False if (outstanding_ps > 0 and outstanding_inv > 0) else True - if resolve: - dunning.status = "Resolved" - dunning.save() + dunning.status = "Resolved" if resolve else "Unresolved" + dunning.save() -def get_unresolved_dunnings(sales_invoice): +def get_linked_dunnings_as_per_state(sales_invoice, state): dunning = frappe.qb.DocType("Dunning") overdue_payment = frappe.qb.DocType("Overdue Payment") @@ -110,7 +115,7 @@ def get_unresolved_dunnings(sales_invoice): .on(overdue_payment.parent == dunning.name) .select(dunning.name) .where( - (dunning.status != "Resolved") + (dunning.status == state) & (dunning.docstatus != 2) & (overdue_payment.sales_invoice == sales_invoice) ) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index c821fcf4e6..6d64f64d1d 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -334,6 +334,7 @@ doc_events = { "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": { From 47852803f0bbe578ffcb4160170eaf0120a1eb4c Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 16 Jun 2023 14:10:07 +0530 Subject: [PATCH 57/60] fix: Set Address via JS and Py files (for API usecases) --- erpnext/accounts/doctype/dunning/dunning.js | 3 +++ erpnext/accounts/doctype/dunning/dunning.py | 29 +++++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 8171bb93ef..7c4e9529a7 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -140,6 +140,9 @@ frappe.ui.form.on("Dunning", { 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); diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 1447ac03f0..c8cfbca27d 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -11,12 +11,11 @@ -> Resolves dunning automatically """ -from __future__ import unicode_literals - import json import frappe from frappe import _ +from frappe.contacts.doctype.address.address import get_address_display from frappe.utils import getdate from erpnext.controllers.accounts_controller import AccountsController @@ -27,6 +26,7 @@ class Dunning(AccountsController): self.validate_same_currency() self.validate_overdue_payments() self.validate_totals() + self.set_party_details() self.set_dunning_level() def validate_same_currency(self): @@ -56,6 +56,31 @@ class Dunning(AccountsController): 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"), + ) + for field in [ + "customer_address", + "address_display", + "company_address", + "contact_person", + "contact_display", + "contact_mobile", + ]: + self.set(field, party_details.get(field)) + + self.set("company_address_display", get_address_display(self.company_address)) + def set_dunning_level(self): for row in self.overdue_payments: past_dunnings = frappe.get_all( From 8f2e5288ff8d1651b8d41a7c7b977e99b65506c4 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 20 Jun 2023 11:47:04 +0530 Subject: [PATCH 58/60] test: Dunning and PE against partially due invoice - Check if the right payment portion is picked - Check if the SI and Dunning are updated on submission and cancellation of PE --- erpnext/accounts/doctype/dunning/dunning.js | 2 +- erpnext/accounts/doctype/dunning/dunning.py | 2 +- .../accounts/doctype/dunning/test_dunning.py | 86 +++++++++++++++++-- 3 files changed, 80 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 7c4e9529a7..1ac909e745 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -1,4 +1,4 @@ -// 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", { diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index c8cfbca27d..9d0d36b970 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -1,4 +1,4 @@ -# 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 diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index be8c533d8d..b29ace275f 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -1,9 +1,7 @@ -# 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 import get_default_cost_center @@ -21,9 +19,10 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import ( test_dependencies = ["Company", "Cost Center"] -class TestDunning(unittest.TestCase): +class TestDunning(FrappeTestCase): @classmethod 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() @@ -31,8 +30,9 @@ class TestDunning(unittest.TestCase): @classmethod def tearDownClass(cls): unlink_payment_on_cancel_of_invoice(0) + super().tearDownClass() - def test_first_dunning(self): + def test_dunning_without_fees(self): dunning = create_dunning(overdue_days=20) self.assertEqual(round(dunning.total_outstanding, 2), 100.00) @@ -41,7 +41,7 @@ class TestDunning(unittest.TestCase): self.assertEqual(round(dunning.dunning_amount, 2), 0.00) self.assertEqual(round(dunning.grand_total, 2), 100.00) - def test_second_dunning(self): + def test_dunning_with_fees_and_interest(self): dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC") self.assertEqual(round(dunning.total_outstanding, 2), 100.00) @@ -50,7 +50,7 @@ class TestDunning(unittest.TestCase): self.assertEqual(round(dunning.dunning_amount, 2), 10.41) self.assertEqual(round(dunning.grand_total, 2), 110.41) - def test_payment_entry(self): + 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) @@ -68,6 +68,44 @@ class TestDunning(unittest.TestCase): 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(overdue_days, dunning_type_name=None): posting_date = add_days(today(), -1 * overdue_days) @@ -125,3 +163,35 @@ def get_income_account(company): pluck="name", )[0] ) + + +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() From 5a952987a316185f50d80e7a646d8edb84512105 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 28 Jun 2023 17:13:34 +0530 Subject: [PATCH 59/60] fix: Use `this.frm` (Linter) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 053b1a324c..d21a50c1c3 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -149,8 +149,8 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e ); if (payment_is_overdue) { - cur_frm.add_custom_button(__('Dunning'), function () { - cur_frm.events.create_dunning(cur_frm); + this.frm.add_custom_button(__('Dunning'), () => { + this.frm.events.create_dunning(this.frm); }, __('Create')); } } From a939431d48efc05896a356e8fd4993e59af6c6cb Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 3 Jul 2023 21:03:24 +0200 Subject: [PATCH 60/60] fix: german translations --- erpnext/translations/de.csv | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 26a775e9de..c02aeb2f83 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -3079,9 +3079,9 @@ Total Leaves,insgesamt Blätter, Total Order Considered,Geschätzte Summe der Bestellungen, Total Order Value,Gesamtbestellwert, Total Outgoing,Summe Auslieferungen, -Total Outstanding,Absolut aussergewöhnlich, -Total Outstanding Amount,Offener Gesamtbetrag, -Total Outstanding: {0},Gesamtsumme: {0}, +Total Outstanding,Summe ausstehende Beträge, +Total Outstanding Amount,Summe ausstehende Beträge, +Total Outstanding: {0},Summe ausstehende Beträge: {0}, Total Paid Amount,Summe gezahlte Beträge, Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total,Der gesamte Zahlungsbetrag im Zahlungsplan muss gleich Groß / Abgerundet sein, Total Payments,Gesamtzahlungen, @@ -8537,13 +8537,14 @@ If this is unchecked Journal Entries will be saved in a Draft state and will hav Enable Distributed Cost Center,Aktivieren Sie die verteilte Kostenstelle, Distributed Cost Center,Verteilte Kostenstelle, Dunning,Mahnung, +Dunning Level,Mahnstufe, DUNN-.MM.-.YY.-,DUNN-.MM .-. YY.-, Overdue Days,Überfällige Tage, Dunning Type,Mahnart, Dunning Fee,Mahngebühr, Dunning Amount,Mahnbetrag, -Resolved,Aufgelöst, -Unresolved,Ungelöst, +Resolved,Geklärt, +Unresolved,Ungeklärt, Printing Setting,Druckeinstellung, Body Text,Hauptteil, Closing Text,Text schließen, @@ -8723,7 +8724,7 @@ Company {0} already exists. Continuing will overwrite the Company and Chart of A Meta Data,Metadaten, Unresolve,Auflösen, Create Document,Dokument erstellen, -Mark as unresolved,Als ungelöst markieren, +Mark as unresolved,Als ungeklärt markieren, TaxJar Settings,TaxJar-Einstellungen, Sandbox Mode,Sandbox-Modus, Enable Tax Calculation,Steuerberechnung aktivieren,