Merge pull request #35689 from marination/payments-based-dunning

feat: Payments based dunning
This commit is contained in:
Deepesh Garg 2023-07-20 12:06:36 +05:30 committed by GitHub
commit 3bc79eebe3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1021 additions and 500 deletions

View File

@ -1,13 +1,14 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Dunning", { frappe.ui.form.on("Dunning", {
setup: function (frm) { setup: function (frm) {
frm.set_query("sales_invoice", () => { frm.set_query("sales_invoice", "overdue_payments", () => {
return { return {
filters: { filters: {
docstatus: 1, docstatus: 1,
company: frm.doc.company, company: frm.doc.company,
customer: frm.doc.customer,
outstanding_amount: [">", 0], outstanding_amount: [">", 0],
status: "Overdue" status: "Overdue"
}, },
@ -22,14 +23,24 @@ 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);
frm.set_query("company_address", erpnext.queries.company_address_query);
// cannot add rows manually, only via button "Fetch Overdue Payments"
frm.set_df_property("overdue_payments", "cannot_add_rows", true);
}, },
refresh: function (frm) { refresh: function (frm) {
frm.set_df_property("company", "read_only", frm.doc.__islocal ? 0 : 1); 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") { if (frm.doc.docstatus === 1 && frm.doc.status === "Unresolved") {
frm.add_custom_button(__("Resolve"), () => { frm.add_custom_button(__("Resolve"), () => {
frm.set_value("status", "Resolved"); frm.set_value("status", "Resolved");
@ -40,42 +51,111 @@ frappe.ui.form.on("Dunning", {
__("Payment"), __("Payment"),
function () { function () {
frm.events.make_payment_entry(frm); frm.events.make_payment_entry(frm);
},__("Create") }, __("Create")
); );
frm.page.set_inner_btn_group_as_primary(__("Create")); frm.page.set_inner_btn_group_as_primary(__("Create"));
} }
if(frm.doc.docstatus > 0) { if (frm.doc.docstatus === 0) {
frm.add_custom_button(__('Ledger'), function() { frm.add_custom_button(__("Fetch Overdue Payments"), () => {
frappe.route_options = { erpnext.utils.map_current_doc({
"voucher_no": frm.doc.name, method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
"from_date": frm.doc.posting_date, source_doctype: "Sales Invoice",
"to_date": frm.doc.posting_date, date_field: "due_date",
"company": frm.doc.company, target: frm,
"show_cancelled_entries": frm.doc.docstatus === 2 setters: {
}; customer: frm.doc.customer || undefined,
frappe.set_route("query-report", "General Ledger"); },
}, __('View')); get_query_filters: {
docstatus: 1,
status: "Overdue",
company: frm.doc.company
},
allow_child_item_selection: true,
child_fieldname: "payment_schedule",
child_columns: ["due_date", "outstanding"],
});
});
} }
frappe.dynamic_link = { doc: frm.doc, fieldname: 'customer', doctype: 'Customer' };
frm.toggle_display("customer_name", (frm.doc.customer_name && frm.doc.customer_name !== frm.doc.customer));
}, },
overdue_days: function (frm) { // When multiple companies are set up. in case company name is changed set default company address
frappe.db.get_value( company: function (frm) {
"Dunning Type", if (frm.doc.company) {
{ frappe.call({
start_day: ["<", frm.doc.overdue_days], method: "erpnext.setup.doctype.company.company.get_default_company_address",
end_day: [">=", frm.doc.overdue_days], args: { name: frm.doc.company, existing_address: frm.doc.company_address || "" },
}, debounce: 2000,
"dunning_type", callback: function (r) {
(r) => { frm.set_value("company_address", r && r.message || "");
if (r) { }
frm.set_value("dunning_type", r.dunning_type); });
} else {
frm.set_value("dunning_type", ""); if (frm.fields_dict.currency) {
frm.set_value("rate_of_interest", ""); const company_currency = erpnext.get_currency(frm.doc.company);
frm.set_value("dunning_fee", "");
if (!frm.doc.currency) {
frm.set_value("currency", company_currency);
}
if (frm.doc.currency == company_currency) {
frm.set_value("conversion_rate", 1.0);
} }
} }
);
const company_doc = frappe.get_doc(":Company", frm.doc.company);
if (company_doc.default_letter_head) {
if (frm.fields_dict.letter_head) {
frm.set_value("letter_head", company_doc.default_letter_head);
}
}
}
},
currency: function (frm) {
// this.set_dynamic_labels();
const company_currency = erpnext.get_currency(frm.doc.company);
// Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc
if (frm.doc.currency && frm.doc.currency !== company_currency) {
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
transaction_date: frm.doc.posting_date,
from_currency: frm.doc.currency,
to_currency: company_currency,
args: "for_selling"
},
freeze: true,
freeze_message: __("Fetching exchange rates ..."),
callback: function(r) {
const exchange_rate = flt(r.message);
if (exchange_rate != frm.doc.conversion_rate) {
frm.set_value("conversion_rate", exchange_rate);
}
}
});
} else {
frm.trigger("conversion_rate");
}
},
customer: (frm) => {
erpnext.utils.get_party_details(frm);
},
conversion_rate: function (frm) {
if (frm.doc.currency === erpnext.get_currency(frm.doc.company)) {
frm.set_value("conversion_rate", 1.0);
}
// Make read only if Accounts Settings doesn't allow stale rates
frm.set_df_property("conversion_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
},
customer_address: function (frm) {
erpnext.utils.get_address_display(frm, "customer_address");
},
company_address: function (frm) {
erpnext.utils.get_address_display(frm, "company_address");
}, },
dunning_type: function (frm) { dunning_type: function (frm) {
frm.trigger("get_dunning_letter_text"); frm.trigger("get_dunning_letter_text");
@ -87,7 +167,7 @@ frappe.ui.form.on("Dunning", {
if (frm.doc.dunning_type) { if (frm.doc.dunning_type) {
frappe.call({ frappe.call({
method: method:
"erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text", "erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text",
args: { args: {
dunning_type: frm.doc.dunning_type, dunning_type: frm.doc.dunning_type,
language: frm.doc.language, language: frm.doc.language,
@ -106,49 +186,62 @@ frappe.ui.form.on("Dunning", {
}); });
} }
}, },
due_date: function (frm) {
frm.trigger("calculate_overdue_days");
},
posting_date: function (frm) { posting_date: function (frm) {
frm.trigger("calculate_overdue_days"); frm.trigger("calculate_overdue_days");
}, },
rate_of_interest: function (frm) { rate_of_interest: function (frm) {
frm.trigger("calculate_interest_and_amount"); frm.trigger("calculate_interest");
},
outstanding_amount: function (frm) {
frm.trigger("calculate_interest_and_amount");
},
interest_amount: function (frm) {
frm.trigger("calculate_interest_and_amount");
}, },
dunning_fee: function (frm) { dunning_fee: function (frm) {
frm.trigger("calculate_interest_and_amount"); frm.trigger("calculate_totals");
}, },
sales_invoice: function (frm) { overdue_payments_add: function (frm) {
frm.trigger("calculate_overdue_days"); frm.trigger("calculate_totals");
},
overdue_payments_remove: function (frm) {
frm.trigger("calculate_totals");
}, },
calculate_overdue_days: function (frm) { calculate_overdue_days: function (frm) {
if (frm.doc.posting_date && frm.doc.due_date) { frm.doc.overdue_payments.forEach((row) => {
const overdue_days = moment(frm.doc.posting_date).diff( if (frm.doc.posting_date && row.due_date) {
frm.doc.due_date, const overdue_days = moment(frm.doc.posting_date).diff(
"days" row.due_date,
); "days"
frm.set_value("overdue_days", overdue_days); );
} frappe.model.set_value(row.doctype, row.name, "overdue_days", overdue_days);
}
});
}, },
calculate_interest_and_amount: function (frm) { calculate_interest: function (frm) {
const interest_per_year = frm.doc.outstanding_amount * frm.doc.rate_of_interest / 100; frm.doc.overdue_payments.forEach((row) => {
const interest_amount = flt((interest_per_year * cint(frm.doc.overdue_days)) / 365 || 0, precision('interest_amount')); const interest_per_day = frm.doc.rate_of_interest / 100 / 365;
const dunning_amount = flt(interest_amount + frm.doc.dunning_fee, precision('dunning_amount')); const interest = flt((interest_per_day * row.overdue_days * row.outstanding), precision("interest", row));
const grand_total = flt(frm.doc.outstanding_amount + dunning_amount, precision('grand_total')); frappe.model.set_value(row.doctype, row.name, "interest", interest);
frm.set_value("interest_amount", interest_amount); });
frm.set_value("dunning_amount", dunning_amount); },
frm.set_value("grand_total", grand_total); calculate_totals: function (frm) {
const total_interest = frm.doc.overdue_payments
.reduce((prev, cur) => prev + cur.interest, 0);
const total_outstanding = frm.doc.overdue_payments
.reduce((prev, cur) => prev + cur.outstanding, 0);
const dunning_amount = total_interest + frm.doc.dunning_fee;
const base_dunning_amount = dunning_amount * frm.doc.conversion_rate;
const grand_total = total_outstanding + dunning_amount;
function setWithPrecison(field, value) {
frm.set_value(field, flt(value, precision(field)));
}
setWithPrecison("total_outstanding", total_outstanding);
setWithPrecison("total_interest", total_interest);
setWithPrecison("dunning_amount", dunning_amount);
setWithPrecison("base_dunning_amount", base_dunning_amount);
setWithPrecison("grand_total", grand_total);
}, },
make_payment_entry: function (frm) { make_payment_entry: function (frm) {
return frappe.call({ return frappe.call({
method: method:
"erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry", "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry",
args: { args: {
dt: frm.doc.doctype, dt: frm.doc.doctype,
dn: frm.doc.name, dn: frm.doc.name,
@ -160,3 +253,9 @@ frappe.ui.form.on("Dunning", {
}); });
}, },
}); });
frappe.ui.form.on("Overdue Payment", {
interest: function (frm) {
frm.trigger("calculate_totals");
}
});

View File

@ -2,49 +2,60 @@
"actions": [], "actions": [],
"allow_events_in_timeline": 1, "allow_events_in_timeline": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"beta": 1,
"creation": "2019-07-05 16:34:31.013238", "creation": "2019-07-05 16:34:31.013238",
"doctype": "DocType", "doctype": "DocType",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"title",
"naming_series", "naming_series",
"sales_invoice",
"customer", "customer",
"customer_name", "customer_name",
"outstanding_amount",
"currency",
"conversion_rate",
"column_break_3", "column_break_3",
"company", "company",
"posting_date", "posting_date",
"posting_time", "posting_time",
"due_date", "status",
"overdue_days", "section_break_9",
"currency",
"column_break_11",
"conversion_rate",
"address_and_contact_section", "address_and_contact_section",
"customer_address",
"address_display", "address_display",
"contact_person",
"contact_display", "contact_display",
"column_break_16",
"company_address",
"company_address_display",
"contact_mobile", "contact_mobile",
"contact_email", "contact_email",
"column_break_18",
"company_address_display",
"section_break_6", "section_break_6",
"dunning_type", "dunning_type",
"dunning_fee",
"column_break_8", "column_break_8",
"rate_of_interest", "rate_of_interest",
"interest_amount",
"section_break_12", "section_break_12",
"dunning_amount", "overdue_payments",
"grand_total", "section_break_28",
"income_account", "total_interest",
"dunning_fee",
"column_break_17", "column_break_17",
"status", "dunning_amount",
"printing_setting_section", "base_dunning_amount",
"section_break_32",
"spacer",
"column_break_33",
"total_outstanding",
"grand_total",
"printing_settings_section",
"language", "language",
"body_text", "body_text",
"column_break_22", "column_break_22",
"letter_head", "letter_head",
"closing_text", "closing_text",
"accounting_details_section",
"income_account",
"column_break_48",
"cost_center",
"amended_from" "amended_from"
], ],
"fields": [ "fields": [
@ -60,32 +71,17 @@
"fieldname": "naming_series", "fieldname": "naming_series",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Series", "label": "Series",
"options": "DUNN-.MM.-.YY.-" "options": "DUNN-.MM.-.YY.-",
"print_hide": 1
}, },
{ {
"fieldname": "sales_invoice", "fetch_from": "customer.customer_name",
"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", "fieldname": "customer_name",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Customer Name", "label": "Customer Name",
"read_only": 1 "read_only": 1
}, },
{
"fetch_from": "sales_invoice.outstanding_amount",
"fieldname": "outstanding_amount",
"fieldtype": "Currency",
"label": "Outstanding Amount",
"read_only": 1
},
{ {
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@ -94,13 +90,8 @@
"default": "Today", "default": "Today",
"fieldname": "posting_date", "fieldname": "posting_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Date" "label": "Date",
}, "reqd": 1
{
"fieldname": "overdue_days",
"fieldtype": "Int",
"label": "Overdue Days",
"read_only": 1
}, },
{ {
"fieldname": "section_break_6", "fieldname": "section_break_6",
@ -112,16 +103,7 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Dunning Type", "label": "Dunning Type",
"options": "Dunning Type", "options": "Dunning Type"
"reqd": 1
},
{
"default": "0",
"fieldname": "interest_amount",
"fieldtype": "Currency",
"label": "Interest Amount",
"precision": "2",
"read_only": 1
}, },
{ {
"fieldname": "column_break_8", "fieldname": "column_break_8",
@ -134,6 +116,7 @@
"fieldname": "dunning_fee", "fieldname": "dunning_fee",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Dunning Fee", "label": "Dunning Fee",
"options": "currency",
"precision": "2" "precision": "2"
}, },
{ {
@ -144,36 +127,24 @@
"fieldname": "column_break_17", "fieldname": "column_break_17",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "printing_setting_section",
"fieldtype": "Section Break",
"label": "Printing Setting"
},
{ {
"fieldname": "language", "fieldname": "language",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Print Language", "label": "Print Language",
"options": "Language" "options": "Language",
"print_hide": 1
}, },
{ {
"fieldname": "letter_head", "fieldname": "letter_head",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Letter Head", "label": "Letter Head",
"options": "Letter Head" "options": "Letter Head",
"print_hide": 1
}, },
{ {
"fieldname": "column_break_22", "fieldname": "column_break_22",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fetch_from": "sales_invoice.currency",
"fieldname": "currency",
"fieldtype": "Link",
"hidden": 1,
"label": "Currency",
"options": "Currency",
"read_only": 1
},
{ {
"fieldname": "amended_from", "fieldname": "amended_from",
"fieldtype": "Link", "fieldtype": "Link",
@ -183,14 +154,6 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{
"allow_on_submit": 1,
"default": "{customer_name}",
"fieldname": "title",
"fieldtype": "Data",
"hidden": 1,
"label": "Title"
},
{ {
"fieldname": "body_text", "fieldname": "body_text",
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
@ -201,13 +164,6 @@
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"label": "Closing Text" "label": "Closing Text"
}, },
{
"fetch_from": "sales_invoice.due_date",
"fieldname": "due_date",
"fieldtype": "Date",
"label": "Due Date",
"read_only": 1
},
{ {
"fieldname": "posting_time", "fieldname": "posting_time",
"fieldtype": "Time", "fieldtype": "Time",
@ -222,26 +178,24 @@
"label": "Rate of Interest (%) Yearly" "label": "Rate of Interest (%) Yearly"
}, },
{ {
"collapsible": 1,
"fieldname": "address_and_contact_section", "fieldname": "address_and_contact_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Address and Contact" "label": "Address and Contact"
}, },
{ {
"fetch_from": "sales_invoice.address_display",
"fieldname": "address_display", "fieldname": "address_display",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Address", "label": "Address",
"read_only": 1 "read_only": 1
}, },
{ {
"fetch_from": "sales_invoice.contact_display",
"fieldname": "contact_display", "fieldname": "contact_display",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Contact", "label": "Contact",
"read_only": 1 "read_only": 1
}, },
{ {
"fetch_from": "sales_invoice.contact_mobile",
"fieldname": "contact_mobile", "fieldname": "contact_mobile",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Mobile No", "label": "Mobile No",
@ -249,18 +203,12 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"fetch_from": "sales_invoice.company_address_display",
"fieldname": "company_address_display", "fieldname": "company_address_display",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Company Address", "label": "Company Address Display",
"read_only": 1 "read_only": 1
}, },
{ {
"fetch_from": "sales_invoice.contact_email",
"fieldname": "contact_email", "fieldname": "contact_email",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Contact Email", "label": "Contact Email",
@ -268,18 +216,18 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fetch_from": "sales_invoice.customer",
"fieldname": "customer", "fieldname": "customer",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Customer", "label": "Customer",
"options": "Customer", "options": "Customer",
"read_only": 1 "reqd": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "grand_total", "fieldname": "grand_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Grand Total", "label": "Grand Total",
"options": "currency",
"precision": "2", "precision": "2",
"read_only": 1 "read_only": 1
}, },
@ -290,33 +238,150 @@
"fieldtype": "Select", "fieldtype": "Select",
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Status", "label": "Status",
"options": "Draft\nResolved\nUnresolved\nCancelled" "options": "Draft\nResolved\nUnresolved\nCancelled",
},
{
"fieldname": "dunning_amount",
"fieldtype": "Currency",
"hidden": 1,
"label": "Dunning Amount",
"read_only": 1 "read_only": 1
}, },
{ {
"description": "For dunning fee and interest",
"fetch_from": "dunning_type.income_account",
"fieldname": "income_account", "fieldname": "income_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Income Account", "label": "Income Account",
"options": "Account" "options": "Account",
"print_hide": 1
},
{
"fieldname": "overdue_payments",
"fieldtype": "Table",
"label": "Overdue Payments",
"options": "Overdue Payment"
},
{
"fieldname": "section_break_28",
"fieldtype": "Section Break"
},
{
"default": "0",
"fieldname": "total_interest",
"fieldtype": "Currency",
"label": "Total Interest",
"options": "currency",
"precision": "2",
"read_only": 1
},
{
"fieldname": "total_outstanding",
"fieldtype": "Currency",
"label": "Total Outstanding",
"options": "currency",
"read_only": 1
},
{
"fieldname": "customer_address",
"fieldtype": "Link",
"label": "Customer Address",
"options": "Address",
"print_hide": 1
},
{
"fieldname": "contact_person",
"fieldtype": "Link",
"label": "Contact Person",
"options": "Contact",
"print_hide": 1
},
{
"default": "0",
"fieldname": "dunning_amount",
"fieldtype": "Currency",
"label": "Dunning Amount",
"options": "currency",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "accounting_details_section",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fetch_from": "dunning_type.cost_center",
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center",
"print_hide": 1
},
{
"collapsible": 1,
"fieldname": "printing_settings_section",
"fieldtype": "Section Break",
"label": "Printing Settings"
},
{
"fieldname": "section_break_32",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_33",
"fieldtype": "Column Break"
},
{
"fieldname": "spacer",
"fieldtype": "Data",
"hidden": 1,
"label": "Spacer",
"print_hide": 1,
"read_only": 1,
"report_hide": 1
},
{
"fieldname": "column_break_16",
"fieldtype": "Column Break"
},
{
"fieldname": "company_address",
"fieldtype": "Link",
"label": "Company Address",
"options": "Address",
"print_hide": 1
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"label": "Currency"
},
{
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
}, },
{ {
"fetch_from": "sales_invoice.conversion_rate",
"fieldname": "conversion_rate", "fieldname": "conversion_rate",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 1, "label": "Conversion Rate"
"label": "Conversion Rate", },
{
"default": "0",
"fieldname": "base_dunning_amount",
"fieldtype": "Currency",
"label": "Dunning Amount (Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
},
{
"fieldname": "column_break_48",
"fieldtype": "Column Break"
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-06-03 16:24:01.677026", "modified": "2023-06-15 15:46:53.865712",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Dunning", "name": "Dunning",

View File

@ -1,131 +1,150 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
"""
# Accounting
1. Payment of outstanding invoices with dunning amount
- Debit full amount to bank
- Credit invoiced amount to receivables
- Credit dunning amount to interest and similar revenue
-> Resolves dunning automatically
"""
import json import json
import frappe import frappe
from frappe.utils import cint, flt, getdate from frappe import _
from frappe.contacts.doctype.address.address import get_address_display
from frappe.utils import getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
class Dunning(AccountsController): class Dunning(AccountsController):
def validate(self): def validate(self):
self.validate_overdue_days() self.validate_same_currency()
self.validate_amount() self.validate_overdue_payments()
if not self.income_account: self.validate_totals()
self.income_account = frappe.get_cached_value("Company", self.company, "default_income_account") self.set_party_details()
self.set_dunning_level()
def validate_overdue_days(self): def validate_same_currency(self):
self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0 """
Throw an error if invoice currency differs from dunning currency.
"""
for row in self.overdue_payments:
invoice_currency = frappe.get_value("Sales Invoice", row.sales_invoice, "currency")
if invoice_currency != self.currency:
frappe.throw(
_(
"The currency of invoice {} ({}) is different from the currency of this dunning ({})."
).format(row.sales_invoice, invoice_currency, self.currency)
)
def validate_amount(self): def validate_overdue_payments(self):
amounts = calculate_interest_and_amount( daily_interest = self.rate_of_interest / 100 / 365
self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days
for row in self.overdue_payments:
row.overdue_days = (getdate(self.posting_date) - getdate(row.due_date)).days or 0
row.interest = row.outstanding * daily_interest * row.overdue_days
def validate_totals(self):
self.total_outstanding = sum(row.outstanding for row in self.overdue_payments)
self.total_interest = sum(row.interest for row in self.overdue_payments)
self.dunning_amount = self.total_interest + self.dunning_fee
self.base_dunning_amount = self.dunning_amount * self.conversion_rate
self.grand_total = self.total_outstanding + self.dunning_amount
def set_party_details(self):
from erpnext.accounts.party import _get_party_details
party_details = _get_party_details(
self.customer,
ignore_permissions=self.flags.ignore_permissions,
doctype=self.doctype,
company=self.company,
posting_date=self.get("posting_date"),
fetch_payment_terms_template=False,
party_address=self.customer_address,
company_address=self.get("company_address"),
) )
if self.interest_amount != amounts.get("interest_amount"): for field in [
self.interest_amount = flt(amounts.get("interest_amount"), self.precision("interest_amount")) "customer_address",
if self.dunning_amount != amounts.get("dunning_amount"): "address_display",
self.dunning_amount = flt(amounts.get("dunning_amount"), self.precision("dunning_amount")) "company_address",
if self.grand_total != amounts.get("grand_total"): "contact_person",
self.grand_total = flt(amounts.get("grand_total"), self.precision("grand_total")) "contact_display",
"contact_mobile",
]:
self.set(field, party_details.get(field))
def on_submit(self): self.set("company_address_display", get_address_display(self.company_address))
self.make_gl_entries()
def on_cancel(self): def set_dunning_level(self):
if self.dunning_amount: for row in self.overdue_payments:
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") past_dunnings = frappe.get_all(
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) "Overdue Payment",
filters={
def make_gl_entries(self): "payment_schedule": row.payment_schedule,
if not self.dunning_amount: "parent": ("!=", row.parent),
return "docstatus": 1,
gl_entries = []
invoice_fields = [
"project",
"cost_center",
"debit_to",
"party_account_currency",
"conversion_rate",
"cost_center",
]
inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
accounting_dimensions = get_accounting_dimensions()
invoice_fields.extend(accounting_dimensions)
dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate)
default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
gl_entries.append(
self.get_gl_dict(
{
"account": inv.debit_to,
"party_type": "Customer",
"party": self.customer,
"due_date": self.due_date,
"against": self.income_account,
"debit": dunning_in_company_currency,
"debit_in_account_currency": self.dunning_amount,
"against_voucher": self.name,
"against_voucher_type": "Dunning",
"cost_center": inv.cost_center or default_cost_center,
"project": inv.project,
}, },
inv.party_account_currency,
item=inv,
) )
) row.dunning_level = len(past_dunnings) + 1
gl_entries.append(
self.get_gl_dict(
{
"account": self.income_account,
"against": self.customer,
"credit": dunning_in_company_currency,
"cost_center": inv.cost_center or default_cost_center,
"credit_in_account_currency": self.dunning_amount,
"project": inv.project,
},
item=inv,
)
)
make_gl_entries(
gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False
)
def resolve_dunning(doc, state): def resolve_dunning(doc, state):
"""
Check if all payments have been made and resolve dunning, if yes. Called
when a Payment Entry is submitted.
"""
for reference in doc.references: for reference in doc.references:
if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0: # Consider partial and full payments:
dunnings = frappe.get_list( # Submitting full payment: outstanding_amount will be 0
"Dunning", # Submitting 1st partial payment: outstanding_amount will be the pending installment
filters={"sales_invoice": reference.reference_name, "status": ("!=", "Resolved")}, # Cancelling full payment: outstanding_amount will revert to total amount
ignore_permissions=True, # Cancelling last partial payment: outstanding_amount will revert to pending amount
) submit_condition = reference.outstanding_amount < reference.total_amount
cancel_condition = reference.outstanding_amount <= reference.total_amount
if reference.reference_doctype == "Sales Invoice" and (
submit_condition if doc.docstatus == 1 else cancel_condition
):
state = "Resolved" if doc.docstatus == 2 else "Unresolved"
dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state)
for dunning in dunnings: for dunning in dunnings:
frappe.db.set_value("Dunning", dunning.name, "status", "Resolved") resolve = True
dunning = frappe.get_doc("Dunning", dunning.get("name"))
for overdue_payment in dunning.overdue_payments:
outstanding_inv = frappe.get_value(
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
)
outstanding_ps = frappe.get_value(
"Payment Schedule", overdue_payment.payment_schedule, "outstanding"
)
resolve = False if (outstanding_ps > 0 and outstanding_inv > 0) else True
dunning.status = "Resolved" if resolve else "Unresolved"
dunning.save()
def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days): def get_linked_dunnings_as_per_state(sales_invoice, state):
interest_amount = 0 dunning = frappe.qb.DocType("Dunning")
grand_total = flt(outstanding_amount) + flt(dunning_fee) overdue_payment = frappe.qb.DocType("Overdue Payment")
if rate_of_interest:
interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100 return (
interest_amount = (interest_per_year * cint(overdue_days)) / 365 frappe.qb.from_(dunning)
grand_total += flt(interest_amount) .join(overdue_payment)
dunning_amount = flt(interest_amount) + flt(dunning_fee) .on(overdue_payment.parent == dunning.name)
return { .select(dunning.name)
"interest_amount": interest_amount, .where(
"grand_total": grand_total, (dunning.status == state)
"dunning_amount": dunning_amount, & (dunning.docstatus != 2)
} & (overdue_payment.sales_invoice == sales_invoice)
)
).run(as_dict=True)
@frappe.whitelist() @frappe.whitelist()

View File

@ -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"]}],
}

View File

@ -1,162 +1,197 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, nowdate, today from frappe.utils import add_days, nowdate, today
from erpnext.accounts.doctype.dunning.dunning import calculate_interest_and_amount from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
unlink_payment_on_cancel_of_invoice, 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 ( from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import (
create_sales_invoice_against_cost_center, create_sales_invoice_against_cost_center,
) )
test_dependencies = ["Company", "Cost Center"]
class TestDunning(unittest.TestCase):
class TestDunning(FrappeTestCase):
@classmethod @classmethod
def setUpClass(self): def setUpClass(cls):
create_dunning_type() super().setUpClass()
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() unlink_payment_on_cancel_of_invoice()
@classmethod @classmethod
def tearDownClass(self): def tearDownClass(cls):
unlink_payment_on_cancel_of_invoice(0) unlink_payment_on_cancel_of_invoice(0)
super().tearDownClass()
def test_dunning(self): def test_dunning_without_fees(self):
dunning = create_dunning() dunning = create_dunning(overdue_days=20)
amounts = calculate_interest_and_amount(
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
)
self.assertEqual(round(amounts.get("interest_amount"), 2), 0.44)
self.assertEqual(round(amounts.get("dunning_amount"), 2), 20.44)
self.assertEqual(round(amounts.get("grand_total"), 2), 120.44)
def test_dunning_with_zero_interest_rate(self): self.assertEqual(round(dunning.total_outstanding, 2), 100.00)
dunning = create_dunning_with_zero_interest_rate() self.assertEqual(round(dunning.total_interest, 2), 0.00)
amounts = calculate_interest_and_amount( self.assertEqual(round(dunning.dunning_fee, 2), 0.00)
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days self.assertEqual(round(dunning.dunning_amount, 2), 0.00)
) self.assertEqual(round(dunning.grand_total, 2), 100.00)
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)
def test_gl_entries(self): def test_dunning_with_fees_and_interest(self):
dunning = create_dunning() dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC")
dunning.submit()
gl_entries = frappe.db.sql(
"""select account, debit, credit
from `tabGL Entry` where voucher_type='Dunning' and voucher_no=%s
order by account asc""",
dunning.name,
as_dict=1,
)
self.assertTrue(gl_entries)
expected_values = dict(
(d[0], d) for d in [["Debtors - _TC", 20.44, 0.0], ["Sales - _TC", 0.0, 20.44]]
)
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_payment_entry(self): self.assertEqual(round(dunning.total_outstanding, 2), 100.00)
dunning = create_dunning() self.assertEqual(round(dunning.total_interest, 2), 0.41)
self.assertEqual(round(dunning.dunning_fee, 2), 10.00)
self.assertEqual(round(dunning.dunning_amount, 2), 10.41)
self.assertEqual(round(dunning.grand_total, 2), 110.41)
def test_dunning_with_payment_entry(self):
dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC")
dunning.submit() dunning.submit()
pe = get_payment_entry("Dunning", dunning.name) pe = get_payment_entry("Dunning", dunning.name)
pe.reference_no = "1" pe.reference_no = "1"
pe.reference_date = nowdate() 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.insert()
pe.submit() pe.submit()
si_doc = frappe.get_doc("Sales Invoice", dunning.sales_invoice)
self.assertEqual(si_doc.outstanding_amount, 0) for overdue_payment in dunning.overdue_payments:
outstanding_amount = frappe.get_value(
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
)
self.assertEqual(outstanding_amount, 0)
dunning.reload()
self.assertEqual(dunning.status, "Resolved")
def test_dunning_and_payment_against_partially_due_invoice(self):
"""
Create SI with first installment overdue. Check impact of Dunning and Payment Entry.
"""
create_payment_terms_template_for_dunning()
sales_invoice = create_sales_invoice_against_cost_center(
posting_date=add_days(today(), -1 * 6),
qty=1,
rate=100,
do_not_submit=True,
)
sales_invoice.payment_terms_template = "_Test 50-50 for Dunning"
sales_invoice.submit()
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
self.assertEqual(len(dunning.overdue_payments), 1)
self.assertEqual(dunning.overdue_payments[0].payment_term, "_Test Payment Term 1 for Dunning")
dunning.submit()
pe = get_payment_entry("Dunning", dunning.name)
pe.reference_no, pe.reference_date = "2", nowdate()
pe.insert()
pe.submit()
sales_invoice.load_from_db()
dunning.load_from_db()
self.assertEqual(sales_invoice.status, "Partly Paid")
self.assertEqual(sales_invoice.payment_schedule[0].outstanding, 0)
self.assertEqual(dunning.status, "Resolved")
# Test impact on cancellation of PE
pe.cancel()
sales_invoice.reload()
dunning.reload()
self.assertEqual(sales_invoice.status, "Overdue")
self.assertEqual(dunning.status, "Unresolved")
def create_dunning(): def create_dunning(overdue_days, dunning_type_name=None):
posting_date = add_days(today(), -20) posting_date = add_days(today(), -1 * overdue_days)
due_date = add_days(today(), -15)
sales_invoice = create_sales_invoice_against_cost_center( sales_invoice = create_sales_invoice_against_cost_center(
posting_date=posting_date, due_date=due_date, status="Overdue" posting_date=posting_date, qty=1, rate=100
) )
dunning_type = frappe.get_doc("Dunning Type", "First Notice") dunning = create_dunning_from_sales_invoice(sales_invoice.name)
dunning = frappe.new_doc("Dunning")
dunning.sales_invoice = sales_invoice.name if dunning_type_name:
dunning.customer_name = sales_invoice.customer_name dunning_type = frappe.get_doc("Dunning Type", dunning_type_name)
dunning.outstanding_amount = sales_invoice.outstanding_amount dunning.dunning_type = dunning_type.name
dunning.debit_to = sales_invoice.debit_to dunning.rate_of_interest = dunning_type.rate_of_interest
dunning.currency = sales_invoice.currency dunning.dunning_fee = dunning_type.dunning_fee
dunning.company = sales_invoice.company dunning.income_account = dunning_type.income_account
dunning.posting_date = nowdate() dunning.cost_center = dunning_type.cost_center
dunning.due_date = sales_invoice.due_date
dunning.dunning_type = "First Notice" return dunning.save()
dunning.rate_of_interest = dunning_type.rate_of_interest
dunning.dunning_fee = dunning_type.dunning_fee
dunning.save()
return dunning
def create_dunning_with_zero_interest_rate(): def create_dunning_type(title, fee, interest, is_default):
posting_date = add_days(today(), -20) company = "_Test Company"
due_date = add_days(today(), -15) if frappe.db.exists("Dunning Type", f"{title} - _TC"):
sales_invoice = create_sales_invoice_against_cost_center( return
posting_date=posting_date, due_date=due_date, status="Overdue"
)
dunning_type = frappe.get_doc("Dunning Type", "First Notice with 0% Rate of Interest")
dunning = frappe.new_doc("Dunning")
dunning.sales_invoice = sales_invoice.name
dunning.customer_name = sales_invoice.customer_name
dunning.outstanding_amount = sales_invoice.outstanding_amount
dunning.debit_to = sales_invoice.debit_to
dunning.currency = sales_invoice.currency
dunning.company = sales_invoice.company
dunning.posting_date = nowdate()
dunning.due_date = sales_invoice.due_date
dunning.dunning_type = "First Notice with 0% Rate of Interest"
dunning.rate_of_interest = dunning_type.rate_of_interest
dunning.dunning_fee = dunning_type.dunning_fee
dunning.save()
return dunning
def create_dunning_type():
dunning_type = frappe.new_doc("Dunning Type") dunning_type = frappe.new_doc("Dunning Type")
dunning_type.dunning_type = "First Notice" dunning_type.dunning_type = title
dunning_type.start_day = 10 dunning_type.company = company
dunning_type.end_day = 20 dunning_type.is_default = is_default
dunning_type.dunning_fee = 20 dunning_type.dunning_fee = fee
dunning_type.rate_of_interest = 8 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_type.append(
"dunning_letter_text", "dunning_letter_text",
{ {
"language": "en", "language": "en",
"body_text": "We have still not received payment for our invoice ", "body_text": "We have still not received payment for our invoice",
"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.", "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.",
}, },
) )
dunning_type.save() dunning_type.insert()
def create_dunning_type_with_zero_interest_rate(): def get_income_account(company):
dunning_type = frappe.new_doc("Dunning Type") return (
dunning_type.dunning_type = "First Notice with 0% Rate of Interest" frappe.get_value("Company", company, "default_income_account")
dunning_type.start_day = 10 or frappe.get_all(
dunning_type.end_day = 20 "Account",
dunning_type.dunning_fee = 20 filters={"is_group": 0, "company": company},
dunning_type.rate_of_interest = 0 or_filters={
dunning_type.append( "report_type": "Profit and Loss",
"dunning_letter_text", "account_type": ("in", ("Income Account", "Temporary")),
{ },
"language": "en", limit=1,
"body_text": "We have still not received payment for our invoice ", pluck="name",
"closing_text": "We kindly request that you pay the outstanding amount immediately, and late fees.", )[0]
},
) )
dunning_type.save()
def create_payment_terms_template_for_dunning():
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_term
create_payment_term("_Test Payment Term 1 for Dunning")
create_payment_term("_Test Payment Term 2 for Dunning")
if not frappe.db.exists("Payment Terms Template", "_Test 50-50 for Dunning"):
frappe.get_doc(
{
"doctype": "Payment Terms Template",
"template_name": "_Test 50-50 for Dunning",
"allocate_payment_based_on_payment_terms": 1,
"terms": [
{
"doctype": "Payment Terms Template Detail",
"payment_term": "_Test Payment Term 1 for Dunning",
"invoice_portion": 50.00,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 5,
},
{
"doctype": "Payment Terms Template Detail",
"payment_term": "_Test Payment Term 2 for Dunning",
"invoice_portion": 50.00,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 10,
},
],
}
).insert()

View File

@ -1,8 +1,24 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Dunning Type', { frappe.ui.form.on("Dunning Type", {
// refresh: function(frm) { 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,
},
};
});
},
}); });

