Merge branch 'develop' into yet-another-desk
This commit is contained in:
commit
1fd1852c6a
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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 = []
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
}
|
@ -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]
|
||||
|
@ -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",
|
||||
|
@ -7,4 +7,4 @@ from __future__ import unicode_literals
|
||||
from frappe.model.document import Document
|
||||
|
||||
class DiscountedInvoice(Document):
|
||||
pass
|
||||
pass
|
@ -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"]:
|
||||
|
@ -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:
|
||||
|
@ -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",
|
||||
|
@ -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()
|
@ -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', [])
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
},
|
||||
{
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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"))
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -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",
|
||||
|
107
erpnext/config/loan_management.py
Normal file
107
erpnext/config/loan_management.py
Normal 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",
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -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"))
|
||||
|
@ -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"],
|
||||
|
@ -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])
|
||||
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
},
|
||||
|
@ -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]] };
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -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,
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
@ -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]] };
|
||||
};
|
||||
}
|
||||
});
|
@ -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"));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
@ -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."))
|
||||
|
||||
|
@ -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."));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -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 = []
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
|
@ -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});
|
||||
}
|
||||
});
|
||||
}
|
@ -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()
|
@ -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']
|
||||
}
|
||||
]
|
||||
}
|
@ -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()
|
||||
]);
|
||||
});
|
@ -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})
|
@ -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");
|
||||
}
|
||||
}
|
||||
});
|
@ -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
|
||||
}
|
@ -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
|
@ -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()
|
||||
]);
|
||||
});
|
@ -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) {
|
||||
}
|
||||
});
|
@ -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
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'loan_type',
|
||||
'transactions': [
|
||||
{
|
||||
'items': ['Loan Application']
|
||||
},
|
||||
],
|
||||
}
|
@ -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()
|
||||
]);
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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'}
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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": [
|
||||
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
190
erpnext/loan_management/doctype/loan/loan.js
Normal file
190
erpnext/loan_management/doctype/loan/loan.js
Normal 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")
|
||||
}
|
||||
});
|
@ -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
|
254
erpnext/loan_management/doctype/loan/loan.py
Normal file
254
erpnext/loan_management/doctype/loan/loan.py
Normal 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()
|
||||
|
||||
|
||||
|
19
erpnext/loan_management/doctype/loan/loan_dashboard.py
Normal file
19
erpnext/loan_management/doctype/loan/loan_dashboard.py
Normal 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']
|
||||
}
|
||||
]
|
||||
}
|
559
erpnext/loan_management/doctype/loan/test_loan.py
Normal file
559
erpnext/loan_management/doctype/loan/test_loan.py
Normal 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
|
@ -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);
|
||||
},
|
||||
})
|
@ -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
|
||||
}
|
@ -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
|
@ -6,7 +6,7 @@ def get_data():
|
||||
'fieldname': 'loan_application',
|
||||
'transactions': [
|
||||
{
|
||||
'items': ['Loan']
|
||||
'items': ['Loan', 'Loan Security Pledge']
|
||||
},
|
||||
],
|
||||
}
|
@ -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):
|
@ -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'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
@ -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
|
||||
}
|
@ -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)
|
@ -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))
|
@ -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) {
|
||||
|
||||
// }
|
||||
});
|
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user