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
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 458e4a2..4491f7a 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -9,7 +9,6 @@
from erpnext.accounts.utils import get_balance_on, get_account_currency
from erpnext.accounts.party import get_party_account
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
-from erpnext.hr.doctype.loan.loan import update_disbursement_status, update_total_amount_paid
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get_party_account_based_on_invoice_discounting
from six import string_types, iteritems
@@ -50,7 +49,6 @@
self.make_gl_entries()
self.update_advance_paid()
self.update_expense_claim()
- self.update_loan()
self.update_inter_company_jv()
self.update_invoice_discounting()
@@ -62,7 +60,6 @@
self.make_gl_entries(1)
self.update_advance_paid()
self.update_expense_claim()
- self.update_loan()
self.unlink_advance_entry_reference()
self.unlink_asset_reference()
self.unlink_inter_company_jv()
@@ -597,17 +594,6 @@
doc = frappe.get_doc("Expense Claim", d.reference_name)
update_reimbursed_amount(doc)
- def update_loan(self):
- if self.paid_loan:
- paid_loan = json.loads(self.paid_loan)
- value = 1 if self.docstatus < 2 else 0
- for name in paid_loan:
- frappe.db.set_value("Repayment Schedule", name, "paid", value)
- for d in self.accounts:
- if d.reference_type=="Loan" and flt(d.debit) > 0:
- doc = frappe.get_doc("Loan", d.reference_name)
- update_disbursement_status(doc)
- update_total_amount_paid(doc)
def validate_expense_claim(self):
for d in self.accounts:
diff --git a/erpnext/config/desktop.py b/erpnext/config/desktop.py
index 95f6f7c..ce7c245 100644
--- a/erpnext/config/desktop.py
+++ b/erpnext/config/desktop.py
@@ -81,6 +81,15 @@
"description": "Sales pipeline, leads, opportunities and customers."
},
{
+ "module_name": "Loan Management",
+ "category": "Modules",
+ "label": _("Loan Management"),
+ "color": "#EF4DB6",
+ "icon": "octicon octicon-repo",
+ "type": "module",
+ "description": "Loan Management for Customer and Employees"
+ },
+ {
"module_name": "Support",
"category": "Modules",
"label": _("Support"),
diff --git a/erpnext/config/loan_management.py b/erpnext/config/loan_management.py
new file mode 100644
index 0000000..a84f13a
--- /dev/null
+++ b/erpnext/config/loan_management.py
@@ -0,0 +1,107 @@
+from __future__ import unicode_literals
+from frappe import _
+import frappe
+
+
+def get_data():
+ return [
+ {
+ "label": _("Loan"),
+ "items": [
+ {
+ "type": "doctype",
+ "name": "Loan Type",
+ "description": _("Loan Type for interest and penalty rates"),
+ },
+ {
+ "type": "doctype",
+ "name": "Loan Application",
+ "description": _("Loan Applications from customers and employees."),
+ },
+ {
+ "type": "doctype",
+ "name": "Loan",
+ "description": _("Loans provided to customers and employees."),
+ },
+
+ ]
+ },
+ {
+ "label": _("Loan Security"),
+ "items": [
+ {
+ "type": "doctype",
+ "name": "Loan Security Type",
+ },
+ {
+ "type": "doctype",
+ "name": "Loan Security Price",
+ },
+ {
+ "type": "doctype",
+ "name": "Loan Security",
+ },
+ {
+ "type": "doctype",
+ "name": "Loan Security Pledge",
+ },
+ {
+ "type": "doctype",
+ "name": "Loan Security Unpledge",
+ },
+ {
+ "type": "doctype",
+ "name": "Loan Security Shortfall",
+ },
+ ]
+ },
+ {
+ "label": _("Disbursement and Repayment"),
+ "items": [
+ {
+ "type": "doctype",
+ "name": "Loan Disbursement",
+ },
+ {
+ "type": "doctype",
+ "name": "Loan Repayment",
+ },
+ {
+ "type": "doctype",
+ "name": "Loan Interest Accrual"
+ }
+ ]
+ },
+ {
+ "label": _("Loan Processes"),
+ "items": [
+ {
+ "type": "doctype",
+ "name": "Process Loan Security Shortfall",
+ },
+ {
+ "type": "doctype",
+ "name": "Process Loan Interest Accrual",
+ }
+ ]
+ },
+ {
+ "label": _("Reports"),
+ "items": [
+ {
+ "type": "report",
+ "is_query_report": True,
+ "name": "Loan Repayment and Closure",
+ "route": "#query-report/Loan Repayment and Closure",
+ "doctype": "Loan Repayment",
+ },
+ {
+ "type": "report",
+ "is_query_report": True,
+ "name": "Loan Security Status",
+ "route": "#query-report/Loan Security Status",
+ "doctype": "Loan Security Pledge",
+ }
+ ]
+ }
+ ]
\ No newline at end of file
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 774c917..54f1a1e 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -314,12 +314,15 @@
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms",
"erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation",
- "erpnext.hr.utils.generate_leave_encashment"
+ "erpnext.hr.utils.generate_leave_encashment",
+ "erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall.check_for_ltv_shortfall",
+ "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.make_accrual_interest_entry_for_term_loans"
],
"monthly_long": [
"erpnext.accounts.deferred_revenue.convert_deferred_revenue_to_income",
"erpnext.accounts.deferred_revenue.convert_deferred_expense_to_expense",
- "erpnext.hr.utils.allocate_earned_leaves"
+ "erpnext.hr.utils.allocate_earned_leaves",
+ "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual"
]
}
diff --git a/erpnext/hr/doctype/loan/loan.js b/erpnext/hr/doctype/loan/loan.js
deleted file mode 100644
index 3f5c30c..0000000
--- a/erpnext/hr/doctype/loan/loan.js
+++ /dev/null
@@ -1,229 +0,0 @@
-// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-{% include 'erpnext/hr/loan_common.js' %};
-
-frappe.ui.form.on('Loan', {
- onload: function (frm) {
- frm.set_query("loan_application", function () {
- return {
- "filters": {
- "applicant": frm.doc.applicant,
- "docstatus": 1,
- "status": "Approved"
- }
- };
- });
-
- frm.set_query("interest_income_account", function () {
- return {
- "filters": {
- "company": frm.doc.company,
- "root_type": "Income",
- "is_group": 0
- }
- };
- });
-
- $.each(["payment_account", "loan_account"], function (i, field) {
- frm.set_query(field, function () {
- return {
- "filters": {
- "company": frm.doc.company,
- "root_type": "Asset",
- "is_group": 0
- }
- };
- });
- })
- },
-
- refresh: function (frm) {
- if (frm.doc.docstatus == 1) {
- if (frm.doc.status == "Sanctioned") {
- frm.add_custom_button(__('Create Disbursement Entry'), function() {
- frm.trigger("make_jv");
- }).addClass("btn-primary");
- } else if (frm.doc.status == "Disbursed" && frm.doc.repayment_start_date && (frm.doc.applicant_type == 'Member' || frm.doc.repay_from_salary == 0)) {
- frm.add_custom_button(__('Create Repayment Entry'), function() {
- frm.trigger("make_repayment_entry");
- }).addClass("btn-primary");
- }
- }
- frm.trigger("toggle_fields");
- },
-
- make_jv: function (frm) {
- frappe.call({
- args: {
- "loan": frm.doc.name,
- "company": frm.doc.company,
- "loan_account": frm.doc.loan_account,
- "applicant_type": frm.doc.applicant_type,
- "applicant": frm.doc.applicant,
- "loan_amount": frm.doc.loan_amount,
- "payment_account": frm.doc.payment_account
- },
- method: "erpnext.hr.doctype.loan.loan.make_jv_entry",
- callback: function (r) {
- if (r.message)
- var doc = frappe.model.sync(r.message)[0];
- frappe.set_route("Form", doc.doctype, doc.name);
- }
- })
- },
- make_repayment_entry: function(frm) {
- var repayment_schedule = $.map(frm.doc.repayment_schedule, function(d) { return d.paid ? d.payment_date : false; });
- if(repayment_schedule.length >= 1){
- frm.repayment_data = [];
- frm.show_dialog = 1;
- let title = "";
- let fields = [
- {fieldtype:'Section Break', label: __('Repayment Schedule')},
- {fieldname: 'payments', fieldtype: 'Table',
- fields: [
- {
- fieldtype:'Data',
- fieldname:'payment_date',
- label: __('Date'),
- read_only:1,
- in_list_view: 1,
- columns: 2
- },
- {
- fieldtype:'Currency',
- fieldname:'principal_amount',
- label: __('Principal Amount'),
- read_only:1,
- in_list_view: 1,
- columns: 3
- },
- {
- fieldtype:'Currency',
- fieldname:'interest_amount',
- label: __('Interest'),
- read_only:1,
- in_list_view: 1,
- columns: 2
- },
- {
- fieldtype:'Currency',
- read_only:1,
- fieldname:'total_payment',
- label: __('Total Payment'),
- in_list_view: 1,
- columns: 3
- },
- ],
- data: frm.repayment_data,
- get_data: function() {
- return frm.repayment_data;
- }
- }
- ]
-
- var dialog = new frappe.ui.Dialog({
- title: title, fields: fields,
- });
- if (frm.doc['repayment_schedule']) {
- frm.doc['repayment_schedule'].forEach((payment, index) => {
- if (payment.paid == 0 && payment.payment_date <= frappe.datetime.now_date()) {
- frm.repayment_data.push ({
- 'id': index,
- 'name': payment.name,
- 'payment_date': payment.payment_date,
- 'principal_amount': payment.principal_amount,
- 'interest_amount': payment.interest_amount,
- 'total_payment': payment.total_payment
- });
- dialog.fields_dict.payments.grid.refresh();
- $(dialog.wrapper.find(".grid-buttons")).hide();
- $(`.octicon.octicon-triangle-down`).hide();
- }
-
- })
- }
-
- dialog.show()
- dialog.set_primary_action(__('Create Repayment Entry'), function() {
- frm.values = dialog.get_values();
- if(frm.values) {
- _make_repayment_entry(frm, dialog.fields_dict.payments.grid.get_selected_children());
- dialog.hide()
- }
- });
- }
-
- dialog.get_close_btn().on('click', () => {
- dialog.hide();
- });
- },
-
- mode_of_payment: function (frm) {
- if (frm.doc.mode_of_payment && frm.doc.company) {
- frappe.call({
- method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.get_bank_cash_account",
- args: {
- "mode_of_payment": frm.doc.mode_of_payment,
- "company": frm.doc.company
- },
- callback: function (r, rt) {
- if (r.message) {
- frm.set_value("payment_account", r.message.account);
- }
- }
- });
- }
- },
-
- loan_application: function (frm) {
- if(frm.doc.loan_application){
- return frappe.call({
- method: "erpnext.hr.doctype.loan.loan.get_loan_application",
- args: {
- "loan_application": frm.doc.loan_application
- },
- callback: function (r) {
- if (!r.exc && r.message) {
- frm.set_value("loan_type", r.message.loan_type);
- frm.set_value("loan_amount", r.message.loan_amount);
- frm.set_value("repayment_method", r.message.repayment_method);
- frm.set_value("monthly_repayment_amount", r.message.repayment_amount);
- frm.set_value("repayment_periods", r.message.repayment_periods);
- frm.set_value("rate_of_interest", r.message.rate_of_interest);
- }
- }
- });
- }
- },
-
- repayment_method: function (frm) {
- frm.trigger("toggle_fields")
- },
-
- toggle_fields: function (frm) {
- frm.toggle_enable("monthly_repayment_amount", frm.doc.repayment_method == "Repay Fixed Amount per Period")
- frm.toggle_enable("repayment_periods", frm.doc.repayment_method == "Repay Over Number of Periods")
- }
-});
-
-var _make_repayment_entry = function(frm, payment_rows) {
- frappe.call({
- method:"erpnext.hr.doctype.loan.loan.make_repayment_entry",
- args: {
- payment_rows: payment_rows,
- "loan": frm.doc.name,
- "company": frm.doc.company,
- "loan_account": frm.doc.loan_account,
- "applicant_type": frm.doc.applicant_type,
- "applicant": frm.doc.applicant,
- "payment_account": frm.doc.payment_account,
- "interest_income_account": frm.doc.interest_income_account
- },
- callback: function(r) {
- if (r.message)
- var doc = frappe.model.sync(r.message)[0];
- frappe.set_route("Form", doc.doctype, doc.name, {'payment_rows': payment_rows});
- }
- });
-}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/loan/loan.py b/erpnext/hr/doctype/loan/loan.py
deleted file mode 100644
index a803863..0000000
--- a/erpnext/hr/doctype/loan/loan.py
+++ /dev/null
@@ -1,240 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe, math, json
-import erpnext
-from frappe import _
-from frappe.utils import flt, rounded, add_months, nowdate, getdate
-from erpnext.controllers.accounts_controller import AccountsController
-
-class Loan(AccountsController):
- def validate(self):
- validate_repayment_method(self.repayment_method, self.loan_amount, self.monthly_repayment_amount, self.repayment_periods)
- self.set_missing_fields()
- self.make_repayment_schedule()
- self.set_repayment_period()
- self.calculate_totals()
-
- def set_missing_fields(self):
- if not self.company:
- self.company = erpnext.get_default_company()
-
- if not self.posting_date:
- self.posting_date = nowdate()
-
- if self.loan_type and not self.rate_of_interest:
- self.rate_of_interest = frappe.db.get_value("Loan Type", self.loan_type, "rate_of_interest")
-
- if self.repayment_method == "Repay Over Number of Periods":
- self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
-
- if self.status == "Repaid/Closed":
- self.total_amount_paid = self.total_payment
-
-
- def make_jv_entry(self):
- self.check_permission('write')
- journal_entry = frappe.new_doc('Journal Entry')
- journal_entry.voucher_type = 'Bank Entry'
- journal_entry.user_remark = _('Against Loan: {0}').format(self.name)
- journal_entry.company = self.company
- journal_entry.posting_date = nowdate()
-
- account_amt_list = []
-
- account_amt_list.append({
- "account": self.loan_account,
- "party_type": self.applicant_type,
- "party": self.applicant,
- "debit_in_account_currency": self.loan_amount,
- "reference_type": "Loan",
- "reference_name": self.name,
- })
- account_amt_list.append({
- "account": self.payment_account,
- "credit_in_account_currency": self.loan_amount,
- "reference_type": "Loan",
- "reference_name": self.name,
- })
- journal_entry.set("accounts", account_amt_list)
- return journal_entry.as_dict()
-
- def make_repayment_schedule(self):
- self.repayment_schedule = []
- payment_date = self.repayment_start_date
- balance_amount = self.loan_amount
- while(balance_amount > 0):
- interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100))
- principal_amount = self.monthly_repayment_amount - interest_amount
- balance_amount = rounded(balance_amount + interest_amount - self.monthly_repayment_amount)
-
- if balance_amount < 0:
- principal_amount += balance_amount
- balance_amount = 0.0
-
- total_payment = principal_amount + interest_amount
- self.append("repayment_schedule", {
- "payment_date": payment_date,
- "principal_amount": principal_amount,
- "interest_amount": interest_amount,
- "total_payment": total_payment,
- "balance_loan_amount": balance_amount
- })
- next_payment_date = add_months(payment_date, 1)
- payment_date = next_payment_date
-
- def set_repayment_period(self):
- if self.repayment_method == "Repay Fixed Amount per Period":
- repayment_periods = len(self.repayment_schedule)
-
- self.repayment_periods = repayment_periods
-
- def calculate_totals(self):
- self.total_payment = 0
- self.total_interest_payable = 0
- self.total_amount_paid = 0
- for data in self.repayment_schedule:
- self.total_payment += data.total_payment
- self.total_interest_payable +=data.interest_amount
- if data.paid:
- self.total_amount_paid += data.total_payment
-
-def update_total_amount_paid(doc):
- total_amount_paid = 0
- for data in doc.repayment_schedule:
- if data.paid:
- total_amount_paid += data.total_payment
- frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid)
-
-def update_disbursement_status(doc):
- disbursement = frappe.db.sql("""
- select posting_date, ifnull(sum(credit_in_account_currency), 0) as disbursed_amount
- from `tabGL Entry`
- where account = %s and against_voucher_type = 'Loan' and against_voucher = %s
- """, (doc.payment_account, doc.name), as_dict=1)[0]
-
- disbursement_date = None
- if not disbursement or disbursement.disbursed_amount == 0:
- status = "Sanctioned"
- elif disbursement.disbursed_amount == doc.loan_amount:
- disbursement_date = disbursement.posting_date
- status = "Disbursed"
- elif disbursement.disbursed_amount > doc.loan_amount:
- frappe.throw(_("Disbursed Amount cannot be greater than Loan Amount {0}").format(doc.loan_amount))
-
- if status == 'Disbursed' and getdate(disbursement_date) > getdate(frappe.db.get_value("Loan", doc.name, "repayment_start_date")):
- frappe.throw(_("Disbursement Date cannot be after Loan Repayment Start Date"))
-
- frappe.db.sql("""
- update `tabLoan`
- set status = %s, disbursement_date = %s
- where name = %s
- """, (status, disbursement_date, doc.name))
-
-def validate_repayment_method(repayment_method, loan_amount, monthly_repayment_amount, repayment_periods):
- if repayment_method == "Repay Over Number of Periods" and not repayment_periods:
- frappe.throw(_("Please enter Repayment Periods"))
-
- if repayment_method == "Repay Fixed Amount per Period":
- if not monthly_repayment_amount:
- frappe.throw(_("Please enter repayment Amount"))
- if monthly_repayment_amount > loan_amount:
- frappe.throw(_("Monthly Repayment Amount cannot be greater than Loan Amount"))
-
-def get_monthly_repayment_amount(repayment_method, loan_amount, rate_of_interest, repayment_periods):
- if rate_of_interest:
- monthly_interest_rate = flt(rate_of_interest) / (12 *100)
- monthly_repayment_amount = math.ceil((loan_amount * monthly_interest_rate *
- (1 + monthly_interest_rate)**repayment_periods) \
- / ((1 + monthly_interest_rate)**repayment_periods - 1))
- else:
- monthly_repayment_amount = math.ceil(flt(loan_amount) / repayment_periods)
- return monthly_repayment_amount
-
-@frappe.whitelist()
-def get_loan_application(loan_application):
- loan = frappe.get_doc("Loan Application", loan_application)
- if loan:
- return loan.as_dict()
-
-@frappe.whitelist()
-def make_repayment_entry(payment_rows, loan, company, loan_account, applicant_type, applicant, \
- payment_account=None, interest_income_account=None):
-
- if isinstance(payment_rows, frappe.string_types):
- payment_rows_list = json.loads(payment_rows)
- else:
- frappe.throw(_("No repayments available for Journal Entry"))
-
- if payment_rows_list:
- row_name = list(set(d["name"] for d in payment_rows_list))
- else:
- frappe.throw(_("No repayments selected for Journal Entry"))
- total_payment = 0
- principal_amount = 0
- interest_amount = 0
- for d in payment_rows_list:
- total_payment += d["total_payment"]
- principal_amount += d["principal_amount"]
- interest_amount += d["interest_amount"]
-
- journal_entry = frappe.new_doc('Journal Entry')
- journal_entry.voucher_type = 'Bank Entry'
- journal_entry.user_remark = _('Against Loan: {0}').format(loan)
- journal_entry.company = company
- journal_entry.posting_date = nowdate()
- journal_entry.paid_loan = json.dumps(row_name)
- account_amt_list = []
-
- account_amt_list.append({
- "account": payment_account,
- "debit_in_account_currency": total_payment,
- "reference_type": "Loan",
- "reference_name": loan,
- })
- account_amt_list.append({
- "account": loan_account,
- "credit_in_account_currency": principal_amount,
- "party_type": applicant_type,
- "party": applicant,
- "reference_type": "Loan",
- "reference_name": loan,
- })
- account_amt_list.append({
- "account": interest_income_account,
- "credit_in_account_currency": interest_amount,
- "reference_type": "Loan",
- "reference_name": loan,
- })
- journal_entry.set("accounts", account_amt_list)
-
- return journal_entry.as_dict()
-
-@frappe.whitelist()
-def make_jv_entry(loan, company, loan_account, applicant_type, applicant, loan_amount,payment_account=None):
-
- journal_entry = frappe.new_doc('Journal Entry')
- journal_entry.voucher_type = 'Bank Entry'
- journal_entry.user_remark = _('Against Loan: {0}').format(loan)
- journal_entry.company = company
- journal_entry.posting_date = nowdate()
- account_amt_list = []
-
- account_amt_list.append({
- "account": loan_account,
- "debit_in_account_currency": loan_amount,
- "party_type": applicant_type,
- "party": applicant,
- "reference_type": "Loan",
- "reference_name": loan,
- })
- account_amt_list.append({
- "account": payment_account,
- "credit_in_account_currency": loan_amount,
- "reference_type": "Loan",
- "reference_name": loan,
- })
- journal_entry.set("accounts", account_amt_list)
- return journal_entry.as_dict()
diff --git a/erpnext/hr/doctype/loan/loan_dashboard.py b/erpnext/hr/doctype/loan/loan_dashboard.py
deleted file mode 100644
index 7256d94..0000000
--- a/erpnext/hr/doctype/loan/loan_dashboard.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-
-def get_data():
- return {
- 'fieldname': 'applicant',
- 'non_standard_fieldnames': {
- 'Journal Entry': 'reference_name',
- 'Salary Slip': 'employee'
- },
- 'transactions': [
- {
- 'label': _('Applicant'),
- 'items': ['Loan Application']
- },
-
- {
- 'label': _('Account'),
- 'items': ['Journal Entry']
- },
- {
- 'label': _('Employee'),
- 'items': ['Salary Slip']
- }
- ]
- }
\ No newline at end of file
diff --git a/erpnext/hr/doctype/loan/test_loan.js b/erpnext/hr/doctype/loan/test_loan.js
deleted file mode 100644
index 28d30c9..0000000
--- a/erpnext/hr/doctype/loan/test_loan.js
+++ /dev/null
@@ -1,79 +0,0 @@
-
-QUnit.test("Test Loan [HR]", function(assert) {
- assert.expect(8);
- let done = assert.async();
- let employee_name;
-
- // To create a loan and check principal,interest and balance amount
- let loan_creation = (ename,lname) => {
- return frappe.run_serially([
- () => frappe.db.get_value('Employee', {'employee_name': ename}, 'name'),
- (r) => {
- employee_name = r.message.name;
- },
- () => frappe.db.get_value('Loan Application', {'loan_type': lname}, 'name'),
- (r) => {
- // Creating loan for an employee
- return frappe.tests.make('Loan', [
- { company: 'For Testing'},
- { posting_date: '2017-08-26'},
- { applicant: employee_name},
- { loan_application: r.message.name},
- { disbursement_date: '2018-08-26'},
- { mode_of_payment: 'Cash'},
- { loan_account: 'Temporary Opening - FT'},
- { interest_income_account: 'Service - FT'}
- ]);
- },
- () => frappe.timeout(3),
- () => frappe.click_button('Submit'),
- () => frappe.timeout(1),
- () => frappe.click_button('Yes'),
- () => frappe.timeout(3),
-
- // Checking if all the amounts are correctly calculated
- () => {
- assert.ok(cur_frm.get_field('applicant_name').value=='Test Employee 1'&&
- (cur_frm.get_field('status').value=='Sanctioned'),
- 'Loan Sanctioned for correct employee');
-
- assert.equal(7270,
- cur_frm.get_doc('repayment_schedule').repayment_schedule[0].principal_amount,
- 'Principal amount for first instalment is correctly calculated');
-
- assert.equal(2333,
- cur_frm.get_doc('repayment_schedule').repayment_schedule[0].interest_amount,
- 'Interest amount for first instalment is correctly calculated');
-
- assert.equal(192730,
- cur_frm.get_doc('repayment_schedule').repayment_schedule[0].balance_loan_amount,
- 'Balance amount after first instalment is correctly calculated');
-
- assert.equal(9479,
- cur_frm.get_doc('repayment_schedule').repayment_schedule[23].principal_amount,
- 'Principal amount for last instalment is correctly calculated');
-
- assert.equal(111,
- cur_frm.get_doc('repayment_schedule').repayment_schedule[23].interest_amount,
- 'Interest amount for last instalment is correctly calculated');
-
- assert.equal(0,
- cur_frm.get_doc('repayment_schedule').repayment_schedule[23].balance_loan_amount,
- 'Balance amount after last instalment is correctly calculated');
-
- },
- () => frappe.set_route('List','Loan','List'),
- () => frappe.timeout(2),
-
- // Checking the submission of Loan
- () => {
- assert.ok(cur_list.data[0].docstatus==1,'Loan sanctioned and submitted successfully');
- },
- ]);
- };
- frappe.run_serially([
- // Creating loan
- () => loan_creation('Test Employee 1','Test Loan'),
- () => done()
- ]);
-});
diff --git a/erpnext/hr/doctype/loan/test_loan.py b/erpnext/hr/doctype/loan/test_loan.py
deleted file mode 100644
index 740e429..0000000
--- a/erpnext/hr/doctype/loan/test_loan.py
+++ /dev/null
@@ -1,71 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-from __future__ import unicode_literals
-
-import frappe
-import erpnext
-import unittest
-from frappe.utils import nowdate, add_days
-from erpnext.hr.doctype.salary_structure.test_salary_structure import make_employee
-
-class TestLoan(unittest.TestCase):
- def setUp(self):
- create_loan_type("Personal Loan", 500000, 8.4)
- self.applicant = make_employee("robert_loan@loan.com")
- create_loan(self.applicant, "Personal Loan", 280000, "Repay Over Number of Periods", 20)
-
- def test_loan(self):
- loan = frappe.get_doc("Loan", {"applicant":self.applicant})
- self.assertEquals(loan.monthly_repayment_amount, 15052)
- self.assertEquals(loan.total_interest_payable, 21034)
- self.assertEquals(loan.total_payment, 301034)
-
- schedule = loan.repayment_schedule
-
- self.assertEqual(len(schedule), 20)
-
- for idx, principal_amount, interest_amount, balance_loan_amount in [[3, 13369, 1683, 227079], [19, 14941, 105, 0], [17, 14740, 312, 29785]]:
- self.assertEqual(schedule[idx].principal_amount, principal_amount)
- self.assertEqual(schedule[idx].interest_amount, interest_amount)
- self.assertEqual(schedule[idx].balance_loan_amount, balance_loan_amount)
-
- loan.repayment_method = "Repay Fixed Amount per Period"
- loan.monthly_repayment_amount = 14000
- loan.save()
-
- self.assertEquals(len(loan.repayment_schedule), 22)
- self.assertEquals(loan.total_interest_payable, 22712)
- self.assertEquals(loan.total_payment, 302712)
-
-def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest):
- if not frappe.db.exists("Loan Type", loan_name):
- frappe.get_doc({
- "doctype": "Loan Type",
- "loan_name": loan_name,
- "maximum_loan_amount": maximum_loan_amount,
- "rate_of_interest": rate_of_interest
- }).insert()
-
-def create_loan(applicant, loan_type, loan_amount, repayment_method, repayment_periods):
- create_loan_type(loan_type, 500000, 8.4)
- if not frappe.db.get_value("Loan", {"applicant":applicant}):
- loan = frappe.new_doc("Loan")
- loan.update({
- "applicant": applicant,
- "loan_type": loan_type,
- "loan_amount": loan_amount,
- "repayment_method": repayment_method,
- "repayment_periods": repayment_periods,
- "disbursement_date": nowdate(),
- "repayment_start_date": nowdate(),
- "status": "Disbursed",
- "mode_of_payment": frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'),
- "payment_account": frappe.db.get_value('Account', {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name"),
- "loan_account": frappe.db.get_value('Account', {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name"),
- "interest_income_account": frappe.db.get_value('Account', {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name")
- })
- loan.insert()
- return loan
- else:
- return frappe.get_doc("Loan", {"applicant":applicant})
\ No newline at end of file
diff --git a/erpnext/hr/doctype/loan_application/loan_application.js b/erpnext/hr/doctype/loan_application/loan_application.js
deleted file mode 100644
index a73b62a..0000000
--- a/erpnext/hr/doctype/loan_application/loan_application.js
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-{% include 'erpnext/hr/loan_common.js' %};
-
-frappe.ui.form.on('Loan Application', {
- refresh: function(frm) {
- frm.trigger("toggle_fields")
- frm.trigger("add_toolbar_buttons")
- },
- repayment_method: function(frm) {
- frm.doc.repayment_amount = frm.doc.repayment_periods = ""
- frm.trigger("toggle_fields")
- frm.trigger("toggle_required")
- },
- toggle_fields: function(frm) {
- frm.toggle_enable("repayment_amount", frm.doc.repayment_method=="Repay Fixed Amount per Period")
- frm.toggle_enable("repayment_periods", frm.doc.repayment_method=="Repay Over Number of Periods")
- },
- toggle_required: function(frm){
- frm.toggle_reqd("repayment_amount", cint(frm.doc.repayment_method=='Repay Fixed Amount per Period'))
- frm.toggle_reqd("repayment_periods", cint(frm.doc.repayment_method=='Repay Over Number of Periods'))
- },
- add_toolbar_buttons: function(frm) {
- if (frm.doc.status == "Approved") {
- frm.add_custom_button(__('Create Loan'), function() {
- frappe.call({
- method: "erpnext.hr.doctype.loan_application.loan_application.make_loan",
- args: {
- "source_name": frm.doc.name
- },
- callback: function(r) {
- if(!r.exc) {
- var doc = frappe.model.sync(r.message);
- frappe.set_route("Form", r.message.doctype, r.message.name);
- }
- }
- });
- }).addClass("btn-primary");
- }
- }
-});
diff --git a/erpnext/hr/doctype/loan_application/loan_application.json b/erpnext/hr/doctype/loan_application/loan_application.json
deleted file mode 100644
index cc73a86..0000000
--- a/erpnext/hr/doctype/loan_application/loan_application.json
+++ /dev/null
@@ -1,840 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "ACC-LOAP-.YYYY.-.#####",
- "beta": 0,
- "creation": "2016-12-02 12:35:56.046811",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "applicant_type",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Applicant Type",
- "length": 0,
- "no_copy": 0,
- "options": "Employee\nMember",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "applicant",
- "fieldtype": "Dynamic Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 1,
- "in_list_view": 0,
- "in_standard_filter": 1,
- "label": "Applicant",
- "length": 0,
- "no_copy": 0,
- "options": "applicant_type",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "applicant",
- "fieldname": "applicant_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 1,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Applicant Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Today",
- "fieldname": "posting_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Posting Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "status",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Status",
- "length": 0,
- "no_copy": 1,
- "options": "Open\nApproved\nRejected",
- "permlevel": 1,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "company",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Company",
- "length": 0,
- "no_copy": 0,
- "options": "Company",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_4",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Loan Info",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "loan_type",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Loan Type",
- "length": 0,
- "no_copy": 0,
- "options": "Loan Type",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "loan_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Loan Amount",
- "length": 0,
- "no_copy": 0,
- "options": "Company:company:default_currency",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "required_by_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Required by Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_7",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "description",
- "fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Reason",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "repayment_info",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Repayment Info",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "repayment_method",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Repayment Method",
- "length": 0,
- "no_copy": 0,
- "options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "loan_type.rate_of_interest",
- "fieldname": "rate_of_interest",
- "fieldtype": "Percent",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Rate of Interest",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "total_payable_interest",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Total Payable Interest",
- "length": 0,
- "no_copy": 0,
- "options": "Company:company:default_currency",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_11",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fieldname": "repayment_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Monthly Repayment Amount",
- "length": 0,
- "no_copy": 0,
- "options": "Company:company:default_currency",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fieldname": "repayment_periods",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Repayment Period in Months",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "total_payable_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Total Payable Amount",
- "length": 0,
- "no_copy": 0,
- "options": "Company:company:default_currency",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "amended_from",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Amended From",
- "length": 0,
- "no_copy": 1,
- "options": "Loan Application",
- "permlevel": 0,
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 1,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-08-21 16:15:53.688596",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "Loan Application",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "HR Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 1,
- "write": 1
- },
- {
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Employee",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 1,
- "write": 1
- },
- {
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "HR Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Employee",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 0
- }
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "search_fields": "applicant_type, applicant, loan_type, loan_amount",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "timeline_field": "applicant",
- "title_field": "applicant",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/loan_application/loan_application.py b/erpnext/hr/doctype/loan_application/loan_application.py
deleted file mode 100644
index 582bf48..0000000
--- a/erpnext/hr/doctype/loan_application/loan_application.py
+++ /dev/null
@@ -1,70 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe, math
-from frappe import _
-from frappe.utils import flt, rounded
-from frappe.model.mapper import get_mapped_doc
-from frappe.model.document import Document
-
-from erpnext.hr.doctype.loan.loan import get_monthly_repayment_amount, validate_repayment_method
-
-class LoanApplication(Document):
- def validate(self):
- validate_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount, self.repayment_periods)
- self.validate_loan_amount()
- self.get_repayment_details()
-
- def validate_loan_amount(self):
- maximum_loan_limit = frappe.db.get_value('Loan Type', self.loan_type, 'maximum_loan_amount')
- if maximum_loan_limit and self.loan_amount > maximum_loan_limit:
- frappe.throw(_("Loan Amount cannot exceed Maximum Loan Amount of {0}").format(maximum_loan_limit))
-
- def get_repayment_details(self):
- if self.repayment_method == "Repay Over Number of Periods":
- self.repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
-
- if self.repayment_method == "Repay Fixed Amount per Period":
- monthly_interest_rate = flt(self.rate_of_interest) / (12 *100)
- if monthly_interest_rate:
- min_repayment_amount = self.loan_amount*monthly_interest_rate
- if (self.repayment_amount - min_repayment_amount) <= 0:
- frappe.throw(_("Repayment Amount must be greater than " \
- + str(flt(min_repayment_amount, 2))))
- self.repayment_periods = math.ceil((math.log(self.repayment_amount) -
- math.log(self.repayment_amount - min_repayment_amount)) /(math.log(1 + monthly_interest_rate)))
- else:
- self.repayment_periods = self.loan_amount / self.repayment_amount
-
- self.calculate_payable_amount()
-
- def calculate_payable_amount(self):
- balance_amount = self.loan_amount
- self.total_payable_amount = 0
- self.total_payable_interest = 0
-
- while(balance_amount > 0):
- interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100))
- balance_amount = rounded(balance_amount + interest_amount - self.repayment_amount)
-
- self.total_payable_interest += interest_amount
-
- self.total_payable_amount = self.loan_amount + self.total_payable_interest
-
-@frappe.whitelist()
-def make_loan(source_name, target_doc = None):
- doclist = get_mapped_doc("Loan Application", source_name, {
- "Loan Application": {
- "doctype": "Loan",
- "field_map": {
- "repayment_amount": "monthly_repayment_amount"
- },
- "validation": {
- "docstatus": ["=", 1]
- }
- }
- }, target_doc)
-
- return doclist
diff --git a/erpnext/hr/doctype/loan_application/test_loan_application.js b/erpnext/hr/doctype/loan_application/test_loan_application.js
deleted file mode 100644
index b8789c7..0000000
--- a/erpnext/hr/doctype/loan_application/test_loan_application.js
+++ /dev/null
@@ -1,68 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Loan Application [HR]", function (assert) {
- assert.expect(8);
- let done = assert.async();
- let employee_name;
-
- frappe.run_serially([
- // Creation of Loan Application
- () => frappe.db.get_value('Employee', {'employee_name': 'Test Employee 1'}, 'name'),
- (r) => {
- employee_name = r.message.name;
- },
- () => {
- return frappe.tests.make('Loan Application', [
- { company: 'For Testing'},
- { applicant: employee_name},
- { applicant_name: 'Test Employee 1'},
- { status: 'Approved'},
- { loan_type: 'Test Loan '},
- { loan_amount: 200000},
- { description: 'This is just a test'},
- { repayment_method: 'Repay Over Number of Periods'},
- { repayment_periods: 24},
- { rate_of_interest: 14}
- ]);
- },
- () => frappe.timeout(6),
- () => frappe.click_button('Submit'),
- () => frappe.timeout(1),
- () => frappe.click_button('Yes'),
- () => frappe.timeout(2),
- () => {
- // To check if all the amounts are correctly calculated
-
- assert.ok(cur_frm.get_field('applicant_name').value == 'Test Employee 1',
- 'Application created successfully');
-
- assert.ok(cur_frm.get_field('status').value=='Approved',
- 'Status of application is correctly set');
-
- assert.ok(cur_frm.get_field('loan_type').value=='Test Loan',
- 'Application is created for correct Loan Type');
-
- assert.ok(cur_frm.get_field('status').value=='Approved',
- 'Status of application is correctly set');
-
- assert.ok(cur_frm.get_field('repayment_amount').value==9603,
- 'Repayment amount is correctly calculated');
-
- assert.ok(cur_frm.get_field('total_payable_interest').value==30459,
- 'Interest amount is correctly calculated');
-
- assert.ok(cur_frm.get_field('total_payable_amount').value==230459,
- 'Total payable amount is correctly calculated');
- },
-
- () => frappe.set_route('List','Loan Application','List'),
- () => frappe.timeout(2),
-
- // Checking the submission of Loan Application
- () => {
- assert.ok(cur_list.data[0].docstatus==1,'Loan Application submitted successfully');
- },
- () => frappe.timeout(1),
- () => done()
- ]);
-});
\ No newline at end of file
diff --git a/erpnext/hr/doctype/loan_application/test_loan_application.py b/erpnext/hr/doctype/loan_application/test_loan_application.py
deleted file mode 100644
index b08b522..0000000
--- a/erpnext/hr/doctype/loan_application/test_loan_application.py
+++ /dev/null
@@ -1,52 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-from __future__ import unicode_literals
-
-import frappe
-import unittest
-from erpnext.hr.doctype.salary_structure.test_salary_structure import make_employee
-
-class TestLoanApplication(unittest.TestCase):
- def setUp(self):
- self.create_loan_type()
- self.applicant = make_employee("kate_loan@loan.com")
- self.create_loan_application()
-
- def create_loan_type(self):
- if not frappe.db.get_value("Loan Type", "Home Loan"):
- frappe.get_doc({
- "doctype": "Loan Type",
- "loan_name": "Home Loan",
- "maximum_loan_amount": 500000,
- "rate_of_interest": 9.2
- }).insert()
-
- def create_loan_application(self):
- if not frappe.db.get_value("Loan Application", {"applicant":self.applicant}, "name"):
- loan_application = frappe.new_doc("Loan Application")
- loan_application.update({
- "applicant": self.applicant,
- "loan_type": "Home Loan",
- "rate_of_interest": 9.2,
- "loan_amount": 250000,
- "repayment_method": "Repay Over Number of Periods",
- "repayment_periods": 18
- })
- loan_application.insert()
-
-
- 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)
diff --git a/erpnext/hr/doctype/loan_type/loan_type.js b/erpnext/hr/doctype/loan_type/loan_type.js
deleted file mode 100644
index 72f5775..0000000
--- a/erpnext/hr/doctype/loan_type/loan_type.js
+++ /dev/null
@@ -1,7 +0,0 @@
-// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Loan Type', {
- refresh: function(frm) {
- }
-});
diff --git a/erpnext/hr/doctype/loan_type/loan_type.json b/erpnext/hr/doctype/loan_type/loan_type.json
deleted file mode 100644
index e595187..0000000
--- a/erpnext/hr/doctype/loan_type/loan_type.json
+++ /dev/null
@@ -1,259 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:loan_name",
- "beta": 0,
- "creation": "2016-12-02 10:41:40.732843",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "loan_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Loan Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "maximum_loan_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Maximum Loan Amount",
- "length": 0,
- "no_copy": 0,
- "options": "Company:company:default_currency",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "rate_of_interest",
- "fieldtype": "Percent",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Rate of Interest (%) Yearly",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fieldname": "disabled",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Disabled",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "description",
- "fieldtype": "Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2017-03-29 21:23:08.665245",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "Loan Type",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "HR Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "Employee",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
- }
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/loan_type/loan_type_dashboard.py b/erpnext/hr/doctype/loan_type/loan_type_dashboard.py
deleted file mode 100644
index 07b11fe..0000000
--- a/erpnext/hr/doctype/loan_type/loan_type_dashboard.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-
-def get_data():
- return {
- 'fieldname': 'loan_type',
- 'transactions': [
- {
- 'items': ['Loan Application']
- },
- ],
- }
\ No newline at end of file
diff --git a/erpnext/hr/doctype/loan_type/test_loan_type.js b/erpnext/hr/doctype/loan_type/test_loan_type.js
deleted file mode 100644
index 71354be..0000000
--- a/erpnext/hr/doctype/loan_type/test_loan_type.js
+++ /dev/null
@@ -1,31 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Loan Type [HR]", function (assert) {
- assert.expect(3);
- let done = assert.async();
-
- frappe.run_serially([
- // Loan Type creation
- () => {
- frappe.tests.make('Loan Type', [
- { loan_name: 'Test Loan'},
- { maximum_loan_amount: 400000},
- { rate_of_interest: 14},
- { description:
- 'This is just a test.'}
- ]);
- },
- () => frappe.timeout(7),
- () => frappe.set_route('List','Loan Type','List'),
- () => frappe.timeout(4),
-
- // Checking if the fields are correctly set
- () => {
- assert.ok(cur_list.data.length==1, 'Loan Type created successfully');
- assert.ok(cur_list.data[0].name=='Test Loan', 'Loan title Correctly set');
- assert.ok(cur_list.data[0].disabled==0, 'Loan enabled');
- },
- () => done()
- ]);
-});
-
diff --git a/erpnext/hr/doctype/loan_type/test_loan_type.py b/erpnext/hr/doctype/loan_type/test_loan_type.py
deleted file mode 100644
index 078e11e..0000000
--- a/erpnext/hr/doctype/loan_type/test_loan_type.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-from __future__ import unicode_literals
-
-import frappe
-import unittest
-
-# test_records = frappe.get_test_records('Loan Type')
-
-class TestLoanType(unittest.TestCase):
- pass
diff --git a/erpnext/hr/doctype/payroll_entry/payroll_entry.py b/erpnext/hr/doctype/payroll_entry/payroll_entry.py
index dfd38eb..9ef3a99 100644
--- a/erpnext/hr/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/hr/doctype/payroll_entry/payroll_entry.py
@@ -157,19 +157,6 @@
for ss in submitted_ss:
ss.email_salary_slip()
- def get_loan_details(self):
- """
- Get loan details from submitted salary slip based on selected criteria
- """
- cond = self.get_filter_condition()
- return frappe.db.sql(""" select eld.loan_account, eld.loan,
- eld.interest_income_account, eld.principal_amount, eld.interest_amount, eld.total_payment,t1.employee
- from
- `tabSalary Slip` t1, `tabSalary Slip Loan` eld
- where
- t1.docstatus = 1 and t1.name = eld.parent and start_date >= %s and end_date <= %s %s
- """ % ('%s', '%s', cond), (self.start_date, self.end_date), as_dict=True) or []
-
def get_salary_component_account(self, salary_component):
account = frappe.db.get_value("Salary Component Account",
{"parent": salary_component, "company": self.company}, "default_account")
@@ -225,7 +212,6 @@
earnings = self.get_salary_component_total(component_type = "earnings") or {}
deductions = self.get_salary_component_total(component_type = "deductions") or {}
default_payroll_payable_account = self.get_default_payroll_payable_account()
- loan_details = self.get_loan_details()
jv_name = ""
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
@@ -262,29 +248,6 @@
"project": self.project
})
- # Loan
- for data in loan_details:
- accounts.append({
- "account": data.loan_account,
- "credit_in_account_currency": data.principal_amount,
- "party_type": "Employee",
- "party": data.employee
- })
-
- if data.interest_amount and not data.interest_income_account:
- frappe.throw(_("Select interest income account in loan {0}").format(data.loan))
-
- if data.interest_income_account and data.interest_amount:
- accounts.append({
- "account": data.interest_income_account,
- "credit_in_account_currency": data.interest_amount,
- "cost_center": self.cost_center,
- "project": self.project,
- "party_type": "Employee",
- "party": data.employee
- })
- payable_amount -= flt(data.total_payment, precision)
-
# Payable amount
accounts.append({
"account": default_payroll_payable_account,
diff --git a/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py b/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py
index 3cf1322..35f5a57 100644
--- a/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py
+++ b/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py
@@ -6,20 +6,22 @@
import frappe
from dateutil.relativedelta import relativedelta
from erpnext.accounts.utils import get_fiscal_year, getdate, nowdate
+from frappe.utils import add_months
from erpnext.hr.doctype.payroll_entry.payroll_entry import get_start_end_dates, get_end_date
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.salary_slip.test_salary_slip import get_salary_component_account, \
make_earning_salary_component, make_deduction_salary_component
from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure
-from erpnext.hr.doctype.loan.test_loan import create_loan
+from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry
+from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import make_accrual_interest_entry_for_term_loans
class TestPayrollEntry(unittest.TestCase):
def setUp(self):
- for dt in ["Salary Slip", "Salary Component", "Salary Component Account", "Payroll Entry", "Loan"]:
+ for dt in ["Salary Slip", "Salary Component", "Salary Component Account", "Payroll Entry"]:
frappe.db.sql("delete from `tab%s`" % dt)
- make_earning_salary_component(setup=True)
- make_deduction_salary_component(setup=True)
+ make_earning_salary_component(setup=True, company_list=["_Test Company"])
+ make_deduction_salary_component(setup=True, company_list=["_Test Company"])
frappe.db.set_value("HR Settings", None, "email_salary_slip_to_employee", 0)
@@ -49,8 +51,8 @@
def test_loan(self):
branch = "Test Employee Branch"
- applicant = make_employee("test_employee@loan.com")
- company = erpnext.get_default_company()
+ applicant = make_employee("test_employee@loan.com", company="_Test Company")
+ company = "_Test Company"
holiday_list = make_holiday("test holiday for loan")
company_doc = frappe.get_doc('Company', company)
@@ -70,16 +72,21 @@
employee_doc.holiday_list = holiday_list
employee_doc.save()
- loan = create_loan(applicant,
- "Personal Loan", 280000, "Repay Over Number of Periods", 20)
+ salary_structure = "Test Salary Structure for Loan"
+ make_salary_structure(salary_structure, "Monthly", employee=employee_doc.name, company="_Test Company")
+
+ loan = create_loan(applicant, "Car Loan", 280000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1))
loan.repay_from_salary = 1
loan.submit()
- salary_structure = "Test Salary Structure for Loan"
- make_salary_structure(salary_structure, "Monthly", employee_doc.name)
+
+ make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1))
+
+ make_accrual_interest_entry_for_term_loans(posting_date=nowdate())
+
dates = get_start_end_dates('Monthly', nowdate())
- make_payroll_entry(start_date=dates.start_date,
- end_date=dates.end_date, branch=branch)
+ make_payroll_entry(company="_Test Company", start_date=dates.start_date,
+ end_date=dates.end_date, branch=branch, cost_center="Main - _TC", payment_account="Cash - _TC")
name = frappe.db.get_value('Salary Slip',
{'posting_date': nowdate(), 'employee': applicant}, 'name')
@@ -109,6 +116,13 @@
payroll_entry.posting_date = nowdate()
payroll_entry.payroll_frequency = "Monthly"
payroll_entry.branch = args.branch or None
+
+ if args.cost_center:
+ payroll_entry.cost_center = args.cost_center
+
+ if args.payment_account:
+ payroll_entry.payment_account = args.payment_account
+
payroll_entry.save()
payroll_entry.create_salary_slips()
payroll_entry.submit_salary_slips()
diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py
index ea531cb..eee7974 100644
--- a/erpnext/hr/doctype/salary_slip/salary_slip.py
+++ b/erpnext/hr/doctype/salary_slip/salary_slip.py
@@ -17,6 +17,7 @@
from erpnext.hr.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period
from erpnext.hr.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount
from erpnext.hr.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits
+from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry
class SalarySlip(TransactionBase):
def __init__(self, *args, **kwargs):
@@ -66,6 +67,7 @@
self.set_status()
self.update_status(self.name)
self.update_salary_slip_in_additional_salary()
+ self.make_loan_repayment_entry()
if (frappe.db.get_single_value("HR Settings", "email_salary_slip_to_employee")) and not frappe.flags.via_payroll_entry:
self.email_salary_slip()
@@ -73,6 +75,7 @@
self.set_status()
self.update_status()
self.update_salary_slip_in_additional_salary()
+ self.cancel_loan_repayment_entry()
def on_trash(self):
from frappe.model.naming import revert_series_if_last
@@ -754,28 +757,35 @@
self.total_principal_amount = 0
for loan in self.get_loan_details():
- self.append('loans', {
- 'loan': loan.name,
- 'total_payment': loan.total_payment,
- 'interest_amount': loan.interest_amount,
- 'principal_amount': loan.principal_amount,
- 'loan_account': loan.loan_account,
- 'interest_income_account': loan.interest_income_account
- })
- self.total_loan_repayment += loan.total_payment
- self.total_interest_amount += loan.interest_amount
- self.total_principal_amount += loan.principal_amount
+ amounts = calculate_amounts(loan.name, self.posting_date, "Regular Payment")
+
+ total_payment = amounts['interest_amount'] + amounts['payable_principal_amount']
+
+ if total_payment:
+ self.append('loans', {
+ 'loan': loan.name,
+ 'total_payment': total_payment,
+ 'interest_amount': amounts['interest_amount'],
+ 'principal_amount': amounts['payable_principal_amount'],
+ 'loan_account': loan.loan_account,
+ 'interest_income_account': loan.interest_income_account
+ })
+
+ self.total_loan_repayment += total_payment
+ self.total_interest_amount += amounts['interest_amount']
+ self.total_principal_amount += amounts['payable_principal_amount']
def get_loan_details(self):
- return frappe.db.sql("""select rps.principal_amount, rps.interest_amount, l.name,
- rps.total_payment, l.loan_account, l.interest_income_account
- from
- `tabRepayment Schedule` as rps, `tabLoan` as l
- where
- l.name = rps.parent and rps.payment_date between %s and %s and
- l.repay_from_salary = 1 and l.docstatus = 1 and l.applicant = %s""",
- (self.start_date, self.end_date, self.employee), as_dict=True) or []
+
+ return frappe.get_all("Loan",
+ fields=["name", "interest_income_account", "loan_account", "loan_type"],
+ filters = {
+ "applicant": self.employee,
+ "docstatus": 1,
+ "repay_from_salary": 1,
+ })
+
def update_salary_slip_in_additional_salary(self):
salary_slip = self.name if self.docstatus==1 else None
@@ -784,6 +794,23 @@
where employee=%s and payroll_date between %s and %s and docstatus=1
""", (salary_slip, self.employee, self.start_date, self.end_date))
+ def make_loan_repayment_entry(self):
+ for loan in self.loans:
+ repayment_entry = create_repayment_entry(loan.loan, self.employee,
+ self.company, self.posting_date, loan.loan_type, "Regular Payment", loan.interest_amount,
+ loan.principal_amount, loan.total_payment)
+
+ repayment_entry.save()
+ repayment_entry.submit()
+
+ loan.loan_repayment_entry = repayment_entry.name
+
+ def cancel_loan_repayment_entry(self):
+ for loan in self.loans:
+ if loan.loan_repayment_entry:
+ repayment_entry = frappe.get_doc("Loan Repayment", loan.loan_repayment_entry)
+ repayment_entry.cancel()
+
def email_salary_slip(self):
receiver = frappe.db.get_value("Employee", self.employee, "prefered_email")
hr_settings = frappe.get_single("HR Settings")
diff --git a/erpnext/hr/doctype/salary_slip/test_salary_slip.py b/erpnext/hr/doctype/salary_slip/test_salary_slip.py
index 16a75f4..9acfd1f 100644
--- a/erpnext/hr/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/hr/doctype/salary_slip/test_salary_slip.py
@@ -18,8 +18,8 @@
class TestSalarySlip(unittest.TestCase):
def setUp(self):
- make_earning_salary_component(setup=True)
- make_deduction_salary_component(setup=True)
+ make_earning_salary_component(setup=True, company_list=["_Test Company"])
+ make_deduction_salary_component(setup=True, company_list=["_Test Company"])
for dt in ["Leave Application", "Leave Allocation", "Salary Slip"]:
frappe.db.sql("delete from `tab%s`" % dt)
@@ -50,7 +50,7 @@
self.assertEqual(ss.deductions[0].amount, 5000)
self.assertEqual(ss.deductions[1].amount, 5000)
self.assertEqual(ss.gross_pay, 78000)
- self.assertEqual(ss.net_pay, 67418.0)
+ self.assertEqual(ss.net_pay, 68000.0)
def test_salary_slip_with_holidays_excluded(self):
no_of_days = self.get_no_of_days()
@@ -70,7 +70,7 @@
self.assertEqual(ss.deductions[0].amount, 5000)
self.assertEqual(ss.deductions[1].amount, 5000)
self.assertEqual(ss.gross_pay, 78000)
- self.assertEqual(ss.net_pay, 67418.0)
+ self.assertEqual(ss.net_pay, 68000.0)
def test_payment_days(self):
no_of_days = self.get_no_of_days()
@@ -137,21 +137,41 @@
make_employee("test_employee@salary.com")
ss = make_employee_salary_slip("test_employee@salary.com", "Monthly")
+ ss.company = "_Test Company"
+ ss.save()
ss.submit()
email_queue = frappe.db.sql("""select name from `tabEmail Queue`""")
self.assertTrue(email_queue)
def test_loan_repayment_salary_slip(self):
- from erpnext.hr.doctype.loan.test_loan import create_loan_type, create_loan
- applicant = make_employee("test_employee@salary.com")
- create_loan_type("Car Loan", 500000, 6.4)
- loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20)
+ from erpnext.loan_management.doctype.loan.test_loan import create_loan_type, create_loan, make_loan_disbursement_entry, create_loan_accounts
+ from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import make_accrual_interest_entry_for_term_loans
+
+ applicant = make_employee("test_loanemployee@salary.com", company="_Test Company")
+
+ create_loan_accounts()
+
+ create_loan_type("Car Loan", 500000, 8.4,
+ is_term_loan=1,
+ mode_of_payment='Cash',
+ payment_account='Payment Account - _TC',
+ loan_account='Loan Account - _TC',
+ interest_income_account='Interest Income Account - _TC',
+ penalty_income_account='Penalty Income Account - _TC')
+
+ loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1))
loan.repay_from_salary = 1
loan.submit()
- ss = make_employee_salary_slip("test_employee@salary.com", "Monthly")
+
+ make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1))
+
+ make_accrual_interest_entry_for_term_loans(posting_date=nowdate())
+
+ ss = make_employee_salary_slip("test_loanemployee@salary.com", "Monthly")
ss.submit()
- self.assertEqual(ss.total_loan_repayment, 582)
+
+ self.assertEqual(ss.total_loan_repayment, 592)
self.assertEqual(ss.net_pay, (flt(ss.gross_pay) - (flt(ss.total_deduction) + flt(ss.total_loan_repayment))))
def test_payroll_frequency(self):
@@ -321,7 +341,7 @@
return salary_slip
-def make_salary_component(salary_components, test_tax):
+def make_salary_component(salary_components, test_tax, company_list=None):
for salary_component in salary_components:
if not frappe.db.exists('Salary Component', salary_component["salary_component"]):
if test_tax:
@@ -336,17 +356,22 @@
salary_component["doctype"] = "Salary Component"
salary_component["salary_component_abbr"] = salary_component["abbr"]
frappe.get_doc(salary_component).insert()
- get_salary_component_account(salary_component["salary_component"])
+ get_salary_component_account(salary_component["salary_component"], company_list)
-def get_salary_component_account(sal_comp):
+def get_salary_component_account(sal_comp, company_list=None):
company = erpnext.get_default_company()
+
+ if company_list and company not in company_list:
+ company_list.append(company)
+
sal_comp = frappe.get_doc("Salary Component", sal_comp)
if not sal_comp.get("accounts"):
- sal_comp.append("accounts", {
- "company": company,
- "default_account": create_account(company)
- })
- sal_comp.save()
+ for d in company_list:
+ sal_comp.append("accounts", {
+ "company": d,
+ "default_account": create_account(d)
+ })
+ sal_comp.save()
def create_account(company):
salary_account = frappe.db.get_value("Account", "Salary - " + frappe.get_cached_value('Company', company, 'abbr'))
@@ -359,7 +384,7 @@
}).insert()
return salary_account
-def make_earning_salary_component(setup=False, test_tax=False):
+def make_earning_salary_component(setup=False, test_tax=False, company_list=None):
data = [
{
"salary_component": 'Basic Salary',
@@ -415,7 +440,7 @@
}
])
if setup or test_tax:
- make_salary_component(data, test_tax)
+ make_salary_component(data, test_tax, company_list)
data.append({
"salary_component": 'Basic Salary',
"abbr":'BS',
@@ -426,7 +451,7 @@
})
return data
-def make_deduction_salary_component(setup=False, test_tax=False):
+def make_deduction_salary_component(setup=False, test_tax=False, company_list=None):
data = [
{
"salary_component": 'Professional Tax',
@@ -458,7 +483,7 @@
"round_to_the_nearest_integer": 1
})
if setup or test_tax:
- make_salary_component(data, test_tax)
+ make_salary_component(data, test_tax, company_list)
return data
diff --git a/erpnext/hr/doctype/salary_slip_loan/salary_slip_loan.json b/erpnext/hr/doctype/salary_slip_loan/salary_slip_loan.json
deleted file mode 100644
index 5d1212b..0000000
--- a/erpnext/hr/doctype/salary_slip_loan/salary_slip_loan.json
+++ /dev/null
@@ -1,263 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-11-08 12:51:12.834479",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "loan",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Loan",
- "length": 0,
- "no_copy": 0,
- "options": "Loan",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "loan_account",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Loan Account",
- "length": 0,
- "no_copy": 0,
- "options": "Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "interest_income_account",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Interest Income Account",
- "length": 0,
- "no_copy": 0,
- "options": "Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_4",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "principal_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Principal Amount",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "interest_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Interest Amount",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "total_payment",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Total Payment",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-02-26 05:24:31.369630",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "Salary Slip Loan",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/salary_structure/test_salary_structure.py b/erpnext/hr/doctype/salary_structure/test_salary_structure.py
index 848c3df..7815094 100644
--- a/erpnext/hr/doctype/salary_structure/test_salary_structure.py
+++ b/erpnext/hr/doctype/salary_structure/test_salary_structure.py
@@ -86,16 +86,17 @@
self.assertEqual(salary_structure_assignment.base, 5000)
self.assertEqual(salary_structure_assignment.variable, 200)
-def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None, test_tax=False):
+def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None,
+ test_tax=False, company=None):
if test_tax:
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure))
if not frappe.db.exists('Salary Structure', salary_structure):
details = {
"doctype": "Salary Structure",
"name": salary_structure,
- "company": erpnext.get_default_company(),
- "earnings": make_earning_salary_component(test_tax=test_tax),
- "deductions": make_deduction_salary_component(test_tax=test_tax),
+ "company": company or erpnext.get_default_company(),
+ "earnings": make_earning_salary_component(test_tax=test_tax, company_list=["_Test Company"]),
+ "deductions": make_deduction_salary_component(test_tax=test_tax, company_list=["_Test Company"]),
"payroll_frequency": payroll_frequency,
"payment_account": get_random("Account")
}
@@ -109,11 +110,11 @@
if employee and not frappe.db.get_value("Salary Structure Assignment",
{'employee':employee, 'docstatus': 1}) and salary_structure_doc.docstatus==1:
- create_salary_structure_assignment(employee, salary_structure)
+ create_salary_structure_assignment(employee, salary_structure, company=company)
return salary_structure_doc
-def create_salary_structure_assignment(employee, salary_structure, from_date=None):
+def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None):
if frappe.db.exists("Salary Structure Assignment", {"employee": employee}):
frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee))
salary_structure_assignment = frappe.new_doc("Salary Structure Assignment")
@@ -122,7 +123,7 @@
salary_structure_assignment.variable = 5000
salary_structure_assignment.from_date = from_date or add_months(nowdate(), -1)
salary_structure_assignment.salary_structure = salary_structure
- salary_structure_assignment.company = erpnext.get_default_company()
+ salary_structure_assignment.company = company or erpnext.get_default_company()
salary_structure_assignment.save(ignore_permissions=True)
salary_structure_assignment.submit()
return salary_structure_assignment
diff --git a/erpnext/hr/loan_common.js b/erpnext/hr/loan_common.js
deleted file mode 100644
index 3e754fa..0000000
--- a/erpnext/hr/loan_common.js
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on(cur_frm.doctype, {
- refresh: function(frm) {
- if (!frappe.boot.active_domains.includes("Non Profit")) {
- frm.set_df_property('applicant_type', 'options', ['Employee']);
- frm.refresh_field('applicant_type');
- }
- },
- applicant_type: function(frm) {
- frm.set_value("applicant", null);
- frm.set_value("applicant_name", null);
- },
- applicant: function(frm) {
- if (frm.doc.applicant) {
- frappe.model.with_doc(frm.doc.applicant_type, frm.doc.applicant, function() {
- var applicant = frappe.model.get_doc(frm.doc.applicant_type, frm.doc.applicant);
- frm.set_value("applicant_name",
- applicant.employee_name || applicant.member_name);
- });
- }
- else {
- frm.set_value("applicant_name", null);
- }
- }
-});
\ No newline at end of file
diff --git a/erpnext/hr/report/loan_repayment/loan_repayment.js b/erpnext/hr/report/loan_repayment/loan_repayment.js
deleted file mode 100644
index 21aa206..0000000
--- a/erpnext/hr/report/loan_repayment/loan_repayment.js
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-/* eslint-disable */
-
-frappe.query_reports["Loan Repayment"] = {
- "filters": [
-
- ]
-}
diff --git a/erpnext/hr/report/loan_repayment/loan_repayment.json b/erpnext/hr/report/loan_repayment/loan_repayment.json
deleted file mode 100644
index b967dfd..0000000
--- a/erpnext/hr/report/loan_repayment/loan_repayment.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "add_total_row": 0,
- "creation": "2019-03-29 18:58:00.166032",
- "disable_prepared_report": 0,
- "disabled": 0,
- "docstatus": 0,
- "doctype": "Report",
- "idx": 0,
- "is_standard": "Yes",
- "letter_head": "",
- "modified": "2019-03-29 18:58:00.166032",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "Loan Repayment",
- "owner": "Administrator",
- "prepared_report": 0,
- "ref_doctype": "Loan",
- "report_name": "Loan Repayment",
- "report_type": "Script Report",
- "roles": [
- {
- "role": "HR Manager"
- },
- {
- "role": "Employee"
- }
- ]
-}
diff --git a/erpnext/hr/report/loan_repayment/loan_repayment.py b/erpnext/hr/report/loan_repayment/loan_repayment.py
deleted file mode 100644
index beca776..0000000
--- a/erpnext/hr/report/loan_repayment/loan_repayment.py
+++ /dev/null
@@ -1,95 +0,0 @@
-# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe import _
-
-def execute(filters=None):
-
- columns = create_columns()
- data = get_record()
- return columns, data
-
-def create_columns():
- return [
- {
- "label": _("Employee"),
- "fieldtype": "Data",
- "fieldname": "employee",
- "options": "Employee",
- "width": 200
- },
- {
- "label": _("Loan"),
- "fieldtype": "Link",
- "fieldname": "loan_name",
- "options": "Loan",
- "width": 200
- },
- {
- "label": _("Loan Amount"),
- "fieldtype": "Currency",
- "fieldname": "loan_amount",
- "options": "currency",
- "width": 100
- },
- {
- "label": _("Interest"),
- "fieldtype": "Data",
- "fieldname": "interest",
- "width": 100
- },
- {
- "label": _("Payable Amount"),
- "fieldtype": "Currency",
- "fieldname": "payable_amount",
- "options": "currency",
- "width": 100
- },
- {
- "label": _("EMI"),
- "fieldtype": "Currency",
- "fieldname": "emi",
- "options": "currency",
- "width": 100
- },
- {
- "label": _("Paid Amount"),
- "fieldtype": "Currency",
- "fieldname": "paid_amount",
- "options": "currency",
- "width": 100
- },
- {
- "label": _("Outstanding Amount"),
- "fieldtype": "Currency",
- "fieldname": "out_amt",
- "options": "currency",
- "width": 100
- },
- ]
-
-def get_record():
- data = []
- loans = frappe.get_all("Loan",
- filters=[("status", "=", "Disbursed")],
- fields=["applicant", "applicant_name", "name", "loan_amount", "rate_of_interest",
- "total_payment", "monthly_repayment_amount", "total_amount_paid"]
- )
-
- for loan in loans:
- row = {
- "employee": loan.applicant + ": " + loan.applicant_name,
- "loan_name": loan.name,
- "loan_amount": loan.loan_amount,
- "interest": str(loan.rate_of_interest) + "%",
- "payable_amount": loan.total_payment,
- "emi": loan.monthly_repayment_amount,
- "paid_amount": loan.total_amount_paid,
- "out_amt": loan.total_payment - loan.total_amount_paid
- }
-
- data.append(row)
-
- return data
diff --git a/erpnext/hr/doctype/loan/__init__.py b/erpnext/loan_management/__init__.py
similarity index 100%
copy from erpnext/hr/doctype/loan/__init__.py
copy to erpnext/loan_management/__init__.py
diff --git a/erpnext/hr/doctype/loan_type/__init__.py b/erpnext/loan_management/doctype/__init__.py
similarity index 100%
copy from erpnext/hr/doctype/loan_type/__init__.py
copy to erpnext/loan_management/doctype/__init__.py
diff --git a/erpnext/hr/doctype/loan/__init__.py b/erpnext/loan_management/doctype/loan/__init__.py
similarity index 100%
rename from erpnext/hr/doctype/loan/__init__.py
rename to erpnext/loan_management/doctype/loan/__init__.py
diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js
new file mode 100644
index 0000000..8b22017
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan/loan.js
@@ -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")
+ }
+});
diff --git a/erpnext/hr/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json
similarity index 69%
rename from erpnext/hr/doctype/loan/loan.json
rename to erpnext/loan_management/doctype/loan/loan.json
index 2b2d481..2834e5b 100644
--- a/erpnext/hr/doctype/loan/loan.json
+++ b/erpnext/loan_management/doctype/loan/loan.json
@@ -2,7 +2,7 @@
"actions": [],
"allow_import": 1,
"autoname": "ACC-LOAN-.YYYY.-.#####",
- "creation": "2016-12-02 10:11:49.673604",
+ "creation": "2019-08-29 17:29:18.176786",
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
@@ -11,32 +11,41 @@
"applicant_type",
"applicant",
"applicant_name",
- "loan_type",
"loan_application",
"column_break_3",
- "posting_date",
"company",
+ "posting_date",
"status",
"repay_from_salary",
"section_break_8",
+ "loan_type",
"loan_amount",
+ "is_secured_loan",
"rate_of_interest",
"disbursement_date",
- "repayment_start_date",
+ "disbursed_amount",
"column_break_11",
+ "is_term_loan",
"repayment_method",
"repayment_periods",
"monthly_repayment_amount",
+ "repayment_start_date",
+ "loan_security_details_section",
+ "loan_security_pledge",
+ "column_break_25",
+ "maximum_loan_value",
"account_info",
"mode_of_payment",
"payment_account",
"column_break_9",
"loan_account",
"interest_income_account",
+ "penalty_income_account",
"section_break_15",
"repayment_schedule",
"section_break_17",
"total_payment",
+ "total_principal_paid",
"column_break_19",
"total_interest_payable",
"total_amount_paid",
@@ -47,7 +56,7 @@
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
- "options": "Employee\nMember",
+ "options": "Employee\nMember\nCustomer",
"reqd": 1
},
{
@@ -75,6 +84,7 @@
"fieldname": "loan_type",
"fieldtype": "Link",
"in_list_view": 1,
+ "in_standard_filter": 1,
"label": "Loan Type",
"options": "Loan Type",
"reqd": 1
@@ -95,6 +105,7 @@
{
"fieldname": "company",
"fieldtype": "Link",
+ "in_standard_filter": 1,
"label": "Company",
"options": "Company",
"remember_last_selected_value": 1,
@@ -104,9 +115,10 @@
"default": "Sanctioned",
"fieldname": "status",
"fieldtype": "Select",
+ "in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
- "options": "Sanctioned\nDisbursed\nRepaid/Closed",
+ "options": "Sanctioned\nPartially Disbursed\nDisbursed\nLoan Closure Requested\nClosed",
"read_only": 1
},
{
@@ -125,8 +137,7 @@
"fieldname": "loan_amount",
"fieldtype": "Currency",
"label": "Loan Amount",
- "options": "Company:company:default_currency",
- "reqd": 1
+ "options": "Company:company:default_currency"
},
{
"fetch_from": "loan_type.rate_of_interest",
@@ -143,29 +154,30 @@
"label": "Disbursement Date"
},
{
+ "depends_on": "is_term_loan",
"fieldname": "repayment_start_date",
"fieldtype": "Date",
- "label": "Repayment Start Date",
- "reqd": 1
+ "label": "Repayment Start Date"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
- "default": "Repay Over Number of Periods",
+ "depends_on": "is_term_loan",
"fieldname": "repayment_method",
"fieldtype": "Select",
"label": "Repayment Method",
- "options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods",
- "reqd": 1
+ "options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods"
},
{
+ "depends_on": "is_term_loan",
"fieldname": "repayment_periods",
"fieldtype": "Int",
"label": "Repayment Period in Months"
},
{
+ "depends_on": "is_term_loan",
"fieldname": "monthly_repayment_amount",
"fieldtype": "Currency",
"label": "Monthly Repayment Amount",
@@ -178,17 +190,21 @@
"label": "Account Info"
},
{
+ "fetch_from": "loan_type.mode_of_payment",
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
"options": "Mode of Payment",
+ "read_only": 1,
"reqd": 1
},
{
+ "fetch_from": "loan_type.payment_account",
"fieldname": "payment_account",
"fieldtype": "Link",
"label": "Payment Account",
"options": "Account",
+ "read_only": 1,
"reqd": 1
},
{
@@ -196,25 +212,31 @@
"fieldtype": "Column Break"
},
{
+ "fetch_from": "loan_type.loan_account",
"fieldname": "loan_account",
"fieldtype": "Link",
"label": "Loan Account",
"options": "Account",
+ "read_only": 1,
"reqd": 1
},
{
+ "fetch_from": "loan_type.interest_income_account",
"fieldname": "interest_income_account",
"fieldtype": "Link",
"label": "Interest Income Account",
"options": "Account",
+ "read_only": 1,
"reqd": 1
},
{
+ "depends_on": "is_term_loan",
"fieldname": "section_break_15",
"fieldtype": "Section Break",
"label": "Repayment Schedule"
},
{
+ "depends_on": "eval:doc.is_term_loan == 1",
"fieldname": "repayment_schedule",
"fieldtype": "Table",
"label": "Repayment Schedule",
@@ -230,7 +252,7 @@
"default": "0",
"fieldname": "total_payment",
"fieldtype": "Currency",
- "label": "Total Payment",
+ "label": "Total Payable Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
@@ -240,6 +262,7 @@
},
{
"default": "0",
+ "depends_on": "is_term_loan",
"fieldname": "total_interest_payable",
"fieldtype": "Currency",
"label": "Total Interest Payable",
@@ -262,13 +285,74 @@
"options": "Loan",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "is_secured_loan",
+ "fieldtype": "Check",
+ "label": "Is Secured Loan"
+ },
+ {
+ "depends_on": "is_secured_loan",
+ "fieldname": "loan_security_details_section",
+ "fieldtype": "Section Break",
+ "label": "Loan Security Details"
+ },
+ {
+ "default": "0",
+ "fetch_from": "loan_type.is_term_loan",
+ "fieldname": "is_term_loan",
+ "fieldtype": "Check",
+ "label": "Is Term Loan",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "loan_type.penalty_income_account",
+ "fieldname": "penalty_income_account",
+ "fieldtype": "Link",
+ "label": "Penalty Income Account",
+ "options": "Account",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "total_principal_paid",
+ "fieldtype": "Currency",
+ "label": "Total Principal Paid",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "loan_security_pledge",
+ "fieldtype": "Link",
+ "label": "Loan Security Pledge",
+ "options": "Loan Security Pledge"
+ },
+ {
+ "fieldname": "disbursed_amount",
+ "fieldtype": "Currency",
+ "label": "Disbursed Amount",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "loan_security_pledge.maximum_loan_value",
+ "fieldname": "maximum_loan_value",
+ "fieldtype": "Currency",
+ "label": "Maximum Loan Value",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_25",
+ "fieldtype": "Column Break"
}
],
"is_submittable": 1,
"links": [],
- "modified": "2019-12-12 14:45:38.823072",
+ "modified": "2020-02-07 01:31:25.172173",
"modified_by": "Administrator",
- "module": "HR",
+ "module": "Loan Management",
"name": "Loan",
"owner": "Administrator",
"permissions": [
@@ -281,7 +365,7 @@
"print": 1,
"read": 1,
"report": 1,
- "role": "HR Manager",
+ "role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
new file mode 100644
index 0000000..696410b
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -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()
+
+
+
diff --git a/erpnext/loan_management/doctype/loan/loan_dashboard.py b/erpnext/loan_management/doctype/loan/loan_dashboard.py
new file mode 100644
index 0000000..90d5ae2
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan/loan_dashboard.py
@@ -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']
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py
new file mode 100644
index 0000000..08c2f22
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan/test_loan.py
@@ -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
\ No newline at end of file
diff --git a/erpnext/hr/doctype/loan_application/__init__.py b/erpnext/loan_management/doctype/loan_application/__init__.py
similarity index 100%
rename from erpnext/hr/doctype/loan_application/__init__.py
rename to erpnext/loan_management/doctype/loan_application/__init__.py
diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.js b/erpnext/loan_management/doctype/loan_application/loan_application.js
new file mode 100644
index 0000000..57050d8
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_application/loan_application.js
@@ -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);
+ },
+})
diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.json b/erpnext/loan_management/doctype/loan_application/loan_application.json
new file mode 100644
index 0000000..4c43302
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_application/loan_application.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py
new file mode 100644
index 0000000..691962b
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_application/loan_application.py
@@ -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
diff --git a/erpnext/hr/doctype/loan_application/loan_application_dashboard.py b/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py
similarity index 77%
rename from erpnext/hr/doctype/loan_application/loan_application_dashboard.py
rename to erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py
index 232c6e3..bf3f58b 100644
--- a/erpnext/hr/doctype/loan_application/loan_application_dashboard.py
+++ b/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py
@@ -6,7 +6,7 @@
'fieldname': 'loan_application',
'transactions': [
{
- 'items': ['Loan']
+ 'items': ['Loan', 'Loan Security Pledge']
},
],
}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_application/test_loan_application.py b/erpnext/loan_management/doctype/loan_application/test_loan_application.py
new file mode 100644
index 0000000..99c807b
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_application/test_loan_application.py
@@ -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)
diff --git a/erpnext/hr/report/loan_repayment/__init__.py b/erpnext/loan_management/doctype/loan_disbursement/__init__.py
similarity index 100%
copy from erpnext/hr/report/loan_repayment/__init__.py
copy to erpnext/loan_management/doctype/loan_disbursement/__init__.py
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.js b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.js
new file mode 100644
index 0000000..487ef23
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.js
@@ -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'
+ }
+ }
+ })
+ }
+});
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
new file mode 100644
index 0000000..72a4ddc
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
new file mode 100644
index 0000000..fa7db2d
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
@@ -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)
diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py
new file mode 100644
index 0000000..968e377
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py
@@ -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))
diff --git a/erpnext/hr/doctype/loan/__init__.py b/erpnext/loan_management/doctype/loan_interest_accrual/__init__.py
similarity index 100%
copy from erpnext/hr/doctype/loan/__init__.py
copy to erpnext/loan_management/doctype/loan_interest_accrual/__init__.py
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.js b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.js
new file mode 100644
index 0000000..177b235
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.js
@@ -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) {
+
+ // }
+});
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json
new file mode 100644
index 0000000..33f496f
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
new file mode 100644
index 0000000..a3442e4
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
@@ -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
+
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py
new file mode 100644
index 0000000..e681ae4
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py
@@ -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))
diff --git a/erpnext/hr/report/loan_repayment/__init__.py b/erpnext/loan_management/doctype/loan_repayment/__init__.py
similarity index 100%
rename from erpnext/hr/report/loan_repayment/__init__.py
rename to erpnext/loan_management/doctype/loan_repayment/__init__.py
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.js b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.js
new file mode 100644
index 0000000..82a2d80
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.js
@@ -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']);
+ }
+ });
+ }
+});
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
new file mode 100644
index 0000000..92e9817
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
new file mode 100644
index 0000000..a70e312
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -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
+
+
+
diff --git a/erpnext/loan_management/doctype/loan_repayment/test_loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/test_loan_repayment.py
new file mode 100644
index 0000000..73585a5
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_repayment/test_loan_repayment.py
@@ -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
diff --git a/erpnext/hr/doctype/loan/__init__.py b/erpnext/loan_management/doctype/loan_security/__init__.py
similarity index 100%
copy from erpnext/hr/doctype/loan/__init__.py
copy to erpnext/loan_management/doctype/loan_security/__init__.py
diff --git a/erpnext/loan_management/doctype/loan_security/loan_security.js b/erpnext/loan_management/doctype/loan_security/loan_security.js
new file mode 100644
index 0000000..0e815af
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security/loan_security.js
@@ -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) {
+
+ // }
+});
diff --git a/erpnext/loan_management/doctype/loan_security/loan_security.json b/erpnext/loan_management/doctype/loan_security/loan_security.json
new file mode 100644
index 0000000..e6984ee
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security/loan_security.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/loan_type/loan_type.py b/erpnext/loan_management/doctype/loan_security/loan_security.py
similarity index 60%
copy from erpnext/hr/doctype/loan_type/loan_type.py
copy to erpnext/loan_management/doctype/loan_security/loan_security.py
index 2714e20..800ad12 100644
--- a/erpnext/hr/doctype/loan_type/loan_type.py
+++ b/erpnext/loan_management/doctype/loan_security/loan_security.py
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
-import frappe
+# import frappe
from frappe.model.document import Document
-class LoanType(Document):
+class LoanSecurity(Document):
pass
diff --git a/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py b/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py
new file mode 100644
index 0000000..878b3fd
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py
@@ -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']
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_security/test_loan_security.py b/erpnext/loan_management/doctype/loan_security/test_loan_security.py
new file mode 100644
index 0000000..24dbc68
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security/test_loan_security.py
@@ -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
diff --git a/erpnext/hr/doctype/loan/__init__.py b/erpnext/loan_management/doctype/loan_security_pledge/__init__.py
similarity index 100%
copy from erpnext/hr/doctype/loan/__init__.py
copy to erpnext/loan_management/doctype/loan_security_pledge/__init__.py
diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js
new file mode 100644
index 0000000..82837b3
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js
@@ -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);
+ },
+});
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json
new file mode 100644
index 0000000..1553844
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py
new file mode 100644
index 0000000..b405cca
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py
@@ -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))
diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge_list.js b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge_list.js
new file mode 100644
index 0000000..174d1b0
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge_list.js
@@ -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];
+ }
+};
diff --git a/erpnext/loan_management/doctype/loan_security_pledge/test_loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/test_loan_security_pledge.py
new file mode 100644
index 0000000..d2347c0
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_pledge/test_loan_security_pledge.py
@@ -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
diff --git a/erpnext/hr/doctype/loan/__init__.py b/erpnext/loan_management/doctype/loan_security_price/__init__.py
similarity index 100%
copy from erpnext/hr/doctype/loan/__init__.py
copy to erpnext/loan_management/doctype/loan_security_price/__init__.py
diff --git a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.js b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.js
new file mode 100644
index 0000000..31b4ec7
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.js
@@ -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) {
+
+ // }
+});
diff --git a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json
new file mode 100644
index 0000000..db260a4
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py
new file mode 100644
index 0000000..2855b52
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py
@@ -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
+
+
+
+
+
+
+
+
+
diff --git a/erpnext/loan_management/doctype/loan_security_price/test_loan_security_price.py b/erpnext/loan_management/doctype/loan_security_price/test_loan_security_price.py
new file mode 100644
index 0000000..2fe0bd5
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_price/test_loan_security_price.py
@@ -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
diff --git a/erpnext/hr/doctype/loan/__init__.py b/erpnext/loan_management/doctype/loan_security_shortfall/__init__.py
similarity index 100%
copy from erpnext/hr/doctype/loan/__init__.py
copy to erpnext/loan_management/doctype/loan_security_shortfall/__init__.py
diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.js b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.js
new file mode 100644
index 0000000..f26c138
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.js
@@ -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);
+ }
+ }
+ });
+ }
+});
diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json
new file mode 100644
index 0000000..102bc0d
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py
new file mode 100644
index 0000000..599f6da
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py
@@ -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()
+
diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/test_loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/test_loan_security_shortfall.py
new file mode 100644
index 0000000..b82f3d2
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_shortfall/test_loan_security_shortfall.py
@@ -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
diff --git a/erpnext/hr/doctype/loan_type/__init__.py b/erpnext/loan_management/doctype/loan_security_type/__init__.py
similarity index 100%
copy from erpnext/hr/doctype/loan_type/__init__.py
copy to erpnext/loan_management/doctype/loan_security_type/__init__.py
diff --git a/erpnext/loan_management/doctype/loan_security_type/loan_security_type.js b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.js
new file mode 100644
index 0000000..3a1e068
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.js
@@ -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) {
+
+ // },
+});
diff --git a/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json
new file mode 100644
index 0000000..a5ab057
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_security_type/loan_security_type.py b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.py
new file mode 100644
index 0000000..cb8a50a
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.py
@@ -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
diff --git a/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py b/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py
new file mode 100644
index 0000000..ac33589
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py
@@ -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']
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_security_type/test_loan_security_type.py b/erpnext/loan_management/doctype/loan_security_type/test_loan_security_type.py
new file mode 100644
index 0000000..f7d845a
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_type/test_loan_security_type.py
@@ -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
diff --git a/erpnext/hr/doctype/loan_type/__init__.py b/erpnext/loan_management/doctype/loan_security_unpledge/__init__.py
similarity index 100%
copy from erpnext/hr/doctype/loan_type/__init__.py
copy to erpnext/loan_management/doctype/loan_security_unpledge/__init__.py
diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js
new file mode 100644
index 0000000..72c5f38
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js
@@ -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"]]]
+ };
+ });
+ }
+});
diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json
new file mode 100644
index 0000000..ba94855
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
new file mode 100644
index 0000000..02b1ecb
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
@@ -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')
diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge_list.js b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge_list.js
new file mode 100644
index 0000000..196ebbb
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge_list.js
@@ -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];
+ }
+};
diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/test_loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/test_loan_security_unpledge.py
new file mode 100644
index 0000000..5b5c205
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_security_unpledge/test_loan_security_unpledge.py
@@ -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
diff --git a/erpnext/hr/doctype/loan_type/__init__.py b/erpnext/loan_management/doctype/loan_type/__init__.py
similarity index 100%
rename from erpnext/hr/doctype/loan_type/__init__.py
rename to erpnext/loan_management/doctype/loan_type/__init__.py
diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.js b/erpnext/loan_management/doctype/loan_type/loan_type.js
new file mode 100644
index 0000000..04c89c4
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_type/loan_type.js
@@ -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
+ }
+ };
+ });
+ });
+ }
+});
diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json
new file mode 100644
index 0000000..a3525db
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_type/loan_type.json
@@ -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"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.py b/erpnext/loan_management/doctype/loan_type/loan_type.py
new file mode 100644
index 0000000..14b18ab
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_type/loan_type.py
@@ -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)))
+
diff --git a/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py b/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py
new file mode 100644
index 0000000..58c6689
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py
@@ -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']
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_type/test_loan_type.py b/erpnext/loan_management/doctype/loan_type/test_loan_type.py
new file mode 100644
index 0000000..5877ab6
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_type/test_loan_type.py
@@ -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
diff --git a/erpnext/hr/doctype/loan_type/__init__.py b/erpnext/loan_management/doctype/pledge/__init__.py
similarity index 100%
copy from erpnext/hr/doctype/loan_type/__init__.py
copy to erpnext/loan_management/doctype/pledge/__init__.py
diff --git a/erpnext/loan_management/doctype/pledge/pledge.js b/erpnext/loan_management/doctype/pledge/pledge.js
new file mode 100644
index 0000000..fb6ab10
--- /dev/null
+++ b/erpnext/loan_management/doctype/pledge/pledge.js
@@ -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) {
+
+ // }
+});
diff --git a/erpnext/loan_management/doctype/pledge/pledge.json b/erpnext/loan_management/doctype/pledge/pledge.json
new file mode 100644
index 0000000..f22a21e
--- /dev/null
+++ b/erpnext/loan_management/doctype/pledge/pledge.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/loan_type/loan_type.py b/erpnext/loan_management/doctype/pledge/pledge.py
similarity index 61%
rename from erpnext/hr/doctype/loan_type/loan_type.py
rename to erpnext/loan_management/doctype/pledge/pledge.py
index 2714e20..0457ad7 100644
--- a/erpnext/hr/doctype/loan_type/loan_type.py
+++ b/erpnext/loan_management/doctype/pledge/pledge.py
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
-import frappe
+# import frappe
from frappe.model.document import Document
-class LoanType(Document):
+class Pledge(Document):
pass
diff --git a/erpnext/loan_management/doctype/pledge/test_pledge.py b/erpnext/loan_management/doctype/pledge/test_pledge.py
new file mode 100644
index 0000000..2e01dc1
--- /dev/null
+++ b/erpnext/loan_management/doctype/pledge/test_pledge.py
@@ -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
diff --git a/erpnext/hr/doctype/loan/__init__.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/__init__.py
similarity index 100%
copy from erpnext/hr/doctype/loan/__init__.py
copy to erpnext/loan_management/doctype/process_loan_interest_accrual/__init__.py
diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.js b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.js
new file mode 100644
index 0000000..c596be2
--- /dev/null
+++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.js
@@ -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) {
+
+ // }
+});
diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json
new file mode 100644
index 0000000..7f79cb1
--- /dev/null
+++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py
new file mode 100644
index 0000000..0f33da9
--- /dev/null
+++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py
@@ -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()
+
diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py
new file mode 100644
index 0000000..243a7a3
--- /dev/null
+++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py
@@ -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']
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/test_process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/test_process_loan_interest_accrual.py
new file mode 100644
index 0000000..6bfd3f4
--- /dev/null
+++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/test_process_loan_interest_accrual.py
@@ -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
diff --git a/erpnext/hr/doctype/loan/__init__.py b/erpnext/loan_management/doctype/process_loan_security_shortfall/__init__.py
similarity index 100%
copy from erpnext/hr/doctype/loan/__init__.py
copy to erpnext/loan_management/doctype/process_loan_security_shortfall/__init__.py
diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.js b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.js
new file mode 100644
index 0000000..645e3ad
--- /dev/null
+++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.js
@@ -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());
+ }
+});
diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json
new file mode 100644
index 0000000..ffc3671
--- /dev/null
+++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py
new file mode 100644
index 0000000..417e367
--- /dev/null
+++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py
@@ -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)
diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py
new file mode 100644
index 0000000..dc9bd81
--- /dev/null
+++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py
@@ -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']
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/test_process_loan_security_shortfall.py b/erpnext/loan_management/doctype/process_loan_security_shortfall/test_process_loan_security_shortfall.py
new file mode 100644
index 0000000..cd379a1
--- /dev/null
+++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/test_process_loan_security_shortfall.py
@@ -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
diff --git a/erpnext/hr/doctype/loan_type/__init__.py b/erpnext/loan_management/doctype/proposed_pledge/__init__.py
similarity index 100%
copy from erpnext/hr/doctype/loan_type/__init__.py
copy to erpnext/loan_management/doctype/proposed_pledge/__init__.py
diff --git a/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json
new file mode 100644
index 0000000..aee7c2c
--- /dev/null
+++ b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.py b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.py
new file mode 100644
index 0000000..dfa5c79
--- /dev/null
+++ b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.py
@@ -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
diff --git a/erpnext/hr/doctype/repayment_schedule/__init__.py b/erpnext/loan_management/doctype/repayment_schedule/__init__.py
similarity index 100%
rename from erpnext/hr/doctype/repayment_schedule/__init__.py
rename to erpnext/loan_management/doctype/repayment_schedule/__init__.py
diff --git a/erpnext/hr/doctype/repayment_schedule/repayment_schedule.json b/erpnext/loan_management/doctype/repayment_schedule/repayment_schedule.json
similarity index 88%
rename from erpnext/hr/doctype/repayment_schedule/repayment_schedule.json
rename to erpnext/loan_management/doctype/repayment_schedule/repayment_schedule.json
index 5bb2d37..7f71beb 100644
--- a/erpnext/hr/doctype/repayment_schedule/repayment_schedule.json
+++ b/erpnext/loan_management/doctype/repayment_schedule/repayment_schedule.json
@@ -1,5 +1,5 @@
{
- "creation": "2016-12-20 15:32:25.078334",
+ "creation": "2019-09-12 12:57:07.940159",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
@@ -9,11 +9,10 @@
"interest_amount",
"total_payment",
"balance_loan_amount",
- "paid"
+ "is_accrued"
],
"fields": [
{
- "allow_on_submit": 1,
"columns": 2,
"fieldname": "payment_date",
"fieldtype": "Date",
@@ -61,17 +60,17 @@
},
{
"default": "0",
- "fieldname": "paid",
+ "fieldname": "is_accrued",
"fieldtype": "Check",
"in_list_view": 1,
- "label": "Paid",
+ "label": "Is Accrued",
"read_only": 1
}
],
"istable": 1,
- "modified": "2019-10-29 11:45:10.694557",
+ "modified": "2019-09-12 12:57:07.940159",
"modified_by": "Administrator",
- "module": "HR",
+ "module": "Loan Management",
"name": "Repayment Schedule",
"owner": "Administrator",
"permissions": [],
diff --git a/erpnext/hr/doctype/repayment_schedule/repayment_schedule.py b/erpnext/loan_management/doctype/repayment_schedule/repayment_schedule.py
similarity index 71%
rename from erpnext/hr/doctype/repayment_schedule/repayment_schedule.py
rename to erpnext/loan_management/doctype/repayment_schedule/repayment_schedule.py
index 8abee5e..2aa27b0 100644
--- a/erpnext/hr/doctype/repayment_schedule/repayment_schedule.py
+++ b/erpnext/loan_management/doctype/repayment_schedule/repayment_schedule.py
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
-import frappe
+# import frappe
from frappe.model.document import Document
class RepaymentSchedule(Document):
diff --git a/erpnext/hr/doctype/salary_slip_loan/__init__.py b/erpnext/loan_management/doctype/salary_slip_loan/__init__.py
similarity index 100%
rename from erpnext/hr/doctype/salary_slip_loan/__init__.py
rename to erpnext/loan_management/doctype/salary_slip_loan/__init__.py
diff --git a/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json
new file mode 100644
index 0000000..ce020ff
--- /dev/null
+++ b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/salary_slip_loan/salary_slip_loan.py b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.py
similarity index 71%
rename from erpnext/hr/doctype/salary_slip_loan/salary_slip_loan.py
rename to erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.py
index 83908ce..9ee0b96 100644
--- a/erpnext/hr/doctype/salary_slip_loan/salary_slip_loan.py
+++ b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.py
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
-import frappe
+# import frappe
from frappe.model.document import Document
class SalarySlipLoan(Document):
diff --git a/erpnext/hr/report/loan_repayment/__init__.py b/erpnext/loan_management/doctype/sanctioned_loan_amount/__init__.py
similarity index 100%
copy from erpnext/hr/report/loan_repayment/__init__.py
copy to erpnext/loan_management/doctype/sanctioned_loan_amount/__init__.py
diff --git a/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.js b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.js
new file mode 100644
index 0000000..5361e7c
--- /dev/null
+++ b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.js
@@ -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) {
+
+ // }
+});
diff --git a/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.json b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.json
new file mode 100644
index 0000000..0447cd9
--- /dev/null
+++ b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py
new file mode 100644
index 0000000..74a1310
--- /dev/null
+++ b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py
@@ -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)
+ ))
diff --git a/erpnext/loan_management/doctype/sanctioned_loan_amount/test_sanctioned_loan_amount.py b/erpnext/loan_management/doctype/sanctioned_loan_amount/test_sanctioned_loan_amount.py
new file mode 100644
index 0000000..ba1372f
--- /dev/null
+++ b/erpnext/loan_management/doctype/sanctioned_loan_amount/test_sanctioned_loan_amount.py
@@ -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
diff --git a/erpnext/hr/doctype/loan/__init__.py b/erpnext/loan_management/doctype/unpledge/__init__.py
similarity index 100%
copy from erpnext/hr/doctype/loan/__init__.py
copy to erpnext/loan_management/doctype/unpledge/__init__.py
diff --git a/erpnext/loan_management/doctype/unpledge/unpledge.json b/erpnext/loan_management/doctype/unpledge/unpledge.json
new file mode 100644
index 0000000..9e6277d
--- /dev/null
+++ b/erpnext/loan_management/doctype/unpledge/unpledge.json
@@ -0,0 +1,84 @@
+{
+ "creation": "2019-09-21 13:22:19.793797",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "loan_security",
+ "against_pledge",
+ "loan_security_type",
+ "loan_security_code",
+ "haircut",
+ "uom",
+ "column_break_5",
+ "qty"
+ ],
+ "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": "column_break_5",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Quantity",
+ "reqd": 1
+ },
+ {
+ "fieldname": "against_pledge",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Against Pledge",
+ "options": "Loan Security Pledge",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "loan_security.haircut",
+ "fieldname": "haircut",
+ "fieldtype": "Percent",
+ "label": "Haircut",
+ "read_only": 1
+ }
+ ],
+ "istable": 1,
+ "modified": "2019-10-02 12:48:18.588236",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Unpledge",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/loan_type/loan_type.py b/erpnext/loan_management/doctype/unpledge/unpledge.py
similarity index 61%
copy from erpnext/hr/doctype/loan_type/loan_type.py
copy to erpnext/loan_management/doctype/unpledge/unpledge.py
index 2714e20..205230a 100644
--- a/erpnext/hr/doctype/loan_type/loan_type.py
+++ b/erpnext/loan_management/doctype/unpledge/unpledge.py
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
-import frappe
+# import frappe
from frappe.model.document import Document
-class LoanType(Document):
+class Unpledge(Document):
pass
diff --git a/erpnext/loan_management/loan_common.js b/erpnext/loan_management/loan_common.js
new file mode 100644
index 0000000..3a47a88
--- /dev/null
+++ b/erpnext/loan_management/loan_common.js
@@ -0,0 +1,40 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on(cur_frm.doctype, {
+ refresh: function(frm) {
+ if (!frappe.boot.active_domains.includes("Non Profit")) {
+ frm.set_df_property('applicant_type', 'options', ['Employee', 'Customer']);
+ frm.refresh_field('applicant_type');
+ }
+
+ if (['Loan Disbursement', 'Loan Repayment', 'Loan Interest Accrual'].includes(frm.doc.doctype)
+ && frm.doc.docstatus == 1) {
+
+ frm.add_custom_button(__("Accounting Ledger"), function() {
+ frappe.route_options = {
+ voucher_no: frm.doc.name,
+ company: frm.doc.company
+ };
+
+ frappe.set_route("query-report", "General Ledger");
+ },__("View"));
+ }
+ },
+ applicant: function(frm) {
+ if (!["Loan Application", "Loan"].includes(frm.doc.doctype)) {
+ return;
+ }
+
+ if (frm.doc.applicant) {
+ frappe.model.with_doc(frm.doc.applicant_type, frm.doc.applicant, function() {
+ var applicant = frappe.model.get_doc(frm.doc.applicant_type, frm.doc.applicant);
+ frm.set_value("applicant_name",
+ applicant.employee_name || applicant.member_name);
+ });
+ }
+ else {
+ frm.set_value("applicant_name", null);
+ }
+ }
+});
\ No newline at end of file
diff --git a/erpnext/hr/doctype/loan/__init__.py b/erpnext/loan_management/report/__init__.py
similarity index 100%
copy from erpnext/hr/doctype/loan/__init__.py
copy to erpnext/loan_management/report/__init__.py
diff --git a/erpnext/hr/doctype/loan_type/__init__.py b/erpnext/loan_management/report/loan_repayment_and_closure/__init__.py
similarity index 100%
copy from erpnext/hr/doctype/loan_type/__init__.py
copy to erpnext/loan_management/report/loan_repayment_and_closure/__init__.py
diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.js b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.js
new file mode 100644
index 0000000..ed5e937
--- /dev/null
+++ b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.js
@@ -0,0 +1,41 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Loan Repayment and Closure"] = {
+ "filters": [
+ {
+ "fieldname":"company",
+ "label": __("Company"),
+ "fieldtype": "Link",
+ "options": "Company",
+ "reqd": 1,
+ "default": frappe.defaults.get_user_default("Company")
+ },
+ {
+ "fieldname":"applicant_type",
+ "label": __("Applicant Type"),
+ "fieldtype": "Select",
+ "options": ["Customer", "Employee"],
+ "reqd": 1,
+ "default": "Customer",
+ on_change: function() {
+ frappe.query_report.set_filter_value('applicant', "");
+ }
+ },
+ {
+ "fieldname": "applicant",
+ "label": __("Applicant"),
+ "fieldtype": "Dynamic Link",
+ "get_options": function() {
+ var applicant_type = frappe.query_report.get_filter_value('applicant_type');
+ var applicant = frappe.query_report.get_filter_value('applicant');
+ if(applicant && !applicant_type) {
+ frappe.throw(__("Please select Applicant Type first"));
+ }
+ return applicant_type;
+ }
+
+ },
+ ]
+};
diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.json b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.json
new file mode 100644
index 0000000..52d5b2c
--- /dev/null
+++ b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.json
@@ -0,0 +1,27 @@
+{
+ "add_total_row": 0,
+ "creation": "2019-09-03 16:54:55.616593",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2020-02-25 07:16:47.696994",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Loan Repayment and Closure",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Loan Repayment",
+ "report_name": "Loan Repayment and Closure",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "System Manager"
+ },
+ {
+ "role": "Loan Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py
new file mode 100644
index 0000000..b63cc8e
--- /dev/null
+++ b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py
@@ -0,0 +1,129 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+
+def execute(filters=None):
+ columns = get_columns()
+ data = get_data(filters)
+ return columns, data
+
+def get_columns():
+ return [
+ {
+ "label": _("Posting Date"),
+ "fieldtype": "Date",
+ "fieldname": "posting_date",
+ "width": 100
+ },
+ {
+ "label": _("Loan Repayment"),
+ "fieldtype": "Link",
+ "fieldname": "loan_repayment",
+ "options": "Loan Repayment",
+ "width": 100
+ },
+ {
+ "label": _("Against Loan"),
+ "fieldtype": "Link",
+ "fieldname": "against_loan",
+ "options": "Loan",
+ "width": 200
+ },
+ {
+ "label": _("Applicant"),
+ "fieldtype": "Data",
+ "fieldname": "applicant",
+ "width": 150
+ },
+ {
+ "label": _("Payment Type"),
+ "fieldtype": "Data",
+ "fieldname": "payment_type",
+ "width": 150
+ },
+ {
+ "label": _("Principal Amount"),
+ "fieldtype": "Currency",
+ "fieldname": "principal_amount",
+ "options": "currency",
+ "width": 100
+ },
+ {
+ "label": _("Interest Amount"),
+ "fieldtype": "Currency",
+ "fieldname": "interest",
+ "options": "currency",
+ "width": 100
+ },
+ {
+ "label": _("Penalty Amount"),
+ "fieldtype": "Currency",
+ "fieldname": "penalty",
+ "options": "currency",
+ "width": 100
+ },
+ {
+ "label": _("Payable Amount"),
+ "fieldtype": "Currency",
+ "fieldname": "payable_amount",
+ "options": "currency",
+ "width": 100
+ },
+ {
+ "label": _("Paid Amount"),
+ "fieldtype": "Currency",
+ "fieldname": "paid_amount",
+ "options": "currency",
+ "width": 100
+ },
+ {
+ "label": _("Currency"),
+ "fieldtype": "Link",
+ "fieldname": "currency",
+ "options": "Currency",
+ "width": 100
+ }
+ ]
+
+def get_data(filters):
+ data = []
+
+ query_filters = {
+ "docstatus": 1,
+ "company": filters.get('company'),
+ }
+
+ if filters.get('applicant'):
+ query_filters.update({
+ "applicant": filters.get('applicant')
+ })
+
+ loan_repayments = frappe.get_all("Loan Repayment",
+ filters = query_filters,
+ fields=["posting_date", "applicant", "name", "against_loan", "payment_type", "payable_amount",
+ "pending_principal_amount", "interest_payable", "penalty_amount", "amount_paid"]
+ )
+
+ default_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
+
+ for repayment in loan_repayments:
+ row = {
+ "posting_date": repayment.posting_date,
+ "loan_repayment": repayment.name,
+ "applicant": repayment.applicant,
+ "payment_type": repayment.payment_type,
+ "against_loan": repayment.against_loan,
+ "principal_amount": repayment.pending_principal_amount,
+ "interest": repayment.interest_payable,
+ "penalty": repayment.penalty_amount,
+ "payable_amount": repayment.payable_amount,
+ "paid_amount": repayment.amount_paid,
+ "currency": default_currency
+ }
+
+ data.append(row)
+
+ return data
\ No newline at end of file
diff --git a/erpnext/hr/doctype/loan/__init__.py b/erpnext/loan_management/report/loan_security_status/__init__.py
similarity index 100%
copy from erpnext/hr/doctype/loan/__init__.py
copy to erpnext/loan_management/report/loan_security_status/__init__.py
diff --git a/erpnext/loan_management/report/loan_security_status/loan_security_status.js b/erpnext/loan_management/report/loan_security_status/loan_security_status.js
new file mode 100644
index 0000000..6e6191c
--- /dev/null
+++ b/erpnext/loan_management/report/loan_security_status/loan_security_status.js
@@ -0,0 +1,46 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Loan Security Status"] = {
+ "filters": [
+ {
+ "fieldname":"company",
+ "label": __("Company"),
+ "fieldtype": "Link",
+ "options": "Company",
+ "reqd": 1,
+ "default": frappe.defaults.get_user_default("Company")
+ },
+ {
+ "fieldname":"applicant_type",
+ "label": __("Applicant Type"),
+ "fieldtype": "Select",
+ "options": ["Customer", "Employee"],
+ "reqd": 1,
+ "default": "Customer",
+ on_change: function() {
+ frappe.query_report.set_filter_value('applicant', "");
+ }
+ },
+ {
+ "fieldname": "applicant",
+ "label": __("Applicant"),
+ "fieldtype": "Dynamic Link",
+ "get_options": function() {
+ var applicant_type = frappe.query_report.get_filter_value('applicant_type');
+ var applicant = frappe.query_report.get_filter_value('applicant');
+ if(applicant && !applicant_type) {
+ frappe.throw(__("Please select Applicant Type first"));
+ }
+ return applicant_type;
+ }
+ },
+ {
+ "fieldname":"pledge_status",
+ "label": __("Pledge Status"),
+ "fieldtype": "Select",
+ "options": ["", "Requested", "Pledged", "Partially Pledged", "Unpledged"],
+ },
+ ]
+};
diff --git a/erpnext/loan_management/report/loan_security_status/loan_security_status.json b/erpnext/loan_management/report/loan_security_status/loan_security_status.json
new file mode 100644
index 0000000..9eb948d
--- /dev/null
+++ b/erpnext/loan_management/report/loan_security_status/loan_security_status.json
@@ -0,0 +1,27 @@
+{
+ "add_total_row": 1,
+ "creation": "2019-10-07 05:57:33.435705",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2019-10-07 13:45:46.793949",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Loan Security Status",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Loan Security",
+ "report_name": "Loan Security Status",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "System Manager"
+ },
+ {
+ "role": "Loan Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/report/loan_security_status/loan_security_status.py b/erpnext/loan_management/report/loan_security_status/loan_security_status.py
new file mode 100644
index 0000000..ea6a2ee6
--- /dev/null
+++ b/erpnext/loan_management/report/loan_security_status/loan_security_status.py
@@ -0,0 +1,135 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+
+def execute(filters=None):
+ columns = get_columns(filters)
+ data = get_data(filters)
+ return columns, data
+
+def get_columns(filters):
+ columns= [
+ {
+ "label": _("Loan Security Pledge"),
+ "fieldtype": "Link",
+ "fieldname": "loan_security_pledge",
+ "options": "Loan Security Pledge",
+ "width": 200
+ },
+ {
+ "label": _("Loan"),
+ "fieldtype": "Link",
+ "fieldname": "loan",
+ "options": "Loan",
+ "width": 200
+ },
+ {
+ "label": _("Applicant"),
+ "fieldtype": "Data",
+ "fieldname": "applicant",
+ "width": 200
+ },
+ {
+ "label": _("Status"),
+ "fieldtype": "Data",
+ "fieldname": "status",
+ "width": 100
+ },
+ {
+ "label": _("Pledge Time"),
+ "fieldtype": "Data",
+ "fieldname": "pledge_time",
+ "width": 150
+ },
+ {
+ "label": _("Loan Security"),
+ "fieldtype": "Link",
+ "fieldname": "loan_security",
+ "options": "Loan Security",
+ "width": 150
+ },
+ {
+ "label": _("Quantity"),
+ "fieldtype": "Float",
+ "fieldname": "qty",
+ "width": 100
+ },
+ {
+ "label": _("Loan Security Price"),
+ "fieldtype": "Currency",
+ "fieldname": "loan_security_price",
+ "options": "currency",
+ "width": 200
+ },
+ {
+ "label": _("Loan Security Value"),
+ "fieldtype": "Currency",
+ "fieldname": "loan_security_value",
+ "options": "currency",
+ "width": 200
+ },
+ {
+ "label": _("Currency"),
+ "fieldtype": "Link",
+ "fieldname": "currency",
+ "options": "Currency",
+ "width": 50
+ }
+ ]
+
+ return columns
+
+def get_data(filters):
+
+ loan_security_price_map = frappe._dict(frappe.get_all("Loan Security",
+ fields=["name", "loan_security_price"], as_list=1
+ ))
+
+ data = []
+ conditions = get_conditions(filters)
+
+ loan_security_pledges = frappe.db.sql("""
+ SELECT
+ p.name, p.applicant, p.loan, p.status, p.pledge_time,
+ c.loan_security, c.qty
+ FROM
+ `tabLoan Security Pledge` p, `tabPledge` c
+ WHERE
+ p.docstatus = 1
+ AND c.parent = p.name
+ AND p.company = %(company)s
+ {conditions}
+ """.format(conditions = conditions), (filters), as_dict=1) #nosec
+
+ default_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
+
+ for pledge in loan_security_pledges:
+ row = {}
+ row["loan_security_pledge"] = pledge.name
+ row["loan"] = pledge.loan
+ row["applicant"] = pledge.applicant
+ row["status"] = pledge.status
+ row["pledge_time"] = pledge.pledge_time
+ row["loan_security"] = pledge.loan_security
+ row["qty"] = pledge.qty
+ row["loan_security_price"] = loan_security_price_map.get(pledge.loan_security)
+ row["loan_security_value"] = row["loan_security_price"] * pledge.qty
+ row["currency"] = default_currency
+
+ data.append(row)
+
+ return data
+
+def get_conditions(filters):
+ conditions = []
+
+ if filters.get("applicant"):
+ conditions.append("p.applicant = %(applicant)s")
+
+ if filters.get("pledge_status"):
+ conditions.append(" p.status = %(pledge_status)s")
+
+ return "AND {}".format(" AND ".join(conditions)) if conditions else ""
diff --git a/erpnext/modules.txt b/erpnext/modules.txt
index 316d6de..3b34758 100644
--- a/erpnext/modules.txt
+++ b/erpnext/modules.txt
@@ -23,4 +23,5 @@
Hotels
Hub Node
Quality Management
-Communication
\ No newline at end of file
+Communication
+Loan Management
\ No newline at end of file
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 42d537a..b0fc7ea 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -510,7 +510,6 @@
erpnext.patches.v11_0.merge_land_unit_with_location
erpnext.patches.v11_0.add_index_on_nestedset_doctypes
erpnext.patches.v11_0.remove_modules_setup_page
-erpnext.patches.v11_0.rename_employee_loan_to_loan
erpnext.patches.v11_0.move_leave_approvers_from_employee #13-06-2018
erpnext.patches.v11_0.update_department_lft_rgt
erpnext.patches.v11_0.add_default_email_template_for_leave
@@ -634,6 +633,7 @@
erpnext.patches.v12_0.add_default_dashboards
erpnext.patches.v12_0.remove_bank_remittance_custom_fields
erpnext.patches.v12_0.generate_leave_ledger_entries
+execute:frappe.delete_doc_if_exists("Report", "Loan Repayment")
erpnext.patches.v12_0.move_credit_limit_to_customer_credit_limit
erpnext.patches.v12_0.add_variant_of_in_item_attribute_table
erpnext.patches.v12_0.rename_bank_account_field_in_journal_entry_account
diff --git a/erpnext/patches/v11_0/rename_employee_loan_to_loan.py b/erpnext/patches/v11_0/rename_employee_loan_to_loan.py
deleted file mode 100644
index b2ff6b8..0000000
--- a/erpnext/patches/v11_0/rename_employee_loan_to_loan.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe.model.utils.rename_field import rename_field
-
-def execute():
- if frappe.db.table_exists("Employee Loan Application") and not frappe.db.table_exists("Loan Application"):
- frappe.rename_doc("DocType", "Employee Loan Application", "Loan Application", force=True)
-
- if frappe.db.table_exists("Employee Loan") and not frappe.db.table_exists("Loan"):
- frappe.rename_doc("DocType", "Employee Loan", "Loan", force=True)
-
- frappe.reload_doc("hr", "doctype", "loan_application")
- frappe.reload_doc("hr", "doctype", "loan")
- frappe.reload_doc("hr", "doctype", "salary_slip_loan")
-
- for doctype in ['Loan', 'Salary Slip Loan']:
- if frappe.db.has_column(doctype, 'employee_loan_account'):
- rename_field(doctype, "employee_loan_account", "loan_account")
-
- columns = {'employee': 'applicant', 'employee_name': 'applicant_name'}
- for doctype in ['Loan Application', 'Loan']:
- frappe.db.sql(""" update `tab{doctype}` set applicant_type = 'Employee' """
- .format(doctype=doctype))
- for column, new_column in columns.items():
- if frappe.db.has_column(doctype, column):
- rename_field(doctype, column, new_column)
-
- frappe.delete_doc('DocType', doctype)
\ No newline at end of file