Browse Source
* feat: Dunning * fix: Replaces spaces with tab Co-authored-by: Nabin Hait <nabinhait@gmail.com>develop
KanchanChauhan
4 years ago
committed by
GitHub
21 changed files with 1103 additions and 21 deletions
@ -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); |
|||
}, |
|||
}); |
|||
}, |
|||
}); |
@ -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 |
|||
} |
@ -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 |
|||
} |
@ -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"]; |
|||
} |
|||
}, |
|||
}; |
@ -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,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) {
|
|||
|
|||
// }
|
|||
}); |
@ -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 |
|||
} |
@ -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 |
@ -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 |
@ -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" |
|||
} |
Loading…
Reference in new issue