View File

@ -1,23 +1,26 @@
{ {
"actions": [], "actions": [],
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:dunning_type", "beta": 1,
"creation": "2019-12-04 04:59:08.003664", "creation": "2019-12-04 04:59:08.003664",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"dunning_type", "dunning_type",
"overdue_interval_section", "is_default",
"start_day", "column_break_3",
"column_break_4", "company",
"end_day",
"section_break_6", "section_break_6",
"dunning_fee", "dunning_fee",
"column_break_8", "column_break_8",
"rate_of_interest", "rate_of_interest",
"text_block_section", "text_block_section",
"dunning_letter_text" "dunning_letter_text",
"section_break_9",
"income_account",
"column_break_13",
"cost_center"
], ],
"fields": [ "fields": [
{ {
@ -45,10 +48,6 @@
"fieldtype": "Table", "fieldtype": "Table",
"options": "Dunning Letter Text" "options": "Dunning Letter Text"
}, },
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{ {
"fieldname": "section_break_6", "fieldname": "section_break_6",
"fieldtype": "Section Break" "fieldtype": "Section Break"
@ -57,33 +56,62 @@
"fieldname": "column_break_8", "fieldname": "column_break_8",
"fieldtype": "Column Break" "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", "fieldname": "rate_of_interest",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Rate of Interest (%) Yearly" "label": "Rate of Interest (%) Yearly"
},
{
"default": "0",
"fieldname": "is_default",
"fieldtype": "Check",
"label": "Is Default"
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fieldname": "income_account",
"fieldtype": "Link",
"label": "Income Account",
"options": "Account"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
} }
], ],
"links": [], "links": [
"modified": "2020-07-15 17:14:17.835074", {
"link_doctype": "Dunning",
"link_fieldname": "dunning_type"
}
],
"modified": "2021-11-13 00:25:35.659283",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Dunning Type", "name": "Dunning Type",
"naming_rule": "By script",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {

View File

@ -2,9 +2,11 @@
# For license information, please see license.txt # For license information, please see license.txt
# import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
class DunningType(Document): class DunningType(Document):
pass def autoname(self):
company_abbr = frappe.get_value("Company", self.company, "abbr")
self.name = f"{self.dunning_type} - {company_abbr}"

View File

@ -0,0 +1,36 @@
[
{
"doctype": "Dunning Type",
"dunning_type": "_Test First Notice",
"company": "_Test Company",
"is_default": 1,
"dunning_fee": 0.0,
"rate_of_interest": 0.0,
"dunning_letter_text": [
{
"language": "en",
"body_text": "We have still not received payment for our invoice",
"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees."
}
],
"income_account": "Sales - _TC",
"cost_center": "_Test Cost Center - _TC"
},
{
"doctype": "Dunning Type",
"dunning_type": "_Test Second Notice",
"company": "_Test Company",
"is_default": 0,
"dunning_fee": 10.0,
"rate_of_interest": 10.0,
"dunning_letter_text": [
{
"language": "en",
"body_text": "We have still not received payment for our invoice",
"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees."
}
],
"income_account": "Sales - _TC",
"cost_center": "_Test Cost Center - _TC"
}
]

View File

@ -0,0 +1,170 @@
{
"actions": [],
"creation": "2021-09-15 18:34:27.172906",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"sales_invoice",
"payment_schedule",
"dunning_level",
"payment_term",
"section_break_15",
"description",
"section_break_4",
"due_date",
"overdue_days",
"mode_of_payment",
"column_break_5",
"invoice_portion",
"section_break_16",
"payment_amount",
"outstanding",
"paid_amount",
"discounted_amount",
"interest"
],
"fields": [
{
"columns": 2,
"fieldname": "payment_term",
"fieldtype": "Link",
"label": "Payment Term",
"options": "Payment Term",
"print_hide": 1,
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "section_break_15",
"fieldtype": "Section Break",
"label": "Description"
},
{
"columns": 2,
"fetch_from": "payment_term.description",
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description",
"read_only": 1
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break"
},
{
"columns": 2,
"fieldname": "due_date",
"fieldtype": "Date",
"label": "Due Date",
"read_only": 1
},
{
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
"options": "Mode of Payment",
"read_only": 1
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"columns": 2,
"fieldname": "invoice_portion",
"fieldtype": "Percent",
"label": "Invoice Portion",
"read_only": 1
},
{
"columns": 2,
"fieldname": "payment_amount",
"fieldtype": "Currency",
"label": "Payment Amount",
"options": "currency",
"read_only": 1
},
{
"fetch_from": "payment_amount",
"fieldname": "outstanding",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Outstanding",
"options": "currency",
"read_only": 1
},
{
"depends_on": "paid_amount",
"fieldname": "paid_amount",
"fieldtype": "Currency",
"label": "Paid Amount",
"options": "currency"
},
{
"default": "0",
"depends_on": "discounted_amount",
"fieldname": "discounted_amount",
"fieldtype": "Currency",
"label": "Discounted Amount",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "sales_invoice",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Sales Invoice",
"options": "Sales Invoice",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "payment_schedule",
"fieldtype": "Data",
"label": "Payment Schedule",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "overdue_days",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Overdue Days",
"read_only": 1
},
{
"default": "1",
"fieldname": "dunning_level",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Dunning Level",
"read_only": 1
},
{
"fieldname": "section_break_16",
"fieldtype": "Section Break"
},
{
"fieldname": "interest",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Interest",
"options": "currency",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-09-23 13:48:27.898830",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Overdue Payment",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class OverduePayment(Document):
pass

View File

@ -2054,28 +2054,27 @@ def get_payment_entry(
pe.append("references", reference) pe.append("references", reference)
else: else:
if dt == "Dunning": 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( pe.append(
"references", "deductions",
{ {
"reference_doctype": "Sales Invoice", "account": doc.income_account,
"reference_name": doc.get("sales_invoice"), "cost_center": doc.cost_center,
"bill_no": doc.get("bill_no"), "amount": -1 * doc.dunning_amount,
"due_date": doc.get("due_date"), "description": _("Interest and/or dunning fee"),
"total_amount": doc.get("outstanding_amount"),
"outstanding_amount": doc.get("outstanding_amount"),
"allocated_amount": doc.get("outstanding_amount"),
},
)
pe.append(
"references",
{
"reference_doctype": dt,
"reference_name": dn,
"bill_no": doc.get("bill_no"),
"due_date": doc.get("due_date"),
"total_amount": doc.get("dunning_amount"),
"outstanding_amount": doc.get("dunning_amount"),
"allocated_amount": doc.get("dunning_amount"),
}, },
) )
else: else:
@ -2169,8 +2168,10 @@ def set_party_account_currency(dt, party_account, doc):
def set_payment_type(dt, doc): def set_payment_type(dt, doc):
if ( 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): or (dt == "Purchase Invoice" and doc.outstanding_amount < 0)
or dt == "Dunning"
):
payment_type = "Receive" payment_type = "Receive"
else: else:
payment_type = "Pay" payment_type = "Pay"

View File

@ -142,9 +142,15 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
cur_frm.events.create_invoice_discounting(cur_frm); cur_frm.events.create_invoice_discounting(cur_frm);
}, __('Create')); }, __('Create'));
if (doc.due_date < frappe.datetime.get_today()) { const payment_is_overdue = doc.payment_schedule.map(
cur_frm.add_custom_button(__('Dunning'), function() { row => Date.parse(row.due_date) < Date.now()
cur_frm.events.create_dunning(cur_frm); ).reduce(
(prev, current) => prev || current
);
if (payment_is_overdue) {
this.frm.add_custom_button(__('Dunning'), () => {
this.frm.events.create_dunning(this.frm);
}, __('Create')); }, __('Create'));
} }
} }

