mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-07 15:12:51 +00:00
feat: Loan management and accounting (#19035)
* fix: Create Loan Management module * fix: Move loan doctype for hr module to loan management * fix: Add loan dashboard * fix: Move loan application form hr module to loan management * fix: Move Loan Type from hr to loan management * fix: Move salary slip loan from hr to loan management * fix: Create loan security pledge doctype * fix: Create loan security type doctype * fix: Create Loan security doctype * fix: Add customer in loan_common.js * fix: Import patch for loan * fix: Add security pledging in loan doctype * fix: Add loan application pledge doctype * fix: Add logic for security pledgeing in loan * fix: Add logic for security pledging in loan application * fix: Rename Loan security doctype to Loan Security Pledge * fix: Add doctype for loan security * fix: Fixes in loan and loan application * fix: Add doctypes for loan repayment * fix: Fixes in loan and loan application * fix: Move loan repayment report from HR module to loan management * fix: Create doctype for loan security price * fix: Create doctype for loan security shortfall * fix: Make fields in Loan shortfall readonly * fix: Make loan type submittable * fix: Add daily job for loan security revaluation in hooks.py * fix: Add loan management module * fix: Doctype for loan disbursement Entry * fix: GL entry fix for loan disbursement * fix: Add company currency options in Loan related doctypes * fix: Changes in Loan Doctypes * fix: Allow miltiple loan securities in Loan Security Pledge * fix: Add proposed pledges in Loan Application * fix: Add test cases for loan * fix: Interest Accrual Entry for loans * fix: Remove loan from journal entry * fix: Update triggers in loan_common.js * fix: Accrual Entries for loan Interest * fix: Proposed Pledges for loan application * fix: Update items in loan management module * fix: Allow multiple disbursements against a loan * fix: Add loan security code in loan security master * fix: Allow multiple securities in a single pledge * fix: Spelling and label fixes * fix: Replace date in loan security price with datetime * fix: Add dashboard to loan master * fix: Move Repayment Schedule from HR to Loan Management * fix: Add back jobs for loan accrual entries * fix: Multiple fixes in loan * fix: Multiple fixes in loan application * fix: Loan Reapayment for term loans * fix: Interest Accural Entries for term loans * fix: Changes in Loan Doctypes * fix: Add test case for term loan repayment * fix: Add custom button to update loan security price and trigger shortfall * fix: Usability fixes in Loan management * fix: Multiple usablity and doctype fixes * fix: Muliple bug and usability fixes in loan * fix: Test case fixes for loan repayment from Salary * fix: Codacy fixes * fix: Test Case fixes * fix: Minor fix in validate_repayment_method * fix: Test case and codacy fixes * fix: Provide consistent naming series to loan doctypes * fix: Loan Application fix * fix: Loan Application Dashboard * fix: Add doctypes to process loan interest accural and loan security price * fix: Provision to make loan topup * fix: Pledge and unpledge statuses and doctype for loan securit unpledge * fix: Multiple fixes in Loan Cycle * fix: Add missing dashboards to loan doctypes * fix: Add Loan Manager role to loan doctypes * fix: Loan Process fixes * fix: Loan Security Unpledge fixes * fix: UX, List and dashboards fixes * fix: Minor fixes * fix: Minor fixes * fix: Status on additional loan security * fix: Codacy fixes * fix: Multiple fixes in loan * fix: Rename Process Loan Security Price to Process Loan Security Shortfall * fix: Loan Repayment and Closure report * fix: Loan Security Status report and minor fixes * fix: Multiple UX fixes * fix: Test Case fixes and UX fixes * fix: Currency symbol fixes in Salary Slip Loan * fix: Make loan account details read only * fix: Changes in loan security price updation * fix: Sanctioned Loan Amount doctype * fix: Updates in process loan interest accrual * fix: Pass loan doc instead of loan name * fix: Fixes in process loan interest accrual * fix: Add missing semicolon * fix: Test case * fix: Test case * fix: Sandbox method to get proposed pledges * fix: Sandbox method * fix: Nonetype fix in loan sanction limit * fix: GL entry fixes * fix: Update maximum loan amount on loan security pledging * fix: Round off loan amounts * fix: Loan Security unpledging * fix: Interest amount for loan closure * fix: Due date for loan repayments * fix: API fixes for loan disbursement method * fix: Disable quick entry for sanctioned loan amount * fix: Add misssing translations for validations and codecleanup * fix: Translation fixes and code cleanup * fix: Test Cases * fix: Loan Interest Accrual test case
This commit is contained in:
0
erpnext/loan_management/__init__.py
Normal file
0
erpnext/loan_management/__init__.py
Normal file
0
erpnext/loan_management/doctype/__init__.py
Normal file
0
erpnext/loan_management/doctype/__init__.py
Normal file
0
erpnext/loan_management/doctype/loan/__init__.py
Normal file
0
erpnext/loan_management/doctype/loan/__init__.py
Normal file
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")
|
||||
}
|
||||
});
|
||||
382
erpnext/loan_management/doctype/loan/loan.json
Normal file
382
erpnext/loan_management/doctype/loan/loan.json
Normal file
@@ -0,0 +1,382 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"autoname": "ACC-LOAN-.YYYY.-.#####",
|
||||
"creation": "2019-08-29 17:29:18.176786",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"applicant_type",
|
||||
"applicant",
|
||||
"applicant_name",
|
||||
"loan_application",
|
||||
"column_break_3",
|
||||
"company",
|
||||
"posting_date",
|
||||
"status",
|
||||
"repay_from_salary",
|
||||
"section_break_8",
|
||||
"loan_type",
|
||||
"loan_amount",
|
||||
"is_secured_loan",
|
||||
"rate_of_interest",
|
||||
"disbursement_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",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "applicant_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Applicant Type",
|
||||
"options": "Employee\nMember\nCustomer",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "applicant",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Applicant",
|
||||
"options": "applicant_type",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "applicant_name",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
"label": "Applicant Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_application",
|
||||
"fieldtype": "Link",
|
||||
"label": "Loan Application",
|
||||
"options": "Loan Application"
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Loan Type",
|
||||
"options": "Loan Type",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "Today",
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Posting Date",
|
||||
"no_copy": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"remember_last_selected_value": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Sanctioned",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "Sanctioned\nPartially Disbursed\nDisbursed\nLoan Closure Requested\nClosed",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.applicant_type==\"Employee\"",
|
||||
"fieldname": "repay_from_salary",
|
||||
"fieldtype": "Check",
|
||||
"label": "Repay from Salary"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Loan Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Loan Amount",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan_type.rate_of_interest",
|
||||
"fieldname": "rate_of_interest",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Rate of Interest (%) / Year",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.status==\"Disbursed\"",
|
||||
"fieldname": "disbursement_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Disbursement Date"
|
||||
},
|
||||
{
|
||||
"depends_on": "is_term_loan",
|
||||
"fieldname": "repayment_start_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Repayment Start Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "is_term_loan",
|
||||
"fieldname": "repayment_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Repayment Method",
|
||||
"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",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "account_info",
|
||||
"fieldtype": "Section Break",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_9",
|
||||
"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",
|
||||
"no_copy": 1,
|
||||
"options": "Repayment Schedule"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_17",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Totals"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "total_payment",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Payable Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_19",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "is_term_loan",
|
||||
"fieldname": "total_interest_payable",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Interest Payable",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "total_amount_paid",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Amount Paid",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"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": "2020-02-07 01:31:25.172173",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Employee"
|
||||
}
|
||||
],
|
||||
"search_fields": "posting_date",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 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,278 @@
|
||||
{
|
||||
"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_from": "loan_type.repayment_method",
|
||||
"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,
|
||||
"modified": "2019-10-24 10:32:03.740558",
|
||||
"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,206 @@
|
||||
# -*- 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_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
|
||||
|
||||
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
|
||||
@@ -0,0 +1,12 @@
|
||||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'loan_application',
|
||||
'transactions': [
|
||||
{
|
||||
'items': ['Loan', 'Loan Security Pledge']
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
# -*- 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 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):
|
||||
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_application(self):
|
||||
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):
|
||||
loan_application = frappe.get_doc("Loan Application", {"applicant":self.applicant})
|
||||
|
||||
self.assertEqual(loan_application.total_payable_interest, 18599)
|
||||
self.assertEqual(loan_application.total_payable_amount, 268599)
|
||||
self.assertEqual(loan_application.repayment_amount, 14923)
|
||||
|
||||
loan_application.repayment_periods = 24
|
||||
loan_application.save()
|
||||
loan_application.reload()
|
||||
|
||||
self.assertEqual(loan_application.total_payable_interest, 24657)
|
||||
self.assertEqual(loan_application.total_payable_amount, 274657)
|
||||
self.assertEqual(loan_application.repayment_amount, 11445)
|
||||
@@ -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))
|
||||
@@ -0,0 +1,64 @@
|
||||
// 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 Repayment', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
onload: function(frm) {
|
||||
frm.set_query('against_loan', function() {
|
||||
return {
|
||||
'filters': {
|
||||
'docstatus': 1
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
if (frm.doc.against_loan && frm.doc.posting_date && frm.doc.docstatus == 0) {
|
||||
frm.trigger('calculate_repayment_amounts');
|
||||
}
|
||||
},
|
||||
|
||||
posting_date : function(frm) {
|
||||
frm.trigger('calculate_repayment_amounts');
|
||||
},
|
||||
|
||||
against_loan: function(frm) {
|
||||
if (frm.doc.posting_date) {
|
||||
frm.trigger('calculate_repayment_amounts');
|
||||
}
|
||||
},
|
||||
|
||||
payment_type: function(frm) {
|
||||
if (frm.doc.posting_date) {
|
||||
frm.trigger('calculate_repayment_amounts');
|
||||
}
|
||||
},
|
||||
|
||||
calculate_repayment_amounts: function(frm) {
|
||||
frappe.call({
|
||||
method: 'erpnext.loan_management.doctype.loan_repayment.loan_repayment.calculate_amounts',
|
||||
args: {
|
||||
'against_loan': frm.doc.against_loan,
|
||||
'posting_date': frm.doc.posting_date,
|
||||
'payment_type': frm.doc.payment_type
|
||||
},
|
||||
callback: function(r) {
|
||||
let amounts = r.message;
|
||||
frm.set_value('amount_paid', 0.0);
|
||||
frm.set_df_property('amount_paid', 'read_only', frm.doc.payment_type == "Loan Closure" ? 1:0);
|
||||
|
||||
frm.set_value('pending_principal_amount', amounts['pending_principal_amount']);
|
||||
if (frm.doc.is_term_loan || frm.doc.payment_type == "Loan Closure") {
|
||||
frm.set_value('payable_principal_amount', amounts['payable_principal_amount']);
|
||||
frm.set_value('amount_paid', amounts['payable_amount']);
|
||||
}
|
||||
frm.set_value('interest_payable', amounts['interest_amount']);
|
||||
frm.set_value('penalty_amount', amounts['penalty_amount']);
|
||||
frm.set_value('payable_amount', amounts['payable_amount']);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,267 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "LM-REP-.####",
|
||||
"creation": "2019-09-03 14:44:39.977266",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"against_loan",
|
||||
"applicant_type",
|
||||
"applicant",
|
||||
"loan_type",
|
||||
"payment_type",
|
||||
"column_break_3",
|
||||
"company",
|
||||
"posting_date",
|
||||
"is_term_loan",
|
||||
"payment_details_section",
|
||||
"due_date",
|
||||
"pending_principal_amount",
|
||||
"interest_payable",
|
||||
"payable_amount",
|
||||
"column_break_9",
|
||||
"payable_principal_amount",
|
||||
"penalty_amount",
|
||||
"amount_paid",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"references_section",
|
||||
"reference_number",
|
||||
"column_break_21",
|
||||
"reference_date",
|
||||
"paid_accrual_entries",
|
||||
"partial_paid_entry",
|
||||
"principal_amount_paid",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "against_loan",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Against Loan",
|
||||
"options": "Loan",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Posting Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Payment Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "penalty_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Penalty Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "interest_payable",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Interest Payable",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "against_loan.applicant",
|
||||
"fieldname": "applicant",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Applicant",
|
||||
"options": "applicant_type",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "against_loan.loan_type",
|
||||
"fieldname": "loan_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Loan Type",
|
||||
"options": "Loan Type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_9",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "Regular Payment",
|
||||
"fieldname": "payment_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Payment Type",
|
||||
"options": "\nRegular Payment\nLoan Closure",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "payable_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Payable Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "amount_paid",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount Paid",
|
||||
"options": "Company:company:default_currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Loan Repayment",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fetch_from": "against_loan.company",
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "pending_principal_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Pending Principal Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "paid_accrual_entries",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 1,
|
||||
"label": "Paid Accrual Entries",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "against_loan.is_term_loan",
|
||||
"fieldname": "is_term_loan",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Term Loan",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.payment_type==\"Loan Closure\" || doc.is_term_loan",
|
||||
"fieldname": "payable_principal_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Payable Principal Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "references_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "References"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference Number"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Reference Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_21",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "partial_paid_entry",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 1,
|
||||
"label": "Partial Paid Entry",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0.0",
|
||||
"fieldname": "principal_amount_paid",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Principal Amount Paid",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "against_loan.applicant_type",
|
||||
"fieldname": "applicant_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Applicant Type",
|
||||
"options": "Employee\nMember\nCustomer",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "due_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Due Date",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-02-24 07:35:47.168123",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Repayment",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Loan Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
315
erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
Normal file
315
erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
Normal file
@@ -0,0 +1,315 @@
|
||||
# -*- 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
|
||||
import json
|
||||
from frappe import _
|
||||
from frappe.utils import flt, getdate
|
||||
from six import iteritems
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import date_diff, add_days, getdate, add_months, get_first_day, get_datetime
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import update_shortfall_status
|
||||
|
||||
class LoanRepayment(AccountsController):
|
||||
|
||||
def validate(self):
|
||||
amounts = calculate_amounts(self.against_loan, self.posting_date, self.payment_type)
|
||||
self.set_missing_values(amounts)
|
||||
|
||||
def before_submit(self):
|
||||
self.mark_as_paid()
|
||||
|
||||
def on_submit(self):
|
||||
self.make_gl_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
self.mark_as_unpaid()
|
||||
self.make_gl_entries(cancel=1)
|
||||
|
||||
def set_missing_values(self, amounts):
|
||||
if not self.posting_date:
|
||||
self.posting_date = get_datetime()
|
||||
|
||||
if not self.cost_center:
|
||||
self.cost_center = erpnext.get_default_cost_center(self.company)
|
||||
|
||||
if not self.interest_payable:
|
||||
self.interest_payable = amounts['interest_amount']
|
||||
|
||||
if not self.penalty_amount:
|
||||
self.penalty_amount = amounts['penalty_amount']
|
||||
|
||||
if not self.pending_principal_amount:
|
||||
self.pending_principal_amount = amounts['pending_principal_amount']
|
||||
|
||||
if not self.payable_principal_amount and self.is_term_loan:
|
||||
self.payable_principal_amount = amounts['payable_principal_amount']
|
||||
|
||||
if not self.payable_amount:
|
||||
self.payable_amount = amounts['payable_amount']
|
||||
|
||||
if amounts.get('paid_accrual_entries'):
|
||||
self.paid_accrual_entries = frappe.as_json(amounts.get('paid_accrual_entries'))
|
||||
|
||||
if amounts.get('due_date'):
|
||||
self.due_date = amounts.get('due_date')
|
||||
|
||||
def mark_as_paid(self):
|
||||
paid_entries = []
|
||||
paid_amount = self.amount_paid
|
||||
interest_paid = paid_amount
|
||||
|
||||
if not paid_amount:
|
||||
frappe.throw(_("Amount paid cannot be zero"))
|
||||
|
||||
if self.amount_paid < self.penalty_amount:
|
||||
msg = _("Paid amount cannot be less than {0}").format(self.penalty_amount)
|
||||
frappe.throw(msg)
|
||||
|
||||
if self.payment_type == "Loan Closure" and flt(self.amount_paid, 2) < flt(self.payable_amount, 2):
|
||||
msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount)
|
||||
frappe.throw(msg)
|
||||
|
||||
loan = frappe.get_doc("Loan", self.against_loan)
|
||||
|
||||
if self.paid_accrual_entries:
|
||||
paid_accrual_entries = json.loads(self.paid_accrual_entries)
|
||||
|
||||
if paid_amount - self.penalty_amount > 0 and self.paid_accrual_entries:
|
||||
|
||||
interest_paid = paid_amount - self.penalty_amount
|
||||
|
||||
for lia, interest_amount in iteritems(paid_accrual_entries):
|
||||
if interest_amount <= interest_paid:
|
||||
paid_entries.append(lia)
|
||||
interest_paid -= interest_amount
|
||||
elif interest_paid:
|
||||
self.partial_paid_entry = frappe.as_json({"name": lia, "interest_amount": interest_amount})
|
||||
frappe.db.set_value("Loan Interest Accrual", lia, "interest_amount",
|
||||
interest_amount - interest_paid)
|
||||
interest_paid = 0
|
||||
|
||||
if paid_entries:
|
||||
self.paid_accrual_entries = frappe.as_json(paid_entries)
|
||||
else:
|
||||
self.paid_accrual_entries = ""
|
||||
|
||||
if interest_paid:
|
||||
self.principal_amount_paid = interest_paid
|
||||
|
||||
if paid_entries:
|
||||
frappe.db.sql("""UPDATE `tabLoan Interest Accrual`
|
||||
SET is_paid = 1 where name in (%s)""" #nosec
|
||||
% ", ".join(['%s']*len(paid_entries)), tuple(paid_entries))
|
||||
|
||||
if flt(loan.total_principal_paid + self.principal_amount_paid, 2) >= flt(loan.total_payment, 2):
|
||||
frappe.db.set_value("Loan", self.against_loan, "status", "Loan Closure Requested")
|
||||
|
||||
frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s
|
||||
WHERE name = %s """, (loan.total_amount_paid + self.amount_paid,
|
||||
loan.total_principal_paid + self.principal_amount_paid, self.against_loan))
|
||||
|
||||
update_shortfall_status(self.against_loan, self.principal_amount_paid)
|
||||
|
||||
def mark_as_unpaid(self):
|
||||
|
||||
loan = frappe.get_doc("Loan", self.against_loan)
|
||||
|
||||
if self.paid_accrual_entries:
|
||||
paid_accrual_entries = json.loads(self.paid_accrual_entries)
|
||||
|
||||
if self.paid_accrual_entries:
|
||||
frappe.db.sql("""UPDATE `tabLoan Interest Accrual`
|
||||
SET is_paid = 0 where name in (%s)""" #nosec
|
||||
% ", ".join(['%s']*len(paid_accrual_entries)), tuple(paid_accrual_entries))
|
||||
|
||||
if self.partial_paid_entry:
|
||||
partial_paid_entry = json.loads(self.partial_paid_entry)
|
||||
frappe.db.set_value("Loan Interest Accrual", partial_paid_entry["name"], "interest_amount",
|
||||
partial_paid_entry["interest_amount"])
|
||||
|
||||
frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s
|
||||
WHERE name = %s """, (loan.total_amount_paid - self.amount_paid,
|
||||
loan.total_principal_paid - self.principal_amount_paid, self.against_loan))
|
||||
|
||||
if loan.status == "Loan Closure Requested":
|
||||
frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed")
|
||||
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
gle_map = []
|
||||
loan_details = frappe.get_doc("Loan", self.against_loan)
|
||||
|
||||
if self.penalty_amount:
|
||||
gle_map.append(
|
||||
self.get_gl_dict({
|
||||
"account": loan_details.loan_account,
|
||||
"against": loan_details.payment_account,
|
||||
"debit": self.penalty_amount,
|
||||
"debit_in_account_currency": self.penalty_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": getdate(self.posting_date)
|
||||
})
|
||||
)
|
||||
|
||||
gle_map.append(
|
||||
self.get_gl_dict({
|
||||
"account": loan_details.penalty_income_account,
|
||||
"against": loan_details.payment_account,
|
||||
"credit": self.penalty_amount,
|
||||
"credit_in_account_currency": self.penalty_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": getdate(self.posting_date)
|
||||
})
|
||||
)
|
||||
|
||||
gle_map.append(
|
||||
self.get_gl_dict({
|
||||
"account": loan_details.payment_account,
|
||||
"against": loan_details.loan_account + ", " + loan_details.interest_income_account
|
||||
+ ", " + loan_details.penalty_income_account,
|
||||
"debit": self.amount_paid,
|
||||
"debit_in_account_currency": self.amount_paid ,
|
||||
"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": getdate(self.posting_date)
|
||||
})
|
||||
)
|
||||
|
||||
gle_map.append(
|
||||
self.get_gl_dict({
|
||||
"account": loan_details.loan_account,
|
||||
"party_type": loan_details.applicant_type,
|
||||
"party": loan_details.applicant,
|
||||
"against": loan_details.payment_account,
|
||||
"credit": self.amount_paid,
|
||||
"credit_in_account_currency": self.amount_paid,
|
||||
"against_voucher_type": "Loan",
|
||||
"against_voucher": self.against_loan,
|
||||
"remarks": _("Against Loan:") + self.against_loan,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(self.posting_date)
|
||||
})
|
||||
)
|
||||
|
||||
if gle_map:
|
||||
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj)
|
||||
|
||||
def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
|
||||
payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None):
|
||||
|
||||
lr = frappe.get_doc({
|
||||
"doctype": "Loan Repayment",
|
||||
"against_loan": loan,
|
||||
"payment_type": payment_type,
|
||||
"company": company,
|
||||
"posting_date": posting_date,
|
||||
"applicant": applicant,
|
||||
"penalty_amount": penalty_amount,
|
||||
"interets_payable": interest_payable,
|
||||
"payable_principal_amount": payable_principal_amount,
|
||||
"amount_paid": amount_paid,
|
||||
"loan_type": loan_type
|
||||
}).insert()
|
||||
|
||||
return lr
|
||||
|
||||
def get_accrued_interest_entries(against_loan):
|
||||
accrued_interest_entries = frappe.get_all("Loan Interest Accrual",
|
||||
fields=["name", "interest_amount", "posting_date", "payable_principal_amount"],
|
||||
filters = {
|
||||
"loan": against_loan,
|
||||
"is_paid": 0
|
||||
}, order_by="posting_date")
|
||||
|
||||
return accrued_interest_entries
|
||||
|
||||
# This function returns the amounts that are payable at the time of loan repayment based on posting date
|
||||
# So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable
|
||||
|
||||
def get_amounts(amounts, against_loan, posting_date, payment_type):
|
||||
|
||||
against_loan_doc = frappe.get_doc("Loan", against_loan)
|
||||
loan_type_details = frappe.get_doc("Loan Type", against_loan_doc.loan_type)
|
||||
accrued_interest_entries = get_accrued_interest_entries(against_loan_doc.name)
|
||||
|
||||
pending_accrual_entries = {}
|
||||
|
||||
total_pending_interest = 0
|
||||
penalty_amount = 0
|
||||
payable_principal_amount = 0
|
||||
final_due_date = ''
|
||||
|
||||
for entry in accrued_interest_entries:
|
||||
# Loan repayment due date is one day after the loan interest is accrued
|
||||
# no of late days are calculated based on loan repayment posting date
|
||||
# and if no_of_late days are positive then penalty is levied
|
||||
|
||||
due_date = add_days(entry.posting_date, 1)
|
||||
no_of_late_days = date_diff(posting_date,
|
||||
add_days(due_date, loan_type_details.grace_period_in_days)) + 1
|
||||
|
||||
if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary):
|
||||
penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)/365
|
||||
|
||||
total_pending_interest += entry.interest_amount
|
||||
payable_principal_amount += entry.payable_principal_amount
|
||||
|
||||
pending_accrual_entries.setdefault(entry.name, entry.interest_amount)
|
||||
final_due_date = due_date
|
||||
|
||||
pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable
|
||||
|
||||
if payment_type == "Loan Closure" and not payable_principal_amount:
|
||||
pending_days = date_diff(posting_date, entry.posting_date) + 1
|
||||
payable_principal_amount = pending_principal_amount
|
||||
per_day_interest = (payable_principal_amount * (loan_type_details.rate_of_interest / 100))/365
|
||||
total_pending_interest += (pending_days * per_day_interest)
|
||||
|
||||
amounts["pending_principal_amount"] = pending_principal_amount
|
||||
amounts["payable_principal_amount"] = payable_principal_amount
|
||||
amounts["interest_amount"] = total_pending_interest
|
||||
amounts["penalty_amount"] = penalty_amount
|
||||
amounts["payable_amount"] = payable_principal_amount + total_pending_interest + penalty_amount
|
||||
amounts["paid_accrual_entries"] = pending_accrual_entries
|
||||
|
||||
if final_due_date:
|
||||
amounts["due_date"] = final_due_date
|
||||
|
||||
return amounts
|
||||
|
||||
@frappe.whitelist()
|
||||
def calculate_amounts(against_loan, posting_date, payment_type):
|
||||
|
||||
amounts = {
|
||||
'penalty_amount': 0.0,
|
||||
'interest_amount': 0.0,
|
||||
'pending_principal_amount': 0.0,
|
||||
'payable_principal_amount': 0.0,
|
||||
'payable_amount': 0.0,
|
||||
'due_date': ''
|
||||
}
|
||||
|
||||
amounts = get_amounts(amounts, against_loan, posting_date, payment_type)
|
||||
|
||||
return amounts
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestLoanRepayment(unittest.TestCase):
|
||||
pass
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Loan Security', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"autoname": "field:loan_security_name",
|
||||
"creation": "2019-09-02 15:07:08.885593",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"loan_security_type",
|
||||
"loan_security_code",
|
||||
"loan_security_name",
|
||||
"unit_of_measure",
|
||||
"column_break_3",
|
||||
"haircut",
|
||||
"disabled"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "loan_security_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Loan Security Name",
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan_security_type.haircut",
|
||||
"fieldname": "haircut",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Haircut %"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_security_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Loan Security Type",
|
||||
"options": "Loan Security Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_security_code",
|
||||
"fieldtype": "Data",
|
||||
"label": "Loan Security Code",
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan_security_type.unit_of_measure",
|
||||
"fieldname": "unit_of_measure",
|
||||
"fieldtype": "Link",
|
||||
"label": "Unit Of Measure",
|
||||
"options": "UOM"
|
||||
}
|
||||
],
|
||||
"modified": "2019-11-16 11:36:37.901656",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Security",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Loan Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"search_fields": "loan_security_code",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- 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
|
||||
from frappe.model.document import Document
|
||||
|
||||
class LoanSecurity(Document):
|
||||
pass
|
||||
@@ -0,0 +1,15 @@
|
||||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'loan_security',
|
||||
'transactions': [
|
||||
{
|
||||
'items': ['Loan Application', 'Loan Security Price']
|
||||
},
|
||||
{
|
||||
'items': ['Loan Security Pledge', 'Loan Security Unpledge']
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestLoanSecurity(unittest.TestCase):
|
||||
pass
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Loan Security Pledge', {
|
||||
calculate_amounts: function(frm, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
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 amount = 0;
|
||||
let maximum_amount = 0;
|
||||
$.each(frm.doc.securities || [], function(i, item){
|
||||
amount += item.amount;
|
||||
maximum_amount += item.post_haircut_amount;
|
||||
});
|
||||
|
||||
frm.set_value('total_security_value', amount);
|
||||
frm.set_value('maximum_loan_value', maximum_amount);
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.on("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);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
qty: function(frm, cdt, cdn) {
|
||||
frm.events.calculate_amounts(frm, cdt, cdn);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"autoname": "LS-.{applicant}.-.#####",
|
||||
"creation": "2019-08-29 18:48:51.371674",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"loan_details_section",
|
||||
"loan_application",
|
||||
"loan",
|
||||
"applicant_type",
|
||||
"applicant",
|
||||
"column_break_3",
|
||||
"company",
|
||||
"pledge_time",
|
||||
"status",
|
||||
"loan_security_details_section",
|
||||
"securities",
|
||||
"section_break_10",
|
||||
"total_security_value",
|
||||
"column_break_11",
|
||||
"maximum_loan_value",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Loan Security Pledge",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan_application.applicant",
|
||||
"fieldname": "applicant",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Applicant",
|
||||
"options": "applicant_type",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_security_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Loan Security Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "loan",
|
||||
"fieldtype": "Link",
|
||||
"label": "Loan",
|
||||
"options": "Loan",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_application",
|
||||
"fieldtype": "Link",
|
||||
"label": "Loan Application",
|
||||
"options": "Loan Application",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_security_value",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Security Value",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "maximum_loan_value",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Maximum Loan Value",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Loan Details"
|
||||
},
|
||||
{
|
||||
"default": "Requested",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "Requested\nUnpledged\nPledged\nPartially Pledged",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "pledge_time",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Pledge Time",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "securities",
|
||||
"fieldtype": "Table",
|
||||
"label": "Securities",
|
||||
"options": "Pledge",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_10",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Totals"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan.applicant_type",
|
||||
"fieldname": "applicant_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Applicant Type",
|
||||
"options": "Employee\nMember\nCustomer",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"modified": "2019-10-10 13:22:53.297519",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Security Pledge",
|
||||
"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,
|
||||
"search_fields": "applicant",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
# -*- 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
|
||||
from frappe import _
|
||||
from frappe.utils import now_datetime, cint
|
||||
from frappe.model.document import Document
|
||||
from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import update_shortfall_status
|
||||
from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price
|
||||
|
||||
class LoanSecurityPledge(Document):
|
||||
def validate(self):
|
||||
self.set_pledge_amount()
|
||||
|
||||
def on_submit(self):
|
||||
if self.loan:
|
||||
self.db_set("status", "Pledged")
|
||||
self.db_set("pledge_time", now_datetime())
|
||||
update_shortfall_status(self.loan, self.total_security_value)
|
||||
update_loan(self.loan, self.maximum_loan_value)
|
||||
|
||||
def set_pledge_amount(self):
|
||||
total_security_value = 0
|
||||
maximum_loan_value = 0
|
||||
|
||||
for pledge in self.securities:
|
||||
|
||||
if not pledge.qty and not pledge.amount:
|
||||
frappe.throw(_("Qty or Amount is mandatroy for loan security"))
|
||||
|
||||
pledge.loan_security_price = get_loan_security_price(pledge.loan_security)
|
||||
|
||||
if not pledge.qty:
|
||||
pledge.qty = cint(pledge.amount/pledge.loan_security_price)
|
||||
|
||||
pledge.amount = pledge.qty * pledge.loan_security_price
|
||||
pledge.post_haircut_amount = cint(pledge.amount - (pledge.amount * pledge.haircut/100))
|
||||
|
||||
total_security_value += pledge.amount
|
||||
maximum_loan_value += pledge.post_haircut_amount
|
||||
|
||||
self.total_security_value = total_security_value
|
||||
self.maximum_loan_value = maximum_loan_value
|
||||
|
||||
def update_loan(loan, maximum_value_against_pledge):
|
||||
maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_value'])
|
||||
|
||||
frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_value=%s, is_secured_loan=1
|
||||
WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan))
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
// render
|
||||
frappe.listview_settings['Loan Security Pledge'] = {
|
||||
add_fields: ["status"],
|
||||
get_indicator: function(doc) {
|
||||
var status_color = {
|
||||
"Unpledged": "orange",
|
||||
"Pledged": "green",
|
||||
"Partially Pledged": "green"
|
||||
};
|
||||
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestLoanSecurityPledge(unittest.TestCase):
|
||||
pass
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Loan Security Price', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"autoname": "LM-LSP-.####",
|
||||
"creation": "2019-09-03 18:20:31.382887",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"loan_security",
|
||||
"loan_security_type",
|
||||
"column_break_2",
|
||||
"uom",
|
||||
"section_break_4",
|
||||
"loan_security_price",
|
||||
"section_break_6",
|
||||
"valid_from",
|
||||
"column_break_8",
|
||||
"valid_upto"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "loan_security",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Loan Security",
|
||||
"options": "Loan Security",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan_security.unit_of_measure",
|
||||
"fieldname": "uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "UOM",
|
||||
"options": "UOM",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_4",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_security_price",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Loan Security Price",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "valid_from",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Valid From",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_8",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "valid_upto",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Valid Upto",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan_security.loan_security_type",
|
||||
"fieldname": "loan_security_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Loan Security Type",
|
||||
"options": "Loan Security Type",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"modified": "2019-10-26 09:46:46.069667",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Security Price",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Loan Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
# -*- 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
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import getdate, now_datetime, add_to_date, get_datetime, get_timestamp, get_datetime_str
|
||||
from six import iteritems
|
||||
|
||||
class LoanSecurityPrice(Document):
|
||||
def validate(self):
|
||||
self.validate_dates()
|
||||
|
||||
def validate_dates(self):
|
||||
|
||||
if self.valid_from > self.valid_upto:
|
||||
frappe.throw(_("Valid From Time must be lesser than Valid Upto Time."))
|
||||
|
||||
existing_loan_security = frappe.db.sql(""" SELECT name from `tabLoan Security Price`
|
||||
WHERE loan_security = %s AND name != %s AND (valid_from BETWEEN %s and %s OR valid_upto BETWEEN %s and %s) """,
|
||||
(self.loan_security, self.name, self.valid_from, self.valid_upto, self.valid_from, self.valid_upto))
|
||||
|
||||
if existing_loan_security:
|
||||
frappe.throw(_("Loan Security Price overlapping with {0}").format(existing_loan_security[0][0]))
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_loan_security_price(loan_security, valid_time=None):
|
||||
if not valid_time:
|
||||
valid_time = get_datetime()
|
||||
|
||||
loan_security_price = frappe.db.get_value("Loan Security Price", {
|
||||
'loan_security': loan_security,
|
||||
'valid_from': ("<=",valid_time),
|
||||
'valid_upto': (">=", valid_time)
|
||||
}, 'loan_security_price')
|
||||
|
||||
if not loan_security_price:
|
||||
frappe.throw(_("No valid <b>Loan Security Price</b> found for {0}").format(frappe.bold(loan_security)))
|
||||
else:
|
||||
return loan_security_price
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestLoanSecurityPrice(unittest.TestCase):
|
||||
pass
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Loan Security Shortfall', {
|
||||
refresh: function(frm) {
|
||||
frm.add_custom_button(__("Add Loan Security"), function() {
|
||||
frm.trigger('shortfall_action');
|
||||
});
|
||||
},
|
||||
|
||||
shortfall_action: function(frm) {
|
||||
frappe.call({
|
||||
method: "erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall.add_security",
|
||||
args: {
|
||||
'loan': frm.doc.loan
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
let doc = frappe.model.sync(r.message)[0];
|
||||
frappe.set_route("Form", doc.doctype, doc.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"autoname": "LM-LSS-.#####",
|
||||
"creation": "2019-09-06 11:33:34.709540",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"loan",
|
||||
"status",
|
||||
"column_break_3",
|
||||
"shortfall_time",
|
||||
"section_break_3",
|
||||
"loan_amount",
|
||||
"shortfall_amount",
|
||||
"column_break_8",
|
||||
"security_value",
|
||||
"section_break_8",
|
||||
"process_loan_security_shortfall"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "loan",
|
||||
"fieldtype": "Link",
|
||||
"label": "Loan ",
|
||||
"options": "Loan",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Loan Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "security_value",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Security Value ",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "shortfall_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Shortfall Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_3",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"description": "America/New_York",
|
||||
"fieldname": "shortfall_time",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Shortfall Time",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "Pending",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "\nPending\nCompleted",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "process_loan_security_shortfall",
|
||||
"fieldtype": "Link",
|
||||
"label": "Process Loan Security Shortfall",
|
||||
"options": "Process Loan Security Shortfall",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_8",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"modified": "2019-10-24 06:24:26.128997",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Security Shortfall",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Loan Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
# -*- 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
|
||||
from frappe.utils import get_datetime
|
||||
from frappe.model.document import Document
|
||||
from six import iteritems
|
||||
|
||||
class LoanSecurityShortfall(Document):
|
||||
pass
|
||||
|
||||
def update_shortfall_status(loan, security_value):
|
||||
loan_security_shortfall = frappe.db.get_value("Loan Security Shortfall",
|
||||
{"loan": loan, "status": "Pending"}, ['name', 'shortfall_amount'], as_dict=1)
|
||||
|
||||
if not loan_security_shortfall:
|
||||
return
|
||||
|
||||
if security_value >= loan_security_shortfall.shortfall_amount:
|
||||
frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, "status", "Completed")
|
||||
else:
|
||||
frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name,
|
||||
"shortfall_amount", loan_security_shortfall.shortfall_amount - security_value)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_security(loan):
|
||||
loan_details = frappe.db.get_value("Loan", loan, ['applicant', 'company', 'applicant_type'], as_dict=1)
|
||||
|
||||
loan_security_pledge = frappe.new_doc("Loan Security Pledge")
|
||||
loan_security_pledge.loan = loan
|
||||
loan_security_pledge.company = loan_details.company
|
||||
loan_security_pledge.applicant_type = loan_details.applicant_type
|
||||
loan_security_pledge.applicant = loan_details.applicant
|
||||
|
||||
return loan_security_pledge.as_dict()
|
||||
|
||||
def check_for_ltv_shortfall(process_loan_security_shortfall=None):
|
||||
|
||||
update_time = get_datetime()
|
||||
|
||||
if not process_loan_security_shortfall:
|
||||
process = frappe.new_doc("Process Loan Security Shortfall")
|
||||
process.update_time = update_time
|
||||
process.submit()
|
||||
|
||||
process_loan_security_shortfall = process.name
|
||||
|
||||
loan_security_price_map = frappe._dict(frappe.get_all("Loan Security Price",
|
||||
fields=["loan_security", "loan_security_price"],
|
||||
filters = {
|
||||
"valid_from": ("<=", update_time),
|
||||
"valid_upto": (">=", update_time)
|
||||
}, as_list=1))
|
||||
|
||||
loans = frappe.db.sql(""" SELECT l.name, l.loan_amount, l.total_principal_paid, lp.loan_security, lp.haircut, lp.qty
|
||||
FROM `tabLoan` l, `tabPledge` lp , `tabLoan Security Pledge`p WHERE lp.parent = p.name and p.loan = l.name and l.docstatus = 1
|
||||
and l.is_secured_loan and l.status = 'Disbursed' and p.status in ('Pledged', 'Partially Unpledged')""", as_dict=1)
|
||||
|
||||
loan_security_map = {}
|
||||
|
||||
for loan in loans:
|
||||
loan_security_map.setdefault(loan.name, {
|
||||
"loan_amount": loan.loan_amount - loan.total_principal_paid,
|
||||
"security_value": 0.0
|
||||
})
|
||||
|
||||
current_loan_security_amount = loan_security_price_map.get(loan.loan_security, 0) * loan.qty
|
||||
|
||||
loan_security_map[loan.name]['security_value'] += current_loan_security_amount - (current_loan_security_amount * loan.haircut/100)
|
||||
|
||||
for loan, value in iteritems(loan_security_map):
|
||||
if value["security_value"] < value["loan_amount"]:
|
||||
create_loan_security_shortfall(loan, value, process_loan_security_shortfall)
|
||||
|
||||
def create_loan_security_shortfall(loan, value, process_loan_security_shortfall):
|
||||
|
||||
existing_shortfall = frappe.db.get_value("Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name")
|
||||
|
||||
if existing_shortfall:
|
||||
ltv_shortfall = frappe.get_doc("Loan Security Shortfall", existing_shortfall)
|
||||
else:
|
||||
ltv_shortfall = frappe.new_doc("Loan Security Shortfall")
|
||||
ltv_shortfall.loan = loan
|
||||
|
||||
ltv_shortfall.shortfall_time = get_datetime()
|
||||
ltv_shortfall.loan_amount = value["loan_amount"]
|
||||
ltv_shortfall.security_value = value["security_value"]
|
||||
ltv_shortfall.shortfall_amount = value["loan_amount"] - value["security_value"]
|
||||
ltv_shortfall.process_loan_security_shortfall = process_loan_security_shortfall
|
||||
ltv_shortfall.save()
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestLoanSecurityShortfall(unittest.TestCase):
|
||||
pass
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Loan Security Type', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// },
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"autoname": "field:loan_security_type",
|
||||
"creation": "2019-08-29 18:46:07.322056",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"loan_security_type",
|
||||
"unit_of_measure",
|
||||
"haircut",
|
||||
"disabled"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_security_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Loan Security Type",
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "haircut",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Haircut %"
|
||||
},
|
||||
{
|
||||
"fieldname": "unit_of_measure",
|
||||
"fieldtype": "Link",
|
||||
"label": "Unit Of Measure",
|
||||
"options": "UOM"
|
||||
}
|
||||
],
|
||||
"modified": "2019-10-10 03:05:37.912866",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Security Type",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Loan Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- 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
|
||||
from frappe.model.document import Document
|
||||
|
||||
class LoanSecurityType(Document):
|
||||
pass
|
||||
@@ -0,0 +1,15 @@
|
||||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'loan_security_type',
|
||||
'transactions': [
|
||||
{
|
||||
'items': ['Loan Security', 'Loan Security Price']
|
||||
},
|
||||
{
|
||||
'items': ['Loan Security Pledge', 'Loan Security Unpledge']
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestLoanSecurityType(unittest.TestCase):
|
||||
pass
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Loan Security Unpledge', {
|
||||
refresh: function(frm) {
|
||||
|
||||
frm.set_query("against_pledge", "securities", () => {
|
||||
return {
|
||||
filters : [["status", "in", ["Pledged", "Partially Pledged"]]]
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,159 @@
|
||||
{
|
||||
"autoname": "LSU-.{applicant}.-.#####",
|
||||
"creation": "2019-09-21 13:23:16.117028",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"loan_details_section",
|
||||
"loan",
|
||||
"applicant_type",
|
||||
"applicant",
|
||||
"column_break_3",
|
||||
"company",
|
||||
"unpledge_time",
|
||||
"status",
|
||||
"loan_security_details_section",
|
||||
"securities",
|
||||
"unpledge_type",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "loan_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Loan Details"
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan_application.applicant",
|
||||
"fieldname": "applicant",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Applicant",
|
||||
"options": "applicant_type",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "loan",
|
||||
"fieldtype": "Link",
|
||||
"label": "Loan",
|
||||
"options": "Loan",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "Requested",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "Requested\nApproved",
|
||||
"permlevel": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "unpledge_time",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Unpledge Time",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_security_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Loan Security Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Loan Security Unpledge",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "securities",
|
||||
"fieldtype": "Table",
|
||||
"label": "Securities",
|
||||
"options": "Unpledge",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "unpledge_type",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Unpledge Type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan.applicant_type",
|
||||
"fieldname": "applicant_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Applicant Type",
|
||||
"options": "Employee\nMember\nCustomer",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"modified": "2019-10-28 07:41:47.084882",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Security Unpledge",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"permlevel": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Loan Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"search_fields": "applicant",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
# -*- 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
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_datetime, flt
|
||||
import json
|
||||
from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price
|
||||
|
||||
class LoanSecurityUnpledge(Document):
|
||||
def validate(self):
|
||||
self.validate_pledges()
|
||||
|
||||
def validate_pledges(self):
|
||||
pledge_details = self.get_pledge_details()
|
||||
|
||||
loan = frappe.get_doc("Loan", self.loan)
|
||||
|
||||
pledge_qty_map = {}
|
||||
remaining_qty = 0
|
||||
unpledge_value = 0
|
||||
|
||||
for pledge in pledge_details:
|
||||
pledge_qty_map.setdefault((pledge.parent, pledge.loan_security), pledge.qty)
|
||||
|
||||
for security in self.securities:
|
||||
pledged_qty = pledge_qty_map.get((security.against_pledge, security.loan_security), 0)
|
||||
if not pledged_qty:
|
||||
frappe.throw(_("Zero qty of {0} pledged against loan {0}").format(frappe.bold(security.loan_security),
|
||||
frappe.bold(self.loan)))
|
||||
|
||||
unpledge_qty = pledged_qty - security.qty
|
||||
security_price = security.qty * get_loan_security_price(security.loan_security)
|
||||
|
||||
if unpledge_qty < 0:
|
||||
frappe.throw(_("Cannot unpledge more than {0} qty of {0}").format(frappe.bold(pledged_qty),
|
||||
frappe.bold(security.loan_security)))
|
||||
|
||||
remaining_qty += unpledge_qty
|
||||
unpledge_value += security_price - flt(security_price * security.haircut/100)
|
||||
|
||||
if unpledge_value > loan.total_principal_paid:
|
||||
frappe.throw(_("Cannot Unpledge, loan security value is greater than the repaid amount"))
|
||||
|
||||
if not remaining_qty:
|
||||
self.db_set('unpledge_type', 'Unpledged')
|
||||
else:
|
||||
self.db_set('unpledge_type', 'Partially Pledged')
|
||||
|
||||
|
||||
def get_pledge_details(self):
|
||||
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
|
||||
AND lsp.status = "Pledged"
|
||||
""",(self.loan), as_dict=1)
|
||||
|
||||
return pledge_details
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if self.status == "Approved":
|
||||
frappe.db.sql("""
|
||||
UPDATE
|
||||
`tabPledge` p, `tabUnpledge` u, `tabLoan Security Pledge` lsp,
|
||||
`tabLoan Security Unpledge` lsu SET p.qty = (p.qty - u.qty)
|
||||
WHERE
|
||||
lsp.loan = %s
|
||||
AND lsu.status = 'Requested'
|
||||
AND u.parent = %s
|
||||
AND p.parent = u.against_pledge
|
||||
AND p.loan_security = u.loan_security""",(self.loan, self.name))
|
||||
|
||||
frappe.db.sql("""UPDATE `tabLoan Security Pledge`
|
||||
SET status = %s WHERE loan = %s""", (self.unpledge_type, self.loan))
|
||||
|
||||
if self.unpledge_type == 'Unpledged':
|
||||
frappe.db.set_value("Loan", self.loan, 'status', 'Closed')
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
// render
|
||||
frappe.listview_settings['Loan Security Unpledge'] = {
|
||||
add_fields: ["status"],
|
||||
get_indicator: function(doc) {
|
||||
var status_color = {
|
||||
"Requested": "orange",
|
||||
"Approved": "green",
|
||||
};
|
||||
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestLoanSecurityUnpledge(unittest.TestCase):
|
||||
pass
|
||||
30
erpnext/loan_management/doctype/loan_type/loan_type.js
Normal file
30
erpnext/loan_management/doctype/loan_type/loan_type.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Loan Type', {
|
||||
onload: function(frm) {
|
||||
$.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
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
170
erpnext/loan_management/doctype/loan_type/loan_type.json
Normal file
170
erpnext/loan_management/doctype/loan_type/loan_type.json
Normal file
@@ -0,0 +1,170 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "field:loan_name",
|
||||
"creation": "2019-08-29 18:08:38.159726",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"loan_name",
|
||||
"maximum_loan_amount",
|
||||
"rate_of_interest",
|
||||
"penalty_interest_rate",
|
||||
"grace_period_in_days",
|
||||
"column_break_2",
|
||||
"company",
|
||||
"is_term_loan",
|
||||
"disabled",
|
||||
"description",
|
||||
"account_details_section",
|
||||
"mode_of_payment",
|
||||
"payment_account",
|
||||
"loan_account",
|
||||
"column_break_12",
|
||||
"interest_income_account",
|
||||
"penalty_income_account",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "loan_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Loan Name",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "maximum_loan_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Maximum Loan Amount",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "rate_of_interest",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Rate of Interest (%) Yearly",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Account Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "mode_of_payment",
|
||||
"fieldtype": "Link",
|
||||
"label": "Mode of Payment",
|
||||
"options": "Mode of Payment",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Loan Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "interest_income_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Interest Income Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "penalty_income_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Penalty Income Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_term_loan",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Term Loan"
|
||||
},
|
||||
{
|
||||
"description": "Penalty Interest Rate is levied on the pending interest amount on a daily basis in case of delayed repayment ",
|
||||
"fieldname": "penalty_interest_rate",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Penalty Interest Rate (%) Per Day"
|
||||
},
|
||||
{
|
||||
"fieldname": "grace_period_in_days",
|
||||
"fieldtype": "Int",
|
||||
"label": "Grace Period in Days"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Loan Type",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-02-03 05:03:00.334813",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Type",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Loan Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Employee"
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
21
erpnext/loan_management/doctype/loan_type/loan_type.py
Normal file
21
erpnext/loan_management/doctype/loan_type/loan_type.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- 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
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
class LoanType(Document):
|
||||
def validate(self):
|
||||
self.validate_accounts()
|
||||
|
||||
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 and company != self.company:
|
||||
frappe.throw(_("Account {0} does not belong to company {1}").format(frappe.bold(self.get(fieldname)),
|
||||
frappe.bold(self.company)))
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'loan_type',
|
||||
'transactions': [
|
||||
{
|
||||
'items': ['Loan Repayment', 'Loan']
|
||||
},
|
||||
{
|
||||
'items': ['Loan Application']
|
||||
}
|
||||
]
|
||||
}
|
||||
10
erpnext/loan_management/doctype/loan_type/test_loan_type.py
Normal file
10
erpnext/loan_management/doctype/loan_type/test_loan_type.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestLoanType(unittest.TestCase):
|
||||
pass
|
||||
0
erpnext/loan_management/doctype/pledge/__init__.py
Normal file
0
erpnext/loan_management/doctype/pledge/__init__.py
Normal file
8
erpnext/loan_management/doctype/pledge/pledge.js
Normal file
8
erpnext/loan_management/doctype/pledge/pledge.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Pledge', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
99
erpnext/loan_management/doctype/pledge/pledge.json
Normal file
99
erpnext/loan_management/doctype/pledge/pledge.json
Normal file
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"creation": "2019-09-09 17:06:16.756573",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"loan_security",
|
||||
"loan_security_type",
|
||||
"loan_security_code",
|
||||
"uom",
|
||||
"column_break_5",
|
||||
"qty",
|
||||
"haircut",
|
||||
"loan_security_price",
|
||||
"amount",
|
||||
"post_haircut_amount"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "loan_security",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Loan Security",
|
||||
"options": "Loan Security",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan_security.loan_security_type",
|
||||
"fieldname": "loan_security_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Loan Security Type",
|
||||
"options": "Loan Security Type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan_security.loan_security_code",
|
||||
"fieldname": "loan_security_code",
|
||||
"fieldtype": "Data",
|
||||
"label": "Loan Security Code"
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan_security.unit_of_measure",
|
||||
"fieldname": "uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "UOM",
|
||||
"options": "UOM"
|
||||
},
|
||||
{
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Quantity"
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_security_price",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Loan Security Price",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan_security.haircut",
|
||||
"fieldname": "haircut",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Haircut %",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "post_haircut_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Post Haircut Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-12-03 10:59:58.001421",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Pledge",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
10
erpnext/loan_management/doctype/pledge/pledge.py
Normal file
10
erpnext/loan_management/doctype/pledge/pledge.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# -*- 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
|
||||
from frappe.model.document import Document
|
||||
|
||||
class Pledge(Document):
|
||||
pass
|
||||
10
erpnext/loan_management/doctype/pledge/test_pledge.py
Normal file
10
erpnext/loan_management/doctype/pledge/test_pledge.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestPledge(unittest.TestCase):
|
||||
pass
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Process Loan Interest Accrual', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "LM-PLA-.#####",
|
||||
"creation": "2019-09-19 06:08:12.363640",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"posting_date",
|
||||
"loan_type",
|
||||
"loan",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Posting Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Process Loan Interest Accrual",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Loan Type",
|
||||
"options": "Loan Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "loan",
|
||||
"fieldtype": "Link",
|
||||
"label": "Loan ",
|
||||
"options": "Loan"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-02-01 08:14:33.978636",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Process Loan Interest Accrual",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Loan Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# -*- 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
|
||||
from frappe.utils import nowdate
|
||||
from frappe.model.document import Document
|
||||
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import make_accrual_interest_entry_for_demand_loans
|
||||
|
||||
class ProcessLoanInterestAccrual(Document):
|
||||
def on_submit(self):
|
||||
open_loans = []
|
||||
|
||||
if self.loan:
|
||||
loan_doc = frappe.get_doc('Loan', self.loan)
|
||||
open_loans.append(loan_doc)
|
||||
|
||||
make_accrual_interest_entry_for_demand_loans(self.posting_date, self.name,
|
||||
open_loans = open_loans, loan_type = self.loan_type)
|
||||
|
||||
def process_loan_interest_accrual(posting_date=None, loan_type=None, loan=None):
|
||||
loan_process = frappe.new_doc('Process Loan Interest Accrual')
|
||||
loan_process.posting_date = posting_date or nowdate()
|
||||
loan_process.loan_type = loan_type
|
||||
loan_process.loan = loan
|
||||
|
||||
loan_process.submit()
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'process_loan_interest_accrual',
|
||||
'transactions': [
|
||||
{
|
||||
'items': ['Loan Interest Accrual']
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestProcessLoanInterestAccrual(unittest.TestCase):
|
||||
pass
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Process Loan Security Shortfall', {
|
||||
onload: function(frm) {
|
||||
frm.set_value('update_time', frappe.datetime.now_datetime());
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "LM-PLS-.#####",
|
||||
"creation": "2019-09-19 06:43:26.742336",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"update_time",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Process Loan Security Shortfall",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "update_time",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Update Time",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-02-01 08:14:05.845161",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Process Loan Security Shortfall",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Loan Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# -*- 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
|
||||
from frappe.utils import get_datetime
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import check_for_ltv_shortfall
|
||||
|
||||
class ProcessLoanSecurityShortfall(Document):
|
||||
def onload(self):
|
||||
self.set_onload('update_time', get_datetime())
|
||||
|
||||
def on_submit(self):
|
||||
check_for_ltv_shortfall(process_loan_security_shortfall = self.name)
|
||||
@@ -0,0 +1,12 @@
|
||||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'process_loan_security_shortfall',
|
||||
'transactions': [
|
||||
{
|
||||
'items': ['Loan Security Shortfall']
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestProcessLoanSecurityShortfall(unittest.TestCase):
|
||||
pass
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"creation": "2019-08-29 22:29:37.628178",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"loan_security",
|
||||
"qty",
|
||||
"loan_security_price",
|
||||
"amount",
|
||||
"haircut",
|
||||
"post_haircut_amount"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "loan_security_price",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Loan Security Price",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan_security.haircut",
|
||||
"fieldname": "haircut",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Haircut %",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan_security_pledge.qty",
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Quantity"
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_security",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Loan Security",
|
||||
"options": "Loan Security"
|
||||
},
|
||||
{
|
||||
"fieldname": "post_haircut_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Post Haircut Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-12-02 10:23:11.498308",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Proposed Pledge",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- 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
|
||||
from frappe.model.document import Document
|
||||
|
||||
class ProposedPledge(Document):
|
||||
pass
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"creation": "2019-09-12 12:57:07.940159",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"payment_date",
|
||||
"principal_amount",
|
||||
"interest_amount",
|
||||
"total_payment",
|
||||
"balance_loan_amount",
|
||||
"is_accrued"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "payment_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Payment Date"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "principal_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Principal Amount",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "interest_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Interest Amount",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "total_payment",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Total Payment",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "balance_loan_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Balance Loan Amount",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_accrued",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Is Accrued",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-09-12 12:57:07.940159",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Repayment Schedule",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- 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
|
||||
from frappe.model.document import Document
|
||||
|
||||
class RepaymentSchedule(Document):
|
||||
pass
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"creation": "2019-08-29 18:11:36.829526",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"loan",
|
||||
"loan_type",
|
||||
"loan_account",
|
||||
"interest_income_account",
|
||||
"column_break_4",
|
||||
"principal_amount",
|
||||
"interest_amount",
|
||||
"total_payment",
|
||||
"loan_repayment_entry"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "loan",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Loan",
|
||||
"options": "Loan",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Loan Account",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "interest_income_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Interest Income Account",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "principal_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Principal Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "interest_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Interest Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_payment",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Payment",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_repayment_entry",
|
||||
"fieldtype": "Link",
|
||||
"label": "Loan Repayment Entry",
|
||||
"options": "Loan Repayment",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan.loan_type",
|
||||
"fieldname": "loan_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Loan Type",
|
||||
"options": "Loan Type"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-10-28 09:15:31.174244",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Salary Slip Loan",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- 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
|
||||
from frappe.model.document import Document
|
||||
|
||||
class SalarySlipLoan(Document):
|
||||
pass
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Sanctioned Loan Amount', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "LM-SLA-.####",
|
||||
"creation": "2019-11-23 10:19:06.179736",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"applicant_type",
|
||||
"applicant",
|
||||
"column_break_3",
|
||||
"company",
|
||||
"sanctioned_amount_limit"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "applicant_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Applicant Type",
|
||||
"options": "Employee\nMember\nCustomer",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "applicant",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Applicant",
|
||||
"options": "applicant_type",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sanctioned_amount_limit",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Sanctioned Amount Limit",
|
||||
"options": "Company:company:default_currency",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-02-25 05:10:52.421193",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Sanctioned Loan Amount",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Loan Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- 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
|
||||
from frappe.model.document import Document
|
||||
|
||||
class SanctionedLoanAmount(Document):
|
||||
def validate(self):
|
||||
sanctioned_doc = frappe.db.exists('Sanctioned Loan Amount', {'applicant': self.applicant, 'company': self.company})
|
||||
|
||||
if sanctioned_doc and sanctioned_doc != self.name:
|
||||
frappe.throw(_("Sanctioned Loan Amount already exists for {0} against company {1}").format(
|
||||
frappe.bold(self.applicant), frappe.bold(self.company)
|
||||
))
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestSanctionedLoanAmount(unittest.TestCase):
|
||||
pass
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user