Merge pull request #35689 from marination/payments-based-dunning
feat: Payments based dunning
This commit is contained in:
commit
3bc79eebe3
@ -1,13 +1,14 @@
|
||||
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Dunning", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("sales_invoice", () => {
|
||||
frm.set_query("sales_invoice", "overdue_payments", () => {
|
||||
return {
|
||||
filters: {
|
||||
docstatus: 1,
|
||||
company: frm.doc.company,
|
||||
customer: frm.doc.customer,
|
||||
outstanding_amount: [">", 0],
|
||||
status: "Overdue"
|
||||
},
|
||||
@ -22,14 +23,24 @@ 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) {
|
||||
frm.set_df_property("company", "read_only", frm.doc.__islocal ? 0 : 1);
|
||||
frm.set_df_property(
|
||||
"sales_invoice",
|
||||
"read_only",
|
||||
frm.doc.__islocal ? 0 : 1
|
||||
);
|
||||
if (frm.doc.docstatus === 1 && frm.doc.status === "Unresolved") {
|
||||
frm.add_custom_button(__("Resolve"), () => {
|
||||
frm.set_value("status", "Resolved");
|
||||
@ -40,42 +51,111 @@ 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() {
|
||||
frappe.route_options = {
|
||||
"voucher_no": frm.doc.name,
|
||||
"from_date": frm.doc.posting_date,
|
||||
"to_date": frm.doc.posting_date,
|
||||
"company": frm.doc.company,
|
||||
"show_cancelled_entries": frm.doc.docstatus === 2
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
}, __('View'));
|
||||
if (frm.doc.docstatus === 0) {
|
||||
frm.add_custom_button(__("Fetch Overdue Payments"), () => {
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
|
||||
source_doctype: "Sales Invoice",
|
||||
date_field: "due_date",
|
||||
target: frm,
|
||||
setters: {
|
||||
customer: frm.doc.customer || undefined,
|
||||
},
|
||||
get_query_filters: {
|
||||
docstatus: 1,
|
||||
status: "Overdue",
|
||||
company: frm.doc.company
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "payment_schedule",
|
||||
child_columns: ["due_date", "outstanding"],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
frappe.dynamic_link = { doc: frm.doc, fieldname: 'customer', doctype: 'Customer' };
|
||||
|
||||
frm.toggle_display("customer_name", (frm.doc.customer_name && frm.doc.customer_name !== frm.doc.customer));
|
||||
},
|
||||
overdue_days: function (frm) {
|
||||
frappe.db.get_value(
|
||||
"Dunning Type",
|
||||
{
|
||||
start_day: ["<", frm.doc.overdue_days],
|
||||
end_day: [">=", frm.doc.overdue_days],
|
||||
},
|
||||
"dunning_type",
|
||||
(r) => {
|
||||
if (r) {
|
||||
frm.set_value("dunning_type", r.dunning_type);
|
||||
} else {
|
||||
frm.set_value("dunning_type", "");
|
||||
frm.set_value("rate_of_interest", "");
|
||||
frm.set_value("dunning_fee", "");
|
||||
// When multiple companies are set up. in case company name is changed set default company address
|
||||
company: function (frm) {
|
||||
if (frm.doc.company) {
|
||||
frappe.call({
|
||||
method: "erpnext.setup.doctype.company.company.get_default_company_address",
|
||||
args: { name: frm.doc.company, existing_address: frm.doc.company_address || "" },
|
||||
debounce: 2000,
|
||||
callback: function (r) {
|
||||
frm.set_value("company_address", r && r.message || "");
|
||||
}
|
||||
});
|
||||
|
||||
if (frm.fields_dict.currency) {
|
||||
const company_currency = erpnext.get_currency(frm.doc.company);
|
||||
|
||||
if (!frm.doc.currency) {
|
||||
frm.set_value("currency", company_currency);
|
||||
}
|
||||
|
||||
if (frm.doc.currency == company_currency) {
|
||||
frm.set_value("conversion_rate", 1.0);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const company_doc = frappe.get_doc(":Company", frm.doc.company);
|
||||
if (company_doc.default_letter_head) {
|
||||
if (frm.fields_dict.letter_head) {
|
||||
frm.set_value("letter_head", company_doc.default_letter_head);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
currency: function (frm) {
|
||||
// this.set_dynamic_labels();
|
||||
const company_currency = erpnext.get_currency(frm.doc.company);
|
||||
// Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc
|
||||
if (frm.doc.currency && frm.doc.currency !== company_currency) {
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
args: {
|
||||
transaction_date: frm.doc.posting_date,
|
||||
from_currency: frm.doc.currency,
|
||||
to_currency: company_currency,
|
||||
args: "for_selling"
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __("Fetching exchange rates ..."),
|
||||
callback: function(r) {
|
||||
const exchange_rate = flt(r.message);
|
||||
if (exchange_rate != frm.doc.conversion_rate) {
|
||||
frm.set_value("conversion_rate", exchange_rate);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
frm.trigger("conversion_rate");
|
||||
}
|
||||
},
|
||||
customer: (frm) => {
|
||||
erpnext.utils.get_party_details(frm);
|
||||
},
|
||||
conversion_rate: function (frm) {
|
||||
if (frm.doc.currency === erpnext.get_currency(frm.doc.company)) {
|
||||
frm.set_value("conversion_rate", 1.0);
|
||||
}
|
||||
|
||||
// Make read only if Accounts Settings doesn't allow stale rates
|
||||
frm.set_df_property("conversion_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
|
||||
},
|
||||
customer_address: function (frm) {
|
||||
erpnext.utils.get_address_display(frm, "customer_address");
|
||||
},
|
||||
company_address: function (frm) {
|
||||
erpnext.utils.get_address_display(frm, "company_address");
|
||||
},
|
||||
dunning_type: function (frm) {
|
||||
frm.trigger("get_dunning_letter_text");
|
||||
@ -87,7 +167,7 @@ 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,
|
||||
@ -106,49 +186,62 @@ 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");
|
||||
},
|
||||
dunning_fee: function (frm) {
|
||||
frm.trigger("calculate_interest_and_amount");
|
||||
frm.trigger("calculate_totals");
|
||||
},
|
||||
sales_invoice: function (frm) {
|
||||
frm.trigger("calculate_overdue_days");
|
||||
overdue_payments_add: function (frm) {
|
||||
frm.trigger("calculate_totals");
|
||||
},
|
||||
overdue_payments_remove: function (frm) {
|
||||
frm.trigger("calculate_totals");
|
||||
},
|
||||
calculate_overdue_days: function (frm) {
|
||||
if (frm.doc.posting_date && frm.doc.due_date) {
|
||||
const overdue_days = moment(frm.doc.posting_date).diff(
|
||||
frm.doc.due_date,
|
||||
"days"
|
||||
);
|
||||
frm.set_value("overdue_days", overdue_days);
|
||||
}
|
||||
frm.doc.overdue_payments.forEach((row) => {
|
||||
if (frm.doc.posting_date && row.due_date) {
|
||||
const overdue_days = moment(frm.doc.posting_date).diff(
|
||||
row.due_date,
|
||||
"days"
|
||||
);
|
||||
frappe.model.set_value(row.doctype, row.name, "overdue_days", overdue_days);
|
||||
}
|
||||
});
|
||||
},
|
||||
calculate_interest_and_amount: function (frm) {
|
||||
const interest_per_year = frm.doc.outstanding_amount * frm.doc.rate_of_interest / 100;
|
||||
const interest_amount = flt((interest_per_year * cint(frm.doc.overdue_days)) / 365 || 0, precision('interest_amount'));
|
||||
const dunning_amount = flt(interest_amount + frm.doc.dunning_fee, precision('dunning_amount'));
|
||||
const grand_total = flt(frm.doc.outstanding_amount + dunning_amount, precision('grand_total'));
|
||||
frm.set_value("interest_amount", interest_amount);
|
||||
frm.set_value("dunning_amount", dunning_amount);
|
||||
frm.set_value("grand_total", grand_total);
|
||||
calculate_interest: function (frm) {
|
||||
frm.doc.overdue_payments.forEach((row) => {
|
||||
const interest_per_day = frm.doc.rate_of_interest / 100 / 365;
|
||||
const interest = flt((interest_per_day * row.overdue_days * row.outstanding), precision("interest", row));
|
||||
frappe.model.set_value(row.doctype, row.name, "interest", interest);
|
||||
});
|
||||
},
|
||||
calculate_totals: function (frm) {
|
||||
const total_interest = frm.doc.overdue_payments
|
||||
.reduce((prev, cur) => prev + cur.interest, 0);
|
||||
const total_outstanding = frm.doc.overdue_payments
|
||||
.reduce((prev, cur) => prev + cur.outstanding, 0);
|
||||
const dunning_amount = total_interest + frm.doc.dunning_fee;
|
||||
const base_dunning_amount = dunning_amount * frm.doc.conversion_rate;
|
||||
const grand_total = total_outstanding + dunning_amount;
|
||||
|
||||
function setWithPrecison(field, value) {
|
||||
frm.set_value(field, flt(value, precision(field)));
|
||||
}
|
||||
|
||||
setWithPrecison("total_outstanding", total_outstanding);
|
||||
setWithPrecison("total_interest", total_interest);
|
||||
setWithPrecison("dunning_amount", dunning_amount);
|
||||
setWithPrecison("base_dunning_amount", base_dunning_amount);
|
||||
setWithPrecison("grand_total", grand_total);
|
||||
},
|
||||
make_payment_entry: function (frm) {
|
||||
return frappe.call({
|
||||
method:
|
||||
"erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry",
|
||||
"erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry",
|
||||
args: {
|
||||
dt: frm.doc.doctype,
|
||||
dn: frm.doc.name,
|
||||
@ -160,3 +253,9 @@ frappe.ui.form.on("Dunning", {
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Overdue Payment", {
|
||||
interest: function (frm) {
|
||||
frm.trigger("calculate_totals");
|
||||
}
|
||||
});
|
@ -2,49 +2,60 @@
|
||||
"actions": [],
|
||||
"allow_events_in_timeline": 1,
|
||||
"autoname": "naming_series:",
|
||||
"beta": 1,
|
||||
"creation": "2019-07-05 16:34:31.013238",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"naming_series",
|
||||
"sales_invoice",
|
||||
"customer",
|
||||
"customer_name",
|
||||
"outstanding_amount",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
"column_break_3",
|
||||
"company",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"due_date",
|
||||
"overdue_days",
|
||||
"status",
|
||||
"section_break_9",
|
||||
"currency",
|
||||
"column_break_11",
|
||||
"conversion_rate",
|
||||
"address_and_contact_section",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"column_break_16",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"column_break_18",
|
||||
"company_address_display",
|
||||
"section_break_6",
|
||||
"dunning_type",
|
||||
"dunning_fee",
|
||||
"column_break_8",
|
||||
"rate_of_interest",
|
||||
"interest_amount",
|
||||
"section_break_12",
|
||||
"dunning_amount",
|
||||
"grand_total",
|
||||
"income_account",
|
||||
"overdue_payments",
|
||||
"section_break_28",
|
||||
"total_interest",
|
||||
"dunning_fee",
|
||||
"column_break_17",
|
||||
"status",
|
||||
"printing_setting_section",
|
||||
"dunning_amount",
|
||||
"base_dunning_amount",
|
||||
"section_break_32",
|
||||
"spacer",
|
||||
"column_break_33",
|
||||
"total_outstanding",
|
||||
"grand_total",
|
||||
"printing_settings_section",
|
||||
"language",
|
||||
"body_text",
|
||||
"column_break_22",
|
||||
"letter_head",
|
||||
"closing_text",
|
||||
"accounting_details_section",
|
||||
"income_account",
|
||||
"column_break_48",
|
||||
"cost_center",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
@ -60,32 +71,17 @@
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"label": "Series",
|
||||
"options": "DUNN-.MM.-.YY.-"
|
||||
"options": "DUNN-.MM.-.YY.-",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sales_invoice",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Sales Invoice",
|
||||
"options": "Sales Invoice",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.customer_name",
|
||||
"fetch_from": "customer.customer_name",
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"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 +90,8 @@
|
||||
"default": "Today",
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "overdue_days",
|
||||
"fieldtype": "Int",
|
||||
"label": "Overdue Days",
|
||||
"read_only": 1
|
||||
"label": "Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
@ -112,16 +103,7 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Dunning Type",
|
||||
"options": "Dunning Type",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "interest_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Interest Amount",
|
||||
"precision": "2",
|
||||
"read_only": 1
|
||||
"options": "Dunning Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_8",
|
||||
@ -134,6 +116,7 @@
|
||||
"fieldname": "dunning_fee",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Dunning Fee",
|
||||
"options": "currency",
|
||||
"precision": "2"
|
||||
},
|
||||
{
|
||||
@ -144,36 +127,24 @@
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "printing_setting_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Printing Setting"
|
||||
},
|
||||
{
|
||||
"fieldname": "language",
|
||||
"fieldtype": "Link",
|
||||
"label": "Print Language",
|
||||
"options": "Language"
|
||||
"options": "Language",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "letter_head",
|
||||
"fieldtype": "Link",
|
||||
"label": "Letter Head",
|
||||
"options": "Letter Head"
|
||||
"options": "Letter Head",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_22",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.currency",
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Currency",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
@ -183,14 +154,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",
|
||||
@ -201,13 +164,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,26 +178,24 @@
|
||||
"label": "Rate of Interest (%) Yearly"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "address_and_contact_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address and Contact"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.address_display",
|
||||
"fieldname": "address_display",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Address",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.contact_display",
|
||||
"fieldname": "contact_display",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Contact",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.contact_mobile",
|
||||
"fieldname": "contact_mobile",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Mobile No",
|
||||
@ -249,18 +203,12 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_18",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.company_address_display",
|
||||
"fieldname": "company_address_display",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Company Address",
|
||||
"label": "Company Address Display",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.contact_email",
|
||||
"fieldname": "contact_email",
|
||||
"fieldtype": "Data",
|
||||
"label": "Contact Email",
|
||||
@ -268,18 +216,18 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.customer",
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer",
|
||||
"options": "Customer",
|
||||
"read_only": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Grand Total",
|
||||
"options": "currency",
|
||||
"precision": "2",
|
||||
"read_only": 1
|
||||
},
|
||||
@ -290,33 +238,150 @@
|
||||
"fieldtype": "Select",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "Draft\nResolved\nUnresolved\nCancelled"
|
||||
},
|
||||
{
|
||||
"fieldname": "dunning_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Dunning Amount",
|
||||
"options": "Draft\nResolved\nUnresolved\nCancelled",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "For dunning fee and interest",
|
||||
"fetch_from": "dunning_type.income_account",
|
||||
"fieldname": "income_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Income Account",
|
||||
"options": "Account"
|
||||
"options": "Account",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "Conversion Rate",
|
||||
"label": "Conversion Rate"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "base_dunning_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Dunning Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_48",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-03 16:24:01.677026",
|
||||
"modified": "2023-06-15 15:46:53.865712",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dunning",
|
||||
|
@ -1,131 +1,150 @@
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
"""
|
||||
# Accounting
|
||||
|
||||
1. Payment of outstanding invoices with dunning amount
|
||||
|
||||
- Debit full amount to bank
|
||||
- Credit invoiced amount to receivables
|
||||
- Credit dunning amount to interest and similar revenue
|
||||
|
||||
-> Resolves dunning automatically
|
||||
"""
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.utils import cint, flt, getdate
|
||||
from frappe import _
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
from frappe.utils import getdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
|
||||
|
||||
class Dunning(AccountsController):
|
||||
def validate(self):
|
||||
self.validate_overdue_days()
|
||||
self.validate_amount()
|
||||
if not self.income_account:
|
||||
self.income_account = frappe.get_cached_value("Company", self.company, "default_income_account")
|
||||
self.validate_same_currency()
|
||||
self.validate_overdue_payments()
|
||||
self.validate_totals()
|
||||
self.set_party_details()
|
||||
self.set_dunning_level()
|
||||
|
||||
def validate_overdue_days(self):
|
||||
self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0
|
||||
def validate_same_currency(self):
|
||||
"""
|
||||
Throw an error if invoice currency differs from dunning currency.
|
||||
"""
|
||||
for row in self.overdue_payments:
|
||||
invoice_currency = frappe.get_value("Sales Invoice", row.sales_invoice, "currency")
|
||||
if invoice_currency != self.currency:
|
||||
frappe.throw(
|
||||
_(
|
||||
"The currency of invoice {} ({}) is different from the currency of this dunning ({})."
|
||||
).format(row.sales_invoice, invoice_currency, self.currency)
|
||||
)
|
||||
|
||||
def validate_amount(self):
|
||||
amounts = calculate_interest_and_amount(
|
||||
self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days
|
||||
def validate_overdue_payments(self):
|
||||
daily_interest = self.rate_of_interest / 100 / 365
|
||||
|
||||
for row in self.overdue_payments:
|
||||
row.overdue_days = (getdate(self.posting_date) - getdate(row.due_date)).days or 0
|
||||
row.interest = row.outstanding * daily_interest * row.overdue_days
|
||||
|
||||
def validate_totals(self):
|
||||
self.total_outstanding = sum(row.outstanding for row in self.overdue_payments)
|
||||
self.total_interest = sum(row.interest for row in self.overdue_payments)
|
||||
self.dunning_amount = self.total_interest + self.dunning_fee
|
||||
self.base_dunning_amount = self.dunning_amount * self.conversion_rate
|
||||
self.grand_total = self.total_outstanding + self.dunning_amount
|
||||
|
||||
def set_party_details(self):
|
||||
from erpnext.accounts.party import _get_party_details
|
||||
|
||||
party_details = _get_party_details(
|
||||
self.customer,
|
||||
ignore_permissions=self.flags.ignore_permissions,
|
||||
doctype=self.doctype,
|
||||
company=self.company,
|
||||
posting_date=self.get("posting_date"),
|
||||
fetch_payment_terms_template=False,
|
||||
party_address=self.customer_address,
|
||||
company_address=self.get("company_address"),
|
||||
)
|
||||
if self.interest_amount != amounts.get("interest_amount"):
|
||||
self.interest_amount = flt(amounts.get("interest_amount"), self.precision("interest_amount"))
|
||||
if self.dunning_amount != amounts.get("dunning_amount"):
|
||||
self.dunning_amount = flt(amounts.get("dunning_amount"), self.precision("dunning_amount"))
|
||||
if self.grand_total != amounts.get("grand_total"):
|
||||
self.grand_total = flt(amounts.get("grand_total"), self.precision("grand_total"))
|
||||
for field in [
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"company_address",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
]:
|
||||
self.set(field, party_details.get(field))
|
||||
|
||||
def on_submit(self):
|
||||
self.make_gl_entries()
|
||||
self.set("company_address_display", get_address_display(self.company_address))
|
||||
|
||||
def on_cancel(self):
|
||||
if self.dunning_amount:
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
def make_gl_entries(self):
|
||||
if not self.dunning_amount:
|
||||
return
|
||||
gl_entries = []
|
||||
invoice_fields = [
|
||||
"project",
|
||||
"cost_center",
|
||||
"debit_to",
|
||||
"party_account_currency",
|
||||
"conversion_rate",
|
||||
"cost_center",
|
||||
]
|
||||
inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
invoice_fields.extend(accounting_dimensions)
|
||||
|
||||
dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate)
|
||||
default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": inv.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"due_date": self.due_date,
|
||||
"against": self.income_account,
|
||||
"debit": dunning_in_company_currency,
|
||||
"debit_in_account_currency": self.dunning_amount,
|
||||
"against_voucher": self.name,
|
||||
"against_voucher_type": "Dunning",
|
||||
"cost_center": inv.cost_center or default_cost_center,
|
||||
"project": inv.project,
|
||||
def set_dunning_level(self):
|
||||
for row in self.overdue_payments:
|
||||
past_dunnings = frappe.get_all(
|
||||
"Overdue Payment",
|
||||
filters={
|
||||
"payment_schedule": row.payment_schedule,
|
||||
"parent": ("!=", row.parent),
|
||||
"docstatus": 1,
|
||||
},
|
||||
inv.party_account_currency,
|
||||
item=inv,
|
||||
)
|
||||
)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.income_account,
|
||||
"against": self.customer,
|
||||
"credit": dunning_in_company_currency,
|
||||
"cost_center": inv.cost_center or default_cost_center,
|
||||
"credit_in_account_currency": self.dunning_amount,
|
||||
"project": inv.project,
|
||||
},
|
||||
item=inv,
|
||||
)
|
||||
)
|
||||
make_gl_entries(
|
||||
gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False
|
||||
)
|
||||
row.dunning_level = len(past_dunnings) + 1
|
||||
|
||||
|
||||
def resolve_dunning(doc, state):
|
||||
"""
|
||||
Check if all payments have been made and resolve dunning, if yes. Called
|
||||
when a Payment Entry is submitted.
|
||||
"""
|
||||
for reference in doc.references:
|
||||
if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0:
|
||||
dunnings = frappe.get_list(
|
||||
"Dunning",
|
||||
filters={"sales_invoice": reference.reference_name, "status": ("!=", "Resolved")},
|
||||
ignore_permissions=True,
|
||||
)
|
||||
# Consider partial and full payments:
|
||||
# Submitting full payment: outstanding_amount will be 0
|
||||
# Submitting 1st partial payment: outstanding_amount will be the pending installment
|
||||
# Cancelling full payment: outstanding_amount will revert to total amount
|
||||
# Cancelling last partial payment: outstanding_amount will revert to pending amount
|
||||
submit_condition = reference.outstanding_amount < reference.total_amount
|
||||
cancel_condition = reference.outstanding_amount <= reference.total_amount
|
||||
|
||||
if reference.reference_doctype == "Sales Invoice" and (
|
||||
submit_condition if doc.docstatus == 1 else cancel_condition
|
||||
):
|
||||
state = "Resolved" if doc.docstatus == 2 else "Unresolved"
|
||||
dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state)
|
||||
|
||||
for dunning in dunnings:
|
||||
frappe.db.set_value("Dunning", dunning.name, "status", "Resolved")
|
||||
resolve = True
|
||||
dunning = frappe.get_doc("Dunning", dunning.get("name"))
|
||||
for overdue_payment in dunning.overdue_payments:
|
||||
outstanding_inv = frappe.get_value(
|
||||
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
|
||||
)
|
||||
outstanding_ps = frappe.get_value(
|
||||
"Payment Schedule", overdue_payment.payment_schedule, "outstanding"
|
||||
)
|
||||
resolve = False if (outstanding_ps > 0 and outstanding_inv > 0) else True
|
||||
|
||||
dunning.status = "Resolved" if resolve else "Unresolved"
|
||||
dunning.save()
|
||||
|
||||
|
||||
def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
|
||||
interest_amount = 0
|
||||
grand_total = flt(outstanding_amount) + flt(dunning_fee)
|
||||
if rate_of_interest:
|
||||
interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100
|
||||
interest_amount = (interest_per_year * cint(overdue_days)) / 365
|
||||
grand_total += flt(interest_amount)
|
||||
dunning_amount = flt(interest_amount) + flt(dunning_fee)
|
||||
return {
|
||||
"interest_amount": interest_amount,
|
||||
"grand_total": grand_total,
|
||||
"dunning_amount": dunning_amount,
|
||||
}
|
||||
def get_linked_dunnings_as_per_state(sales_invoice, state):
|
||||
dunning = frappe.qb.DocType("Dunning")
|
||||
overdue_payment = frappe.qb.DocType("Overdue Payment")
|
||||
|
||||
return (
|
||||
frappe.qb.from_(dunning)
|
||||
.join(overdue_payment)
|
||||
.on(overdue_payment.parent == dunning.name)
|
||||
.select(dunning.name)
|
||||
.where(
|
||||
(dunning.status == state)
|
||||
& (dunning.docstatus != 2)
|
||||
& (overdue_payment.sales_invoice == sales_invoice)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -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"]}],
|
||||
}
|
@ -1,162 +1,197 @@
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, nowdate, today
|
||||
|
||||
from erpnext.accounts.doctype.dunning.dunning import calculate_interest_and_amount
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
|
||||
unlink_payment_on_cancel_of_invoice,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
create_dunning as create_dunning_from_sales_invoice,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import (
|
||||
create_sales_invoice_against_cost_center,
|
||||
)
|
||||
|
||||
test_dependencies = ["Company", "Cost Center"]
|
||||
|
||||
class TestDunning(unittest.TestCase):
|
||||
|
||||
class TestDunning(FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
create_dunning_type()
|
||||
create_dunning_type_with_zero_interest_rate()
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
create_dunning_type("First Notice", fee=0.0, interest=0.0, is_default=1)
|
||||
create_dunning_type("Second Notice", fee=10.0, interest=10.0, is_default=0)
|
||||
unlink_payment_on_cancel_of_invoice()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(self):
|
||||
def tearDownClass(cls):
|
||||
unlink_payment_on_cancel_of_invoice(0)
|
||||
super().tearDownClass()
|
||||
|
||||
def test_dunning(self):
|
||||
dunning = create_dunning()
|
||||
amounts = calculate_interest_and_amount(
|
||||
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
|
||||
)
|
||||
self.assertEqual(round(amounts.get("interest_amount"), 2), 0.44)
|
||||
self.assertEqual(round(amounts.get("dunning_amount"), 2), 20.44)
|
||||
self.assertEqual(round(amounts.get("grand_total"), 2), 120.44)
|
||||
def test_dunning_without_fees(self):
|
||||
dunning = create_dunning(overdue_days=20)
|
||||
|
||||
def test_dunning_with_zero_interest_rate(self):
|
||||
dunning = create_dunning_with_zero_interest_rate()
|
||||
amounts = calculate_interest_and_amount(
|
||||
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
|
||||
)
|
||||
self.assertEqual(round(amounts.get("interest_amount"), 2), 0)
|
||||
self.assertEqual(round(amounts.get("dunning_amount"), 2), 20)
|
||||
self.assertEqual(round(amounts.get("grand_total"), 2), 120)
|
||||
self.assertEqual(round(dunning.total_outstanding, 2), 100.00)
|
||||
self.assertEqual(round(dunning.total_interest, 2), 0.00)
|
||||
self.assertEqual(round(dunning.dunning_fee, 2), 0.00)
|
||||
self.assertEqual(round(dunning.dunning_amount, 2), 0.00)
|
||||
self.assertEqual(round(dunning.grand_total, 2), 100.00)
|
||||
|
||||
def test_gl_entries(self):
|
||||
dunning = create_dunning()
|
||||
dunning.submit()
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select account, debit, credit
|
||||
from `tabGL Entry` where voucher_type='Dunning' and voucher_no=%s
|
||||
order by account asc""",
|
||||
dunning.name,
|
||||
as_dict=1,
|
||||
)
|
||||
self.assertTrue(gl_entries)
|
||||
expected_values = dict(
|
||||
(d[0], d) for d in [["Debtors - _TC", 20.44, 0.0], ["Sales - _TC", 0.0, 20.44]]
|
||||
)
|
||||
for gle in gl_entries:
|
||||
self.assertEqual(expected_values[gle.account][0], gle.account)
|
||||
self.assertEqual(expected_values[gle.account][1], gle.debit)
|
||||
self.assertEqual(expected_values[gle.account][2], gle.credit)
|
||||
def test_dunning_with_fees_and_interest(self):
|
||||
dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC")
|
||||
|
||||
def test_payment_entry(self):
|
||||
dunning = create_dunning()
|
||||
self.assertEqual(round(dunning.total_outstanding, 2), 100.00)
|
||||
self.assertEqual(round(dunning.total_interest, 2), 0.41)
|
||||
self.assertEqual(round(dunning.dunning_fee, 2), 10.00)
|
||||
self.assertEqual(round(dunning.dunning_amount, 2), 10.41)
|
||||
self.assertEqual(round(dunning.grand_total, 2), 110.41)
|
||||
|
||||
def test_dunning_with_payment_entry(self):
|
||||
dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC")
|
||||
dunning.submit()
|
||||
pe = get_payment_entry("Dunning", dunning.name)
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = nowdate()
|
||||
pe.paid_from_account_currency = dunning.currency
|
||||
pe.paid_to_account_currency = dunning.currency
|
||||
pe.source_exchange_rate = 1
|
||||
pe.target_exchange_rate = 1
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
si_doc = frappe.get_doc("Sales Invoice", dunning.sales_invoice)
|
||||
self.assertEqual(si_doc.outstanding_amount, 0)
|
||||
|
||||
for overdue_payment in dunning.overdue_payments:
|
||||
outstanding_amount = frappe.get_value(
|
||||
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
|
||||
)
|
||||
self.assertEqual(outstanding_amount, 0)
|
||||
|
||||
dunning.reload()
|
||||
self.assertEqual(dunning.status, "Resolved")
|
||||
|
||||
def test_dunning_and_payment_against_partially_due_invoice(self):
|
||||
"""
|
||||
Create SI with first installment overdue. Check impact of Dunning and Payment Entry.
|
||||
"""
|
||||
create_payment_terms_template_for_dunning()
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=add_days(today(), -1 * 6),
|
||||
qty=1,
|
||||
rate=100,
|
||||
do_not_submit=True,
|
||||
)
|
||||
sales_invoice.payment_terms_template = "_Test 50-50 for Dunning"
|
||||
sales_invoice.submit()
|
||||
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
|
||||
|
||||
self.assertEqual(len(dunning.overdue_payments), 1)
|
||||
self.assertEqual(dunning.overdue_payments[0].payment_term, "_Test Payment Term 1 for Dunning")
|
||||
|
||||
dunning.submit()
|
||||
pe = get_payment_entry("Dunning", dunning.name)
|
||||
pe.reference_no, pe.reference_date = "2", nowdate()
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
sales_invoice.load_from_db()
|
||||
dunning.load_from_db()
|
||||
|
||||
self.assertEqual(sales_invoice.status, "Partly Paid")
|
||||
self.assertEqual(sales_invoice.payment_schedule[0].outstanding, 0)
|
||||
self.assertEqual(dunning.status, "Resolved")
|
||||
|
||||
# Test impact on cancellation of PE
|
||||
pe.cancel()
|
||||
sales_invoice.reload()
|
||||
dunning.reload()
|
||||
|
||||
self.assertEqual(sales_invoice.status, "Overdue")
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
|
||||
def create_dunning():
|
||||
posting_date = add_days(today(), -20)
|
||||
due_date = add_days(today(), -15)
|
||||
def create_dunning(overdue_days, dunning_type_name=None):
|
||||
posting_date = add_days(today(), -1 * overdue_days)
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=posting_date, due_date=due_date, status="Overdue"
|
||||
posting_date=posting_date, qty=1, rate=100
|
||||
)
|
||||
dunning_type = frappe.get_doc("Dunning Type", "First Notice")
|
||||
dunning = frappe.new_doc("Dunning")
|
||||
dunning.sales_invoice = sales_invoice.name
|
||||
dunning.customer_name = sales_invoice.customer_name
|
||||
dunning.outstanding_amount = sales_invoice.outstanding_amount
|
||||
dunning.debit_to = sales_invoice.debit_to
|
||||
dunning.currency = sales_invoice.currency
|
||||
dunning.company = sales_invoice.company
|
||||
dunning.posting_date = nowdate()
|
||||
dunning.due_date = sales_invoice.due_date
|
||||
dunning.dunning_type = "First Notice"
|
||||
dunning.rate_of_interest = dunning_type.rate_of_interest
|
||||
dunning.dunning_fee = dunning_type.dunning_fee
|
||||
dunning.save()
|
||||
return dunning
|
||||
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
|
||||
|
||||
if dunning_type_name:
|
||||
dunning_type = frappe.get_doc("Dunning Type", dunning_type_name)
|
||||
dunning.dunning_type = dunning_type.name
|
||||
dunning.rate_of_interest = dunning_type.rate_of_interest
|
||||
dunning.dunning_fee = dunning_type.dunning_fee
|
||||
dunning.income_account = dunning_type.income_account
|
||||
dunning.cost_center = dunning_type.cost_center
|
||||
|
||||
return dunning.save()
|
||||
|
||||
|
||||
def create_dunning_with_zero_interest_rate():
|
||||
posting_date = add_days(today(), -20)
|
||||
due_date = add_days(today(), -15)
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=posting_date, due_date=due_date, status="Overdue"
|
||||
)
|
||||
dunning_type = frappe.get_doc("Dunning Type", "First Notice with 0% Rate of Interest")
|
||||
dunning = frappe.new_doc("Dunning")
|
||||
dunning.sales_invoice = sales_invoice.name
|
||||
dunning.customer_name = sales_invoice.customer_name
|
||||
dunning.outstanding_amount = sales_invoice.outstanding_amount
|
||||
dunning.debit_to = sales_invoice.debit_to
|
||||
dunning.currency = sales_invoice.currency
|
||||
dunning.company = sales_invoice.company
|
||||
dunning.posting_date = nowdate()
|
||||
dunning.due_date = sales_invoice.due_date
|
||||
dunning.dunning_type = "First Notice with 0% Rate of Interest"
|
||||
dunning.rate_of_interest = dunning_type.rate_of_interest
|
||||
dunning.dunning_fee = dunning_type.dunning_fee
|
||||
dunning.save()
|
||||
return dunning
|
||||
def create_dunning_type(title, fee, interest, is_default):
|
||||
company = "_Test Company"
|
||||
if frappe.db.exists("Dunning Type", f"{title} - _TC"):
|
||||
return
|
||||
|
||||
|
||||
def create_dunning_type():
|
||||
dunning_type = frappe.new_doc("Dunning Type")
|
||||
dunning_type.dunning_type = "First Notice"
|
||||
dunning_type.start_day = 10
|
||||
dunning_type.end_day = 20
|
||||
dunning_type.dunning_fee = 20
|
||||
dunning_type.rate_of_interest = 8
|
||||
dunning_type.dunning_type = title
|
||||
dunning_type.company = company
|
||||
dunning_type.is_default = is_default
|
||||
dunning_type.dunning_fee = fee
|
||||
dunning_type.rate_of_interest = interest
|
||||
dunning_type.income_account = get_income_account(company)
|
||||
dunning_type.cost_center = get_default_cost_center(company)
|
||||
dunning_type.append(
|
||||
"dunning_letter_text",
|
||||
{
|
||||
"language": "en",
|
||||
"body_text": "We have still not received payment for our invoice ",
|
||||
"body_text": "We have still not received payment for our invoice",
|
||||
"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.",
|
||||
},
|
||||
)
|
||||
dunning_type.save()
|
||||
dunning_type.insert()
|
||||
|
||||
|
||||
def create_dunning_type_with_zero_interest_rate():
|
||||
dunning_type = frappe.new_doc("Dunning Type")
|
||||
dunning_type.dunning_type = "First Notice with 0% Rate of Interest"
|
||||
dunning_type.start_day = 10
|
||||
dunning_type.end_day = 20
|
||||
dunning_type.dunning_fee = 20
|
||||
dunning_type.rate_of_interest = 0
|
||||
dunning_type.append(
|
||||
"dunning_letter_text",
|
||||
{
|
||||
"language": "en",
|
||||
"body_text": "We have still not received payment for our invoice ",
|
||||
"closing_text": "We kindly request that you pay the outstanding amount immediately, and late fees.",
|
||||
},
|
||||
def get_income_account(company):
|
||||
return (
|
||||
frappe.get_value("Company", company, "default_income_account")
|
||||
or frappe.get_all(
|
||||
"Account",
|
||||
filters={"is_group": 0, "company": company},
|
||||
or_filters={
|
||||
"report_type": "Profit and Loss",
|
||||
"account_type": ("in", ("Income Account", "Temporary")),
|
||||
},
|
||||
limit=1,
|
||||
pluck="name",
|
||||
)[0]
|
||||
)
|
||||
dunning_type.save()
|
||||
|
||||
|
||||
def create_payment_terms_template_for_dunning():
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_term
|
||||
|
||||
create_payment_term("_Test Payment Term 1 for Dunning")
|
||||
create_payment_term("_Test Payment Term 2 for Dunning")
|
||||
|
||||
if not frappe.db.exists("Payment Terms Template", "_Test 50-50 for Dunning"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Terms Template",
|
||||
"template_name": "_Test 50-50 for Dunning",
|
||||
"allocate_payment_based_on_payment_terms": 1,
|
||||
"terms": [
|
||||
{
|
||||
"doctype": "Payment Terms Template Detail",
|
||||
"payment_term": "_Test Payment Term 1 for Dunning",
|
||||
"invoice_portion": 50.00,
|
||||
"credit_days_based_on": "Day(s) after invoice date",
|
||||
"credit_days": 5,
|
||||
},
|
||||
{
|
||||
"doctype": "Payment Terms Template Detail",
|
||||
"payment_term": "_Test Payment Term 2 for Dunning",
|
||||
"invoice_portion": 50.00,
|
||||
"credit_days_based_on": "Day(s) after invoice date",
|
||||
"credit_days": 10,
|
||||
},
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -1,23 +1,26 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:dunning_type",
|
||||
"beta": 1,
|
||||
"creation": "2019-12-04 04:59:08.003664",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"dunning_type",
|
||||
"overdue_interval_section",
|
||||
"start_day",
|
||||
"column_break_4",
|
||||
"end_day",
|
||||
"is_default",
|
||||
"column_break_3",
|
||||
"company",
|
||||
"section_break_6",
|
||||
"dunning_fee",
|
||||
"column_break_8",
|
||||
"rate_of_interest",
|
||||
"text_block_section",
|
||||
"dunning_letter_text"
|
||||
"dunning_letter_text",
|
||||
"section_break_9",
|
||||
"income_account",
|
||||
"column_break_13",
|
||||
"cost_center"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -45,10 +48,6 @@
|
||||
"fieldtype": "Table",
|
||||
"options": "Dunning Letter Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break"
|
||||
@ -57,33 +56,62 @@
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_9",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "income_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Income Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-07-15 17:14:17.835074",
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "Dunning",
|
||||
"link_fieldname": "dunning_type"
|
||||
}
|
||||
],
|
||||
"modified": "2021-11-13 00:25:35.659283",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dunning Type",
|
||||
"naming_rule": "By script",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
@ -2,9 +2,11 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
# import frappe
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class DunningType(Document):
|
||||
pass
|
||||
def autoname(self):
|
||||
company_abbr = frappe.get_value("Company", self.company, "abbr")
|
||||
self.name = f"{self.dunning_type} - {company_abbr}"
|
||||
|
36
erpnext/accounts/doctype/dunning_type/test_records.json
Normal file
36
erpnext/accounts/doctype/dunning_type/test_records.json
Normal 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"
|
||||
}
|
||||
]
|
170
erpnext/accounts/doctype/overdue_payment/overdue_payment.json
Normal file
170
erpnext/accounts/doctype/overdue_payment/overdue_payment.json
Normal 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
|
||||
}
|
@ -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
|
@ -2054,28 +2054,27 @@ def get_payment_entry(
|
||||
pe.append("references", reference)
|
||||
else:
|
||||
if dt == "Dunning":
|
||||
for overdue_payment in doc.overdue_payments:
|
||||
pe.append(
|
||||
"references",
|
||||
{
|
||||
"reference_doctype": "Sales Invoice",
|
||||
"reference_name": overdue_payment.sales_invoice,
|
||||
"payment_term": overdue_payment.payment_term,
|
||||
"due_date": overdue_payment.due_date,
|
||||
"total_amount": overdue_payment.outstanding,
|
||||
"outstanding_amount": overdue_payment.outstanding,
|
||||
"allocated_amount": overdue_payment.outstanding,
|
||||
},
|
||||
)
|
||||
|
||||
pe.append(
|
||||
"references",
|
||||
"deductions",
|
||||
{
|
||||
"reference_doctype": "Sales Invoice",
|
||||
"reference_name": doc.get("sales_invoice"),
|
||||
"bill_no": doc.get("bill_no"),
|
||||
"due_date": doc.get("due_date"),
|
||||
"total_amount": doc.get("outstanding_amount"),
|
||||
"outstanding_amount": doc.get("outstanding_amount"),
|
||||
"allocated_amount": doc.get("outstanding_amount"),
|
||||
},
|
||||
)
|
||||
pe.append(
|
||||
"references",
|
||||
{
|
||||
"reference_doctype": dt,
|
||||
"reference_name": dn,
|
||||
"bill_no": doc.get("bill_no"),
|
||||
"due_date": doc.get("due_date"),
|
||||
"total_amount": doc.get("dunning_amount"),
|
||||
"outstanding_amount": doc.get("dunning_amount"),
|
||||
"allocated_amount": doc.get("dunning_amount"),
|
||||
"account": doc.income_account,
|
||||
"cost_center": doc.cost_center,
|
||||
"amount": -1 * doc.dunning_amount,
|
||||
"description": _("Interest and/or dunning fee"),
|
||||
},
|
||||
)
|
||||
else:
|
||||
@ -2169,8 +2168,10 @@ 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"
|
||||
|
@ -142,9 +142,15 @@ 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() {
|
||||
cur_frm.events.create_dunning(cur_frm);
|
||||
const payment_is_overdue = doc.payment_schedule.map(
|
||||
row => Date.parse(row.due_date) < Date.now()
|
||||
).reduce(
|
||||
(prev, current) => prev || current
|
||||
);
|
||||
|
||||
if (payment_is_overdue) {
|
||||
this.frm.add_custom_button(__('Dunning'), () => {
|
||||
this.frm.events.create_dunning(this.frm);
|
||||
}, __('Create'));
|
||||
}
|
||||
}
|
||||
|
@ -2516,55 +2516,49 @@ 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, "company": source.company})
|
||||
if dunning_type:
|
||||
dunning_type = frappe.get_doc("Dunning Type", dunning_type)
|
||||
target.dunning_type = dunning_type.name
|
||||
target.rate_of_interest = dunning_type.rate_of_interest
|
||||
target.dunning_fee = dunning_type.dunning_fee
|
||||
letter_text = get_dunning_letter_text(dunning_type=dunning_type.name, doc=target.as_dict())
|
||||
target.income_account = dunning_type.income_account
|
||||
target.cost_center = dunning_type.cost_center
|
||||
letter_text = get_dunning_letter_text(
|
||||
dunning_type=dunning_type.name, doc=target.as_dict(), language=source.language
|
||||
)
|
||||
|
||||
if letter_text:
|
||||
target.body_text = letter_text.get("body_text")
|
||||
target.closing_text = letter_text.get("closing_text")
|
||||
target.language = letter_text.get("language")
|
||||
amounts = calculate_interest_and_amount(
|
||||
target.outstanding_amount,
|
||||
target.rate_of_interest,
|
||||
target.dunning_fee,
|
||||
target.overdue_days,
|
||||
)
|
||||
target.interest_amount = amounts.get("interest_amount")
|
||||
target.dunning_amount = amounts.get("dunning_amount")
|
||||
target.grand_total = amounts.get("grand_total")
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Sales Invoice",
|
||||
source_name,
|
||||
{
|
||||
target.validate()
|
||||
|
||||
return get_mapped_doc(
|
||||
from_doctype="Sales Invoice",
|
||||
from_docname=source_name,
|
||||
target_doc=target_doc,
|
||||
table_maps={
|
||||
"Sales Invoice": {
|
||||
"doctype": "Dunning",
|
||||
}
|
||||
"field_map": {"customer_address": "customer_address", "parent": "sales_invoice"},
|
||||
},
|
||||
"Payment Schedule": {
|
||||
"doctype": "Overdue Payment",
|
||||
"field_map": {"name": "payment_schedule", "parent": "sales_invoice"},
|
||||
"condition": lambda doc: doc.outstanding > 0 and getdate(doc.due_date) < getdate(),
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
postprocess=postprocess_dunning,
|
||||
ignore_permissions=ignore_permissions,
|
||||
)
|
||||
return doclist
|
||||
|
||||
|
||||
def check_if_return_invoice_linked_with_payment_entry(self):
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"absolute_value": 0,
|
||||
"align_labels_right": 0,
|
||||
"creation": "2019-12-11 04:37:14.012805",
|
||||
"css": ".print-format th {\n background-color: transparent !important;\n border-bottom: 1px solid !important;\n border-top: none !important;\n}\n.print-format .ql-editor {\n padding-left: 0px;\n padding-right: 0px;\n}\n\n.print-format table {\n margin-bottom: 0px;\n }\n.print-format .table-data tr:last-child { \n border-bottom: 1px solid !important;\n}\n\n.print-format .table-inner tr:last-child {\n border-bottom:none !important;\n}\n.print-format .table-inner {\n margin: 0px 0px;\n}\n\n.print-format .table-data ul li { \n color:#787878 !important;\n}\n\n.no-top-border {\n border-top:none !important;\n}\n\n.table-inner td {\n padding-left: 0px !important; \n padding-top: 1px !important;\n padding-bottom: 1px !important;\n color:#787878 !important;\n}\n\n.total {\n background-color: lightgrey !important;\n padding-top: 4px !important;\n padding-bottom: 4px !important;\n}\n",
|
||||
@ -9,10 +10,10 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Print Format",
|
||||
"font": "Arial",
|
||||
"format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"<div></div>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<b>{{doc.customer_name}}</b> <br />\\n{{doc.address_display}}\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<div style=\\\"text-align:left;\\\">\\n<div style=\\\"font-size:24px; text-transform:uppercase;\\\">{{_(doc.dunning_type)}}</div>\\n<div style=\\\"font-size:16px;padding-bottom:5px;\\\">{{ doc.name }}</div>\\n</div>\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldname\": \"sales_invoice\", \"print_hide\": 0, \"label\": \"Sales Invoice\"}, {\"fieldname\": \"due_date\", \"print_hide\": 0, \"label\": \"Due Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"body_text\", \"print_hide\": 0, \"label\": \"Body Text\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<table class=\\\"table table-borderless table-data\\\">\\n <tbody>\\n <tr>\\n <th>{{_(\\\"Description\\\")}}</th>\\n\\t <th style=\\\"text-align: right;\\\">{{_(\\\"Amount\\\")}}</th>\\n </tr>\\n <tr>\\n <td>\\n {{_(\\\"Outstanding Amount\\\")}}\\n </td>\\n <td style=\\\"text-align: right;\\\">\\n {{doc.get_formatted(\\\"outstanding_amount\\\")}}\\n </td>\\n </tr>\\n {%if doc.rate_of_interest > 0%}\\n <tr>\\n <td>\\n {{_(\\\"Interest \\\")}} {{doc.rate_of_interest}}% p.a. ({{doc.overdue_days}} {{_(\\\"days\\\")}})\\n </td>\\n <td style=\\\"text-align: right;\\\">\\n {{doc.get_formatted(\\\"interest_amount\\\")}}\\n </td>\\n </tr>\\n {% endif %}\\n {%if doc.dunning_fee > 0%}\\n <tr>\\n <td>\\n {{_(\\\"Dunning Fee\\\")}}\\n </td>\\n <td style=\\\"text-align: right;\\\">\\n {{doc.get_formatted(\\\"dunning_fee\\\")}}\\n </td>\\n </tr>\\n {% endif %}\\n </tbody>\\n</table>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"\\n<div class=\\\"row total\\\" style =\\\"margin-right: 0px;\\\">\\n\\t\\t<div class=\\\"col-xs-5\\\">\\n\\t\\t\\t<b>{{_(\\\"Grand Total\\\")}}</b></div>\\n\\t\\t<div class=\\\"col-xs-7 text-right\\\" style=\\\"padding-right: 4px;\\\">\\n\\t\\t\\t<b>{{doc.get_formatted(\\\"grand_total\\\")}}</b>\\n\\t\\t</div>\\n</div>\\n\\n\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"closing_text\", \"print_hide\": 0, \"label\": \"Closing Text\"}]",
|
||||
"format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"<div></div>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<b>{{doc.customer_name}}</b> <br />\\n{{doc.address_display}}\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<div style=\\\"text-align:left;\\\">\\n<div style=\\\"font-size:24px; text-transform:uppercase;\\\">{{_(doc.dunning_type)}}</div>\\n<div style=\\\"font-size:16px;padding-bottom:5px;\\\">{{ doc.name }}</div>\\n</div>\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"body_text\", \"print_hide\": 0, \"label\": \"Body Text\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"overdue_payments\", \"print_hide\": 0, \"label\": \"Overdue Payments\", \"visible_columns\": [{\"fieldname\": \"sales_invoice\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"dunning_level\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"due_date\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"overdue_days\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"invoice_portion\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"outstanding\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"interest\", \"print_width\": \"\", \"print_hide\": 0}]}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"total_outstanding\", \"print_hide\": 0, \"label\": \"Total Outstanding\"}, {\"fieldname\": \"dunning_fee\", \"print_hide\": 0, \"label\": \"Dunning Fee\"}, {\"fieldname\": \"total_interest\", \"print_hide\": 0, \"label\": \"Total Interest\"}, {\"fieldname\": \"grand_total\", \"print_hide\": 0, \"label\": \"Grand Total\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"closing_text\", \"print_hide\": 0, \"label\": \"Closing Text\"}]",
|
||||
"idx": 0,
|
||||
"line_breaks": 0,
|
||||
"modified": "2020-07-14 18:25:44.348207",
|
||||
"modified": "2021-09-30 10:22:02.603871",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dunning Letter",
|
||||
|
@ -358,6 +358,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": {
|
||||
|
@ -336,3 +336,4 @@ erpnext.patches.v14_0.set_report_in_process_SOA
|
||||
erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users
|
||||
execute:frappe.defaults.clear_default("fiscal_year")
|
||||
erpnext.patches.v15_0.remove_exotel_integration
|
||||
erpnext.patches.v14_0.single_to_multi_dunning
|
||||
|
49
erpnext/patches/v14_0/single_to_multi_dunning.py
Normal file
49
erpnext/patches/v14_0/single_to_multi_dunning.py
Normal 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)
|
@ -3085,9 +3085,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,
|
||||
@ -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,
|
||||
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,
|
||||
@ -8741,7 +8742,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,
|
||||
|
Can't render this file because it is too large.
|
Loading…
Reference in New Issue
Block a user