View File

@ -2516,55 +2516,49 @@ def get_mode_of_payment_info(mode_of_payment, company):
@frappe.whitelist() @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 frappe.model.mapper import get_mapped_doc
from erpnext.accounts.doctype.dunning.dunning import ( def postprocess_dunning(source, target):
calculate_interest_and_amount, from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text
get_dunning_letter_text,
)
def set_missing_values(source, target): dunning_type = frappe.db.exists("Dunning Type", {"is_default": 1, "company": source.company})
target.sales_invoice = source_name if dunning_type:
target.outstanding_amount = source.outstanding_amount dunning_type = frappe.get_doc("Dunning Type", dunning_type)
overdue_days = (getdate(target.posting_date) - getdate(source.due_date)).days
target.overdue_days = overdue_days
if frappe.db.exists(
"Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]}
):
dunning_type = frappe.get_doc(
"Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]}
)
target.dunning_type = dunning_type.name target.dunning_type = dunning_type.name
target.rate_of_interest = dunning_type.rate_of_interest target.rate_of_interest = dunning_type.rate_of_interest
target.dunning_fee = dunning_type.dunning_fee target.dunning_fee = dunning_type.dunning_fee
letter_text = get_dunning_letter_text(dunning_type=dunning_type.name, doc=target.as_dict()) target.income_account = dunning_type.income_account
target.cost_center = dunning_type.cost_center
letter_text = get_dunning_letter_text(
dunning_type=dunning_type.name, doc=target.as_dict(), language=source.language
)
if letter_text: if letter_text:
target.body_text = letter_text.get("body_text") target.body_text = letter_text.get("body_text")
target.closing_text = letter_text.get("closing_text") target.closing_text = letter_text.get("closing_text")
target.language = letter_text.get("language") target.language = letter_text.get("language")
amounts = calculate_interest_and_amount(
target.outstanding_amount,
target.rate_of_interest,
target.dunning_fee,
target.overdue_days,
)
target.interest_amount = amounts.get("interest_amount")
target.dunning_amount = amounts.get("dunning_amount")
target.grand_total = amounts.get("grand_total")
doclist = get_mapped_doc( target.validate()
"Sales Invoice",
source_name, return get_mapped_doc(
{ from_doctype="Sales Invoice",
from_docname=source_name,
target_doc=target_doc,
table_maps={
"Sales Invoice": { "Sales Invoice": {
"doctype": "Dunning", "doctype": "Dunning",
} "field_map": {"customer_address": "customer_address", "parent": "sales_invoice"},
},
"Payment Schedule": {
"doctype": "Overdue Payment",
"field_map": {"name": "payment_schedule", "parent": "sales_invoice"},
"condition": lambda doc: doc.outstanding > 0 and getdate(doc.due_date) < getdate(),
},
}, },
target_doc, postprocess=postprocess_dunning,
set_missing_values, ignore_permissions=ignore_permissions,
) )
return doclist
def check_if_return_invoice_linked_with_payment_entry(self): def check_if_return_invoice_linked_with_payment_entry(self):

