Merge branch 'develop' into yet-another-desk

This commit is contained in:
Shivam Mishra 2020-03-10 18:20:27 +05:30 committed by GitHub
commit 1fd1852c6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
287 changed files with 18053 additions and 7679 deletions

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe, json
from frappe import _
from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate
from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate, get_link_to_form
from erpnext.accounts.report.general_ledger.general_ledger import execute
from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan
from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_period_ending
@ -30,8 +30,13 @@ def get(chart_name = None, chart = None, no_cache = None, from_date = None, to_d
account = filters.get("account")
company = filters.get("company")
if not account and chart:
frappe.throw(_("Account is not set for the dashboard chart {0}").format(chart))
if not account and chart_name:
frappe.throw(_("Account is not set for the dashboard chart {0}")
.format(get_link_to_form("Dashboard Chart", chart_name)))
if not frappe.db.exists("Account", account) and chart_name:
frappe.throw(_("Account {0} does not exists in the dashboard chart {1}")
.format(account, get_link_to_form("Dashboard Chart", chart_name)))
if not to_date:
to_date = nowdate()

View File

@ -108,9 +108,9 @@ class Account(NestedSet):
parent_acc_name_map = {}
parent_acc_name, parent_acc_number = frappe.db.get_value('Account', self.parent_account, \
["account_name", "account_number"])
filters = {
filters = {
"company": ["in", descendants],
"account_name": parent_acc_name,
"account_name": parent_acc_name,
}
if parent_acc_number:
filters["account_number"] = parent_acc_number

View File

@ -13,13 +13,15 @@ form_grid_templates = {
class BankReconciliation(Document):
def get_payment_entries(self):
if not (self.bank_account and self.from_date and self.to_date):
msgprint(_("Bank Account, From Date and To Date are Mandatory"))
return
if not (self.from_date and self.to_date):
frappe.throw(_("From Date and To Date are Mandatory"))
if not self.account:
frappe.throw(_("Account is mandatory to get payment entries"))
condition = ""
if not self.include_reconciled_entries:
condition = " and (clearance_date is null or clearance_date='0000-00-00')"
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
journal_entries = frappe.db.sql("""
select
@ -32,10 +34,13 @@ class BankReconciliation(Document):
where
t2.parent = t1.name and t2.account = %(account)s and t1.docstatus=1
and t1.posting_date >= %(from)s and t1.posting_date <= %(to)s
and ifnull(t1.is_opening, 'No') = 'No' %(condition)s
and ifnull(t1.is_opening, 'No') = 'No' {condition}
group by t2.account, t1.name
order by t1.posting_date ASC, t1.name DESC
""", {"condition":condition, "account": self.account, "from": self.from_date, "to": self.to_date}, as_dict=1)
""".format(condition=condition), {"account": self.account, "from": self.from_date, "to": self.to_date}, as_dict=1)
if self.bank_account:
condition += 'and bank_account = %(bank_account)s'
payment_entries = frappe.db.sql("""
select
@ -49,10 +54,10 @@ class BankReconciliation(Document):
where
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
and posting_date >= %(from)s and posting_date <= %(to)s
and bank_account = %(bank_account)s
{condition}
order by
posting_date ASC, name DESC
""", {"account": self.account, "from":self.from_date,
""".format(condition=condition), {"account": self.account, "from":self.from_date,
"to": self.to_date, "bank_account": self.bank_account}, as_dict=1)
pos_entries = []

View File

@ -17,17 +17,60 @@ frappe.ui.form.on('Chart of Accounts Importer', {
if (frm.page && frm.page.show_import_button) {
create_import_button(frm);
}
},
// show download template button when company is properly selected
if(frm.doc.company) {
// download the csv template file
frm.add_custom_button(__("Download template"), function () {
let get_template_url = 'erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.download_template';
open_url_post(frappe.request.url, { cmd: get_template_url, doctype: frm.doc.doctype });
});
} else {
frm.set_value("import_file", "");
}
download_template: function(frm) {
var d = new frappe.ui.Dialog({
title: __("Download Template"),
fields: [
{
label : "File Type",
fieldname: "file_type",
fieldtype: "Select",
reqd: 1,
options: ["Excel", "CSV"]
},
{
label: "Template Type",
fieldname: "template_type",
fieldtype: "Select",
reqd: 1,
options: ["Sample Template", "Blank Template"],
change: () => {
let template_type = d.get_value('template_type');
if (template_type === "Sample Template") {
d.set_df_property('template_type', 'description',
`The Sample Template contains all the required accounts pre filled in the template.
You can add more accounts or change existing accounts in the template as per your choice.`);
} else {
d.set_df_property('template_type', 'description',
`The Blank Template contains just the account type and root type required to build the Chart
of Accounts. Please enter the account names and add more rows as per your requirement.`);
}
}
}
],
primary_action: function() {
var data = d.get_values();
if (!data.template_type) {
frappe.throw(__('Please select <b>Template Type</b> to download template'));
}
open_url_post(
'/api/method/erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.download_template',
{
file_type: data.file_type,
template_type: data.template_type
}
);
d.hide();
},
primary_action_label: __('Download')
});
d.show();
},
import_file: function (frm) {
@ -41,21 +84,24 @@ frappe.ui.form.on('Chart of Accounts Importer', {
},
company: function (frm) {
// validate that no Gl Entry record for the company exists.
frappe.call({
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.validate_company",
args: {
company: frm.doc.company
},
callback: function(r) {
if(r.message===false) {
frm.set_value("company", "");
frappe.throw(__("Transactions against the company already exist! "));
} else {
frm.trigger("refresh");
if (frm.doc.company) {
// validate that no Gl Entry record for the company exists.
frappe.call({
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.validate_company",
args: {
company: frm.doc.company
},
callback: function(r) {
if(r.message===false) {
frm.set_value("company", "");
frappe.throw(__(`Transactions against the company already exist!
Chart Of accounts can be imported for company with no transactions`));
} else {
frm.trigger("refresh");
}
}
}
});
});
}
}
});
@ -77,7 +123,7 @@ var validate_csv_data = function(frm) {
};
var create_import_button = function(frm) {
frm.page.set_primary_action(__("Start Import"), function () {
frm.page.set_primary_action(__("Import"), function () {
frappe.call({
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.import_coa",
args: {
@ -118,7 +164,8 @@ var generate_tree_preview = function(frm) {
args: {
file_name: frm.doc.import_file,
parent: parent,
doctype: 'Chart of Accounts Importer'
doctype: 'Chart of Accounts Importer',
file_type: frm.doc.file_type
},
onclick: function(node) {
parent = node.value;

View File

@ -1,226 +1,71 @@
{
"allow_copy": 1,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2019-02-01 12:24:34.761380",
"custom": 0,
"actions": [],
"allow_copy": 1,
"creation": "2019-02-01 12:24:34.761380",
"description": "Import Chart of Accounts from a csv file",
"docstatus": 0,
"doctype": "DocType",
"document_type": "Other",
"editable_grid": 1,
"engine": "InnoDB",
"doctype": "DocType",
"document_type": "Other",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"download_template",
"import_file",
"chart_preview",
"chart_tree"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "import_file_section",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"depends_on": "company",
"fieldname": "import_file",
"fieldtype": "Attach",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Attach custom Chart of Accounts file",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"label": "Attach custom Chart of Accounts file"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fieldname": "chart_preview",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Chart Preview",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldtype": "Section Break",
"label": "Chart Preview"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "chart_tree",
"fieldtype": "HTML",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Chart Tree",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Chart Tree"
},
{
"depends_on": "company",
"fieldname": "download_template",
"fieldtype": "Button",
"label": "Download Template"
}
],
"has_web_view": 0,
"hide_heading": 1,
"hide_toolbar": 1,
"idx": 0,
"image_view": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2019-02-04 23:10:30.136807",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Chart of Accounts Importer",
"name_case": "",
"owner": "Administrator",
],
"hide_toolbar": 1,
"in_create": 1,
"issingle": 1,
"links": [],
"modified": "2020-02-28 08:49:11.422846",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Chart of Accounts Importer",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"read": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"read_only": 1,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0,
"track_views": 0
],
"quick_entry": 1,
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@ -4,18 +4,28 @@
from __future__ import unicode_literals
from functools import reduce
import frappe, csv
import frappe, csv, os
from frappe import _
from frappe.utils import cstr
from frappe.utils import cstr, cint
from frappe.model.document import Document
from frappe.utils.csvutils import UnicodeWriter
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts, build_tree_from_json
from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file, read_xls_file_from_attached_file
class ChartofAccountsImporter(Document):
pass
@frappe.whitelist()
def validate_company(company):
parent_company, allow_account_creation_against_child_company = frappe.db.get_value('Company',
{'name': company}, ['parent_company',
'allow_account_creation_against_child_company'])
if parent_company and (not allow_account_creation_against_child_company):
frappe.throw(_("""{0} is a child company. Please import accounts against parent company
or enable {1} in company master""").format(frappe.bold(company),
frappe.bold('Allow Account Creation Against Child Company')), title='Wrong Company')
if frappe.db.get_all('GL Entry', {"company": company}, "name", limit=1):
return False
@ -25,42 +35,85 @@ def import_coa(file_name, company):
unset_existing_data(company)
# create accounts
forest = build_forest(generate_data_from_csv(file_name))
file_doc, extension = get_file(file_name)
if extension == 'csv':
data = generate_data_from_csv(file_doc)
else:
data = generate_data_from_excel(file_doc, extension)
forest = build_forest(data)
create_charts(company, custom_chart=forest)
# trigger on_update for company to reset default accounts
set_default_accounts(company)
def generate_data_from_csv(file_name, as_dict=False):
''' read csv file and return the generated nested tree '''
if not file_name.endswith('.csv'):
frappe.throw("Only CSV files can be used to for importing data. Please check the file format you are trying to upload")
def get_file(file_name):
file_doc = frappe.get_doc("File", {"file_url": file_name})
parts = file_doc.get_extension()
extension = parts[1]
extension = extension.lstrip(".")
if extension not in ('csv', 'xlsx', 'xls'):
frappe.throw("Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload")
return file_doc, extension
def generate_data_from_csv(file_doc, as_dict=False):
''' read csv file and return the generated nested tree '''
file_doc = frappe.get_doc('File', {"file_url": file_name})
file_path = file_doc.get_full_path()
data = []
with open(file_path, 'r') as in_file:
csv_reader = list(csv.reader(in_file))
headers = csv_reader[1][1:]
del csv_reader[0:2] # delete top row and headers row
headers = csv_reader[0]
del csv_reader[0] # delete top row and headers row
for row in csv_reader:
if as_dict:
data.append({frappe.scrub(header): row[index+1] for index, header in enumerate(headers)})
data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)})
else:
if not row[2]: row[2] = row[1]
data.append(row[1:])
if not row[1]: row[1] = row[0]
data.append(row)
# convert csv data
return data
def generate_data_from_excel(file_doc, extension, as_dict=False):
content = file_doc.get_content()
if extension == "xlsx":
rows = read_xlsx_file_from_attached_file(fcontent=content)
elif extension == "xls":
rows = read_xls_file_from_attached_file(content)
data = []
headers = rows[0]
del rows[0]
for row in rows:
if as_dict:
data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)})
else:
if not row[1]: row[1] = row[0]
data.append(row)
return data
@frappe.whitelist()
def get_coa(doctype, parent, is_root=False, file_name=None):
''' called by tree view (to fetch node's children) '''
file_doc, extension = get_file(file_name)
parent = None if parent==_('All Accounts') else parent
forest = build_forest(generate_data_from_csv(file_name))
if extension == 'csv':
data = generate_data_from_csv(file_doc)
else:
data = generate_data_from_excel(file_doc, extension)
forest = build_forest(data)
accounts = build_tree_from_json("", chart_data=forest) # returns alist of dict in a tree render-able form
# filter out to show data for the selected node only
@ -91,6 +144,8 @@ def build_forest(data):
# returns the path of any node in list format
def return_parent(data, child):
from frappe import _
for row in data:
account_name, parent_account = row[0:2]
if parent_account == account_name == child:
@ -98,8 +153,9 @@ def build_forest(data):
elif account_name == child:
parent_account_list = return_parent(data, parent_account)
if not parent_account_list:
frappe.throw(_("The parent account {0} does not exists")
.format(parent_account))
frappe.throw(_("The parent account {0} does not exists in the uploaded template").format(
frappe.bold(parent_account)))
return [child] + parent_account_list
charts_map, paths = {}, []
@ -114,7 +170,7 @@ def build_forest(data):
error_messages.append("Row {0}: Please enter Account Name".format(line_no))
charts_map[account_name] = {}
if is_group == 1: charts_map[account_name]["is_group"] = is_group
if cint(is_group) == 1: charts_map[account_name]["is_group"] = is_group
if account_type: charts_map[account_name]["account_type"] = account_type
if root_type: charts_map[account_name]["root_type"] = root_type
if account_number: charts_map[account_name]["account_number"] = account_number
@ -132,24 +188,94 @@ def build_forest(data):
return out
def build_response_as_excel(writer):
filename = frappe.generate_hash("", 10)
with open(filename, 'wb') as f:
f.write(cstr(writer.getvalue()).encode('utf-8'))
f = open(filename)
reader = csv.reader(f)
from frappe.utils.xlsxutils import make_xlsx
xlsx_file = make_xlsx(reader, "Chart Of Accounts Importer Template")
f.close()
os.remove(filename)
# write out response as a xlsx type
frappe.response['filename'] = 'coa_importer_template.xlsx'
frappe.response['filecontent'] = xlsx_file.getvalue()
frappe.response['type'] = 'binary'
@frappe.whitelist()
def download_template():
def download_template(file_type, template_type):
data = frappe._dict(frappe.local.form_dict)
writer = get_template(template_type)
if file_type == 'CSV':
# download csv file
frappe.response['result'] = cstr(writer.getvalue())
frappe.response['type'] = 'csv'
frappe.response['doctype'] = 'Chart of Accounts Importer'
else:
build_response_as_excel(writer)
def get_template(template_type):
fields = ["Account Name", "Parent Account", "Account Number", "Is Group", "Account Type", "Root Type"]
writer = UnicodeWriter()
writer.writerow(fields)
writer.writerow([_('Chart of Accounts Template')])
writer.writerow([_("Column Labels : ")] + fields)
writer.writerow([_("Start entering data from here : ")])
if template_type == 'Blank Template':
for root_type in get_root_types():
writer.writerow(['', '', '', 1, '', root_type])
for account in get_mandatory_group_accounts():
writer.writerow(['', '', '', 1, account, "Asset"])
for account_type in get_mandatory_account_types():
writer.writerow(['', '', '', 0, account_type.get('account_type'), account_type.get('root_type')])
else:
writer = get_sample_template(writer)
return writer
def get_sample_template(writer):
template = [
["Application Of Funds(Assets)", "", "", 1, "", "Asset"],
["Sources Of Funds(Liabilities)", "", "", 1, "", "Liability"],
["Equity", "", "", 1, "", "Equity"],
["Expenses", "", "", 1, "", "Expense"],
["Income", "", "", 1, "", "Income"],
["Bank Accounts", "Application Of Funds(Assets)", "", 1, "Bank", "Asset"],
["Cash In Hand", "Application Of Funds(Assets)", "", 1, "Cash", "Asset"],
["Stock Assets", "Application Of Funds(Assets)", "", 1, "Stock", "Asset"],
["Cost Of Goods Sold", "Expenses", "", 0, "Cost of Goods Sold", "Expense"],
["Asset Depreciation", "Expenses", "", 0, "Depreciation", "Expense"],
["Fixed Assets", "Application Of Funds(Assets)", "", 0, "Fixed Asset", "Asset"],
["Accounts Payable", "Sources Of Funds(Liabilities)", "", 0, "Payable", "Liability"],
["Accounts Receivable", "Application Of Funds(Assets)", "", 1, "Receivable", "Asset"],
["Stock Expenses", "Expenses", "", 0, "Stock Adjustment", "Expense"],
["Sample Bank", "Bank Accounts", "", 0, "Bank", "Asset"],
["Cash", "Cash In Hand", "", 0, "Cash", "Asset"],
["Stores", "Stock Assets", "", 0, "Stock", "Asset"],
]
for row in template:
writer.writerow(row)
return writer
# download csv file
frappe.response['result'] = cstr(writer.getvalue())
frappe.response['type'] = 'csv'
frappe.response['doctype'] = data.get('doctype')
@frappe.whitelist()
def validate_accounts(file_name):
accounts = generate_data_from_csv(file_name, as_dict=True)
file_doc, extension = get_file(file_name)
if extension == 'csv':
accounts = generate_data_from_csv(file_doc, as_dict=True)
else:
accounts = generate_data_from_excel(file_doc, extension, as_dict=True)
accounts_dict = {}
for account in accounts:
@ -174,12 +300,38 @@ def validate_root(accounts):
for account in roots:
if not account.get("root_type") and account.get("account_name"):
error_messages.append("Please enter Root Type for account- {0}".format(account.get("account_name")))
elif account.get("root_type") not in ("Asset", "Liability", "Expense", "Income", "Equity") and account.get("account_name"):
elif account.get("root_type") not in get_root_types() and account.get("account_name"):
error_messages.append("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity".format(account.get("account_name")))
if error_messages:
return "<br>".join(error_messages)
def get_root_types():
return ('Asset', 'Liability', 'Expense', 'Income', 'Equity')
def get_report_type(root_type):
if root_type in ('Asset', 'Liability', 'Equity'):
return 'Balance Sheet'
else:
return 'Profit and Loss'
def get_mandatory_group_accounts():
return ('Bank', 'Cash', 'Stock')
def get_mandatory_account_types():
return [
{'account_type': 'Cost of Goods Sold', 'root_type': 'Expense'},
{'account_type': 'Depreciation', 'root_type': 'Expense'},
{'account_type': 'Fixed Asset', 'root_type': 'Asset'},
{'account_type': 'Payable', 'root_type': 'Liability'},
{'account_type': 'Receivable', 'root_type': 'Asset'},
{'account_type': 'Stock Adjustment', 'root_type': 'Expense'},
{'account_type': 'Bank', 'root_type': 'Asset'},
{'account_type': 'Cash', 'root_type': 'Asset'},
{'account_type': 'Stock', 'root_type': 'Asset'}
]
def validate_account_types(accounts):
account_types_for_ledger = ["Cost of Goods Sold", "Depreciation", "Fixed Asset", "Payable", "Receivable", "Stock Adjustment"]
account_types = [accounts[d]["account_type"] for d in accounts if not accounts[d]['is_group'] == 1]

View File

@ -18,7 +18,8 @@
"in_list_view": 1,
"label": "Invoice",
"options": "Sales Invoice",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"fetch_from": "sales_invoice.customer",
@ -60,7 +61,7 @@
}
],
"istable": 1,
"modified": "2019-09-26 11:05:36.016772",
"modified": "2020-02-20 16:16:20.724620",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Discounted Invoice",

View File

@ -7,4 +7,4 @@ from __future__ import unicode_literals
from frappe.model.document import Document
class DiscountedInvoice(Document):
pass
pass

View File

@ -232,11 +232,36 @@ def update_outstanding_amt(account, party_type, party, against_voucher_type, aga
if bal < 0 and not on_cancel:
frappe.throw(_("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal)))
# Update outstanding amt on against voucher
if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]:
update_outstanding_amt_in_ref(against_voucher, against_voucher_type, bal)
def update_outstanding_amt_in_ref(against_voucher, against_voucher_type, bal):
data = []
# Update outstanding amt on against voucher
if against_voucher_type == "Fees":
ref_doc = frappe.get_doc(against_voucher_type, against_voucher)
ref_doc.db_set('outstanding_amount', bal)
ref_doc.set_status(update=True)
return
elif against_voucher_type == "Purchase Invoice":
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import get_status
data = frappe.db.get_value(against_voucher_type, against_voucher,
["name as purchase_invoice", "outstanding_amount",
"is_return", "due_date", "docstatus"])
elif against_voucher_type == "Sales Invoice":
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_status
data = frappe.db.get_value(against_voucher_type, against_voucher,
["name as sales_invoice", "outstanding_amount", "is_discounted",
"is_return", "due_date", "docstatus"])
precision = frappe.get_precision(against_voucher_type, "outstanding_amount")
data = list(data)
data.append(precision)
status = get_status(data)
frappe.db.set_value(against_voucher_type, against_voucher, {
'outstanding_amount': bal,
'status': status
})
def validate_frozen_account(account, adv_adj=None):
frozen_account = frappe.db.get_value("Account", account, "freeze_account")
@ -274,6 +299,9 @@ def update_against_account(voucher_type, voucher_no):
if d.against != new_against:
frappe.db.set_value("GL Entry", d.name, "against", new_against)
def on_doctype_update():
frappe.db.add_index("GL Entry", ["against_voucher_type", "against_voucher"])
frappe.db.add_index("GL Entry", ["voucher_type", "voucher_no"])
def rename_gle_sle_docs():
for doctype in ["GL Entry", "Stock Ledger Entry"]:

View File

@ -9,7 +9,6 @@ from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.utils import get_balance_on, get_account_currency
from erpnext.accounts.party import get_party_account
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
from erpnext.hr.doctype.loan.loan import update_disbursement_status, update_total_amount_paid
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get_party_account_based_on_invoice_discounting
from six import string_types, iteritems
@ -50,7 +49,6 @@ class JournalEntry(AccountsController):
self.make_gl_entries()
self.update_advance_paid()
self.update_expense_claim()
self.update_loan()
self.update_inter_company_jv()
self.update_invoice_discounting()
@ -62,7 +60,6 @@ class JournalEntry(AccountsController):
self.make_gl_entries(1)
self.update_advance_paid()
self.update_expense_claim()
self.update_loan()
self.unlink_advance_entry_reference()
self.unlink_asset_reference()
self.unlink_inter_company_jv()
@ -460,8 +457,7 @@ class JournalEntry(AccountsController):
for d in self.get('accounts'):
if d.party_type in ['Customer', 'Supplier'] and d.party:
if not pay_to_recd_from:
pay_to_recd_from = frappe.db.get_value(d.party_type, d.party,
"customer_name" if d.party_type=="Customer" else "supplier_name")
pay_to_recd_from = d.party
if pay_to_recd_from and pay_to_recd_from == d.party:
party_amount += (d.debit_in_account_currency or d.credit_in_account_currency)
@ -472,7 +468,8 @@ class JournalEntry(AccountsController):
bank_account_currency = d.account_currency
if pay_to_recd_from:
self.pay_to_recd_from = pay_to_recd_from
self.pay_to_recd_from = frappe.db.get_value(d.party_type, pay_to_recd_from,
"customer_name" if d.party_type=="Customer" else "supplier_name")
if bank_amount:
total_amount = bank_amount
currency = bank_account_currency
@ -597,17 +594,6 @@ class JournalEntry(AccountsController):
doc = frappe.get_doc("Expense Claim", d.reference_name)
update_reimbursed_amount(doc)
def update_loan(self):
if self.paid_loan:
paid_loan = json.loads(self.paid_loan)
value = 1 if self.docstatus < 2 else 0
for name in paid_loan:
frappe.db.set_value("Repayment Schedule", name, "paid", value)
for d in self.accounts:
if d.reference_type=="Loan" and flt(d.debit) > 0:
doc = frappe.get_doc("Loan", d.reference_name)
update_disbursement_status(doc)
update_total_amount_paid(doc)
def validate_expense_claim(self):
for d in self.accounts:

View File

@ -253,6 +253,19 @@ frappe.ui.form.on('Payment Entry', {
frappe.throw(__("Party can only be one of "+ party_types.join(", ")));
}
frm.set_query("party", function() {
if(frm.doc.party_type == 'Employee'){
return {
query: "erpnext.controllers.queries.employee_query"
}
}
else if(frm.doc.party_type == 'Customer'){
return {
query: "erpnext.controllers.queries.customer_query"
}
}
});
if(frm.doc.party) {
$.each(["party", "party_balance", "paid_from", "paid_to",
"paid_from_account_currency", "paid_from_account_balance",

View File

@ -149,6 +149,49 @@ class TestPaymentEntry(unittest.TestCase):
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", pi.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 0)
def test_payment_against_sales_invoice_to_check_status(self):
si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
currency="USD", conversion_rate=50)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
pe.target_exchange_rate = 50
pe.insert()
pe.submit()
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 0)
self.assertEqual(si.status, 'Paid')
pe.cancel()
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 100)
self.assertEqual(si.status, 'Unpaid')
def test_payment_against_purchase_invoice_to_check_status(self):
pi = make_purchase_invoice(supplier="_Test Supplier USD", debit_to="_Test Payable USD - _TC",
currency="USD", conversion_rate=50)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
pe.source_exchange_rate = 50
pe.insert()
pe.submit()
outstanding_amount = flt(frappe.db.get_value("Purchase Invoice", pi.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 0)
self.assertEqual(pi.status, 'Paid')
pe.cancel()
outstanding_amount = flt(frappe.db.get_value("Purchase Invoice", pi.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 100)
self.assertEqual(pi.status, 'Unpaid')
def test_payment_entry_against_ec(self):
payable = frappe.get_cached_value('Company', "_Test Company", 'default_payable_account')
@ -566,4 +609,4 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(expected_party_account_balance, party_account_balance)
accounts_settings.allow_cost_center_in_entry_of_bs_account = 0
accounts_settings.save()
accounts_settings.save()

View File

@ -92,6 +92,7 @@ class PaymentReconciliation(Document):
FROM `tab{doc}`, `tabGL Entry`
WHERE
(`tab{doc}`.name = `tabGL Entry`.against_voucher or `tab{doc}`.name = `tabGL Entry`.voucher_no)
and `tab{doc}`.{party_type_field} = %(party)s
and `tab{doc}`.is_return = 1 and `tab{doc}`.return_against IS NULL
and `tabGL Entry`.against_voucher_type = %(voucher_type)s
and `tab{doc}`.docstatus = 1 and `tabGL Entry`.party = %(party)s
@ -99,12 +100,17 @@ class PaymentReconciliation(Document):
GROUP BY `tab{doc}`.name
Having
amount > 0
""".format(doc=voucher_type, dr_or_cr=dr_or_cr, reconciled_dr_or_cr=reconciled_dr_or_cr), {
'party': self.party,
'party_type': self.party_type,
'voucher_type': voucher_type,
'account': self.receivable_payable_account
}, as_dict=1)
""".format(
doc=voucher_type,
dr_or_cr=dr_or_cr,
reconciled_dr_or_cr=reconciled_dr_or_cr,
party_type_field=frappe.scrub(self.party_type)),
{
'party': self.party,
'party_type': self.party_type,
'voucher_type': voucher_type,
'account': self.receivable_payable_account
}, as_dict=1)
def add_payment_entries(self, entries):
self.set('payments', [])

View File

@ -29,27 +29,29 @@ class TestPOSProfile(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Profile`")
def make_pos_profile():
def make_pos_profile(**args):
frappe.db.sql("delete from `tabPOS Profile`")
args = frappe._dict(args)
pos_profile = frappe.get_doc({
"company": "_Test Company",
"cost_center": "_Test Cost Center - _TC",
"currency": "INR",
"company": args.company or "_Test Company",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"currency": args.currency or "INR",
"doctype": "POS Profile",
"expense_account": "_Test Account Cost for Goods Sold - _TC",
"income_account": "Sales - _TC",
"name": "_Test POS Profile",
"expense_account": args.expense_account or "_Test Account Cost for Goods Sold - _TC",
"income_account": args.income_account or "Sales - _TC",
"name": args.name or "_Test POS Profile",
"naming_series": "_T-POS Profile-",
"selling_price_list": "_Test Price List",
"territory": "_Test Territory",
"selling_price_list": args.selling_price_list or "_Test Price List",
"territory": args.territory or "_Test Territory",
"customer_group": frappe.db.get_value('Customer Group', {'is_group': 0}, 'name'),
"warehouse": "_Test Warehouse - _TC",
"write_off_account": "_Test Write Off - _TC",
"write_off_cost_center": "_Test Write Off Cost Center - _TC"
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"write_off_account": args.write_off_account or "_Test Write Off - _TC",
"write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC"
})
if not frappe.db.exists("POS Profile", "_Test POS Profile"):
if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"):
pos_profile.insert()
return pos_profile

View File

@ -248,7 +248,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
if pricing_rule.price_or_product_discount == "Price":
apply_price_discount_rule(pricing_rule, item_details, args)
else:
get_product_discount_rule(pricing_rule, item_details, doc)
get_product_discount_rule(pricing_rule, item_details, args, doc)
item_details.has_pricing_rule = 1

View File

@ -326,6 +326,66 @@ class TestPricingRule(unittest.TestCase):
self.assertEquals(item.discount_amount, 110)
self.assertEquals(item.rate, 990)
def test_pricing_rule_for_product_discount_on_same_item(self):
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
test_record = {
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule",
"apply_on": "Item Code",
"currency": "USD",
"items": [{
"item_code": "_Test Item",
}],
"selling": 1,
"rate_or_discount": "Discount Percentage",
"rate": 0,
"min_qty": 0,
"max_qty": 7,
"discount_percentage": 17.5,
"price_or_product_discount": "Product",
"same_item": 1,
"free_qty": 1,
"company": "_Test Company"
}
frappe.get_doc(test_record.copy()).insert()
# With pricing rule
so = make_sales_order(item_code="_Test Item", qty=1)
so.load_from_db()
self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item")
def test_pricing_rule_for_product_discount_on_different_item(self):
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
test_record = {
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule",
"apply_on": "Item Code",
"currency": "USD",
"items": [{
"item_code": "_Test Item",
}],
"selling": 1,
"rate_or_discount": "Discount Percentage",
"rate": 0,
"min_qty": 0,
"max_qty": 7,
"discount_percentage": 17.5,
"price_or_product_discount": "Product",
"same_item": 0,
"free_item": "_Test Item 2",
"free_qty": 1,
"company": "_Test Company"
}
frappe.get_doc(test_record.copy()).insert()
# With pricing rule
so = make_sales_order(item_code="_Test Item", qty=1)
so.load_from_db()
self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item 2")
def make_pricing_rule(**args):
args = frappe._dict(args)

View File

@ -245,7 +245,7 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
def validate_quantity_and_amount_for_suggestion(args, qty, amount, item_code, transaction_type):
fieldname, msg = '', ''
type_of_transaction = 'purcahse' if transaction_type == "buying" else "sale"
type_of_transaction = 'purchase' if transaction_type == 'buying' else 'sale'
for field, value in {'min_qty': qty, 'min_amt': amount}.items():
if (args.get(field) and value < args.get(field)
@ -435,7 +435,7 @@ def apply_pricing_rule_on_transaction(doc):
doc.calculate_taxes_and_totals()
elif d.price_or_product_discount == 'Product':
item_details = frappe._dict({'parenttype': doc.doctype})
get_product_discount_rule(d, item_details, doc)
get_product_discount_rule(d, item_details, doc=doc)
apply_pricing_rule_for_free_items(doc, item_details.free_item_data)
doc.set_missing_values()
@ -443,9 +443,10 @@ def get_applied_pricing_rules(item_row):
return (item_row.get("pricing_rules").split(',')
if item_row.get("pricing_rules") else [])
def get_product_discount_rule(pricing_rule, item_details, doc=None):
free_item = (pricing_rule.free_item
if not pricing_rule.same_item or pricing_rule.apply_on == 'Transaction' else item_details.item_code)
def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
free_item = pricing_rule.free_item
if pricing_rule.same_item:
free_item = item_details.item_code or args.item_code
if not free_item:
frappe.throw(_("Free item not set in the pricing rule {0}")
@ -464,7 +465,7 @@ def get_product_discount_rule(pricing_rule, item_details, doc=None):
item_details.free_item_data.update(item_data)
item_details.free_item_data['uom'] = pricing_rule.free_item_uom or item_data.stock_uom
item_details.free_item_data['conversion_factor'] = get_conversion_factor(free_item,
item_details.free_item_data['conversion_factor'] = get_conversion_factor(free_item,
item_details.free_item_data['uom']).get("conversion_factor", 1)
if item_details.get("parenttype") == 'Purchase Order':
@ -507,7 +508,7 @@ def validate_coupon_code(coupon_name):
frappe.throw(_("Sorry,coupon code validity has not started"))
elif coupon.valid_upto:
if coupon.valid_upto < getdate(today()) :
frappe.throw(_("Sorry,coupon code validity has expired"))
frappe.throw(_("Sorry,coupon code validity has expired"))
elif coupon.used>=coupon.maximum_use:
frappe.throw(_("Sorry,coupon code are exhausted"))
else:

View File

@ -125,6 +125,27 @@ class PurchaseInvoice(BuyingController):
else:
self.remarks = _("No Remarks")
def set_status(self, update=False, status=None, update_modified=True):
if self.is_new():
if self.get('amended_from'):
self.status = 'Draft'
return
if not status:
precision = self.precision("outstanding_amount")
args = [
self.name,
self.outstanding_amount,
self.is_return,
self.due_date,
self.docstatus,
precision
]
self.status = get_status(args)
if update:
self.db_set('status', self.status, update_modified = update_modified)
def set_missing_values(self, for_validate=False):
if not self.credit_to:
self.credit_to = get_party_account("Supplier", self.supplier, self.company)
@ -380,7 +401,7 @@ class PurchaseInvoice(BuyingController):
update_outstanding_amt(self.credit_to, "Supplier", self.supplier,
self.doctype, self.return_against if cint(self.is_return) and self.return_against else self.name)
if repost_future_gle and cint(self.update_stock) and self.auto_accounting_for_stock:
if (repost_future_gle or self.flags.repost_future_gle) and cint(self.update_stock) and self.auto_accounting_for_stock:
from erpnext.controllers.stock_controller import update_gl_entries_after
items, warehouses = self.get_items_and_warehouses()
update_gl_entries_after(self.posting_date, self.posting_time,
@ -1007,6 +1028,34 @@ class PurchaseInvoice(BuyingController):
# calculate totals again after applying TDS
self.calculate_taxes_and_totals()
def get_status(*args):
purchase_invoice, outstanding_amount, is_return, due_date, docstatus, precision = args[0]
outstanding_amount = flt(outstanding_amount, precision)
due_date = getdate(due_date)
now_date = getdate()
if docstatus == 2:
status = "Cancelled"
elif docstatus == 1:
if outstanding_amount > 0 and due_date < now_date:
status = "Overdue"
elif outstanding_amount > 0 and due_date >= now_date:
status = "Unpaid"
#Check if outstanding amount is 0 due to debit note issued against invoice
elif outstanding_amount <= 0 and is_return == 0 and frappe.db.get_value('Purchase Invoice', {'is_return': 1, 'return_against': purchase_invoice, 'docstatus': 1}):
status = "Debit Note Issued"
elif is_return == 1:
status = "Return"
elif outstanding_amount <=0:
status = "Paid"
else:
status = "Submitted"
else:
status = "Draft"
return status
def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context
list_context = get_list_context(context)

View File

@ -63,7 +63,6 @@
"warehouse_section",
"warehouse",
"rejected_warehouse",
"from_warehouse",
"quality_inspection",
"batch_no",
"col_br_wh",
@ -199,7 +198,6 @@
"fieldtype": "Link",
"label": "UOM",
"options": "UOM",
"print_hide": 1,
"reqd": 1
},
{
@ -763,22 +761,16 @@
"fetch_from": "item_code.asset_category",
"fieldname": "asset_category",
"fieldtype": "Data",
"in_preview": 1,
"label": "Asset Category",
"options": "Asset Category",
"read_only": 1
},
{
"fieldname": "from_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Supplier Warehouse",
"options": "Warehouse"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2020-01-13 16:04:14.200462",
"modified": "2020-03-05 14:20:17.297284",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@ -207,7 +207,7 @@ def get_customers_list(pos_profile={}):
if pos_profile.get('customer_groups'):
# Get customers based on the customer groups defined in the POS profile
for d in pos_profile.get('customer_groups'):
customer_groups.extend([d.name for d in get_child_nodes('Customer Group', d.customer_group)])
customer_groups.extend([d.get('name') for d in get_child_nodes('Customer Group', d.get('customer_group'))])
cond = "customer_group in (%s)" % (', '.join(['%s'] * len(customer_groups)))
return frappe.db.sql(""" select name, customer_name, customer_group,
@ -387,7 +387,9 @@ def get_pricing_rule_data(doc):
@frappe.whitelist()
def make_invoice(doc_list={}, email_queue_list={}, customers_list={}):
def make_invoice(pos_profile, doc_list={}, email_queue_list={}, customers_list={}):
import json
if isinstance(doc_list, string_types):
doc_list = json.loads(doc_list)
@ -421,7 +423,8 @@ def make_invoice(doc_list={}, email_queue_list={}, customers_list={}):
name_list.append(name)
email_queue = make_email_queue(email_queue_list)
customers = get_customers_list()
pos_profile = json.loads(pos_profile)
customers = get_customers_list(pos_profile)
return {
'invoice': name_list,
'email_queue': email_queue,

View File

@ -420,7 +420,7 @@ class SalesInvoice(SellingController):
if pos:
self.allow_print_before_pay = pos.allow_print_before_pay
if not for_validate:
self.tax_category = pos.get("tax_category")
@ -723,7 +723,7 @@ class SalesInvoice(SellingController):
update_outstanding_amt(self.debit_to, "Customer", self.customer,
self.doctype, self.return_against if cint(self.is_return) and self.return_against else self.name)
if repost_future_gle and cint(self.update_stock) \
if (repost_future_gle or self.flags.repost_future_gle) and cint(self.update_stock) \
and cint(auto_accounting_for_stock):
items, warehouses = self.get_items_and_warehouses()
update_gl_entries_after(self.posting_date, self.posting_time,
@ -1217,63 +1217,84 @@ class SalesInvoice(SellingController):
self.set_missing_values(for_validate = True)
def get_discounting_status(self):
status = None
if self.is_discounted:
invoice_discounting_list = frappe.db.sql("""
select status
from `tabInvoice Discounting` id, `tabDiscounted Invoice` d
where
id.name = d.parent
and d.sales_invoice=%s
and id.docstatus=1
and status in ('Disbursed', 'Settled')
""", self.name)
for d in invoice_discounting_list:
status = d[0]
if status == "Disbursed":
break
return status
def set_status(self, update=False, status=None, update_modified=True):
if self.is_new():
if self.get('amended_from'):
self.status = 'Draft'
return
precision = self.precision("outstanding_amount")
outstanding_amount = flt(self.outstanding_amount, precision)
due_date = getdate(self.due_date)
nowdate = getdate()
discountng_status = self.get_discounting_status()
if not status:
if self.docstatus == 2:
status = "Cancelled"
elif self.docstatus == 1:
if outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discountng_status=='Disbursed':
self.status = "Overdue and Discounted"
elif outstanding_amount > 0 and due_date < nowdate:
self.status = "Overdue"
elif outstanding_amount > 0 and due_date >= nowdate and self.is_discounted and discountng_status=='Disbursed':
self.status = "Unpaid and Discounted"
elif outstanding_amount > 0 and due_date >= nowdate:
self.status = "Unpaid"
#Check if outstanding amount is 0 due to credit note issued against invoice
elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
self.status = "Credit Note Issued"
elif self.is_return == 1:
self.status = "Return"
elif outstanding_amount<=0:
self.status = "Paid"
else:
self.status = "Submitted"
else:
self.status = "Draft"
precision = self.precision("outstanding_amount")
args = [
self.name,
self.outstanding_amount,
self.is_discounted,
self.is_return,
self.due_date,
self.docstatus,
precision,
]
self.status = get_status(args)
if update:
self.db_set('status', self.status, update_modified = update_modified)
def get_discounting_status(sales_invoice):
status = None
invoice_discounting_list = frappe.db.sql("""
select status
from `tabInvoice Discounting` id, `tabDiscounted Invoice` d
where
id.name = d.parent
and d.sales_invoice=%s
and id.docstatus=1
and status in ('Disbursed', 'Settled')
""", sales_invoice)
for d in invoice_discounting_list:
status = d[0]
if status == "Disbursed":
break
return status
def get_status(*args):
sales_invoice, outstanding_amount, is_discounted, is_return, due_date, docstatus, precision = args[0]
discounting_status = None
if is_discounted:
discounting_status = get_discounting_status(sales_invoice)
outstanding_amount = flt(outstanding_amount, precision)
due_date = getdate(due_date)
now_date = getdate()
if docstatus == 2:
status = "Cancelled"
elif docstatus == 1:
if outstanding_amount > 0 and due_date < now_date and is_discounted and discounting_status=='Disbursed':
status = "Overdue and Discounted"
elif outstanding_amount > 0 and due_date < now_date:
status = "Overdue"
elif outstanding_amount > 0 and due_date >= now_date and is_discounted and discounting_status=='Disbursed':
status = "Unpaid and Discounted"
elif outstanding_amount > 0 and due_date >= now_date:
status = "Unpaid"
#Check if outstanding amount is 0 due to credit note issued against invoice
elif outstanding_amount <= 0 and is_return == 0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': sales_invoice, 'docstatus': 1}):
status = "Credit Note Issued"
elif is_return == 1:
status = "Return"
elif outstanding_amount <=0:
status = "Paid"
else:
status = "Submitted"
else:
status = "Draft"
return status
def validate_inter_company_party(doctype, party, company, inter_company_reference):
if not party:
return
@ -1548,6 +1569,9 @@ def get_loyalty_programs(customer):
else:
return lp_details
def on_doctype_update():
frappe.db.add_index("Sales Invoice", ["customer", "is_return", "return_against"])
@frappe.whitelist()
def create_invoice_discounting(source_name, target_doc=None):
invoice = frappe.get_doc("Sales Invoice", source_name)

View File

@ -705,6 +705,64 @@ class TestSalesInvoice(unittest.TestCase):
self.pos_gl_entry(si, pos, 50)
def test_pos_returns_without_repayment(self):
pos_profile = make_pos_profile()
pos = create_sales_invoice(qty = 10, do_not_save=True)
pos.is_pos = 1
pos.pos_profile = pos_profile.name
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500})
pos.insert()
pos.submit()
pos_return = create_sales_invoice(is_return=1,
return_against=pos.name, qty=-5, do_not_save=True)
pos_return.is_pos = 1
pos_return.pos_profile = pos_profile.name
pos_return.insert()
pos_return.submit()
self.assertFalse(pos_return.is_pos)
self.assertFalse(pos_return.get('payments'))
def test_pos_returns_with_repayment(self):
pos_profile = make_pos_profile()
pos_profile.append('payments', {
'default': 1,
'mode_of_payment': 'Cash',
'amount': 0.0
})
pos_profile.save()
pos = create_sales_invoice(qty = 10, do_not_save=True)
pos.is_pos = 1
pos.pos_profile = pos_profile.name
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500})
pos.insert()
pos.submit()
pos_return = create_sales_invoice(is_return=1,
return_against=pos.name, qty=-5, do_not_save=True)
pos_return.is_pos = 1
pos_return.pos_profile = pos_profile.name
pos_return.insert()
pos_return.submit()
self.assertEqual(pos_return.get('payments')[0].amount, -500)
pos_profile.payments = []
pos_profile.save()
def test_pos_change_amount(self):
make_pos_profile()

View File

@ -140,8 +140,11 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle = frappe.get_doc(args)
gle.flags.ignore_permissions = 1
gle.flags.from_repost = from_repost
gle.insert()
gle.validate()
gle.flags.ignore_permissions = True
gle.db_insert()
gle.run_method("on_update_with_args", adv_adj, update_outstanding, from_repost)
gle.flags.ignore_validate = True
gle.submit()
def validate_account_for_perpetual_inventory(gl_map):

View File

@ -1769,6 +1769,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
method: "erpnext.accounts.doctype.sales_invoice.pos.make_invoice",
freeze: true,
args: {
pos_profile: me.pos_profile_data,
doc_list: me.si_docs,
email_queue_list: me.email_queue_list,
customers_list: me.customers_list

View File

@ -11,18 +11,18 @@ frappe.query_reports["Accounts Payable"] = {
"reqd": 1,
"default": frappe.defaults.get_user_default("Company")
},
{
"fieldname":"report_date",
"label": __("Posting Date"),
"fieldtype": "Date",
"default": frappe.datetime.get_today()
},
{
"fieldname":"ageing_based_on",
"label": __("Ageing Based On"),
"fieldtype": "Select",
"options": 'Posting Date\nDue Date\nSupplier Invoice Date',
"default": "Posting Date"
},
{
"fieldname":"report_date",
"label": __("As on Date"),
"fieldtype": "Date",
"default": frappe.datetime.get_today()
"default": "Due Date"
},
{
"fieldname":"range1",

View File

@ -10,18 +10,18 @@ frappe.query_reports["Accounts Payable Summary"] = {
"options": "Company",
"default": frappe.defaults.get_user_default("Company")
},
{
"fieldname":"report_date",
"label": __("Posting Date"),
"fieldtype": "Date",
"default": frappe.datetime.get_today()
},
{
"fieldname":"ageing_based_on",
"label": __("Ageing Based On"),
"fieldtype": "Select",
"options": 'Posting Date\nDue Date',
"default": "Posting Date"
},
{
"fieldname":"report_date",
"label": __("Date"),
"fieldtype": "Date",
"default": frappe.datetime.get_today()
"default": "Due Date"
},
{
"fieldname":"range1",

View File

@ -11,18 +11,18 @@ frappe.query_reports["Accounts Receivable"] = {
"reqd": 1,
"default": frappe.defaults.get_user_default("Company")
},
{
"fieldname":"report_date",
"label": __("Posting Date"),
"fieldtype": "Date",
"default": frappe.datetime.get_today()
},
{
"fieldname":"ageing_based_on",
"label": __("Ageing Based On"),
"fieldtype": "Select",
"options": 'Posting Date\nDue Date',
"default": "Posting Date"
},
{
"fieldname":"report_date",
"label": __("As on Date"),
"fieldtype": "Date",
"default": frappe.datetime.get_today()
"default": "Due Date"
},
{
"fieldname":"range1",
@ -148,7 +148,7 @@ frappe.query_reports["Accounts Receivable"] = {
},
{
"fieldname":"show_delivery_notes",
"label": __("Show Delivery Notes"),
"label": __("Show Linked Delivery Notes"),
"fieldtype": "Check",
},
{

View File

@ -10,18 +10,18 @@ frappe.query_reports["Accounts Receivable Summary"] = {
"options": "Company",
"default": frappe.defaults.get_user_default("Company")
},
{
"fieldname":"report_date",
"label": __("Posting Date"),
"fieldtype": "Date",
"default": frappe.datetime.get_today()
},
{
"fieldname":"ageing_based_on",
"label": __("Ageing Based On"),
"fieldtype": "Select",
"options": 'Posting Date\nDue Date',
"default": "Posting Date"
},
{
"fieldname":"report_date",
"label": __("Date"),
"fieldtype": "Date",
"default": frappe.datetime.get_today()
"default": "Due Date"
},
{
"fieldname":"range1",

View File

@ -29,7 +29,7 @@ def get_data(filters):
row.update(next(asset for asset in assets if asset["asset_category"] == asset_category.get("asset_category", "")))
row.accumulated_depreciation_as_on_to_date = (flt(row.accumulated_depreciation_as_on_from_date) +
flt(row.depreciation_amount_during_the_period) - flt(row.depreciation_eliminated))
flt(row.depreciation_amount_during_the_period) - flt(row.depreciation_eliminated_during_the_period))
row.net_asset_value_as_on_from_date = (flt(row.cost_as_on_from_date) -
flt(row.accumulated_depreciation_as_on_from_date))
@ -86,7 +86,6 @@ def get_asset_categories(filters):
group by asset_category
""", {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, as_dict=1)
def get_assets(filters):
return frappe.db.sql("""
SELECT results.asset_category,
@ -94,9 +93,7 @@ def get_assets(filters):
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
from (SELECT a.asset_category,
ifnull(sum(a.opening_accumulated_depreciation +
case when ds.schedule_date < %(from_date)s and
(ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
ifnull(sum(case when ds.schedule_date < %(from_date)s then
ds.depreciation_amount
else
0
@ -107,7 +104,6 @@ def get_assets(filters):
else
0
end), 0) as depreciation_eliminated_during_the_period,
ifnull(sum(case when ds.schedule_date >= %(from_date)s and ds.schedule_date <= %(to_date)s
and (ifnull(a.disposal_date, 0) = 0 or ds.schedule_date <= a.disposal_date) then
ds.depreciation_amount
@ -120,7 +116,8 @@ def get_assets(filters):
union
SELECT a.asset_category,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s)
then
0
else
a.opening_accumulated_depreciation
@ -133,7 +130,6 @@ def get_assets(filters):
0 as depreciation_amount_during_the_period
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s
and not exists(select * from `tabDepreciation Schedule` ds where a.name = ds.parent)
group by a.asset_category) as results
group by results.asset_category
""", {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, as_dict=1)

View File

@ -54,8 +54,8 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
'description': d.description,
'invoice': d.parent,
'posting_date': d.posting_date,
'customer': d.supplier,
'customer_name': d.supplier_name
'supplier': d.supplier,
'supplier_name': d.supplier_name
}
if additional_query_columns:
@ -306,10 +306,6 @@ def get_conditions(filters):
def get_items(filters, additional_query_columns):
conditions = get_conditions(filters)
match_conditions = frappe.build_match_conditions("Purchase Invoice")
if match_conditions:
match_conditions = " and {0} ".format(match_conditions)
if additional_query_columns:
additional_query_columns = ', ' + ', '.join(additional_query_columns)
@ -327,8 +323,8 @@ def get_items(filters, additional_query_columns):
`tabPurchase Invoice`.supplier_name, `tabPurchase Invoice`.mode_of_payment {0}
from `tabPurchase Invoice`, `tabPurchase Invoice Item`
where `tabPurchase Invoice`.name = `tabPurchase Invoice Item`.`parent` and
`tabPurchase Invoice`.docstatus = 1 %s %s
""".format(additional_query_columns) % (conditions, match_conditions), filters, as_dict=1)
`tabPurchase Invoice`.docstatus = 1 %s
""".format(additional_query_columns) % (conditions), filters, as_dict=1)
def get_aii_accounts():
return dict(frappe.db.sql("select name, stock_received_but_not_billed from tabCompany"))

View File

@ -119,7 +119,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
add_sub_total_row(total_row, total_row_map, 'total_row', tax_columns)
data.append(total_row_map.get('total_row'))
skip_total_row = 1
return columns, data, None, None, None, skip_total_row
def get_columns(additional_table_columns, filters):
@ -370,10 +370,6 @@ def get_group_by_conditions(filters, doctype):
def get_items(filters, additional_query_columns):
conditions = get_conditions(filters)
match_conditions = frappe.build_match_conditions("Sales Invoice")
if match_conditions:
match_conditions = " and {0} ".format(match_conditions)
if additional_query_columns:
additional_query_columns = ', ' + ', '.join(additional_query_columns)
@ -394,8 +390,8 @@ def get_items(filters, additional_query_columns):
`tabSales Invoice`.update_stock, `tabSales Invoice Item`.uom, `tabSales Invoice Item`.qty {0}
from `tabSales Invoice`, `tabSales Invoice Item`
where `tabSales Invoice`.name = `tabSales Invoice Item`.parent
and `tabSales Invoice`.docstatus = 1 {1} {2}
""".format(additional_query_columns or '', conditions, match_conditions), filters, as_dict=1) #nosec
and `tabSales Invoice`.docstatus = 1 {1}
""".format(additional_query_columns or '', conditions), filters, as_dict=1) #nosec
def get_delivery_notes_against_sales_order(item_list):
so_dn_map = frappe._dict()

View File

@ -31,7 +31,52 @@ def execute(filters=None):
chart = get_chart_data(filters, columns, income, expense, net_profit_loss)
return columns, data, None, chart
report_summary = get_report_summary(columns, income, expense, net_profit_loss, filters.periodicity, period_list)
return columns, data, None, chart, report_summary
def get_report_summary(columns, income, expense, net_profit_loss, period_list, periodicity):
income_data, expense_data, net_profit = [], [], []
for p in columns[2:]:
if income:
income_data.append(income[-2].get(p.get("fieldname")))
if expense:
expense_data.append(expense[-2].get(p.get("fieldname")))
if net_profit_loss:
net_profit.append(net_profit_loss.get(p.get("fieldname")))
if (len(period_list) == 1 and periodicity== 'Yearly'):
profit_label = _("Profit This Year")
income_label = _("Total Income This Year")
expense_label = _("Total Expense This Year")
else:
profit_label = _("Net Profit")
income_label = _("Total Income")
expense_label = _("Total Expense")
return [
{
"value": net_profit[-1],
"indicator": "Green" if net_profit[-1] > 0 else "Red",
"label": profit_label,
"datatype": "Currency",
"currency": net_profit_loss.get("currency")
},
{
"value": income_data[-1],
"label": income_label,
"datatype": "Currency",
"currency": income[-1].get('currency')
},
{
"value": expense_data[-1],
"label": expense_label,
"datatype": "Currency",
"currency": expense[-1].get('currency')
}
]
def get_net_profit_loss(income, expense, period_list, company, currency=None, consolidated=False):
total = 0

View File

@ -12,7 +12,6 @@ from erpnext.stock.doctype.item.item import validate_end_of_life
def update_last_purchase_rate(doc, is_submit):
"""updates last_purchase_rate in item table for each item"""
import frappe.utils
this_purchase_date = frappe.utils.getdate(doc.get('posting_date') or doc.get('transaction_date'))
@ -23,7 +22,7 @@ def update_last_purchase_rate(doc, is_submit):
# compare last purchase date and this transaction's date
last_purchase_rate = None
if last_purchase_details and \
(last_purchase_details.purchase_date > this_purchase_date):
(doc.get('docstatus') == 2 or last_purchase_details.purchase_date > this_purchase_date):
last_purchase_rate = last_purchase_details['base_net_rate']
elif is_submit == 1:
# even if this transaction is the latest one, it should be submitted

View File

@ -7,6 +7,13 @@ def get_data():
"label": _("Purchasing"),
"icon": "fa fa-star",
"items": [
{
"type": "doctype",
"name": "Material Request",
"onboard": 1,
"dependencies": ["Item"],
"description": _("Request for purchase."),
},
{
"type": "doctype",
"name": "Purchase Order",
@ -20,13 +27,6 @@ def get_data():
"onboard": 1,
"dependencies": ["Item", "Supplier"]
},
{
"type": "doctype",
"name": "Material Request",
"onboard": 1,
"dependencies": ["Item"],
"description": _("Request for purchase."),
},
{
"type": "doctype",
"name": "Request for Quotation",
@ -63,6 +63,11 @@ def get_data():
"name": "Price List",
"description": _("Price List master.")
},
{
"type": "doctype",
"name": "Pricing Rule",
"description": _("Rules for applying pricing and discount.")
},
{
"type": "doctype",
"name": "Product Bundle",
@ -80,11 +85,6 @@ def get_data():
"type": "doctype",
"name": "Promotional Scheme",
"description": _("Rules for applying different promotional schemes.")
},
{
"type": "doctype",
"name": "Pricing Rule",
"description": _("Rules for applying pricing and discount.")
}
]
},
@ -149,13 +149,6 @@ def get_data():
"reference_doctype": "Purchase Order",
"onboard": 1
},
{
"type": "report",
"is_query_report": True,
"name": "Supplier-Wise Sales Analytics",
"reference_doctype": "Stock Ledger Entry",
"onboard": 1
},
{
"type": "report",
"is_query_report": True,
@ -177,6 +170,16 @@ def get_data():
"reference_doctype": "Material Request",
"onboard": 1,
},
{
"type": "report",
"is_query_report": True,
"name": "Address And Contacts",
"label": _("Supplier Addresses And Contacts"),
"reference_doctype": "Address",
"route_options": {
"party_type": "Supplier"
}
}
]
},
{
@ -226,18 +229,15 @@ def get_data():
{
"type": "report",
"is_query_report": True,
"name": "Material Requests for which Supplier Quotations are not created",
"reference_doctype": "Material Request"
"name": "Supplier-Wise Sales Analytics",
"reference_doctype": "Stock Ledger Entry",
"onboard": 1
},
{
"type": "report",
"is_query_report": True,
"name": "Address And Contacts",
"label": _("Supplier Addresses And Contacts"),
"reference_doctype": "Address",
"route_options": {
"party_type": "Supplier"
}
"name": "Material Requests for which Supplier Quotations are not created",
"reference_doctype": "Material Request"
}
]
},

View File

@ -80,6 +80,15 @@ def get_data():
"type": "module",
"description": "Sales pipeline, leads, opportunities and customers."
},
{
"module_name": "Loan Management",
"category": "Modules",
"label": _("Loan Management"),
"color": "#EF4DB6",
"icon": "octicon octicon-repo",
"type": "module",
"description": "Loan Management for Customer and Employees"
},
{
"module_name": "Support",
"category": "Modules",

View File

@ -0,0 +1,107 @@
from __future__ import unicode_literals
from frappe import _
import frappe
def get_data():
return [
{
"label": _("Loan"),
"items": [
{
"type": "doctype",
"name": "Loan Type",
"description": _("Loan Type for interest and penalty rates"),
},
{
"type": "doctype",
"name": "Loan Application",
"description": _("Loan Applications from customers and employees."),
},
{
"type": "doctype",
"name": "Loan",
"description": _("Loans provided to customers and employees."),
},
]
},
{
"label": _("Loan Security"),
"items": [
{
"type": "doctype",
"name": "Loan Security Type",
},
{
"type": "doctype",
"name": "Loan Security Price",
},
{
"type": "doctype",
"name": "Loan Security",
},
{
"type": "doctype",
"name": "Loan Security Pledge",
},
{
"type": "doctype",
"name": "Loan Security Unpledge",
},
{
"type": "doctype",
"name": "Loan Security Shortfall",
},
]
},
{
"label": _("Disbursement and Repayment"),
"items": [
{
"type": "doctype",
"name": "Loan Disbursement",
},
{
"type": "doctype",
"name": "Loan Repayment",
},
{
"type": "doctype",
"name": "Loan Interest Accrual"
}
]
},
{
"label": _("Loan Processes"),
"items": [
{
"type": "doctype",
"name": "Process Loan Security Shortfall",
},
{
"type": "doctype",
"name": "Process Loan Interest Accrual",
}
]
},
{
"label": _("Reports"),
"items": [
{
"type": "report",
"is_query_report": True,
"name": "Loan Repayment and Closure",
"route": "#query-report/Loan Repayment and Closure",
"doctype": "Loan Repayment",
},
{
"type": "report",
"is_query_report": True,
"name": "Loan Security Status",
"route": "#query-report/Loan Security Status",
"doctype": "Loan Security Pledge",
}
]
}
]

View File

@ -1200,6 +1200,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
child_item = set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, d.get("item_code"))
else:
child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname"))
if flt(child_item.get("rate")) == flt(d.get("rate")) and flt(child_item.get("qty")) == flt(d.get("qty")):
continue
if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty):
frappe.throw(_("Cannot set quantity less than delivered quantity"))

View File

@ -44,17 +44,6 @@ status_map = {
["Closed", "eval:self.status=='Closed'"],
["On Hold", "eval:self.status=='On Hold'"],
],
"Purchase Invoice": [
["Draft", None],
["Submitted", "eval:self.docstatus==1"],
["Paid", "eval:self.outstanding_amount==0 and self.docstatus==1"],
["Return", "eval:self.is_return==1 and self.docstatus==1"],
["Debit Note Issued",
"eval:self.outstanding_amount <= 0 and self.docstatus==1 and self.is_return==0 and get_value('Purchase Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1})"],
["Unpaid", "eval:self.outstanding_amount > 0 and getdate(self.due_date) >= getdate(nowdate()) and self.docstatus==1"],
["Overdue", "eval:self.outstanding_amount > 0 and getdate(self.due_date) < getdate(nowdate()) and self.docstatus==1"],
["Cancelled", "eval:self.docstatus==2"],
],
"Purchase Order": [
["Draft", None],
["To Receive and Bill", "eval:self.per_received < 100 and self.per_billed < 100 and self.docstatus == 1"],

View File

@ -34,7 +34,7 @@ class StockController(AccountsController):
gl_entries = self.get_gl_entries(warehouse_account)
make_gl_entries(gl_entries, from_repost=from_repost)
if repost_future_gle:
if (repost_future_gle or self.flags.repost_future_gle):
items, warehouses = self.get_items_and_warehouses()
update_gl_entries_after(self.posting_date, self.posting_time, warehouses, items,
warehouse_account, company=self.company)
@ -238,6 +238,10 @@ class StockController(AccountsController):
for d in self.items:
if not d.batch_no: continue
serial_nos = [d.name for d in frappe.get_all("Serial No", {'batch_no': d.batch_no})]
if serial_nos:
frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None)
d.batch_no = None
d.db_set("batch_no", None)
@ -429,7 +433,7 @@ def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, f
for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
from `tabStock Ledger Entry` sle
where timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s) {condition}
order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc""".format(condition=condition),
order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format(condition=condition),
tuple([posting_date, posting_time] + values), as_dict=True):
future_stock_vouchers.append([d.voucher_type, d.voucher_no])

View File

@ -514,7 +514,7 @@ class calculate_taxes_and_totals(object):
if self.doc.doctype == "Sales Invoice":
self.calculate_paid_amount()
if self.doc.is_return and self.doc.return_against: return
if self.doc.is_return and self.doc.return_against and not self.doc.get('is_pos'): return
self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"])
self._set_in_company_currency(self.doc, ['write_off_amount'])
@ -532,7 +532,7 @@ class calculate_taxes_and_totals(object):
self.doc.round_floats_in(self.doc, ["paid_amount"])
change_amount = 0
if self.doc.doctype == "Sales Invoice":
if self.doc.doctype == "Sales Invoice" and not self.doc.get('is_return'):
self.calculate_write_off_amount()
self.calculate_change_amount()
change_amount = self.doc.change_amount \
@ -544,6 +544,9 @@ class calculate_taxes_and_totals(object):
self.doc.outstanding_amount = flt(total_amount_to_pay - flt(paid_amount) + flt(change_amount),
self.doc.precision("outstanding_amount"))
if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'):
self.update_paid_amount_for_return(total_amount_to_pay)
def calculate_paid_amount(self):
paid_amount = base_paid_amount = 0.0
@ -614,6 +617,27 @@ class calculate_taxes_and_totals(object):
def set_item_wise_tax_breakup(self):
self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc)
def update_paid_amount_for_return(self, total_amount_to_pay):
default_mode_of_payment = frappe.db.get_value('Sales Invoice Payment',
{'parent': self.doc.pos_profile, 'default': 1},
['mode_of_payment', 'type', 'account'], as_dict=1)
self.doc.payments = []
if default_mode_of_payment:
self.doc.append('payments', {
'mode_of_payment': default_mode_of_payment.mode_of_payment,
'type': default_mode_of_payment.type,
'account': default_mode_of_payment.account,
'amount': total_amount_to_pay
})
else:
self.doc.is_pos = 0
self.doc.pos_profile = ''
self.calculate_paid_amount()
def get_itemised_tax_breakup_html(doc):
if not doc.taxes:
return

View File

@ -59,6 +59,7 @@ frappe.ui.form.on("Opportunity", {
contact_person: erpnext.utils.get_contact_details,
opportunity_from: function(frm) {
frm.trigger('setup_queries');
frm.toggle_reqd("party_name", frm.doc.opportunity_from);
frm.trigger("set_dynamic_field_label");
},

View File

@ -6,21 +6,21 @@ frappe.ui.form.on("Course", "refresh", function(frm) {
}
frappe.set_route("List", "Program");
});
frm.add_custom_button(__("Student Group"), function() {
frappe.route_options = {
course: frm.doc.name
}
frappe.set_route("List", "Student Group");
});
frm.add_custom_button(__("Course Schedule"), function() {
frappe.route_options = {
course: frm.doc.name
}
frappe.set_route("List", "Course Schedule");
});
frm.add_custom_button(__("Assessment Plan"), function() {
frappe.route_options = {
course: frm.doc.name
@ -36,4 +36,17 @@ frappe.ui.form.on("Course", "refresh", function(frm) {
}
}
});
});
});
frappe.ui.form.on('Course Topic', {
topics_add: function(frm){
frm.fields_dict['topics'].grid.get_field('topic').get_query = function(doc){
var topics_list = [];
if(!doc.__islocal) topics_list.push(doc.name);
$.each(doc.topics, function(idx, val){
if (val.topic) topics_list.push(val.topic);
});
return { filters: [['Topic', 'name', 'not in', topics_list]] };
};
}
});

View File

@ -12,7 +12,6 @@ frappe.ui.form.on("Instructor", {
}
};
});
frm.set_query("department", "instructor_log", function() {
return {
"filters": {
@ -49,5 +48,12 @@ frappe.ui.form.on("Instructor", {
frappe.set_route("List", "Assessment Plan");
}, __("Assessment Plan"));
}
frm.set_query("employee", function(doc) {
return {
"filters": {
"department": doc.department,
}
};
});
}
});
});

View File

@ -81,3 +81,16 @@ frappe.ui.form.on("Program Enrollment", {
})
}
});
frappe.ui.form.on('Program Enrollment Course', {
courses_add: function(frm){
frm.fields_dict['courses'].grid.get_field('course').get_query = function(doc){
var course_list = [];
if(!doc.__islocal) course_list.push(doc.name);
$.each(doc.courses, function(idx, val){
if (val.course) course_list.push(val.course);
});
return { filters: [['Course', 'name', 'not in', course_list]] };
};
}
});

View File

@ -4,5 +4,18 @@
frappe.ui.form.on('Quiz', {
refresh: function(frm) {
},
validate: function(frm){
frm.events.check_duplicate_question(frm.doc.question);
},
check_duplicate_question: function(questions_data){
var questions = [];
questions_data.forEach(function(q){
questions.push(q.question_link);
});
var questions_set = new Set(questions);
if (questions.length != questions_set.size) {
frappe.throw(__("The question cannot be duplicate"));
}
}
});
});

View File

@ -22,6 +22,10 @@ class Student(Document):
self.update_student_name_in_linked_doctype()
def validate_dates(self):
for sibling in self.siblings:
if sibling.date_of_birth and getdate(sibling.date_of_birth) > getdate():
frappe.throw(_("Row {0}:Sibling Date of Birth cannot be greater than today.").format(sibling.idx))
if self.date_of_birth and getdate(self.date_of_birth) >= getdate(today()):
frappe.throw(_("Date of Birth cannot be greater than today."))

View File

@ -11,5 +11,12 @@ frappe.ui.form.on('Student Admission', {
academic_year: function(frm) {
frm.trigger("program");
},
admission_end_date: function(frm) {
if(frm.doc.admission_end_date && frm.doc.admission_end_date <= frm.doc.admission_start_date){
frm.set_value("admission_end_date", "");
frappe.throw(__("Admission End Date should be greater than Admission Start Date."));
}
}
});

View File

@ -314,12 +314,15 @@ scheduler_events = {
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms",
"erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation",
"erpnext.hr.utils.generate_leave_encashment"
"erpnext.hr.utils.generate_leave_encashment",
"erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall.check_for_ltv_shortfall",
"erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.make_accrual_interest_entry_for_term_loans"
],
"monthly_long": [
"erpnext.accounts.deferred_revenue.convert_deferred_revenue_to_income",
"erpnext.accounts.deferred_revenue.convert_deferred_expense_to_expense",
"erpnext.hr.utils.allocate_earned_leaves"
"erpnext.hr.utils.allocate_earned_leaves",
"erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual"
]
}

View File

@ -39,19 +39,21 @@ class AdditionalSalary(Document):
return amount_per_day * no_of_days
@frappe.whitelist()
def get_additional_salary_component(employee, start_date, end_date):
def get_additional_salary_component(employee, start_date, end_date, component_type):
additional_components = frappe.db.sql("""
select salary_component, sum(amount) as amount, overwrite_salary_structure_amount, deduct_full_tax_on_selected_payroll_date
from `tabAdditional Salary`
where employee=%(employee)s
and docstatus = 1
and payroll_date between %(from_date)s and %(to_date)s
and type = %(component_type)s
group by salary_component, overwrite_salary_structure_amount
order by salary_component, overwrite_salary_structure_amount
""", {
'employee': employee,
'from_date': start_date,
'to_date': end_date
'to_date': end_date,
'component_type': "Earning" if component_type == "earnings" else "Deduction"
}, as_dict=1)
additional_components_list = []

View File

@ -1,720 +1,215 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
"autoname": "HR-TAX-PRF-.YYYY.-.#####",
"beta": 0,
"creation": "2018-04-13 17:24:11.456132",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "HR-TAX-PRF-.YYYY.-.#####",
"creation": "2018-04-13 17:24:11.456132",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"employee",
"employee_name",
"department",
"column_break_2",
"submission_date",
"payroll_period",
"company",
"section_break_5",
"tax_exemption_proofs",
"section_break_10",
"total_actual_amount",
"column_break_12",
"exemption_amount",
"other_incomes_section",
"income_from_other_sources",
"attachment_section",
"attachments",
"amended_from"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "employee",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Employee",
"length": 0,
"no_copy": 0,
"options": "Employee",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "employee",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Employee",
"options": "Employee",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "employee.employee_name",
"fetch_if_empty": 0,
"fieldname": "employee_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Employee Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fetch_from": "employee.employee_name",
"fieldname": "employee_name",
"fieldtype": "Data",
"label": "Employee Name",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "employee.department",
"fetch_if_empty": 0,
"fieldname": "department",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Department",
"length": 0,
"no_copy": 0,
"options": "Department",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fetch_from": "employee.department",
"fieldname": "department",
"fieldtype": "Link",
"label": "Department",
"options": "Department",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Today",
"fetch_if_empty": 0,
"fieldname": "submission_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Submission Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"default": "Today",
"fieldname": "submission_date",
"fieldtype": "Date",
"label": "Submission Date",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "payroll_period",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Payroll Period",
"length": 0,
"no_copy": 0,
"options": "Payroll Period",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "payroll_period",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Payroll Period",
"options": "Payroll Period",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "employee.company",
"fetch_if_empty": 0,
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fetch_from": "employee.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"read_only": 1,
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "tax_exemption_proofs",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Tax Exemption Proofs",
"length": 0,
"no_copy": 0,
"options": "Employee Tax Exemption Proof Submission Detail",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "tax_exemption_proofs",
"fieldtype": "Table",
"label": "Tax Exemption Proofs",
"options": "Employee Tax Exemption Proof Submission Detail"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "section_break_10",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "section_break_10",
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "total_actual_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Total Actual Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "total_actual_amount",
"fieldtype": "Currency",
"label": "Total Actual Amount",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_12",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "exemption_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Total Exemption Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "exemption_amount",
"fieldtype": "Currency",
"label": "Total Exemption Amount",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "other_incomes_section",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Other Incomes",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "other_incomes_section",
"fieldtype": "Section Break",
"label": "Other Incomes"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "income_from_other_sources",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Income From Other Sources",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "income_from_other_sources",
"fieldtype": "Currency",
"label": "Income From Other Sources"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "attachment_section",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "attachment_section",
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "attachments",
"fieldtype": "Attach",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Attachments",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "attachments",
"fieldtype": "Attach",
"label": "Attachments"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "amended_from",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Amended From",
"length": 0,
"no_copy": 1,
"options": "Employee Tax Exemption Proof Submission",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Employee Tax Exemption Proof Submission",
"print_hide": 1,
"read_only": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 1,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-05-13 12:17:18.045171",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Tax Exemption Proof Submission",
"name_case": "",
"owner": "Administrator",
],
"is_submittable": 1,
"links": [],
"modified": "2020-03-02 19:02:15.398486",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Tax Exemption Proof Submission",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"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
},
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"submit": 1,
"write": 1
},
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"submit": 1,
"write": 1
},
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"share": 1,
"submit": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -64,6 +64,9 @@ class LeaveEncashment(Document):
allocation = self.get_leave_allocation()
if not allocation:
frappe.throw(_("No Leaves Allocated to Employee: {0} for Leave Type: {1}").format(self.employee, self.leave_type))
self.leave_balance = allocation.total_leaves_allocated - allocation.carry_forwarded_leaves_count\
- get_unused_leaves(self.employee, self.leave_type, allocation.from_date, self.encashment_date)
@ -116,4 +119,4 @@ def create_leave_encashment(leave_allocation):
leave_type=allocation.leave_type,
encashment_date=allocation.to_date
))
leave_encashment.insert(ignore_permissions=True)
leave_encashment.insert(ignore_permissions=True)

View File

@ -1,229 +0,0 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
{% include 'erpnext/hr/loan_common.js' %};
frappe.ui.form.on('Loan', {
onload: function (frm) {
frm.set_query("loan_application", function () {
return {
"filters": {
"applicant": frm.doc.applicant,
"docstatus": 1,
"status": "Approved"
}
};
});
frm.set_query("interest_income_account", function () {
return {
"filters": {
"company": frm.doc.company,
"root_type": "Income",
"is_group": 0
}
};
});
$.each(["payment_account", "loan_account"], function (i, field) {
frm.set_query(field, function () {
return {
"filters": {
"company": frm.doc.company,
"root_type": "Asset",
"is_group": 0
}
};
});
})
},
refresh: function (frm) {
if (frm.doc.docstatus == 1) {
if (frm.doc.status == "Sanctioned") {
frm.add_custom_button(__('Create Disbursement Entry'), function() {
frm.trigger("make_jv");
}).addClass("btn-primary");
} else if (frm.doc.status == "Disbursed" && frm.doc.repayment_start_date && (frm.doc.applicant_type == 'Member' || frm.doc.repay_from_salary == 0)) {
frm.add_custom_button(__('Create Repayment Entry'), function() {
frm.trigger("make_repayment_entry");
}).addClass("btn-primary");
}
}
frm.trigger("toggle_fields");
},
make_jv: function (frm) {
frappe.call({
args: {
"loan": frm.doc.name,
"company": frm.doc.company,
"loan_account": frm.doc.loan_account,
"applicant_type": frm.doc.applicant_type,
"applicant": frm.doc.applicant,
"loan_amount": frm.doc.loan_amount,
"payment_account": frm.doc.payment_account
},
method: "erpnext.hr.doctype.loan.loan.make_jv_entry",
callback: function (r) {
if (r.message)
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
})
},
make_repayment_entry: function(frm) {
var repayment_schedule = $.map(frm.doc.repayment_schedule, function(d) { return d.paid ? d.payment_date : false; });
if(repayment_schedule.length >= 1){
frm.repayment_data = [];
frm.show_dialog = 1;
let title = "";
let fields = [
{fieldtype:'Section Break', label: __('Repayment Schedule')},
{fieldname: 'payments', fieldtype: 'Table',
fields: [
{
fieldtype:'Data',
fieldname:'payment_date',
label: __('Date'),
read_only:1,
in_list_view: 1,
columns: 2
},
{
fieldtype:'Currency',
fieldname:'principal_amount',
label: __('Principal Amount'),
read_only:1,
in_list_view: 1,
columns: 3
},
{
fieldtype:'Currency',
fieldname:'interest_amount',
label: __('Interest'),
read_only:1,
in_list_view: 1,
columns: 2
},
{
fieldtype:'Currency',
read_only:1,
fieldname:'total_payment',
label: __('Total Payment'),
in_list_view: 1,
columns: 3
},
],
data: frm.repayment_data,
get_data: function() {
return frm.repayment_data;
}
}
]
var dialog = new frappe.ui.Dialog({
title: title, fields: fields,
});
if (frm.doc['repayment_schedule']) {
frm.doc['repayment_schedule'].forEach((payment, index) => {
if (payment.paid == 0 && payment.payment_date <= frappe.datetime.now_date()) {
frm.repayment_data.push ({
'id': index,
'name': payment.name,
'payment_date': payment.payment_date,
'principal_amount': payment.principal_amount,
'interest_amount': payment.interest_amount,
'total_payment': payment.total_payment
});
dialog.fields_dict.payments.grid.refresh();
$(dialog.wrapper.find(".grid-buttons")).hide();
$(`.octicon.octicon-triangle-down`).hide();
}
})
}
dialog.show()
dialog.set_primary_action(__('Create Repayment Entry'), function() {
frm.values = dialog.get_values();
if(frm.values) {
_make_repayment_entry(frm, dialog.fields_dict.payments.grid.get_selected_children());
dialog.hide()
}
});
}
dialog.get_close_btn().on('click', () => {
dialog.hide();
});
},
mode_of_payment: function (frm) {
if (frm.doc.mode_of_payment && frm.doc.company) {
frappe.call({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.get_bank_cash_account",
args: {
"mode_of_payment": frm.doc.mode_of_payment,
"company": frm.doc.company
},
callback: function (r, rt) {
if (r.message) {
frm.set_value("payment_account", r.message.account);
}
}
});
}
},
loan_application: function (frm) {
if(frm.doc.loan_application){
return frappe.call({
method: "erpnext.hr.doctype.loan.loan.get_loan_application",
args: {
"loan_application": frm.doc.loan_application
},
callback: function (r) {
if (!r.exc && r.message) {
frm.set_value("loan_type", r.message.loan_type);
frm.set_value("loan_amount", r.message.loan_amount);
frm.set_value("repayment_method", r.message.repayment_method);
frm.set_value("monthly_repayment_amount", r.message.repayment_amount);
frm.set_value("repayment_periods", r.message.repayment_periods);
frm.set_value("rate_of_interest", r.message.rate_of_interest);
}
}
});
}
},
repayment_method: function (frm) {
frm.trigger("toggle_fields")
},
toggle_fields: function (frm) {
frm.toggle_enable("monthly_repayment_amount", frm.doc.repayment_method == "Repay Fixed Amount per Period")
frm.toggle_enable("repayment_periods", frm.doc.repayment_method == "Repay Over Number of Periods")
}
});
var _make_repayment_entry = function(frm, payment_rows) {
frappe.call({
method:"erpnext.hr.doctype.loan.loan.make_repayment_entry",
args: {
payment_rows: payment_rows,
"loan": frm.doc.name,
"company": frm.doc.company,
"loan_account": frm.doc.loan_account,
"applicant_type": frm.doc.applicant_type,
"applicant": frm.doc.applicant,
"payment_account": frm.doc.payment_account,
"interest_income_account": frm.doc.interest_income_account
},
callback: function(r) {
if (r.message)
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name, {'payment_rows': payment_rows});
}
});
}

View File

@ -1,240 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, math, json
import erpnext
from frappe import _
from frappe.utils import flt, rounded, add_months, nowdate, getdate
from erpnext.controllers.accounts_controller import AccountsController
class Loan(AccountsController):
def validate(self):
validate_repayment_method(self.repayment_method, self.loan_amount, self.monthly_repayment_amount, self.repayment_periods)
self.set_missing_fields()
self.make_repayment_schedule()
self.set_repayment_period()
self.calculate_totals()
def set_missing_fields(self):
if not self.company:
self.company = erpnext.get_default_company()
if not self.posting_date:
self.posting_date = nowdate()
if self.loan_type and not self.rate_of_interest:
self.rate_of_interest = frappe.db.get_value("Loan Type", self.loan_type, "rate_of_interest")
if self.repayment_method == "Repay Over Number of Periods":
self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
if self.status == "Repaid/Closed":
self.total_amount_paid = self.total_payment
def make_jv_entry(self):
self.check_permission('write')
journal_entry = frappe.new_doc('Journal Entry')
journal_entry.voucher_type = 'Bank Entry'
journal_entry.user_remark = _('Against Loan: {0}').format(self.name)
journal_entry.company = self.company
journal_entry.posting_date = nowdate()
account_amt_list = []
account_amt_list.append({
"account": self.loan_account,
"party_type": self.applicant_type,
"party": self.applicant,
"debit_in_account_currency": self.loan_amount,
"reference_type": "Loan",
"reference_name": self.name,
})
account_amt_list.append({
"account": self.payment_account,
"credit_in_account_currency": self.loan_amount,
"reference_type": "Loan",
"reference_name": self.name,
})
journal_entry.set("accounts", account_amt_list)
return journal_entry.as_dict()
def make_repayment_schedule(self):
self.repayment_schedule = []
payment_date = self.repayment_start_date
balance_amount = self.loan_amount
while(balance_amount > 0):
interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100))
principal_amount = self.monthly_repayment_amount - interest_amount
balance_amount = rounded(balance_amount + interest_amount - self.monthly_repayment_amount)
if balance_amount < 0:
principal_amount += balance_amount
balance_amount = 0.0
total_payment = principal_amount + interest_amount
self.append("repayment_schedule", {
"payment_date": payment_date,
"principal_amount": principal_amount,
"interest_amount": interest_amount,
"total_payment": total_payment,
"balance_loan_amount": balance_amount
})
next_payment_date = add_months(payment_date, 1)
payment_date = next_payment_date
def set_repayment_period(self):
if self.repayment_method == "Repay Fixed Amount per Period":
repayment_periods = len(self.repayment_schedule)
self.repayment_periods = repayment_periods
def calculate_totals(self):
self.total_payment = 0
self.total_interest_payable = 0
self.total_amount_paid = 0
for data in self.repayment_schedule:
self.total_payment += data.total_payment
self.total_interest_payable +=data.interest_amount
if data.paid:
self.total_amount_paid += data.total_payment
def update_total_amount_paid(doc):
total_amount_paid = 0
for data in doc.repayment_schedule:
if data.paid:
total_amount_paid += data.total_payment
frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid)
def update_disbursement_status(doc):
disbursement = frappe.db.sql("""
select posting_date, ifnull(sum(credit_in_account_currency), 0) as disbursed_amount
from `tabGL Entry`
where account = %s and against_voucher_type = 'Loan' and against_voucher = %s
""", (doc.payment_account, doc.name), as_dict=1)[0]
disbursement_date = None
if not disbursement or disbursement.disbursed_amount == 0:
status = "Sanctioned"
elif disbursement.disbursed_amount == doc.loan_amount:
disbursement_date = disbursement.posting_date
status = "Disbursed"
elif disbursement.disbursed_amount > doc.loan_amount:
frappe.throw(_("Disbursed Amount cannot be greater than Loan Amount {0}").format(doc.loan_amount))
if status == 'Disbursed' and getdate(disbursement_date) > getdate(frappe.db.get_value("Loan", doc.name, "repayment_start_date")):
frappe.throw(_("Disbursement Date cannot be after Loan Repayment Start Date"))
frappe.db.sql("""
update `tabLoan`
set status = %s, disbursement_date = %s
where name = %s
""", (status, disbursement_date, doc.name))
def validate_repayment_method(repayment_method, loan_amount, monthly_repayment_amount, repayment_periods):
if repayment_method == "Repay Over Number of Periods" and not repayment_periods:
frappe.throw(_("Please enter Repayment Periods"))
if repayment_method == "Repay Fixed Amount per Period":
if not monthly_repayment_amount:
frappe.throw(_("Please enter repayment Amount"))
if monthly_repayment_amount > loan_amount:
frappe.throw(_("Monthly Repayment Amount cannot be greater than Loan Amount"))
def get_monthly_repayment_amount(repayment_method, loan_amount, rate_of_interest, repayment_periods):
if rate_of_interest:
monthly_interest_rate = flt(rate_of_interest) / (12 *100)
monthly_repayment_amount = math.ceil((loan_amount * monthly_interest_rate *
(1 + monthly_interest_rate)**repayment_periods) \
/ ((1 + monthly_interest_rate)**repayment_periods - 1))
else:
monthly_repayment_amount = math.ceil(flt(loan_amount) / repayment_periods)
return monthly_repayment_amount
@frappe.whitelist()
def get_loan_application(loan_application):
loan = frappe.get_doc("Loan Application", loan_application)
if loan:
return loan.as_dict()
@frappe.whitelist()
def make_repayment_entry(payment_rows, loan, company, loan_account, applicant_type, applicant, \
payment_account=None, interest_income_account=None):
if isinstance(payment_rows, frappe.string_types):
payment_rows_list = json.loads(payment_rows)
else:
frappe.throw(_("No repayments available for Journal Entry"))
if payment_rows_list:
row_name = list(set(d["name"] for d in payment_rows_list))
else:
frappe.throw(_("No repayments selected for Journal Entry"))
total_payment = 0
principal_amount = 0
interest_amount = 0
for d in payment_rows_list:
total_payment += d["total_payment"]
principal_amount += d["principal_amount"]
interest_amount += d["interest_amount"]
journal_entry = frappe.new_doc('Journal Entry')
journal_entry.voucher_type = 'Bank Entry'
journal_entry.user_remark = _('Against Loan: {0}').format(loan)
journal_entry.company = company
journal_entry.posting_date = nowdate()
journal_entry.paid_loan = json.dumps(row_name)
account_amt_list = []
account_amt_list.append({
"account": payment_account,
"debit_in_account_currency": total_payment,
"reference_type": "Loan",
"reference_name": loan,
})
account_amt_list.append({
"account": loan_account,
"credit_in_account_currency": principal_amount,
"party_type": applicant_type,
"party": applicant,
"reference_type": "Loan",
"reference_name": loan,
})
account_amt_list.append({
"account": interest_income_account,
"credit_in_account_currency": interest_amount,
"reference_type": "Loan",
"reference_name": loan,
})
journal_entry.set("accounts", account_amt_list)
return journal_entry.as_dict()
@frappe.whitelist()
def make_jv_entry(loan, company, loan_account, applicant_type, applicant, loan_amount,payment_account=None):
journal_entry = frappe.new_doc('Journal Entry')
journal_entry.voucher_type = 'Bank Entry'
journal_entry.user_remark = _('Against Loan: {0}').format(loan)
journal_entry.company = company
journal_entry.posting_date = nowdate()
account_amt_list = []
account_amt_list.append({
"account": loan_account,
"debit_in_account_currency": loan_amount,
"party_type": applicant_type,
"party": applicant,
"reference_type": "Loan",
"reference_name": loan,
})
account_amt_list.append({
"account": payment_account,
"credit_in_account_currency": loan_amount,
"reference_type": "Loan",
"reference_name": loan,
})
journal_entry.set("accounts", account_amt_list)
return journal_entry.as_dict()

View File

@ -1,26 +0,0 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'applicant',
'non_standard_fieldnames': {
'Journal Entry': 'reference_name',
'Salary Slip': 'employee'
},
'transactions': [
{
'label': _('Applicant'),
'items': ['Loan Application']
},
{
'label': _('Account'),
'items': ['Journal Entry']
},
{
'label': _('Employee'),
'items': ['Salary Slip']
}
]
}

View File

@ -1,79 +0,0 @@
QUnit.test("Test Loan [HR]", function(assert) {
assert.expect(8);
let done = assert.async();
let employee_name;
// To create a loan and check principal,interest and balance amount
let loan_creation = (ename,lname) => {
return frappe.run_serially([
() => frappe.db.get_value('Employee', {'employee_name': ename}, 'name'),
(r) => {
employee_name = r.message.name;
},
() => frappe.db.get_value('Loan Application', {'loan_type': lname}, 'name'),
(r) => {
// Creating loan for an employee
return frappe.tests.make('Loan', [
{ company: 'For Testing'},
{ posting_date: '2017-08-26'},
{ applicant: employee_name},
{ loan_application: r.message.name},
{ disbursement_date: '2018-08-26'},
{ mode_of_payment: 'Cash'},
{ loan_account: 'Temporary Opening - FT'},
{ interest_income_account: 'Service - FT'}
]);
},
() => frappe.timeout(3),
() => frappe.click_button('Submit'),
() => frappe.timeout(1),
() => frappe.click_button('Yes'),
() => frappe.timeout(3),
// Checking if all the amounts are correctly calculated
() => {
assert.ok(cur_frm.get_field('applicant_name').value=='Test Employee 1'&&
(cur_frm.get_field('status').value=='Sanctioned'),
'Loan Sanctioned for correct employee');
assert.equal(7270,
cur_frm.get_doc('repayment_schedule').repayment_schedule[0].principal_amount,
'Principal amount for first instalment is correctly calculated');
assert.equal(2333,
cur_frm.get_doc('repayment_schedule').repayment_schedule[0].interest_amount,
'Interest amount for first instalment is correctly calculated');
assert.equal(192730,
cur_frm.get_doc('repayment_schedule').repayment_schedule[0].balance_loan_amount,
'Balance amount after first instalment is correctly calculated');
assert.equal(9479,
cur_frm.get_doc('repayment_schedule').repayment_schedule[23].principal_amount,
'Principal amount for last instalment is correctly calculated');
assert.equal(111,
cur_frm.get_doc('repayment_schedule').repayment_schedule[23].interest_amount,
'Interest amount for last instalment is correctly calculated');
assert.equal(0,
cur_frm.get_doc('repayment_schedule').repayment_schedule[23].balance_loan_amount,
'Balance amount after last instalment is correctly calculated');
},
() => frappe.set_route('List','Loan','List'),
() => frappe.timeout(2),
// Checking the submission of Loan
() => {
assert.ok(cur_list.data[0].docstatus==1,'Loan sanctioned and submitted successfully');
},
]);
};
frappe.run_serially([
// Creating loan
() => loan_creation('Test Employee 1','Test Loan'),
() => done()
]);
});

View File

@ -1,71 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import erpnext
import unittest
from frappe.utils import nowdate, add_days
from erpnext.hr.doctype.salary_structure.test_salary_structure import make_employee
class TestLoan(unittest.TestCase):
def setUp(self):
create_loan_type("Personal Loan", 500000, 8.4)
self.applicant = make_employee("robert_loan@loan.com")
create_loan(self.applicant, "Personal Loan", 280000, "Repay Over Number of Periods", 20)
def test_loan(self):
loan = frappe.get_doc("Loan", {"applicant":self.applicant})
self.assertEquals(loan.monthly_repayment_amount, 15052)
self.assertEquals(loan.total_interest_payable, 21034)
self.assertEquals(loan.total_payment, 301034)
schedule = loan.repayment_schedule
self.assertEqual(len(schedule), 20)
for idx, principal_amount, interest_amount, balance_loan_amount in [[3, 13369, 1683, 227079], [19, 14941, 105, 0], [17, 14740, 312, 29785]]:
self.assertEqual(schedule[idx].principal_amount, principal_amount)
self.assertEqual(schedule[idx].interest_amount, interest_amount)
self.assertEqual(schedule[idx].balance_loan_amount, balance_loan_amount)
loan.repayment_method = "Repay Fixed Amount per Period"
loan.monthly_repayment_amount = 14000
loan.save()
self.assertEquals(len(loan.repayment_schedule), 22)
self.assertEquals(loan.total_interest_payable, 22712)
self.assertEquals(loan.total_payment, 302712)
def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest):
if not frappe.db.exists("Loan Type", loan_name):
frappe.get_doc({
"doctype": "Loan Type",
"loan_name": loan_name,
"maximum_loan_amount": maximum_loan_amount,
"rate_of_interest": rate_of_interest
}).insert()
def create_loan(applicant, loan_type, loan_amount, repayment_method, repayment_periods):
create_loan_type(loan_type, 500000, 8.4)
if not frappe.db.get_value("Loan", {"applicant":applicant}):
loan = frappe.new_doc("Loan")
loan.update({
"applicant": applicant,
"loan_type": loan_type,
"loan_amount": loan_amount,
"repayment_method": repayment_method,
"repayment_periods": repayment_periods,
"disbursement_date": nowdate(),
"repayment_start_date": nowdate(),
"status": "Disbursed",
"mode_of_payment": frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'),
"payment_account": frappe.db.get_value('Account', {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name"),
"loan_account": frappe.db.get_value('Account', {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name"),
"interest_income_account": frappe.db.get_value('Account', {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name")
})
loan.insert()
return loan
else:
return frappe.get_doc("Loan", {"applicant":applicant})

View File

@ -1,42 +0,0 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
{% include 'erpnext/hr/loan_common.js' %};
frappe.ui.form.on('Loan Application', {
refresh: function(frm) {
frm.trigger("toggle_fields")
frm.trigger("add_toolbar_buttons")
},
repayment_method: function(frm) {
frm.doc.repayment_amount = frm.doc.repayment_periods = ""
frm.trigger("toggle_fields")
frm.trigger("toggle_required")
},
toggle_fields: function(frm) {
frm.toggle_enable("repayment_amount", frm.doc.repayment_method=="Repay Fixed Amount per Period")
frm.toggle_enable("repayment_periods", frm.doc.repayment_method=="Repay Over Number of Periods")
},
toggle_required: function(frm){
frm.toggle_reqd("repayment_amount", cint(frm.doc.repayment_method=='Repay Fixed Amount per Period'))
frm.toggle_reqd("repayment_periods", cint(frm.doc.repayment_method=='Repay Over Number of Periods'))
},
add_toolbar_buttons: function(frm) {
if (frm.doc.status == "Approved") {
frm.add_custom_button(__('Create Loan'), function() {
frappe.call({
method: "erpnext.hr.doctype.loan_application.loan_application.make_loan",
args: {
"source_name": frm.doc.name
},
callback: function(r) {
if(!r.exc) {
var doc = frappe.model.sync(r.message);
frappe.set_route("Form", r.message.doctype, r.message.name);
}
}
});
}).addClass("btn-primary");
}
}
});

View File

@ -1,840 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "ACC-LOAP-.YYYY.-.#####",
"beta": 0,
"creation": "2016-12-02 12:35:56.046811",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "applicant_type",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Applicant Type",
"length": 0,
"no_copy": 0,
"options": "Employee\nMember",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 0,
"in_standard_filter": 1,
"label": "Applicant",
"length": 0,
"no_copy": 0,
"options": "applicant_type",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "applicant",
"fieldname": "applicant_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Applicant Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Posting Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "status",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Status",
"length": 0,
"no_copy": 1,
"options": "Open\nApproved\nRejected",
"permlevel": 1,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_4",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Loan Info",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "loan_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Loan Type",
"length": 0,
"no_copy": 0,
"options": "Loan Type",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "loan_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Loan Amount",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "required_by_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Required by Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_7",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Reason",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "repayment_info",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Repayment Info",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "repayment_method",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Repayment Method",
"length": 0,
"no_copy": 0,
"options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "loan_type.rate_of_interest",
"fieldname": "rate_of_interest",
"fieldtype": "Percent",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Rate of Interest",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "total_payable_interest",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Total Payable Interest",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_11",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "repayment_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Monthly Repayment Amount",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "repayment_periods",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Repayment Period in Months",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "total_payable_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Total Payable Amount",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amended_from",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Amended From",
"length": 0,
"no_copy": 1,
"options": "Loan Application",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 1,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-08-21 16:15:53.688596",
"modified_by": "Administrator",
"module": "HR",
"name": "Loan Application",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 0
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "applicant_type, applicant, loan_type, loan_amount",
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"timeline_field": "applicant",
"title_field": "applicant",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View File

@ -1,70 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, math
from frappe import _
from frappe.utils import flt, rounded
from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document
from erpnext.hr.doctype.loan.loan import get_monthly_repayment_amount, validate_repayment_method
class LoanApplication(Document):
def validate(self):
validate_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount, self.repayment_periods)
self.validate_loan_amount()
self.get_repayment_details()
def validate_loan_amount(self):
maximum_loan_limit = frappe.db.get_value('Loan Type', self.loan_type, 'maximum_loan_amount')
if maximum_loan_limit and self.loan_amount > maximum_loan_limit:
frappe.throw(_("Loan Amount cannot exceed Maximum Loan Amount of {0}").format(maximum_loan_limit))
def get_repayment_details(self):
if self.repayment_method == "Repay Over Number of Periods":
self.repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
if self.repayment_method == "Repay Fixed Amount per Period":
monthly_interest_rate = flt(self.rate_of_interest) / (12 *100)
if monthly_interest_rate:
min_repayment_amount = self.loan_amount*monthly_interest_rate
if (self.repayment_amount - min_repayment_amount) <= 0:
frappe.throw(_("Repayment Amount must be greater than " \
+ str(flt(min_repayment_amount, 2))))
self.repayment_periods = math.ceil((math.log(self.repayment_amount) -
math.log(self.repayment_amount - min_repayment_amount)) /(math.log(1 + monthly_interest_rate)))
else:
self.repayment_periods = self.loan_amount / self.repayment_amount
self.calculate_payable_amount()
def calculate_payable_amount(self):
balance_amount = self.loan_amount
self.total_payable_amount = 0
self.total_payable_interest = 0
while(balance_amount > 0):
interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100))
balance_amount = rounded(balance_amount + interest_amount - self.repayment_amount)
self.total_payable_interest += interest_amount
self.total_payable_amount = self.loan_amount + self.total_payable_interest
@frappe.whitelist()
def make_loan(source_name, target_doc = None):
doclist = get_mapped_doc("Loan Application", source_name, {
"Loan Application": {
"doctype": "Loan",
"field_map": {
"repayment_amount": "monthly_repayment_amount"
},
"validation": {
"docstatus": ["=", 1]
}
}
}, target_doc)
return doclist

View File

@ -1,68 +0,0 @@
QUnit.module('hr');
QUnit.test("Test: Loan Application [HR]", function (assert) {
assert.expect(8);
let done = assert.async();
let employee_name;
frappe.run_serially([
// Creation of Loan Application
() => frappe.db.get_value('Employee', {'employee_name': 'Test Employee 1'}, 'name'),
(r) => {
employee_name = r.message.name;
},
() => {
return frappe.tests.make('Loan Application', [
{ company: 'For Testing'},
{ applicant: employee_name},
{ applicant_name: 'Test Employee 1'},
{ status: 'Approved'},
{ loan_type: 'Test Loan '},
{ loan_amount: 200000},
{ description: 'This is just a test'},
{ repayment_method: 'Repay Over Number of Periods'},
{ repayment_periods: 24},
{ rate_of_interest: 14}
]);
},
() => frappe.timeout(6),
() => frappe.click_button('Submit'),
() => frappe.timeout(1),
() => frappe.click_button('Yes'),
() => frappe.timeout(2),
() => {
// To check if all the amounts are correctly calculated
assert.ok(cur_frm.get_field('applicant_name').value == 'Test Employee 1',
'Application created successfully');
assert.ok(cur_frm.get_field('status').value=='Approved',
'Status of application is correctly set');
assert.ok(cur_frm.get_field('loan_type').value=='Test Loan',
'Application is created for correct Loan Type');
assert.ok(cur_frm.get_field('status').value=='Approved',
'Status of application is correctly set');
assert.ok(cur_frm.get_field('repayment_amount').value==9603,
'Repayment amount is correctly calculated');
assert.ok(cur_frm.get_field('total_payable_interest').value==30459,
'Interest amount is correctly calculated');
assert.ok(cur_frm.get_field('total_payable_amount').value==230459,
'Total payable amount is correctly calculated');
},
() => frappe.set_route('List','Loan Application','List'),
() => frappe.timeout(2),
// Checking the submission of Loan Application
() => {
assert.ok(cur_list.data[0].docstatus==1,'Loan Application submitted successfully');
},
() => frappe.timeout(1),
() => done()
]);
});

View File

@ -1,7 +0,0 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Loan Type', {
refresh: function(frm) {
}
});

View File

@ -1,259 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "field:loan_name",
"beta": 0,
"creation": "2016-12-02 10:41:40.732843",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "loan_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Loan Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "maximum_loan_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Maximum Loan Amount",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "rate_of_interest",
"fieldtype": "Percent",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Rate of Interest (%) Yearly",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "disabled",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Disabled",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-03-29 21:23:08.665245",
"modified_by": "Administrator",
"module": "HR",
"name": "Loan Type",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Employee",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0
}

View File

@ -1,12 +0,0 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'loan_type',
'transactions': [
{
'items': ['Loan Application']
},
],
}

View File

@ -1,31 +0,0 @@
QUnit.module('hr');
QUnit.test("Test: Loan Type [HR]", function (assert) {
assert.expect(3);
let done = assert.async();
frappe.run_serially([
// Loan Type creation
() => {
frappe.tests.make('Loan Type', [
{ loan_name: 'Test Loan'},
{ maximum_loan_amount: 400000},
{ rate_of_interest: 14},
{ description:
'This is just a test.'}
]);
},
() => frappe.timeout(7),
() => frappe.set_route('List','Loan Type','List'),
() => frappe.timeout(4),
// Checking if the fields are correctly set
() => {
assert.ok(cur_list.data.length==1, 'Loan Type created successfully');
assert.ok(cur_list.data[0].name=='Test Loan', 'Loan title Correctly set');
assert.ok(cur_list.data[0].disabled==0, 'Loan enabled');
},
() => done()
]);
});

View File

@ -157,19 +157,6 @@ class PayrollEntry(Document):
for ss in submitted_ss:
ss.email_salary_slip()
def get_loan_details(self):
"""
Get loan details from submitted salary slip based on selected criteria
"""
cond = self.get_filter_condition()
return frappe.db.sql(""" select eld.loan_account, eld.loan,
eld.interest_income_account, eld.principal_amount, eld.interest_amount, eld.total_payment,t1.employee
from
`tabSalary Slip` t1, `tabSalary Slip Loan` eld
where
t1.docstatus = 1 and t1.name = eld.parent and start_date >= %s and end_date <= %s %s
""" % ('%s', '%s', cond), (self.start_date, self.end_date), as_dict=True) or []
def get_salary_component_account(self, salary_component):
account = frappe.db.get_value("Salary Component Account",
{"parent": salary_component, "company": self.company}, "default_account")
@ -225,7 +212,6 @@ class PayrollEntry(Document):
earnings = self.get_salary_component_total(component_type = "earnings") or {}
deductions = self.get_salary_component_total(component_type = "deductions") or {}
default_payroll_payable_account = self.get_default_payroll_payable_account()
loan_details = self.get_loan_details()
jv_name = ""
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
@ -262,29 +248,6 @@ class PayrollEntry(Document):
"project": self.project
})
# Loan
for data in loan_details:
accounts.append({
"account": data.loan_account,
"credit_in_account_currency": data.principal_amount,
"party_type": "Employee",
"party": data.employee
})
if data.interest_amount and not data.interest_income_account:
frappe.throw(_("Select interest income account in loan {0}").format(data.loan))
if data.interest_income_account and data.interest_amount:
accounts.append({
"account": data.interest_income_account,
"credit_in_account_currency": data.interest_amount,
"cost_center": self.cost_center,
"project": self.project,
"party_type": "Employee",
"party": data.employee
})
payable_amount -= flt(data.total_payment, precision)
# Payable amount
accounts.append({
"account": default_payroll_payable_account,

View File

@ -6,20 +6,22 @@ import erpnext
import frappe
from dateutil.relativedelta import relativedelta
from erpnext.accounts.utils import get_fiscal_year, getdate, nowdate
from frappe.utils import add_months
from erpnext.hr.doctype.payroll_entry.payroll_entry import get_start_end_dates, get_end_date
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.salary_slip.test_salary_slip import get_salary_component_account, \
make_earning_salary_component, make_deduction_salary_component
from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure
from erpnext.hr.doctype.loan.test_loan import create_loan
from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import make_accrual_interest_entry_for_term_loans
class TestPayrollEntry(unittest.TestCase):
def setUp(self):
for dt in ["Salary Slip", "Salary Component", "Salary Component Account", "Payroll Entry", "Loan"]:
for dt in ["Salary Slip", "Salary Component", "Salary Component Account", "Payroll Entry"]:
frappe.db.sql("delete from `tab%s`" % dt)
make_earning_salary_component(setup=True)
make_deduction_salary_component(setup=True)
make_earning_salary_component(setup=True, company_list=["_Test Company"])
make_deduction_salary_component(setup=True, company_list=["_Test Company"])
frappe.db.set_value("HR Settings", None, "email_salary_slip_to_employee", 0)
@ -49,8 +51,8 @@ class TestPayrollEntry(unittest.TestCase):
def test_loan(self):
branch = "Test Employee Branch"
applicant = make_employee("test_employee@loan.com")
company = erpnext.get_default_company()
applicant = make_employee("test_employee@loan.com", company="_Test Company")
company = "_Test Company"
holiday_list = make_holiday("test holiday for loan")
company_doc = frappe.get_doc('Company', company)
@ -70,16 +72,21 @@ class TestPayrollEntry(unittest.TestCase):
employee_doc.holiday_list = holiday_list
employee_doc.save()
loan = create_loan(applicant,
"Personal Loan", 280000, "Repay Over Number of Periods", 20)
salary_structure = "Test Salary Structure for Loan"
make_salary_structure(salary_structure, "Monthly", employee=employee_doc.name, company="_Test Company")
loan = create_loan(applicant, "Car Loan", 280000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1))
loan.repay_from_salary = 1
loan.submit()
salary_structure = "Test Salary Structure for Loan"
make_salary_structure(salary_structure, "Monthly", employee_doc.name)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1))
make_accrual_interest_entry_for_term_loans(posting_date=nowdate())
dates = get_start_end_dates('Monthly', nowdate())
make_payroll_entry(start_date=dates.start_date,
end_date=dates.end_date, branch=branch)
make_payroll_entry(company="_Test Company", start_date=dates.start_date,
end_date=dates.end_date, branch=branch, cost_center="Main - _TC", payment_account="Cash - _TC")
name = frappe.db.get_value('Salary Slip',
{'posting_date': nowdate(), 'employee': applicant}, 'name')
@ -109,6 +116,13 @@ def make_payroll_entry(**args):
payroll_entry.posting_date = nowdate()
payroll_entry.payroll_frequency = "Monthly"
payroll_entry.branch = args.branch or None
if args.cost_center:
payroll_entry.cost_center = args.cost_center
if args.payment_account:
payroll_entry.payment_account = args.payment_account
payroll_entry.save()
payroll_entry.create_salary_slips()
payroll_entry.submit_salary_slips()

View File

@ -17,6 +17,7 @@ from erpnext.hr.doctype.additional_salary.additional_salary import get_additiona
from erpnext.hr.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period
from erpnext.hr.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount
from erpnext.hr.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry
class SalarySlip(TransactionBase):
def __init__(self, *args, **kwargs):
@ -66,6 +67,7 @@ class SalarySlip(TransactionBase):
self.set_status()
self.update_status(self.name)
self.update_salary_slip_in_additional_salary()
self.make_loan_repayment_entry()
if (frappe.db.get_single_value("HR Settings", "email_salary_slip_to_employee")) and not frappe.flags.via_payroll_entry:
self.email_salary_slip()
@ -73,6 +75,7 @@ class SalarySlip(TransactionBase):
self.set_status()
self.update_status()
self.update_salary_slip_in_additional_salary()
self.cancel_loan_repayment_entry()
def on_trash(self):
from frappe.model.naming import revert_series_if_last
@ -296,9 +299,11 @@ class SalarySlip(TransactionBase):
def calculate_net_pay(self):
if self.salary_structure:
self.calculate_component_amounts()
self.calculate_component_amounts("earnings")
self.gross_pay = self.get_component_totals("earnings")
if self.salary_structure:
self.calculate_component_amounts("deductions")
self.total_deduction = self.get_component_totals("deductions")
self.set_loan_repayment()
@ -306,25 +311,27 @@ class SalarySlip(TransactionBase):
self.net_pay = flt(self.gross_pay) - (flt(self.total_deduction) + flt(self.total_loan_repayment))
self.rounded_total = rounded(self.net_pay)
def calculate_component_amounts(self):
def calculate_component_amounts(self, component_type):
if not getattr(self, '_salary_structure_doc', None):
self._salary_structure_doc = frappe.get_doc('Salary Structure', self.salary_structure)
payroll_period = get_payroll_period(self.start_date, self.end_date, self.company)
self.add_structure_components()
self.add_employee_benefits(payroll_period)
self.add_additional_salary_components()
self.add_tax_components(payroll_period)
self.set_component_amounts_based_on_payment_days()
self.add_structure_components(component_type)
self.add_additional_salary_components(component_type)
if component_type == "earnings":
self.add_employee_benefits(payroll_period)
else:
self.add_tax_components(payroll_period)
def add_structure_components(self):
self.set_component_amounts_based_on_payment_days(component_type)
def add_structure_components(self, component_type):
data = self.get_data_for_eval()
for key in ('earnings', 'deductions'):
for struct_row in self._salary_structure_doc.get(key):
amount = self.eval_condition_and_formula(struct_row, data)
if amount and struct_row.statistical_component == 0:
self.update_component_row(struct_row, amount, key)
for struct_row in self._salary_structure_doc.get(component_type):
amount = self.eval_condition_and_formula(struct_row, data)
if amount and struct_row.statistical_component == 0:
self.update_component_row(struct_row, amount, component_type)
def get_data_for_eval(self):
'''Returns data for evaluating formula'''
@ -397,14 +404,15 @@ class SalarySlip(TransactionBase):
amount = last_benefit.amount
self.update_component_row(frappe._dict(last_benefit.struct_row), amount, "earnings")
def add_additional_salary_components(self):
additional_components = get_additional_salary_component(self.employee, self.start_date, self.end_date)
def add_additional_salary_components(self, component_type):
additional_components = get_additional_salary_component(self.employee,
self.start_date, self.end_date, component_type)
if additional_components:
for additional_component in additional_components:
amount = additional_component.amount
overwrite = additional_component.overwrite
key = "earnings" if additional_component.type == "Earning" else "deductions"
self.update_component_row(frappe._dict(additional_component.struct_row), amount, key, overwrite=overwrite)
self.update_component_row(frappe._dict(additional_component.struct_row), amount,
component_type, overwrite=overwrite)
def add_tax_components(self, payroll_period):
# Calculate variable_based_on_taxable_salary after all components updated in salary slip
@ -733,7 +741,7 @@ class SalarySlip(TransactionBase):
total += d.amount
return total
def set_component_amounts_based_on_payment_days(self):
def set_component_amounts_based_on_payment_days(self, component_type):
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
["date_of_joining", "relieving_date"])
@ -743,9 +751,8 @@ class SalarySlip(TransactionBase):
if not joining_date:
frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name)))
for component_type in ("earnings", "deductions"):
for d in self.get(component_type):
d.amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0]
for d in self.get(component_type):
d.amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0]
def set_loan_repayment(self):
self.set('loans', [])
@ -754,28 +761,35 @@ class SalarySlip(TransactionBase):
self.total_principal_amount = 0
for loan in self.get_loan_details():
self.append('loans', {
'loan': loan.name,
'total_payment': loan.total_payment,
'interest_amount': loan.interest_amount,
'principal_amount': loan.principal_amount,
'loan_account': loan.loan_account,
'interest_income_account': loan.interest_income_account
})
self.total_loan_repayment += loan.total_payment
self.total_interest_amount += loan.interest_amount
self.total_principal_amount += loan.principal_amount
amounts = calculate_amounts(loan.name, self.posting_date, "Regular Payment")
total_payment = amounts['interest_amount'] + amounts['payable_principal_amount']
if total_payment:
self.append('loans', {
'loan': loan.name,
'total_payment': total_payment,
'interest_amount': amounts['interest_amount'],
'principal_amount': amounts['payable_principal_amount'],
'loan_account': loan.loan_account,
'interest_income_account': loan.interest_income_account
})
self.total_loan_repayment += total_payment
self.total_interest_amount += amounts['interest_amount']
self.total_principal_amount += amounts['payable_principal_amount']
def get_loan_details(self):
return frappe.db.sql("""select rps.principal_amount, rps.interest_amount, l.name,
rps.total_payment, l.loan_account, l.interest_income_account
from
`tabRepayment Schedule` as rps, `tabLoan` as l
where
l.name = rps.parent and rps.payment_date between %s and %s and
l.repay_from_salary = 1 and l.docstatus = 1 and l.applicant = %s""",
(self.start_date, self.end_date, self.employee), as_dict=True) or []
return frappe.get_all("Loan",
fields=["name", "interest_income_account", "loan_account", "loan_type"],
filters = {
"applicant": self.employee,
"docstatus": 1,
"repay_from_salary": 1,
})
def update_salary_slip_in_additional_salary(self):
salary_slip = self.name if self.docstatus==1 else None
@ -784,6 +798,23 @@ class SalarySlip(TransactionBase):
where employee=%s and payroll_date between %s and %s and docstatus=1
""", (salary_slip, self.employee, self.start_date, self.end_date))
def make_loan_repayment_entry(self):
for loan in self.loans:
repayment_entry = create_repayment_entry(loan.loan, self.employee,
self.company, self.posting_date, loan.loan_type, "Regular Payment", loan.interest_amount,
loan.principal_amount, loan.total_payment)
repayment_entry.save()
repayment_entry.submit()
loan.loan_repayment_entry = repayment_entry.name
def cancel_loan_repayment_entry(self):
for loan in self.loans:
if loan.loan_repayment_entry:
repayment_entry = frappe.get_doc("Loan Repayment", loan.loan_repayment_entry)
repayment_entry.cancel()
def email_salary_slip(self):
receiver = frappe.db.get_value("Employee", self.employee, "prefered_email")
hr_settings = frappe.get_single("HR Settings")

View File

@ -18,8 +18,8 @@ from erpnext.hr.doctype.employee_tax_exemption_declaration.test_employee_tax_exe
class TestSalarySlip(unittest.TestCase):
def setUp(self):
make_earning_salary_component(setup=True)
make_deduction_salary_component(setup=True)
make_earning_salary_component(setup=True, company_list=["_Test Company"])
make_deduction_salary_component(setup=True, company_list=["_Test Company"])
for dt in ["Leave Application", "Leave Allocation", "Salary Slip"]:
frappe.db.sql("delete from `tab%s`" % dt)
@ -50,7 +50,7 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(ss.deductions[0].amount, 5000)
self.assertEqual(ss.deductions[1].amount, 5000)
self.assertEqual(ss.gross_pay, 78000)
self.assertEqual(ss.net_pay, 67418.0)
self.assertEqual(ss.net_pay, 68000.0)
def test_salary_slip_with_holidays_excluded(self):
no_of_days = self.get_no_of_days()
@ -70,7 +70,7 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(ss.deductions[0].amount, 5000)
self.assertEqual(ss.deductions[1].amount, 5000)
self.assertEqual(ss.gross_pay, 78000)
self.assertEqual(ss.net_pay, 67418.0)
self.assertEqual(ss.net_pay, 68000.0)
def test_payment_days(self):
no_of_days = self.get_no_of_days()
@ -137,21 +137,41 @@ class TestSalarySlip(unittest.TestCase):
make_employee("test_employee@salary.com")
ss = make_employee_salary_slip("test_employee@salary.com", "Monthly")
ss.company = "_Test Company"
ss.save()
ss.submit()
email_queue = frappe.db.sql("""select name from `tabEmail Queue`""")
self.assertTrue(email_queue)
def test_loan_repayment_salary_slip(self):
from erpnext.hr.doctype.loan.test_loan import create_loan_type, create_loan
applicant = make_employee("test_employee@salary.com")
create_loan_type("Car Loan", 500000, 6.4)
loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20)
from erpnext.loan_management.doctype.loan.test_loan import create_loan_type, create_loan, make_loan_disbursement_entry, create_loan_accounts
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import make_accrual_interest_entry_for_term_loans
applicant = make_employee("test_loanemployee@salary.com", company="_Test Company")
create_loan_accounts()
create_loan_type("Car Loan", 500000, 8.4,
is_term_loan=1,
mode_of_payment='Cash',
payment_account='Payment Account - _TC',
loan_account='Loan Account - _TC',
interest_income_account='Interest Income Account - _TC',
penalty_income_account='Penalty Income Account - _TC')
loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1))
loan.repay_from_salary = 1
loan.submit()
ss = make_employee_salary_slip("test_employee@salary.com", "Monthly")
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1))
make_accrual_interest_entry_for_term_loans(posting_date=nowdate())
ss = make_employee_salary_slip("test_loanemployee@salary.com", "Monthly")
ss.submit()
self.assertEqual(ss.total_loan_repayment, 582)
self.assertEqual(ss.total_loan_repayment, 592)
self.assertEqual(ss.net_pay, (flt(ss.gross_pay) - (flt(ss.total_deduction) + flt(ss.total_loan_repayment))))
def test_payroll_frequency(self):
@ -321,7 +341,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
return salary_slip
def make_salary_component(salary_components, test_tax):
def make_salary_component(salary_components, test_tax, company_list=None):
for salary_component in salary_components:
if not frappe.db.exists('Salary Component', salary_component["salary_component"]):
if test_tax:
@ -336,17 +356,22 @@ def make_salary_component(salary_components, test_tax):
salary_component["doctype"] = "Salary Component"
salary_component["salary_component_abbr"] = salary_component["abbr"]
frappe.get_doc(salary_component).insert()
get_salary_component_account(salary_component["salary_component"])
get_salary_component_account(salary_component["salary_component"], company_list)
def get_salary_component_account(sal_comp):
def get_salary_component_account(sal_comp, company_list=None):
company = erpnext.get_default_company()
if company_list and company not in company_list:
company_list.append(company)
sal_comp = frappe.get_doc("Salary Component", sal_comp)
if not sal_comp.get("accounts"):
sal_comp.append("accounts", {
"company": company,
"default_account": create_account(company)
})
sal_comp.save()
for d in company_list:
sal_comp.append("accounts", {
"company": d,
"default_account": create_account(d)
})
sal_comp.save()
def create_account(company):
salary_account = frappe.db.get_value("Account", "Salary - " + frappe.get_cached_value('Company', company, 'abbr'))
@ -359,7 +384,7 @@ def create_account(company):
}).insert()
return salary_account
def make_earning_salary_component(setup=False, test_tax=False):
def make_earning_salary_component(setup=False, test_tax=False, company_list=None):
data = [
{
"salary_component": 'Basic Salary',
@ -415,7 +440,7 @@ def make_earning_salary_component(setup=False, test_tax=False):
}
])
if setup or test_tax:
make_salary_component(data, test_tax)
make_salary_component(data, test_tax, company_list)
data.append({
"salary_component": 'Basic Salary',
"abbr":'BS',
@ -426,7 +451,7 @@ def make_earning_salary_component(setup=False, test_tax=False):
})
return data
def make_deduction_salary_component(setup=False, test_tax=False):
def make_deduction_salary_component(setup=False, test_tax=False, company_list=None):
data = [
{
"salary_component": 'Professional Tax',
@ -458,7 +483,7 @@ def make_deduction_salary_component(setup=False, test_tax=False):
"round_to_the_nearest_integer": 1
})
if setup or test_tax:
make_salary_component(data, test_tax)
make_salary_component(data, test_tax, company_list)
return data

View File

@ -1,263 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2017-11-08 12:51:12.834479",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "loan",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Loan",
"length": 0,
"no_copy": 0,
"options": "Loan",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "loan_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Loan Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "interest_income_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Interest Income Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_4",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "principal_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Principal Amount",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "interest_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Interest Amount",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "total_payment",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Total Payment",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-02-26 05:24:31.369630",
"modified_by": "Administrator",
"module": "HR",
"name": "Salary Slip Loan",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View File

@ -25,7 +25,6 @@ class TestSalaryStructure(unittest.TestCase):
make_employee("test_employee@salary.com")
make_employee("test_employee_2@salary.com")
def make_holiday_list(self):
if not frappe.db.get_value("Holiday List", "Salary Structure Test Holiday List"):
holiday_list = frappe.get_doc({
@ -38,6 +37,29 @@ class TestSalaryStructure(unittest.TestCase):
holiday_list.get_weekly_off_dates()
holiday_list.save()
def test_salary_structure_deduction_based_on_gross_pay(self):
emp = make_employee("test_employee_3@salary.com")
sal_struct = make_salary_structure("Salary Structure 2", "Monthly", dont_submit = True)
sal_struct.earnings = [sal_struct.earnings[0]]
sal_struct.earnings[0].amount_based_on_formula = 1
sal_struct.earnings[0].formula = "base"
sal_struct.deductions = [sal_struct.deductions[0]]
sal_struct.deductions[0].amount_based_on_formula = 1
sal_struct.deductions[0].condition = "gross_pay > 100"
sal_struct.deductions[0].formula = "gross_pay * 0.2"
sal_struct.submit()
assignment = create_salary_structure_assignment(emp, "Salary Structure 2")
ss = make_salary_slip(sal_struct.name, employee = emp)
self.assertEqual(assignment.base * 0.2, ss.deductions[0].amount)
def test_amount_totals(self):
frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 0)
sal_slip = frappe.get_value("Salary Slip", {"employee_name":"test_employee_2@salary.com"})
@ -86,16 +108,17 @@ class TestSalaryStructure(unittest.TestCase):
self.assertEqual(salary_structure_assignment.base, 5000)
self.assertEqual(salary_structure_assignment.variable, 200)
def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None, test_tax=False):
def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None,
test_tax=False, company=None):
if test_tax:
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure))
if not frappe.db.exists('Salary Structure', salary_structure):
details = {
"doctype": "Salary Structure",
"name": salary_structure,
"company": erpnext.get_default_company(),
"earnings": make_earning_salary_component(test_tax=test_tax),
"deductions": make_deduction_salary_component(test_tax=test_tax),
"company": company or erpnext.get_default_company(),
"earnings": make_earning_salary_component(test_tax=test_tax, company_list=["_Test Company"]),
"deductions": make_deduction_salary_component(test_tax=test_tax, company_list=["_Test Company"]),
"payroll_frequency": payroll_frequency,
"payment_account": get_random("Account")
}
@ -109,11 +132,11 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do
if employee and not frappe.db.get_value("Salary Structure Assignment",
{'employee':employee, 'docstatus': 1}) and salary_structure_doc.docstatus==1:
create_salary_structure_assignment(employee, salary_structure)
create_salary_structure_assignment(employee, salary_structure, company=company)
return salary_structure_doc
def create_salary_structure_assignment(employee, salary_structure, from_date=None):
def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None):
if frappe.db.exists("Salary Structure Assignment", {"employee": employee}):
frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee))
salary_structure_assignment = frappe.new_doc("Salary Structure Assignment")
@ -122,7 +145,7 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non
salary_structure_assignment.variable = 5000
salary_structure_assignment.from_date = from_date or add_months(nowdate(), -1)
salary_structure_assignment.salary_structure = salary_structure
salary_structure_assignment.company = erpnext.get_default_company()
salary_structure_assignment.company = company or erpnext.get_default_company()
salary_structure_assignment.save(ignore_permissions=True)
salary_structure_assignment.submit()
return salary_structure_assignment

View File

@ -75,7 +75,7 @@ class ShiftType(Document):
for date in dates:
shift_details = get_employee_shift(employee, date, True)
if shift_details and shift_details.shift_type.name == self.name:
mark_attendance(employee, date, self.name, 'Absent')
mark_attendance(employee, date, 'Absent', self.name)
def get_assigned_employee(self, from_date=None, consider_default_shift=False):
filters = {'date':('>=', from_date), 'shift_type': self.name, 'docstatus': '1'}

View File

@ -76,7 +76,7 @@ def get_data(filters, leave_types):
opening = get_leave_balance_on(employee.name, leave_type, filters.from_date)
# closing balance
closing = get_leave_balance_on(employee.name, leave_type, filters.to_date)
closing = max(opening - leaves_taken, 0)
row += [opening, leaves_taken, closing]

View File

@ -1,9 +0,0 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Loan Repayment"] = {
"filters": [
]
}

View File

@ -1,28 +0,0 @@
{
"add_total_row": 0,
"creation": "2019-03-29 18:58:00.166032",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
"letter_head": "",
"modified": "2019-03-29 18:58:00.166032",
"modified_by": "Administrator",
"module": "HR",
"name": "Loan Repayment",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Loan",
"report_name": "Loan Repayment",
"report_type": "Script Report",
"roles": [
{
"role": "HR Manager"
},
{
"role": "Employee"
}
]
}

View File

@ -1,95 +0,0 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
def execute(filters=None):
columns = create_columns()
data = get_record()
return columns, data
def create_columns():
return [
{
"label": _("Employee"),
"fieldtype": "Data",
"fieldname": "employee",
"options": "Employee",
"width": 200
},
{
"label": _("Loan"),
"fieldtype": "Link",
"fieldname": "loan_name",
"options": "Loan",
"width": 200
},
{
"label": _("Loan Amount"),
"fieldtype": "Currency",
"fieldname": "loan_amount",
"options": "currency",
"width": 100
},
{
"label": _("Interest"),
"fieldtype": "Data",
"fieldname": "interest",
"width": 100
},
{
"label": _("Payable Amount"),
"fieldtype": "Currency",
"fieldname": "payable_amount",
"options": "currency",
"width": 100
},
{
"label": _("EMI"),
"fieldtype": "Currency",
"fieldname": "emi",
"options": "currency",
"width": 100
},
{
"label": _("Paid Amount"),
"fieldtype": "Currency",
"fieldname": "paid_amount",
"options": "currency",
"width": 100
},
{
"label": _("Outstanding Amount"),
"fieldtype": "Currency",
"fieldname": "out_amt",
"options": "currency",
"width": 100
},
]
def get_record():
data = []
loans = frappe.get_all("Loan",
filters=[("status", "=", "Disbursed")],
fields=["applicant", "applicant_name", "name", "loan_amount", "rate_of_interest",
"total_payment", "monthly_repayment_amount", "total_amount_paid"]
)
for loan in loans:
row = {
"employee": loan.applicant + ": " + loan.applicant_name,
"loan_name": loan.name,
"loan_amount": loan.loan_amount,
"interest": str(loan.rate_of_interest) + "%",
"payable_amount": loan.total_payment,
"emi": loan.monthly_repayment_amount,
"paid_amount": loan.total_amount_paid,
"out_amt": loan.total_payment - loan.total_amount_paid
}
data.append(row)
return data

View File

@ -0,0 +1,190 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
{% include 'erpnext/loan_management/loan_common.js' %};
frappe.ui.form.on('Loan', {
setup: function(frm) {
frm.make_methods = {
'Loan Disbursement': function() { frm.trigger('make_loan_disbursement') },
'Loan Security Unpledge': function() { frm.trigger('create_loan_security_unpledge') }
}
},
onload: function (frm) {
frm.set_query("loan_application", function () {
return {
"filters": {
"applicant": frm.doc.applicant,
"docstatus": 1,
"status": "Approved"
}
};
});
$.each(["penalty_income_account", "interest_income_account"], function(i, field) {
frm.set_query(field, function () {
return {
"filters": {
"company": frm.doc.company,
"root_type": "Income",
"is_group": 0
}
};
});
});
$.each(["payment_account", "loan_account"], function (i, field) {
frm.set_query(field, function () {
return {
"filters": {
"company": frm.doc.company,
"root_type": "Asset",
"is_group": 0
}
};
});
})
frm.set_query('loan_security_pledge', function(doc, cdt, cdn) {
return {
filters: {
applicant: frm.doc.applicant,
docstatus: 1,
loan_application: frm.doc.loan_application || ''
}
};
});
},
refresh: function (frm) {
if (frm.doc.docstatus == 1) {
if (frm.doc.status == "Sanctioned" || frm.doc.status == 'Partially Disbursed') {
frm.add_custom_button(__('Loan Disbursement'), function() {
frm.trigger("make_loan_disbursement");
},__('Create'));
}
if (["Disbursed", "Partially Disbursed"].includes(frm.doc.status) && (!frm.doc.repay_from_salary)) {
frm.add_custom_button(__('Loan Repayment'), function() {
frm.trigger("make_repayment_entry");
},__('Create'));
}
if (frm.doc.status == "Loan Closure Requested") {
frm.add_custom_button(__('Loan Security Unpledge'), function() {
frm.trigger("create_loan_security_unpledge");
},__('Create'));
}
}
frm.trigger("toggle_fields");
},
loan_type: function(frm) {
frm.toggle_reqd("repayment_method", frm.doc.is_term_loan);
frm.toggle_display("repayment_method", 1 - frm.doc.is_term_loan);
frm.toggle_display("repayment_periods", s1 - frm.doc.is_term_loan);
},
is_secured_loan: function(frm) {
frm.toggle_reqd("loan_security_pledge", frm.doc.is_secured_loan);
},
make_loan_disbursement: function (frm) {
frappe.call({
args: {
"loan": frm.doc.name,
"company": frm.doc.company,
"applicant_type": frm.doc.applicant_type,
"applicant": frm.doc.applicant,
"as_dict": 1
},
method: "erpnext.loan_management.doctype.loan.loan.make_loan_disbursement",
callback: function (r) {
if (r.message)
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
})
},
make_repayment_entry: function(frm) {
frappe.call({
args: {
"loan": frm.doc.name,
"applicant_type": frm.doc.applicant_type,
"applicant": frm.doc.applicant,
"loan_type": frm.doc.loan_type,
"company": frm.doc.company,
"as_dict": 1
},
method: "erpnext.loan_management.doctype.loan.loan.make_repayment_entry",
callback: function (r) {
if (r.message)
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
})
},
create_loan_security_unpledge: function(frm) {
frappe.call({
method: "erpnext.loan_management.doctype.loan.loan.create_loan_security_unpledge",
args : {
"loan": frm.doc.name,
"applicant_type": frm.doc.applicant_type,
"applicant": frm.doc.applicant,
"company": frm.doc.company
},
callback: function(r) {
if (r.message)
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
})
},
loan_application: function (frm) {
if(frm.doc.loan_application){
return frappe.call({
method: "erpnext.loan_management.doctype.loan.loan.get_loan_application",
args: {
"loan_application": frm.doc.loan_application
},
callback: function (r) {
if (!r.exc && r.message) {
let loan_fields = ["loan_type", "loan_amount", "repayment_method",
"monthly_repayment_amount", "repayment_periods", "rate_of_interest", "is_secured_loan"]
loan_fields.forEach(field => {
frm.set_value(field, r.message[field]);
});
if (frm.doc.is_secured_loan) {
$.each(r.message.proposed_pledges, function(i, d) {
let row = frm.add_child("securities");
row.loan_security = d.loan_security;
row.qty = d.qty;
row.loan_security_price = d.loan_security_price;
row.amount = d.amount;
row.haircut = d.haircut;
});
frm.refresh_fields("securities");
}
}
}
});
}
},
repayment_method: function (frm) {
frm.trigger("toggle_fields")
},
toggle_fields: function (frm) {
frm.toggle_enable("monthly_repayment_amount", frm.doc.repayment_method == "Repay Fixed Amount per Period")
frm.toggle_enable("repayment_periods", frm.doc.repayment_method == "Repay Over Number of Periods")
}
});

View File

@ -2,7 +2,7 @@
"actions": [],
"allow_import": 1,
"autoname": "ACC-LOAN-.YYYY.-.#####",
"creation": "2016-12-02 10:11:49.673604",
"creation": "2019-08-29 17:29:18.176786",
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
@ -11,32 +11,41 @@
"applicant_type",
"applicant",
"applicant_name",
"loan_type",
"loan_application",
"column_break_3",
"posting_date",
"company",
"posting_date",
"status",
"repay_from_salary",
"section_break_8",
"loan_type",
"loan_amount",
"is_secured_loan",
"rate_of_interest",
"disbursement_date",
"repayment_start_date",
"disbursed_amount",
"column_break_11",
"is_term_loan",
"repayment_method",
"repayment_periods",
"monthly_repayment_amount",
"repayment_start_date",
"loan_security_details_section",
"loan_security_pledge",
"column_break_25",
"maximum_loan_value",
"account_info",
"mode_of_payment",
"payment_account",
"column_break_9",
"loan_account",
"interest_income_account",
"penalty_income_account",
"section_break_15",
"repayment_schedule",
"section_break_17",
"total_payment",
"total_principal_paid",
"column_break_19",
"total_interest_payable",
"total_amount_paid",
@ -47,7 +56,7 @@
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember",
"options": "Employee\nMember\nCustomer",
"reqd": 1
},
{
@ -75,6 +84,7 @@
"fieldname": "loan_type",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Loan Type",
"options": "Loan Type",
"reqd": 1
@ -95,6 +105,7 @@
{
"fieldname": "company",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"remember_last_selected_value": 1,
@ -104,9 +115,10 @@
"default": "Sanctioned",
"fieldname": "status",
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Sanctioned\nDisbursed\nRepaid/Closed",
"options": "Sanctioned\nPartially Disbursed\nDisbursed\nLoan Closure Requested\nClosed",
"read_only": 1
},
{
@ -125,8 +137,7 @@
"fieldname": "loan_amount",
"fieldtype": "Currency",
"label": "Loan Amount",
"options": "Company:company:default_currency",
"reqd": 1
"options": "Company:company:default_currency"
},
{
"fetch_from": "loan_type.rate_of_interest",
@ -143,29 +154,30 @@
"label": "Disbursement Date"
},
{
"depends_on": "is_term_loan",
"fieldname": "repayment_start_date",
"fieldtype": "Date",
"label": "Repayment Start Date",
"reqd": 1
"label": "Repayment Start Date"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"default": "Repay Over Number of Periods",
"depends_on": "is_term_loan",
"fieldname": "repayment_method",
"fieldtype": "Select",
"label": "Repayment Method",
"options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods",
"reqd": 1
"options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods"
},
{
"depends_on": "is_term_loan",
"fieldname": "repayment_periods",
"fieldtype": "Int",
"label": "Repayment Period in Months"
},
{
"depends_on": "is_term_loan",
"fieldname": "monthly_repayment_amount",
"fieldtype": "Currency",
"label": "Monthly Repayment Amount",
@ -178,17 +190,21 @@
"label": "Account Info"
},
{
"fetch_from": "loan_type.mode_of_payment",
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
"options": "Mode of Payment",
"read_only": 1,
"reqd": 1
},
{
"fetch_from": "loan_type.payment_account",
"fieldname": "payment_account",
"fieldtype": "Link",
"label": "Payment Account",
"options": "Account",
"read_only": 1,
"reqd": 1
},
{
@ -196,25 +212,31 @@
"fieldtype": "Column Break"
},
{
"fetch_from": "loan_type.loan_account",
"fieldname": "loan_account",
"fieldtype": "Link",
"label": "Loan Account",
"options": "Account",
"read_only": 1,
"reqd": 1
},
{
"fetch_from": "loan_type.interest_income_account",
"fieldname": "interest_income_account",
"fieldtype": "Link",
"label": "Interest Income Account",
"options": "Account",
"read_only": 1,
"reqd": 1
},
{
"depends_on": "is_term_loan",
"fieldname": "section_break_15",
"fieldtype": "Section Break",
"label": "Repayment Schedule"
},
{
"depends_on": "eval:doc.is_term_loan == 1",
"fieldname": "repayment_schedule",
"fieldtype": "Table",
"label": "Repayment Schedule",
@ -230,7 +252,7 @@
"default": "0",
"fieldname": "total_payment",
"fieldtype": "Currency",
"label": "Total Payment",
"label": "Total Payable Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
@ -240,6 +262,7 @@
},
{
"default": "0",
"depends_on": "is_term_loan",
"fieldname": "total_interest_payable",
"fieldtype": "Currency",
"label": "Total Interest Payable",
@ -262,13 +285,74 @@
"options": "Loan",
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "is_secured_loan",
"fieldtype": "Check",
"label": "Is Secured Loan"
},
{
"depends_on": "is_secured_loan",
"fieldname": "loan_security_details_section",
"fieldtype": "Section Break",
"label": "Loan Security Details"
},
{
"default": "0",
"fetch_from": "loan_type.is_term_loan",
"fieldname": "is_term_loan",
"fieldtype": "Check",
"label": "Is Term Loan",
"read_only": 1
},
{
"fetch_from": "loan_type.penalty_income_account",
"fieldname": "penalty_income_account",
"fieldtype": "Link",
"label": "Penalty Income Account",
"options": "Account",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "total_principal_paid",
"fieldtype": "Currency",
"label": "Total Principal Paid",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "loan_security_pledge",
"fieldtype": "Link",
"label": "Loan Security Pledge",
"options": "Loan Security Pledge"
},
{
"fieldname": "disbursed_amount",
"fieldtype": "Currency",
"label": "Disbursed Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fetch_from": "loan_security_pledge.maximum_loan_value",
"fieldname": "maximum_loan_value",
"fieldtype": "Currency",
"label": "Maximum Loan Value",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "column_break_25",
"fieldtype": "Column Break"
}
],
"is_submittable": 1,
"links": [],
"modified": "2019-12-12 14:45:38.823072",
"modified": "2020-02-07 01:31:25.172173",
"modified_by": "Administrator",
"module": "HR",
"module": "Loan Management",
"name": "Loan",
"owner": "Administrator",
"permissions": [
@ -281,7 +365,7 @@
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1

View File

@ -0,0 +1,254 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, math, json
import erpnext
from frappe import _
from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime
from erpnext.controllers.accounts_controller import AccountsController
class Loan(AccountsController):
def validate(self):
self.set_loan_amount()
self.set_missing_fields()
self.validate_accounts()
self.validate_loan_security_pledge()
self.validate_loan_amount()
self.check_sanctioned_amount_limit()
if self.is_term_loan:
validate_repayment_method(self.repayment_method, self.loan_amount, self.monthly_repayment_amount,
self.repayment_periods, self.is_term_loan)
self.make_repayment_schedule()
self.set_repayment_period()
self.calculate_totals()
def validate_accounts(self):
for fieldname in ['payment_account', 'loan_account', 'interest_income_account', 'penalty_income_account']:
company = frappe.get_value("Account", self.get(fieldname), 'company')
if company != self.company:
frappe.throw(_("Account {0} does not belongs to company {1}").format(frappe.bold(self.get(fieldname)),
frappe.bold(self.company)))
def on_submit(self):
self.link_loan_security_pledge()
def on_cancel(self):
self.unlink_loan_security_pledge()
def set_missing_fields(self):
if not self.company:
self.company = erpnext.get_default_company()
if not self.posting_date:
self.posting_date = nowdate()
if self.loan_type and not self.rate_of_interest:
self.rate_of_interest = frappe.db.get_value("Loan Type", self.loan_type, "rate_of_interest")
if self.repayment_method == "Repay Over Number of Periods":
self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
def validate_loan_security_pledge(self):
if self.is_secured_loan and not self.loan_security_pledge:
frappe.throw(_("Loan Security Pledge is mandatory for secured loan"))
if self.loan_security_pledge:
loan_security_details = frappe.db.get_value("Loan Security Pledge", self.loan_security_pledge,
['loan', 'company'], as_dict=1)
if loan_security_details.loan:
frappe.throw(_("Loan Security Pledge already pledged against loan {0}").format(loan_security_details.loan))
if loan_security_details.company != self.company:
frappe.throw(_("Loan Security Pledge Company and Loan Company must be same"))
def check_sanctioned_amount_limit(self):
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company)
if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit):
frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant)))
def make_repayment_schedule(self):
if not self.repayment_start_date:
frappe.throw(_("Repayment Start Date is mandatory for term loans"))
self.repayment_schedule = []
payment_date = self.repayment_start_date
balance_amount = self.loan_amount
while(balance_amount > 0):
interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100))
principal_amount = self.monthly_repayment_amount - interest_amount
balance_amount = rounded(balance_amount + interest_amount - self.monthly_repayment_amount)
if balance_amount < 0:
principal_amount += balance_amount
balance_amount = 0.0
total_payment = principal_amount + interest_amount
self.append("repayment_schedule", {
"payment_date": payment_date,
"principal_amount": principal_amount,
"interest_amount": interest_amount,
"total_payment": total_payment,
"balance_loan_amount": balance_amount
})
next_payment_date = add_months(payment_date, 1)
payment_date = next_payment_date
def set_repayment_period(self):
if self.repayment_method == "Repay Fixed Amount per Period":
repayment_periods = len(self.repayment_schedule)
self.repayment_periods = repayment_periods
def calculate_totals(self):
self.total_payment = 0
self.total_interest_payable = 0
self.total_amount_paid = 0
if self.is_term_loan:
for data in self.repayment_schedule:
self.total_payment += data.total_payment
self.total_interest_payable +=data.interest_amount
else:
self.total_payment = self.loan_amount
def set_loan_amount(self):
if not self.loan_amount and self.is_secured_loan and self.loan_security_pledge:
self.loan_amount = self.maximum_loan_value
def validate_loan_amount(self):
if self.is_secured_loan and self.loan_amount > self.maximum_loan_value:
msg = _("Loan amount cannot be greater than {0}").format(self.maximum_loan_value)
frappe.throw(msg)
if not self.loan_amount:
frappe.throw(_("Loan amount is mandatory"))
def link_loan_security_pledge(self):
frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET
loan = %s, status = 'Pledged', pledge_time = %s
where name = %s """, (self.name, now_datetime(), self.loan_security_pledge))
def unlink_loan_security_pledge(self):
frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET
loan = '', status = 'Unpledged'
where name = %s """, (self.loan_security_pledge))
def update_total_amount_paid(doc):
total_amount_paid = 0
for data in doc.repayment_schedule:
if data.paid:
total_amount_paid += data.total_payment
frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid)
def get_total_loan_amount(applicant_type, applicant, company):
return frappe.db.get_value('Loan',
{'applicant_type': applicant_type, 'company': company, 'applicant': applicant, 'docstatus': 1},
'sum(loan_amount)')
def get_sanctioned_amount_limit(applicant_type, applicant, company):
return frappe.db.get_value('Sanctioned Loan Amount',
{'applicant_type': applicant_type, 'company': company, 'applicant': applicant},
'sanctioned_amount_limit')
def validate_repayment_method(repayment_method, loan_amount, monthly_repayment_amount, repayment_periods, is_term_loan):
if is_term_loan and not repayment_method:
frappe.throw(_("Repayment Method is mandatory for term loans"))
if repayment_method == "Repay Over Number of Periods" and not repayment_periods:
frappe.throw(_("Please enter Repayment Periods"))
if repayment_method == "Repay Fixed Amount per Period":
if not monthly_repayment_amount:
frappe.throw(_("Please enter repayment Amount"))
if monthly_repayment_amount > loan_amount:
frappe.throw(_("Monthly Repayment Amount cannot be greater than Loan Amount"))
def get_monthly_repayment_amount(repayment_method, loan_amount, rate_of_interest, repayment_periods):
if rate_of_interest:
monthly_interest_rate = flt(rate_of_interest) / (12 *100)
monthly_repayment_amount = math.ceil((loan_amount * monthly_interest_rate *
(1 + monthly_interest_rate)**repayment_periods) \
/ ((1 + monthly_interest_rate)**repayment_periods - 1))
else:
monthly_repayment_amount = math.ceil(flt(loan_amount) / repayment_periods)
return monthly_repayment_amount
@frappe.whitelist()
def get_loan_application(loan_application):
loan = frappe.get_doc("Loan Application", loan_application)
if loan:
return loan.as_dict()
def close_loan(loan, total_amount_paid):
frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid)
frappe.db.set_value("Loan", loan, "status", "Closed")
@frappe.whitelist()
def make_loan_disbursement(loan, company, applicant_type, applicant, disbursed_amount=0, as_dict=0):
disbursement_entry = frappe.new_doc("Loan Disbursement")
disbursement_entry.against_loan = loan
disbursement_entry.applicant_type = applicant_type
disbursement_entry.applicant = applicant
disbursement_entry.company = company
disbursement_entry.disbursement_date = nowdate()
if disbursed_amount:
disbursement_entry.disbursed_amount = disbursed_amount
if as_dict:
return disbursement_entry.as_dict()
else:
return disbursement_entry
@frappe.whitelist()
def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as_dict=0):
repayment_entry = frappe.new_doc("Loan Repayment")
repayment_entry.against_loan = loan
repayment_entry.applicant_type = applicant_type
repayment_entry.applicant = applicant
repayment_entry.company = company
repayment_entry.loan_type = loan_type
repayment_entry.posting_date = nowdate()
if as_dict:
return repayment_entry.as_dict()
else:
return repayment_entry
@frappe.whitelist()
def create_loan_security_unpledge(loan, applicant_type, applicant, company):
loan_security_pledge_details = frappe.db.sql("""
SELECT p.parent, p.loan_security, p.qty as qty FROM `tabLoan Security Pledge` lsp , `tabPledge` p
WHERE p.parent = lsp.name AND lsp.loan = %s AND lsp.docstatus = 1
""",(loan), as_dict=1)
unpledge_request = frappe.new_doc("Loan Security Unpledge")
unpledge_request.applicant_type = applicant_type
unpledge_request.applicant = applicant
unpledge_request.loan = loan
unpledge_request.company = company
for loan_security in loan_security_pledge_details:
unpledge_request.append('securities', {
"loan_security": loan_security.loan_security,
"qty": loan_security.qty,
"against_pledge": loan_security.parent
})
return unpledge_request.as_dict()

View File

@ -0,0 +1,19 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'loan',
'non_standard_fieldnames': {
'Loan Disbursement': 'against_loan',
'Loan Repayment': 'against_loan',
},
'transactions': [
{
'items': ['Loan Security Pledge', 'Loan Security Shortfall', 'Loan Disbursement']
},
{
'items': ['Loan Repayment', 'Loan Interest Accrual', 'Loan Security Unpledge']
}
]
}

View File

@ -0,0 +1,559 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import erpnext
import unittest
from frappe.utils import (nowdate, add_days, getdate, now_datetime, add_to_date, get_datetime,
add_months, get_first_day, get_last_day, flt, date_diff)
from erpnext.selling.doctype.customer.test_customer import get_customer_dict
from erpnext.hr.doctype.salary_structure.test_salary_structure import make_employee
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import (make_accrual_interest_entry_for_term_loans, days_in_year)
from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import check_for_ltv_shortfall
class TestLoan(unittest.TestCase):
def setUp(self):
create_loan_accounts()
create_loan_type("Personal Loan", 500000, 8.4,
is_term_loan=1,
mode_of_payment='Cash',
payment_account='Payment Account - _TC',
loan_account='Loan Account - _TC',
interest_income_account='Interest Income Account - _TC',
penalty_income_account='Penalty Income Account - _TC')
create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
'Interest Income Account - _TC', 'Penalty Income Account - _TC')
create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
'Interest Income Account - _TC', 'Penalty Income Account - _TC')
create_loan_security_type()
create_loan_security()
create_loan_security_price("Test Security 1", 500, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24)))
create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24)))
self.applicant1 = make_employee("robert_loan@loan.com")
if not frappe.db.exists("Customer", "_Test Loan Customer"):
frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True)
self.applicant2 = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name')
create_loan(self.applicant1, "Personal Loan", 280000, "Repay Over Number of Periods", 20)
def test_loan(self):
loan = frappe.get_doc("Loan", {"applicant":self.applicant1})
self.assertEquals(loan.monthly_repayment_amount, 15052)
self.assertEquals(loan.total_interest_payable, 21034)
self.assertEquals(loan.total_payment, 301034)
schedule = loan.repayment_schedule
self.assertEqual(len(schedule), 20)
for idx, principal_amount, interest_amount, balance_loan_amount in [[3, 13369, 1683, 227079], [19, 14941, 105, 0], [17, 14740, 312, 29785]]:
self.assertEqual(schedule[idx].principal_amount, principal_amount)
self.assertEqual(schedule[idx].interest_amount, interest_amount)
self.assertEqual(schedule[idx].balance_loan_amount, balance_loan_amount)
loan.repayment_method = "Repay Fixed Amount per Period"
loan.monthly_repayment_amount = 14000
loan.save()
self.assertEquals(len(loan.repayment_schedule), 22)
self.assertEquals(loan.total_interest_payable, 22712)
self.assertEquals(loan.total_payment, 302712)
def test_loan_with_security(self):
pledges = []
pledges.append({
"loan_security": "Test Security 1",
"qty": 4000.00,
"haircut": 50,
"loan_security_price": 500.00
})
loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges)
loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_security_pledge.name)
self.assertEquals(loan.loan_amount, 1000000)
def test_loan_disbursement(self):
pledges = []
pledges.append({
"loan_security": "Test Security 1",
"qty": 4000.00,
"haircut": 50
})
loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges)
loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_security_pledge.name)
self.assertEquals(loan.loan_amount, 1000000)
loan.submit()
loan_disbursement_entry1 = make_loan_disbursement_entry(loan.name, 500000)
loan_disbursement_entry2 = make_loan_disbursement_entry(loan.name, 500000)
loan = frappe.get_doc("Loan", loan.name)
gl_entries1 = frappe.db.get_all("GL Entry",
fields=["name"],
filters = {'voucher_type': 'Loan Disbursement', 'voucher_no': loan_disbursement_entry1.name}
)
gl_entries2 = frappe.db.get_all("GL Entry",
fields=["name"],
filters = {'voucher_type': 'Loan Disbursement', 'voucher_no': loan_disbursement_entry2.name}
)
self.assertEquals(loan.status, "Disbursed")
self.assertEquals(loan.disbursed_amount, 1000000)
self.assertTrue(gl_entries1)
self.assertTrue(gl_entries2)
def test_regular_loan_repayment(self):
pledges = []
pledges.append({
"loan_security": "Test Security 1",
"qty": 4000.00,
"haircut": 50
})
loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_security_pledge.name,
posting_date=get_first_day(nowdate()))
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
first_date = '2019-10-01'
last_date = '2019-10-30'
no_of_days = date_diff(last_date, first_date) + 1
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual(posting_date = last_date)
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), "Regular Payment", 111118.68)
repayment_entry.save()
penalty_amount = (accrued_interest_amount * 5 * 25) / (100 * days_in_year(get_datetime(first_date).year))
self.assertEquals(flt(repayment_entry.interest_payable, 2), flt(accrued_interest_amount, 2))
self.assertEquals(flt(repayment_entry.penalty_amount, 2), flt(penalty_amount, 2))
repayment_entry.submit()
def test_loan_closure_repayment(self):
pledges = []
pledges.append({
"loan_security": "Test Security 1",
"qty": 4000.00,
"haircut": 50
})
loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_security_pledge.name,
posting_date=get_first_day(nowdate()))
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
first_date = '2019-10-01'
last_date = '2019-10-30'
no_of_days = date_diff(last_date, first_date) + 1
# Adding 6 since repayment is made 5 days late after due date
# and since payment type is loan closure so interest should be considered for those
# 6 days as well though in grace period
no_of_days += 6
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual(posting_date = last_date)
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
"Loan Closure", 13315.0681)
repayment_entry.save()
repayment_entry.amount_paid = repayment_entry.payable_amount
self.assertEquals(flt(repayment_entry.interest_payable, 3), flt(accrued_interest_amount, 3))
self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0)
repayment_entry.submit()
loan.load_from_db()
self.assertEquals(loan.status, "Loan Closure Requested")
def test_loan_repayment_for_term_loan(self):
pledges = []
pledges.append({
"loan_security": "Test Security 2",
"qty": 4000.00,
"haircut": 50
})
pledges.append({
"loan_security": "Test Security 1",
"qty": 2000.00,
"haircut": 50
})
loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges)
loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12,
loan_security_pledge.name, posting_date=add_months(nowdate(), -1))
loan.submit()
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1))
make_accrual_interest_entry_for_term_loans(posting_date=nowdate())
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(get_last_day(nowdate()), 5),
"Regular Payment", 89768.7534247)
repayment_entry.save()
repayment_entry.submit()
repayment_entry.load_from_db()
self.assertEquals(repayment_entry.interest_payable, 11250.00)
self.assertEquals(repayment_entry.payable_principal_amount, 78303.00)
def test_partial_loan_repayment(self):
pledges = []
pledges.append({
"loan_security": "Test Security 1",
"qty": 4000.00,
"haircut": 50
})
loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_security_pledge.name,
posting_date=get_first_day(nowdate()))
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
first_date = '2019-10-01'
last_date = '2019-10-30'
no_of_days = date_diff(last_date, first_date) + 1
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime().year) * 100)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual(posting_date = add_days(first_date, 15))
process_loan_interest_accrual(posting_date = add_days(first_date, 30))
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 1), "Regular Payment", 6500)
repayment_entry.save()
repayment_entry.submit()
penalty_amount = (accrued_interest_amount * 4 * 25) / (100 * days_in_year(get_datetime(first_date).year))
lia = frappe.get_all("Loan Interest Accrual", fields=["is_paid"],
filters={"loan": loan.name}, order_by="posting_date")
self.assertTrue(lia[0].get('is_paid'))
self.assertFalse(lia[1].get('is_paid'))
def test_security_shortfall(self):
pledges = []
pledges.append({
"loan_security": "Test Security 2",
"qty": 8000.00,
"haircut": 50,
})
loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges)
loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_security_pledge.name)
loan.submit()
make_loan_disbursement_entry(loan.name, loan.loan_amount)
frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = %s
where loan_security=%s""", (100, 'Test Security 2'))
check_for_ltv_shortfall()
loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name})
self.assertTrue(loan_security_shortfall)
self.assertEquals(loan_security_shortfall.loan_amount, 1000000.00)
self.assertEquals(loan_security_shortfall.security_value, 400000.00)
self.assertEquals(loan_security_shortfall.shortfall_amount, 600000.00)
def create_loan_accounts():
if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"):
frappe.get_doc({
"doctype": "Account",
"account_name": "Loans and Advances (Assets)",
"company": "_Test Company",
"root_type": "Asset",
"report_type": "Balance Sheet",
"currency": "INR",
"parent_account": "Current Assets - _TC",
"account_type": "Bank",
"is_group": 1
}).insert(ignore_permissions=True)
if not frappe.db.exists("Account", "Loan Account - _TC"):
frappe.get_doc({
"doctype": "Account",
"company": "_Test Company",
"account_name": "Loan Account",
"root_type": "Asset",
"report_type": "Balance Sheet",
"currency": "INR",
"parent_account": "Loans and Advances (Assets) - _TC",
"account_type": "Bank",
}).insert(ignore_permissions=True)
if not frappe.db.exists("Account", "Payment Account - _TC"):
frappe.get_doc({
"doctype": "Account",
"company": "_Test Company",
"account_name": "Payment Account",
"root_type": "Asset",
"report_type": "Balance Sheet",
"currency": "INR",
"parent_account": "Bank Accounts - _TC",
"account_type": "Bank",
}).insert(ignore_permissions=True)
if not frappe.db.exists("Account", "Interest Income Account - _TC"):
frappe.get_doc({
"doctype": "Account",
"company": "_Test Company",
"root_type": "Income",
"account_name": "Interest Income Account",
"report_type": "Profit and Loss",
"currency": "INR",
"parent_account": "Direct Income - _TC",
"account_type": "Income Account",
}).insert(ignore_permissions=True)
if not frappe.db.exists("Account", "Penalty Income Account - _TC"):
frappe.get_doc({
"doctype": "Account",
"company": "_Test Company",
"account_name": "Penalty Income Account",
"root_type": "Income",
"report_type": "Profit and Loss",
"currency": "INR",
"parent_account": "Direct Income - _TC",
"account_type": "Income Account",
}).insert(ignore_permissions=True)
def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_interest_rate=None, is_term_loan=None, grace_period_in_days=None,
mode_of_payment=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None,
repayment_method=None, repayment_periods=None):
if not frappe.db.exists("Loan Type", loan_name):
loan_type = frappe.get_doc({
"doctype": "Loan Type",
"company": "_Test Company",
"loan_name": loan_name,
"is_term_loan": is_term_loan,
"maximum_loan_amount": maximum_loan_amount,
"rate_of_interest": rate_of_interest,
"penalty_interest_rate": penalty_interest_rate,
"grace_period_in_days": grace_period_in_days,
"mode_of_payment": mode_of_payment,
"payment_account": payment_account,
"loan_account": loan_account,
"interest_income_account": interest_income_account,
"penalty_income_account": penalty_income_account,
"repayment_method": repayment_method,
"repayment_periods": repayment_periods
}).insert()
loan_type.submit()
def create_loan_security_type():
if not frappe.db.exists("Loan Security Type", "Stock"):
frappe.get_doc({
"doctype": "Loan Security Type",
"loan_security_type": "Stock",
"unit_of_measure": "Nos",
"haircut": 50.00
}).insert(ignore_permissions=True)
def create_loan_security():
if not frappe.db.exists("Loan Security", "Test Security 1"):
frappe.get_doc({
"doctype": "Loan Security",
"loan_security_type": "Stock",
"loan_security_code": "532779",
"loan_security_name": "Test Security 1",
"unit_of_measure": "Nos",
"haircut": 50.00,
}).insert(ignore_permissions=True)
if not frappe.db.exists("Loan Security", "Test Security 2"):
frappe.get_doc({
"doctype": "Loan Security",
"loan_security_type": "Stock",
"loan_security_code": "531335",
"loan_security_name": "Test Security 2",
"unit_of_measure": "Nos",
"haircut": 50.00,
}).insert(ignore_permissions=True)
def create_loan_security_pledge(applicant, pledges):
lsp = frappe.new_doc("Loan Security Pledge")
lsp.applicant_type = 'Customer'
lsp.applicant = applicant
lsp.company = "_Test Company"
for pledge in pledges:
lsp.append('securities', {
"loan_security": pledge['loan_security'],
"qty": pledge['qty'],
"haircut": pledge['haircut']
})
lsp.save()
lsp.submit()
return lsp
def make_loan_disbursement_entry(loan, amount, disbursement_date=None):
loan_disbursement_entry = frappe.get_doc({
"doctype": "Loan Disbursement",
"against_loan": loan,
"disbursement_date": disbursement_date,
"company": "_Test Company",
"disbursed_amount": amount,
"cost_center": 'Main - _TC'
}).insert(ignore_permissions=True)
loan_disbursement_entry.save()
loan_disbursement_entry.submit()
return loan_disbursement_entry
def create_loan_security_price(loan_security, loan_security_price, uom, from_date, to_date):
if not frappe.db.get_value("Loan Security Price",{"loan_security": loan_security,
"valid_from": ("<=", from_date), "valid_upto": (">=", to_date)}, 'name'):
lsp = frappe.get_doc({
"doctype": "Loan Security Price",
"loan_security": loan_security,
"loan_security_price": loan_security_price,
"uom": uom,
"valid_from":from_date,
"valid_upto": to_date
}).insert(ignore_permissions=True)
def create_repayment_entry(loan, applicant, posting_date, payment_type, paid_amount):
lr = frappe.get_doc({
"doctype": "Loan Repayment",
"against_loan": loan,
"payment_type": payment_type,
"company": "_Test Company",
"posting_date": posting_date or nowdate(),
"applicant": applicant,
"amount_paid": paid_amount,
"loan_type": "Stock Loan"
}).insert(ignore_permissions=True)
return lr
def create_loan(applicant, loan_type, loan_amount, repayment_method, repayment_periods,
repayment_start_date=None, posting_date=None):
loan = frappe.get_doc({
"doctype": "Loan",
"applicant_type": "Employee",
"company": "_Test Company",
"applicant": applicant,
"loan_type": loan_type,
"loan_amount": loan_amount,
"repayment_method": repayment_method,
"repayment_periods": repayment_periods,
"repayment_start_date": nowdate(),
"is_term_loan": 1,
"posting_date": posting_date or nowdate()
})
loan.save()
return loan
def create_loan_with_security(applicant, loan_type, repayment_method, repayment_periods, loan_security_pledge,
posting_date=None, repayment_start_date=None):
loan = frappe.get_doc({
"doctype": "Loan",
"company": "_Test Company",
"applicant_type": "Customer",
"posting_date": posting_date or nowdate(),
"applicant": applicant,
"loan_type": loan_type,
"is_term_loan": 1,
"is_secured_loan": 1,
"repayment_method": repayment_method,
"repayment_periods": repayment_periods,
"repayment_start_date": repayment_start_date or nowdate(),
"mode_of_payment": frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'),
"loan_security_pledge": loan_security_pledge,
"payment_account": 'Payment Account - _TC',
"loan_account": 'Loan Account - _TC',
"interest_income_account": 'Interest Income Account - _TC',
"penalty_income_account": 'Penalty Income Account - _TC',
})
loan.save()
return loan
def create_demand_loan(applicant, loan_type, loan_security_pledge, posting_date=None):
loan = frappe.get_doc({
"doctype": "Loan",
"company": "_Test Company",
"applicant_type": "Customer",
"posting_date": posting_date or nowdate(),
"applicant": applicant,
"loan_type": loan_type,
"is_term_loan": 0,
"is_secured_loan": 1,
"mode_of_payment": frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'),
"loan_security_pledge": loan_security_pledge,
"payment_account": 'Payment Account - _TC',
"loan_account": 'Loan Account - _TC',
"interest_income_account": 'Interest Income Account - _TC',
"penalty_income_account": 'Penalty Income Account - _TC',
})
loan.save()
return loan

View File

@ -0,0 +1,127 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
{% include 'erpnext/loan_management/loan_common.js' %};
frappe.ui.form.on('Loan Application', {
setup: function(frm) {
frm.make_methods = {
'Loan': function() { frm.trigger('create_loan') },
'Loan Security Pledge': function() { frm.trigger('create_loan_security_pledge') },
}
},
refresh: function(frm) {
frm.trigger("toggle_fields");
frm.trigger("add_toolbar_buttons");
},
repayment_method: function(frm) {
frm.doc.repayment_amount = frm.doc.repayment_periods = ""
frm.trigger("toggle_fields")
frm.trigger("toggle_required")
},
toggle_fields: function(frm) {
frm.toggle_enable("repayment_amount", frm.doc.repayment_method=="Repay Fixed Amount per Period")
frm.toggle_enable("repayment_periods", frm.doc.repayment_method=="Repay Over Number of Periods")
},
toggle_required: function(frm){
frm.toggle_reqd("repayment_amount", cint(frm.doc.repayment_method=='Repay Fixed Amount per Period'))
frm.toggle_reqd("repayment_periods", cint(frm.doc.repayment_method=='Repay Over Number of Periods'))
},
add_toolbar_buttons: function(frm) {
if (frm.doc.status == "Approved") {
frappe.db.get_value("Loan Security Pledge", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => {
if (!r) {
frm.add_custom_button(__('Loan Security Pledge'), function() {
frm.trigger('create_loan_security_pledge')
},__('Create'))
}
});
frappe.db.get_value("Loan", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => {
if (!r) {
frm.add_custom_button(__('Loan'), function() {
frm.trigger('create_loan')
},__('Create'))
} else {
frm.set_df_property('status', 'read_only', 1);
}
});
}
},
create_loan: function(frm) {
if (frm.doc.status != "Approved") {
frappe.throw(__("Cannot create loan until application is approved"))
}
frappe.model.open_mapped_doc({
method: 'erpnext.loan_management.doctype.loan_application.loan_application.create_loan',
frm: frm
});
},
create_loan_security_pledge: function(frm) {
frappe.call({
method: "erpnext.loan_management.doctype.loan_application.loan_application.create_pledge",
args: {
loan_application: frm.doc.name
},
callback: function(r) {
frappe.set_route("Form", "Loan Security Pledge", r.message);
}
})
},
is_term_loan: function(frm) {
frm.set_df_property('repayment_method', 'hidden', 1 - frm.doc.is_term_loan);
frm.set_df_property('repayment_method', 'reqd', frm.doc.is_term_loan);
},
is_secured_loan: function(frm) {
frm.set_df_property('proposed_pledges', 'reqd', frm.doc.is_secured_loan);
},
calculate_amounts: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.qty) {
frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.loan_security_price);
frappe.model.set_value(cdt, cdn, 'post_haircut_amount', cint(row.amount - (row.amount * row.haircut/100)));
} else if (row.amount) {
frappe.model.set_value(cdt, cdn, 'qty', cint(row.amount / row.loan_security_price));
frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.loan_security_price);
frappe.model.set_value(cdt, cdn, 'post_haircut_amount', cint(row.amount - (row.amount * row.haircut/100)));
}
let maximum_amount = 0;
$.each(frm.doc.proposed_pledges || [], function(i, item){
maximum_amount += item.post_haircut_amount;
});
if (flt(maximum_amount)) {
frm.set_value('maximum_loan_amount', flt(maximum_amount));
}
}
});
frappe.ui.form.on("Proposed Pledge", {
loan_security: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
frappe.call({
method: "erpnext.loan_management.doctype.loan_security_price.loan_security_price.get_loan_security_price",
args: {
loan_security: row.loan_security
},
callback: function(r) {
frappe.model.set_value(cdt, cdn, 'loan_security_price', r.message);
frm.events.calculate_amounts(frm, cdt, cdn);
}
})
},
amount: function(frm, cdt, cdn) {
frm.events.calculate_amounts(frm, cdt, cdn);
},
qty: function(frm, cdt, cdn) {
frm.events.calculate_amounts(frm, cdt, cdn);
},
})

View File

@ -0,0 +1,279 @@
{
"actions": [],
"autoname": "ACC-LOAP-.YYYY.-.#####",
"creation": "2019-08-29 17:46:49.201740",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"applicant_type",
"applicant",
"applicant_name",
"column_break_2",
"company",
"posting_date",
"status",
"section_break_4",
"loan_type",
"is_term_loan",
"loan_amount",
"is_secured_loan",
"rate_of_interest",
"column_break_7",
"description",
"loan_security_details_section",
"proposed_pledges",
"maximum_loan_amount",
"repayment_info",
"repayment_method",
"total_payable_amount",
"column_break_11",
"repayment_periods",
"repayment_amount",
"total_payable_interest",
"amended_from"
],
"fields": [
{
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"reqd": 1
},
{
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"in_global_search": 1,
"in_standard_filter": 1,
"label": "Applicant",
"options": "applicant_type",
"reqd": 1
},
{
"depends_on": "applicant",
"fieldname": "applicant_name",
"fieldtype": "Data",
"in_global_search": 1,
"label": "Applicant Name",
"read_only": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"label": "Posting Date"
},
{
"allow_on_submit": 1,
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"no_copy": 1,
"options": "Open\nApproved\nRejected",
"permlevel": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break",
"label": "Loan Info"
},
{
"fieldname": "loan_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Loan Type",
"options": "Loan Type",
"reqd": 1
},
{
"bold": 1,
"fieldname": "loan_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Loan Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Reason"
},
{
"depends_on": "eval: doc.is_term_loan == 1",
"fieldname": "repayment_info",
"fieldtype": "Section Break",
"label": "Repayment Info"
},
{
"depends_on": "eval: doc.is_term_loan == 1",
"fetch_if_empty": 1,
"fieldname": "repayment_method",
"fieldtype": "Select",
"label": "Repayment Method",
"options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods"
},
{
"fetch_from": "loan_type.rate_of_interest",
"fieldname": "rate_of_interest",
"fieldtype": "Percent",
"label": "Rate of Interest",
"read_only": 1
},
{
"depends_on": "is_term_loan",
"fieldname": "total_payable_interest",
"fieldtype": "Currency",
"label": "Total Payable Interest",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"depends_on": "repayment_method",
"fieldname": "repayment_amount",
"fieldtype": "Currency",
"label": "Monthly Repayment Amount",
"options": "Company:company:default_currency"
},
{
"depends_on": "repayment_method",
"fieldname": "repayment_periods",
"fieldtype": "Int",
"label": "Repayment Period in Months"
},
{
"fieldname": "total_payable_amount",
"fieldtype": "Currency",
"label": "Total Payable Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Application",
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "is_secured_loan",
"fieldtype": "Check",
"label": "Is Secured Loan"
},
{
"depends_on": "eval:doc.is_secured_loan == 1",
"fieldname": "loan_security_details_section",
"fieldtype": "Section Break",
"label": "Loan Security Details"
},
{
"depends_on": "eval:doc.is_secured_loan == 1",
"fieldname": "proposed_pledges",
"fieldtype": "Table",
"label": "Proposed Pledges",
"options": "Proposed Pledge"
},
{
"fieldname": "maximum_loan_amount",
"fieldtype": "Currency",
"label": "Maximum Loan Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"default": "0",
"fetch_from": "loan_type.is_term_loan",
"fieldname": "is_term_loan",
"fieldtype": "Check",
"label": "Is Term Loan",
"read_only": 1
}
],
"is_submittable": 1,
"links": [],
"modified": "2020-03-01 10:21:44.413353",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Application",
"owner": "Administrator",
"permissions": [
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"share": 1,
"submit": 1,
"write": 1
},
{
"delete": 1,
"email": 1,
"export": 1,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"share": 1
}
],
"search_fields": "applicant_type, applicant, loan_type, loan_amount",
"sort_field": "modified",
"sort_order": "DESC",
"timeline_field": "applicant",
"title_field": "applicant",
"track_changes": 1
}

View File

@ -0,0 +1,209 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, math
from frappe import _
from frappe.utils import flt, rounded, cint
from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document
from erpnext.loan_management.doctype.loan.loan import (get_monthly_repayment_amount, validate_repayment_method,
get_total_loan_amount, get_sanctioned_amount_limit)
from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price
import json
from six import string_types
class LoanApplication(Document):
def validate(self):
validate_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount,
self.repayment_periods, self.is_term_loan)
self.validate_loan_type()
self.set_pledge_amount()
self.set_loan_amount()
self.validate_loan_amount()
self.get_repayment_details()
self.check_sanctioned_amount_limit()
def validate_loan_type(self):
company = frappe.get_value("Loan Type", self.loan_type, "company")
if company != self.company:
frappe.throw(_("Please select Loan Type for company {0}").format(frappe.bold(self.company)))
def validate_loan_amount(self):
if not self.loan_amount:
frappe.throw(_("Loan Amount is mandatory"))
maximum_loan_limit = frappe.db.get_value('Loan Type', self.loan_type, 'maximum_loan_amount')
if maximum_loan_limit and self.loan_amount > maximum_loan_limit:
frappe.throw(_("Loan Amount cannot exceed Maximum Loan Amount of {0}").format(maximum_loan_limit))
if self.maximum_loan_amount and self.loan_amount > self.maximum_loan_amount:
frappe.throw(_("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format(self.maximum_loan_amount))
def check_sanctioned_amount_limit(self):
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company)
if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit):
frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant)))
def set_pledge_amount(self):
for proposed_pledge in self.proposed_pledges:
if not proposed_pledge.qty and not proposed_pledge.amount:
frappe.throw(_("Qty or Amount is mandatroy for loan security"))
proposed_pledge.loan_security_price = get_loan_security_price(proposed_pledge.loan_security)
if not proposed_pledge.qty:
proposed_pledge.qty = cint(proposed_pledge.amount/proposed_pledge.loan_security_price)
proposed_pledge.amount = proposed_pledge.qty * proposed_pledge.loan_security_price
proposed_pledge.post_haircut_amount = cint(proposed_pledge.amount - (proposed_pledge.amount * proposed_pledge.haircut/100))
def get_repayment_details(self):
if self.is_term_loan:
if self.repayment_method == "Repay Over Number of Periods":
self.repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
if self.repayment_method == "Repay Fixed Amount per Period":
monthly_interest_rate = flt(self.rate_of_interest) / (12 *100)
if monthly_interest_rate:
min_repayment_amount = self.loan_amount*monthly_interest_rate
if self.repayment_amount - min_repayment_amount <= 0:
frappe.throw(_("Repayment Amount must be greater than " \
+ str(flt(min_repayment_amount, 2))))
self.repayment_periods = math.ceil((math.log(self.repayment_amount) -
math.log(self.repayment_amount - min_repayment_amount)) /(math.log(1 + monthly_interest_rate)))
else:
self.repayment_periods = self.loan_amount / self.repayment_amount
self.calculate_payable_amount()
else:
self.total_payable_amount = self.loan_amount
def calculate_payable_amount(self):
balance_amount = self.loan_amount
self.total_payable_amount = 0
self.total_payable_interest = 0
while(balance_amount > 0):
interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100))
balance_amount = rounded(balance_amount + interest_amount - self.repayment_amount)
self.total_payable_interest += interest_amount
self.total_payable_amount = self.loan_amount + self.total_payable_interest
def set_loan_amount(self):
if self.is_secured_loan and not self.proposed_pledges:
frappe.throw(_("Proposed Pledges are mandatory for secured Loans"))
if not self.loan_amount and self.is_secured_loan and self.proposed_pledges:
self.loan_amount = 0
for security in self.proposed_pledges:
self.loan_amount += security.post_haircut_amount
@frappe.whitelist()
def create_loan(source_name, target_doc=None, submit=0):
def update_accounts(source_doc, target_doc, source_parent):
account_details = frappe.get_all("Loan Type",
fields=["mode_of_payment", "payment_account","loan_account", "interest_income_account", "penalty_income_account"],
filters = {'name': source_doc.loan_type}
)[0]
loan_security_pledge = frappe.db.get_value("Loan Security Pledge", {"loan_application": source_name}, 'name')
target_doc.mode_of_payment = account_details.mode_of_payment
target_doc.payment_account = account_details.payment_account
target_doc.loan_account = account_details.loan_account
target_doc.interest_income_account = account_details.interest_income_account
target_doc.penalty_income_account = account_details.penalty_income_account
if loan_security_pledge:
target_doc.is_secured_loan = 1
target_doc.loan_security_pledge = loan_security_pledge
doclist = get_mapped_doc("Loan Application", source_name, {
"Loan Application": {
"doctype": "Loan",
"validation": {
"docstatus": ["=", 1]
},
"postprocess": update_accounts,
"field_no_map": [
"is_secured_loan"
]
}
}, target_doc)
if submit:
doclist.submit()
return doclist
@frappe.whitelist()
def create_pledge(loan_application, loan=None):
loan_application_doc = frappe.get_doc("Loan Application", loan_application)
lsp = frappe.new_doc("Loan Security Pledge")
lsp.applicant_type = loan_application_doc.applicant_type
lsp.applicant = loan_application_doc.applicant
lsp.loan_application = loan_application_doc.name
lsp.company = loan_application_doc.company
if loan:
lsp.loan = loan
for pledge in loan_application_doc.proposed_pledges:
lsp.append('securities', {
"loan_security": pledge.loan_security,
"qty": pledge.qty,
"loan_security_price": pledge.loan_security_price,
"haircut": pledge.haircut
})
lsp.save()
lsp.submit()
message = _("Loan Security Pledge Created : {0}").format(lsp.name)
frappe.msgprint(message)
return lsp.name
#This is a sandbox method to get the proposed pledges
@frappe.whitelist()
def get_proposed_pledge(securities):
if isinstance(securities, string_types):
securities = json.loads(securities)
proposed_pledges = {
'securities': []
}
maximum_loan_amount = 0
for security in securities:
security = frappe._dict(security)
if not security.qty and not security.amount:
frappe.throw(_("Qty or Amount is mandatroy for loan security"))
security.loan_security_price = get_loan_security_price(security.loan_security)
if not security.qty:
security.qty = cint(security.amount/security.loan_security_price)
security.amount = security.qty * security.loan_security_price
security.post_haircut_amount = security.amount - (security.amount * security.haircut/100)
maximum_loan_amount += security.post_haircut_amount
proposed_pledges['securities'].append(security)
proposed_pledges['maximum_loan_amount'] = maximum_loan_amount
return proposed_pledges

View File

@ -6,7 +6,7 @@ def get_data():
'fieldname': 'loan_application',
'transactions': [
{
'items': ['Loan']
'items': ['Loan', 'Loan Security Pledge']
},
],
}

View File

@ -1,39 +1,33 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from erpnext.hr.doctype.salary_structure.test_salary_structure import make_employee
from erpnext.loan_management.doctype.loan.test_loan import create_loan_type, create_loan_accounts
class TestLoanApplication(unittest.TestCase):
def setUp(self):
self.create_loan_type()
self.applicant = make_employee("kate_loan@loan.com")
create_loan_accounts()
create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
'Interest Income Account - _TC', 'Penalty Income Account - _TC', 'Repay Over Number of Periods', 18)
self.applicant = make_employee("kate_loan@loan.com", "_Test Company")
self.create_loan_application()
def create_loan_type(self):
if not frappe.db.get_value("Loan Type", "Home Loan"):
frappe.get_doc({
"doctype": "Loan Type",
"loan_name": "Home Loan",
"maximum_loan_amount": 500000,
"rate_of_interest": 9.2
}).insert()
def create_loan_application(self):
if not frappe.db.get_value("Loan Application", {"applicant":self.applicant}, "name"):
loan_application = frappe.new_doc("Loan Application")
loan_application.update({
"applicant": self.applicant,
"loan_type": "Home Loan",
"rate_of_interest": 9.2,
"loan_amount": 250000,
"repayment_method": "Repay Over Number of Periods",
"repayment_periods": 18
})
loan_application.insert()
loan_application = frappe.new_doc("Loan Application")
loan_application.update({
"applicant": self.applicant,
"loan_type": "Home Loan",
"rate_of_interest": 9.2,
"loan_amount": 250000,
"repayment_method": "Repay Over Number of Periods",
"repayment_periods": 18,
"company": "_Test Company"
})
loan_application.insert()
def test_loan_totals(self):

View File

@ -0,0 +1,17 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
{% include 'erpnext/loan_management/loan_common.js' %};
frappe.ui.form.on('Loan Disbursement', {
refresh: function(frm) {
frm.set_query('against_loan', function() {
return {
'filters': {
'docstatus': 1,
'status': 'Sanctioned'
}
}
})
}
});

View File

@ -0,0 +1,165 @@
{
"autoname": "LM-DIS-.#####",
"creation": "2019-09-07 12:44:49.125452",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"against_loan",
"disbursement_date",
"posting_date",
"column_break_4",
"company",
"applicant_type",
"applicant",
"section_break_7",
"pending_amount_for_disbursal",
"disbursed_amount",
"accounting_dimensions_section",
"cost_center",
"section_break_13",
"customer_details_section",
"bank_account",
"amended_from"
],
"fields": [
{
"fieldname": "against_loan",
"fieldtype": "Link",
"label": "Against Loan ",
"options": "Loan"
},
{
"fieldname": "disbursement_date",
"fieldtype": "Date",
"label": "Disbursement Date"
},
{
"fieldname": "disbursed_amount",
"fieldtype": "Currency",
"label": "Disbursed Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Disbursement",
"print_hide": 1,
"read_only": 1
},
{
"fetch_from": "against_loan.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"read_only": 1
},
{
"fetch_from": "against_loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"label": "Applicant",
"options": "applicant_type",
"read_only": 1
},
{
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "posting_date",
"fieldtype": "Date",
"hidden": 1,
"label": "Posting Date",
"read_only": 1
},
{
"fieldname": "pending_amount_for_disbursal",
"fieldtype": "Currency",
"label": "Pending Amount For Disbursal",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break"
},
{
"fieldname": "section_break_13",
"fieldtype": "Section Break"
},
{
"fieldname": "customer_details_section",
"fieldtype": "Section Break",
"label": "Customer Details"
},
{
"fetch_from": "against_loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"read_only": 1
},
{
"fieldname": "bank_account",
"fieldtype": "Link",
"label": "Bank Account",
"options": "Bank Account"
}
],
"is_submittable": 1,
"modified": "2019-10-24 12:32:32.230881",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Disbursement",
"owner": "Administrator",
"permissions": [
{
"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": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, erpnext
from frappe.model.document import Document
from frappe.utils import nowdate, getdate, add_days, flt
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual
class LoanDisbursement(AccountsController):
def validate(self):
self.set_missing_values()
self.set_pending_amount_for_disbursal()
def before_submit(self):
self.set_status_and_amounts()
def on_submit(self):
self.make_gl_entries()
def on_cancel(self):
self.make_gl_entries(cancel=1)
def set_missing_values(self):
if not self.disbursement_date:
self.disbursement_date = nowdate()
if not self.cost_center:
self.cost_center = erpnext.get_default_cost_center(self.company)
if not self.posting_date:
self.posting_date = self.disbursement_date or nowdate()
if not self.bank_account and self.applicant_type == "Customer":
self.bank_account = frappe.db.get_value("Customer", self.applicant, "default_bank_account")
def set_pending_amount_for_disbursal(self):
loan_amount, disbursed_amount = frappe.db.get_value('Loan',
{'name': self.against_loan}, ['loan_amount', 'disbursed_amount'])
self.pending_amount_for_disbursal = loan_amount - disbursed_amount
def set_status_and_amounts(self):
loan_details = frappe.get_all("Loan",
fields = ["loan_amount", "disbursed_amount", "total_principal_paid", "status", "is_term_loan"],
filters= { "name": self.against_loan }
)[0]
if loan_details.status == "Disbursed" and not loan_details.is_term_loan:
process_loan_interest_accrual(posting_date=add_days(self.disbursement_date, -1),
loan=self.against_loan)
disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount
if flt(disbursed_amount) - flt(loan_details.total_principal_paid) > flt(loan_details.loan_amount):
frappe.throw(_("Disbursed Amount cannot be greater than loan amount"))
if flt(disbursed_amount) > flt(loan_details.loan_amount):
total_principal_paid = loan_details.total_principal_paid - (disbursed_amount - loan_details.loan_amount)
frappe.db.set_value("Loan", self.against_loan, "total_principal_paid", total_principal_paid)
if flt(loan_details.loan_amount) == flt(disbursed_amount):
frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed")
else:
frappe.db.set_value("Loan", self.against_loan, "status", "Partially Disbursed")
frappe.db.set_value("Loan", self.against_loan, {
"disbursement_date": self.disbursement_date,
"disbursed_amount": disbursed_amount
})
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
loan_details = frappe.get_doc("Loan", self.against_loan)
gle_map.append(
self.get_gl_dict({
"account": loan_details.loan_account,
"against": loan_details.applicant,
"debit": self.disbursed_amount,
"debit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": "Against Loan:" + self.against_loan,
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": self.disbursement_date
})
)
gle_map.append(
self.get_gl_dict({
"account": loan_details.payment_account,
"against": loan_details.applicant,
"credit": self.disbursed_amount,
"credit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": "Against Loan:" + self.against_loan,
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": self.disbursement_date
})
)
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj)

View File

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import (nowdate, add_days, get_datetime, get_first_day, get_last_day, date_diff, flt, add_to_date)
from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_pledge, create_repayment_entry,
make_loan_disbursement_entry, create_loan_accounts, create_loan_security_type, create_loan_security, create_demand_loan, create_loan_security_price)
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import (make_accrual_interest_entry_for_term_loans, days_in_year)
from erpnext.selling.doctype.customer.test_customer import get_customer_dict
class TestLoanDisbursement(unittest.TestCase):
def setUp(self):
create_loan_accounts()
create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
'Interest Income Account - _TC', 'Penalty Income Account - _TC')
create_loan_security_type()
create_loan_security()
create_loan_security_price("Test Security 1", 500, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24)))
create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24)))
if not frappe.db.exists("Customer", "_Test Loan Customer"):
frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True)
self.applicant = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name')
def test_loan_topup(self):
pledges = []
pledges.append({
"loan_security": "Test Security 1",
"qty": 4000.00,
"haircut": 50,
"loan_security_price": 500.00
})
loan_security_pledge = create_loan_security_pledge(self.applicant, pledges)
loan = create_demand_loan(self.applicant, "Demand Loan", loan_security_pledge.name,
posting_date=get_first_day(nowdate()))
loan.submit()
first_date = get_first_day(nowdate())
last_date = get_last_day(nowdate())
no_of_days = date_diff(last_date, first_date) + 1
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime().year) * 100)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual(posting_date=add_days(last_date, 1))
# Paid 511095.89 amount includes 5,00,000 principal amount and 11095.89 interest amount
repayment_entry = create_repayment_entry(loan.name, self.applicant, add_days(get_last_day(nowdate()), 5),
"Regular Payment", 611095.89)
repayment_entry.submit()
loan.reload()
make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 16))
total_principal_paid = loan.total_principal_paid
loan.reload()
# Loan Topup will result in decreasing the Total Principal Paid
self.assertEqual(flt(loan.total_principal_paid, 2), flt(total_principal_paid - 500000, 2))

View File

@ -0,0 +1,10 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
{% include 'erpnext/loan_management/loan_common.js' %};
frappe.ui.form.on('Loan Interest Accrual', {
// refresh: function(frm) {
// }
});

View File

@ -0,0 +1,182 @@
{
"actions": [],
"autoname": "LM-LIA-.#####",
"creation": "2019-09-09 22:34:36.346812",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan",
"applicant_type",
"applicant",
"interest_income_account",
"loan_account",
"column_break_4",
"company",
"posting_date",
"is_term_loan",
"is_paid",
"section_break_7",
"pending_principal_amount",
"payable_principal_amount",
"column_break_14",
"interest_amount",
"section_break_15",
"process_loan_interest_accrual",
"amended_from"
],
"fields": [
{
"fieldname": "loan",
"fieldtype": "Link",
"label": "Loan",
"options": "Loan"
},
{
"fieldname": "posting_date",
"fieldtype": "Date",
"label": "Posting Date"
},
{
"fieldname": "pending_principal_amount",
"fieldtype": "Currency",
"label": "Pending Principal Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "interest_amount",
"fieldtype": "Currency",
"label": "Interest Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Interest Accrual",
"print_hide": 1,
"read_only": 1
},
{
"fetch_from": "loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer"
},
{
"fetch_from": "loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"label": "Applicant",
"options": "applicant_type"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fetch_from": "loan.interest_income_account",
"fieldname": "interest_income_account",
"fieldtype": "Data",
"label": "Interest Income Account"
},
{
"fetch_from": "loan.loan_account",
"fieldname": "loan_account",
"fieldtype": "Data",
"label": "Loan Account"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"label": "Amounts"
},
{
"fetch_from": "loan.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
},
{
"default": "0",
"fieldname": "is_paid",
"fieldtype": "Check",
"label": "Is Paid",
"read_only": 1
},
{
"default": "0",
"fetch_from": "loan.is_term_loan",
"fieldname": "is_term_loan",
"fieldtype": "Check",
"label": "Is Term Loan",
"read_only": 1
},
{
"depends_on": "is_term_loan",
"fieldname": "payable_principal_amount",
"fieldtype": "Currency",
"label": "Payable Principal Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "section_break_15",
"fieldtype": "Section Break"
},
{
"fieldname": "process_loan_interest_accrual",
"fieldtype": "Link",
"label": "Process Loan Interest Accrual",
"options": "Process Loan Interest Accrual"
},
{
"fieldname": "column_break_14",
"fieldtype": "Column Break"
}
],
"in_create": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-02-07 01:22:06.924125",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Interest Accrual",
"owner": "Administrator",
"permissions": [
{
"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": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,180 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, erpnext
from frappe import _
from frappe.model.document import Document
from frappe.utils import (nowdate, getdate, now_datetime, get_datetime, flt, date_diff, get_last_day, cint,
get_first_day, get_datetime, add_days)
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.general_ledger import make_gl_entries
class LoanInterestAccrual(AccountsController):
def validate(self):
if not self.loan:
frappe.throw(_("Loan is mandatory"))
if not self.posting_date:
self.posting_date = nowdate()
if not self.interest_amount:
frappe.throw(_("Interest Amount is mandatory"))
def on_submit(self):
self.make_gl_entries()
def on_cancel(self):
self.make_gl_entries(cancel=1)
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
gle_map.append(
self.get_gl_dict({
"account": self.loan_account,
"party_type": self.applicant_type,
"party": self.applicant,
"against": self.interest_income_account,
"debit": self.interest_amount,
"debit_in_account_currency": self.interest_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _("Against Loan:") + self.loan,
"cost_center": erpnext.get_default_cost_center(self.company),
"posting_date": self.posting_date
})
)
gle_map.append(
self.get_gl_dict({
"account": self.interest_income_account,
"party_type": self.applicant_type,
"party": self.applicant,
"against": self.loan_account,
"credit": self.interest_amount,
"credit_in_account_currency": self.interest_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _("Against Loan:") + self.loan,
"cost_center": erpnext.get_default_cost_center(self.company),
"posting_date": self.posting_date
})
)
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj)
# For Eg: If Loan disbursement date is '01-09-2019' and disbursed amount is 1000000 and
# rate of interest is 13.5 then first loan interest accural will be on '01-10-2019'
# which means interest will be accrued for 30 days which should be equal to 11095.89
def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest):
no_of_days = get_no_of_days_for_interest_accural(loan, posting_date)
if no_of_days <= 0:
return
pending_principal_amount = loan.total_payment - loan.total_interest_payable \
- loan.total_amount_paid
interest_per_day = (pending_principal_amount * loan.rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100)
payable_interest = interest_per_day * no_of_days
make_loan_interest_accrual_entry(loan.name, loan.applicant_type, loan.applicant,loan.interest_income_account,
loan.loan_account, pending_principal_amount, payable_interest, process_loan_interest = process_loan_interest,
posting_date=posting_date)
def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None):
query_filters = {
"status": "Disbursed",
"docstatus": 1
}
if loan_type:
query_filters.update({
"loan_type": loan_type
})
if not open_loans:
open_loans = frappe.get_all("Loan",
fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", "is_term_loan",
"disbursement_date", "applicant_type", "applicant", "rate_of_interest", "total_interest_payable", "repayment_start_date"],
filters=query_filters)
for loan in open_loans:
calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest)
def make_accrual_interest_entry_for_term_loans(posting_date=None):
curr_date = posting_date or add_days(nowdate(), 1)
term_loans = frappe.db.sql("""SELECT l.name, l.total_payment, l.total_amount_paid, l.loan_account,
l.interest_income_account, l.is_term_loan, l.disbursement_date, l.applicant_type, l.applicant,
l.rate_of_interest, l.total_interest_payable, l.repayment_start_date, rs.name as payment_entry,
rs.payment_date, rs.principal_amount, rs.interest_amount, rs.is_accrued , rs.balance_loan_amount
FROM `tabLoan` l, `tabRepayment Schedule` rs
WHERE rs.parent = l.name
AND l.docstatus=1
AND l.is_term_loan =1
AND rs.payment_date <= %s
AND rs.is_accrued=0
AND l.status = 'Disbursed'""", (curr_date), as_dict=1)
accrued_entries = []
for loan in term_loans:
accrued_entries.append(loan.payment_entry)
make_loan_interest_accrual_entry(loan.name, loan.applicant_type, loan.applicant,loan.interest_income_account,
loan.loan_account, loan.principal_amount + loan.balance_loan_amount, loan.interest_amount,
payable_principal = loan.principal_amount , posting_date=posting_date)
frappe.db.sql("""UPDATE `tabRepayment Schedule`
SET is_accrued = 1 where name in (%s)""" #nosec
% ", ".join(['%s']*len(accrued_entries)), tuple(accrued_entries))
def make_loan_interest_accrual_entry(loan, applicant_type, applicant, interest_income_account, loan_account,
pending_principal_amount, interest_amount, payable_principal=None, process_loan_interest=None, posting_date=None):
loan_interest_accrual = frappe.new_doc("Loan Interest Accrual")
loan_interest_accrual.loan = loan
loan_interest_accrual.applicant_type = applicant_type
loan_interest_accrual.applicant = applicant
loan_interest_accrual.interest_income_account = interest_income_account
loan_interest_accrual.loan_account = loan_account
loan_interest_accrual.pending_principal_amount = flt(pending_principal_amount, 2)
loan_interest_accrual.interest_amount = flt(interest_amount, 2)
loan_interest_accrual.posting_date = posting_date or nowdate()
loan_interest_accrual.process_loan_interest_accrual = process_loan_interest
if payable_principal:
loan_interest_accrual.payable_principal_amount = payable_principal
loan_interest_accrual.save()
loan_interest_accrual.submit()
def get_no_of_days_for_interest_accural(loan, posting_date):
last_interest_accrual_date = get_last_accural_date_in_current_month(loan)
no_of_days = date_diff(posting_date or nowdate(), last_interest_accrual_date) + 1
return no_of_days
def get_last_accural_date_in_current_month(loan):
last_posting_date = frappe.db.sql(""" SELECT MAX(posting_date) from `tabLoan Interest Accrual`
WHERE loan = %s""", (loan.name))
if last_posting_date[0][0]:
return last_posting_date[0][0]
else:
return loan.disbursement_date
def days_in_year(year):
days = 365
if (year % 4 == 0) and (year % 100 != 0) or (year % 400 == 0):
days = 366
return days

View File

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import (nowdate, add_days, get_datetime, get_first_day, get_last_day, date_diff, flt, add_to_date)
from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_pledge, create_loan_security_price,
make_loan_disbursement_entry, create_loan_accounts, create_loan_security_type, create_loan_security, create_demand_loan)
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import (make_accrual_interest_entry_for_term_loans, days_in_year)
from erpnext.selling.doctype.customer.test_customer import get_customer_dict
class TestLoanInterestAccrual(unittest.TestCase):
def setUp(self):
create_loan_accounts()
create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
'Interest Income Account - _TC', 'Penalty Income Account - _TC')
create_loan_security_type()
create_loan_security()
create_loan_security_price("Test Security 1", 500, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24)))
if not frappe.db.exists("Customer", "_Test Loan Customer"):
frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True)
self.applicant = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name')
def test_loan_interest_accural(self):
pledges = []
pledges.append({
"loan_security": "Test Security 1",
"qty": 4000.00,
"haircut": 50,
"loan_security_price": 500.00
})
loan_security_pledge = create_loan_security_pledge(self.applicant, pledges)
loan = create_demand_loan(self.applicant, "Demand Loan", loan_security_pledge.name,
posting_date=get_first_day(nowdate()))
loan.submit()
first_date = '2019-10-01'
last_date = '2019-10-30'
no_of_days = date_diff(last_date, first_date) + 1
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual(posting_date=last_date)
loan_interest_accural = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name})
self.assertEquals(flt(loan_interest_accural.interest_amount, 2), flt(accrued_interest_amount, 2))

Some files were not shown because too many files have changed in this diff Show More