feat: Dunning (#22559)
* feat: Dunning * fix: Replaces spaces with tab Co-authored-by: Nabin Hait <nabinhait@gmail.com>
This commit is contained in:
parent
23481cc484
commit
2826fd3c20
0
erpnext/accounts/doctype/dunning/__init__.py
Normal file
0
erpnext/accounts/doctype/dunning/__init__.py
Normal file
149
erpnext/accounts/doctype/dunning/dunning.js
Normal file
149
erpnext/accounts/doctype/dunning/dunning.js
Normal file
@ -0,0 +1,149 @@
|
||||
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Dunning", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("sales_invoice", () => {
|
||||
return {
|
||||
filters: {
|
||||
docstatus: 1,
|
||||
company: frm.doc.company,
|
||||
outstanding_amount: [">", 0],
|
||||
status: "Overdue"
|
||||
},
|
||||
};
|
||||
});
|
||||
frm.set_query("income_account", () => {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
root_type: "Income",
|
||||
is_group: 0
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
refresh: function (frm) {
|
||||
frm.set_df_property("company", "read_only", frm.doc.__islocal ? 0 : 1);
|
||||
frm.set_df_property(
|
||||
"sales_invoice",
|
||||
"read_only",
|
||||
frm.doc.__islocal ? 0 : 1
|
||||
);
|
||||
if (frm.doc.docstatus === 1 && frm.doc.status === "Unresolved") {
|
||||
frm.add_custom_button(__("Resolve"), () => {
|
||||
frm.set_value("status", "Resolved");
|
||||
});
|
||||
}
|
||||
if (frm.doc.docstatus === 1 && frm.doc.status !== "Resolved") {
|
||||
frm.add_custom_button(
|
||||
__("Payment"),
|
||||
function () {
|
||||
frm.events.make_payment_entry(frm);
|
||||
},__("Create")
|
||||
);
|
||||
frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
}
|
||||
},
|
||||
overdue_days: function (frm) {
|
||||
frappe.db.get_value(
|
||||
"Dunning Type",
|
||||
{
|
||||
start_day: ["<", frm.doc.overdue_days],
|
||||
end_day: [">=", frm.doc.overdue_days],
|
||||
},
|
||||
"dunning_type",
|
||||
(r) => {
|
||||
if (r) {
|
||||
frm.set_value("dunning_type", r.dunning_type);
|
||||
} else {
|
||||
frm.set_value("dunning_type", "");
|
||||
frm.set_value("rate_of_interest", "");
|
||||
frm.set_value("dunning_fee", "");
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
dunning_type: function (frm) {
|
||||
frm.trigger("get_dunning_letter_text");
|
||||
},
|
||||
language: function (frm) {
|
||||
frm.trigger("get_dunning_letter_text");
|
||||
},
|
||||
get_dunning_letter_text: function (frm) {
|
||||
if (frm.doc.dunning_type) {
|
||||
frappe.call({
|
||||
method:
|
||||
"erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text",
|
||||
args: {
|
||||
dunning_type: frm.doc.dunning_type,
|
||||
language: frm.doc.language,
|
||||
doc: frm.doc,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
frm.set_value("body_text", r.message.body_text);
|
||||
frm.set_value("closing_text", r.message.closing_text);
|
||||
frm.set_value("language", r.message.language);
|
||||
} else {
|
||||
frm.set_value("body_text", "");
|
||||
frm.set_value("closing_text", "");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
due_date: function (frm) {
|
||||
frm.trigger("calculate_overdue_days");
|
||||
},
|
||||
posting_date: function (frm) {
|
||||
frm.trigger("calculate_overdue_days");
|
||||
},
|
||||
rate_of_interest: function (frm) {
|
||||
frm.trigger("calculate_interest_and_amount");
|
||||
},
|
||||
outstanding_amount: function (frm) {
|
||||
frm.trigger("calculate_interest_and_amount");
|
||||
},
|
||||
interest_amount: function (frm) {
|
||||
frm.trigger("calculate_interest_and_amount");
|
||||
},
|
||||
dunning_fee: function (frm) {
|
||||
frm.trigger("calculate_interest_and_amount");
|
||||
},
|
||||
sales_invoice: function (frm) {
|
||||
frm.trigger("calculate_overdue_days");
|
||||
},
|
||||
calculate_overdue_days: function (frm) {
|
||||
if (frm.doc.posting_date && frm.doc.due_date) {
|
||||
const overdue_days = moment(frm.doc.posting_date).diff(
|
||||
frm.doc.due_date,
|
||||
"days"
|
||||
);
|
||||
frm.set_value("overdue_days", overdue_days);
|
||||
}
|
||||
},
|
||||
calculate_interest_and_amount: function (frm) {
|
||||
const interest_per_year = frm.doc.outstanding_amount * frm.doc.rate_of_interest / 100;
|
||||
const interest_amount = interest_per_year / 365 * frm.doc.overdue_days || 0;
|
||||
const dunning_amount = interest_amount + frm.doc.dunning_fee;
|
||||
const grand_total = frm.doc.outstanding_amount + dunning_amount;
|
||||
frm.set_value("interest_amount", interest_amount);
|
||||
frm.set_value("dunning_amount", dunning_amount);
|
||||
frm.set_value("grand_total", grand_total);
|
||||
},
|
||||
make_payment_entry: function (frm) {
|
||||
return frappe.call({
|
||||
method:
|
||||
"erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry",
|
||||
args: {
|
||||
dt: frm.doc.doctype,
|
||||
dn: frm.doc.name,
|
||||
},
|
||||
callback: function (r) {
|
||||
var doc = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
370
erpnext/accounts/doctype/dunning/dunning.json
Normal file
370
erpnext/accounts/doctype/dunning/dunning.json
Normal file
@ -0,0 +1,370 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_events_in_timeline": 1,
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2019-07-05 16:34:31.013238",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"naming_series",
|
||||
"sales_invoice",
|
||||
"customer",
|
||||
"customer_name",
|
||||
"outstanding_amount",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
"column_break_3",
|
||||
"company",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"due_date",
|
||||
"overdue_days",
|
||||
"address_and_contact_section",
|
||||
"address_display",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"column_break_18",
|
||||
"company_address_display",
|
||||
"section_break_6",
|
||||
"dunning_type",
|
||||
"interest_amount",
|
||||
"column_break_8",
|
||||
"rate_of_interest",
|
||||
"dunning_fee",
|
||||
"section_break_12",
|
||||
"dunning_amount",
|
||||
"grand_total",
|
||||
"income_account",
|
||||
"column_break_17",
|
||||
"status",
|
||||
"printing_setting_section",
|
||||
"language",
|
||||
"body_text",
|
||||
"column_break_22",
|
||||
"letter_head",
|
||||
"closing_text",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "DUNN-.MM.-.YY.-",
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"label": "Series",
|
||||
"options": "DUNN-.MM.-.YY.-"
|
||||
},
|
||||
{
|
||||
"fieldname": "sales_invoice",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Sales Invoice",
|
||||
"options": "Sales Invoice",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.customer_name",
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Customer Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.outstanding_amount",
|
||||
"fieldname": "outstanding_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Outstanding Amount",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "Today",
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "overdue_days",
|
||||
"fieldtype": "Int",
|
||||
"label": "Overdue Days",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "dunning_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Dunning Type",
|
||||
"options": "Dunning Type",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "interest_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Interest Amount",
|
||||
"precision": "2",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_8",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "dunning_type.dunning_fee",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "dunning_fee",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Dunning Fee",
|
||||
"precision": "2"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_12",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "printing_setting_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Printing Setting"
|
||||
},
|
||||
{
|
||||
"fieldname": "language",
|
||||
"fieldtype": "Link",
|
||||
"label": "Print Language",
|
||||
"options": "Language"
|
||||
},
|
||||
{
|
||||
"fieldname": "letter_head",
|
||||
"fieldtype": "Link",
|
||||
"label": "Letter Head",
|
||||
"options": "Letter Head"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_22",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.currency",
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Currency",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Dunning",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "{customer_name}",
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"fieldname": "body_text",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Body Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "closing_text",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Closing Text"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.due_date",
|
||||
"fieldname": "due_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Due Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "Posting Time"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "dunning_type.interest_rate",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "rate_of_interest",
|
||||
"fieldtype": "Float",
|
||||
"label": "Rate of Interest (%) Yearly"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_and_contact_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address and Contact"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.address_display",
|
||||
"fieldname": "address_display",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Address",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.contact_display",
|
||||
"fieldname": "contact_display",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Contact",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.contact_mobile",
|
||||
"fieldname": "contact_mobile",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Mobile No",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_18",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.company_address_display",
|
||||
"fieldname": "company_address_display",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Company Address",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.contact_email",
|
||||
"fieldname": "contact_email",
|
||||
"fieldtype": "Data",
|
||||
"label": "Contact Email",
|
||||
"options": "Email",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.customer",
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer",
|
||||
"options": "Customer",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Grand Total",
|
||||
"precision": "2",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "Unresolved",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "Draft\nResolved\nUnresolved\nCancelled"
|
||||
},
|
||||
{
|
||||
"fieldname": "dunning_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Dunning Amount",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "income_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Income Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.conversion_rate",
|
||||
"fieldname": "conversion_rate",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "Conversion Rate",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-07-21 18:20:23.512151",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dunning",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1
|
||||
}
|
119
erpnext/accounts/doctype/dunning/dunning.py
Normal file
119
erpnext/accounts/doctype/dunning/dunning.py
Normal file
@ -0,0 +1,119 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import json
|
||||
from six import string_types
|
||||
from frappe.utils import getdate, get_datetime, rounded, flt
|
||||
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year
|
||||
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
|
||||
|
||||
class Dunning(AccountsController):
|
||||
def validate(self):
|
||||
self.validate_overdue_days()
|
||||
self.validate_amount()
|
||||
if not self.income_account:
|
||||
self.income_account = frappe.db.get_value('Company', self.company, 'default_income_account')
|
||||
|
||||
def validate_overdue_days(self):
|
||||
self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0
|
||||
|
||||
def validate_amount(self):
|
||||
amounts = calculate_interest_and_amount(
|
||||
self.posting_date, self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days)
|
||||
if self.interest_amount != amounts.get('interest_amount'):
|
||||
self.interest_amount = amounts.get('interest_amount')
|
||||
if self.dunning_amount != amounts.get('dunning_amount'):
|
||||
self.dunning_amount = amounts.get('dunning_amount')
|
||||
if self.grand_total != amounts.get('grand_total'):
|
||||
self.grand_total = amounts.get('grand_total')
|
||||
|
||||
def on_submit(self):
|
||||
self.make_gl_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
if self.dunning_amount:
|
||||
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
def make_gl_entries(self):
|
||||
if not self.dunning_amount:
|
||||
return
|
||||
gl_entries = []
|
||||
invoice_fields = ["project", "cost_center", "debit_to", "party_account_currency", "conversion_rate", "cost_center"]
|
||||
inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
invoice_fields.extend(accounting_dimensions)
|
||||
dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate)
|
||||
default_cost_center = frappe.get_cached_value('Company', self.company, 'cost_center')
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": inv.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"due_date": self.due_date,
|
||||
"against": self.income_account,
|
||||
"debit": dunning_in_company_currency,
|
||||
"debit_in_account_currency": self.dunning_amount,
|
||||
"against_voucher": self.name,
|
||||
"against_voucher_type": "Dunning",
|
||||
"cost_center": inv.cost_center or default_cost_center,
|
||||
"project": inv.project
|
||||
}, inv.party_account_currency, item=inv)
|
||||
)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": self.income_account,
|
||||
"against": self.customer,
|
||||
"credit": dunning_in_company_currency,
|
||||
"cost_center": inv.cost_center or default_cost_center,
|
||||
"credit_in_account_currency": self.dunning_amount,
|
||||
"project": inv.project
|
||||
}, item=inv)
|
||||
)
|
||||
make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False)
|
||||
|
||||
|
||||
def resolve_dunning(doc, state):
|
||||
for reference in doc.references:
|
||||
if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0:
|
||||
dunnings = frappe.get_list('Dunning', filters={
|
||||
'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')})
|
||||
|
||||
for dunning in dunnings:
|
||||
frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved')
|
||||
|
||||
def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
|
||||
interest_amount = 0
|
||||
if rate_of_interest:
|
||||
interest_per_year = rounded(flt(outstanding_amount) * flt(rate_of_interest))/100
|
||||
interest_amount = (
|
||||
interest_per_year / days_in_year(get_datetime(posting_date).year)) * int(overdue_days)
|
||||
grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee)
|
||||
dunning_amount = flt(interest_amount) + flt(dunning_fee)
|
||||
return {
|
||||
'interest_amount': interest_amount,
|
||||
'grand_total': grand_total,
|
||||
'dunning_amount': dunning_amount}
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_dunning_letter_text(dunning_type, doc, language=None):
|
||||
if isinstance(doc, string_types):
|
||||
doc = json.loads(doc)
|
||||
if language:
|
||||
filters = {'parent': dunning_type, 'language': language}
|
||||
else:
|
||||
filters = {'parent': dunning_type, 'is_default_language': 1}
|
||||
letter_text = frappe.db.get_value('Dunning Letter Text', filters,
|
||||
['body_text', 'closing_text', 'language'], as_dict=1)
|
||||
if letter_text:
|
||||
return {
|
||||
'body_text': frappe.render_template(letter_text.body_text, doc),
|
||||
'closing_text': frappe.render_template(letter_text.closing_text, doc),
|
||||
'language': letter_text.language
|
||||
}
|
9
erpnext/accounts/doctype/dunning/dunning_list.js
Normal file
9
erpnext/accounts/doctype/dunning/dunning_list.js
Normal file
@ -0,0 +1,9 @@
|
||||
frappe.listview_settings["Dunning"] = {
|
||||
get_indicator: function (doc) {
|
||||
if (doc.status === "Resolved") {
|
||||
return [__("Resolved"), "green", "status,=,Resolved"];
|
||||
} else {
|
||||
return [__("Unresolved"), "red", "status,=,Unresolved"];
|
||||
}
|
||||
},
|
||||
};
|
100
erpnext/accounts/doctype/dunning/test_dunning.py
Normal file
100
erpnext/accounts/doctype/dunning/test_dunning.py
Normal file
@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
import unittest
|
||||
from frappe.utils import add_days, today, nowdate
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice_against_cost_center
|
||||
from erpnext.accounts.doctype.dunning.dunning import calculate_interest_and_amount
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
|
||||
|
||||
class TestDunning(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
create_dunning_type()
|
||||
unlink_payment_on_cancel_of_invoice()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(self):
|
||||
unlink_payment_on_cancel_of_invoice(0)
|
||||
|
||||
def test_dunning(self):
|
||||
dunning = create_dunning()
|
||||
amounts = calculate_interest_and_amount(
|
||||
dunning.posting_date, dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
|
||||
self.assertEqual(round(amounts.get('interest_amount'), 2), 0.44)
|
||||
self.assertEqual(round(amounts.get('dunning_amount'), 2), 20.44)
|
||||
self.assertEqual(round(amounts.get('grand_total'), 2), 120.44)
|
||||
|
||||
def test_gl_entries(self):
|
||||
dunning = create_dunning()
|
||||
dunning.submit()
|
||||
gl_entries = frappe.db.sql("""select account, debit, credit
|
||||
from `tabGL Entry` where voucher_type='Dunning' and voucher_no=%s
|
||||
order by account asc""", dunning.name, as_dict=1)
|
||||
self.assertTrue(gl_entries)
|
||||
expected_values = dict((d[0], d) for d in [
|
||||
['Debtors - _TC', 20.44, 0.0],
|
||||
['Sales - _TC', 0.0, 20.44]
|
||||
])
|
||||
for gle in gl_entries:
|
||||
self.assertEquals(expected_values[gle.account][0], gle.account)
|
||||
self.assertEquals(expected_values[gle.account][1], gle.debit)
|
||||
self.assertEquals(expected_values[gle.account][2], gle.credit)
|
||||
|
||||
def test_payment_entry(self):
|
||||
dunning = create_dunning()
|
||||
dunning.submit()
|
||||
pe = get_payment_entry("Dunning", dunning.name)
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = nowdate()
|
||||
pe.paid_from_account_currency = dunning.currency
|
||||
pe.paid_to_account_currency = dunning.currency
|
||||
pe.source_exchange_rate = 1
|
||||
pe.target_exchange_rate = 1
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
si_doc = frappe.get_doc('Sales Invoice', dunning.sales_invoice)
|
||||
self.assertEqual(si_doc.outstanding_amount, 0)
|
||||
|
||||
|
||||
def create_dunning():
|
||||
posting_date = add_days(today(), -20)
|
||||
due_date = add_days(today(), -15)
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=posting_date, due_date=due_date, status='Overdue')
|
||||
dunning_type = frappe.get_doc("Dunning Type", 'First Notice')
|
||||
dunning = frappe.new_doc("Dunning")
|
||||
dunning.sales_invoice = sales_invoice.name
|
||||
dunning.customer_name = sales_invoice.customer_name
|
||||
dunning.outstanding_amount = sales_invoice.outstanding_amount
|
||||
dunning.debit_to = sales_invoice.debit_to
|
||||
dunning.currency = sales_invoice.currency
|
||||
dunning.company = sales_invoice.company
|
||||
dunning.posting_date = nowdate()
|
||||
dunning.due_date = sales_invoice.due_date
|
||||
dunning.dunning_type = 'First Notice'
|
||||
dunning.rate_of_interest = dunning_type.rate_of_interest
|
||||
dunning.dunning_fee = dunning_type.dunning_fee
|
||||
dunning.save()
|
||||
return dunning
|
||||
|
||||
def create_dunning_type():
|
||||
dunning_type = frappe.new_doc("Dunning Type")
|
||||
dunning_type.dunning_type = 'First Notice'
|
||||
dunning_type.start_day = 10
|
||||
dunning_type.end_day = 20
|
||||
dunning_type.dunning_fee = 20
|
||||
dunning_type.rate_of_interest = 8
|
||||
dunning_type.append(
|
||||
"dunning_letter_text", {
|
||||
'language': 'en',
|
||||
'body_text': 'We have still not received payment for our invoice ',
|
||||
'closing_text': 'We kindly request that you pay the outstanding amount immediately, including interest and late fees.'
|
||||
}
|
||||
)
|
||||
dunning_type.save()
|
@ -0,0 +1,70 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2019-12-06 04:25:40.215625",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"language",
|
||||
"is_default_language",
|
||||
"section_break_4",
|
||||
"body_text",
|
||||
"closing_text",
|
||||
"section_break_7",
|
||||
"body_and_closing_text_help"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "language",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Language",
|
||||
"options": "Language"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_default_language",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Default Language"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_4",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"description": "Letter or Email Body Text",
|
||||
"fieldname": "body_text",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_list_view": 1,
|
||||
"label": "Body Text"
|
||||
},
|
||||
{
|
||||
"description": "Letter or Email Closing Text",
|
||||
"fieldname": "closing_text",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_list_view": 1,
|
||||
"label": "Closing Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "body_and_closing_text_help",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Body and Closing Text Help",
|
||||
"options": "<h4>Body Text and Closing Text Example</h4>\n\n<div>We have noticed that you have not yet paid invoice {{sales_invoice}} for {{frappe.db.get_value(\"Currency\", currency, \"symbol\")}} {{outstanding_amount}}. This is a friendly reminder that the invoice was due on {{due_date}}. Please pay the amount due immediately to avoid any further dunning cost.</div>\n\n<h4>How to get fieldnames</h4>\n\n<p>The fieldnames you can use in your template are the fields in the document. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Sales Invoice)</p>\n\n<h4>Templating</h4>\n\n<p>Templates are compiled using the Jinja Templating Language. To learn more about Jinja, <a class=\"strong\" href=\"http://jinja.pocoo.org/docs/dev/templates/\">read this documentation.</a></p>"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-07-14 18:02:35.988958",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dunning Letter Text",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class DunningLetterText(Document):
|
||||
pass
|
0
erpnext/accounts/doctype/dunning_type/__init__.py
Normal file
0
erpnext/accounts/doctype/dunning_type/__init__.py
Normal file
8
erpnext/accounts/doctype/dunning_type/dunning_type.js
Normal file
8
erpnext/accounts/doctype/dunning_type/dunning_type.js
Normal file
@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Dunning Type', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
129
erpnext/accounts/doctype/dunning_type/dunning_type.json
Normal file
129
erpnext/accounts/doctype/dunning_type/dunning_type.json
Normal file
@ -0,0 +1,129 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:dunning_type",
|
||||
"creation": "2019-12-04 04:59:08.003664",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"dunning_type",
|
||||
"overdue_interval_section",
|
||||
"start_day",
|
||||
"column_break_4",
|
||||
"end_day",
|
||||
"section_break_6",
|
||||
"dunning_fee",
|
||||
"column_break_8",
|
||||
"rate_of_interest",
|
||||
"text_block_section",
|
||||
"dunning_letter_text"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "dunning_type",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Dunning Type",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "dunning_fee",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Dunning Fee"
|
||||
},
|
||||
{
|
||||
"description": "This section allows the user to set the Body and Closing text of the Dunning Letter for the Dunning Type based on language, which can be used in Print.",
|
||||
"fieldname": "text_block_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Dunning Letter"
|
||||
},
|
||||
{
|
||||
"fieldname": "dunning_letter_text",
|
||||
"fieldtype": "Table",
|
||||
"options": "Dunning Letter Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_8",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "overdue_interval_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Overdue Interval"
|
||||
},
|
||||
{
|
||||
"fieldname": "start_day",
|
||||
"fieldtype": "Int",
|
||||
"label": "Start Day"
|
||||
},
|
||||
{
|
||||
"fieldname": "end_day",
|
||||
"fieldtype": "Int",
|
||||
"label": "End Day"
|
||||
},
|
||||
{
|
||||
"fieldname": "rate_of_interest",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate of Interest (%) Yearly"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-07-15 17:14:17.835074",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dunning Type",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
10
erpnext/accounts/doctype/dunning_type/dunning_type.py
Normal file
10
erpnext/accounts/doctype/dunning_type/dunning_type.py
Normal file
@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class DunningType(Document):
|
||||
pass
|
10
erpnext/accounts/doctype/dunning_type/test_dunning_type.py
Normal file
10
erpnext/accounts/doctype/dunning_type/test_dunning_type.py
Normal file
@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestDunningType(unittest.TestCase):
|
||||
pass
|
@ -90,7 +90,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
|
||||
frm.set_query("reference_doctype", "references", function() {
|
||||
if (frm.doc.party_type=="Customer") {
|
||||
var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry"];
|
||||
var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
|
||||
} else if (frm.doc.party_type=="Supplier") {
|
||||
var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"];
|
||||
} else if (frm.doc.party_type=="Employee") {
|
||||
@ -125,7 +125,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
const child = locals[cdt][cdn];
|
||||
const filters = {"docstatus": 1, "company": doc.company};
|
||||
const party_type_doctypes = ['Sales Invoice', 'Sales Order', 'Purchase Invoice',
|
||||
'Purchase Order', 'Expense Claim', 'Fees'];
|
||||
'Purchase Order', 'Expense Claim', 'Fees', 'Dunning'];
|
||||
|
||||
if (in_list(party_type_doctypes, child.reference_doctype)) {
|
||||
filters[doc.party_type.toLowerCase()] = doc.party;
|
||||
@ -863,10 +863,10 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
|
||||
if(frm.doc.party_type=="Customer" &&
|
||||
!in_list(["Sales Order", "Sales Invoice", "Journal Entry"], row.reference_doctype)
|
||||
!in_list(["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"], row.reference_doctype)
|
||||
) {
|
||||
frappe.model.set_value(row.doctype, row.name, "reference_doctype", null);
|
||||
frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Sales Order, Sales Invoice or Journal Entry", [row.idx]));
|
||||
frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Sales Order, Sales Invoice, Journal Entry or Dunning", [row.idx]));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -199,8 +199,8 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def validate_account_type(self, account, account_types):
|
||||
account_type = frappe.db.get_value("Account", account, "account_type")
|
||||
if account_type not in account_types:
|
||||
frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types)))
|
||||
# if account_type not in account_types:
|
||||
# frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types)))
|
||||
|
||||
def set_exchange_rate(self):
|
||||
if self.paid_from and not self.source_exchange_rate:
|
||||
@ -223,7 +223,7 @@ class PaymentEntry(AccountsController):
|
||||
if self.party_type == "Student":
|
||||
valid_reference_doctypes = ("Fees")
|
||||
elif self.party_type == "Customer":
|
||||
valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry")
|
||||
valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning")
|
||||
elif self.party_type == "Supplier":
|
||||
valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry")
|
||||
elif self.party_type == "Employee":
|
||||
@ -897,6 +897,10 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
||||
total_amount = ref_doc.get("grand_total")
|
||||
exchange_rate = 1
|
||||
outstanding_amount = ref_doc.get("outstanding_amount")
|
||||
if reference_doctype == "Dunning":
|
||||
total_amount = ref_doc.get("dunning_amount")
|
||||
exchange_rate = 1
|
||||
outstanding_amount = ref_doc.get("dunning_amount")
|
||||
elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1:
|
||||
total_amount = ref_doc.get("total_amount")
|
||||
if ref_doc.multi_currency:
|
||||
@ -951,7 +955,7 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
|
||||
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0:
|
||||
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
|
||||
|
||||
if dt in ("Sales Invoice", "Sales Order"):
|
||||
if dt in ("Sales Invoice", "Sales Order", "Dunning"):
|
||||
party_type = "Customer"
|
||||
elif dt in ("Purchase Invoice", "Purchase Order"):
|
||||
party_type = "Supplier"
|
||||
@ -980,7 +984,7 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
|
||||
party_account_currency = doc.get("party_account_currency") or get_account_currency(party_account)
|
||||
|
||||
# payment type
|
||||
if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees") and doc.outstanding_amount > 0)) \
|
||||
if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \
|
||||
or (dt=="Purchase Invoice" and doc.outstanding_amount < 0):
|
||||
payment_type = "Receive"
|
||||
else:
|
||||
@ -1006,6 +1010,9 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
|
||||
elif dt == "Fees":
|
||||
grand_total = doc.grand_total
|
||||
outstanding_amount = doc.outstanding_amount
|
||||
elif dt == "Dunning":
|
||||
grand_total = doc.grand_total
|
||||
outstanding_amount = doc.grand_total
|
||||
else:
|
||||
if party_account_currency == doc.company_currency:
|
||||
grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)
|
||||
@ -1075,15 +1082,35 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
|
||||
for reference in get_reference_as_per_payment_terms(doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount):
|
||||
pe.append('references', reference)
|
||||
else:
|
||||
pe.append("references", {
|
||||
'reference_doctype': dt,
|
||||
'reference_name': dn,
|
||||
"bill_no": doc.get("bill_no"),
|
||||
"due_date": doc.get("due_date"),
|
||||
'total_amount': grand_total,
|
||||
'outstanding_amount': outstanding_amount,
|
||||
'allocated_amount': outstanding_amount
|
||||
})
|
||||
if dt == "Dunning":
|
||||
pe.append("references", {
|
||||
'reference_doctype': 'Sales Invoice',
|
||||
'reference_name': doc.get('sales_invoice'),
|
||||
"bill_no": doc.get("bill_no"),
|
||||
"due_date": doc.get("due_date"),
|
||||
'total_amount': doc.get('outstanding_amount'),
|
||||
'outstanding_amount': doc.get('outstanding_amount'),
|
||||
'allocated_amount': doc.get('outstanding_amount')
|
||||
})
|
||||
pe.append("references", {
|
||||
'reference_doctype': dt,
|
||||
'reference_name': dn,
|
||||
"bill_no": doc.get("bill_no"),
|
||||
"due_date": doc.get("due_date"),
|
||||
'total_amount': doc.get('dunning_amount'),
|
||||
'outstanding_amount': doc.get('dunning_amount'),
|
||||
'allocated_amount': doc.get('dunning_amount')
|
||||
})
|
||||
else:
|
||||
pe.append("references", {
|
||||
'reference_doctype': dt,
|
||||
'reference_name': dn,
|
||||
"bill_no": doc.get("bill_no"),
|
||||
"due_date": doc.get("due_date"),
|
||||
'total_amount': grand_total,
|
||||
'outstanding_amount': outstanding_amount,
|
||||
'allocated_amount': outstanding_amount
|
||||
})
|
||||
|
||||
pe.setup_party_account_field()
|
||||
pe.set_missing_values()
|
||||
@ -1172,4 +1199,4 @@ def make_payment_order(source_name, target_doc=None):
|
||||
|
||||
}, target_doc, set_missing_values)
|
||||
|
||||
return doclist
|
||||
return doclist
|
@ -96,6 +96,12 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
|
||||
cur_frm.add_custom_button(__('Invoice Discounting'), function() {
|
||||
cur_frm.events.create_invoice_discounting(cur_frm);
|
||||
}, __('Create'));
|
||||
|
||||
if (doc.due_date < frappe.datetime.get_today()) {
|
||||
cur_frm.add_custom_button(__('Dunning'), function() {
|
||||
cur_frm.events.create_dunning(cur_frm);
|
||||
}, __('Create'));
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.docstatus === 1) {
|
||||
@ -824,6 +830,12 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting",
|
||||
frm: frm
|
||||
});
|
||||
},
|
||||
create_dunning: function(frm) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
|
||||
frm: frm
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -1602,3 +1602,37 @@ def create_invoice_discounting(source_name, target_doc=None):
|
||||
})
|
||||
|
||||
return invoice_discounting
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_dunning(source_name, target_doc=None):
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text, calculate_interest_and_amount
|
||||
def set_missing_values(source, target):
|
||||
target.sales_invoice = source_name
|
||||
target.outstanding_amount = source.outstanding_amount
|
||||
overdue_days = (getdate(target.posting_date) - getdate(source.due_date)).days
|
||||
target.overdue_days = overdue_days
|
||||
if frappe.db.exists('Dunning Type', {'start_day': [
|
||||
'<', overdue_days], 'end_day': ['>=', overdue_days]}):
|
||||
dunning_type = frappe.get_doc('Dunning Type', {'start_day': [
|
||||
'<', overdue_days], 'end_day': ['>=', overdue_days]})
|
||||
target.dunning_type = dunning_type.name
|
||||
target.rate_of_interest = dunning_type.rate_of_interest
|
||||
target.dunning_fee = dunning_type.dunning_fee
|
||||
letter_text = get_dunning_letter_text(dunning_type = dunning_type.name, doc = target.as_dict())
|
||||
if letter_text:
|
||||
target.body_text = letter_text.get('body_text')
|
||||
target.closing_text = letter_text.get('closing_text')
|
||||
target.language = letter_text.get('language')
|
||||
amounts = calculate_interest_and_amount(target.posting_date, target.outstanding_amount,
|
||||
target.rate_of_interest, target.dunning_fee, target.overdue_days)
|
||||
target.interest_amount = amounts.get('interest_amount')
|
||||
target.dunning_amount = amounts.get('dunning_amount')
|
||||
target.grand_total = amounts.get('grand_total')
|
||||
|
||||
doclist = get_mapped_doc("Sales Invoice", source_name, {
|
||||
"Sales Invoice": {
|
||||
"doctype": "Dunning",
|
||||
}
|
||||
}, target_doc, set_missing_values)
|
||||
return doclist
|
@ -0,0 +1,25 @@
|
||||
{
|
||||
"align_labels_right": 0,
|
||||
"creation": "2019-12-11 04:37:14.012805",
|
||||
"css": ".print-format th {\n background-color: transparent !important;\n border-bottom: 1px solid !important;\n border-top: none !important;\n}\n.print-format .ql-editor {\n padding-left: 0px;\n padding-right: 0px;\n}\n\n.print-format table {\n margin-bottom: 0px;\n }\n.print-format .table-data tr:last-child { \n border-bottom: 1px solid !important;\n}\n\n.print-format .table-inner tr:last-child {\n border-bottom:none !important;\n}\n.print-format .table-inner {\n margin: 0px 0px;\n}\n\n.print-format .table-data ul li { \n color:#787878 !important;\n}\n\n.no-top-border {\n border-top:none !important;\n}\n\n.table-inner td {\n padding-left: 0px !important; \n padding-top: 1px !important;\n padding-bottom: 1px !important;\n color:#787878 !important;\n}\n\n.total {\n background-color: lightgrey !important;\n padding-top: 4px !important;\n padding-bottom: 4px !important;\n}\n",
|
||||
"custom_format": 0,
|
||||
"default_print_language": "en",
|
||||
"disabled": 0,
|
||||
"doc_type": "Dunning",
|
||||
"docstatus": 0,
|
||||
"doctype": "Print Format",
|
||||
"font": "Arial",
|
||||
"format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"<div></div>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<b>{{doc.customer_name}}</b> <br />\\n{{doc.address_display}}\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<div style=\\\"text-align:left;\\\">\\n<div style=\\\"font-size:24px; text-transform:uppercase;\\\">{{_(doc.dunning_type)}}</div>\\n<div style=\\\"font-size:16px;padding-bottom:5px;\\\">{{ doc.name }}</div>\\n</div>\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldname\": \"sales_invoice\", \"print_hide\": 0, \"label\": \"Sales Invoice\"}, {\"fieldname\": \"due_date\", \"print_hide\": 0, \"label\": \"Due Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"body_text\", \"print_hide\": 0, \"label\": \"Body Text\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<table class=\\\"table table-borderless table-data\\\">\\n <tbody>\\n <tr>\\n <th>{{_(\\\"Description\\\")}}</th>\\n\\t <th style=\\\"text-align: right;\\\">{{_(\\\"Amount\\\")}}</th>\\n </tr>\\n <tr>\\n <td>\\n {{_(\\\"Outstanding Amount\\\")}}\\n </td>\\n <td style=\\\"text-align: right;\\\">\\n {{doc.get_formatted(\\\"outstanding_amount\\\")}}\\n </td>\\n </tr>\\n {%if doc.rate_of_interest > 0%}\\n <tr>\\n <td>\\n {{_(\\\"Interest \\\")}} {{doc.rate_of_interest}}% p.a. ({{doc.overdue_days}} {{_(\\\"days\\\")}})\\n </td>\\n <td style=\\\"text-align: right;\\\">\\n {{doc.get_formatted(\\\"interest_amount\\\")}}\\n </td>\\n </tr>\\n {% endif %}\\n {%if doc.dunning_fee > 0%}\\n <tr>\\n <td>\\n {{_(\\\"Dunning Fee\\\")}}\\n </td>\\n <td style=\\\"text-align: right;\\\">\\n {{doc.get_formatted(\\\"dunning_fee\\\")}}\\n </td>\\n </tr>\\n {% endif %}\\n </tbody>\\n</table>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"\\n<div class=\\\"row total\\\" style =\\\"margin-right: 0px;\\\">\\n\\t\\t<div class=\\\"col-xs-5\\\">\\n\\t\\t\\t<b>{{_(\\\"Grand Total\\\")}}</b></div>\\n\\t\\t<div class=\\\"col-xs-7 text-right\\\" style=\\\"padding-right: 4px;\\\">\\n\\t\\t\\t<b>{{doc.get_formatted(\\\"grand_total\\\")}}</b>\\n\\t\\t</div>\\n</div>\\n\\n\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"closing_text\", \"print_hide\": 0, \"label\": \"Closing Text\"}]",
|
||||
"idx": 0,
|
||||
"line_breaks": 0,
|
||||
"modified": "2020-07-14 18:25:44.348207",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dunning Letter",
|
||||
"owner": "Administrator",
|
||||
"print_format_builder": 0,
|
||||
"print_format_type": "Jinja",
|
||||
"raw_printing": 0,
|
||||
"show_section_headings": 0,
|
||||
"standard": "Yes"
|
||||
}
|
@ -249,7 +249,7 @@ doc_events = {
|
||||
"validate": "erpnext.regional.india.utils.update_grand_total_for_rcm"
|
||||
},
|
||||
"Payment Entry": {
|
||||
"on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status"],
|
||||
"on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
|
||||
"on_trash": "erpnext.regional.check_deletion_permission"
|
||||
},
|
||||
'Address': {
|
||||
@ -552,4 +552,4 @@ global_search_doctypes = {
|
||||
{'doctype': 'Hotel Room Package', 'index': 3},
|
||||
{'doctype': 'Hotel Room Type', 'index': 4}
|
||||
]
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user