View File

@ -1,4 +1,5 @@
{ {
"absolute_value": 0,
"align_labels_right": 0, "align_labels_right": 0,
"creation": "2019-12-11 04:37:14.012805", "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", "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, "docstatus": 0,
"doctype": "Print Format", "doctype": "Print Format",
"font": "Arial", "font": "Arial",
"format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"<div></div>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<b>{{doc.customer_name}}</b> <br />\\n{{doc.address_display}}\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<div style=\\\"text-align:left;\\\">\\n<div style=\\\"font-size:24px; text-transform:uppercase;\\\">{{_(doc.dunning_type)}}</div>\\n<div style=\\\"font-size:16px;padding-bottom:5px;\\\">{{ doc.name }}</div>\\n</div>\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldname\": \"sales_invoice\", \"print_hide\": 0, \"label\": \"Sales Invoice\"}, {\"fieldname\": \"due_date\", \"print_hide\": 0, \"label\": \"Due Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"body_text\", \"print_hide\": 0, \"label\": \"Body Text\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<table class=\\\"table table-borderless table-data\\\">\\n <tbody>\\n <tr>\\n <th>{{_(\\\"Description\\\")}}</th>\\n\\t <th style=\\\"text-align: right;\\\">{{_(\\\"Amount\\\")}}</th>\\n </tr>\\n <tr>\\n <td>\\n {{_(\\\"Outstanding Amount\\\")}}\\n </td>\\n <td style=\\\"text-align: right;\\\">\\n {{doc.get_formatted(\\\"outstanding_amount\\\")}}\\n </td>\\n </tr>\\n {%if doc.rate_of_interest > 0%}\\n <tr>\\n <td>\\n {{_(\\\"Interest \\\")}} {{doc.rate_of_interest}}% p.a. ({{doc.overdue_days}} {{_(\\\"days\\\")}})\\n </td>\\n <td style=\\\"text-align: right;\\\">\\n {{doc.get_formatted(\\\"interest_amount\\\")}}\\n </td>\\n </tr>\\n {% endif %}\\n {%if doc.dunning_fee > 0%}\\n <tr>\\n <td>\\n {{_(\\\"Dunning Fee\\\")}}\\n </td>\\n <td style=\\\"text-align: right;\\\">\\n {{doc.get_formatted(\\\"dunning_fee\\\")}}\\n </td>\\n </tr>\\n {% endif %}\\n </tbody>\\n</table>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"\\n<div class=\\\"row total\\\" style =\\\"margin-right: 0px;\\\">\\n\\t\\t<div class=\\\"col-xs-5\\\">\\n\\t\\t\\t<b>{{_(\\\"Grand Total\\\")}}</b></div>\\n\\t\\t<div class=\\\"col-xs-7 text-right\\\" style=\\\"padding-right: 4px;\\\">\\n\\t\\t\\t<b>{{doc.get_formatted(\\\"grand_total\\\")}}</b>\\n\\t\\t</div>\\n</div>\\n\\n\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"closing_text\", \"print_hide\": 0, \"label\": \"Closing Text\"}]", "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"<div></div>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<b>{{doc.customer_name}}</b> <br />\\n{{doc.address_display}}\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<div style=\\\"text-align:left;\\\">\\n<div style=\\\"font-size:24px; text-transform:uppercase;\\\">{{_(doc.dunning_type)}}</div>\\n<div style=\\\"font-size:16px;padding-bottom:5px;\\\">{{ doc.name }}</div>\\n</div>\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"body_text\", \"print_hide\": 0, \"label\": \"Body Text\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"overdue_payments\", \"print_hide\": 0, \"label\": \"Overdue Payments\", \"visible_columns\": [{\"fieldname\": \"sales_invoice\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"dunning_level\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"due_date\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"overdue_days\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"invoice_portion\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"outstanding\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"interest\", \"print_width\": \"\", \"print_hide\": 0}]}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"total_outstanding\", \"print_hide\": 0, \"label\": \"Total Outstanding\"}, {\"fieldname\": \"dunning_fee\", \"print_hide\": 0, \"label\": \"Dunning Fee\"}, {\"fieldname\": \"total_interest\", \"print_hide\": 0, \"label\": \"Total Interest\"}, {\"fieldname\": \"grand_total\", \"print_hide\": 0, \"label\": \"Grand Total\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"closing_text\", \"print_hide\": 0, \"label\": \"Closing Text\"}]",
"idx": 0, "idx": 0,
"line_breaks": 0, "line_breaks": 0,
"modified": "2020-07-14 18:25:44.348207", "modified": "2021-09-30 10:22:02.603871",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Dunning Letter", "name": "Dunning Letter",

View File

@ -358,6 +358,7 @@ doc_events = {
"erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status",
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning",
], ],
"on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
"on_trash": "erpnext.regional.check_deletion_permission", "on_trash": "erpnext.regional.check_deletion_permission",
}, },
"Address": { "Address": {

View File

@ -336,3 +336,4 @@ erpnext.patches.v14_0.set_report_in_process_SOA
erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users
execute:frappe.defaults.clear_default("fiscal_year") execute:frappe.defaults.clear_default("fiscal_year")
erpnext.patches.v15_0.remove_exotel_integration erpnext.patches.v15_0.remove_exotel_integration
erpnext.patches.v14_0.single_to_multi_dunning

View File

@ -0,0 +1,49 @@
import frappe
from erpnext.accounts.general_ledger import make_reverse_gl_entries
def execute():
frappe.reload_doc("accounts", "doctype", "overdue_payment")
frappe.reload_doc("accounts", "doctype", "dunning")
all_dunnings = frappe.get_all("Dunning", filters={"docstatus": ("!=", 2)}, pluck="name")
for dunning_name in all_dunnings:
dunning = frappe.get_doc("Dunning", dunning_name)
if not dunning.sales_invoice:
# nothing we can do
continue
if dunning.overdue_payments:
# something's already here, doesn't need patching
continue
payment_schedules = frappe.get_all(
"Payment Schedule",
filters={"parent": dunning.sales_invoice},
fields=[
"parent as sales_invoice",
"name as payment_schedule",
"payment_term",
"due_date",
"invoice_portion",
"payment_amount",
# at the time of creating this dunning, the full amount was outstanding
"payment_amount as outstanding",
"'0' as paid_amount",
"discounted_amount",
],
)
dunning.extend("overdue_payments", payment_schedules)
dunning.validate()
dunning.flags.ignore_validate_update_after_submit = True
dunning.save()
if dunning.status != "Resolved":
# With the new logic, dunning amount gets recorded as additional income
# at time of payment. We don't want to record the dunning amount twice,
# so we reverse previous GL Entries that recorded the dunning amount at
# time of submission of the Dunning.
make_reverse_gl_entries(voucher_type="Dunning", voucher_no=dunning.name)

View File

@ -3085,9 +3085,9 @@ Total Leaves,insgesamt Blätter,
Total Order Considered,Geschätzte Summe der Bestellungen, Total Order Considered,Geschätzte Summe der Bestellungen,
Total Order Value,Gesamtbestellwert, Total Order Value,Gesamtbestellwert,
Total Outgoing,Summe Auslieferungen, Total Outgoing,Summe Auslieferungen,
Total Outstanding,Absolut aussergewöhnlich, Total Outstanding,Summe ausstehende Beträge,
Total Outstanding Amount,Offener Gesamtbetrag, Total Outstanding Amount,Summe ausstehende Beträge,
Total Outstanding: {0},Gesamtsumme: {0}, Total Outstanding: {0},Summe ausstehende Beträge: {0},
Total Paid Amount,Summe gezahlte Beträge, 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 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, Total Payments,Gesamtzahlungen,
@ -8555,13 +8555,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, Enable Distributed Cost Center,Aktivieren Sie die verteilte Kostenstelle,
Distributed Cost Center,Verteilte Kostenstelle, Distributed Cost Center,Verteilte Kostenstelle,
Dunning,Mahnung, Dunning,Mahnung,
Dunning Level,Mahnstufe,
DUNN-.MM.-.YY.-,DUNN-.MM .-. YY.-, DUNN-.MM.-.YY.-,DUNN-.MM .-. YY.-,
Overdue Days,Überfällige Tage, Overdue Days,Überfällige Tage,
Dunning Type,Mahnart, Dunning Type,Mahnart,
Dunning Fee,Mahngebühr, Dunning Fee,Mahngebühr,
Dunning Amount,Mahnbetrag, Dunning Amount,Mahnbetrag,
Resolved,Aufgelöst, Resolved,Geklärt,
Unresolved,Ungelöst, Unresolved,Ungeklärt,
Printing Setting,Druckeinstellung, Printing Setting,Druckeinstellung,
Body Text,Hauptteil, Body Text,Hauptteil,
Closing Text,Text schließen, Closing Text,Text schließen,
@ -8741,7 +8742,7 @@ Company {0} already exists. Continuing will overwrite the Company and Chart of A
Meta Data,Metadaten, Meta Data,Metadaten,
Unresolve,Auflösen, Unresolve,Auflösen,
Create Document,Dokument erstellen, Create Document,Dokument erstellen,
Mark as unresolved,Als ungelöst markieren, Mark as unresolved,Als ungeklärt markieren,
TaxJar Settings,TaxJar-Einstellungen, TaxJar Settings,TaxJar-Einstellungen,
Sandbox Mode,Sandbox-Modus, Sandbox Mode,Sandbox-Modus,
Enable Tax Calculation,Steuerberechnung aktivieren, Enable Tax Calculation,Steuerberechnung aktivieren,

Can't render this file because it is too large.