Merge branch 'develop' into fix-payroll-attendance
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 4b59887..af6c696 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -1401,6 +1401,7 @@
def set_missing_values(source, target):
target.ignore_pricing_rule = 1
target.run_method("set_missing_values")
+ target.run_method("set_po_nos")
target.run_method("calculate_taxes_and_totals")
def update_item(source_doc, target_doc, source_parent):
diff --git a/erpnext/communication/doctype/communication_medium/communication_medium.json b/erpnext/communication/doctype/communication_medium/communication_medium.json
index f009b38..1e1fe3b 100644
--- a/erpnext/communication/doctype/communication_medium/communication_medium.json
+++ b/erpnext/communication/doctype/communication_medium/communication_medium.json
@@ -1,12 +1,14 @@
{
+ "actions": [],
"autoname": "Prompt",
"creation": "2019-06-05 11:48:30.572795",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
+ "communication_channel",
"communication_medium_type",
- "catch_all",
"column_break_3",
+ "catch_all",
"provider",
"disabled",
"timeslots_section",
@@ -54,9 +56,16 @@
"fieldtype": "Table",
"label": "Timeslots",
"options": "Communication Medium Timeslot"
+ },
+ {
+ "fieldname": "communication_channel",
+ "fieldtype": "Select",
+ "label": "Communication Channel",
+ "options": "\nExotel"
}
],
- "modified": "2019-06-05 11:49:30.769006",
+ "links": [],
+ "modified": "2020-10-27 16:22:08.068542",
"modified_by": "Administrator",
"module": "Communication",
"name": "Communication Medium",
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 5886171..7504746 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -371,13 +371,27 @@
self.make_sl_entries(sl_entries)
def set_po_nos(self):
- if self.doctype in ("Delivery Note", "Sales Invoice") and hasattr(self, "items"):
- ref_fieldname = "against_sales_order" if self.doctype == "Delivery Note" else "sales_order"
- sales_orders = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)]))
- if sales_orders:
- po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)})
- if po_nos and po_nos[0].get('po_no'):
- self.po_no = ', '.join(list(set([d.po_no for d in po_nos if d.po_no])))
+ if self.doctype == 'Sales Invoice' and hasattr(self, "items"):
+ self.set_pos_for_sales_invoice()
+ if self.doctype == 'Delivery Note' and hasattr(self, "items"):
+ self.set_pos_for_delivery_note()
+
+ def set_pos_for_sales_invoice(self):
+ po_nos = []
+ self.get_po_nos('Sales Order', 'sales_order', po_nos)
+ self.get_po_nos('Delivery Note', 'delivery_note', po_nos)
+ self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(','))))
+
+ def set_pos_for_delivery_note(self):
+ po_nos = []
+ self.get_po_nos('Sales Order', 'against_sales_order', po_nos)
+ self.get_po_nos('Sales Invoice', 'against_sales_invoice', po_nos)
+ self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(','))))
+
+ def get_po_nos(self, ref_doctype, ref_fieldname, po_nos):
+ doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)]))
+ if doc_list:
+ po_nos += [d.po_no for d in frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) if d.get('po_no')]
def set_gross_profit(self):
if self.doctype in ["Sales Order", "Quotation"]:
diff --git a/erpnext/loan_management/desk_page/loan/loan.json b/erpnext/loan_management/desk_page/loan/loan.json
index 3bdd1ce..fc59c19 100644
--- a/erpnext/loan_management/desk_page/loan/loan.json
+++ b/erpnext/loan_management/desk_page/loan/loan.json
@@ -3,7 +3,7 @@
{
"hidden": 0,
"label": "Loan",
- "links": "[\n {\n \"description\": \"Loan Type for interest and penalty rates\",\n \"label\": \"Loan Type\",\n \"name\": \"Loan Type\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loan Applications from customers and employees.\",\n \"label\": \"Loan Application\",\n \"name\": \"Loan Application\",\n \"type\": \"doctype\"\n },\n { \"dependencies\": [\n \"Loan Type\"\n ],\n \"description\": \"Loans provided to customers and employees.\",\n \"label\": \"Loan\",\n \"name\": \"Loan\",\n \"type\": \"doctype\"\n }\n]"
+ "links": "[\n {\n \"description\": \"Loan Type for interest and penalty rates\",\n \"label\": \"Loan Type\",\n \"name\": \"Loan Type\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loan Applications from customers and employees.\",\n \"label\": \"Loan Application\",\n \"name\": \"Loan Application\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loans provided to customers and employees.\",\n \"label\": \"Loan\",\n \"name\": \"Loan\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
@@ -13,7 +13,7 @@
{
"hidden": 0,
"label": "Disbursement and Repayment",
- "links": "[\n {\n \"label\": \"Loan Disbursement\",\n \"name\": \"Loan Disbursement\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Repayment\",\n \"name\": \"Loan Repayment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Interest Accrual\",\n \"name\": \"Loan Interest Accrual\",\n \"type\": \"doctype\"\n }\n]"
+ "links": "[\n {\n \"label\": \"Loan Disbursement\",\n \"name\": \"Loan Disbursement\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Repayment\",\n \"name\": \"Loan Repayment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Write Off\",\n \"name\": \"Loan Write Off\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Interest Accrual\",\n \"name\": \"Loan Interest Accrual\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
@@ -34,10 +34,11 @@
"docstatus": 0,
"doctype": "Desk Page",
"extends_another_page": 0,
+ "hide_custom": 0,
"idx": 0,
"is_standard": 1,
"label": "Loan",
- "modified": "2020-06-07 19:42:14.947902",
+ "modified": "2020-10-17 12:59:50.336085",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",
diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js
index 9b4c217..28af3a9 100644
--- a/erpnext/loan_management/doctype/loan/loan.js
+++ b/erpnext/loan_management/doctype/loan/loan.js
@@ -7,10 +7,14 @@
setup: function(frm) {
frm.make_methods = {
'Loan Disbursement': function() { frm.trigger('make_loan_disbursement') },
- 'Loan Security Unpledge': function() { frm.trigger('create_loan_security_unpledge') }
+ 'Loan Security Unpledge': function() { frm.trigger('create_loan_security_unpledge') },
+ 'Loan Write Off': function() { frm.trigger('make_loan_write_off_entry') }
}
},
onload: function (frm) {
+ // Ignore loan security pledge on cancel of loan
+ frm.ignore_doctypes_on_cancel_all = ["Loan Security Pledge"];
+
frm.set_query("loan_application", function () {
return {
"filters": {
@@ -21,6 +25,14 @@
};
});
+ frm.set_query("loan_type", function () {
+ return {
+ "filters": {
+ "docstatus": 1
+ }
+ };
+ });
+
$.each(["penalty_income_account", "interest_income_account"], function(i, field) {
frm.set_query(field, function () {
return {
@@ -49,24 +61,33 @@
refresh: function (frm) {
if (frm.doc.docstatus == 1) {
- if (frm.doc.status == "Sanctioned" || frm.doc.status == 'Partially Disbursed') {
+ if (["Disbursed", "Partially Disbursed"].includes(frm.doc.status) && (!frm.doc.repay_from_salary)) {
+ frm.add_custom_button(__('Request Loan Closure'), function() {
+ frm.trigger("request_loan_closure");
+ },__('Status'));
+
+ frm.add_custom_button(__('Loan Repayment'), function() {
+ frm.trigger("make_repayment_entry");
+ },__('Create'));
+ }
+
+ if (["Sanctioned", "Partially Disbursed"].includes(frm.doc.status)) {
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'));
}
+
+ if (["Loan Closure Requested", "Disbursed", "Partially Disbursed"].includes(frm.doc.status)) {
+ frm.add_custom_button(__('Loan Write Off'), function() {
+ frm.trigger("make_loan_write_off_entry");
+ },__('Create'));
+ }
}
frm.trigger("toggle_fields");
},
@@ -117,6 +138,38 @@
})
},
+ make_loan_write_off_entry: function(frm) {
+ frappe.call({
+ args: {
+ "loan": frm.doc.name,
+ "company": frm.doc.company,
+ "as_dict": 1
+ },
+ method: "erpnext.loan_management.doctype.loan.loan.make_loan_write_off",
+ callback: function (r) {
+ if (r.message)
+ var doc = frappe.model.sync(r.message)[0];
+ frappe.set_route("Form", doc.doctype, doc.name);
+ }
+ })
+ },
+
+ request_loan_closure: function(frm) {
+ frappe.confirm(__("Do you really want to close this loan"),
+ function() {
+ frappe.call({
+ args: {
+ 'loan': frm.doc.name
+ },
+ method: "erpnext.loan_management.doctype.loan.loan.request_loan_closure",
+ callback: function() {
+ frm.reload_doc();
+ }
+ });
+ }
+ );
+ },
+
create_loan_security_unpledge: function(frm) {
frappe.call({
method: "erpnext.loan_management.doctype.loan.loan.unpledge_security",
diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json
index aa5e21b..e8ecf01 100644
--- a/erpnext/loan_management/doctype/loan/loan.json
+++ b/erpnext/loan_management/doctype/loan/loan.json
@@ -43,6 +43,7 @@
"section_break_17",
"total_payment",
"total_principal_paid",
+ "written_off_amount",
"column_break_19",
"total_interest_payable",
"total_amount_paid",
@@ -75,6 +76,7 @@
"fieldname": "loan_application",
"fieldtype": "Link",
"label": "Loan Application",
+ "no_copy": 1,
"options": "Loan Application"
},
{
@@ -134,6 +136,7 @@
"fieldname": "loan_amount",
"fieldtype": "Currency",
"label": "Loan Amount",
+ "non_negative": 1,
"options": "Company:company:default_currency"
},
{
@@ -148,7 +151,8 @@
"depends_on": "eval:doc.status==\"Disbursed\"",
"fieldname": "disbursement_date",
"fieldtype": "Date",
- "label": "Disbursement Date"
+ "label": "Disbursement Date",
+ "no_copy": 1
},
{
"depends_on": "is_term_loan",
@@ -252,6 +256,7 @@
"fieldname": "total_payment",
"fieldtype": "Currency",
"label": "Total Payable Amount",
+ "no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
@@ -265,6 +270,7 @@
"fieldname": "total_interest_payable",
"fieldtype": "Currency",
"label": "Total Interest Payable",
+ "no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
@@ -273,6 +279,7 @@
"fieldname": "total_amount_paid",
"fieldtype": "Currency",
"label": "Total Amount Paid",
+ "no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
@@ -289,8 +296,7 @@
"default": "0",
"fieldname": "is_secured_loan",
"fieldtype": "Check",
- "label": "Is Secured Loan",
- "read_only": 1
+ "label": "Is Secured Loan"
},
{
"default": "0",
@@ -313,6 +319,7 @@
"fieldname": "total_principal_paid",
"fieldtype": "Currency",
"label": "Total Principal Paid",
+ "no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
@@ -320,6 +327,7 @@
"fieldname": "disbursed_amount",
"fieldtype": "Currency",
"label": "Disbursed Amount",
+ "no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
@@ -328,13 +336,23 @@
"fieldname": "maximum_loan_amount",
"fieldtype": "Currency",
"label": "Maximum Loan Amount",
+ "no_copy": 1,
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "written_off_amount",
+ "fieldtype": "Currency",
+ "label": "Written Off Amount",
+ "no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
}
],
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-08-01 12:36:11.255233",
+ "modified": "2020-11-05 10:04:00.762975",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index d1b7589..8405d6e 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -9,6 +9,7 @@
from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
from erpnext.controllers.accounts_controller import AccountsController
+from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
class Loan(AccountsController):
def validate(self):
@@ -137,9 +138,12 @@
})
def unlink_loan_security_pledge(self):
- frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET
- loan = '', status = 'Unpledged'
- where name = %s """, (self.loan_security_pledge))
+ pledges = frappe.get_all('Loan Security Pledge', fields=['name'], filters={'loan': self.name})
+ pledge_list = [d.name for d in pledges]
+ if pledge_list:
+ frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET
+ loan = '', status = 'Unpledged'
+ where name in (%s) """ % (', '.join(['%s']*len(pledge_list))), tuple(pledge_list)) #nosec
def update_total_amount_paid(doc):
total_amount_paid = 0
@@ -183,6 +187,24 @@
return monthly_repayment_amount
@frappe.whitelist()
+def request_loan_closure(loan, posting_date=None):
+ if not posting_date:
+ posting_date = getdate()
+
+ amounts = calculate_amounts(loan, posting_date)
+ pending_amount = amounts['payable_amount'] + amounts['unaccrued_interest']
+
+ loan_type = frappe.get_value('Loan', loan, 'loan_type')
+ write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount')
+
+ # checking greater than 0 as there may be some minor precision error
+ if pending_amount < write_off_limit:
+ # update status as loan closure requested
+ frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
+ else:
+ frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount))
+
+@frappe.whitelist()
def get_loan_application(loan_application):
loan = frappe.get_doc("Loan Application", loan_application)
if loan:
@@ -200,6 +222,7 @@
disbursement_entry.applicant = applicant
disbursement_entry.company = company
disbursement_entry.disbursement_date = nowdate()
+ disbursement_entry.posting_date = nowdate()
disbursement_entry.disbursed_amount = pending_amount
if as_dict:
@@ -223,6 +246,38 @@
return repayment_entry
@frappe.whitelist()
+def make_loan_write_off(loan, company=None, posting_date=None, amount=0, as_dict=0):
+ if not company:
+ company = frappe.get_value('Loan', loan, 'company')
+
+ if not posting_date:
+ posting_date = getdate()
+
+ amounts = calculate_amounts(loan, posting_date)
+ pending_amount = amounts['pending_principal_amount']
+
+ if amount and (amount > pending_amount):
+ frappe.throw('Write Off amount cannot be greater than pending loan amount')
+
+ if not amount:
+ amount = pending_amount
+
+ # get default write off account from company master
+ write_off_account = frappe.get_value('Company', company, 'write_off_account')
+
+ write_off = frappe.new_doc('Loan Write Off')
+ write_off.loan = loan
+ write_off.posting_date = posting_date
+ write_off.write_off_account = write_off_account
+ write_off.write_off_amount = amount
+ write_off.save()
+
+ if as_dict:
+ return write_off.as_dict()
+ else:
+ return write_off
+
+@frappe.whitelist()
def unpledge_security(loan=None, loan_security_pledge=None, as_dict=0, save=0, submit=0, approve=0):
# if loan is passed it will be considered as full unpledge
if loan:
diff --git a/erpnext/loan_management/doctype/loan/loan_dashboard.py b/erpnext/loan_management/doctype/loan/loan_dashboard.py
index 90d5ae2..7a8190f 100644
--- a/erpnext/loan_management/doctype/loan/loan_dashboard.py
+++ b/erpnext/loan_management/doctype/loan/loan_dashboard.py
@@ -13,7 +13,7 @@
'items': ['Loan Security Pledge', 'Loan Security Shortfall', 'Loan Disbursement']
},
{
- 'items': ['Loan Repayment', 'Loan Interest Accrual', 'Loan Security Unpledge']
+ 'items': ['Loan Repayment', 'Loan Interest Accrual', 'Loan Write Off', 'Loan Security Unpledge']
}
]
}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan/loan_list.js b/erpnext/loan_management/doctype/loan/loan_list.js
new file mode 100644
index 0000000..6591b72
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan/loan_list.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+// License: GNU General Public License v3. See license.txt
+
+frappe.listview_settings['Loan'] = {
+ get_indicator: function(doc) {
+ var status_color = {
+ "Draft": "red",
+ "Sanctioned": "blue",
+ "Disbursed": "orange",
+ "Partially Disbursed": "yellow",
+ "Loan Closure Requested": "green",
+ "Closed": "green"
+ };
+ return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
+ },
+};
diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py
index 5a4a19a..10a7b11 100644
--- a/erpnext/loan_management/doctype/loan/test_loan.py
+++ b/erpnext/loan_management/doctype/loan/test_loan.py
@@ -14,7 +14,7 @@
process_loan_interest_accrual_for_term_loans)
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year
from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall
-from erpnext.loan_management.doctype.loan.loan import unpledge_security
+from erpnext.loan_management.doctype.loan.loan import unpledge_security, request_loan_closure, make_loan_write_off
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge
from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount
@@ -132,7 +132,7 @@
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
- loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()))
+ loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
@@ -142,30 +142,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)
+ accrued_interest_amount = flt((loan.loan_amount * loan.rate_of_interest * no_of_days)
+ / (days_in_year(get_datetime(first_date).year) * 100), 2)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
- repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), "Regular Payment", 111118.68)
+ repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), 111119)
repayment_entry.save()
repayment_entry.submit()
- penalty_amount = (accrued_interest_amount * 4 * 25) / (100 * days_in_year(get_datetime(first_date).year))
- self.assertEquals(flt(repayment_entry.penalty_amount, 2), flt(penalty_amount, 2))
+ penalty_amount = (accrued_interest_amount * 5 * 25) / 100
+ self.assertEquals(flt(repayment_entry.penalty_amount,0), flt(penalty_amount, 0))
- amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
- 'paid_principal_amount'])
+ amounts = frappe.db.get_all('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount'])
loan.load_from_db()
- self.assertEquals(amounts[0], repayment_entry.interest_payable)
- self.assertEquals(flt(loan.total_principal_paid, 2), flt(repayment_entry.amount_paid -
- penalty_amount - amounts[0], 2))
+ total_interest_paid = amounts[0]['paid_interest_amount'] + amounts[1]['paid_interest_amount']
+ self.assertEquals(amounts[1]['paid_interest_amount'], repayment_entry.interest_payable)
+ self.assertEquals(flt(loan.total_principal_paid, 0), flt(repayment_entry.amount_paid -
+ penalty_amount - total_interest_paid, 0))
- def test_loan_closure_repayment(self):
+ def test_loan_closure(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
@@ -174,7 +174,7 @@
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
- loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()))
+ loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
@@ -184,10 +184,10 @@
no_of_days = date_diff(last_date, first_date) + 1
- # Adding 6 since repayment is made 5 days late after due date
+ # Adding 5 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
+ # 5 days as well though in grace period
+ no_of_days += 5
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
@@ -195,15 +195,17 @@
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
- repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6),
- "Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
+ repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
+ flt(loan.loan_amount + accrued_interest_amount))
+
repayment_entry.submit()
amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)'])
- self.assertEquals(flt(amount, 2),flt(accrued_interest_amount, 2))
+ self.assertEquals(flt(amount, 0),flt(accrued_interest_amount, 0))
self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0)
+ request_loan_closure(loan.name)
loan.load_from_db()
self.assertEquals(loan.status, "Loan Closure Requested")
@@ -230,8 +232,7 @@
process_loan_interest_accrual_for_term_loans(posting_date=nowdate())
- repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(nowdate(), 5),
- "Regular Payment", 89768.75)
+ repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(nowdate(), 5), 89768.75)
repayment_entry.submit()
@@ -281,7 +282,7 @@
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
- loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()))
+ loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
@@ -291,7 +292,7 @@
no_of_days = date_diff(last_date, first_date) + 1
- no_of_days += 6
+ no_of_days += 5
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
@@ -299,10 +300,10 @@
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
- repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6),
- "Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
+ repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount))
repayment_entry.submit()
+ request_loan_closure(loan.name)
loan.load_from_db()
self.assertEquals(loan.status, "Loan Closure Requested")
@@ -317,9 +318,9 @@
self.assertEqual(loan.status, 'Closed')
self.assertEquals(sum(pledged_qty.values()), 0)
- amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment")
- self.assertEqual(amounts['pending_principal_amount'], 0)
- self.assertEqual(amounts['payable_principal_amount'], 0)
+ amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 5))
+ self.assertTrue(amounts['pending_principal_amount'] < 0)
+ self.assertEquals(amounts['payable_principal_amount'], 0.0)
self.assertEqual(amounts['interest_amount'], 0)
def test_disbursal_check_with_shortfall(self):
@@ -381,7 +382,7 @@
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
- loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()))
+ loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
@@ -391,7 +392,7 @@
no_of_days = date_diff(last_date, first_date) + 1
- no_of_days += 6
+ no_of_days += 5
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
@@ -399,20 +400,192 @@
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
- amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment")
+ amounts = calculate_amounts(loan.name, add_days(last_date, 5))
- repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6),
- "Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
+ repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount))
repayment_entry.submit()
amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
'paid_principal_amount'])
+ request_loan_closure(loan.name)
loan.load_from_db()
self.assertEquals(loan.status, "Loan Closure Requested")
- amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment")
- self.assertEquals(amounts['pending_principal_amount'], 0.0)
+ amounts = calculate_amounts(loan.name, add_days(last_date, 5))
+ self.assertTrue(amounts['pending_principal_amount'] < 0.0)
+
+ def test_partial_unaccrued_interest_payment(self):
+ pledge = [{
+ "loan_security": "Test Security 1",
+ "qty": 4000.00
+ }]
+
+ loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
+ create_pledge(loan_application)
+
+ loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
+ 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
+
+ no_of_days += 5.5
+
+ # get partial unaccrued interest amount
+ paid_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_for_demand_loans(posting_date = last_date)
+
+ amounts = calculate_amounts(loan.name, add_days(last_date, 5))
+
+ repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
+ paid_amount)
+
+ repayment_entry.submit()
+ repayment_entry.load_from_db()
+
+ partial_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 5) \
+ / (days_in_year(get_datetime(first_date).year) * 100)
+
+ interest_amount = flt(amounts['interest_amount'] + partial_accrued_interest_amount, 2)
+ self.assertEqual(flt(repayment_entry.total_interest_paid, 0), flt(interest_amount, 0))
+
+ def test_penalty(self):
+ pledge = [{
+ "loan_security": "Test Security 1",
+ "qty": 4000.00
+ }]
+
+ loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
+ create_pledge(loan_application)
+
+ loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
+ loan.submit()
+
+ self.assertEquals(loan.loan_amount, 1000000)
+
+ first_date = '2019-10-01'
+ last_date = '2019-10-30'
+
+ make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
+ process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
+
+ amounts = calculate_amounts(loan.name, add_days(last_date, 1))
+ paid_amount = amounts['interest_amount']/2
+
+ repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
+ paid_amount)
+
+ repayment_entry.submit()
+
+ # 30 days - grace period
+ penalty_days = 30 - 4
+ penalty_applicable_amount = flt(amounts['interest_amount']/2, 2)
+ penalty_amount = flt((((penalty_applicable_amount * 25) / 100) * penalty_days), 2)
+ process = process_loan_interest_accrual_for_demand_loans(posting_date = '2019-11-30')
+
+ calculated_penalty_amount = frappe.db.get_value('Loan Interest Accrual',
+ {'process_loan_interest_accrual': process, 'loan': loan.name}, 'penalty_amount')
+
+ self.assertEquals(calculated_penalty_amount, penalty_amount)
+
+ def test_loan_write_off_limit(self):
+ pledge = [{
+ "loan_security": "Test Security 1",
+ "qty": 4000.00
+ }]
+
+ loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
+ create_pledge(loan_application)
+
+ loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
+ 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
+ no_of_days += 5
+
+ 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_for_demand_loans(posting_date = last_date)
+
+ # repay 50 less so that it can be automatically written off
+ repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
+ flt(loan.loan_amount + accrued_interest_amount - 50))
+
+ repayment_entry.submit()
+
+ amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)'])
+
+ self.assertEquals(flt(amount, 0),flt(accrued_interest_amount, 0))
+ self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0)
+
+ amounts = calculate_amounts(loan.name, add_days(last_date, 5))
+ self.assertEquals(flt(amounts['pending_principal_amount'], 0), 50)
+
+ request_loan_closure(loan.name)
+ loan.load_from_db()
+ self.assertEquals(loan.status, "Loan Closure Requested")
+
+ def test_loan_amount_write_off(self):
+ pledge = [{
+ "loan_security": "Test Security 1",
+ "qty": 4000.00
+ }]
+
+ loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
+ create_pledge(loan_application)
+
+ loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
+ 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
+ no_of_days += 5
+
+ 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_for_demand_loans(posting_date = last_date)
+
+ # repay 100 less so that it can be automatically written off
+ repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
+ flt(loan.loan_amount + accrued_interest_amount - 100))
+
+ repayment_entry.submit()
+
+ amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)'])
+
+ self.assertEquals(flt(amount, 0),flt(accrued_interest_amount, 0))
+ self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0)
+
+ amounts = calculate_amounts(loan.name, add_days(last_date, 5))
+ self.assertEquals(flt(amounts['pending_principal_amount'], 0), 100)
+
+ we = make_loan_write_off(loan.name, amount=amounts['pending_principal_amount'])
+ we.submit()
+
+ amounts = calculate_amounts(loan.name, add_days(last_date, 5))
+ self.assertEquals(flt(amounts['pending_principal_amount'], 0), 0)
+
def create_loan_accounts():
if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"):
@@ -496,7 +669,8 @@
"interest_income_account": interest_income_account,
"penalty_income_account": penalty_income_account,
"repayment_method": repayment_method,
- "repayment_periods": repayment_periods
+ "repayment_periods": repayment_periods,
+ "write_off_amount": 100
}).insert()
loan_type.submit()
@@ -532,7 +706,7 @@
"haircut": 50.00,
}).insert(ignore_permissions=True)
-def create_loan_security_pledge(applicant, pledges, loan_application):
+def create_loan_security_pledge(applicant, pledges, loan_application=None, loan=None):
lsp = frappe.new_doc("Loan Security Pledge")
lsp.applicant_type = 'Customer'
@@ -540,11 +714,13 @@
lsp.company = "_Test Company"
lsp.loan_application = loan_application
+ if loan:
+ lsp.loan = loan
+
for pledge in pledges:
lsp.append('securities', {
"loan_security": pledge['loan_security'],
- "qty": pledge['qty'],
- "haircut": pledge['haircut']
+ "qty": pledge['qty']
})
lsp.save()
@@ -582,12 +758,11 @@
"valid_upto": to_date
}).insert(ignore_permissions=True)
-def create_repayment_entry(loan, applicant, posting_date, payment_type, paid_amount):
+def create_repayment_entry(loan, applicant, posting_date, 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,
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
index c437a98..cd5df4d 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
@@ -26,19 +26,24 @@
{
"fieldname": "against_loan",
"fieldtype": "Link",
+ "in_list_view": 1,
"label": "Against Loan ",
- "options": "Loan"
+ "options": "Loan",
+ "reqd": 1
},
{
"fieldname": "disbursement_date",
"fieldtype": "Date",
- "label": "Disbursement Date"
+ "label": "Disbursement Date",
+ "reqd": 1
},
{
"fieldname": "disbursed_amount",
"fieldtype": "Currency",
"label": "Disbursed Amount",
- "options": "Company:company:default_currency"
+ "non_negative": 1,
+ "options": "Company:company:default_currency",
+ "reqd": 1
},
{
"fieldname": "amended_from",
@@ -53,17 +58,21 @@
"fetch_from": "against_loan.company",
"fieldname": "company",
"fieldtype": "Link",
+ "in_list_view": 1,
"label": "Company",
"options": "Company",
- "read_only": 1
+ "read_only": 1,
+ "reqd": 1
},
{
"fetch_from": "against_loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
+ "in_list_view": 1,
"label": "Applicant",
"options": "applicant_type",
- "read_only": 1
+ "read_only": 1,
+ "reqd": 1
},
{
"collapsible": 1,
@@ -102,9 +111,11 @@
"fetch_from": "against_loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
+ "in_list_view": 1,
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
- "read_only": 1
+ "read_only": 1,
+ "reqd": 1
},
{
"fieldname": "bank_account",
@@ -117,9 +128,10 @@
"fieldtype": "Column Break"
}
],
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-04-29 05:20:41.629911",
+ "modified": "2020-11-06 10:04:30.882322",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Disbursement",
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
index 260fada..233862b 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
@@ -17,6 +17,7 @@
def validate(self):
self.set_missing_values()
+ self.validate_disbursal_amount()
def on_submit(self):
self.set_status_and_amounts()
@@ -40,57 +41,21 @@
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_status_and_amounts(self, cancel=0):
+ def validate_disbursal_amount(self):
+ possible_disbursal_amount = get_disbursal_amount(self.against_loan)
+ if self.disbursed_amount > possible_disbursal_amount:
+ frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount))
+
+ def set_status_and_amounts(self, cancel=0):
loan_details = frappe.get_all("Loan",
fields = ["loan_amount", "disbursed_amount", "total_payment", "total_principal_paid", "total_interest_payable",
"status", "is_term_loan", "is_secured_loan"], filters= { "name": self.against_loan })[0]
if cancel:
- disbursed_amount = loan_details.disbursed_amount - self.disbursed_amount
- total_payment = loan_details.total_payment
-
- if loan_details.disbursed_amount > loan_details.loan_amount:
- topup_amount = loan_details.disbursed_amount - loan_details.loan_amount
- if topup_amount > self.disbursed_amount:
- topup_amount = self.disbursed_amount
-
- total_payment = total_payment - topup_amount
-
- if disbursed_amount == 0:
- status = "Sanctioned"
- elif disbursed_amount >= loan_details.loan_amount:
- status = "Disbursed"
- else:
- status = "Partially Disbursed"
+ disbursed_amount, status, total_payment = self.get_values_on_cancel(loan_details)
else:
- disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount
- total_payment = loan_details.total_payment
-
- possible_disbursal_amount = get_disbursal_amount(self.against_loan)
-
- if self.disbursed_amount > possible_disbursal_amount:
- frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount))
-
- if loan_details.status == "Disbursed" and not loan_details.is_term_loan:
- process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1),
- loan=self.against_loan)
-
- if disbursed_amount > loan_details.loan_amount:
- topup_amount = disbursed_amount - loan_details.loan_amount
-
- if topup_amount < 0:
- topup_amount = 0
-
- if topup_amount > self.disbursed_amount:
- topup_amount = self.disbursed_amount
-
- total_payment = total_payment + topup_amount
-
- if flt(disbursed_amount) >= loan_details.loan_amount:
- status = "Disbursed"
- else:
- status = "Partially Disbursed"
+ disbursed_amount, status, total_payment = self.get_values_on_submit(loan_details)
frappe.db.set_value("Loan", self.against_loan, {
"disbursement_date": self.disbursement_date,
@@ -99,6 +64,53 @@
"total_payment": total_payment
})
+ def get_values_on_cancel(self, loan_details):
+ disbursed_amount = loan_details.disbursed_amount - self.disbursed_amount
+ total_payment = loan_details.total_payment
+
+ if loan_details.disbursed_amount > loan_details.loan_amount:
+ topup_amount = loan_details.disbursed_amount - loan_details.loan_amount
+ if topup_amount > self.disbursed_amount:
+ topup_amount = self.disbursed_amount
+
+ total_payment = total_payment - topup_amount
+
+ if disbursed_amount == 0:
+ status = "Sanctioned"
+
+ elif disbursed_amount >= loan_details.loan_amount:
+ status = "Disbursed"
+ else:
+ status = "Partially Disbursed"
+
+ return disbursed_amount, status, total_payment
+
+ def get_values_on_submit(self, loan_details):
+ disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount
+ total_payment = loan_details.total_payment
+
+ if loan_details.status in ("Disbursed", "Partially Disbursed") and not loan_details.is_term_loan:
+ process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1),
+ loan=self.against_loan, accrual_type="Disbursement")
+
+ if disbursed_amount > loan_details.loan_amount:
+ topup_amount = disbursed_amount - loan_details.loan_amount
+
+ if topup_amount < 0:
+ topup_amount = 0
+
+ if topup_amount > self.disbursed_amount:
+ topup_amount = self.disbursed_amount
+
+ total_payment = total_payment + topup_amount
+
+ if flt(disbursed_amount) >= loan_details.loan_amount:
+ status = "Disbursed"
+ else:
+ status = "Partially Disbursed"
+
+ return disbursed_amount, status, total_payment
+
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
loan_details = frappe.get_doc("Loan", self.against_loan)
@@ -111,7 +123,7 @@
"debit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
- "remarks": "Against Loan:" + self.against_loan,
+ "remarks": _("Disbursement against loan:") + self.against_loan,
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
@@ -127,10 +139,8 @@
"credit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
- "remarks": "Against Loan:" + self.against_loan,
+ "remarks": _("Disbursement against loan:") + self.against_loan,
"cost_center": self.cost_center,
- "party_type": self.applicant_type,
- "party": self.applicant,
"posting_date": self.disbursement_date
})
)
@@ -155,7 +165,8 @@
pledged_securities = get_pledged_security_qty(loan)
for security, qty in pledged_securities.items():
- security_value += (loan_security_price_map.get(security) * qty * hair_cut_map.get(security))/100
+ after_haircut_percentage = 100 - hair_cut_map.get(security)
+ security_value += (loan_security_price_map.get(security) * qty * after_haircut_percentage)/100
return security_value
@@ -173,7 +184,8 @@
pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \
- flt(loan_details.total_principal_paid)
else:
- pending_principal_amount = flt(loan_details.disbursed_amount)
+ pending_principal_amount = flt(loan_details.disbursed_amount) - flt(loan_details.total_interest_payable) \
+ - flt(loan_details.total_principal_paid)
security_value = 0.0
if loan_details.is_secured_loan:
@@ -184,6 +196,9 @@
disbursal_amount = flt(security_value) - flt(pending_principal_amount)
+ if loan_details.is_term_loan and (disbursal_amount + loan_details.loan_amount) > loan_details.loan_amount:
+ disbursal_amount = loan_details.loan_amount - loan_details.disbursed_amount
+
return disbursal_amount
diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py
index 2cb2637..a875387 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py
+++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py
@@ -8,9 +8,10 @@
from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_pledge, create_repayment_entry, create_loan_application,
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_for_demand_loans
-from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year
+from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year, get_per_day_interest
from erpnext.selling.doctype.customer.test_customer import get_customer_dict
from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge
+from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
class TestLoanDisbursement(unittest.TestCase):
@@ -60,8 +61,7 @@
self.assertRaises(frappe.ValidationError, make_loan_disbursement_entry, loan.name,
500000, first_date)
- repayment_entry = create_repayment_entry(loan.name, self.applicant, add_days(get_last_day(nowdate()), 5),
- "Regular Payment", 611095.89)
+ repayment_entry = create_repayment_entry(loan.name, self.applicant, add_days(get_last_day(nowdate()), 5), 611095.89)
repayment_entry.submit()
loan.reload()
@@ -69,3 +69,50 @@
# After repayment loan disbursement entry should go through
make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 16))
+ # check for disbursement accrual
+ loan_interest_accrual = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name,
+ 'accrual_type': 'Disbursement'})
+
+ self.assertTrue(loan_interest_accrual)
+
+ def test_loan_topup_with_additional_pledge(self):
+ pledge = [{
+ "loan_security": "Test Security 1",
+ "qty": 4000.00
+ }]
+
+ loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge)
+ create_pledge(loan_application)
+
+ loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, posting_date='2019-10-01')
+ loan.submit()
+
+ self.assertEquals(loan.loan_amount, 1000000)
+
+ first_date = '2019-10-01'
+ last_date = '2019-10-30'
+
+ # Disbursed 10,00,000 amount
+ make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
+ process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
+ amounts = calculate_amounts(loan.name, add_days(last_date, 1))
+
+ previous_interest = amounts['interest_amount']
+
+ pledge1 = [{
+ "loan_security": "Test Security 1",
+ "qty": 2000.00
+ }]
+
+ create_loan_security_pledge(self.applicant, pledge1, loan=loan.name)
+
+ # Topup 500000
+ make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 1))
+ process_loan_interest_accrual_for_demand_loans(posting_date = add_days(last_date, 15))
+ amounts = calculate_amounts(loan.name, add_days(last_date, 15))
+
+ per_day_interest = get_per_day_interest(1500000, 13.5, '2019-10-30')
+ interest = per_day_interest * 15
+
+ self.assertEquals(amounts['pending_principal_amount'], 1500000)
+ self.assertEquals(amounts['interest_amount'], flt(interest + previous_interest, 2))
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
index 5fc3e8f..f157f0d 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json
@@ -14,6 +14,7 @@
"column_break_4",
"company",
"posting_date",
+ "accrual_type",
"is_term_loan",
"section_break_7",
"pending_principal_amount",
@@ -22,9 +23,11 @@
"column_break_14",
"interest_amount",
"paid_interest_amount",
+ "penalty_amount",
"section_break_15",
"process_loan_interest_accrual",
"repayment_schedule_name",
+ "last_accrual_date",
"amended_from"
],
"fields": [
@@ -139,6 +142,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.is_term_loan",
"fieldname": "paid_principal_amount",
"fieldtype": "Currency",
"label": "Paid Principal Amount",
@@ -149,12 +153,32 @@
"fieldtype": "Currency",
"label": "Paid Interest Amount",
"options": "Company:company:default_currency"
+ },
+ {
+ "fieldname": "accrual_type",
+ "fieldtype": "Select",
+ "label": "Accrual Type",
+ "options": "Regular\nRepayment\nDisbursement"
+ },
+ {
+ "fieldname": "penalty_amount",
+ "fieldtype": "Currency",
+ "label": "Penalty Amount",
+ "options": "Company:company:default_currency"
+ },
+ {
+ "fieldname": "last_accrual_date",
+ "fieldtype": "Date",
+ "hidden": 1,
+ "label": "Last Accrual Date",
+ "read_only": 1
}
],
"in_create": 1,
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-04-16 11:24:23.258404",
+ "modified": "2020-11-07 05:49:25.448875",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Interest Accrual",
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
index 2d959bf..d17f5af 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
@@ -22,6 +22,8 @@
if not self.interest_amount and not self.payable_principal_amount:
frappe.throw(_("Interest Amount or Principal Amount is mandatory"))
+ if not self.last_accrual_date:
+ self.last_accrual_date = get_last_accrual_date(self.loan)
def on_submit(self):
self.make_gl_entries()
@@ -50,7 +52,8 @@
"debit_in_account_currency": self.interest_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
- "remarks": _("Against Loan:") + self.loan,
+ "remarks": _("Interest accrued from {0} to {1} against loan: {2}").format(
+ self.last_accrual_date, self.posting_date, self.loan),
"cost_center": erpnext.get_default_cost_center(self.company),
"posting_date": self.posting_date
})
@@ -59,14 +62,13 @@
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,
+ "remarks": ("Interest accrued from {0} to {1} against loan: {2}").format(
+ self.last_accrual_date, self.posting_date, self.loan),
"cost_center": erpnext.get_default_cost_center(self.company),
"posting_date": self.posting_date
})
@@ -79,19 +81,23 @@
# 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):
+def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest, accrual_type):
+ from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
+
no_of_days = get_no_of_days_for_interest_accural(loan, posting_date)
+ precision = cint(frappe.db.get_default("currency_precision")) or 2
if no_of_days <= 0:
return
if loan.status == 'Disbursed':
pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \
- - flt(loan.total_principal_paid)
+ - flt(loan.total_principal_paid) - flt(loan.written_off_amount)
else:
- pending_principal_amount = loan.disbursed_amount
+ pending_principal_amount = flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \
+ - flt(loan.total_principal_paid) - flt(loan.written_off_amount)
- interest_per_day = (pending_principal_amount * loan.rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100)
+ interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date)
payable_interest = interest_per_day * no_of_days
args = frappe._dict({
@@ -102,13 +108,16 @@
'loan_account': loan.loan_account,
'pending_principal_amount': pending_principal_amount,
'interest_amount': payable_interest,
+ 'penalty_amount': calculate_amounts(loan.name, posting_date)['penalty_amount'],
'process_loan_interest': process_loan_interest,
- 'posting_date': posting_date
+ 'posting_date': posting_date,
+ 'accrual_type': accrual_type
})
- make_loan_interest_accrual_entry(args)
+ if flt(payable_interest, precision) > 0.0:
+ make_loan_interest_accrual_entry(args)
-def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None):
+def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None, accrual_type="Regular"):
query_filters = {
"status": ('in', ['Disbursed', 'Partially Disbursed']),
"docstatus": 1
@@ -123,13 +132,13 @@
open_loans = frappe.get_all("Loan",
fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account",
"is_term_loan", "status", "disbursement_date", "disbursed_amount", "applicant_type", "applicant",
- "rate_of_interest", "total_interest_payable", "total_principal_paid", "repayment_start_date"],
+ "rate_of_interest", "total_interest_payable", "written_off_amount", "total_principal_paid", "repayment_start_date"],
filters=query_filters)
for loan in open_loans:
- calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest)
+ calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest, accrual_type)
-def make_accrual_interest_entry_for_term_loans(posting_date, process_loan_interest, term_loan=None, loan_type=None):
+def make_accrual_interest_entry_for_term_loans(posting_date, process_loan_interest, term_loan=None, loan_type=None, accrual_type="Regular"):
curr_date = posting_date or add_days(nowdate(), 1)
term_loans = get_term_loans(curr_date, term_loan, loan_type)
@@ -148,7 +157,8 @@
'payable_principal': loan.principal_amount,
'process_loan_interest': process_loan_interest,
'repayment_schedule_name': loan.payment_entry,
- 'posting_date': posting_date
+ 'posting_date': posting_date,
+ 'accrual_type': accrual_type
})
make_loan_interest_accrual_entry(args)
@@ -192,31 +202,33 @@
loan_interest_accrual.loan_account = args.loan_account
loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, precision)
loan_interest_accrual.interest_amount = flt(args.interest_amount, precision)
+ loan_interest_accrual.penalty_amount = flt(args.penalty_amount, precision)
loan_interest_accrual.posting_date = args.posting_date or nowdate()
loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest
loan_interest_accrual.repayment_schedule_name = args.repayment_schedule_name
loan_interest_accrual.payable_principal_amount = args.payable_principal
+ loan_interest_accrual.accrual_type = args.accrual_type
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)
+ last_interest_accrual_date = get_last_accrual_date(loan.name)
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):
+def get_last_accrual_date(loan):
last_posting_date = frappe.db.sql(""" SELECT MAX(posting_date) from `tabLoan Interest Accrual`
- WHERE loan = %s""", (loan.name))
+ WHERE loan = %s and docstatus = 1""", (loan))
if last_posting_date[0][0]:
# interest for last interest accrual date is already booked, so add 1 day
return add_days(last_posting_date[0][0], 1)
else:
- return loan.disbursement_date
+ return frappe.db.get_value('Loan', loan, 'disbursement_date')
def days_in_year(year):
days = 365
@@ -226,3 +238,11 @@
return days
+def get_per_day_interest(principal_amount, rate_of_interest, posting_date=None):
+ if not posting_date:
+ posting_date = getdate()
+
+ precision = cint(frappe.db.get_default("currency_precision")) or 2
+
+ return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100), precision)
+
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
index 4b85b21..46a6440 100644
--- 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
@@ -5,7 +5,7 @@
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,
+from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_price,
make_loan_disbursement_entry, create_loan_accounts, create_loan_security_type, create_loan_security, create_demand_loan, create_loan_application)
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year
@@ -57,4 +57,4 @@
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))
+ self.assertEquals(flt(loan_interest_accural.interest_amount, 0), flt(accrued_interest_amount, 0))
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
index 5942455..2b5df4b 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
@@ -10,11 +10,11 @@
"applicant_type",
"applicant",
"loan_type",
- "payment_type",
"column_break_3",
"company",
"posting_date",
"is_term_loan",
+ "rate_of_interest",
"payment_details_section",
"due_date",
"pending_principal_amount",
@@ -31,6 +31,7 @@
"column_break_21",
"reference_date",
"principal_amount_paid",
+ "total_interest_paid",
"repayment_details",
"amended_from"
],
@@ -96,15 +97,6 @@
"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",
@@ -116,6 +108,7 @@
"fieldname": "amount_paid",
"fieldtype": "Currency",
"label": "Amount Paid",
+ "non_negative": 1,
"options": "Company:company:default_currency",
"reqd": 1
},
@@ -195,6 +188,7 @@
"fieldtype": "Currency",
"hidden": 1,
"label": "Principal Amount Paid",
+ "options": "Company:company:default_currency",
"read_only": 1
},
{
@@ -217,11 +211,27 @@
"hidden": 1,
"label": "Repayment Details",
"options": "Loan Repayment Detail"
+ },
+ {
+ "fieldname": "total_interest_paid",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Total Interest Paid",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "loan_type.rate_of_interest",
+ "fieldname": "rate_of_interest",
+ "fieldtype": "Percent",
+ "label": "Rate Of Interest",
+ "read_only": 1
}
],
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-05-16 09:40:15.581165",
+ "modified": "2020-11-05 10:06:58.792841",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Repayment",
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index 97dbc44..415ba99 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -14,14 +14,15 @@
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import update_shortfall_status
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans
+from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import get_per_day_interest, get_last_accrual_date
class LoanRepayment(AccountsController):
def validate(self):
- amounts = calculate_amounts(self.against_loan, self.posting_date, self.payment_type)
+ amounts = calculate_amounts(self.against_loan, self.posting_date)
self.set_missing_values(amounts)
self.validate_amount()
- self.allocate_amounts(amounts['pending_accrual_entries'])
+ self.allocate_amounts(amounts)
def before_submit(self):
self.book_unaccrued_interest()
@@ -32,8 +33,8 @@
def on_cancel(self):
self.mark_as_unpaid()
- self.make_gl_entries(cancel=1)
self.ignore_linked_doctypes = ['GL Entry']
+ self.make_gl_entries(cancel=1)
def set_missing_values(self, amounts):
precision = cint(frappe.db.get_default("currency_precision")) or 2
@@ -72,29 +73,36 @@
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, precision) < flt(self.payable_amount, precision):
- msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount)
- frappe.throw(msg)
-
def book_unaccrued_interest(self):
- if self.payment_type == 'Loan Closure':
- total_interest_paid = 0
- for payment in self.repayment_details:
- total_interest_paid += payment.paid_interest_amount
+ precision = cint(frappe.db.get_default("currency_precision")) or 2
+ if self.total_interest_paid > self.interest_payable:
+ if not self.is_term_loan:
+ # get last loan interest accrual date
+ last_accrual_date = get_last_accrual_date(self.against_loan)
- if total_interest_paid < self.interest_payable:
- if not self.is_term_loan:
- process = process_loan_interest_accrual_for_demand_loans(posting_date=self.posting_date,
- loan=self.against_loan)
+ # get posting date upto which interest has to be accrued
+ per_day_interest = flt(get_per_day_interest(self.pending_principal_amount,
+ self.rate_of_interest, self.posting_date), 2)
- lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual':
- process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1)
+ no_of_days = flt(flt(self.total_interest_paid - self.interest_payable,
+ precision)/per_day_interest, 0) - 1
- self.append('repayment_details', {
- 'loan_interest_accrual': lia.name,
- 'paid_interest_amount': lia.interest_amount,
- 'paid_principal_amount': lia.payable_principal_amount
- })
+ posting_date = add_days(last_accrual_date, no_of_days)
+
+ # book excess interest paid
+ process = process_loan_interest_accrual_for_demand_loans(posting_date=posting_date,
+ loan=self.against_loan, accrual_type="Repayment")
+
+ # get loan interest accrual to update paid amount
+ lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual':
+ process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1)
+
+ self.append('repayment_details', {
+ 'loan_interest_accrual': lia.name,
+ 'paid_interest_amount': flt(self.total_interest_paid - self.interest_payable, precision),
+ 'paid_principal_amount': 0.0,
+ 'accrual_type': 'Repayment'
+ })
def update_paid_amount(self):
precision = cint(frappe.db.get_default("currency_precision")) or 2
@@ -108,12 +116,6 @@
WHERE name = %s""",
(flt(payment.paid_principal_amount, precision), flt(payment.paid_interest_amount, precision), payment.loan_interest_accrual))
- if flt(loan.total_principal_paid + self.principal_amount_paid, precision) >= flt(loan.total_payment, precision):
- if loan.is_secured_loan:
- frappe.db.set_value("Loan", self.against_loan, "status", "Loan Closure Requested")
- else:
- frappe.db.set_value("Loan", self.against_loan, "status", "Closed")
-
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))
@@ -123,6 +125,8 @@
def mark_as_unpaid(self):
loan = frappe.get_doc("Loan", self.against_loan)
+ no_of_repayments = len(self.repayment_details)
+
for payment in self.repayment_details:
frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
SET paid_principal_amount = `paid_principal_amount` - %s,
@@ -130,6 +134,12 @@
WHERE name = %s""",
(payment.paid_principal_amount, payment.paid_interest_amount, payment.loan_interest_accrual))
+ # Cancel repayment interest accrual
+ # checking idx as a preventive measure, repayment accrual will always be the last entry
+ if payment.accrual_type == 'Repayment' and payment.idx == no_of_repayments:
+ lia_doc = frappe.get_doc('Loan Interest Accrual', payment.loan_interest_accrual)
+ lia_doc.cancel()
+
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))
@@ -137,15 +147,17 @@
if loan.status == "Loan Closure Requested":
frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed")
- def allocate_amounts(self, paid_entries):
+ def allocate_amounts(self, repayment_details):
+ precision = cint(frappe.db.get_default("currency_precision")) or 2
+
self.set('repayment_details', [])
self.principal_amount_paid = 0
total_interest_paid = 0
interest_paid = self.amount_paid - self.penalty_amount
- if self.amount_paid - self.penalty_amount > 0 and paid_entries:
+ if self.amount_paid - self.penalty_amount > 0:
interest_paid = self.amount_paid - self.penalty_amount
- for lia, amounts in iteritems(paid_entries):
+ for lia, amounts in iteritems(repayment_details.get('pending_accrual_entries', [])):
if amounts['interest_amount'] + amounts['payable_principal_amount'] <= interest_paid:
interest_amount = amounts['interest_amount']
paid_principal = amounts['payable_principal_amount']
@@ -169,9 +181,24 @@
'paid_principal_amount': paid_principal
})
- if self.payment_type == 'Loan Closure' and total_interest_paid < self.interest_payable:
- unaccrued_interest = self.interest_payable - total_interest_paid
- interest_paid -= unaccrued_interest
+ if repayment_details['unaccrued_interest'] and interest_paid:
+ # no of days for which to accrue interest
+ # Interest can only be accrued for an entire day and not partial
+ if interest_paid > repayment_details['unaccrued_interest']:
+ per_day_interest = flt(get_per_day_interest(self.pending_principal_amount,
+ self.rate_of_interest, self.posting_date), precision)
+ interest_paid -= repayment_details['unaccrued_interest']
+ total_interest_paid += repayment_details['unaccrued_interest']
+ else:
+ # get no of days for which interest can be paid
+ per_day_interest = flt(get_per_day_interest(self.pending_principal_amount,
+ self.rate_of_interest, self.posting_date), precision)
+
+ no_of_days = cint(interest_paid/per_day_interest)
+ total_interest_paid += no_of_days * per_day_interest
+ interest_paid -= no_of_days * per_day_interest
+
+ self.total_interest_paid = total_interest_paid
if interest_paid:
self.principal_amount_paid += interest_paid
@@ -189,7 +216,7 @@
"debit_in_account_currency": self.penalty_amount,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
- "remarks": _("Against Loan:") + self.against_loan,
+ "remarks": _("Penalty against loan:") + self.against_loan,
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
@@ -205,10 +232,8 @@
"credit_in_account_currency": self.penalty_amount,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
- "remarks": _("Against Loan:") + self.against_loan,
+ "remarks": _("Penalty against loan:") + self.against_loan,
"cost_center": self.cost_center,
- "party_type": self.applicant_type,
- "party": self.applicant,
"posting_date": getdate(self.posting_date)
})
)
@@ -219,13 +244,11 @@
"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 ,
+ "debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
- "remarks": _("Against Loan:") + self.against_loan,
+ "remarks": _("Repayment against Loan: ") + self.against_loan,
"cost_center": self.cost_center,
- "party_type": self.applicant_type,
- "party": self.applicant,
"posting_date": getdate(self.posting_date)
})
)
@@ -240,7 +263,7 @@
"credit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
- "remarks": _("Against Loan:") + self.against_loan,
+ "remarks": _("Repayment against Loan: ") + self.against_loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
@@ -273,7 +296,8 @@
unpaid_accrued_entries = frappe.db.sql(
"""
SELECT name, posting_date, interest_amount - paid_interest_amount as interest_amount,
- payable_principal_amount - paid_principal_amount as payable_principal_amount
+ payable_principal_amount - paid_principal_amount as payable_principal_amount,
+ accrual_type
FROM
`tabLoan Interest Accrual`
WHERE
@@ -282,6 +306,7 @@
payable_principal_amount - paid_principal_amount > 0)
AND
docstatus = 1
+ ORDER BY posting_date
""", (against_loan), as_dict=1)
return unpaid_accrued_entries
@@ -289,7 +314,7 @@
# 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):
+def get_amounts(amounts, against_loan, posting_date):
precision = cint(frappe.db.get_default("currency_precision")) or 2
against_loan_doc = frappe.get_doc("Loan", against_loan)
@@ -311,10 +336,10 @@
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))
+ 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
+ if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary) and entry.accrual_type == 'Regular':
+ penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)
total_pending_interest += entry.interest_amount
payable_principal_amount += entry.payable_principal_amount
@@ -324,23 +349,27 @@
'payable_principal_amount': flt(entry.payable_principal_amount, precision)
})
- if not final_due_date:
+ if due_date and not final_due_date:
final_due_date = add_days(due_date, loan_type_details.grace_period_in_days)
if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested', 'Closed'):
- pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable
+ pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid \
+ - against_loan_doc.total_interest_payable - against_loan_doc.written_off_amount
else:
- pending_principal_amount = against_loan_doc.disbursed_amount
+ pending_principal_amount = against_loan_doc.disbursed_amount - against_loan_doc.total_principal_paid \
+ - against_loan_doc.total_interest_payable - against_loan_doc.written_off_amount
- if payment_type == "Loan Closure":
- if due_date:
- pending_days = date_diff(posting_date, due_date) + 1
- else:
- pending_days = date_diff(posting_date, against_loan_doc.disbursement_date) + 1
+ unaccrued_interest = 0
+ if due_date:
+ pending_days = date_diff(posting_date, due_date) + 1
+ else:
+ last_accrual_date = get_last_accrual_date(against_loan_doc.name)
+ pending_days = date_diff(posting_date, last_accrual_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)
+ if pending_days > 0:
+ principal_amount = flt(pending_principal_amount, precision)
+ per_day_interest = get_per_day_interest(principal_amount, loan_type_details.rate_of_interest, posting_date)
+ unaccrued_interest += (pending_days * flt(per_day_interest, precision))
amounts["pending_principal_amount"] = flt(pending_principal_amount, precision)
amounts["payable_principal_amount"] = flt(payable_principal_amount, precision)
@@ -348,6 +377,7 @@
amounts["penalty_amount"] = flt(penalty_amount, precision)
amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision)
amounts["pending_accrual_entries"] = pending_accrual_entries
+ amounts["unaccrued_interest"] = unaccrued_interest
if final_due_date:
amounts["due_date"] = final_due_date
@@ -355,7 +385,7 @@
return amounts
@frappe.whitelist()
-def calculate_amounts(against_loan, posting_date, payment_type):
+def calculate_amounts(against_loan, posting_date, payment_type=''):
amounts = {
'penalty_amount': 0.0,
@@ -363,10 +393,17 @@
'pending_principal_amount': 0.0,
'payable_principal_amount': 0.0,
'payable_amount': 0.0,
+ 'unaccrued_interest': 0.0,
'due_date': ''
}
- amounts = get_amounts(amounts, against_loan, posting_date, payment_type)
+ amounts = get_amounts(amounts, against_loan, posting_date)
+
+ # update values for closure
+ if payment_type == 'Loan Closure':
+ amounts['payable_principal_amount'] = amounts['pending_principal_amount']
+ amounts['interest_amount'] += amounts['unaccrued_interest']
+ amounts['payable_amount'] = amounts['payable_principal_amount'] + amounts['interest_amount']
return amounts
diff --git a/erpnext/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.json b/erpnext/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.json
index cff1dbb..4b9b191 100644
--- a/erpnext/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.json
+++ b/erpnext/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.json
@@ -7,7 +7,8 @@
"field_order": [
"loan_interest_accrual",
"paid_principal_amount",
- "paid_interest_amount"
+ "paid_interest_amount",
+ "accrual_type"
],
"fields": [
{
@@ -27,11 +28,20 @@
"fieldtype": "Currency",
"label": "Paid Interest Amount",
"options": "Company:company:default_currency"
+ },
+ {
+ "fetch_from": "loan_interest_accrual.accrual_type",
+ "fetch_if_empty": 1,
+ "fieldname": "accrual_type",
+ "fieldtype": "Select",
+ "label": "Accrual Type",
+ "options": "Regular\nRepayment\nDisbursement"
}
],
+ "index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-04-15 21:50:03.837019",
+ "modified": "2020-10-23 08:09:18.267030",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Repayment Detail",
diff --git a/erpnext/loan_management/doctype/loan_security/loan_security.json b/erpnext/loan_management/doctype/loan_security/loan_security.json
index 1d0bb30..c698601 100644
--- a/erpnext/loan_management/doctype/loan_security/loan_security.json
+++ b/erpnext/loan_management/doctype/loan_security/loan_security.json
@@ -25,6 +25,7 @@
},
{
"fetch_from": "loan_security_type.haircut",
+ "fetch_if_empty": 1,
"fieldname": "haircut",
"fieldtype": "Percent",
"label": "Haircut %"
@@ -64,8 +65,9 @@
"reqd": 1
}
],
+ "index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-04-29 13:21:26.043492",
+ "modified": "2020-10-26 07:34:48.601766",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security",
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
index 2bb6fd8..cbc8376 100644
--- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py
+++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py
@@ -78,7 +78,7 @@
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'])
+ maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_amount'])
- frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_value=%s, is_secured_loan=1
+ frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%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_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py
index 0f42bde..8ec0bfb 100644
--- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py
+++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py
@@ -22,7 +22,7 @@
if security_value >= loan_security_shortfall.shortfall_amount:
frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, {
"status": "Completed",
- "shortfall_value": loan_security_shortfall.shortfall_amount})
+ "shortfall_amount": loan_security_shortfall.shortfall_amount})
else:
frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name,
"shortfall_amount", loan_security_shortfall.shortfall_amount - security_value)
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
index b3eb600..c29f325 100644
--- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
+++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
@@ -42,18 +42,20 @@
"valid_upto": (">=", get_datetime())
}, as_list=1))
- total_payment, principal_paid, interest_payable = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid',
- 'total_interest_payable'])
+ total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid',
+ 'total_interest_payable', 'written_off_amount'])
- pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid)
+ pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount)
security_value = 0
for security in self.securities:
pledged_qty = pledge_qty_map.get(security.loan_security, 0)
if security.qty > pledged_qty:
- frappe.throw(_("""Row {0}: {1} {2} of {3} is pledged against Loan {4}.
- You are trying to unpledge more""").format(security.idx, pledged_qty, security.uom,
- frappe.bold(security.loan_security), frappe.bold(self.loan)))
+ msg = _("Row {0}: {1} {2} of {3} is pledged against Loan {4}.").format(security.idx, pledged_qty, security.uom,
+ frappe.bold(security.loan_security), frappe.bold(self.loan))
+ msg += "<br>"
+ msg += _("You are trying to unpledge more.")
+ frappe.throw(msg, title=_("Loan Security Unpledge Error"))
qty_after_unpledge = pledged_qty - security.qty
ltv_ratio = ltv_ratio_map.get(security.loan_security_type)
@@ -65,10 +67,18 @@
security_value += qty_after_unpledge * current_price
if not security_value and flt(pending_principal_amount, 2) > 0:
- frappe.throw("Cannot Unpledge, loan to value ratio is breaching")
+ self._throw(security_value, pending_principal_amount, ltv_ratio)
if security_value and flt(pending_principal_amount/security_value) * 100 > ltv_ratio:
- frappe.throw("Cannot Unpledge, loan to value ratio is breaching")
+ self._throw(security_value, pending_principal_amount, ltv_ratio)
+
+ def _throw(self, security_value, pending_principal_amount, ltv_ratio):
+ msg = _("Loan Security Value after unpledge is {0}").format(frappe.bold(security_value))
+ msg += '<br>'
+ msg += _("Pending principal amount is {0}").format(frappe.bold(flt(pending_principal_amount, 2)))
+ msg += '<br>'
+ msg += _("Loan To Security Value ratio must always be {0}").format(frappe.bold(ltv_ratio))
+ frappe.throw(msg, title=_("Loan To Value ratio breach"))
def on_update_after_submit(self):
self.approve()
diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json
index 669490a..18a9731 100644
--- a/erpnext/loan_management/doctype/loan_type/loan_type.json
+++ b/erpnext/loan_management/doctype/loan_type/loan_type.json
@@ -11,6 +11,7 @@
"rate_of_interest",
"penalty_interest_rate",
"grace_period_in_days",
+ "write_off_amount",
"column_break_2",
"company",
"is_term_loan",
@@ -76,7 +77,6 @@
"reqd": 1
},
{
- "description": "This account is used for booking loan repayments from the borrower and also disbursing loans to the borrower",
"fieldname": "payment_account",
"fieldtype": "Link",
"label": "Payment Account",
@@ -84,7 +84,6 @@
"reqd": 1
},
{
- "description": "This account is capital account which is used to allocate capital for loan disbursal account ",
"fieldname": "loan_account",
"fieldtype": "Link",
"label": "Loan Account",
@@ -96,7 +95,6 @@
"fieldtype": "Column Break"
},
{
- "description": "This account will be used for booking loan interest accruals",
"fieldname": "interest_income_account",
"fieldtype": "Link",
"label": "Interest Income Account",
@@ -104,7 +102,6 @@
"reqd": 1
},
{
- "description": "This account will be used for booking penalties levied due to delayed repayments",
"fieldname": "penalty_income_account",
"fieldtype": "Link",
"label": "Penalty Income Account",
@@ -113,7 +110,6 @@
},
{
"default": "0",
- "description": "If this is not checked the loan by default will be considered as a Demand Loan",
"fieldname": "is_term_loan",
"fieldtype": "Check",
"label": "Is Term Loan"
@@ -145,17 +141,27 @@
"label": "Company",
"options": "Company",
"reqd": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "description": "Pending amount that will be automatically ignored on loan closure request ",
+ "fieldname": "write_off_amount",
+ "fieldtype": "Currency",
+ "label": "Write Off Amount ",
+ "options": "Company:company:default_currency"
}
],
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-06-07 18:55:59.346292",
+ "modified": "2020-10-26 07:13:55.029811",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Type",
"owner": "Administrator",
"permissions": [
{
+ "cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
@@ -165,6 +171,7 @@
"report": 1,
"role": "Loan Manager",
"share": 1,
+ "submit": 1,
"write": 1
},
{
diff --git a/erpnext/loan_management/doctype/loan_write_off/__init__.py b/erpnext/loan_management/doctype/loan_write_off/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_write_off/__init__.py
diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.js b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.js
new file mode 100644
index 0000000..4e3319c
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.js
@@ -0,0 +1,36 @@
+// Copyright (c) 2020, 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 Write Off', {
+ loan: function(frm) {
+ frm.trigger('show_pending_principal_amount');
+ },
+ onload: function(frm) {
+ frm.trigger('show_pending_principal_amount');
+ },
+ refresh: function(frm) {
+ frm.set_query('write_off_account', function(){
+ return {
+ filters: {
+ 'company': frm.doc.company,
+ 'root_type': 'Expense',
+ 'is_group': 0
+ }
+ }
+ });
+ },
+ show_pending_principal_amount: function(frm) {
+ if (frm.doc.loan && frm.doc.docstatus === 0) {
+ frappe.db.get_value('Loan', frm.doc.loan, ['total_payment', 'total_interest_payable',
+ 'total_principal_paid', 'written_off_amount'], function(values) {
+ frm.set_df_property('write_off_amount', 'description',
+ "Pending principal amount is " + cstr(flt(values.total_payment - values.total_interest_payable
+ - values.total_principal_paid - values.written_off_amount, 2)));
+ frm.refresh_field('write_off_amount');
+ });
+
+ }
+ }
+});
diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.json b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.json
new file mode 100644
index 0000000..4617a62
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.json
@@ -0,0 +1,157 @@
+{
+ "actions": [],
+ "autoname": "LM-WO-.#####",
+ "creation": "2020-10-16 11:09:14.495066",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "loan",
+ "applicant_type",
+ "applicant",
+ "column_break_3",
+ "company",
+ "posting_date",
+ "accounting_dimensions_section",
+ "cost_center",
+ "section_break_9",
+ "write_off_account",
+ "column_break_11",
+ "write_off_amount",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "loan",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Loan",
+ "options": "Loan",
+ "reqd": 1
+ },
+ {
+ "default": "Today",
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Posting Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "loan.company",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fetch_from": "loan.applicant_type",
+ "fieldname": "applicant_type",
+ "fieldtype": "Select",
+ "label": "Applicant Type",
+ "options": "Employee\nMember\nCustomer",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "loan.applicant",
+ "fieldname": "applicant",
+ "fieldtype": "Dynamic Link",
+ "label": "Applicant ",
+ "options": "applicant_type",
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "accounting_dimensions_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Dimensions"
+ },
+ {
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "options": "Cost Center"
+ },
+ {
+ "fieldname": "section_break_9",
+ "fieldtype": "Section Break",
+ "label": "Write Off Details"
+ },
+ {
+ "fieldname": "write_off_account",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Write Off Account",
+ "options": "Account",
+ "reqd": 1
+ },
+ {
+ "fieldname": "write_off_amount",
+ "fieldtype": "Currency",
+ "label": "Write Off Amount",
+ "options": "Company:company:default_currency",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_11",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Loan Write Off",
+ "print_hide": 1,
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-10-26 07:13:43.663924",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Loan Write Off",
+ "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
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py
new file mode 100644
index 0000000..54a3f2c
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe, erpnext
+from frappe import _
+from frappe.utils import getdate, flt, cint
+from erpnext.controllers.accounts_controller import AccountsController
+from erpnext.accounts.general_ledger import make_gl_entries
+
+class LoanWriteOff(AccountsController):
+ def validate(self):
+ self.set_missing_values()
+ self.validate_write_off_amount()
+
+ def set_missing_values(self):
+ if not self.cost_center:
+ self.cost_center = erpnext.get_default_cost_center(self.company)
+
+ def validate_write_off_amount(self):
+ precision = cint(frappe.db.get_default("currency_precision")) or 2
+ total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan,
+ ['total_payment', 'total_principal_paid','total_interest_payable', 'written_off_amount'])
+
+ pending_principal_amount = flt(flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount),
+ precision)
+
+ if self.write_off_amount > pending_principal_amount:
+ frappe.throw(_("Write off amount cannot be greater than pending principal amount"))
+
+ def on_submit(self):
+ self.update_outstanding_amount()
+ self.make_gl_entries()
+
+ def on_cancel(self):
+ self.update_outstanding_amount(cancel=1)
+ self.ignore_linked_doctypes = ['GL Entry']
+ self.make_gl_entries(cancel=1)
+
+ def update_outstanding_amount(self, cancel=0):
+ written_off_amount = frappe.db.get_value('Loan', self.loan, 'written_off_amount')
+
+ if cancel:
+ written_off_amount -= self.write_off_amount
+ else:
+ written_off_amount += self.write_off_amount
+
+ frappe.db.set_value('Loan', self.loan, 'written_off_amount', written_off_amount)
+
+
+ def make_gl_entries(self, cancel=0):
+ gl_entries = []
+ loan_details = frappe.get_doc("Loan", self.loan)
+
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": self.write_off_account,
+ "against": loan_details.loan_account,
+ "debit": self.write_off_amount,
+ "debit_in_account_currency": self.write_off_amount,
+ "against_voucher_type": "Loan",
+ "against_voucher": self.loan,
+ "remarks": _("Against Loan:") + self.loan,
+ "cost_center": self.cost_center,
+ "posting_date": getdate(self.posting_date)
+ })
+ )
+
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": loan_details.loan_account,
+ "party_type": loan_details.applicant_type,
+ "party": loan_details.applicant,
+ "against": self.write_off_account,
+ "credit": self.write_off_amount,
+ "credit_in_account_currency": self.write_off_amount,
+ "against_voucher_type": "Loan",
+ "against_voucher": self.loan,
+ "remarks": _("Against Loan:") + self.loan,
+ "cost_center": self.cost_center,
+ "posting_date": getdate(self.posting_date)
+ })
+ )
+
+ make_gl_entries(gl_entries, cancel=cancel, merge_entries=False)
+
+
diff --git a/erpnext/loan_management/doctype/loan_write_off/test_loan_write_off.py b/erpnext/loan_management/doctype/loan_write_off/test_loan_write_off.py
new file mode 100644
index 0000000..9f6700e
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_write_off/test_loan_write_off.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestLoanWriteOff(unittest.TestCase):
+ pass
diff --git a/erpnext/loan_management/doctype/pledge/pledge.json b/erpnext/loan_management/doctype/pledge/pledge.json
index f22a21e..801e3a3 100644
--- a/erpnext/loan_management/doctype/pledge/pledge.json
+++ b/erpnext/loan_management/doctype/pledge/pledge.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2019-09-09 17:06:16.756573",
"doctype": "DocType",
"editable_grid": 1,
@@ -49,7 +50,8 @@
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Quantity"
+ "label": "Quantity",
+ "non_negative": 1
},
{
"fieldname": "loan_security_price",
@@ -86,7 +88,8 @@
}
],
"istable": 1,
- "modified": "2019-12-03 10:59:58.001421",
+ "links": [],
+ "modified": "2020-11-05 10:07:15.424937",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Pledge",
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
index 0ef098f..828df2e 100644
--- 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
@@ -10,6 +10,7 @@
"loan_type",
"loan",
"process_type",
+ "accrual_type",
"amended_from"
],
"fields": [
@@ -47,17 +48,27 @@
"hidden": 1,
"label": "Process Type",
"read_only": 1
+ },
+ {
+ "fieldname": "accrual_type",
+ "fieldtype": "Select",
+ "hidden": 1,
+ "label": "Accrual Type",
+ "options": "Regular\nRepayment\nDisbursement",
+ "read_only": 1
}
],
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-04-09 22:52:53.911416",
+ "modified": "2020-11-06 13:28:51.478909",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Process Loan Interest Accrual",
"owner": "Administrator",
"permissions": [
{
+ "cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
@@ -67,9 +78,11 @@
"report": 1,
"role": "System Manager",
"share": 1,
+ "submit": 1,
"write": 1
},
{
+ "cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
@@ -79,6 +92,7 @@
"report": 1,
"role": "Loan Manager",
"share": 1,
+ "submit": 1,
"write": 1
}
],
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
index 0fa9686..11333dc 100644
--- 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
@@ -20,19 +20,20 @@
if (not self.loan or not loan_doc.is_term_loan) and self.process_type != 'Term Loans':
make_accrual_interest_entry_for_demand_loans(self.posting_date, self.name,
- open_loans = open_loans, loan_type = self.loan_type)
+ open_loans = open_loans, loan_type = self.loan_type, accrual_type=self.accrual_type)
if (not self.loan or loan_doc.is_term_loan) and self.process_type != 'Demand Loans':
make_accrual_interest_entry_for_term_loans(self.posting_date, self.name, term_loan=self.loan,
- loan_type=self.loan_type)
+ loan_type=self.loan_type, accrual_type=self.accrual_type)
-def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type=None, loan=None):
+def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type=None, loan=None, accrual_type="Regular"):
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.process_type = 'Demand Loans'
loan_process.loan = loan
+ loan_process.accrual_type = accrual_type
loan_process.submit()
diff --git a/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json
index aee7c2c..3e7e778 100644
--- a/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json
+++ b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2019-08-29 22:29:37.628178",
"doctype": "DocType",
"editable_grid": 1,
@@ -39,7 +40,8 @@
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Quantity"
+ "label": "Quantity",
+ "non_negative": 1
},
{
"fieldname": "loan_security",
@@ -56,8 +58,10 @@
"read_only": 1
}
],
+ "index_web_pages_for_search": 1,
"istable": 1,
- "modified": "2019-12-02 10:23:11.498308",
+ "links": [],
+ "modified": "2020-11-05 10:07:37.542344",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Proposed Pledge",
diff --git a/erpnext/loan_management/doctype/unpledge/unpledge.json b/erpnext/loan_management/doctype/unpledge/unpledge.json
index ee192d7..0035668 100644
--- a/erpnext/loan_management/doctype/unpledge/unpledge.json
+++ b/erpnext/loan_management/doctype/unpledge/unpledge.json
@@ -52,6 +52,7 @@
"fieldtype": "Float",
"in_list_view": 1,
"label": "Quantity",
+ "non_negative": 1,
"reqd": 1
},
{
@@ -62,9 +63,10 @@
"read_only": 1
}
],
+ "index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-05-06 10:50:18.448552",
+ "modified": "2020-11-05 10:07:28.106961",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Unpledge",
diff --git a/erpnext/loan_management/loan_common.js b/erpnext/loan_management/loan_common.js
index d9dd415..50b68da 100644
--- a/erpnext/loan_management/loan_common.js
+++ b/erpnext/loan_management/loan_common.js
@@ -8,14 +8,14 @@
frm.refresh_field('applicant_type');
}
- if (['Loan Disbursement', 'Loan Repayment', 'Loan Interest Accrual'].includes(frm.doc.doctype)
+ if (['Loan Disbursement', 'Loan Repayment', 'Loan Interest Accrual', 'Loan Write Off'].includes(frm.doc.doctype)
&& frm.doc.docstatus > 0) {
frm.add_custom_button(__("Accounting Ledger"), function() {
frappe.route_options = {
voucher_no: frm.doc.name,
company: frm.doc.company,
- from_date: frm.doc.posting_date,
+ from_date: moment(frm.doc.posting_date).format('YYYY-MM-DD'),
to_date: moment(frm.doc.modified).format('YYYY-MM-DD'),
show_cancelled_entries: frm.doc.docstatus === 2
};
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json
index 850d5ae..7daf706 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.json
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json
@@ -19,6 +19,7 @@
"column_break2",
"from_date",
"to_date",
+ "sales_order_status",
"sales_orders_detail",
"get_sales_orders",
"sales_orders",
@@ -301,13 +302,20 @@
"label": "Warehouses",
"options": "Production Plan Material Request Warehouse",
"read_only": 1
+ },
+ {
+ "depends_on": "eval: doc.get_items_from == \"Sales Order\"",
+ "fieldname": "sales_order_status",
+ "fieldtype": "Select",
+ "label": "Sales Order Status",
+ "options": "\nTo Deliver and Bill\nTo Bill\nTo Deliver"
}
],
"icon": "fa fa-calendar",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-26 13:00:54.335319",
+ "modified": "2020-11-10 18:01:54.991970",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index a314a15..3833e86 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -571,6 +571,8 @@
so_filter += " and so.customer = %(customer)s"
if self.project:
so_filter += " and so.project = %(project)s"
+ if self.sales_order_status:
+ so_filter += "and so.status = %(sales_order_status)s"
if self.item_code:
item_filter += " and so_item.item_code = %(item)s"
@@ -594,8 +596,8 @@
"customer": self.customer,
"project": self.project,
"item": self.item_code,
- "company": self.company
-
+ "company": self.company,
+ "sales_order_status": self.sales_order_status
}, as_dict=1)
return open_so
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index fa9d080..27335aa 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -137,7 +137,8 @@
'from_date': so.transaction_date,
'to_date': so.transaction_date,
'customer': so.customer,
- 'item_code': item
+ 'item_code': item,
+ 'sales_order_status': so.status
})
sales_orders = get_sales_orders(pln) or {}
sales_orders = [d.get('name') for d in sales_orders if d.get('name') == sales_order]
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index a33d401..643e7cf 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -1064,6 +1064,7 @@
so.company = args.company or "_Test Company"
so.customer = args.customer or "_Test Customer"
so.currency = args.currency or "INR"
+ so.po_no = args.po_no or '12345'
if args.selling_price_list:
so.selling_price_list = args.selling_price_list
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index ea385c8..3c5129b 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -413,7 +413,8 @@
{
"fieldname": "company_address_display",
"fieldtype": "Small Text",
- "label": "Company Address"
+ "label": "Company Address",
+ "read_only": 1
},
{
"collapsible": 1,
@@ -1255,7 +1256,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2020-08-03 23:18:47.739997",
+ "modified": "2020-11-11 14:57:16.388139",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 0168613..9566af7 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -442,9 +442,15 @@
self.assertEqual(dn.status, "To Bill")
self.assertEqual(dn.per_billed, 0)
+ # Testing if Customer's Purchase Order No was rightly copied
+ self.assertEqual(dn.po_no, so.po_no)
+
si = make_sales_invoice(dn.name)
si.submit()
+ # Testing if Customer's Purchase Order No was rightly copied
+ self.assertEqual(dn.po_no, si.po_no)
+
dn.load_from_db()
self.assertEqual(dn.get("items")[0].billed_amt, 200)
self.assertEqual(dn.per_billed, 100)
@@ -461,16 +467,25 @@
si.insert()
si.submit()
+ # Testing if Customer's Purchase Order No was rightly copied
+ self.assertEqual(so.po_no, si.po_no)
+
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
dn1 = make_delivery_note(so.name)
dn1.get("items")[0].qty = 2
dn1.submit()
+ # Testing if Customer's Purchase Order No was rightly copied
+ self.assertEqual(so.po_no, dn1.po_no)
+
dn2 = make_delivery_note(so.name)
dn2.get("items")[0].qty = 3
dn2.submit()
+ # Testing if Customer's Purchase Order No was rightly copied
+ self.assertEqual(so.po_no, dn2.po_no)
+
dn1.load_from_db()
self.assertEqual(dn1.get("items")[0].billed_amt, 200)
self.assertEqual(dn1.per_billed, 100)
@@ -492,9 +507,15 @@
dn1.get("items")[0].qty = 2
dn1.submit()
+ # Testing if Customer's Purchase Order No was rightly copied
+ self.assertEqual(dn1.po_no, so.po_no)
+
si1 = make_sales_invoice(dn1.name)
si1.submit()
+ # Testing if Customer's Purchase Order No was rightly copied
+ self.assertEqual(dn1.po_no, si1.po_no)
+
dn1.load_from_db()
self.assertEqual(dn1.per_billed, 100)
@@ -502,10 +523,16 @@
si2.get("items")[0].qty = 4
si2.submit()
+ # Testing if Customer's Purchase Order No was rightly copied
+ self.assertEqual(si2.po_no, so.po_no)
+
dn2 = make_delivery_note(so.name)
dn2.get("items")[0].qty = 5
dn2.submit()
+ # Testing if Customer's Purchase Order No was rightly copied
+ self.assertEqual(dn2.po_no, so.po_no)
+
dn1.load_from_db()
self.assertEqual(dn1.get("items")[0].billed_amt, 200)
self.assertEqual(dn1.per_billed, 100)
@@ -525,9 +552,15 @@
si = make_sales_invoice(so.name)
si.submit()
+ # Testing if Customer's Purchase Order No was rightly copied
+ self.assertEqual(so.po_no, si.po_no)
+
dn = make_delivery_note(si.name)
dn.submit()
+ # Testing if Customer's Purchase Order No was rightly copied
+ self.assertEqual(dn.po_no, si.po_no)
+
self.assertEqual(dn.get("items")[0].billed_amt, 1000)
self.assertEqual(dn.per_billed, 100)
self.assertEqual(dn.status, "Completed")