From d1d0a50a70cb2698018ea5f26c25700b79017438 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Wed, 26 Feb 2020 10:44:24 +0530 Subject: [PATCH] feat: Loan management and accounting (#19035) * fix: Create Loan Management module * fix: Move loan doctype for hr module to loan management * fix: Add loan dashboard * fix: Move loan application form hr module to loan management * fix: Move Loan Type from hr to loan management * fix: Move salary slip loan from hr to loan management * fix: Create loan security pledge doctype * fix: Create loan security type doctype * fix: Create Loan security doctype * fix: Add customer in loan_common.js * fix: Import patch for loan * fix: Add security pledging in loan doctype * fix: Add loan application pledge doctype * fix: Add logic for security pledgeing in loan * fix: Add logic for security pledging in loan application * fix: Rename Loan security doctype to Loan Security Pledge * fix: Add doctype for loan security * fix: Fixes in loan and loan application * fix: Add doctypes for loan repayment * fix: Fixes in loan and loan application * fix: Move loan repayment report from HR module to loan management * fix: Create doctype for loan security price * fix: Create doctype for loan security shortfall * fix: Make fields in Loan shortfall readonly * fix: Make loan type submittable * fix: Add daily job for loan security revaluation in hooks.py * fix: Add loan management module * fix: Doctype for loan disbursement Entry * fix: GL entry fix for loan disbursement * fix: Add company currency options in Loan related doctypes * fix: Changes in Loan Doctypes * fix: Allow miltiple loan securities in Loan Security Pledge * fix: Add proposed pledges in Loan Application * fix: Add test cases for loan * fix: Interest Accrual Entry for loans * fix: Remove loan from journal entry * fix: Update triggers in loan_common.js * fix: Accrual Entries for loan Interest * fix: Proposed Pledges for loan application * fix: Update items in loan management module * fix: Allow multiple disbursements against a loan * fix: Add loan security code in loan security master * fix: Allow multiple securities in a single pledge * fix: Spelling and label fixes * fix: Replace date in loan security price with datetime * fix: Add dashboard to loan master * fix: Move Repayment Schedule from HR to Loan Management * fix: Add back jobs for loan accrual entries * fix: Multiple fixes in loan * fix: Multiple fixes in loan application * fix: Loan Reapayment for term loans * fix: Interest Accural Entries for term loans * fix: Changes in Loan Doctypes * fix: Add test case for term loan repayment * fix: Add custom button to update loan security price and trigger shortfall * fix: Usability fixes in Loan management * fix: Multiple usablity and doctype fixes * fix: Muliple bug and usability fixes in loan * fix: Test case fixes for loan repayment from Salary * fix: Codacy fixes * fix: Test Case fixes * fix: Minor fix in validate_repayment_method * fix: Test case and codacy fixes * fix: Provide consistent naming series to loan doctypes * fix: Loan Application fix * fix: Loan Application Dashboard * fix: Add doctypes to process loan interest accural and loan security price * fix: Provision to make loan topup * fix: Pledge and unpledge statuses and doctype for loan securit unpledge * fix: Multiple fixes in Loan Cycle * fix: Add missing dashboards to loan doctypes * fix: Add Loan Manager role to loan doctypes * fix: Loan Process fixes * fix: Loan Security Unpledge fixes * fix: UX, List and dashboards fixes * fix: Minor fixes * fix: Minor fixes * fix: Status on additional loan security * fix: Codacy fixes * fix: Multiple fixes in loan * fix: Rename Process Loan Security Price to Process Loan Security Shortfall * fix: Loan Repayment and Closure report * fix: Loan Security Status report and minor fixes * fix: Multiple UX fixes * fix: Test Case fixes and UX fixes * fix: Currency symbol fixes in Salary Slip Loan * fix: Make loan account details read only * fix: Changes in loan security price updation * fix: Sanctioned Loan Amount doctype * fix: Updates in process loan interest accrual * fix: Pass loan doc instead of loan name * fix: Fixes in process loan interest accrual * fix: Add missing semicolon * fix: Test case * fix: Test case * fix: Sandbox method to get proposed pledges * fix: Sandbox method * fix: Nonetype fix in loan sanction limit * fix: GL entry fixes * fix: Update maximum loan amount on loan security pledging * fix: Round off loan amounts * fix: Loan Security unpledging * fix: Interest amount for loan closure * fix: Due date for loan repayments * fix: API fixes for loan disbursement method * fix: Disable quick entry for sanctioned loan amount * fix: Add misssing translations for validations and codecleanup * fix: Translation fixes and code cleanup * fix: Test Cases * fix: Loan Interest Accrual test case --- .../doctype/journal_entry/journal_entry.py | 14 - erpnext/config/desktop.py | 9 + erpnext/config/loan_management.py | 107 +++ erpnext/hooks.py | 7 +- erpnext/hr/doctype/loan/loan.js | 229 ----- erpnext/hr/doctype/loan/loan.py | 240 ----- erpnext/hr/doctype/loan/loan_dashboard.py | 26 - erpnext/hr/doctype/loan/test_loan.js | 79 -- erpnext/hr/doctype/loan/test_loan.py | 71 -- .../loan_application/loan_application.js | 42 - .../loan_application/loan_application.json | 840 ------------------ .../loan_application/loan_application.py | 70 -- .../loan_application/test_loan_application.js | 68 -- erpnext/hr/doctype/loan_type/loan_type.js | 7 - erpnext/hr/doctype/loan_type/loan_type.json | 259 ------ .../doctype/loan_type/loan_type_dashboard.py | 12 - .../hr/doctype/loan_type/test_loan_type.js | 31 - .../hr/doctype/payroll_entry/payroll_entry.py | 37 - .../payroll_entry/test_payroll_entry.py | 38 +- erpnext/hr/doctype/salary_slip/salary_slip.py | 65 +- .../doctype/salary_slip/test_salary_slip.py | 69 +- .../salary_slip_loan/salary_slip_loan.json | 263 ------ .../salary_structure/test_salary_structure.py | 15 +- .../report/loan_repayment/loan_repayment.js | 9 - .../report/loan_repayment/loan_repayment.json | 28 - .../report/loan_repayment/loan_repayment.py | 95 -- .../loan => loan_management}/__init__.py | 0 .../doctype}/__init__.py | 0 .../doctype/loan}/__init__.py | 0 erpnext/loan_management/doctype/loan/loan.js | 190 ++++ .../doctype/loan/loan.json | 118 ++- erpnext/loan_management/doctype/loan/loan.py | 254 ++++++ .../doctype/loan/loan_dashboard.py | 19 + .../loan_management/doctype/loan/test_loan.py | 559 ++++++++++++ .../doctype/loan_application}/__init__.py | 0 .../loan_application/loan_application.js | 127 +++ .../loan_application/loan_application.json | 278 ++++++ .../loan_application/loan_application.py | 206 +++++ .../loan_application_dashboard.py | 2 +- .../loan_application/test_loan_application.py | 40 +- .../doctype/loan_disbursement}/__init__.py | 0 .../loan_disbursement/loan_disbursement.js | 17 + .../loan_disbursement/loan_disbursement.json | 165 ++++ .../loan_disbursement/loan_disbursement.py | 114 +++ .../test_loan_disbursement.py | 75 ++ .../loan_interest_accrual}/__init__.py | 0 .../loan_interest_accrual.js | 10 + .../loan_interest_accrual.json | 182 ++++ .../loan_interest_accrual.py | 180 ++++ .../test_loan_interest_accrual.py | 61 ++ .../doctype/loan_repayment/__init__.py | 0 .../doctype/loan_repayment/loan_repayment.js | 64 ++ .../loan_repayment/loan_repayment.json | 267 ++++++ .../doctype/loan_repayment/loan_repayment.py | 315 +++++++ .../loan_repayment/test_loan_repayment.py | 10 + .../doctype/loan_security/__init__.py | 0 .../doctype/loan_security/loan_security.js | 8 + .../doctype/loan_security/loan_security.json | 95 ++ .../doctype/loan_security/loan_security.py | 10 + .../loan_security/loan_security_dashboard.py | 15 + .../loan_security/test_loan_security.py | 10 + .../doctype/loan_security_pledge/__init__.py | 0 .../loan_security_pledge.js | 40 + .../loan_security_pledge.json | 176 ++++ .../loan_security_pledge.py | 51 ++ .../loan_security_pledge_list.js | 15 + .../test_loan_security_pledge.py | 10 + .../doctype/loan_security_price/__init__.py | 0 .../loan_security_price.js | 8 + .../loan_security_price.json | 117 +++ .../loan_security_price.py | 51 ++ .../test_loan_security_price.py | 10 + .../loan_security_shortfall/__init__.py | 0 .../loan_security_shortfall.js | 25 + .../loan_security_shortfall.json | 126 +++ .../loan_security_shortfall.py | 94 ++ .../test_loan_security_shortfall.py | 10 + .../doctype/loan_security_type/__init__.py | 0 .../loan_security_type/loan_security_type.js | 8 + .../loan_security_type.json | 73 ++ .../loan_security_type/loan_security_type.py | 10 + .../loan_security_type_dashboard.py | 15 + .../test_loan_security_type.py | 10 + .../loan_security_unpledge/__init__.py | 0 .../loan_security_unpledge.js | 13 + .../loan_security_unpledge.json | 159 ++++ .../loan_security_unpledge.py | 85 ++ .../loan_security_unpledge_list.js | 14 + .../test_loan_security_unpledge.py | 10 + .../doctype/loan_type/__init__.py | 0 .../doctype/loan_type/loan_type.js | 30 + .../doctype/loan_type/loan_type.json | 170 ++++ .../doctype/loan_type/loan_type.py | 21 + .../doctype/loan_type/loan_type_dashboard.py | 15 + .../doctype/loan_type/test_loan_type.py | 6 +- .../doctype/pledge/__init__.py | 0 .../loan_management/doctype/pledge/pledge.js | 8 + .../doctype/pledge/pledge.json | 99 +++ .../doctype/pledge/pledge.py} | 6 +- .../doctype/pledge/test_pledge.py | 10 + .../process_loan_interest_accrual/__init__.py | 0 .../process_loan_interest_accrual.js | 8 + .../process_loan_interest_accrual.json | 81 ++ .../process_loan_interest_accrual.py | 29 + ...process_loan_interest_accrual_dashboard.py | 12 + .../test_process_loan_interest_accrual.py | 10 + .../__init__.py | 0 .../process_loan_security_shortfall.js | 8 + .../process_loan_security_shortfall.json | 67 ++ .../process_loan_security_shortfall.py | 17 + ...ocess_loan_security_shortfall_dashboard.py | 12 + .../test_process_loan_security_shortfall.py | 10 + .../doctype/proposed_pledge/__init__.py | 0 .../proposed_pledge/proposed_pledge.json | 70 ++ .../proposed_pledge/proposed_pledge.py | 10 + .../doctype/repayment_schedule/__init__.py | 0 .../repayment_schedule.json | 13 +- .../repayment_schedule/repayment_schedule.py | 4 +- .../doctype/salary_slip_loan/__init__.py | 0 .../salary_slip_loan/salary_slip_loan.json | 96 ++ .../salary_slip_loan/salary_slip_loan.py | 4 +- .../sanctioned_loan_amount/__init__.py | 0 .../sanctioned_loan_amount.js | 8 + .../sanctioned_loan_amount.json | 88 ++ .../sanctioned_loan_amount.py | 16 + .../test_sanctioned_loan_amount.py | 10 + .../doctype/unpledge/__init__.py | 0 .../doctype/unpledge/unpledge.json | 84 ++ .../doctype/unpledge/unpledge.py | 10 + .../{hr => loan_management}/loan_common.js | 23 +- erpnext/loan_management/report/__init__.py | 0 .../loan_repayment_and_closure/__init__.py | 0 .../loan_repayment_and_closure.js | 41 + .../loan_repayment_and_closure.json | 27 + .../loan_repayment_and_closure.py | 129 +++ .../report/loan_security_status/__init__.py | 0 .../loan_security_status.js | 46 + .../loan_security_status.json | 27 + .../loan_security_status.py | 135 +++ erpnext/modules.txt | 3 +- erpnext/patches.txt | 2 +- .../v11_0/rename_employee_loan_to_loan.py | 28 - 142 files changed, 6168 insertions(+), 2576 deletions(-) create mode 100644 erpnext/config/loan_management.py delete mode 100644 erpnext/hr/doctype/loan/loan.js delete mode 100644 erpnext/hr/doctype/loan/loan.py delete mode 100644 erpnext/hr/doctype/loan/loan_dashboard.py delete mode 100644 erpnext/hr/doctype/loan/test_loan.js delete mode 100644 erpnext/hr/doctype/loan/test_loan.py delete mode 100644 erpnext/hr/doctype/loan_application/loan_application.js delete mode 100644 erpnext/hr/doctype/loan_application/loan_application.json delete mode 100644 erpnext/hr/doctype/loan_application/loan_application.py delete mode 100644 erpnext/hr/doctype/loan_application/test_loan_application.js delete mode 100644 erpnext/hr/doctype/loan_type/loan_type.js delete mode 100644 erpnext/hr/doctype/loan_type/loan_type.json delete mode 100644 erpnext/hr/doctype/loan_type/loan_type_dashboard.py delete mode 100644 erpnext/hr/doctype/loan_type/test_loan_type.js delete mode 100644 erpnext/hr/doctype/salary_slip_loan/salary_slip_loan.json delete mode 100644 erpnext/hr/report/loan_repayment/loan_repayment.js delete mode 100644 erpnext/hr/report/loan_repayment/loan_repayment.json delete mode 100644 erpnext/hr/report/loan_repayment/loan_repayment.py rename erpnext/{hr/doctype/loan => loan_management}/__init__.py (100%) rename erpnext/{hr/doctype/loan_application => loan_management/doctype}/__init__.py (100%) rename erpnext/{hr/doctype/loan_type => loan_management/doctype/loan}/__init__.py (100%) create mode 100644 erpnext/loan_management/doctype/loan/loan.js rename erpnext/{hr => loan_management}/doctype/loan/loan.json (69%) create mode 100644 erpnext/loan_management/doctype/loan/loan.py create mode 100644 erpnext/loan_management/doctype/loan/loan_dashboard.py create mode 100644 erpnext/loan_management/doctype/loan/test_loan.py rename erpnext/{hr/doctype/repayment_schedule => loan_management/doctype/loan_application}/__init__.py (100%) create mode 100644 erpnext/loan_management/doctype/loan_application/loan_application.js create mode 100644 erpnext/loan_management/doctype/loan_application/loan_application.json create mode 100644 erpnext/loan_management/doctype/loan_application/loan_application.py rename erpnext/{hr => loan_management}/doctype/loan_application/loan_application_dashboard.py (77%) rename erpnext/{hr => loan_management}/doctype/loan_application/test_loan_application.py (53%) rename erpnext/{hr/doctype/salary_slip_loan => loan_management/doctype/loan_disbursement}/__init__.py (100%) create mode 100644 erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.js create mode 100644 erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json create mode 100644 erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py create mode 100644 erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py rename erpnext/{hr/report/loan_repayment => loan_management/doctype/loan_interest_accrual}/__init__.py (100%) create mode 100644 erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.js create mode 100644 erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json create mode 100644 erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py create mode 100644 erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py create mode 100644 erpnext/loan_management/doctype/loan_repayment/__init__.py create mode 100644 erpnext/loan_management/doctype/loan_repayment/loan_repayment.js create mode 100644 erpnext/loan_management/doctype/loan_repayment/loan_repayment.json create mode 100644 erpnext/loan_management/doctype/loan_repayment/loan_repayment.py create mode 100644 erpnext/loan_management/doctype/loan_repayment/test_loan_repayment.py create mode 100644 erpnext/loan_management/doctype/loan_security/__init__.py create mode 100644 erpnext/loan_management/doctype/loan_security/loan_security.js create mode 100644 erpnext/loan_management/doctype/loan_security/loan_security.json create mode 100644 erpnext/loan_management/doctype/loan_security/loan_security.py create mode 100644 erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py create mode 100644 erpnext/loan_management/doctype/loan_security/test_loan_security.py create mode 100644 erpnext/loan_management/doctype/loan_security_pledge/__init__.py create mode 100644 erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js create mode 100644 erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json create mode 100644 erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py create mode 100644 erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge_list.js create mode 100644 erpnext/loan_management/doctype/loan_security_pledge/test_loan_security_pledge.py create mode 100644 erpnext/loan_management/doctype/loan_security_price/__init__.py create mode 100644 erpnext/loan_management/doctype/loan_security_price/loan_security_price.js create mode 100644 erpnext/loan_management/doctype/loan_security_price/loan_security_price.json create mode 100644 erpnext/loan_management/doctype/loan_security_price/loan_security_price.py create mode 100644 erpnext/loan_management/doctype/loan_security_price/test_loan_security_price.py create mode 100644 erpnext/loan_management/doctype/loan_security_shortfall/__init__.py create mode 100644 erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.js create mode 100644 erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json create mode 100644 erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py create mode 100644 erpnext/loan_management/doctype/loan_security_shortfall/test_loan_security_shortfall.py create mode 100644 erpnext/loan_management/doctype/loan_security_type/__init__.py create mode 100644 erpnext/loan_management/doctype/loan_security_type/loan_security_type.js create mode 100644 erpnext/loan_management/doctype/loan_security_type/loan_security_type.json create mode 100644 erpnext/loan_management/doctype/loan_security_type/loan_security_type.py create mode 100644 erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py create mode 100644 erpnext/loan_management/doctype/loan_security_type/test_loan_security_type.py create mode 100644 erpnext/loan_management/doctype/loan_security_unpledge/__init__.py create mode 100644 erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js create mode 100644 erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json create mode 100644 erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py create mode 100644 erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge_list.js create mode 100644 erpnext/loan_management/doctype/loan_security_unpledge/test_loan_security_unpledge.py create mode 100644 erpnext/loan_management/doctype/loan_type/__init__.py create mode 100644 erpnext/loan_management/doctype/loan_type/loan_type.js create mode 100644 erpnext/loan_management/doctype/loan_type/loan_type.json create mode 100644 erpnext/loan_management/doctype/loan_type/loan_type.py create mode 100644 erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py rename erpnext/{hr => loan_management}/doctype/loan_type/test_loan_type.py (53%) create mode 100644 erpnext/loan_management/doctype/pledge/__init__.py create mode 100644 erpnext/loan_management/doctype/pledge/pledge.js create mode 100644 erpnext/loan_management/doctype/pledge/pledge.json rename erpnext/{hr/doctype/loan_type/loan_type.py => loan_management/doctype/pledge/pledge.py} (62%) create mode 100644 erpnext/loan_management/doctype/pledge/test_pledge.py create mode 100644 erpnext/loan_management/doctype/process_loan_interest_accrual/__init__.py create mode 100644 erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.js create mode 100644 erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json create mode 100644 erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py create mode 100644 erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py create mode 100644 erpnext/loan_management/doctype/process_loan_interest_accrual/test_process_loan_interest_accrual.py create mode 100644 erpnext/loan_management/doctype/process_loan_security_shortfall/__init__.py create mode 100644 erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.js create mode 100644 erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json create mode 100644 erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py create mode 100644 erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py create mode 100644 erpnext/loan_management/doctype/process_loan_security_shortfall/test_process_loan_security_shortfall.py create mode 100644 erpnext/loan_management/doctype/proposed_pledge/__init__.py create mode 100644 erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json create mode 100644 erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.py create mode 100644 erpnext/loan_management/doctype/repayment_schedule/__init__.py rename erpnext/{hr => loan_management}/doctype/repayment_schedule/repayment_schedule.json (89%) rename erpnext/{hr => loan_management}/doctype/repayment_schedule/repayment_schedule.py (71%) create mode 100644 erpnext/loan_management/doctype/salary_slip_loan/__init__.py create mode 100644 erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json rename erpnext/{hr => loan_management}/doctype/salary_slip_loan/salary_slip_loan.py (71%) create mode 100644 erpnext/loan_management/doctype/sanctioned_loan_amount/__init__.py create mode 100644 erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.js create mode 100644 erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.json create mode 100644 erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py create mode 100644 erpnext/loan_management/doctype/sanctioned_loan_amount/test_sanctioned_loan_amount.py create mode 100644 erpnext/loan_management/doctype/unpledge/__init__.py create mode 100644 erpnext/loan_management/doctype/unpledge/unpledge.json create mode 100644 erpnext/loan_management/doctype/unpledge/unpledge.py rename erpnext/{hr => loan_management}/loan_common.js (56%) create mode 100644 erpnext/loan_management/report/__init__.py create mode 100644 erpnext/loan_management/report/loan_repayment_and_closure/__init__.py create mode 100644 erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.js create mode 100644 erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.json create mode 100644 erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py create mode 100644 erpnext/loan_management/report/loan_security_status/__init__.py create mode 100644 erpnext/loan_management/report/loan_security_status/loan_security_status.js create mode 100644 erpnext/loan_management/report/loan_security_status/loan_security_status.json create mode 100644 erpnext/loan_management/report/loan_security_status/loan_security_status.py delete mode 100644 erpnext/patches/v11_0/rename_employee_loan_to_loan.py diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 458e4a2526..4491f7a225 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -9,7 +9,6 @@ from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.utils import get_balance_on, get_account_currency from erpnext.accounts.party import get_party_account from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount -from erpnext.hr.doctype.loan.loan import update_disbursement_status, update_total_amount_paid from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get_party_account_based_on_invoice_discounting from six import string_types, iteritems @@ -50,7 +49,6 @@ class JournalEntry(AccountsController): self.make_gl_entries() self.update_advance_paid() self.update_expense_claim() - self.update_loan() self.update_inter_company_jv() self.update_invoice_discounting() @@ -62,7 +60,6 @@ class JournalEntry(AccountsController): self.make_gl_entries(1) self.update_advance_paid() self.update_expense_claim() - self.update_loan() self.unlink_advance_entry_reference() self.unlink_asset_reference() self.unlink_inter_company_jv() @@ -597,17 +594,6 @@ class JournalEntry(AccountsController): doc = frappe.get_doc("Expense Claim", d.reference_name) update_reimbursed_amount(doc) - def update_loan(self): - if self.paid_loan: - paid_loan = json.loads(self.paid_loan) - value = 1 if self.docstatus < 2 else 0 - for name in paid_loan: - frappe.db.set_value("Repayment Schedule", name, "paid", value) - for d in self.accounts: - if d.reference_type=="Loan" and flt(d.debit) > 0: - doc = frappe.get_doc("Loan", d.reference_name) - update_disbursement_status(doc) - update_total_amount_paid(doc) def validate_expense_claim(self): for d in self.accounts: diff --git a/erpnext/config/desktop.py b/erpnext/config/desktop.py index 95f6f7c510..ce7c245a63 100644 --- a/erpnext/config/desktop.py +++ b/erpnext/config/desktop.py @@ -80,6 +80,15 @@ def get_data(): "type": "module", "description": "Sales pipeline, leads, opportunities and customers." }, + { + "module_name": "Loan Management", + "category": "Modules", + "label": _("Loan Management"), + "color": "#EF4DB6", + "icon": "octicon octicon-repo", + "type": "module", + "description": "Loan Management for Customer and Employees" + }, { "module_name": "Support", "category": "Modules", diff --git a/erpnext/config/loan_management.py b/erpnext/config/loan_management.py new file mode 100644 index 0000000000..a84f13abab --- /dev/null +++ b/erpnext/config/loan_management.py @@ -0,0 +1,107 @@ +from __future__ import unicode_literals +from frappe import _ +import frappe + + +def get_data(): + return [ + { + "label": _("Loan"), + "items": [ + { + "type": "doctype", + "name": "Loan Type", + "description": _("Loan Type for interest and penalty rates"), + }, + { + "type": "doctype", + "name": "Loan Application", + "description": _("Loan Applications from customers and employees."), + }, + { + "type": "doctype", + "name": "Loan", + "description": _("Loans provided to customers and employees."), + }, + + ] + }, + { + "label": _("Loan Security"), + "items": [ + { + "type": "doctype", + "name": "Loan Security Type", + }, + { + "type": "doctype", + "name": "Loan Security Price", + }, + { + "type": "doctype", + "name": "Loan Security", + }, + { + "type": "doctype", + "name": "Loan Security Pledge", + }, + { + "type": "doctype", + "name": "Loan Security Unpledge", + }, + { + "type": "doctype", + "name": "Loan Security Shortfall", + }, + ] + }, + { + "label": _("Disbursement and Repayment"), + "items": [ + { + "type": "doctype", + "name": "Loan Disbursement", + }, + { + "type": "doctype", + "name": "Loan Repayment", + }, + { + "type": "doctype", + "name": "Loan Interest Accrual" + } + ] + }, + { + "label": _("Loan Processes"), + "items": [ + { + "type": "doctype", + "name": "Process Loan Security Shortfall", + }, + { + "type": "doctype", + "name": "Process Loan Interest Accrual", + } + ] + }, + { + "label": _("Reports"), + "items": [ + { + "type": "report", + "is_query_report": True, + "name": "Loan Repayment and Closure", + "route": "#query-report/Loan Repayment and Closure", + "doctype": "Loan Repayment", + }, + { + "type": "report", + "is_query_report": True, + "name": "Loan Security Status", + "route": "#query-report/Loan Security Status", + "doctype": "Loan Security Pledge", + } + ] + } + ] \ No newline at end of file diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 774c917f68..54f1a1e452 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -314,12 +314,15 @@ scheduler_events = { "erpnext.setup.doctype.email_digest.email_digest.send", "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms", "erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation", - "erpnext.hr.utils.generate_leave_encashment" + "erpnext.hr.utils.generate_leave_encashment", + "erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall.check_for_ltv_shortfall", + "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.make_accrual_interest_entry_for_term_loans" ], "monthly_long": [ "erpnext.accounts.deferred_revenue.convert_deferred_revenue_to_income", "erpnext.accounts.deferred_revenue.convert_deferred_expense_to_expense", - "erpnext.hr.utils.allocate_earned_leaves" + "erpnext.hr.utils.allocate_earned_leaves", + "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual" ] } diff --git a/erpnext/hr/doctype/loan/loan.js b/erpnext/hr/doctype/loan/loan.js deleted file mode 100644 index 3f5c30c475..0000000000 --- a/erpnext/hr/doctype/loan/loan.js +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -{% include 'erpnext/hr/loan_common.js' %}; - -frappe.ui.form.on('Loan', { - onload: function (frm) { - frm.set_query("loan_application", function () { - return { - "filters": { - "applicant": frm.doc.applicant, - "docstatus": 1, - "status": "Approved" - } - }; - }); - - frm.set_query("interest_income_account", function () { - return { - "filters": { - "company": frm.doc.company, - "root_type": "Income", - "is_group": 0 - } - }; - }); - - $.each(["payment_account", "loan_account"], function (i, field) { - frm.set_query(field, function () { - return { - "filters": { - "company": frm.doc.company, - "root_type": "Asset", - "is_group": 0 - } - }; - }); - }) - }, - - refresh: function (frm) { - if (frm.doc.docstatus == 1) { - if (frm.doc.status == "Sanctioned") { - frm.add_custom_button(__('Create Disbursement Entry'), function() { - frm.trigger("make_jv"); - }).addClass("btn-primary"); - } else if (frm.doc.status == "Disbursed" && frm.doc.repayment_start_date && (frm.doc.applicant_type == 'Member' || frm.doc.repay_from_salary == 0)) { - frm.add_custom_button(__('Create Repayment Entry'), function() { - frm.trigger("make_repayment_entry"); - }).addClass("btn-primary"); - } - } - frm.trigger("toggle_fields"); - }, - - make_jv: function (frm) { - frappe.call({ - args: { - "loan": frm.doc.name, - "company": frm.doc.company, - "loan_account": frm.doc.loan_account, - "applicant_type": frm.doc.applicant_type, - "applicant": frm.doc.applicant, - "loan_amount": frm.doc.loan_amount, - "payment_account": frm.doc.payment_account - }, - method: "erpnext.hr.doctype.loan.loan.make_jv_entry", - callback: function (r) { - if (r.message) - var doc = frappe.model.sync(r.message)[0]; - frappe.set_route("Form", doc.doctype, doc.name); - } - }) - }, - make_repayment_entry: function(frm) { - var repayment_schedule = $.map(frm.doc.repayment_schedule, function(d) { return d.paid ? d.payment_date : false; }); - if(repayment_schedule.length >= 1){ - frm.repayment_data = []; - frm.show_dialog = 1; - let title = ""; - let fields = [ - {fieldtype:'Section Break', label: __('Repayment Schedule')}, - {fieldname: 'payments', fieldtype: 'Table', - fields: [ - { - fieldtype:'Data', - fieldname:'payment_date', - label: __('Date'), - read_only:1, - in_list_view: 1, - columns: 2 - }, - { - fieldtype:'Currency', - fieldname:'principal_amount', - label: __('Principal Amount'), - read_only:1, - in_list_view: 1, - columns: 3 - }, - { - fieldtype:'Currency', - fieldname:'interest_amount', - label: __('Interest'), - read_only:1, - in_list_view: 1, - columns: 2 - }, - { - fieldtype:'Currency', - read_only:1, - fieldname:'total_payment', - label: __('Total Payment'), - in_list_view: 1, - columns: 3 - }, - ], - data: frm.repayment_data, - get_data: function() { - return frm.repayment_data; - } - } - ] - - var dialog = new frappe.ui.Dialog({ - title: title, fields: fields, - }); - if (frm.doc['repayment_schedule']) { - frm.doc['repayment_schedule'].forEach((payment, index) => { - if (payment.paid == 0 && payment.payment_date <= frappe.datetime.now_date()) { - frm.repayment_data.push ({ - 'id': index, - 'name': payment.name, - 'payment_date': payment.payment_date, - 'principal_amount': payment.principal_amount, - 'interest_amount': payment.interest_amount, - 'total_payment': payment.total_payment - }); - dialog.fields_dict.payments.grid.refresh(); - $(dialog.wrapper.find(".grid-buttons")).hide(); - $(`.octicon.octicon-triangle-down`).hide(); - } - - }) - } - - dialog.show() - dialog.set_primary_action(__('Create Repayment Entry'), function() { - frm.values = dialog.get_values(); - if(frm.values) { - _make_repayment_entry(frm, dialog.fields_dict.payments.grid.get_selected_children()); - dialog.hide() - } - }); - } - - dialog.get_close_btn().on('click', () => { - dialog.hide(); - }); - }, - - mode_of_payment: function (frm) { - if (frm.doc.mode_of_payment && frm.doc.company) { - frappe.call({ - method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.get_bank_cash_account", - args: { - "mode_of_payment": frm.doc.mode_of_payment, - "company": frm.doc.company - }, - callback: function (r, rt) { - if (r.message) { - frm.set_value("payment_account", r.message.account); - } - } - }); - } - }, - - loan_application: function (frm) { - if(frm.doc.loan_application){ - return frappe.call({ - method: "erpnext.hr.doctype.loan.loan.get_loan_application", - args: { - "loan_application": frm.doc.loan_application - }, - callback: function (r) { - if (!r.exc && r.message) { - frm.set_value("loan_type", r.message.loan_type); - frm.set_value("loan_amount", r.message.loan_amount); - frm.set_value("repayment_method", r.message.repayment_method); - frm.set_value("monthly_repayment_amount", r.message.repayment_amount); - frm.set_value("repayment_periods", r.message.repayment_periods); - frm.set_value("rate_of_interest", r.message.rate_of_interest); - } - } - }); - } - }, - - repayment_method: function (frm) { - frm.trigger("toggle_fields") - }, - - toggle_fields: function (frm) { - frm.toggle_enable("monthly_repayment_amount", frm.doc.repayment_method == "Repay Fixed Amount per Period") - frm.toggle_enable("repayment_periods", frm.doc.repayment_method == "Repay Over Number of Periods") - } -}); - -var _make_repayment_entry = function(frm, payment_rows) { - frappe.call({ - method:"erpnext.hr.doctype.loan.loan.make_repayment_entry", - args: { - payment_rows: payment_rows, - "loan": frm.doc.name, - "company": frm.doc.company, - "loan_account": frm.doc.loan_account, - "applicant_type": frm.doc.applicant_type, - "applicant": frm.doc.applicant, - "payment_account": frm.doc.payment_account, - "interest_income_account": frm.doc.interest_income_account - }, - callback: function(r) { - if (r.message) - var doc = frappe.model.sync(r.message)[0]; - frappe.set_route("Form", doc.doctype, doc.name, {'payment_rows': payment_rows}); - } - }); -} \ No newline at end of file diff --git a/erpnext/hr/doctype/loan/loan.py b/erpnext/hr/doctype/loan/loan.py deleted file mode 100644 index a803863124..0000000000 --- a/erpnext/hr/doctype/loan/loan.py +++ /dev/null @@ -1,240 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe, math, json -import erpnext -from frappe import _ -from frappe.utils import flt, rounded, add_months, nowdate, getdate -from erpnext.controllers.accounts_controller import AccountsController - -class Loan(AccountsController): - def validate(self): - validate_repayment_method(self.repayment_method, self.loan_amount, self.monthly_repayment_amount, self.repayment_periods) - self.set_missing_fields() - self.make_repayment_schedule() - self.set_repayment_period() - self.calculate_totals() - - def set_missing_fields(self): - if not self.company: - self.company = erpnext.get_default_company() - - if not self.posting_date: - self.posting_date = nowdate() - - if self.loan_type and not self.rate_of_interest: - self.rate_of_interest = frappe.db.get_value("Loan Type", self.loan_type, "rate_of_interest") - - if self.repayment_method == "Repay Over Number of Periods": - self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods) - - if self.status == "Repaid/Closed": - self.total_amount_paid = self.total_payment - - - def make_jv_entry(self): - self.check_permission('write') - journal_entry = frappe.new_doc('Journal Entry') - journal_entry.voucher_type = 'Bank Entry' - journal_entry.user_remark = _('Against Loan: {0}').format(self.name) - journal_entry.company = self.company - journal_entry.posting_date = nowdate() - - account_amt_list = [] - - account_amt_list.append({ - "account": self.loan_account, - "party_type": self.applicant_type, - "party": self.applicant, - "debit_in_account_currency": self.loan_amount, - "reference_type": "Loan", - "reference_name": self.name, - }) - account_amt_list.append({ - "account": self.payment_account, - "credit_in_account_currency": self.loan_amount, - "reference_type": "Loan", - "reference_name": self.name, - }) - journal_entry.set("accounts", account_amt_list) - return journal_entry.as_dict() - - def make_repayment_schedule(self): - self.repayment_schedule = [] - payment_date = self.repayment_start_date - balance_amount = self.loan_amount - while(balance_amount > 0): - interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100)) - principal_amount = self.monthly_repayment_amount - interest_amount - balance_amount = rounded(balance_amount + interest_amount - self.monthly_repayment_amount) - - if balance_amount < 0: - principal_amount += balance_amount - balance_amount = 0.0 - - total_payment = principal_amount + interest_amount - self.append("repayment_schedule", { - "payment_date": payment_date, - "principal_amount": principal_amount, - "interest_amount": interest_amount, - "total_payment": total_payment, - "balance_loan_amount": balance_amount - }) - next_payment_date = add_months(payment_date, 1) - payment_date = next_payment_date - - def set_repayment_period(self): - if self.repayment_method == "Repay Fixed Amount per Period": - repayment_periods = len(self.repayment_schedule) - - self.repayment_periods = repayment_periods - - def calculate_totals(self): - self.total_payment = 0 - self.total_interest_payable = 0 - self.total_amount_paid = 0 - for data in self.repayment_schedule: - self.total_payment += data.total_payment - self.total_interest_payable +=data.interest_amount - if data.paid: - self.total_amount_paid += data.total_payment - -def update_total_amount_paid(doc): - total_amount_paid = 0 - for data in doc.repayment_schedule: - if data.paid: - total_amount_paid += data.total_payment - frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid) - -def update_disbursement_status(doc): - disbursement = frappe.db.sql(""" - select posting_date, ifnull(sum(credit_in_account_currency), 0) as disbursed_amount - from `tabGL Entry` - where account = %s and against_voucher_type = 'Loan' and against_voucher = %s - """, (doc.payment_account, doc.name), as_dict=1)[0] - - disbursement_date = None - if not disbursement or disbursement.disbursed_amount == 0: - status = "Sanctioned" - elif disbursement.disbursed_amount == doc.loan_amount: - disbursement_date = disbursement.posting_date - status = "Disbursed" - elif disbursement.disbursed_amount > doc.loan_amount: - frappe.throw(_("Disbursed Amount cannot be greater than Loan Amount {0}").format(doc.loan_amount)) - - if status == 'Disbursed' and getdate(disbursement_date) > getdate(frappe.db.get_value("Loan", doc.name, "repayment_start_date")): - frappe.throw(_("Disbursement Date cannot be after Loan Repayment Start Date")) - - frappe.db.sql(""" - update `tabLoan` - set status = %s, disbursement_date = %s - where name = %s - """, (status, disbursement_date, doc.name)) - -def validate_repayment_method(repayment_method, loan_amount, monthly_repayment_amount, repayment_periods): - if repayment_method == "Repay Over Number of Periods" and not repayment_periods: - frappe.throw(_("Please enter Repayment Periods")) - - if repayment_method == "Repay Fixed Amount per Period": - if not monthly_repayment_amount: - frappe.throw(_("Please enter repayment Amount")) - if monthly_repayment_amount > loan_amount: - frappe.throw(_("Monthly Repayment Amount cannot be greater than Loan Amount")) - -def get_monthly_repayment_amount(repayment_method, loan_amount, rate_of_interest, repayment_periods): - if rate_of_interest: - monthly_interest_rate = flt(rate_of_interest) / (12 *100) - monthly_repayment_amount = math.ceil((loan_amount * monthly_interest_rate * - (1 + monthly_interest_rate)**repayment_periods) \ - / ((1 + monthly_interest_rate)**repayment_periods - 1)) - else: - monthly_repayment_amount = math.ceil(flt(loan_amount) / repayment_periods) - return monthly_repayment_amount - -@frappe.whitelist() -def get_loan_application(loan_application): - loan = frappe.get_doc("Loan Application", loan_application) - if loan: - return loan.as_dict() - -@frappe.whitelist() -def make_repayment_entry(payment_rows, loan, company, loan_account, applicant_type, applicant, \ - payment_account=None, interest_income_account=None): - - if isinstance(payment_rows, frappe.string_types): - payment_rows_list = json.loads(payment_rows) - else: - frappe.throw(_("No repayments available for Journal Entry")) - - if payment_rows_list: - row_name = list(set(d["name"] for d in payment_rows_list)) - else: - frappe.throw(_("No repayments selected for Journal Entry")) - total_payment = 0 - principal_amount = 0 - interest_amount = 0 - for d in payment_rows_list: - total_payment += d["total_payment"] - principal_amount += d["principal_amount"] - interest_amount += d["interest_amount"] - - journal_entry = frappe.new_doc('Journal Entry') - journal_entry.voucher_type = 'Bank Entry' - journal_entry.user_remark = _('Against Loan: {0}').format(loan) - journal_entry.company = company - journal_entry.posting_date = nowdate() - journal_entry.paid_loan = json.dumps(row_name) - account_amt_list = [] - - account_amt_list.append({ - "account": payment_account, - "debit_in_account_currency": total_payment, - "reference_type": "Loan", - "reference_name": loan, - }) - account_amt_list.append({ - "account": loan_account, - "credit_in_account_currency": principal_amount, - "party_type": applicant_type, - "party": applicant, - "reference_type": "Loan", - "reference_name": loan, - }) - account_amt_list.append({ - "account": interest_income_account, - "credit_in_account_currency": interest_amount, - "reference_type": "Loan", - "reference_name": loan, - }) - journal_entry.set("accounts", account_amt_list) - - return journal_entry.as_dict() - -@frappe.whitelist() -def make_jv_entry(loan, company, loan_account, applicant_type, applicant, loan_amount,payment_account=None): - - journal_entry = frappe.new_doc('Journal Entry') - journal_entry.voucher_type = 'Bank Entry' - journal_entry.user_remark = _('Against Loan: {0}').format(loan) - journal_entry.company = company - journal_entry.posting_date = nowdate() - account_amt_list = [] - - account_amt_list.append({ - "account": loan_account, - "debit_in_account_currency": loan_amount, - "party_type": applicant_type, - "party": applicant, - "reference_type": "Loan", - "reference_name": loan, - }) - account_amt_list.append({ - "account": payment_account, - "credit_in_account_currency": loan_amount, - "reference_type": "Loan", - "reference_name": loan, - }) - journal_entry.set("accounts", account_amt_list) - return journal_entry.as_dict() diff --git a/erpnext/hr/doctype/loan/loan_dashboard.py b/erpnext/hr/doctype/loan/loan_dashboard.py deleted file mode 100644 index 7256d9424a..0000000000 --- a/erpnext/hr/doctype/loan/loan_dashboard.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import unicode_literals -from frappe import _ - -def get_data(): - return { - 'fieldname': 'applicant', - 'non_standard_fieldnames': { - 'Journal Entry': 'reference_name', - 'Salary Slip': 'employee' - }, - 'transactions': [ - { - 'label': _('Applicant'), - 'items': ['Loan Application'] - }, - - { - 'label': _('Account'), - 'items': ['Journal Entry'] - }, - { - 'label': _('Employee'), - 'items': ['Salary Slip'] - } - ] - } \ No newline at end of file diff --git a/erpnext/hr/doctype/loan/test_loan.js b/erpnext/hr/doctype/loan/test_loan.js deleted file mode 100644 index 28d30c9832..0000000000 --- a/erpnext/hr/doctype/loan/test_loan.js +++ /dev/null @@ -1,79 +0,0 @@ - -QUnit.test("Test Loan [HR]", function(assert) { - assert.expect(8); - let done = assert.async(); - let employee_name; - - // To create a loan and check principal,interest and balance amount - let loan_creation = (ename,lname) => { - return frappe.run_serially([ - () => frappe.db.get_value('Employee', {'employee_name': ename}, 'name'), - (r) => { - employee_name = r.message.name; - }, - () => frappe.db.get_value('Loan Application', {'loan_type': lname}, 'name'), - (r) => { - // Creating loan for an employee - return frappe.tests.make('Loan', [ - { company: 'For Testing'}, - { posting_date: '2017-08-26'}, - { applicant: employee_name}, - { loan_application: r.message.name}, - { disbursement_date: '2018-08-26'}, - { mode_of_payment: 'Cash'}, - { loan_account: 'Temporary Opening - FT'}, - { interest_income_account: 'Service - FT'} - ]); - }, - () => frappe.timeout(3), - () => frappe.click_button('Submit'), - () => frappe.timeout(1), - () => frappe.click_button('Yes'), - () => frappe.timeout(3), - - // Checking if all the amounts are correctly calculated - () => { - assert.ok(cur_frm.get_field('applicant_name').value=='Test Employee 1'&& - (cur_frm.get_field('status').value=='Sanctioned'), - 'Loan Sanctioned for correct employee'); - - assert.equal(7270, - cur_frm.get_doc('repayment_schedule').repayment_schedule[0].principal_amount, - 'Principal amount for first instalment is correctly calculated'); - - assert.equal(2333, - cur_frm.get_doc('repayment_schedule').repayment_schedule[0].interest_amount, - 'Interest amount for first instalment is correctly calculated'); - - assert.equal(192730, - cur_frm.get_doc('repayment_schedule').repayment_schedule[0].balance_loan_amount, - 'Balance amount after first instalment is correctly calculated'); - - assert.equal(9479, - cur_frm.get_doc('repayment_schedule').repayment_schedule[23].principal_amount, - 'Principal amount for last instalment is correctly calculated'); - - assert.equal(111, - cur_frm.get_doc('repayment_schedule').repayment_schedule[23].interest_amount, - 'Interest amount for last instalment is correctly calculated'); - - assert.equal(0, - cur_frm.get_doc('repayment_schedule').repayment_schedule[23].balance_loan_amount, - 'Balance amount after last instalment is correctly calculated'); - - }, - () => frappe.set_route('List','Loan','List'), - () => frappe.timeout(2), - - // Checking the submission of Loan - () => { - assert.ok(cur_list.data[0].docstatus==1,'Loan sanctioned and submitted successfully'); - }, - ]); - }; - frappe.run_serially([ - // Creating loan - () => loan_creation('Test Employee 1','Test Loan'), - () => done() - ]); -}); diff --git a/erpnext/hr/doctype/loan/test_loan.py b/erpnext/hr/doctype/loan/test_loan.py deleted file mode 100644 index 740e429510..0000000000 --- a/erpnext/hr/doctype/loan/test_loan.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals - -import frappe -import erpnext -import unittest -from frappe.utils import nowdate, add_days -from erpnext.hr.doctype.salary_structure.test_salary_structure import make_employee - -class TestLoan(unittest.TestCase): - def setUp(self): - create_loan_type("Personal Loan", 500000, 8.4) - self.applicant = make_employee("robert_loan@loan.com") - create_loan(self.applicant, "Personal Loan", 280000, "Repay Over Number of Periods", 20) - - def test_loan(self): - loan = frappe.get_doc("Loan", {"applicant":self.applicant}) - self.assertEquals(loan.monthly_repayment_amount, 15052) - self.assertEquals(loan.total_interest_payable, 21034) - self.assertEquals(loan.total_payment, 301034) - - schedule = loan.repayment_schedule - - self.assertEqual(len(schedule), 20) - - for idx, principal_amount, interest_amount, balance_loan_amount in [[3, 13369, 1683, 227079], [19, 14941, 105, 0], [17, 14740, 312, 29785]]: - self.assertEqual(schedule[idx].principal_amount, principal_amount) - self.assertEqual(schedule[idx].interest_amount, interest_amount) - self.assertEqual(schedule[idx].balance_loan_amount, balance_loan_amount) - - loan.repayment_method = "Repay Fixed Amount per Period" - loan.monthly_repayment_amount = 14000 - loan.save() - - self.assertEquals(len(loan.repayment_schedule), 22) - self.assertEquals(loan.total_interest_payable, 22712) - self.assertEquals(loan.total_payment, 302712) - -def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest): - if not frappe.db.exists("Loan Type", loan_name): - frappe.get_doc({ - "doctype": "Loan Type", - "loan_name": loan_name, - "maximum_loan_amount": maximum_loan_amount, - "rate_of_interest": rate_of_interest - }).insert() - -def create_loan(applicant, loan_type, loan_amount, repayment_method, repayment_periods): - create_loan_type(loan_type, 500000, 8.4) - if not frappe.db.get_value("Loan", {"applicant":applicant}): - loan = frappe.new_doc("Loan") - loan.update({ - "applicant": applicant, - "loan_type": loan_type, - "loan_amount": loan_amount, - "repayment_method": repayment_method, - "repayment_periods": repayment_periods, - "disbursement_date": nowdate(), - "repayment_start_date": nowdate(), - "status": "Disbursed", - "mode_of_payment": frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'), - "payment_account": frappe.db.get_value('Account', {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name"), - "loan_account": frappe.db.get_value('Account', {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name"), - "interest_income_account": frappe.db.get_value('Account', {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name") - }) - loan.insert() - return loan - else: - return frappe.get_doc("Loan", {"applicant":applicant}) \ No newline at end of file diff --git a/erpnext/hr/doctype/loan_application/loan_application.js b/erpnext/hr/doctype/loan_application/loan_application.js deleted file mode 100644 index a73b62a894..0000000000 --- a/erpnext/hr/doctype/loan_application/loan_application.js +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -{% include 'erpnext/hr/loan_common.js' %}; - -frappe.ui.form.on('Loan Application', { - refresh: function(frm) { - frm.trigger("toggle_fields") - frm.trigger("add_toolbar_buttons") - }, - repayment_method: function(frm) { - frm.doc.repayment_amount = frm.doc.repayment_periods = "" - frm.trigger("toggle_fields") - frm.trigger("toggle_required") - }, - toggle_fields: function(frm) { - frm.toggle_enable("repayment_amount", frm.doc.repayment_method=="Repay Fixed Amount per Period") - frm.toggle_enable("repayment_periods", frm.doc.repayment_method=="Repay Over Number of Periods") - }, - toggle_required: function(frm){ - frm.toggle_reqd("repayment_amount", cint(frm.doc.repayment_method=='Repay Fixed Amount per Period')) - frm.toggle_reqd("repayment_periods", cint(frm.doc.repayment_method=='Repay Over Number of Periods')) - }, - add_toolbar_buttons: function(frm) { - if (frm.doc.status == "Approved") { - frm.add_custom_button(__('Create Loan'), function() { - frappe.call({ - method: "erpnext.hr.doctype.loan_application.loan_application.make_loan", - args: { - "source_name": frm.doc.name - }, - callback: function(r) { - if(!r.exc) { - var doc = frappe.model.sync(r.message); - frappe.set_route("Form", r.message.doctype, r.message.name); - } - } - }); - }).addClass("btn-primary"); - } - } -}); diff --git a/erpnext/hr/doctype/loan_application/loan_application.json b/erpnext/hr/doctype/loan_application/loan_application.json deleted file mode 100644 index cc73a86ed0..0000000000 --- a/erpnext/hr/doctype/loan_application/loan_application.json +++ /dev/null @@ -1,840 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "ACC-LOAP-.YYYY.-.#####", - "beta": 0, - "creation": "2016-12-02 12:35:56.046811", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "applicant_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Applicant Type", - "length": 0, - "no_copy": 0, - "options": "Employee\nMember", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "applicant", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 1, - "label": "Applicant", - "length": 0, - "no_copy": 0, - "options": "applicant_type", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "applicant", - "fieldname": "applicant_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Applicant Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fieldname": "posting_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Posting Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "status", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Status", - "length": 0, - "no_copy": 1, - "options": "Open\nApproved\nRejected", - "permlevel": 1, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_4", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Loan Info", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "loan_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Loan Type", - "length": 0, - "no_copy": 0, - "options": "Loan Type", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "loan_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Loan Amount", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "required_by_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Required by Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_7", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Reason", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "repayment_info", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Repayment Info", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "repayment_method", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Repayment Method", - "length": 0, - "no_copy": 0, - "options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "loan_type.rate_of_interest", - "fieldname": "rate_of_interest", - "fieldtype": "Percent", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Rate of Interest", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "total_payable_interest", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Payable Interest", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_11", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "repayment_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Monthly Repayment Amount", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "repayment_periods", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Repayment Period in Months", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "total_payable_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Payable Amount", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amended_from", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Loan Application", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-08-21 16:15:53.688596", - "modified_by": "Administrator", - "module": "HR", - "name": "Loan Application", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Employee", - "set_user_permissions": 0, - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Employee", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 0 - } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "applicant_type, applicant, loan_type, loan_amount", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "timeline_field": "applicant", - "title_field": "applicant", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} \ No newline at end of file diff --git a/erpnext/hr/doctype/loan_application/loan_application.py b/erpnext/hr/doctype/loan_application/loan_application.py deleted file mode 100644 index 582bf48bf0..0000000000 --- a/erpnext/hr/doctype/loan_application/loan_application.py +++ /dev/null @@ -1,70 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe, math -from frappe import _ -from frappe.utils import flt, rounded -from frappe.model.mapper import get_mapped_doc -from frappe.model.document import Document - -from erpnext.hr.doctype.loan.loan import get_monthly_repayment_amount, validate_repayment_method - -class LoanApplication(Document): - def validate(self): - validate_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount, self.repayment_periods) - self.validate_loan_amount() - self.get_repayment_details() - - def validate_loan_amount(self): - maximum_loan_limit = frappe.db.get_value('Loan Type', self.loan_type, 'maximum_loan_amount') - if maximum_loan_limit and self.loan_amount > maximum_loan_limit: - frappe.throw(_("Loan Amount cannot exceed Maximum Loan Amount of {0}").format(maximum_loan_limit)) - - def get_repayment_details(self): - if self.repayment_method == "Repay Over Number of Periods": - self.repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods) - - if self.repayment_method == "Repay Fixed Amount per Period": - monthly_interest_rate = flt(self.rate_of_interest) / (12 *100) - if monthly_interest_rate: - min_repayment_amount = self.loan_amount*monthly_interest_rate - if (self.repayment_amount - min_repayment_amount) <= 0: - frappe.throw(_("Repayment Amount must be greater than " \ - + str(flt(min_repayment_amount, 2)))) - self.repayment_periods = math.ceil((math.log(self.repayment_amount) - - math.log(self.repayment_amount - min_repayment_amount)) /(math.log(1 + monthly_interest_rate))) - else: - self.repayment_periods = self.loan_amount / self.repayment_amount - - self.calculate_payable_amount() - - def calculate_payable_amount(self): - balance_amount = self.loan_amount - self.total_payable_amount = 0 - self.total_payable_interest = 0 - - while(balance_amount > 0): - interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100)) - balance_amount = rounded(balance_amount + interest_amount - self.repayment_amount) - - self.total_payable_interest += interest_amount - - self.total_payable_amount = self.loan_amount + self.total_payable_interest - -@frappe.whitelist() -def make_loan(source_name, target_doc = None): - doclist = get_mapped_doc("Loan Application", source_name, { - "Loan Application": { - "doctype": "Loan", - "field_map": { - "repayment_amount": "monthly_repayment_amount" - }, - "validation": { - "docstatus": ["=", 1] - } - } - }, target_doc) - - return doclist diff --git a/erpnext/hr/doctype/loan_application/test_loan_application.js b/erpnext/hr/doctype/loan_application/test_loan_application.js deleted file mode 100644 index b8789c7a87..0000000000 --- a/erpnext/hr/doctype/loan_application/test_loan_application.js +++ /dev/null @@ -1,68 +0,0 @@ -QUnit.module('hr'); - -QUnit.test("Test: Loan Application [HR]", function (assert) { - assert.expect(8); - let done = assert.async(); - let employee_name; - - frappe.run_serially([ - // Creation of Loan Application - () => frappe.db.get_value('Employee', {'employee_name': 'Test Employee 1'}, 'name'), - (r) => { - employee_name = r.message.name; - }, - () => { - return frappe.tests.make('Loan Application', [ - { company: 'For Testing'}, - { applicant: employee_name}, - { applicant_name: 'Test Employee 1'}, - { status: 'Approved'}, - { loan_type: 'Test Loan '}, - { loan_amount: 200000}, - { description: 'This is just a test'}, - { repayment_method: 'Repay Over Number of Periods'}, - { repayment_periods: 24}, - { rate_of_interest: 14} - ]); - }, - () => frappe.timeout(6), - () => frappe.click_button('Submit'), - () => frappe.timeout(1), - () => frappe.click_button('Yes'), - () => frappe.timeout(2), - () => { - // To check if all the amounts are correctly calculated - - assert.ok(cur_frm.get_field('applicant_name').value == 'Test Employee 1', - 'Application created successfully'); - - assert.ok(cur_frm.get_field('status').value=='Approved', - 'Status of application is correctly set'); - - assert.ok(cur_frm.get_field('loan_type').value=='Test Loan', - 'Application is created for correct Loan Type'); - - assert.ok(cur_frm.get_field('status').value=='Approved', - 'Status of application is correctly set'); - - assert.ok(cur_frm.get_field('repayment_amount').value==9603, - 'Repayment amount is correctly calculated'); - - assert.ok(cur_frm.get_field('total_payable_interest').value==30459, - 'Interest amount is correctly calculated'); - - assert.ok(cur_frm.get_field('total_payable_amount').value==230459, - 'Total payable amount is correctly calculated'); - }, - - () => frappe.set_route('List','Loan Application','List'), - () => frappe.timeout(2), - - // Checking the submission of Loan Application - () => { - assert.ok(cur_list.data[0].docstatus==1,'Loan Application submitted successfully'); - }, - () => frappe.timeout(1), - () => done() - ]); -}); \ No newline at end of file diff --git a/erpnext/hr/doctype/loan_type/loan_type.js b/erpnext/hr/doctype/loan_type/loan_type.js deleted file mode 100644 index 72f5775add..0000000000 --- a/erpnext/hr/doctype/loan_type/loan_type.js +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Loan Type', { - refresh: function(frm) { - } -}); diff --git a/erpnext/hr/doctype/loan_type/loan_type.json b/erpnext/hr/doctype/loan_type/loan_type.json deleted file mode 100644 index e595187e18..0000000000 --- a/erpnext/hr/doctype/loan_type/loan_type.json +++ /dev/null @@ -1,259 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:loan_name", - "beta": 0, - "creation": "2016-12-02 10:41:40.732843", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "loan_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Loan Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "maximum_loan_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Maximum Loan Amount", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "rate_of_interest", - "fieldtype": "Percent", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Rate of Interest (%) Yearly", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "disabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Disabled", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2017-03-29 21:23:08.665245", - "modified_by": "Administrator", - "module": "HR", - "name": "Loan Type", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Employee", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/hr/doctype/loan_type/loan_type_dashboard.py b/erpnext/hr/doctype/loan_type/loan_type_dashboard.py deleted file mode 100644 index 07b11fe627..0000000000 --- a/erpnext/hr/doctype/loan_type/loan_type_dashboard.py +++ /dev/null @@ -1,12 +0,0 @@ -from __future__ import unicode_literals -from frappe import _ - -def get_data(): - return { - 'fieldname': 'loan_type', - 'transactions': [ - { - 'items': ['Loan Application'] - }, - ], - } \ No newline at end of file diff --git a/erpnext/hr/doctype/loan_type/test_loan_type.js b/erpnext/hr/doctype/loan_type/test_loan_type.js deleted file mode 100644 index 71354be48d..0000000000 --- a/erpnext/hr/doctype/loan_type/test_loan_type.js +++ /dev/null @@ -1,31 +0,0 @@ -QUnit.module('hr'); - -QUnit.test("Test: Loan Type [HR]", function (assert) { - assert.expect(3); - let done = assert.async(); - - frappe.run_serially([ - // Loan Type creation - () => { - frappe.tests.make('Loan Type', [ - { loan_name: 'Test Loan'}, - { maximum_loan_amount: 400000}, - { rate_of_interest: 14}, - { description: - 'This is just a test.'} - ]); - }, - () => frappe.timeout(7), - () => frappe.set_route('List','Loan Type','List'), - () => frappe.timeout(4), - - // Checking if the fields are correctly set - () => { - assert.ok(cur_list.data.length==1, 'Loan Type created successfully'); - assert.ok(cur_list.data[0].name=='Test Loan', 'Loan title Correctly set'); - assert.ok(cur_list.data[0].disabled==0, 'Loan enabled'); - }, - () => done() - ]); -}); - diff --git a/erpnext/hr/doctype/payroll_entry/payroll_entry.py b/erpnext/hr/doctype/payroll_entry/payroll_entry.py index dfd38ebbe3..9ef3a99930 100644 --- a/erpnext/hr/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/hr/doctype/payroll_entry/payroll_entry.py @@ -157,19 +157,6 @@ class PayrollEntry(Document): for ss in submitted_ss: ss.email_salary_slip() - def get_loan_details(self): - """ - Get loan details from submitted salary slip based on selected criteria - """ - cond = self.get_filter_condition() - return frappe.db.sql(""" select eld.loan_account, eld.loan, - eld.interest_income_account, eld.principal_amount, eld.interest_amount, eld.total_payment,t1.employee - from - `tabSalary Slip` t1, `tabSalary Slip Loan` eld - where - t1.docstatus = 1 and t1.name = eld.parent and start_date >= %s and end_date <= %s %s - """ % ('%s', '%s', cond), (self.start_date, self.end_date), as_dict=True) or [] - def get_salary_component_account(self, salary_component): account = frappe.db.get_value("Salary Component Account", {"parent": salary_component, "company": self.company}, "default_account") @@ -225,7 +212,6 @@ class PayrollEntry(Document): earnings = self.get_salary_component_total(component_type = "earnings") or {} deductions = self.get_salary_component_total(component_type = "deductions") or {} default_payroll_payable_account = self.get_default_payroll_payable_account() - loan_details = self.get_loan_details() jv_name = "" precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") @@ -262,29 +248,6 @@ class PayrollEntry(Document): "project": self.project }) - # Loan - for data in loan_details: - accounts.append({ - "account": data.loan_account, - "credit_in_account_currency": data.principal_amount, - "party_type": "Employee", - "party": data.employee - }) - - if data.interest_amount and not data.interest_income_account: - frappe.throw(_("Select interest income account in loan {0}").format(data.loan)) - - if data.interest_income_account and data.interest_amount: - accounts.append({ - "account": data.interest_income_account, - "credit_in_account_currency": data.interest_amount, - "cost_center": self.cost_center, - "project": self.project, - "party_type": "Employee", - "party": data.employee - }) - payable_amount -= flt(data.total_payment, precision) - # Payable amount accounts.append({ "account": default_payroll_payable_account, diff --git a/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py b/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py index 3cf13226db..35f5a57a1c 100644 --- a/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py @@ -6,20 +6,22 @@ import erpnext import frappe from dateutil.relativedelta import relativedelta from erpnext.accounts.utils import get_fiscal_year, getdate, nowdate +from frappe.utils import add_months from erpnext.hr.doctype.payroll_entry.payroll_entry import get_start_end_dates, get_end_date from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.salary_slip.test_salary_slip import get_salary_component_account, \ make_earning_salary_component, make_deduction_salary_component from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure -from erpnext.hr.doctype.loan.test_loan import create_loan +from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry +from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import make_accrual_interest_entry_for_term_loans class TestPayrollEntry(unittest.TestCase): def setUp(self): - for dt in ["Salary Slip", "Salary Component", "Salary Component Account", "Payroll Entry", "Loan"]: + for dt in ["Salary Slip", "Salary Component", "Salary Component Account", "Payroll Entry"]: frappe.db.sql("delete from `tab%s`" % dt) - make_earning_salary_component(setup=True) - make_deduction_salary_component(setup=True) + make_earning_salary_component(setup=True, company_list=["_Test Company"]) + make_deduction_salary_component(setup=True, company_list=["_Test Company"]) frappe.db.set_value("HR Settings", None, "email_salary_slip_to_employee", 0) @@ -49,8 +51,8 @@ class TestPayrollEntry(unittest.TestCase): def test_loan(self): branch = "Test Employee Branch" - applicant = make_employee("test_employee@loan.com") - company = erpnext.get_default_company() + applicant = make_employee("test_employee@loan.com", company="_Test Company") + company = "_Test Company" holiday_list = make_holiday("test holiday for loan") company_doc = frappe.get_doc('Company', company) @@ -70,16 +72,21 @@ class TestPayrollEntry(unittest.TestCase): employee_doc.holiday_list = holiday_list employee_doc.save() - loan = create_loan(applicant, - "Personal Loan", 280000, "Repay Over Number of Periods", 20) + salary_structure = "Test Salary Structure for Loan" + make_salary_structure(salary_structure, "Monthly", employee=employee_doc.name, company="_Test Company") + + loan = create_loan(applicant, "Car Loan", 280000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) loan.repay_from_salary = 1 loan.submit() - salary_structure = "Test Salary Structure for Loan" - make_salary_structure(salary_structure, "Monthly", employee_doc.name) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1)) + + make_accrual_interest_entry_for_term_loans(posting_date=nowdate()) + dates = get_start_end_dates('Monthly', nowdate()) - make_payroll_entry(start_date=dates.start_date, - end_date=dates.end_date, branch=branch) + make_payroll_entry(company="_Test Company", start_date=dates.start_date, + end_date=dates.end_date, branch=branch, cost_center="Main - _TC", payment_account="Cash - _TC") name = frappe.db.get_value('Salary Slip', {'posting_date': nowdate(), 'employee': applicant}, 'name') @@ -109,6 +116,13 @@ def make_payroll_entry(**args): payroll_entry.posting_date = nowdate() payroll_entry.payroll_frequency = "Monthly" payroll_entry.branch = args.branch or None + + if args.cost_center: + payroll_entry.cost_center = args.cost_center + + if args.payment_account: + payroll_entry.payment_account = args.payment_account + payroll_entry.save() payroll_entry.create_salary_slips() payroll_entry.submit_salary_slips() diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index ea531cbf9b..eee7974710 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -17,6 +17,7 @@ from erpnext.hr.doctype.additional_salary.additional_salary import get_additiona from erpnext.hr.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period from erpnext.hr.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount from erpnext.hr.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits +from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry class SalarySlip(TransactionBase): def __init__(self, *args, **kwargs): @@ -66,6 +67,7 @@ class SalarySlip(TransactionBase): self.set_status() self.update_status(self.name) self.update_salary_slip_in_additional_salary() + self.make_loan_repayment_entry() if (frappe.db.get_single_value("HR Settings", "email_salary_slip_to_employee")) and not frappe.flags.via_payroll_entry: self.email_salary_slip() @@ -73,6 +75,7 @@ class SalarySlip(TransactionBase): self.set_status() self.update_status() self.update_salary_slip_in_additional_salary() + self.cancel_loan_repayment_entry() def on_trash(self): from frappe.model.naming import revert_series_if_last @@ -754,28 +757,35 @@ class SalarySlip(TransactionBase): self.total_principal_amount = 0 for loan in self.get_loan_details(): - self.append('loans', { - 'loan': loan.name, - 'total_payment': loan.total_payment, - 'interest_amount': loan.interest_amount, - 'principal_amount': loan.principal_amount, - 'loan_account': loan.loan_account, - 'interest_income_account': loan.interest_income_account - }) - self.total_loan_repayment += loan.total_payment - self.total_interest_amount += loan.interest_amount - self.total_principal_amount += loan.principal_amount + amounts = calculate_amounts(loan.name, self.posting_date, "Regular Payment") + + total_payment = amounts['interest_amount'] + amounts['payable_principal_amount'] + + if total_payment: + self.append('loans', { + 'loan': loan.name, + 'total_payment': total_payment, + 'interest_amount': amounts['interest_amount'], + 'principal_amount': amounts['payable_principal_amount'], + 'loan_account': loan.loan_account, + 'interest_income_account': loan.interest_income_account + }) + + self.total_loan_repayment += total_payment + self.total_interest_amount += amounts['interest_amount'] + self.total_principal_amount += amounts['payable_principal_amount'] def get_loan_details(self): - return frappe.db.sql("""select rps.principal_amount, rps.interest_amount, l.name, - rps.total_payment, l.loan_account, l.interest_income_account - from - `tabRepayment Schedule` as rps, `tabLoan` as l - where - l.name = rps.parent and rps.payment_date between %s and %s and - l.repay_from_salary = 1 and l.docstatus = 1 and l.applicant = %s""", - (self.start_date, self.end_date, self.employee), as_dict=True) or [] + + return frappe.get_all("Loan", + fields=["name", "interest_income_account", "loan_account", "loan_type"], + filters = { + "applicant": self.employee, + "docstatus": 1, + "repay_from_salary": 1, + }) + def update_salary_slip_in_additional_salary(self): salary_slip = self.name if self.docstatus==1 else None @@ -784,6 +794,23 @@ class SalarySlip(TransactionBase): where employee=%s and payroll_date between %s and %s and docstatus=1 """, (salary_slip, self.employee, self.start_date, self.end_date)) + def make_loan_repayment_entry(self): + for loan in self.loans: + repayment_entry = create_repayment_entry(loan.loan, self.employee, + self.company, self.posting_date, loan.loan_type, "Regular Payment", loan.interest_amount, + loan.principal_amount, loan.total_payment) + + repayment_entry.save() + repayment_entry.submit() + + loan.loan_repayment_entry = repayment_entry.name + + def cancel_loan_repayment_entry(self): + for loan in self.loans: + if loan.loan_repayment_entry: + repayment_entry = frappe.get_doc("Loan Repayment", loan.loan_repayment_entry) + repayment_entry.cancel() + def email_salary_slip(self): receiver = frappe.db.get_value("Employee", self.employee, "prefered_email") hr_settings = frappe.get_single("HR Settings") diff --git a/erpnext/hr/doctype/salary_slip/test_salary_slip.py b/erpnext/hr/doctype/salary_slip/test_salary_slip.py index 16a75f473f..9acfd1f5c6 100644 --- a/erpnext/hr/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/test_salary_slip.py @@ -18,8 +18,8 @@ from erpnext.hr.doctype.employee_tax_exemption_declaration.test_employee_tax_exe class TestSalarySlip(unittest.TestCase): def setUp(self): - make_earning_salary_component(setup=True) - make_deduction_salary_component(setup=True) + make_earning_salary_component(setup=True, company_list=["_Test Company"]) + make_deduction_salary_component(setup=True, company_list=["_Test Company"]) for dt in ["Leave Application", "Leave Allocation", "Salary Slip"]: frappe.db.sql("delete from `tab%s`" % dt) @@ -50,7 +50,7 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.deductions[0].amount, 5000) self.assertEqual(ss.deductions[1].amount, 5000) self.assertEqual(ss.gross_pay, 78000) - self.assertEqual(ss.net_pay, 67418.0) + self.assertEqual(ss.net_pay, 68000.0) def test_salary_slip_with_holidays_excluded(self): no_of_days = self.get_no_of_days() @@ -70,7 +70,7 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.deductions[0].amount, 5000) self.assertEqual(ss.deductions[1].amount, 5000) self.assertEqual(ss.gross_pay, 78000) - self.assertEqual(ss.net_pay, 67418.0) + self.assertEqual(ss.net_pay, 68000.0) def test_payment_days(self): no_of_days = self.get_no_of_days() @@ -137,21 +137,41 @@ class TestSalarySlip(unittest.TestCase): make_employee("test_employee@salary.com") ss = make_employee_salary_slip("test_employee@salary.com", "Monthly") + ss.company = "_Test Company" + ss.save() ss.submit() email_queue = frappe.db.sql("""select name from `tabEmail Queue`""") self.assertTrue(email_queue) def test_loan_repayment_salary_slip(self): - from erpnext.hr.doctype.loan.test_loan import create_loan_type, create_loan - applicant = make_employee("test_employee@salary.com") - create_loan_type("Car Loan", 500000, 6.4) - loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20) + from erpnext.loan_management.doctype.loan.test_loan import create_loan_type, create_loan, make_loan_disbursement_entry, create_loan_accounts + from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import make_accrual_interest_entry_for_term_loans + + applicant = make_employee("test_loanemployee@salary.com", company="_Test Company") + + create_loan_accounts() + + create_loan_type("Car Loan", 500000, 8.4, + is_term_loan=1, + mode_of_payment='Cash', + payment_account='Payment Account - _TC', + loan_account='Loan Account - _TC', + interest_income_account='Interest Income Account - _TC', + penalty_income_account='Penalty Income Account - _TC') + + loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) loan.repay_from_salary = 1 loan.submit() - ss = make_employee_salary_slip("test_employee@salary.com", "Monthly") + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1)) + + make_accrual_interest_entry_for_term_loans(posting_date=nowdate()) + + ss = make_employee_salary_slip("test_loanemployee@salary.com", "Monthly") ss.submit() - self.assertEqual(ss.total_loan_repayment, 582) + + self.assertEqual(ss.total_loan_repayment, 592) self.assertEqual(ss.net_pay, (flt(ss.gross_pay) - (flt(ss.total_deduction) + flt(ss.total_loan_repayment)))) def test_payroll_frequency(self): @@ -321,7 +341,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): return salary_slip -def make_salary_component(salary_components, test_tax): +def make_salary_component(salary_components, test_tax, company_list=None): for salary_component in salary_components: if not frappe.db.exists('Salary Component', salary_component["salary_component"]): if test_tax: @@ -336,17 +356,22 @@ def make_salary_component(salary_components, test_tax): salary_component["doctype"] = "Salary Component" salary_component["salary_component_abbr"] = salary_component["abbr"] frappe.get_doc(salary_component).insert() - get_salary_component_account(salary_component["salary_component"]) + get_salary_component_account(salary_component["salary_component"], company_list) -def get_salary_component_account(sal_comp): +def get_salary_component_account(sal_comp, company_list=None): company = erpnext.get_default_company() + + if company_list and company not in company_list: + company_list.append(company) + sal_comp = frappe.get_doc("Salary Component", sal_comp) if not sal_comp.get("accounts"): - sal_comp.append("accounts", { - "company": company, - "default_account": create_account(company) - }) - sal_comp.save() + for d in company_list: + sal_comp.append("accounts", { + "company": d, + "default_account": create_account(d) + }) + sal_comp.save() def create_account(company): salary_account = frappe.db.get_value("Account", "Salary - " + frappe.get_cached_value('Company', company, 'abbr')) @@ -359,7 +384,7 @@ def create_account(company): }).insert() return salary_account -def make_earning_salary_component(setup=False, test_tax=False): +def make_earning_salary_component(setup=False, test_tax=False, company_list=None): data = [ { "salary_component": 'Basic Salary', @@ -415,7 +440,7 @@ def make_earning_salary_component(setup=False, test_tax=False): } ]) if setup or test_tax: - make_salary_component(data, test_tax) + make_salary_component(data, test_tax, company_list) data.append({ "salary_component": 'Basic Salary', "abbr":'BS', @@ -426,7 +451,7 @@ def make_earning_salary_component(setup=False, test_tax=False): }) return data -def make_deduction_salary_component(setup=False, test_tax=False): +def make_deduction_salary_component(setup=False, test_tax=False, company_list=None): data = [ { "salary_component": 'Professional Tax', @@ -458,7 +483,7 @@ def make_deduction_salary_component(setup=False, test_tax=False): "round_to_the_nearest_integer": 1 }) if setup or test_tax: - make_salary_component(data, test_tax) + make_salary_component(data, test_tax, company_list) return data diff --git a/erpnext/hr/doctype/salary_slip_loan/salary_slip_loan.json b/erpnext/hr/doctype/salary_slip_loan/salary_slip_loan.json deleted file mode 100644 index 5d1212b461..0000000000 --- a/erpnext/hr/doctype/salary_slip_loan/salary_slip_loan.json +++ /dev/null @@ -1,263 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-11-08 12:51:12.834479", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "loan", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Loan", - "length": 0, - "no_copy": 0, - "options": "Loan", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "loan_account", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Loan Account", - "length": 0, - "no_copy": 0, - "options": "Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "interest_income_account", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Interest Income Account", - "length": 0, - "no_copy": 0, - "options": "Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_4", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "principal_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Principal Amount", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "interest_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Interest Amount", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "total_payment", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Payment", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-02-26 05:24:31.369630", - "modified_by": "Administrator", - "module": "HR", - "name": "Salary Slip Loan", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/hr/doctype/salary_structure/test_salary_structure.py b/erpnext/hr/doctype/salary_structure/test_salary_structure.py index 848c3df57a..78150946c8 100644 --- a/erpnext/hr/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/hr/doctype/salary_structure/test_salary_structure.py @@ -86,16 +86,17 @@ class TestSalaryStructure(unittest.TestCase): self.assertEqual(salary_structure_assignment.base, 5000) self.assertEqual(salary_structure_assignment.variable, 200) -def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None, test_tax=False): +def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None, + test_tax=False, company=None): if test_tax: frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) if not frappe.db.exists('Salary Structure', salary_structure): details = { "doctype": "Salary Structure", "name": salary_structure, - "company": erpnext.get_default_company(), - "earnings": make_earning_salary_component(test_tax=test_tax), - "deductions": make_deduction_salary_component(test_tax=test_tax), + "company": company or erpnext.get_default_company(), + "earnings": make_earning_salary_component(test_tax=test_tax, company_list=["_Test Company"]), + "deductions": make_deduction_salary_component(test_tax=test_tax, company_list=["_Test Company"]), "payroll_frequency": payroll_frequency, "payment_account": get_random("Account") } @@ -109,11 +110,11 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do if employee and not frappe.db.get_value("Salary Structure Assignment", {'employee':employee, 'docstatus': 1}) and salary_structure_doc.docstatus==1: - create_salary_structure_assignment(employee, salary_structure) + create_salary_structure_assignment(employee, salary_structure, company=company) return salary_structure_doc -def create_salary_structure_assignment(employee, salary_structure, from_date=None): +def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None): if frappe.db.exists("Salary Structure Assignment", {"employee": employee}): frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee)) salary_structure_assignment = frappe.new_doc("Salary Structure Assignment") @@ -122,7 +123,7 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non salary_structure_assignment.variable = 5000 salary_structure_assignment.from_date = from_date or add_months(nowdate(), -1) salary_structure_assignment.salary_structure = salary_structure - salary_structure_assignment.company = erpnext.get_default_company() + salary_structure_assignment.company = company or erpnext.get_default_company() salary_structure_assignment.save(ignore_permissions=True) salary_structure_assignment.submit() return salary_structure_assignment diff --git a/erpnext/hr/report/loan_repayment/loan_repayment.js b/erpnext/hr/report/loan_repayment/loan_repayment.js deleted file mode 100644 index 21aa206160..0000000000 --- a/erpnext/hr/report/loan_repayment/loan_repayment.js +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt -/* eslint-disable */ - -frappe.query_reports["Loan Repayment"] = { - "filters": [ - - ] -} diff --git a/erpnext/hr/report/loan_repayment/loan_repayment.json b/erpnext/hr/report/loan_repayment/loan_repayment.json deleted file mode 100644 index b967dfdb38..0000000000 --- a/erpnext/hr/report/loan_repayment/loan_repayment.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "add_total_row": 0, - "creation": "2019-03-29 18:58:00.166032", - "disable_prepared_report": 0, - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "letter_head": "", - "modified": "2019-03-29 18:58:00.166032", - "modified_by": "Administrator", - "module": "HR", - "name": "Loan Repayment", - "owner": "Administrator", - "prepared_report": 0, - "ref_doctype": "Loan", - "report_name": "Loan Repayment", - "report_type": "Script Report", - "roles": [ - { - "role": "HR Manager" - }, - { - "role": "Employee" - } - ] -} diff --git a/erpnext/hr/report/loan_repayment/loan_repayment.py b/erpnext/hr/report/loan_repayment/loan_repayment.py deleted file mode 100644 index beca776964..0000000000 --- a/erpnext/hr/report/loan_repayment/loan_repayment.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe import _ - -def execute(filters=None): - - columns = create_columns() - data = get_record() - return columns, data - -def create_columns(): - return [ - { - "label": _("Employee"), - "fieldtype": "Data", - "fieldname": "employee", - "options": "Employee", - "width": 200 - }, - { - "label": _("Loan"), - "fieldtype": "Link", - "fieldname": "loan_name", - "options": "Loan", - "width": 200 - }, - { - "label": _("Loan Amount"), - "fieldtype": "Currency", - "fieldname": "loan_amount", - "options": "currency", - "width": 100 - }, - { - "label": _("Interest"), - "fieldtype": "Data", - "fieldname": "interest", - "width": 100 - }, - { - "label": _("Payable Amount"), - "fieldtype": "Currency", - "fieldname": "payable_amount", - "options": "currency", - "width": 100 - }, - { - "label": _("EMI"), - "fieldtype": "Currency", - "fieldname": "emi", - "options": "currency", - "width": 100 - }, - { - "label": _("Paid Amount"), - "fieldtype": "Currency", - "fieldname": "paid_amount", - "options": "currency", - "width": 100 - }, - { - "label": _("Outstanding Amount"), - "fieldtype": "Currency", - "fieldname": "out_amt", - "options": "currency", - "width": 100 - }, - ] - -def get_record(): - data = [] - loans = frappe.get_all("Loan", - filters=[("status", "=", "Disbursed")], - fields=["applicant", "applicant_name", "name", "loan_amount", "rate_of_interest", - "total_payment", "monthly_repayment_amount", "total_amount_paid"] - ) - - for loan in loans: - row = { - "employee": loan.applicant + ": " + loan.applicant_name, - "loan_name": loan.name, - "loan_amount": loan.loan_amount, - "interest": str(loan.rate_of_interest) + "%", - "payable_amount": loan.total_payment, - "emi": loan.monthly_repayment_amount, - "paid_amount": loan.total_amount_paid, - "out_amt": loan.total_payment - loan.total_amount_paid - } - - data.append(row) - - return data diff --git a/erpnext/hr/doctype/loan/__init__.py b/erpnext/loan_management/__init__.py similarity index 100% rename from erpnext/hr/doctype/loan/__init__.py rename to erpnext/loan_management/__init__.py diff --git a/erpnext/hr/doctype/loan_application/__init__.py b/erpnext/loan_management/doctype/__init__.py similarity index 100% rename from erpnext/hr/doctype/loan_application/__init__.py rename to erpnext/loan_management/doctype/__init__.py diff --git a/erpnext/hr/doctype/loan_type/__init__.py b/erpnext/loan_management/doctype/loan/__init__.py similarity index 100% rename from erpnext/hr/doctype/loan_type/__init__.py rename to erpnext/loan_management/doctype/loan/__init__.py diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js new file mode 100644 index 0000000000..8b220171e8 --- /dev/null +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -0,0 +1,190 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +{% include 'erpnext/loan_management/loan_common.js' %}; + +frappe.ui.form.on('Loan', { + setup: function(frm) { + frm.make_methods = { + 'Loan Disbursement': function() { frm.trigger('make_loan_disbursement') }, + 'Loan Security Unpledge': function() { frm.trigger('create_loan_security_unpledge') } + } + }, + onload: function (frm) { + frm.set_query("loan_application", function () { + return { + "filters": { + "applicant": frm.doc.applicant, + "docstatus": 1, + "status": "Approved" + } + }; + }); + + $.each(["penalty_income_account", "interest_income_account"], function(i, field) { + frm.set_query(field, function () { + return { + "filters": { + "company": frm.doc.company, + "root_type": "Income", + "is_group": 0 + } + }; + }); + }); + + $.each(["payment_account", "loan_account"], function (i, field) { + frm.set_query(field, function () { + return { + "filters": { + "company": frm.doc.company, + "root_type": "Asset", + "is_group": 0 + } + }; + }); + }) + + frm.set_query('loan_security_pledge', function(doc, cdt, cdn) { + return { + filters: { + applicant: frm.doc.applicant, + docstatus: 1, + loan_application: frm.doc.loan_application || '' + } + }; + }); + }, + + refresh: function (frm) { + if (frm.doc.docstatus == 1) { + if (frm.doc.status == "Sanctioned" || frm.doc.status == 'Partially Disbursed') { + frm.add_custom_button(__('Loan Disbursement'), function() { + frm.trigger("make_loan_disbursement"); + },__('Create')); + } + + if (["Disbursed", "Partially Disbursed"].includes(frm.doc.status) && (!frm.doc.repay_from_salary)) { + frm.add_custom_button(__('Loan Repayment'), function() { + frm.trigger("make_repayment_entry"); + },__('Create')); + + } + + if (frm.doc.status == "Loan Closure Requested") { + frm.add_custom_button(__('Loan Security Unpledge'), function() { + frm.trigger("create_loan_security_unpledge"); + },__('Create')); + } + } + frm.trigger("toggle_fields"); + }, + + loan_type: function(frm) { + frm.toggle_reqd("repayment_method", frm.doc.is_term_loan); + frm.toggle_display("repayment_method", 1 - frm.doc.is_term_loan); + frm.toggle_display("repayment_periods", s1 - frm.doc.is_term_loan); + }, + + is_secured_loan: function(frm) { + frm.toggle_reqd("loan_security_pledge", frm.doc.is_secured_loan); + }, + + make_loan_disbursement: function (frm) { + frappe.call({ + args: { + "loan": frm.doc.name, + "company": frm.doc.company, + "applicant_type": frm.doc.applicant_type, + "applicant": frm.doc.applicant, + "as_dict": 1 + }, + method: "erpnext.loan_management.doctype.loan.loan.make_loan_disbursement", + callback: function (r) { + if (r.message) + var doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + }) + }, + + make_repayment_entry: function(frm) { + frappe.call({ + args: { + "loan": frm.doc.name, + "applicant_type": frm.doc.applicant_type, + "applicant": frm.doc.applicant, + "loan_type": frm.doc.loan_type, + "company": frm.doc.company, + "as_dict": 1 + }, + method: "erpnext.loan_management.doctype.loan.loan.make_repayment_entry", + callback: function (r) { + if (r.message) + var doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + }) + }, + + create_loan_security_unpledge: function(frm) { + frappe.call({ + method: "erpnext.loan_management.doctype.loan.loan.create_loan_security_unpledge", + args : { + "loan": frm.doc.name, + "applicant_type": frm.doc.applicant_type, + "applicant": frm.doc.applicant, + "company": frm.doc.company + }, + callback: function(r) { + if (r.message) + var doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + }) + }, + + loan_application: function (frm) { + if(frm.doc.loan_application){ + return frappe.call({ + method: "erpnext.loan_management.doctype.loan.loan.get_loan_application", + args: { + "loan_application": frm.doc.loan_application + }, + callback: function (r) { + if (!r.exc && r.message) { + + let loan_fields = ["loan_type", "loan_amount", "repayment_method", + "monthly_repayment_amount", "repayment_periods", "rate_of_interest", "is_secured_loan"] + + loan_fields.forEach(field => { + frm.set_value(field, r.message[field]); + }); + + if (frm.doc.is_secured_loan) { + $.each(r.message.proposed_pledges, function(i, d) { + let row = frm.add_child("securities"); + row.loan_security = d.loan_security; + row.qty = d.qty; + row.loan_security_price = d.loan_security_price; + row.amount = d.amount; + row.haircut = d.haircut; + }); + + frm.refresh_fields("securities"); + } + } + } + }); + } + }, + + repayment_method: function (frm) { + frm.trigger("toggle_fields") + }, + + toggle_fields: function (frm) { + frm.toggle_enable("monthly_repayment_amount", frm.doc.repayment_method == "Repay Fixed Amount per Period") + frm.toggle_enable("repayment_periods", frm.doc.repayment_method == "Repay Over Number of Periods") + } +}); diff --git a/erpnext/hr/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json similarity index 69% rename from erpnext/hr/doctype/loan/loan.json rename to erpnext/loan_management/doctype/loan/loan.json index 2b2d481827..2834e5b655 100644 --- a/erpnext/hr/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -2,7 +2,7 @@ "actions": [], "allow_import": 1, "autoname": "ACC-LOAN-.YYYY.-.#####", - "creation": "2016-12-02 10:11:49.673604", + "creation": "2019-08-29 17:29:18.176786", "doctype": "DocType", "document_type": "Document", "editable_grid": 1, @@ -11,32 +11,41 @@ "applicant_type", "applicant", "applicant_name", - "loan_type", "loan_application", "column_break_3", - "posting_date", "company", + "posting_date", "status", "repay_from_salary", "section_break_8", + "loan_type", "loan_amount", + "is_secured_loan", "rate_of_interest", "disbursement_date", - "repayment_start_date", + "disbursed_amount", "column_break_11", + "is_term_loan", "repayment_method", "repayment_periods", "monthly_repayment_amount", + "repayment_start_date", + "loan_security_details_section", + "loan_security_pledge", + "column_break_25", + "maximum_loan_value", "account_info", "mode_of_payment", "payment_account", "column_break_9", "loan_account", "interest_income_account", + "penalty_income_account", "section_break_15", "repayment_schedule", "section_break_17", "total_payment", + "total_principal_paid", "column_break_19", "total_interest_payable", "total_amount_paid", @@ -47,7 +56,7 @@ "fieldname": "applicant_type", "fieldtype": "Select", "label": "Applicant Type", - "options": "Employee\nMember", + "options": "Employee\nMember\nCustomer", "reqd": 1 }, { @@ -75,6 +84,7 @@ "fieldname": "loan_type", "fieldtype": "Link", "in_list_view": 1, + "in_standard_filter": 1, "label": "Loan Type", "options": "Loan Type", "reqd": 1 @@ -95,6 +105,7 @@ { "fieldname": "company", "fieldtype": "Link", + "in_standard_filter": 1, "label": "Company", "options": "Company", "remember_last_selected_value": 1, @@ -104,9 +115,10 @@ "default": "Sanctioned", "fieldname": "status", "fieldtype": "Select", + "in_standard_filter": 1, "label": "Status", "no_copy": 1, - "options": "Sanctioned\nDisbursed\nRepaid/Closed", + "options": "Sanctioned\nPartially Disbursed\nDisbursed\nLoan Closure Requested\nClosed", "read_only": 1 }, { @@ -125,8 +137,7 @@ "fieldname": "loan_amount", "fieldtype": "Currency", "label": "Loan Amount", - "options": "Company:company:default_currency", - "reqd": 1 + "options": "Company:company:default_currency" }, { "fetch_from": "loan_type.rate_of_interest", @@ -143,29 +154,30 @@ "label": "Disbursement Date" }, { + "depends_on": "is_term_loan", "fieldname": "repayment_start_date", "fieldtype": "Date", - "label": "Repayment Start Date", - "reqd": 1 + "label": "Repayment Start Date" }, { "fieldname": "column_break_11", "fieldtype": "Column Break" }, { - "default": "Repay Over Number of Periods", + "depends_on": "is_term_loan", "fieldname": "repayment_method", "fieldtype": "Select", "label": "Repayment Method", - "options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods", - "reqd": 1 + "options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods" }, { + "depends_on": "is_term_loan", "fieldname": "repayment_periods", "fieldtype": "Int", "label": "Repayment Period in Months" }, { + "depends_on": "is_term_loan", "fieldname": "monthly_repayment_amount", "fieldtype": "Currency", "label": "Monthly Repayment Amount", @@ -178,17 +190,21 @@ "label": "Account Info" }, { + "fetch_from": "loan_type.mode_of_payment", "fieldname": "mode_of_payment", "fieldtype": "Link", "label": "Mode of Payment", "options": "Mode of Payment", + "read_only": 1, "reqd": 1 }, { + "fetch_from": "loan_type.payment_account", "fieldname": "payment_account", "fieldtype": "Link", "label": "Payment Account", "options": "Account", + "read_only": 1, "reqd": 1 }, { @@ -196,25 +212,31 @@ "fieldtype": "Column Break" }, { + "fetch_from": "loan_type.loan_account", "fieldname": "loan_account", "fieldtype": "Link", "label": "Loan Account", "options": "Account", + "read_only": 1, "reqd": 1 }, { + "fetch_from": "loan_type.interest_income_account", "fieldname": "interest_income_account", "fieldtype": "Link", "label": "Interest Income Account", "options": "Account", + "read_only": 1, "reqd": 1 }, { + "depends_on": "is_term_loan", "fieldname": "section_break_15", "fieldtype": "Section Break", "label": "Repayment Schedule" }, { + "depends_on": "eval:doc.is_term_loan == 1", "fieldname": "repayment_schedule", "fieldtype": "Table", "label": "Repayment Schedule", @@ -230,7 +252,7 @@ "default": "0", "fieldname": "total_payment", "fieldtype": "Currency", - "label": "Total Payment", + "label": "Total Payable Amount", "options": "Company:company:default_currency", "read_only": 1 }, @@ -240,6 +262,7 @@ }, { "default": "0", + "depends_on": "is_term_loan", "fieldname": "total_interest_payable", "fieldtype": "Currency", "label": "Total Interest Payable", @@ -262,13 +285,74 @@ "options": "Loan", "print_hide": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_secured_loan", + "fieldtype": "Check", + "label": "Is Secured Loan" + }, + { + "depends_on": "is_secured_loan", + "fieldname": "loan_security_details_section", + "fieldtype": "Section Break", + "label": "Loan Security Details" + }, + { + "default": "0", + "fetch_from": "loan_type.is_term_loan", + "fieldname": "is_term_loan", + "fieldtype": "Check", + "label": "Is Term Loan", + "read_only": 1 + }, + { + "fetch_from": "loan_type.penalty_income_account", + "fieldname": "penalty_income_account", + "fieldtype": "Link", + "label": "Penalty Income Account", + "options": "Account", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "total_principal_paid", + "fieldtype": "Currency", + "label": "Total Principal Paid", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "loan_security_pledge", + "fieldtype": "Link", + "label": "Loan Security Pledge", + "options": "Loan Security Pledge" + }, + { + "fieldname": "disbursed_amount", + "fieldtype": "Currency", + "label": "Disbursed Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fetch_from": "loan_security_pledge.maximum_loan_value", + "fieldname": "maximum_loan_value", + "fieldtype": "Currency", + "label": "Maximum Loan Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "column_break_25", + "fieldtype": "Column Break" } ], "is_submittable": 1, "links": [], - "modified": "2019-12-12 14:45:38.823072", + "modified": "2020-02-07 01:31:25.172173", "modified_by": "Administrator", - "module": "HR", + "module": "Loan Management", "name": "Loan", "owner": "Administrator", "permissions": [ @@ -281,7 +365,7 @@ "print": 1, "read": 1, "report": 1, - "role": "HR Manager", + "role": "Loan Manager", "share": 1, "submit": 1, "write": 1 diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py new file mode 100644 index 0000000000..696410b7bb --- /dev/null +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -0,0 +1,254 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, math, json +import erpnext +from frappe import _ +from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime + +from erpnext.controllers.accounts_controller import AccountsController + +class Loan(AccountsController): + def validate(self): + self.set_loan_amount() + + self.set_missing_fields() + self.validate_accounts() + self.validate_loan_security_pledge() + self.validate_loan_amount() + self.check_sanctioned_amount_limit() + + if self.is_term_loan: + validate_repayment_method(self.repayment_method, self.loan_amount, self.monthly_repayment_amount, + self.repayment_periods, self.is_term_loan) + self.make_repayment_schedule() + self.set_repayment_period() + + self.calculate_totals() + + def validate_accounts(self): + for fieldname in ['payment_account', 'loan_account', 'interest_income_account', 'penalty_income_account']: + company = frappe.get_value("Account", self.get(fieldname), 'company') + + if company != self.company: + frappe.throw(_("Account {0} does not belongs to company {1}").format(frappe.bold(self.get(fieldname)), + frappe.bold(self.company))) + + def on_submit(self): + self.link_loan_security_pledge() + + def on_cancel(self): + self.unlink_loan_security_pledge() + + def set_missing_fields(self): + if not self.company: + self.company = erpnext.get_default_company() + + if not self.posting_date: + self.posting_date = nowdate() + + if self.loan_type and not self.rate_of_interest: + self.rate_of_interest = frappe.db.get_value("Loan Type", self.loan_type, "rate_of_interest") + + if self.repayment_method == "Repay Over Number of Periods": + self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods) + + def validate_loan_security_pledge(self): + + if self.is_secured_loan and not self.loan_security_pledge: + frappe.throw(_("Loan Security Pledge is mandatory for secured loan")) + + if self.loan_security_pledge: + loan_security_details = frappe.db.get_value("Loan Security Pledge", self.loan_security_pledge, + ['loan', 'company'], as_dict=1) + + if loan_security_details.loan: + frappe.throw(_("Loan Security Pledge already pledged against loan {0}").format(loan_security_details.loan)) + + if loan_security_details.company != self.company: + frappe.throw(_("Loan Security Pledge Company and Loan Company must be same")) + + def check_sanctioned_amount_limit(self): + total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) + sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company) + + if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit): + frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant))) + + def make_repayment_schedule(self): + + if not self.repayment_start_date: + frappe.throw(_("Repayment Start Date is mandatory for term loans")) + + self.repayment_schedule = [] + payment_date = self.repayment_start_date + balance_amount = self.loan_amount + while(balance_amount > 0): + interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100)) + principal_amount = self.monthly_repayment_amount - interest_amount + balance_amount = rounded(balance_amount + interest_amount - self.monthly_repayment_amount) + + if balance_amount < 0: + principal_amount += balance_amount + balance_amount = 0.0 + + total_payment = principal_amount + interest_amount + self.append("repayment_schedule", { + "payment_date": payment_date, + "principal_amount": principal_amount, + "interest_amount": interest_amount, + "total_payment": total_payment, + "balance_loan_amount": balance_amount + }) + next_payment_date = add_months(payment_date, 1) + payment_date = next_payment_date + + def set_repayment_period(self): + if self.repayment_method == "Repay Fixed Amount per Period": + repayment_periods = len(self.repayment_schedule) + + self.repayment_periods = repayment_periods + + def calculate_totals(self): + self.total_payment = 0 + self.total_interest_payable = 0 + self.total_amount_paid = 0 + + if self.is_term_loan: + for data in self.repayment_schedule: + self.total_payment += data.total_payment + self.total_interest_payable +=data.interest_amount + else: + self.total_payment = self.loan_amount + + def set_loan_amount(self): + + if not self.loan_amount and self.is_secured_loan and self.loan_security_pledge: + self.loan_amount = self.maximum_loan_value + + def validate_loan_amount(self): + if self.is_secured_loan and self.loan_amount > self.maximum_loan_value: + msg = _("Loan amount cannot be greater than {0}").format(self.maximum_loan_value) + frappe.throw(msg) + + if not self.loan_amount: + frappe.throw(_("Loan amount is mandatory")) + + def link_loan_security_pledge(self): + frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET + loan = %s, status = 'Pledged', pledge_time = %s + where name = %s """, (self.name, now_datetime(), self.loan_security_pledge)) + + def unlink_loan_security_pledge(self): + frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET + loan = '', status = 'Unpledged' + where name = %s """, (self.loan_security_pledge)) + +def update_total_amount_paid(doc): + total_amount_paid = 0 + for data in doc.repayment_schedule: + if data.paid: + total_amount_paid += data.total_payment + frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid) + +def get_total_loan_amount(applicant_type, applicant, company): + return frappe.db.get_value('Loan', + {'applicant_type': applicant_type, 'company': company, 'applicant': applicant, 'docstatus': 1}, + 'sum(loan_amount)') + +def get_sanctioned_amount_limit(applicant_type, applicant, company): + return frappe.db.get_value('Sanctioned Loan Amount', + {'applicant_type': applicant_type, 'company': company, 'applicant': applicant}, + 'sanctioned_amount_limit') + +def validate_repayment_method(repayment_method, loan_amount, monthly_repayment_amount, repayment_periods, is_term_loan): + + if is_term_loan and not repayment_method: + frappe.throw(_("Repayment Method is mandatory for term loans")) + + if repayment_method == "Repay Over Number of Periods" and not repayment_periods: + frappe.throw(_("Please enter Repayment Periods")) + + if repayment_method == "Repay Fixed Amount per Period": + if not monthly_repayment_amount: + frappe.throw(_("Please enter repayment Amount")) + if monthly_repayment_amount > loan_amount: + frappe.throw(_("Monthly Repayment Amount cannot be greater than Loan Amount")) + +def get_monthly_repayment_amount(repayment_method, loan_amount, rate_of_interest, repayment_periods): + if rate_of_interest: + monthly_interest_rate = flt(rate_of_interest) / (12 *100) + monthly_repayment_amount = math.ceil((loan_amount * monthly_interest_rate * + (1 + monthly_interest_rate)**repayment_periods) \ + / ((1 + monthly_interest_rate)**repayment_periods - 1)) + else: + monthly_repayment_amount = math.ceil(flt(loan_amount) / repayment_periods) + return monthly_repayment_amount + +@frappe.whitelist() +def get_loan_application(loan_application): + loan = frappe.get_doc("Loan Application", loan_application) + if loan: + return loan.as_dict() + +def close_loan(loan, total_amount_paid): + frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid) + frappe.db.set_value("Loan", loan, "status", "Closed") + +@frappe.whitelist() +def make_loan_disbursement(loan, company, applicant_type, applicant, disbursed_amount=0, as_dict=0): + disbursement_entry = frappe.new_doc("Loan Disbursement") + disbursement_entry.against_loan = loan + disbursement_entry.applicant_type = applicant_type + disbursement_entry.applicant = applicant + disbursement_entry.company = company + disbursement_entry.disbursement_date = nowdate() + + if disbursed_amount: + disbursement_entry.disbursed_amount = disbursed_amount + if as_dict: + return disbursement_entry.as_dict() + else: + return disbursement_entry + +@frappe.whitelist() +def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as_dict=0): + repayment_entry = frappe.new_doc("Loan Repayment") + repayment_entry.against_loan = loan + repayment_entry.applicant_type = applicant_type + repayment_entry.applicant = applicant + repayment_entry.company = company + repayment_entry.loan_type = loan_type + repayment_entry.posting_date = nowdate() + + if as_dict: + return repayment_entry.as_dict() + else: + return repayment_entry + +@frappe.whitelist() +def create_loan_security_unpledge(loan, applicant_type, applicant, company): + loan_security_pledge_details = frappe.db.sql(""" + SELECT p.parent, p.loan_security, p.qty as qty FROM `tabLoan Security Pledge` lsp , `tabPledge` p + WHERE p.parent = lsp.name AND lsp.loan = %s AND lsp.docstatus = 1 + """,(loan), as_dict=1) + + unpledge_request = frappe.new_doc("Loan Security Unpledge") + unpledge_request.applicant_type = applicant_type + unpledge_request.applicant = applicant + unpledge_request.loan = loan + unpledge_request.company = company + + for loan_security in loan_security_pledge_details: + unpledge_request.append('securities', { + "loan_security": loan_security.loan_security, + "qty": loan_security.qty, + "against_pledge": loan_security.parent + }) + + return unpledge_request.as_dict() + + + diff --git a/erpnext/loan_management/doctype/loan/loan_dashboard.py b/erpnext/loan_management/doctype/loan/loan_dashboard.py new file mode 100644 index 0000000000..90d5ae2650 --- /dev/null +++ b/erpnext/loan_management/doctype/loan/loan_dashboard.py @@ -0,0 +1,19 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'loan', + 'non_standard_fieldnames': { + 'Loan Disbursement': 'against_loan', + 'Loan Repayment': 'against_loan', + }, + 'transactions': [ + { + 'items': ['Loan Security Pledge', 'Loan Security Shortfall', 'Loan Disbursement'] + }, + { + 'items': ['Loan Repayment', 'Loan Interest Accrual', 'Loan Security Unpledge'] + } + ] + } \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py new file mode 100644 index 0000000000..08c2f2267f --- /dev/null +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -0,0 +1,559 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import erpnext +import unittest +from frappe.utils import (nowdate, add_days, getdate, now_datetime, add_to_date, get_datetime, + add_months, get_first_day, get_last_day, flt, date_diff) +from erpnext.selling.doctype.customer.test_customer import get_customer_dict +from erpnext.hr.doctype.salary_structure.test_salary_structure import make_employee +from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual +from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import (make_accrual_interest_entry_for_term_loans, days_in_year) + +from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import check_for_ltv_shortfall + +class TestLoan(unittest.TestCase): + def setUp(self): + create_loan_accounts() + create_loan_type("Personal Loan", 500000, 8.4, + is_term_loan=1, + mode_of_payment='Cash', + payment_account='Payment Account - _TC', + loan_account='Loan Account - _TC', + interest_income_account='Interest Income Account - _TC', + penalty_income_account='Penalty Income Account - _TC') + + create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', + 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + + create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', + 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + + create_loan_security_type() + create_loan_security() + + create_loan_security_price("Test Security 1", 500, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) + create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) + + self.applicant1 = make_employee("robert_loan@loan.com") + if not frappe.db.exists("Customer", "_Test Loan Customer"): + frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) + + self.applicant2 = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name') + + create_loan(self.applicant1, "Personal Loan", 280000, "Repay Over Number of Periods", 20) + + def test_loan(self): + loan = frappe.get_doc("Loan", {"applicant":self.applicant1}) + self.assertEquals(loan.monthly_repayment_amount, 15052) + self.assertEquals(loan.total_interest_payable, 21034) + self.assertEquals(loan.total_payment, 301034) + + schedule = loan.repayment_schedule + + self.assertEqual(len(schedule), 20) + + for idx, principal_amount, interest_amount, balance_loan_amount in [[3, 13369, 1683, 227079], [19, 14941, 105, 0], [17, 14740, 312, 29785]]: + self.assertEqual(schedule[idx].principal_amount, principal_amount) + self.assertEqual(schedule[idx].interest_amount, interest_amount) + self.assertEqual(schedule[idx].balance_loan_amount, balance_loan_amount) + + loan.repayment_method = "Repay Fixed Amount per Period" + loan.monthly_repayment_amount = 14000 + loan.save() + + self.assertEquals(len(loan.repayment_schedule), 22) + self.assertEquals(loan.total_interest_payable, 22712) + self.assertEquals(loan.total_payment, 302712) + + def test_loan_with_security(self): + pledges = [] + pledges.append({ + "loan_security": "Test Security 1", + "qty": 4000.00, + "haircut": 50, + "loan_security_price": 500.00 + }) + + loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) + + loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_security_pledge.name) + + self.assertEquals(loan.loan_amount, 1000000) + + def test_loan_disbursement(self): + pledges = [] + pledges.append({ + "loan_security": "Test Security 1", + "qty": 4000.00, + "haircut": 50 + }) + + loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) + + loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_security_pledge.name) + self.assertEquals(loan.loan_amount, 1000000) + + loan.submit() + + loan_disbursement_entry1 = make_loan_disbursement_entry(loan.name, 500000) + loan_disbursement_entry2 = make_loan_disbursement_entry(loan.name, 500000) + + loan = frappe.get_doc("Loan", loan.name) + gl_entries1 = frappe.db.get_all("GL Entry", + fields=["name"], + filters = {'voucher_type': 'Loan Disbursement', 'voucher_no': loan_disbursement_entry1.name} + ) + + gl_entries2 = frappe.db.get_all("GL Entry", + fields=["name"], + filters = {'voucher_type': 'Loan Disbursement', 'voucher_no': loan_disbursement_entry2.name} + ) + + self.assertEquals(loan.status, "Disbursed") + self.assertEquals(loan.disbursed_amount, 1000000) + self.assertTrue(gl_entries1) + self.assertTrue(gl_entries2) + + def test_regular_loan_repayment(self): + pledges = [] + pledges.append({ + "loan_security": "Test Security 1", + "qty": 4000.00, + "haircut": 50 + }) + + loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_security_pledge.name, + posting_date=get_first_day(nowdate())) + + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime(first_date).year) * 100) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + + process_loan_interest_accrual(posting_date = last_date) + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), "Regular Payment", 111118.68) + repayment_entry.save() + + penalty_amount = (accrued_interest_amount * 5 * 25) / (100 * days_in_year(get_datetime(first_date).year)) + + self.assertEquals(flt(repayment_entry.interest_payable, 2), flt(accrued_interest_amount, 2)) + self.assertEquals(flt(repayment_entry.penalty_amount, 2), flt(penalty_amount, 2)) + + repayment_entry.submit() + + def test_loan_closure_repayment(self): + pledges = [] + pledges.append({ + "loan_security": "Test Security 1", + "qty": 4000.00, + "haircut": 50 + }) + + loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_security_pledge.name, + posting_date=get_first_day(nowdate())) + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + + # Adding 6 since repayment is made 5 days late after due date + # and since payment type is loan closure so interest should be considered for those + # 6 days as well though in grace period + no_of_days += 6 + + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime(first_date).year) * 100) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + process_loan_interest_accrual(posting_date = last_date) + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), + "Loan Closure", 13315.0681) + repayment_entry.save() + + repayment_entry.amount_paid = repayment_entry.payable_amount + + self.assertEquals(flt(repayment_entry.interest_payable, 3), flt(accrued_interest_amount, 3)) + self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) + + repayment_entry.submit() + loan.load_from_db() + self.assertEquals(loan.status, "Loan Closure Requested") + + def test_loan_repayment_for_term_loan(self): + pledges = [] + pledges.append({ + "loan_security": "Test Security 2", + "qty": 4000.00, + "haircut": 50 + }) + + pledges.append({ + "loan_security": "Test Security 1", + "qty": 2000.00, + "haircut": 50 + }) + + loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) + + loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, + loan_security_pledge.name, posting_date=add_months(nowdate(), -1)) + + loan.submit() + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1)) + + make_accrual_interest_entry_for_term_loans(posting_date=nowdate()) + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(get_last_day(nowdate()), 5), + "Regular Payment", 89768.7534247) + + repayment_entry.save() + repayment_entry.submit() + + repayment_entry.load_from_db() + + self.assertEquals(repayment_entry.interest_payable, 11250.00) + self.assertEquals(repayment_entry.payable_principal_amount, 78303.00) + + def test_partial_loan_repayment(self): + pledges = [] + pledges.append({ + "loan_security": "Test Security 1", + "qty": 4000.00, + "haircut": 50 + }) + + loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_security_pledge.name, + posting_date=get_first_day(nowdate())) + + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime().year) * 100) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + + process_loan_interest_accrual(posting_date = add_days(first_date, 15)) + process_loan_interest_accrual(posting_date = add_days(first_date, 30)) + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 1), "Regular Payment", 6500) + repayment_entry.save() + repayment_entry.submit() + + penalty_amount = (accrued_interest_amount * 4 * 25) / (100 * days_in_year(get_datetime(first_date).year)) + + lia = frappe.get_all("Loan Interest Accrual", fields=["is_paid"], + filters={"loan": loan.name}, order_by="posting_date") + + self.assertTrue(lia[0].get('is_paid')) + self.assertFalse(lia[1].get('is_paid')) + + def test_security_shortfall(self): + pledges = [] + pledges.append({ + "loan_security": "Test Security 2", + "qty": 8000.00, + "haircut": 50, + }) + + loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) + + loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_security_pledge.name) + loan.submit() + + make_loan_disbursement_entry(loan.name, loan.loan_amount) + + frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = %s + where loan_security=%s""", (100, 'Test Security 2')) + + check_for_ltv_shortfall() + loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name}) + + self.assertTrue(loan_security_shortfall) + + self.assertEquals(loan_security_shortfall.loan_amount, 1000000.00) + self.assertEquals(loan_security_shortfall.security_value, 400000.00) + self.assertEquals(loan_security_shortfall.shortfall_amount, 600000.00) + +def create_loan_accounts(): + if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): + frappe.get_doc({ + "doctype": "Account", + "account_name": "Loans and Advances (Assets)", + "company": "_Test Company", + "root_type": "Asset", + "report_type": "Balance Sheet", + "currency": "INR", + "parent_account": "Current Assets - _TC", + "account_type": "Bank", + "is_group": 1 + }).insert(ignore_permissions=True) + + if not frappe.db.exists("Account", "Loan Account - _TC"): + frappe.get_doc({ + "doctype": "Account", + "company": "_Test Company", + "account_name": "Loan Account", + "root_type": "Asset", + "report_type": "Balance Sheet", + "currency": "INR", + "parent_account": "Loans and Advances (Assets) - _TC", + "account_type": "Bank", + }).insert(ignore_permissions=True) + + if not frappe.db.exists("Account", "Payment Account - _TC"): + frappe.get_doc({ + "doctype": "Account", + "company": "_Test Company", + "account_name": "Payment Account", + "root_type": "Asset", + "report_type": "Balance Sheet", + "currency": "INR", + "parent_account": "Bank Accounts - _TC", + "account_type": "Bank", + }).insert(ignore_permissions=True) + + if not frappe.db.exists("Account", "Interest Income Account - _TC"): + frappe.get_doc({ + "doctype": "Account", + "company": "_Test Company", + "root_type": "Income", + "account_name": "Interest Income Account", + "report_type": "Profit and Loss", + "currency": "INR", + "parent_account": "Direct Income - _TC", + "account_type": "Income Account", + }).insert(ignore_permissions=True) + + if not frappe.db.exists("Account", "Penalty Income Account - _TC"): + frappe.get_doc({ + "doctype": "Account", + "company": "_Test Company", + "account_name": "Penalty Income Account", + "root_type": "Income", + "report_type": "Profit and Loss", + "currency": "INR", + "parent_account": "Direct Income - _TC", + "account_type": "Income Account", + }).insert(ignore_permissions=True) + +def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_interest_rate=None, is_term_loan=None, grace_period_in_days=None, + mode_of_payment=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None, + repayment_method=None, repayment_periods=None): + + if not frappe.db.exists("Loan Type", loan_name): + loan_type = frappe.get_doc({ + "doctype": "Loan Type", + "company": "_Test Company", + "loan_name": loan_name, + "is_term_loan": is_term_loan, + "maximum_loan_amount": maximum_loan_amount, + "rate_of_interest": rate_of_interest, + "penalty_interest_rate": penalty_interest_rate, + "grace_period_in_days": grace_period_in_days, + "mode_of_payment": mode_of_payment, + "payment_account": payment_account, + "loan_account": loan_account, + "interest_income_account": interest_income_account, + "penalty_income_account": penalty_income_account, + "repayment_method": repayment_method, + "repayment_periods": repayment_periods + }).insert() + + loan_type.submit() + +def create_loan_security_type(): + if not frappe.db.exists("Loan Security Type", "Stock"): + frappe.get_doc({ + "doctype": "Loan Security Type", + "loan_security_type": "Stock", + "unit_of_measure": "Nos", + "haircut": 50.00 + }).insert(ignore_permissions=True) + +def create_loan_security(): + if not frappe.db.exists("Loan Security", "Test Security 1"): + frappe.get_doc({ + "doctype": "Loan Security", + "loan_security_type": "Stock", + "loan_security_code": "532779", + "loan_security_name": "Test Security 1", + "unit_of_measure": "Nos", + "haircut": 50.00, + }).insert(ignore_permissions=True) + + if not frappe.db.exists("Loan Security", "Test Security 2"): + frappe.get_doc({ + "doctype": "Loan Security", + "loan_security_type": "Stock", + "loan_security_code": "531335", + "loan_security_name": "Test Security 2", + "unit_of_measure": "Nos", + "haircut": 50.00, + }).insert(ignore_permissions=True) + +def create_loan_security_pledge(applicant, pledges): + + lsp = frappe.new_doc("Loan Security Pledge") + lsp.applicant_type = 'Customer' + lsp.applicant = applicant + lsp.company = "_Test Company" + + for pledge in pledges: + lsp.append('securities', { + "loan_security": pledge['loan_security'], + "qty": pledge['qty'], + "haircut": pledge['haircut'] + }) + + lsp.save() + lsp.submit() + + return lsp + +def make_loan_disbursement_entry(loan, amount, disbursement_date=None): + + loan_disbursement_entry = frappe.get_doc({ + "doctype": "Loan Disbursement", + "against_loan": loan, + "disbursement_date": disbursement_date, + "company": "_Test Company", + "disbursed_amount": amount, + "cost_center": 'Main - _TC' + }).insert(ignore_permissions=True) + + loan_disbursement_entry.save() + loan_disbursement_entry.submit() + + return loan_disbursement_entry + +def create_loan_security_price(loan_security, loan_security_price, uom, from_date, to_date): + + if not frappe.db.get_value("Loan Security Price",{"loan_security": loan_security, + "valid_from": ("<=", from_date), "valid_upto": (">=", to_date)}, 'name'): + + lsp = frappe.get_doc({ + "doctype": "Loan Security Price", + "loan_security": loan_security, + "loan_security_price": loan_security_price, + "uom": uom, + "valid_from":from_date, + "valid_upto": to_date + }).insert(ignore_permissions=True) + +def create_repayment_entry(loan, applicant, posting_date, payment_type, paid_amount): + + lr = frappe.get_doc({ + "doctype": "Loan Repayment", + "against_loan": loan, + "payment_type": payment_type, + "company": "_Test Company", + "posting_date": posting_date or nowdate(), + "applicant": applicant, + "amount_paid": paid_amount, + "loan_type": "Stock Loan" + }).insert(ignore_permissions=True) + + return lr + + +def create_loan(applicant, loan_type, loan_amount, repayment_method, repayment_periods, + repayment_start_date=None, posting_date=None): + + loan = frappe.get_doc({ + "doctype": "Loan", + "applicant_type": "Employee", + "company": "_Test Company", + "applicant": applicant, + "loan_type": loan_type, + "loan_amount": loan_amount, + "repayment_method": repayment_method, + "repayment_periods": repayment_periods, + "repayment_start_date": nowdate(), + "is_term_loan": 1, + "posting_date": posting_date or nowdate() + }) + + loan.save() + return loan + +def create_loan_with_security(applicant, loan_type, repayment_method, repayment_periods, loan_security_pledge, + posting_date=None, repayment_start_date=None): + + loan = frappe.get_doc({ + "doctype": "Loan", + "company": "_Test Company", + "applicant_type": "Customer", + "posting_date": posting_date or nowdate(), + "applicant": applicant, + "loan_type": loan_type, + "is_term_loan": 1, + "is_secured_loan": 1, + "repayment_method": repayment_method, + "repayment_periods": repayment_periods, + "repayment_start_date": repayment_start_date or nowdate(), + "mode_of_payment": frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'), + "loan_security_pledge": loan_security_pledge, + "payment_account": 'Payment Account - _TC', + "loan_account": 'Loan Account - _TC', + "interest_income_account": 'Interest Income Account - _TC', + "penalty_income_account": 'Penalty Income Account - _TC', + }) + + loan.save() + + return loan + +def create_demand_loan(applicant, loan_type, loan_security_pledge, posting_date=None): + + loan = frappe.get_doc({ + "doctype": "Loan", + "company": "_Test Company", + "applicant_type": "Customer", + "posting_date": posting_date or nowdate(), + "applicant": applicant, + "loan_type": loan_type, + "is_term_loan": 0, + "is_secured_loan": 1, + "mode_of_payment": frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'), + "loan_security_pledge": loan_security_pledge, + "payment_account": 'Payment Account - _TC', + "loan_account": 'Loan Account - _TC', + "interest_income_account": 'Interest Income Account - _TC', + "penalty_income_account": 'Penalty Income Account - _TC', + }) + + loan.save() + + return loan \ No newline at end of file diff --git a/erpnext/hr/doctype/repayment_schedule/__init__.py b/erpnext/loan_management/doctype/loan_application/__init__.py similarity index 100% rename from erpnext/hr/doctype/repayment_schedule/__init__.py rename to erpnext/loan_management/doctype/loan_application/__init__.py diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.js b/erpnext/loan_management/doctype/loan_application/loan_application.js new file mode 100644 index 0000000000..57050d86c6 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_application/loan_application.js @@ -0,0 +1,127 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +{% include 'erpnext/loan_management/loan_common.js' %}; + +frappe.ui.form.on('Loan Application', { + + setup: function(frm) { + frm.make_methods = { + 'Loan': function() { frm.trigger('create_loan') }, + 'Loan Security Pledge': function() { frm.trigger('create_loan_security_pledge') }, + } + }, + refresh: function(frm) { + frm.trigger("toggle_fields"); + frm.trigger("add_toolbar_buttons"); + }, + repayment_method: function(frm) { + frm.doc.repayment_amount = frm.doc.repayment_periods = "" + frm.trigger("toggle_fields") + frm.trigger("toggle_required") + }, + toggle_fields: function(frm) { + frm.toggle_enable("repayment_amount", frm.doc.repayment_method=="Repay Fixed Amount per Period") + frm.toggle_enable("repayment_periods", frm.doc.repayment_method=="Repay Over Number of Periods") + }, + toggle_required: function(frm){ + frm.toggle_reqd("repayment_amount", cint(frm.doc.repayment_method=='Repay Fixed Amount per Period')) + frm.toggle_reqd("repayment_periods", cint(frm.doc.repayment_method=='Repay Over Number of Periods')) + }, + add_toolbar_buttons: function(frm) { + if (frm.doc.status == "Approved") { + + frappe.db.get_value("Loan Security Pledge", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { + if (!r) { + frm.add_custom_button(__('Loan Security Pledge'), function() { + frm.trigger('create_loan_security_pledge') + },__('Create')) + } + }); + + frappe.db.get_value("Loan", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { + if (!r) { + frm.add_custom_button(__('Loan'), function() { + frm.trigger('create_loan') + },__('Create')) + } else { + frm.set_df_property('status', 'read_only', 1); + } + }); + } + }, + create_loan: function(frm) { + if (frm.doc.status != "Approved") { + frappe.throw(__("Cannot create loan until application is approved")) + } + + frappe.model.open_mapped_doc({ + method: 'erpnext.loan_management.doctype.loan_application.loan_application.create_loan', + frm: frm + }); + }, + create_loan_security_pledge: function(frm) { + frappe.call({ + method: "erpnext.loan_management.doctype.loan_application.loan_application.create_pledge", + args: { + loan_application: frm.doc.name + }, + callback: function(r) { + frappe.set_route("Form", "Loan Security Pledge", r.message); + } + }) + }, + is_term_loan: function(frm) { + frm.set_df_property('repayment_method', 'hidden', 1 - frm.doc.is_term_loan); + frm.set_df_property('repayment_method', 'reqd', frm.doc.is_term_loan); + }, + is_secured_loan: function(frm) { + frm.set_df_property('proposed_pledges', 'reqd', frm.doc.is_secured_loan); + }, + + calculate_amounts: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.qty) { + frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.loan_security_price); + frappe.model.set_value(cdt, cdn, 'post_haircut_amount', cint(row.amount - (row.amount * row.haircut/100))); + } else if (row.amount) { + frappe.model.set_value(cdt, cdn, 'qty', cint(row.amount / row.loan_security_price)); + frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.loan_security_price); + frappe.model.set_value(cdt, cdn, 'post_haircut_amount', cint(row.amount - (row.amount * row.haircut/100))); + } + + let maximum_amount = 0; + + $.each(frm.doc.proposed_pledges || [], function(i, item){ + maximum_amount += item.post_haircut_amount; + }); + + if (flt(maximum_amount)) { + frm.set_value('maximum_loan_amount', flt(maximum_amount)); + } + } +}); + +frappe.ui.form.on("Proposed Pledge", { + loan_security: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + frappe.call({ + method: "erpnext.loan_management.doctype.loan_security_price.loan_security_price.get_loan_security_price", + args: { + loan_security: row.loan_security + }, + callback: function(r) { + frappe.model.set_value(cdt, cdn, 'loan_security_price', r.message); + frm.events.calculate_amounts(frm, cdt, cdn); + } + }) + }, + + amount: function(frm, cdt, cdn) { + frm.events.calculate_amounts(frm, cdt, cdn); + }, + + qty: function(frm, cdt, cdn) { + frm.events.calculate_amounts(frm, cdt, cdn); + }, +}) diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.json b/erpnext/loan_management/doctype/loan_application/loan_application.json new file mode 100644 index 0000000000..4c433029d7 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_application/loan_application.json @@ -0,0 +1,278 @@ +{ + "autoname": "ACC-LOAP-.YYYY.-.#####", + "creation": "2019-08-29 17:46:49.201740", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "applicant_type", + "applicant", + "applicant_name", + "column_break_2", + "company", + "posting_date", + "status", + "section_break_4", + "loan_type", + "is_term_loan", + "loan_amount", + "is_secured_loan", + "rate_of_interest", + "column_break_7", + "description", + "loan_security_details_section", + "proposed_pledges", + "maximum_loan_amount", + "repayment_info", + "repayment_method", + "total_payable_amount", + "column_break_11", + "repayment_periods", + "repayment_amount", + "total_payable_interest", + "amended_from" + ], + "fields": [ + { + "fieldname": "applicant_type", + "fieldtype": "Select", + "label": "Applicant Type", + "options": "Employee\nMember\nCustomer", + "reqd": 1 + }, + { + "fieldname": "applicant", + "fieldtype": "Dynamic Link", + "in_global_search": 1, + "in_standard_filter": 1, + "label": "Applicant", + "options": "applicant_type", + "reqd": 1 + }, + { + "depends_on": "applicant", + "fieldname": "applicant_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Applicant Name", + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date" + }, + { + "allow_on_submit": 1, + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "no_copy": 1, + "options": "Open\nApproved\nRejected", + "permlevel": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "label": "Loan Info" + }, + { + "fieldname": "loan_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Loan Type", + "options": "Loan Type", + "reqd": 1 + }, + { + "bold": 1, + "fieldname": "loan_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Loan Amount", + "options": "Company:company:default_currency" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Reason" + }, + { + "depends_on": "eval: doc.is_term_loan == 1", + "fieldname": "repayment_info", + "fieldtype": "Section Break", + "label": "Repayment Info" + }, + { + "depends_on": "eval: doc.is_term_loan == 1", + "fetch_from": "loan_type.repayment_method", + "fetch_if_empty": 1, + "fieldname": "repayment_method", + "fieldtype": "Select", + "label": "Repayment Method", + "options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods" + }, + { + "fetch_from": "loan_type.rate_of_interest", + "fieldname": "rate_of_interest", + "fieldtype": "Percent", + "label": "Rate of Interest", + "read_only": 1 + }, + { + "depends_on": "is_term_loan", + "fieldname": "total_payable_interest", + "fieldtype": "Currency", + "label": "Total Payable Interest", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "depends_on": "repayment_method", + "fieldname": "repayment_amount", + "fieldtype": "Currency", + "label": "Monthly Repayment Amount", + "options": "Company:company:default_currency" + }, + { + "depends_on": "repayment_method", + "fieldname": "repayment_periods", + "fieldtype": "Int", + "label": "Repayment Period in Months" + }, + { + "fieldname": "total_payable_amount", + "fieldtype": "Currency", + "label": "Total Payable Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Loan Application", + "print_hide": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_secured_loan", + "fieldtype": "Check", + "label": "Is Secured Loan" + }, + { + "depends_on": "eval:doc.is_secured_loan == 1", + "fieldname": "loan_security_details_section", + "fieldtype": "Section Break", + "label": "Loan Security Details" + }, + { + "depends_on": "eval:doc.is_secured_loan == 1", + "fieldname": "proposed_pledges", + "fieldtype": "Table", + "label": "Proposed Pledges", + "options": "Proposed Pledge" + }, + { + "fieldname": "maximum_loan_amount", + "fieldtype": "Currency", + "label": "Maximum Loan Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "loan_type.is_term_loan", + "fieldname": "is_term_loan", + "fieldtype": "Check", + "label": "Is Term Loan", + "read_only": 1 + } + ], + "is_submittable": 1, + "modified": "2019-10-24 10:32:03.740558", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Application", + "owner": "Administrator", + "permissions": [ + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "delete": 1, + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1 + } + ], + "search_fields": "applicant_type, applicant, loan_type, loan_amount", + "sort_field": "modified", + "sort_order": "DESC", + "timeline_field": "applicant", + "title_field": "applicant", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py new file mode 100644 index 0000000000..691962bf14 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_application/loan_application.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, math +from frappe import _ +from frappe.utils import flt, rounded, cint +from frappe.model.mapper import get_mapped_doc +from frappe.model.document import Document +from erpnext.loan_management.doctype.loan.loan import (get_monthly_repayment_amount, validate_repayment_method, + get_total_loan_amount, get_sanctioned_amount_limit) +from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price +import json +from six import string_types + +class LoanApplication(Document): + def validate(self): + + validate_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount, + self.repayment_periods, self.is_term_loan) + + self.validate_loan_type() + self.set_pledge_amount() + self.set_loan_amount() + self.validate_loan_amount() + self.get_repayment_details() + self.check_sanctioned_amount_limit() + + def validate_loan_type(self): + company = frappe.get_value("Loan Type", self.loan_type, "company") + if company != self.company: + frappe.throw(_("Please select Loan Type for company {0}").format(frappe.bold(self.company))) + + def validate_loan_amount(self): + if not self.loan_amount: + frappe.throw(_("Loan Amount is mandatory")) + + maximum_loan_limit = frappe.db.get_value('Loan Type', self.loan_type, 'maximum_loan_amount') + if maximum_loan_limit and self.loan_amount > maximum_loan_limit: + frappe.throw(_("Loan Amount cannot exceed Maximum Loan Amount of {0}").format(maximum_loan_limit)) + + if self.maximum_loan_amount and self.loan_amount > self.maximum_loan_amount: + frappe.throw(_("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format(self.maximum_loan_amount)) + + def check_sanctioned_amount_limit(self): + total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) + sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company) + + if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit): + frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant))) + + def set_pledge_amount(self): + for proposed_pledge in self.proposed_pledges: + + if not proposed_pledge.qty and not proposed_pledge.amount: + frappe.throw(_("Qty or Amount is mandatroy for loan security")) + + proposed_pledge.loan_security_price = get_loan_security_price(proposed_pledge.loan_security) + + if not proposed_pledge.qty: + proposed_pledge.qty = cint(proposed_pledge.amount/proposed_pledge.loan_security_price) + + proposed_pledge.amount = proposed_pledge.qty * proposed_pledge.loan_security_price + proposed_pledge.post_haircut_amount = cint(proposed_pledge.amount - (proposed_pledge.amount * proposed_pledge.haircut/100)) + + def get_repayment_details(self): + + if self.is_term_loan: + if self.repayment_method == "Repay Over Number of Periods": + self.repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods) + + if self.repayment_method == "Repay Fixed Amount per Period": + monthly_interest_rate = flt(self.rate_of_interest) / (12 *100) + if monthly_interest_rate: + min_repayment_amount = self.loan_amount*monthly_interest_rate + if self.repayment_amount - min_repayment_amount <= 0: + frappe.throw(_("Repayment Amount must be greater than " \ + + str(flt(min_repayment_amount, 2)))) + self.repayment_periods = math.ceil((math.log(self.repayment_amount) - + math.log(self.repayment_amount - min_repayment_amount)) /(math.log(1 + monthly_interest_rate))) + else: + self.repayment_periods = self.loan_amount / self.repayment_amount + + self.calculate_payable_amount() + else: + self.total_payable_amount = self.loan_amount + + def calculate_payable_amount(self): + balance_amount = self.loan_amount + self.total_payable_amount = 0 + self.total_payable_interest = 0 + + while(balance_amount > 0): + interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100)) + balance_amount = rounded(balance_amount + interest_amount - self.repayment_amount) + + self.total_payable_interest += interest_amount + + self.total_payable_amount = self.loan_amount + self.total_payable_interest + + def set_loan_amount(self): + if self.is_secured_loan and not self.proposed_pledges: + frappe.throw(_("Proposed Pledges are mandatory for secured Loans")) + + if not self.loan_amount and self.is_secured_loan and self.proposed_pledges: + self.loan_amount = 0 + for security in self.proposed_pledges: + self.loan_amount += security.post_haircut_amount + +@frappe.whitelist() +def create_loan(source_name, target_doc=None, submit=0): + def update_accounts(source_doc, target_doc, source_parent): + account_details = frappe.get_all("Loan Type", + fields=["mode_of_payment", "payment_account","loan_account", "interest_income_account", "penalty_income_account"], + filters = {'name': source_doc.loan_type} + )[0] + + loan_security_pledge = frappe.db.get_value("Loan Security Pledge", {"loan_application": source_name}, 'name') + + target_doc.mode_of_payment = account_details.mode_of_payment + target_doc.payment_account = account_details.payment_account + target_doc.loan_account = account_details.loan_account + target_doc.interest_income_account = account_details.interest_income_account + target_doc.penalty_income_account = account_details.penalty_income_account + + if loan_security_pledge: + target_doc.is_secured_loan = 1 + target_doc.loan_security_pledge = loan_security_pledge + + doclist = get_mapped_doc("Loan Application", source_name, { + "Loan Application": { + "doctype": "Loan", + "validation": { + "docstatus": ["=", 1] + }, + "postprocess": update_accounts, + "field_no_map": [ + "is_secured_loan" + ] + } + }, target_doc) + + if submit: + doclist.submit() + + return doclist + +@frappe.whitelist() +def create_pledge(loan_application): + loan_application_doc = frappe.get_doc("Loan Application", loan_application) + + lsp = frappe.new_doc("Loan Security Pledge") + lsp.applicant_type = loan_application_doc.applicant_type + lsp.applicant = loan_application_doc.applicant + lsp.loan_application = loan_application_doc.name + lsp.company = loan_application_doc.company + + for pledge in loan_application_doc.proposed_pledges: + + lsp.append('securities', { + "loan_security": pledge.loan_security, + "qty": pledge.qty, + "loan_security_price": pledge.loan_security_price, + "haircut": pledge.haircut + }) + + lsp.save() + lsp.submit() + + message = _("Loan Security Pledge Created : {0}").format(lsp.name) + frappe.msgprint(message) + + return lsp.name + +#This is a sandbox method to get the proposed pledges +@frappe.whitelist() +def get_proposed_pledge(securities): + if isinstance(securities, string_types): + securities = json.loads(securities) + + proposed_pledges = { + 'securities': [] + } + maximum_loan_amount = 0 + + for security in securities: + security = frappe._dict(security) + if not security.qty and not security.amount: + frappe.throw(_("Qty or Amount is mandatroy for loan security")) + + security.loan_security_price = get_loan_security_price(security.loan_security) + + if not security.qty: + security.qty = cint(security.amount/security.loan_security_price) + + security.amount = security.qty * security.loan_security_price + security.post_haircut_amount = security.amount - (security.amount * security.haircut/100) + + maximum_loan_amount += security.post_haircut_amount + + proposed_pledges['securities'].append(security) + + proposed_pledges['maximum_loan_amount'] = maximum_loan_amount + + return proposed_pledges diff --git a/erpnext/hr/doctype/loan_application/loan_application_dashboard.py b/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py similarity index 77% rename from erpnext/hr/doctype/loan_application/loan_application_dashboard.py rename to erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py index 232c6e3c5b..bf3f58b83e 100644 --- a/erpnext/hr/doctype/loan_application/loan_application_dashboard.py +++ b/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py @@ -6,7 +6,7 @@ def get_data(): 'fieldname': 'loan_application', 'transactions': [ { - 'items': ['Loan'] + 'items': ['Loan', 'Loan Security Pledge'] }, ], } \ No newline at end of file diff --git a/erpnext/hr/doctype/loan_application/test_loan_application.py b/erpnext/loan_management/doctype/loan_application/test_loan_application.py similarity index 53% rename from erpnext/hr/doctype/loan_application/test_loan_application.py rename to erpnext/loan_management/doctype/loan_application/test_loan_application.py index b08b522503..99c807b2cd 100644 --- a/erpnext/hr/doctype/loan_application/test_loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/test_loan_application.py @@ -1,39 +1,33 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals import frappe import unittest from erpnext.hr.doctype.salary_structure.test_salary_structure import make_employee +from erpnext.loan_management.doctype.loan.test_loan import create_loan_type, create_loan_accounts class TestLoanApplication(unittest.TestCase): def setUp(self): - self.create_loan_type() - self.applicant = make_employee("kate_loan@loan.com") + create_loan_accounts() + create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', + 'Interest Income Account - _TC', 'Penalty Income Account - _TC', 'Repay Over Number of Periods', 18) + self.applicant = make_employee("kate_loan@loan.com", "_Test Company") self.create_loan_application() - def create_loan_type(self): - if not frappe.db.get_value("Loan Type", "Home Loan"): - frappe.get_doc({ - "doctype": "Loan Type", - "loan_name": "Home Loan", - "maximum_loan_amount": 500000, - "rate_of_interest": 9.2 - }).insert() - def create_loan_application(self): - if not frappe.db.get_value("Loan Application", {"applicant":self.applicant}, "name"): - loan_application = frappe.new_doc("Loan Application") - loan_application.update({ - "applicant": self.applicant, - "loan_type": "Home Loan", - "rate_of_interest": 9.2, - "loan_amount": 250000, - "repayment_method": "Repay Over Number of Periods", - "repayment_periods": 18 - }) - loan_application.insert() + loan_application = frappe.new_doc("Loan Application") + loan_application.update({ + "applicant": self.applicant, + "loan_type": "Home Loan", + "rate_of_interest": 9.2, + "loan_amount": 250000, + "repayment_method": "Repay Over Number of Periods", + "repayment_periods": 18, + "company": "_Test Company" + }) + loan_application.insert() def test_loan_totals(self): diff --git a/erpnext/hr/doctype/salary_slip_loan/__init__.py b/erpnext/loan_management/doctype/loan_disbursement/__init__.py similarity index 100% rename from erpnext/hr/doctype/salary_slip_loan/__init__.py rename to erpnext/loan_management/doctype/loan_disbursement/__init__.py diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.js b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.js new file mode 100644 index 0000000000..487ef23102 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.js @@ -0,0 +1,17 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +{% include 'erpnext/loan_management/loan_common.js' %}; + +frappe.ui.form.on('Loan Disbursement', { + refresh: function(frm) { + frm.set_query('against_loan', function() { + return { + 'filters': { + 'docstatus': 1, + 'status': 'Sanctioned' + } + } + }) + } +}); diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json new file mode 100644 index 0000000000..72a4ddcc8f --- /dev/null +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json @@ -0,0 +1,165 @@ +{ + "autoname": "LM-DIS-.#####", + "creation": "2019-09-07 12:44:49.125452", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "against_loan", + "disbursement_date", + "posting_date", + "column_break_4", + "company", + "applicant_type", + "applicant", + "section_break_7", + "pending_amount_for_disbursal", + "disbursed_amount", + "accounting_dimensions_section", + "cost_center", + "section_break_13", + "customer_details_section", + "bank_account", + "amended_from" + ], + "fields": [ + { + "fieldname": "against_loan", + "fieldtype": "Link", + "label": "Against Loan ", + "options": "Loan" + }, + { + "fieldname": "disbursement_date", + "fieldtype": "Date", + "label": "Disbursement Date" + }, + { + "fieldname": "disbursed_amount", + "fieldtype": "Currency", + "label": "Disbursed Amount", + "options": "Company:company:default_currency" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Loan Disbursement", + "print_hide": 1, + "read_only": 1 + }, + { + "fetch_from": "against_loan.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "read_only": 1 + }, + { + "fetch_from": "against_loan.applicant", + "fieldname": "applicant", + "fieldtype": "Dynamic Link", + "label": "Applicant", + "options": "applicant_type", + "read_only": 1 + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "posting_date", + "fieldtype": "Date", + "hidden": 1, + "label": "Posting Date", + "read_only": 1 + }, + { + "fieldname": "pending_amount_for_disbursal", + "fieldtype": "Currency", + "label": "Pending Amount For Disbursal", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_7", + "fieldtype": "Section Break" + }, + { + "fieldname": "section_break_13", + "fieldtype": "Section Break" + }, + { + "fieldname": "customer_details_section", + "fieldtype": "Section Break", + "label": "Customer Details" + }, + { + "fetch_from": "against_loan.applicant_type", + "fieldname": "applicant_type", + "fieldtype": "Select", + "label": "Applicant Type", + "options": "Employee\nMember\nCustomer", + "read_only": 1 + }, + { + "fieldname": "bank_account", + "fieldtype": "Link", + "label": "Bank Account", + "options": "Bank Account" + } + ], + "is_submittable": 1, + "modified": "2019-10-24 12:32:32.230881", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Disbursement", + "owner": "Administrator", + "permissions": [ + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py new file mode 100644 index 0000000000..fa7db2d565 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, erpnext +from frappe.model.document import Document +from frappe.utils import nowdate, getdate, add_days, flt +from erpnext.controllers.accounts_controller import AccountsController +from erpnext.accounts.general_ledger import make_gl_entries +from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual + +class LoanDisbursement(AccountsController): + + def validate(self): + self.set_missing_values() + self.set_pending_amount_for_disbursal() + + def before_submit(self): + self.set_status_and_amounts() + + def on_submit(self): + self.make_gl_entries() + + def on_cancel(self): + self.make_gl_entries(cancel=1) + + def set_missing_values(self): + if not self.disbursement_date: + self.disbursement_date = nowdate() + + if not self.cost_center: + self.cost_center = erpnext.get_default_cost_center(self.company) + + if not self.posting_date: + self.posting_date = self.disbursement_date or nowdate() + + if not self.bank_account and self.applicant_type == "Customer": + self.bank_account = frappe.db.get_value("Customer", self.applicant, "default_bank_account") + + def set_pending_amount_for_disbursal(self): + loan_amount, disbursed_amount = frappe.db.get_value('Loan', + {'name': self.against_loan}, ['loan_amount', 'disbursed_amount']) + + self.pending_amount_for_disbursal = loan_amount - disbursed_amount + + def set_status_and_amounts(self): + + loan_details = frappe.get_all("Loan", + fields = ["loan_amount", "disbursed_amount", "total_principal_paid", "status", "is_term_loan"], + filters= { "name": self.against_loan } + )[0] + + if loan_details.status == "Disbursed" and not loan_details.is_term_loan: + process_loan_interest_accrual(posting_date=add_days(self.disbursement_date, -1), + loan=self.against_loan) + + disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount + + if flt(disbursed_amount) - flt(loan_details.total_principal_paid) > flt(loan_details.loan_amount): + frappe.throw(_("Disbursed Amount cannot be greater than loan amount")) + + if flt(disbursed_amount) > flt(loan_details.loan_amount): + total_principal_paid = loan_details.total_principal_paid - (disbursed_amount - loan_details.loan_amount) + frappe.db.set_value("Loan", self.against_loan, "total_principal_paid", total_principal_paid) + + if flt(loan_details.loan_amount) == flt(disbursed_amount): + frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed") + else: + frappe.db.set_value("Loan", self.against_loan, "status", "Partially Disbursed") + + frappe.db.set_value("Loan", self.against_loan, { + "disbursement_date": self.disbursement_date, + "disbursed_amount": disbursed_amount + }) + + def make_gl_entries(self, cancel=0, adv_adj=0): + gle_map = [] + loan_details = frappe.get_doc("Loan", self.against_loan) + + gle_map.append( + self.get_gl_dict({ + "account": loan_details.loan_account, + "against": loan_details.applicant, + "debit": self.disbursed_amount, + "debit_in_account_currency": self.disbursed_amount, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": "Against Loan:" + self.against_loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "posting_date": self.disbursement_date + }) + ) + + gle_map.append( + self.get_gl_dict({ + "account": loan_details.payment_account, + "against": loan_details.applicant, + "credit": self.disbursed_amount, + "credit_in_account_currency": self.disbursed_amount, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": "Against Loan:" + self.against_loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "posting_date": self.disbursement_date + }) + ) + + if gle_map: + make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj) diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py new file mode 100644 index 0000000000..968e377fcc --- /dev/null +++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals +import frappe +import unittest +from frappe.utils import (nowdate, add_days, get_datetime, get_first_day, get_last_day, date_diff, flt, add_to_date) +from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_pledge, create_repayment_entry, + make_loan_disbursement_entry, create_loan_accounts, create_loan_security_type, create_loan_security, create_demand_loan, create_loan_security_price) +from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual +from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import (make_accrual_interest_entry_for_term_loans, days_in_year) +from erpnext.selling.doctype.customer.test_customer import get_customer_dict + +class TestLoanDisbursement(unittest.TestCase): + + def setUp(self): + create_loan_accounts() + + create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', + 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + + create_loan_security_type() + create_loan_security() + + create_loan_security_price("Test Security 1", 500, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) + create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) + + if not frappe.db.exists("Customer", "_Test Loan Customer"): + frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) + + self.applicant = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name') + + def test_loan_topup(self): + pledges = [] + pledges.append({ + "loan_security": "Test Security 1", + "qty": 4000.00, + "haircut": 50, + "loan_security_price": 500.00 + }) + + loan_security_pledge = create_loan_security_pledge(self.applicant, pledges) + + loan = create_demand_loan(self.applicant, "Demand Loan", loan_security_pledge.name, + posting_date=get_first_day(nowdate())) + + loan.submit() + + first_date = get_first_day(nowdate()) + last_date = get_last_day(nowdate()) + + no_of_days = date_diff(last_date, first_date) + 1 + + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime().year) * 100) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + + process_loan_interest_accrual(posting_date=add_days(last_date, 1)) + + # Paid 511095.89 amount includes 5,00,000 principal amount and 11095.89 interest amount + repayment_entry = create_repayment_entry(loan.name, self.applicant, add_days(get_last_day(nowdate()), 5), + "Regular Payment", 611095.89) + repayment_entry.submit() + + loan.reload() + + make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 16)) + + total_principal_paid = loan.total_principal_paid + + loan.reload() + + # Loan Topup will result in decreasing the Total Principal Paid + self.assertEqual(flt(loan.total_principal_paid, 2), flt(total_principal_paid - 500000, 2)) diff --git a/erpnext/hr/report/loan_repayment/__init__.py b/erpnext/loan_management/doctype/loan_interest_accrual/__init__.py similarity index 100% rename from erpnext/hr/report/loan_repayment/__init__.py rename to erpnext/loan_management/doctype/loan_interest_accrual/__init__.py diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.js b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.js new file mode 100644 index 0000000000..177b23593c --- /dev/null +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.js @@ -0,0 +1,10 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +{% include 'erpnext/loan_management/loan_common.js' %}; + +frappe.ui.form.on('Loan Interest Accrual', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json new file mode 100644 index 0000000000..33f496fc3e --- /dev/null +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json @@ -0,0 +1,182 @@ +{ + "actions": [], + "autoname": "LM-LIA-.#####", + "creation": "2019-09-09 22:34:36.346812", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan", + "applicant_type", + "applicant", + "interest_income_account", + "loan_account", + "column_break_4", + "company", + "posting_date", + "is_term_loan", + "is_paid", + "section_break_7", + "pending_principal_amount", + "payable_principal_amount", + "column_break_14", + "interest_amount", + "section_break_15", + "process_loan_interest_accrual", + "amended_from" + ], + "fields": [ + { + "fieldname": "loan", + "fieldtype": "Link", + "label": "Loan", + "options": "Loan" + }, + { + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date" + }, + { + "fieldname": "pending_principal_amount", + "fieldtype": "Currency", + "label": "Pending Principal Amount", + "options": "Company:company:default_currency" + }, + { + "fieldname": "interest_amount", + "fieldtype": "Currency", + "label": "Interest Amount", + "options": "Company:company:default_currency" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Loan Interest Accrual", + "print_hide": 1, + "read_only": 1 + }, + { + "fetch_from": "loan.applicant_type", + "fieldname": "applicant_type", + "fieldtype": "Select", + "label": "Applicant Type", + "options": "Employee\nMember\nCustomer" + }, + { + "fetch_from": "loan.applicant", + "fieldname": "applicant", + "fieldtype": "Dynamic Link", + "label": "Applicant", + "options": "applicant_type" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fetch_from": "loan.interest_income_account", + "fieldname": "interest_income_account", + "fieldtype": "Data", + "label": "Interest Income Account" + }, + { + "fetch_from": "loan.loan_account", + "fieldname": "loan_account", + "fieldtype": "Data", + "label": "Loan Account" + }, + { + "fieldname": "section_break_7", + "fieldtype": "Section Break", + "label": "Amounts" + }, + { + "fetch_from": "loan.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "default": "0", + "fieldname": "is_paid", + "fieldtype": "Check", + "label": "Is Paid", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "loan.is_term_loan", + "fieldname": "is_term_loan", + "fieldtype": "Check", + "label": "Is Term Loan", + "read_only": 1 + }, + { + "depends_on": "is_term_loan", + "fieldname": "payable_principal_amount", + "fieldtype": "Currency", + "label": "Payable Principal Amount", + "options": "Company:company:default_currency" + }, + { + "fieldname": "section_break_15", + "fieldtype": "Section Break" + }, + { + "fieldname": "process_loan_interest_accrual", + "fieldtype": "Link", + "label": "Process Loan Interest Accrual", + "options": "Process Loan Interest Accrual" + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + } + ], + "in_create": 1, + "is_submittable": 1, + "links": [], + "modified": "2020-02-07 01:22:06.924125", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Interest Accrual", + "owner": "Administrator", + "permissions": [ + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py new file mode 100644 index 0000000000..a3442e4439 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, erpnext +from frappe import _ +from frappe.model.document import Document +from frappe.utils import (nowdate, getdate, now_datetime, get_datetime, flt, date_diff, get_last_day, cint, + get_first_day, get_datetime, add_days) +from erpnext.controllers.accounts_controller import AccountsController +from erpnext.accounts.general_ledger import make_gl_entries + +class LoanInterestAccrual(AccountsController): + def validate(self): + if not self.loan: + frappe.throw(_("Loan is mandatory")) + + if not self.posting_date: + self.posting_date = nowdate() + + if not self.interest_amount: + frappe.throw(_("Interest Amount is mandatory")) + + + def on_submit(self): + self.make_gl_entries() + + def on_cancel(self): + self.make_gl_entries(cancel=1) + + def make_gl_entries(self, cancel=0, adv_adj=0): + gle_map = [] + + gle_map.append( + self.get_gl_dict({ + "account": self.loan_account, + "party_type": self.applicant_type, + "party": self.applicant, + "against": self.interest_income_account, + "debit": self.interest_amount, + "debit_in_account_currency": self.interest_amount, + "against_voucher_type": "Loan", + "against_voucher": self.loan, + "remarks": _("Against Loan:") + self.loan, + "cost_center": erpnext.get_default_cost_center(self.company), + "posting_date": self.posting_date + }) + ) + + gle_map.append( + self.get_gl_dict({ + "account": self.interest_income_account, + "party_type": self.applicant_type, + "party": self.applicant, + "against": self.loan_account, + "credit": self.interest_amount, + "credit_in_account_currency": self.interest_amount, + "against_voucher_type": "Loan", + "against_voucher": self.loan, + "remarks": _("Against Loan:") + self.loan, + "cost_center": erpnext.get_default_cost_center(self.company), + "posting_date": self.posting_date + }) + ) + + if gle_map: + make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj) + + +# For Eg: If Loan disbursement date is '01-09-2019' and disbursed amount is 1000000 and +# rate of interest is 13.5 then first loan interest accural will be on '01-10-2019' +# which means interest will be accrued for 30 days which should be equal to 11095.89 +def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest): + no_of_days = get_no_of_days_for_interest_accural(loan, posting_date) + + if no_of_days <= 0: + return + + pending_principal_amount = loan.total_payment - loan.total_interest_payable \ + - loan.total_amount_paid + + interest_per_day = (pending_principal_amount * loan.rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100) + payable_interest = interest_per_day * no_of_days + + make_loan_interest_accrual_entry(loan.name, loan.applicant_type, loan.applicant,loan.interest_income_account, + loan.loan_account, pending_principal_amount, payable_interest, process_loan_interest = process_loan_interest, + posting_date=posting_date) + +def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None): + query_filters = { + "status": "Disbursed", + "docstatus": 1 + } + + if loan_type: + query_filters.update({ + "loan_type": loan_type + }) + + if not open_loans: + open_loans = frappe.get_all("Loan", + fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", "is_term_loan", + "disbursement_date", "applicant_type", "applicant", "rate_of_interest", "total_interest_payable", "repayment_start_date"], + filters=query_filters) + + for loan in open_loans: + calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest) + +def make_accrual_interest_entry_for_term_loans(posting_date=None): + curr_date = posting_date or add_days(nowdate(), 1) + + term_loans = frappe.db.sql("""SELECT l.name, l.total_payment, l.total_amount_paid, l.loan_account, + l.interest_income_account, l.is_term_loan, l.disbursement_date, l.applicant_type, l.applicant, + l.rate_of_interest, l.total_interest_payable, l.repayment_start_date, rs.name as payment_entry, + rs.payment_date, rs.principal_amount, rs.interest_amount, rs.is_accrued , rs.balance_loan_amount + FROM `tabLoan` l, `tabRepayment Schedule` rs + WHERE rs.parent = l.name + AND l.docstatus=1 + AND l.is_term_loan =1 + AND rs.payment_date <= %s + AND rs.is_accrued=0 + AND l.status = 'Disbursed'""", (curr_date), as_dict=1) + + accrued_entries = [] + + for loan in term_loans: + accrued_entries.append(loan.payment_entry) + make_loan_interest_accrual_entry(loan.name, loan.applicant_type, loan.applicant,loan.interest_income_account, + loan.loan_account, loan.principal_amount + loan.balance_loan_amount, loan.interest_amount, + payable_principal = loan.principal_amount , posting_date=posting_date) + + frappe.db.sql("""UPDATE `tabRepayment Schedule` + SET is_accrued = 1 where name in (%s)""" #nosec + % ", ".join(['%s']*len(accrued_entries)), tuple(accrued_entries)) + +def make_loan_interest_accrual_entry(loan, applicant_type, applicant, interest_income_account, loan_account, + pending_principal_amount, interest_amount, payable_principal=None, process_loan_interest=None, posting_date=None): + loan_interest_accrual = frappe.new_doc("Loan Interest Accrual") + loan_interest_accrual.loan = loan + loan_interest_accrual.applicant_type = applicant_type + loan_interest_accrual.applicant = applicant + loan_interest_accrual.interest_income_account = interest_income_account + loan_interest_accrual.loan_account = loan_account + loan_interest_accrual.pending_principal_amount = flt(pending_principal_amount, 2) + loan_interest_accrual.interest_amount = flt(interest_amount, 2) + loan_interest_accrual.posting_date = posting_date or nowdate() + loan_interest_accrual.process_loan_interest_accrual = process_loan_interest + + if payable_principal: + loan_interest_accrual.payable_principal_amount = payable_principal + + loan_interest_accrual.save() + loan_interest_accrual.submit() + + +def get_no_of_days_for_interest_accural(loan, posting_date): + last_interest_accrual_date = get_last_accural_date_in_current_month(loan) + + no_of_days = date_diff(posting_date or nowdate(), last_interest_accrual_date) + 1 + + return no_of_days + +def get_last_accural_date_in_current_month(loan): + last_posting_date = frappe.db.sql(""" SELECT MAX(posting_date) from `tabLoan Interest Accrual` + WHERE loan = %s""", (loan.name)) + + if last_posting_date[0][0]: + return last_posting_date[0][0] + else: + return loan.disbursement_date + +def days_in_year(year): + days = 365 + + if (year % 4 == 0) and (year % 100 != 0) or (year % 400 == 0): + days = 366 + + return days + diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py new file mode 100644 index 0000000000..e681ae42c3 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals +import frappe +import unittest +from frappe.utils import (nowdate, add_days, get_datetime, get_first_day, get_last_day, date_diff, flt, add_to_date) +from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_pledge, create_loan_security_price, + make_loan_disbursement_entry, create_loan_accounts, create_loan_security_type, create_loan_security, create_demand_loan) +from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual +from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import (make_accrual_interest_entry_for_term_loans, days_in_year) +from erpnext.selling.doctype.customer.test_customer import get_customer_dict + +class TestLoanInterestAccrual(unittest.TestCase): + def setUp(self): + create_loan_accounts() + + create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', + 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + + create_loan_security_type() + create_loan_security() + + create_loan_security_price("Test Security 1", 500, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) + + if not frappe.db.exists("Customer", "_Test Loan Customer"): + frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) + + self.applicant = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name') + + def test_loan_interest_accural(self): + pledges = [] + pledges.append({ + "loan_security": "Test Security 1", + "qty": 4000.00, + "haircut": 50, + "loan_security_price": 500.00 + }) + + loan_security_pledge = create_loan_security_pledge(self.applicant, pledges) + + loan = create_demand_loan(self.applicant, "Demand Loan", loan_security_pledge.name, + posting_date=get_first_day(nowdate())) + + loan.submit() + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime(first_date).year) * 100) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + + process_loan_interest_accrual(posting_date=last_date) + + loan_interest_accural = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name}) + + self.assertEquals(flt(loan_interest_accural.interest_amount, 2), flt(accrued_interest_amount, 2)) diff --git a/erpnext/loan_management/doctype/loan_repayment/__init__.py b/erpnext/loan_management/doctype/loan_repayment/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.js b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.js new file mode 100644 index 0000000000..82a2d802b8 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.js @@ -0,0 +1,64 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +{% include 'erpnext/loan_management/loan_common.js' %}; + +frappe.ui.form.on('Loan Repayment', { + // refresh: function(frm) { + + // } + onload: function(frm) { + frm.set_query('against_loan', function() { + return { + 'filters': { + 'docstatus': 1 + } + }; + }); + + if (frm.doc.against_loan && frm.doc.posting_date && frm.doc.docstatus == 0) { + frm.trigger('calculate_repayment_amounts'); + } + }, + + posting_date : function(frm) { + frm.trigger('calculate_repayment_amounts'); + }, + + against_loan: function(frm) { + if (frm.doc.posting_date) { + frm.trigger('calculate_repayment_amounts'); + } + }, + + payment_type: function(frm) { + if (frm.doc.posting_date) { + frm.trigger('calculate_repayment_amounts'); + } + }, + + calculate_repayment_amounts: function(frm) { + frappe.call({ + method: 'erpnext.loan_management.doctype.loan_repayment.loan_repayment.calculate_amounts', + args: { + 'against_loan': frm.doc.against_loan, + 'posting_date': frm.doc.posting_date, + 'payment_type': frm.doc.payment_type + }, + callback: function(r) { + let amounts = r.message; + frm.set_value('amount_paid', 0.0); + frm.set_df_property('amount_paid', 'read_only', frm.doc.payment_type == "Loan Closure" ? 1:0); + + frm.set_value('pending_principal_amount', amounts['pending_principal_amount']); + if (frm.doc.is_term_loan || frm.doc.payment_type == "Loan Closure") { + frm.set_value('payable_principal_amount', amounts['payable_principal_amount']); + frm.set_value('amount_paid', amounts['payable_amount']); + } + frm.set_value('interest_payable', amounts['interest_amount']); + frm.set_value('penalty_amount', amounts['penalty_amount']); + frm.set_value('payable_amount', amounts['payable_amount']); + } + }); + } +}); diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json new file mode 100644 index 0000000000..92e98177ea --- /dev/null +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json @@ -0,0 +1,267 @@ +{ + "actions": [], + "autoname": "LM-REP-.####", + "creation": "2019-09-03 14:44:39.977266", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "against_loan", + "applicant_type", + "applicant", + "loan_type", + "payment_type", + "column_break_3", + "company", + "posting_date", + "is_term_loan", + "payment_details_section", + "due_date", + "pending_principal_amount", + "interest_payable", + "payable_amount", + "column_break_9", + "payable_principal_amount", + "penalty_amount", + "amount_paid", + "accounting_dimensions_section", + "cost_center", + "references_section", + "reference_number", + "column_break_21", + "reference_date", + "paid_accrual_entries", + "partial_paid_entry", + "principal_amount_paid", + "amended_from" + ], + "fields": [ + { + "fieldname": "against_loan", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Against Loan", + "options": "Loan", + "reqd": 1 + }, + { + "fieldname": "posting_date", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Posting Date", + "reqd": 1 + }, + { + "fieldname": "payment_details_section", + "fieldtype": "Section Break", + "label": "Payment Details" + }, + { + "fieldname": "penalty_amount", + "fieldtype": "Currency", + "label": "Penalty Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "interest_payable", + "fieldtype": "Currency", + "label": "Interest Payable", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fetch_from": "against_loan.applicant", + "fieldname": "applicant", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Applicant", + "options": "applicant_type", + "read_only": 1, + "reqd": 1 + }, + { + "fetch_from": "against_loan.loan_type", + "fieldname": "loan_type", + "fieldtype": "Link", + "label": "Loan Type", + "options": "Loan Type", + "read_only": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "default": "Regular Payment", + "fieldname": "payment_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Payment Type", + "options": "\nRegular Payment\nLoan Closure", + "reqd": 1 + }, + { + "fieldname": "payable_amount", + "fieldtype": "Currency", + "label": "Payable Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "bold": 1, + "fieldname": "amount_paid", + "fieldtype": "Currency", + "label": "Amount Paid", + "options": "Company:company:default_currency", + "reqd": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Loan Repayment", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fetch_from": "against_loan.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "read_only": 1 + }, + { + "fieldname": "pending_principal_amount", + "fieldtype": "Currency", + "label": "Pending Principal Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "paid_accrual_entries", + "fieldtype": "Text", + "hidden": 1, + "label": "Paid Accrual Entries", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "against_loan.is_term_loan", + "fieldname": "is_term_loan", + "fieldtype": "Check", + "label": "Is Term Loan", + "read_only": 1 + }, + { + "depends_on": "eval:doc.payment_type==\"Loan Closure\" || doc.is_term_loan", + "fieldname": "payable_principal_amount", + "fieldtype": "Currency", + "label": "Payable Principal Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "references_section", + "fieldtype": "Section Break", + "label": "References" + }, + { + "fieldname": "reference_number", + "fieldtype": "Data", + "label": "Reference Number" + }, + { + "fieldname": "reference_date", + "fieldtype": "Date", + "label": "Reference Date" + }, + { + "fieldname": "column_break_21", + "fieldtype": "Column Break" + }, + { + "fieldname": "partial_paid_entry", + "fieldtype": "Text", + "hidden": 1, + "label": "Partial Paid Entry", + "read_only": 1 + }, + { + "default": "0.0", + "fieldname": "principal_amount_paid", + "fieldtype": "Currency", + "hidden": 1, + "label": "Principal Amount Paid", + "read_only": 1 + }, + { + "fetch_from": "against_loan.applicant_type", + "fieldname": "applicant_type", + "fieldtype": "Select", + "label": "Applicant Type", + "options": "Employee\nMember\nCustomer", + "read_only": 1 + }, + { + "fieldname": "due_date", + "fieldtype": "Date", + "label": "Due Date", + "read_only": 1 + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-02-24 07:35:47.168123", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Repayment", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py new file mode 100644 index 0000000000..a70e312880 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -0,0 +1,315 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, erpnext +import json +from frappe import _ +from frappe.utils import flt, getdate +from six import iteritems +from frappe.model.document import Document +from frappe.utils import date_diff, add_days, getdate, add_months, get_first_day, get_datetime +from erpnext.controllers.accounts_controller import AccountsController +from erpnext.accounts.general_ledger import make_gl_entries +from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import update_shortfall_status + +class LoanRepayment(AccountsController): + + def validate(self): + amounts = calculate_amounts(self.against_loan, self.posting_date, self.payment_type) + self.set_missing_values(amounts) + + def before_submit(self): + self.mark_as_paid() + + def on_submit(self): + self.make_gl_entries() + + def on_cancel(self): + self.mark_as_unpaid() + self.make_gl_entries(cancel=1) + + def set_missing_values(self, amounts): + if not self.posting_date: + self.posting_date = get_datetime() + + if not self.cost_center: + self.cost_center = erpnext.get_default_cost_center(self.company) + + if not self.interest_payable: + self.interest_payable = amounts['interest_amount'] + + if not self.penalty_amount: + self.penalty_amount = amounts['penalty_amount'] + + if not self.pending_principal_amount: + self.pending_principal_amount = amounts['pending_principal_amount'] + + if not self.payable_principal_amount and self.is_term_loan: + self.payable_principal_amount = amounts['payable_principal_amount'] + + if not self.payable_amount: + self.payable_amount = amounts['payable_amount'] + + if amounts.get('paid_accrual_entries'): + self.paid_accrual_entries = frappe.as_json(amounts.get('paid_accrual_entries')) + + if amounts.get('due_date'): + self.due_date = amounts.get('due_date') + + def mark_as_paid(self): + paid_entries = [] + paid_amount = self.amount_paid + interest_paid = paid_amount + + if not paid_amount: + frappe.throw(_("Amount paid cannot be zero")) + + if self.amount_paid < self.penalty_amount: + msg = _("Paid amount cannot be less than {0}").format(self.penalty_amount) + frappe.throw(msg) + + if self.payment_type == "Loan Closure" and flt(self.amount_paid, 2) < flt(self.payable_amount, 2): + msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount) + frappe.throw(msg) + + loan = frappe.get_doc("Loan", self.against_loan) + + if self.paid_accrual_entries: + paid_accrual_entries = json.loads(self.paid_accrual_entries) + + if paid_amount - self.penalty_amount > 0 and self.paid_accrual_entries: + + interest_paid = paid_amount - self.penalty_amount + + for lia, interest_amount in iteritems(paid_accrual_entries): + if interest_amount <= interest_paid: + paid_entries.append(lia) + interest_paid -= interest_amount + elif interest_paid: + self.partial_paid_entry = frappe.as_json({"name": lia, "interest_amount": interest_amount}) + frappe.db.set_value("Loan Interest Accrual", lia, "interest_amount", + interest_amount - interest_paid) + interest_paid = 0 + + if paid_entries: + self.paid_accrual_entries = frappe.as_json(paid_entries) + else: + self.paid_accrual_entries = "" + + if interest_paid: + self.principal_amount_paid = interest_paid + + if paid_entries: + frappe.db.sql("""UPDATE `tabLoan Interest Accrual` + SET is_paid = 1 where name in (%s)""" #nosec + % ", ".join(['%s']*len(paid_entries)), tuple(paid_entries)) + + if flt(loan.total_principal_paid + self.principal_amount_paid, 2) >= flt(loan.total_payment, 2): + frappe.db.set_value("Loan", self.against_loan, "status", "Loan Closure Requested") + + frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s + WHERE name = %s """, (loan.total_amount_paid + self.amount_paid, + loan.total_principal_paid + self.principal_amount_paid, self.against_loan)) + + update_shortfall_status(self.against_loan, self.principal_amount_paid) + + def mark_as_unpaid(self): + + loan = frappe.get_doc("Loan", self.against_loan) + + if self.paid_accrual_entries: + paid_accrual_entries = json.loads(self.paid_accrual_entries) + + if self.paid_accrual_entries: + frappe.db.sql("""UPDATE `tabLoan Interest Accrual` + SET is_paid = 0 where name in (%s)""" #nosec + % ", ".join(['%s']*len(paid_accrual_entries)), tuple(paid_accrual_entries)) + + if self.partial_paid_entry: + partial_paid_entry = json.loads(self.partial_paid_entry) + frappe.db.set_value("Loan Interest Accrual", partial_paid_entry["name"], "interest_amount", + partial_paid_entry["interest_amount"]) + + frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s + WHERE name = %s """, (loan.total_amount_paid - self.amount_paid, + loan.total_principal_paid - self.principal_amount_paid, self.against_loan)) + + if loan.status == "Loan Closure Requested": + frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed") + + def make_gl_entries(self, cancel=0, adv_adj=0): + gle_map = [] + loan_details = frappe.get_doc("Loan", self.against_loan) + + if self.penalty_amount: + gle_map.append( + self.get_gl_dict({ + "account": loan_details.loan_account, + "against": loan_details.payment_account, + "debit": self.penalty_amount, + "debit_in_account_currency": self.penalty_amount, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Against Loan:") + self.against_loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "posting_date": getdate(self.posting_date) + }) + ) + + gle_map.append( + self.get_gl_dict({ + "account": loan_details.penalty_income_account, + "against": loan_details.payment_account, + "credit": self.penalty_amount, + "credit_in_account_currency": self.penalty_amount, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Against Loan:") + self.against_loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "posting_date": getdate(self.posting_date) + }) + ) + + gle_map.append( + self.get_gl_dict({ + "account": loan_details.payment_account, + "against": loan_details.loan_account + ", " + loan_details.interest_income_account + + ", " + loan_details.penalty_income_account, + "debit": self.amount_paid, + "debit_in_account_currency": self.amount_paid , + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Against Loan:") + self.against_loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "posting_date": getdate(self.posting_date) + }) + ) + + gle_map.append( + self.get_gl_dict({ + "account": loan_details.loan_account, + "party_type": loan_details.applicant_type, + "party": loan_details.applicant, + "against": loan_details.payment_account, + "credit": self.amount_paid, + "credit_in_account_currency": self.amount_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Against Loan:") + self.against_loan, + "cost_center": self.cost_center, + "posting_date": getdate(self.posting_date) + }) + ) + + if gle_map: + make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj) + +def create_repayment_entry(loan, applicant, company, posting_date, loan_type, + payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None): + + lr = frappe.get_doc({ + "doctype": "Loan Repayment", + "against_loan": loan, + "payment_type": payment_type, + "company": company, + "posting_date": posting_date, + "applicant": applicant, + "penalty_amount": penalty_amount, + "interets_payable": interest_payable, + "payable_principal_amount": payable_principal_amount, + "amount_paid": amount_paid, + "loan_type": loan_type + }).insert() + + return lr + +def get_accrued_interest_entries(against_loan): + accrued_interest_entries = frappe.get_all("Loan Interest Accrual", + fields=["name", "interest_amount", "posting_date", "payable_principal_amount"], + filters = { + "loan": against_loan, + "is_paid": 0 + }, order_by="posting_date") + + return accrued_interest_entries + +# This function returns the amounts that are payable at the time of loan repayment based on posting date +# So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable + +def get_amounts(amounts, against_loan, posting_date, payment_type): + + against_loan_doc = frappe.get_doc("Loan", against_loan) + loan_type_details = frappe.get_doc("Loan Type", against_loan_doc.loan_type) + accrued_interest_entries = get_accrued_interest_entries(against_loan_doc.name) + + pending_accrual_entries = {} + + total_pending_interest = 0 + penalty_amount = 0 + payable_principal_amount = 0 + final_due_date = '' + + for entry in accrued_interest_entries: + # Loan repayment due date is one day after the loan interest is accrued + # no of late days are calculated based on loan repayment posting date + # and if no_of_late days are positive then penalty is levied + + due_date = add_days(entry.posting_date, 1) + no_of_late_days = date_diff(posting_date, + add_days(due_date, loan_type_details.grace_period_in_days)) + 1 + + if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary): + penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)/365 + + total_pending_interest += entry.interest_amount + payable_principal_amount += entry.payable_principal_amount + + pending_accrual_entries.setdefault(entry.name, entry.interest_amount) + final_due_date = due_date + + pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable + + if payment_type == "Loan Closure" and not payable_principal_amount: + pending_days = date_diff(posting_date, entry.posting_date) + 1 + payable_principal_amount = pending_principal_amount + per_day_interest = (payable_principal_amount * (loan_type_details.rate_of_interest / 100))/365 + total_pending_interest += (pending_days * per_day_interest) + + amounts["pending_principal_amount"] = pending_principal_amount + amounts["payable_principal_amount"] = payable_principal_amount + amounts["interest_amount"] = total_pending_interest + amounts["penalty_amount"] = penalty_amount + amounts["payable_amount"] = payable_principal_amount + total_pending_interest + penalty_amount + amounts["paid_accrual_entries"] = pending_accrual_entries + + if final_due_date: + amounts["due_date"] = final_due_date + + return amounts + +@frappe.whitelist() +def calculate_amounts(against_loan, posting_date, payment_type): + + amounts = { + 'penalty_amount': 0.0, + 'interest_amount': 0.0, + 'pending_principal_amount': 0.0, + 'payable_principal_amount': 0.0, + 'payable_amount': 0.0, + 'due_date': '' + } + + amounts = get_amounts(amounts, against_loan, posting_date, payment_type) + + return amounts + + + diff --git a/erpnext/loan_management/doctype/loan_repayment/test_loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/test_loan_repayment.py new file mode 100644 index 0000000000..73585a5159 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_repayment/test_loan_repayment.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLoanRepayment(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/loan_security/__init__.py b/erpnext/loan_management/doctype/loan_security/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/loan_security/loan_security.js b/erpnext/loan_management/doctype/loan_security/loan_security.js new file mode 100644 index 0000000000..0e815af76a --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security/loan_security.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Loan Security', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/loan_management/doctype/loan_security/loan_security.json b/erpnext/loan_management/doctype/loan_security/loan_security.json new file mode 100644 index 0000000000..e6984ee7f1 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security/loan_security.json @@ -0,0 +1,95 @@ +{ + "autoname": "field:loan_security_name", + "creation": "2019-09-02 15:07:08.885593", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan_security_type", + "loan_security_code", + "loan_security_name", + "unit_of_measure", + "column_break_3", + "haircut", + "disabled" + ], + "fields": [ + { + "fieldname": "loan_security_name", + "fieldtype": "Data", + "label": "Loan Security Name", + "unique": 1 + }, + { + "fetch_from": "loan_security_type.haircut", + "fieldname": "haircut", + "fieldtype": "Percent", + "label": "Haircut %" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "loan_security_type", + "fieldtype": "Link", + "label": "Loan Security Type", + "options": "Loan Security Type" + }, + { + "fieldname": "loan_security_code", + "fieldtype": "Data", + "label": "Loan Security Code", + "unique": 1 + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fetch_from": "loan_security_type.unit_of_measure", + "fieldname": "unit_of_measure", + "fieldtype": "Link", + "label": "Unit Of Measure", + "options": "UOM" + } + ], + "modified": "2019-11-16 11:36:37.901656", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Security", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "search_fields": "loan_security_code", + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_security/loan_security.py b/erpnext/loan_management/doctype/loan_security/loan_security.py new file mode 100644 index 0000000000..800ad12957 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security/loan_security.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class LoanSecurity(Document): + pass diff --git a/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py b/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py new file mode 100644 index 0000000000..878b3fd051 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'loan_security', + 'transactions': [ + { + 'items': ['Loan Application', 'Loan Security Price'] + }, + { + 'items': ['Loan Security Pledge', 'Loan Security Unpledge'] + } + ] + } \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_security/test_loan_security.py b/erpnext/loan_management/doctype/loan_security/test_loan_security.py new file mode 100644 index 0000000000..24dbc68046 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security/test_loan_security.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLoanSecurity(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/loan_security_pledge/__init__.py b/erpnext/loan_management/doctype/loan_security_pledge/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js new file mode 100644 index 0000000000..82837b3dac --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js @@ -0,0 +1,40 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Loan Security Pledge', { + calculate_amounts: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.loan_security_price); + frappe.model.set_value(cdt, cdn, 'post_haircut_amount', cint(row.amount - (row.amount * row.haircut/100))); + + let amount = 0; + let maximum_amount = 0; + $.each(frm.doc.securities || [], function(i, item){ + amount += item.amount; + maximum_amount += item.post_haircut_amount; + }); + + frm.set_value('total_security_value', amount); + frm.set_value('maximum_loan_value', maximum_amount); + } +}); + +frappe.ui.form.on("Pledge", { + loan_security: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + frappe.call({ + method: "erpnext.loan_management.doctype.loan_security_price.loan_security_price.get_loan_security_price", + args: { + loan_security: row.loan_security + }, + callback: function(r) { + frappe.model.set_value(cdt, cdn, 'loan_security_price', r.message); + frm.events.calculate_amounts(frm, cdt, cdn); + } + }); + }, + + qty: function(frm, cdt, cdn) { + frm.events.calculate_amounts(frm, cdt, cdn); + }, +}); \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json new file mode 100644 index 0000000000..1553844704 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json @@ -0,0 +1,176 @@ +{ + "autoname": "LS-.{applicant}.-.#####", + "creation": "2019-08-29 18:48:51.371674", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan_details_section", + "loan_application", + "loan", + "applicant_type", + "applicant", + "column_break_3", + "company", + "pledge_time", + "status", + "loan_security_details_section", + "securities", + "section_break_10", + "total_security_value", + "column_break_11", + "maximum_loan_value", + "amended_from" + ], + "fields": [ + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Loan Security Pledge", + "print_hide": 1, + "read_only": 1 + }, + { + "fetch_from": "loan_application.applicant", + "fieldname": "applicant", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Applicant", + "options": "applicant_type", + "reqd": 1 + }, + { + "fieldname": "loan_security_details_section", + "fieldtype": "Section Break", + "label": "Loan Security Details" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "loan", + "fieldtype": "Link", + "label": "Loan", + "options": "Loan", + "read_only": 1 + }, + { + "fieldname": "loan_application", + "fieldtype": "Link", + "label": "Loan Application", + "options": "Loan Application", + "read_only": 1 + }, + { + "fieldname": "total_security_value", + "fieldtype": "Currency", + "label": "Total Security Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "maximum_loan_value", + "fieldtype": "Currency", + "label": "Maximum Loan Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "loan_details_section", + "fieldtype": "Section Break", + "label": "Loan Details" + }, + { + "default": "Requested", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Requested\nUnpledged\nPledged\nPartially Pledged", + "read_only": 1 + }, + { + "fieldname": "pledge_time", + "fieldtype": "Datetime", + "label": "Pledge Time", + "read_only": 1 + }, + { + "fieldname": "securities", + "fieldtype": "Table", + "label": "Securities", + "options": "Pledge", + "reqd": 1 + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break", + "label": "Totals" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fetch_from": "loan.applicant_type", + "fieldname": "applicant_type", + "fieldtype": "Select", + "label": "Applicant Type", + "options": "Employee\nMember\nCustomer", + "reqd": 1 + } + ], + "is_submittable": 1, + "modified": "2019-10-10 13:22:53.297519", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Security Pledge", + "owner": "Administrator", + "permissions": [ + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "quick_entry": 1, + "search_fields": "applicant", + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py new file mode 100644 index 0000000000..b405ccae55 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import now_datetime, cint +from frappe.model.document import Document +from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import update_shortfall_status +from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price + +class LoanSecurityPledge(Document): + def validate(self): + self.set_pledge_amount() + + def on_submit(self): + if self.loan: + self.db_set("status", "Pledged") + self.db_set("pledge_time", now_datetime()) + update_shortfall_status(self.loan, self.total_security_value) + update_loan(self.loan, self.maximum_loan_value) + + def set_pledge_amount(self): + total_security_value = 0 + maximum_loan_value = 0 + + for pledge in self.securities: + + if not pledge.qty and not pledge.amount: + frappe.throw(_("Qty or Amount is mandatroy for loan security")) + + pledge.loan_security_price = get_loan_security_price(pledge.loan_security) + + if not pledge.qty: + pledge.qty = cint(pledge.amount/pledge.loan_security_price) + + pledge.amount = pledge.qty * pledge.loan_security_price + pledge.post_haircut_amount = cint(pledge.amount - (pledge.amount * pledge.haircut/100)) + + total_security_value += pledge.amount + maximum_loan_value += pledge.post_haircut_amount + + self.total_security_value = total_security_value + self.maximum_loan_value = maximum_loan_value + +def update_loan(loan, maximum_value_against_pledge): + maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_value']) + + frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_value=%s, is_secured_loan=1 + WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan)) diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge_list.js b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge_list.js new file mode 100644 index 0000000000..174d1b0d62 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge_list.js @@ -0,0 +1,15 @@ +// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// License: GNU General Public License v3. See license.txt + +// render +frappe.listview_settings['Loan Security Pledge'] = { + add_fields: ["status"], + get_indicator: function(doc) { + var status_color = { + "Unpledged": "orange", + "Pledged": "green", + "Partially Pledged": "green" + }; + return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; + } +}; diff --git a/erpnext/loan_management/doctype/loan_security_pledge/test_loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/test_loan_security_pledge.py new file mode 100644 index 0000000000..d2347c0098 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_pledge/test_loan_security_pledge.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLoanSecurityPledge(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/loan_security_price/__init__.py b/erpnext/loan_management/doctype/loan_security_price/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.js b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.js new file mode 100644 index 0000000000..31b4ec7249 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Loan Security Price', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json new file mode 100644 index 0000000000..db260a4a9e --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json @@ -0,0 +1,117 @@ +{ + "autoname": "LM-LSP-.####", + "creation": "2019-09-03 18:20:31.382887", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan_security", + "loan_security_type", + "column_break_2", + "uom", + "section_break_4", + "loan_security_price", + "section_break_6", + "valid_from", + "column_break_8", + "valid_upto" + ], + "fields": [ + { + "fieldname": "loan_security", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Loan Security", + "options": "Loan Security", + "reqd": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fetch_from": "loan_security.unit_of_measure", + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM", + "read_only": 1 + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, + { + "fieldname": "loan_security_price", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Loan Security Price", + "reqd": 1 + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "valid_from", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Valid From", + "reqd": 1 + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "fieldname": "valid_upto", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Valid Upto", + "reqd": 1 + }, + { + "fetch_from": "loan_security.loan_security_type", + "fieldname": "loan_security_type", + "fieldtype": "Link", + "label": "Loan Security Type", + "options": "Loan Security Type", + "read_only": 1 + } + ], + "modified": "2019-10-26 09:46:46.069667", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Security Price", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py new file mode 100644 index 0000000000..2855b52610 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import getdate, now_datetime, add_to_date, get_datetime, get_timestamp, get_datetime_str +from six import iteritems + +class LoanSecurityPrice(Document): + def validate(self): + self.validate_dates() + + def validate_dates(self): + + if self.valid_from > self.valid_upto: + frappe.throw(_("Valid From Time must be lesser than Valid Upto Time.")) + + existing_loan_security = frappe.db.sql(""" SELECT name from `tabLoan Security Price` + WHERE loan_security = %s AND name != %s AND (valid_from BETWEEN %s and %s OR valid_upto BETWEEN %s and %s) """, + (self.loan_security, self.name, self.valid_from, self.valid_upto, self.valid_from, self.valid_upto)) + + if existing_loan_security: + frappe.throw(_("Loan Security Price overlapping with {0}").format(existing_loan_security[0][0])) + +@frappe.whitelist() +def get_loan_security_price(loan_security, valid_time=None): + if not valid_time: + valid_time = get_datetime() + + loan_security_price = frappe.db.get_value("Loan Security Price", { + 'loan_security': loan_security, + 'valid_from': ("<=",valid_time), + 'valid_upto': (">=", valid_time) + }, 'loan_security_price') + + if not loan_security_price: + frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(loan_security))) + else: + return loan_security_price + + + + + + + + + diff --git a/erpnext/loan_management/doctype/loan_security_price/test_loan_security_price.py b/erpnext/loan_management/doctype/loan_security_price/test_loan_security_price.py new file mode 100644 index 0000000000..2fe0bd5a24 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_price/test_loan_security_price.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLoanSecurityPrice(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/__init__.py b/erpnext/loan_management/doctype/loan_security_shortfall/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.js b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.js new file mode 100644 index 0000000000..f26c138371 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.js @@ -0,0 +1,25 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Loan Security Shortfall', { + refresh: function(frm) { + frm.add_custom_button(__("Add Loan Security"), function() { + frm.trigger('shortfall_action'); + }); + }, + + shortfall_action: function(frm) { + frappe.call({ + method: "erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall.add_security", + args: { + 'loan': frm.doc.loan + }, + callback: function(r) { + if (r.message) { + let doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + } + }); + } +}); diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json new file mode 100644 index 0000000000..102bc0d71d --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json @@ -0,0 +1,126 @@ +{ + "autoname": "LM-LSS-.#####", + "creation": "2019-09-06 11:33:34.709540", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan", + "status", + "column_break_3", + "shortfall_time", + "section_break_3", + "loan_amount", + "shortfall_amount", + "column_break_8", + "security_value", + "section_break_8", + "process_loan_security_shortfall" + ], + "fields": [ + { + "fieldname": "loan", + "fieldtype": "Link", + "label": "Loan ", + "options": "Loan", + "read_only": 1 + }, + { + "fieldname": "loan_amount", + "fieldtype": "Currency", + "label": "Loan Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "security_value", + "fieldtype": "Currency", + "label": "Security Value ", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "shortfall_amount", + "fieldtype": "Currency", + "label": "Shortfall Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break" + }, + { + "description": "America/New_York", + "fieldname": "shortfall_time", + "fieldtype": "Datetime", + "label": "Shortfall Time", + "read_only": 1 + }, + { + "default": "Pending", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "\nPending\nCompleted", + "read_only": 1 + }, + { + "fieldname": "section_break_8", + "fieldtype": "Section Break" + }, + { + "fieldname": "process_loan_security_shortfall", + "fieldtype": "Link", + "label": "Process Loan Security Shortfall", + "options": "Process Loan Security Shortfall", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + } + ], + "in_create": 1, + "modified": "2019-10-24 06:24:26.128997", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Security Shortfall", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py new file mode 100644 index 0000000000..599f6dafaa --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import get_datetime +from frappe.model.document import Document +from six import iteritems + +class LoanSecurityShortfall(Document): + pass + +def update_shortfall_status(loan, security_value): + loan_security_shortfall = frappe.db.get_value("Loan Security Shortfall", + {"loan": loan, "status": "Pending"}, ['name', 'shortfall_amount'], as_dict=1) + + if not loan_security_shortfall: + return + + if security_value >= loan_security_shortfall.shortfall_amount: + frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, "status", "Completed") + else: + frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, + "shortfall_amount", loan_security_shortfall.shortfall_amount - security_value) + + +@frappe.whitelist() +def add_security(loan): + loan_details = frappe.db.get_value("Loan", loan, ['applicant', 'company', 'applicant_type'], as_dict=1) + + loan_security_pledge = frappe.new_doc("Loan Security Pledge") + loan_security_pledge.loan = loan + loan_security_pledge.company = loan_details.company + loan_security_pledge.applicant_type = loan_details.applicant_type + loan_security_pledge.applicant = loan_details.applicant + + return loan_security_pledge.as_dict() + +def check_for_ltv_shortfall(process_loan_security_shortfall=None): + + update_time = get_datetime() + + if not process_loan_security_shortfall: + process = frappe.new_doc("Process Loan Security Shortfall") + process.update_time = update_time + process.submit() + + process_loan_security_shortfall = process.name + + loan_security_price_map = frappe._dict(frappe.get_all("Loan Security Price", + fields=["loan_security", "loan_security_price"], + filters = { + "valid_from": ("<=", update_time), + "valid_upto": (">=", update_time) + }, as_list=1)) + + loans = frappe.db.sql(""" SELECT l.name, l.loan_amount, l.total_principal_paid, lp.loan_security, lp.haircut, lp.qty + FROM `tabLoan` l, `tabPledge` lp , `tabLoan Security Pledge`p WHERE lp.parent = p.name and p.loan = l.name and l.docstatus = 1 + and l.is_secured_loan and l.status = 'Disbursed' and p.status in ('Pledged', 'Partially Unpledged')""", as_dict=1) + + loan_security_map = {} + + for loan in loans: + loan_security_map.setdefault(loan.name, { + "loan_amount": loan.loan_amount - loan.total_principal_paid, + "security_value": 0.0 + }) + + current_loan_security_amount = loan_security_price_map.get(loan.loan_security, 0) * loan.qty + + loan_security_map[loan.name]['security_value'] += current_loan_security_amount - (current_loan_security_amount * loan.haircut/100) + + for loan, value in iteritems(loan_security_map): + if value["security_value"] < value["loan_amount"]: + create_loan_security_shortfall(loan, value, process_loan_security_shortfall) + +def create_loan_security_shortfall(loan, value, process_loan_security_shortfall): + + existing_shortfall = frappe.db.get_value("Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name") + + if existing_shortfall: + ltv_shortfall = frappe.get_doc("Loan Security Shortfall", existing_shortfall) + else: + ltv_shortfall = frappe.new_doc("Loan Security Shortfall") + ltv_shortfall.loan = loan + + ltv_shortfall.shortfall_time = get_datetime() + ltv_shortfall.loan_amount = value["loan_amount"] + ltv_shortfall.security_value = value["security_value"] + ltv_shortfall.shortfall_amount = value["loan_amount"] - value["security_value"] + ltv_shortfall.process_loan_security_shortfall = process_loan_security_shortfall + ltv_shortfall.save() + diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/test_loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/test_loan_security_shortfall.py new file mode 100644 index 0000000000..b82f3d2593 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_shortfall/test_loan_security_shortfall.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLoanSecurityShortfall(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/loan_security_type/__init__.py b/erpnext/loan_management/doctype/loan_security_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/loan_security_type/loan_security_type.js b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.js new file mode 100644 index 0000000000..3a1e0689c1 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Loan Security Type', { + // refresh: function(frm) { + + // }, +}); diff --git a/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json new file mode 100644 index 0000000000..a5ab057cdc --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json @@ -0,0 +1,73 @@ +{ + "autoname": "field:loan_security_type", + "creation": "2019-08-29 18:46:07.322056", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan_security_type", + "unit_of_measure", + "haircut", + "disabled" + ], + "fields": [ + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "loan_security_type", + "fieldtype": "Data", + "label": "Loan Security Type", + "unique": 1 + }, + { + "fieldname": "haircut", + "fieldtype": "Percent", + "label": "Haircut %" + }, + { + "fieldname": "unit_of_measure", + "fieldtype": "Link", + "label": "Unit Of Measure", + "options": "UOM" + } + ], + "modified": "2019-10-10 03:05:37.912866", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Security Type", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_security_type/loan_security_type.py b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.py new file mode 100644 index 0000000000..cb8a50a27b --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class LoanSecurityType(Document): + pass diff --git a/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py b/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py new file mode 100644 index 0000000000..ac33589b54 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'loan_security_type', + 'transactions': [ + { + 'items': ['Loan Security', 'Loan Security Price'] + }, + { + 'items': ['Loan Security Pledge', 'Loan Security Unpledge'] + } + ] + } \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_security_type/test_loan_security_type.py b/erpnext/loan_management/doctype/loan_security_type/test_loan_security_type.py new file mode 100644 index 0000000000..f7d845a779 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_type/test_loan_security_type.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLoanSecurityType(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/__init__.py b/erpnext/loan_management/doctype/loan_security_unpledge/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js new file mode 100644 index 0000000000..72c5f38cf3 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js @@ -0,0 +1,13 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Loan Security Unpledge', { + refresh: function(frm) { + + frm.set_query("against_pledge", "securities", () => { + return { + filters : [["status", "in", ["Pledged", "Partially Pledged"]]] + }; + }); + } +}); diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json new file mode 100644 index 0000000000..ba94855031 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json @@ -0,0 +1,159 @@ +{ + "autoname": "LSU-.{applicant}.-.#####", + "creation": "2019-09-21 13:23:16.117028", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan_details_section", + "loan", + "applicant_type", + "applicant", + "column_break_3", + "company", + "unpledge_time", + "status", + "loan_security_details_section", + "securities", + "unpledge_type", + "amended_from" + ], + "fields": [ + { + "fieldname": "loan_details_section", + "fieldtype": "Section Break", + "label": "Loan Details" + }, + { + "fetch_from": "loan_application.applicant", + "fieldname": "applicant", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Applicant", + "options": "applicant_type", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "loan", + "fieldtype": "Link", + "label": "Loan", + "options": "Loan", + "reqd": 1 + }, + { + "allow_on_submit": 1, + "default": "Requested", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Requested\nApproved", + "permlevel": 1 + }, + { + "fieldname": "unpledge_time", + "fieldtype": "Datetime", + "label": "Unpledge Time", + "read_only": 1 + }, + { + "fieldname": "loan_security_details_section", + "fieldtype": "Section Break", + "label": "Loan Security Details" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Loan Security Unpledge", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "securities", + "fieldtype": "Table", + "label": "Securities", + "options": "Unpledge", + "reqd": 1 + }, + { + "fieldname": "unpledge_type", + "fieldtype": "Data", + "hidden": 1, + "label": "Unpledge Type", + "read_only": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fetch_from": "loan.applicant_type", + "fieldname": "applicant_type", + "fieldtype": "Select", + "label": "Applicant Type", + "options": "Employee\nMember\nCustomer", + "reqd": 1 + } + ], + "is_submittable": 1, + "modified": "2019-10-28 07:41:47.084882", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Security Unpledge", + "owner": "Administrator", + "permissions": [ + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "delete": 1, + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "search_fields": "applicant", + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py new file mode 100644 index 0000000000..02b1ecb4ca --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import get_datetime, flt +import json +from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price + +class LoanSecurityUnpledge(Document): + def validate(self): + self.validate_pledges() + + def validate_pledges(self): + pledge_details = self.get_pledge_details() + + loan = frappe.get_doc("Loan", self.loan) + + pledge_qty_map = {} + remaining_qty = 0 + unpledge_value = 0 + + for pledge in pledge_details: + pledge_qty_map.setdefault((pledge.parent, pledge.loan_security), pledge.qty) + + for security in self.securities: + pledged_qty = pledge_qty_map.get((security.against_pledge, security.loan_security), 0) + if not pledged_qty: + frappe.throw(_("Zero qty of {0} pledged against loan {0}").format(frappe.bold(security.loan_security), + frappe.bold(self.loan))) + + unpledge_qty = pledged_qty - security.qty + security_price = security.qty * get_loan_security_price(security.loan_security) + + if unpledge_qty < 0: + frappe.throw(_("Cannot unpledge more than {0} qty of {0}").format(frappe.bold(pledged_qty), + frappe.bold(security.loan_security))) + + remaining_qty += unpledge_qty + unpledge_value += security_price - flt(security_price * security.haircut/100) + + if unpledge_value > loan.total_principal_paid: + frappe.throw(_("Cannot Unpledge, loan security value is greater than the repaid amount")) + + if not remaining_qty: + self.db_set('unpledge_type', 'Unpledged') + else: + self.db_set('unpledge_type', 'Partially Pledged') + + + def get_pledge_details(self): + pledge_details = frappe.db.sql(""" + SELECT p.parent, p.loan_security, p.qty as qty FROM + `tabLoan Security Pledge` lsp, + `tabPledge` p + WHERE + p.parent = lsp.name + AND lsp.loan = %s + AND lsp.docstatus = 1 + AND lsp.status = "Pledged" + """,(self.loan), as_dict=1) + + return pledge_details + + def on_update_after_submit(self): + if self.status == "Approved": + frappe.db.sql(""" + UPDATE + `tabPledge` p, `tabUnpledge` u, `tabLoan Security Pledge` lsp, + `tabLoan Security Unpledge` lsu SET p.qty = (p.qty - u.qty) + WHERE + lsp.loan = %s + AND lsu.status = 'Requested' + AND u.parent = %s + AND p.parent = u.against_pledge + AND p.loan_security = u.loan_security""",(self.loan, self.name)) + + frappe.db.sql("""UPDATE `tabLoan Security Pledge` + SET status = %s WHERE loan = %s""", (self.unpledge_type, self.loan)) + + if self.unpledge_type == 'Unpledged': + frappe.db.set_value("Loan", self.loan, 'status', 'Closed') diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge_list.js b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge_list.js new file mode 100644 index 0000000000..196ebbb96a --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge_list.js @@ -0,0 +1,14 @@ +// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// License: GNU General Public License v3. See license.txt + +// render +frappe.listview_settings['Loan Security Unpledge'] = { + add_fields: ["status"], + get_indicator: function(doc) { + var status_color = { + "Requested": "orange", + "Approved": "green", + }; + return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; + } +}; diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/test_loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/test_loan_security_unpledge.py new file mode 100644 index 0000000000..5b5c205bda --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_unpledge/test_loan_security_unpledge.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLoanSecurityUnpledge(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/loan_type/__init__.py b/erpnext/loan_management/doctype/loan_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.js b/erpnext/loan_management/doctype/loan_type/loan_type.js new file mode 100644 index 0000000000..04c89c4549 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_type/loan_type.js @@ -0,0 +1,30 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Loan Type', { + onload: function(frm) { + $.each(["penalty_income_account", "interest_income_account"], function (i, field) { + frm.set_query(field, function () { + return { + "filters": { + "company": frm.doc.company, + "root_type": "Income", + "is_group": 0 + } + }; + }); + }); + + $.each(["payment_account", "loan_account"], function (i, field) { + frm.set_query(field, function () { + return { + "filters": { + "company": frm.doc.company, + "root_type": "Asset", + "is_group": 0 + } + }; + }); + }); + } +}); diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json new file mode 100644 index 0000000000..a3525db9a5 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_type/loan_type.json @@ -0,0 +1,170 @@ +{ + "actions": [], + "autoname": "field:loan_name", + "creation": "2019-08-29 18:08:38.159726", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan_name", + "maximum_loan_amount", + "rate_of_interest", + "penalty_interest_rate", + "grace_period_in_days", + "column_break_2", + "company", + "is_term_loan", + "disabled", + "description", + "account_details_section", + "mode_of_payment", + "payment_account", + "loan_account", + "column_break_12", + "interest_income_account", + "penalty_income_account", + "amended_from" + ], + "fields": [ + { + "fieldname": "loan_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Loan Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "maximum_loan_amount", + "fieldtype": "Currency", + "label": "Maximum Loan Amount", + "options": "Company:company:default_currency" + }, + { + "fieldname": "rate_of_interest", + "fieldtype": "Percent", + "label": "Rate of Interest (%) Yearly", + "reqd": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "description", + "fieldtype": "Text", + "label": "Description" + }, + { + "fieldname": "account_details_section", + "fieldtype": "Section Break", + "label": "Account Details" + }, + { + "fieldname": "mode_of_payment", + "fieldtype": "Link", + "label": "Mode of Payment", + "options": "Mode of Payment", + "reqd": 1 + }, + { + "fieldname": "payment_account", + "fieldtype": "Link", + "label": "Payment Account", + "options": "Account", + "reqd": 1 + }, + { + "fieldname": "loan_account", + "fieldtype": "Link", + "label": "Loan Account", + "options": "Account", + "reqd": 1 + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "fieldname": "interest_income_account", + "fieldtype": "Link", + "label": "Interest Income Account", + "options": "Account", + "reqd": 1 + }, + { + "fieldname": "penalty_income_account", + "fieldtype": "Link", + "label": "Penalty Income Account", + "options": "Account", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "is_term_loan", + "fieldtype": "Check", + "label": "Is Term Loan" + }, + { + "description": "Penalty Interest Rate is levied on the pending interest amount on a daily basis in case of delayed repayment ", + "fieldname": "penalty_interest_rate", + "fieldtype": "Percent", + "label": "Penalty Interest Rate (%) Per Day" + }, + { + "fieldname": "grace_period_in_days", + "fieldtype": "Int", + "label": "Grace Period in Days" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Loan Type", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-02-03 05:03:00.334813", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Type", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "Employee" + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.py b/erpnext/loan_management/doctype/loan_type/loan_type.py new file mode 100644 index 0000000000..14b18ab57a --- /dev/null +++ b/erpnext/loan_management/doctype/loan_type/loan_type.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.model.document import Document + +class LoanType(Document): + def validate(self): + self.validate_accounts() + + def validate_accounts(self): + for fieldname in ['payment_account', 'loan_account', 'interest_income_account', 'penalty_income_account']: + company = frappe.get_value("Account", self.get(fieldname), 'company') + + if company and company != self.company: + frappe.throw(_("Account {0} does not belong to company {1}").format(frappe.bold(self.get(fieldname)), + frappe.bold(self.company))) + diff --git a/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py b/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py new file mode 100644 index 0000000000..58c668948c --- /dev/null +++ b/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'loan_type', + 'transactions': [ + { + 'items': ['Loan Repayment', 'Loan'] + }, + { + 'items': ['Loan Application'] + } + ] + } \ No newline at end of file diff --git a/erpnext/hr/doctype/loan_type/test_loan_type.py b/erpnext/loan_management/doctype/loan_type/test_loan_type.py similarity index 53% rename from erpnext/hr/doctype/loan_type/test_loan_type.py rename to erpnext/loan_management/doctype/loan_type/test_loan_type.py index 078e11e262..5877ab6f7f 100644 --- a/erpnext/hr/doctype/loan_type/test_loan_type.py +++ b/erpnext/loan_management/doctype/loan_type/test_loan_type.py @@ -1,12 +1,10 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals -import frappe +# import frappe import unittest -# test_records = frappe.get_test_records('Loan Type') - class TestLoanType(unittest.TestCase): pass diff --git a/erpnext/loan_management/doctype/pledge/__init__.py b/erpnext/loan_management/doctype/pledge/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/pledge/pledge.js b/erpnext/loan_management/doctype/pledge/pledge.js new file mode 100644 index 0000000000..fb6ab10778 --- /dev/null +++ b/erpnext/loan_management/doctype/pledge/pledge.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Pledge', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/loan_management/doctype/pledge/pledge.json b/erpnext/loan_management/doctype/pledge/pledge.json new file mode 100644 index 0000000000..f22a21e3be --- /dev/null +++ b/erpnext/loan_management/doctype/pledge/pledge.json @@ -0,0 +1,99 @@ +{ + "creation": "2019-09-09 17:06:16.756573", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan_security", + "loan_security_type", + "loan_security_code", + "uom", + "column_break_5", + "qty", + "haircut", + "loan_security_price", + "amount", + "post_haircut_amount" + ], + "fields": [ + { + "fieldname": "loan_security", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Loan Security", + "options": "Loan Security", + "reqd": 1 + }, + { + "fetch_from": "loan_security.loan_security_type", + "fieldname": "loan_security_type", + "fieldtype": "Link", + "label": "Loan Security Type", + "options": "Loan Security Type", + "read_only": 1 + }, + { + "fetch_from": "loan_security.loan_security_code", + "fieldname": "loan_security_code", + "fieldtype": "Data", + "label": "Loan Security Code" + }, + { + "fetch_from": "loan_security.unit_of_measure", + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM" + }, + { + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Quantity" + }, + { + "fieldname": "loan_security_price", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Loan Security Price", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fetch_from": "loan_security.haircut", + "fieldname": "haircut", + "fieldtype": "Percent", + "label": "Haircut %", + "read_only": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "Company:company:default_currency" + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "post_haircut_amount", + "fieldtype": "Currency", + "label": "Post Haircut Amount", + "options": "Company:company:default_currency", + "read_only": 1 + } + ], + "istable": 1, + "modified": "2019-12-03 10:59:58.001421", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Pledge", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/loan_type/loan_type.py b/erpnext/loan_management/doctype/pledge/pledge.py similarity index 62% rename from erpnext/hr/doctype/loan_type/loan_type.py rename to erpnext/loan_management/doctype/pledge/pledge.py index 2714e206d8..0457ad7abd 100644 --- a/erpnext/hr/doctype/loan_type/loan_type.py +++ b/erpnext/loan_management/doctype/pledge/pledge.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt from __future__ import unicode_literals -import frappe +# import frappe from frappe.model.document import Document -class LoanType(Document): +class Pledge(Document): pass diff --git a/erpnext/loan_management/doctype/pledge/test_pledge.py b/erpnext/loan_management/doctype/pledge/test_pledge.py new file mode 100644 index 0000000000..2e01dc114d --- /dev/null +++ b/erpnext/loan_management/doctype/pledge/test_pledge.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestPledge(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/__init__.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.js b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.js new file mode 100644 index 0000000000..c596be2d2a --- /dev/null +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Process Loan Interest Accrual', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json new file mode 100644 index 0000000000..7f79cb1fd9 --- /dev/null +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json @@ -0,0 +1,81 @@ +{ + "actions": [], + "autoname": "LM-PLA-.#####", + "creation": "2019-09-19 06:08:12.363640", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "posting_date", + "loan_type", + "loan", + "amended_from" + ], + "fields": [ + { + "fieldname": "posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Posting Date", + "reqd": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Process Loan Interest Accrual", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "loan_type", + "fieldtype": "Link", + "label": "Loan Type", + "options": "Loan Type" + }, + { + "fieldname": "loan", + "fieldtype": "Link", + "label": "Loan ", + "options": "Loan" + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-02-01 08:14:33.978636", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Process Loan Interest Accrual", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py new file mode 100644 index 0000000000..0f33da918d --- /dev/null +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import nowdate +from frappe.model.document import Document +from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import make_accrual_interest_entry_for_demand_loans + +class ProcessLoanInterestAccrual(Document): + def on_submit(self): + open_loans = [] + + if self.loan: + loan_doc = frappe.get_doc('Loan', self.loan) + open_loans.append(loan_doc) + + make_accrual_interest_entry_for_demand_loans(self.posting_date, self.name, + open_loans = open_loans, loan_type = self.loan_type) + +def process_loan_interest_accrual(posting_date=None, loan_type=None, loan=None): + loan_process = frappe.new_doc('Process Loan Interest Accrual') + loan_process.posting_date = posting_date or nowdate() + loan_process.loan_type = loan_type + loan_process.loan = loan + + loan_process.submit() + diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py new file mode 100644 index 0000000000..243a7a3ba6 --- /dev/null +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'process_loan_interest_accrual', + 'transactions': [ + { + 'items': ['Loan Interest Accrual'] + } + ] + } \ No newline at end of file diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/test_process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/test_process_loan_interest_accrual.py new file mode 100644 index 0000000000..6bfd3f4210 --- /dev/null +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/test_process_loan_interest_accrual.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestProcessLoanInterestAccrual(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/__init__.py b/erpnext/loan_management/doctype/process_loan_security_shortfall/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.js b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.js new file mode 100644 index 0000000000..645e3ada9a --- /dev/null +++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Process Loan Security Shortfall', { + onload: function(frm) { + frm.set_value('update_time', frappe.datetime.now_datetime()); + } +}); diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json new file mode 100644 index 0000000000..ffc3671132 --- /dev/null +++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json @@ -0,0 +1,67 @@ +{ + "actions": [], + "autoname": "LM-PLS-.#####", + "creation": "2019-09-19 06:43:26.742336", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "update_time", + "amended_from" + ], + "fields": [ + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Process Loan Security Shortfall", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "update_time", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Update Time", + "read_only": 1, + "reqd": 1 + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-02-01 08:14:05.845161", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Process Loan Security Shortfall", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py new file mode 100644 index 0000000000..417e3678c9 --- /dev/null +++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import get_datetime +from frappe import _ +from frappe.model.document import Document +from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import check_for_ltv_shortfall + +class ProcessLoanSecurityShortfall(Document): + def onload(self): + self.set_onload('update_time', get_datetime()) + + def on_submit(self): + check_for_ltv_shortfall(process_loan_security_shortfall = self.name) diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py new file mode 100644 index 0000000000..dc9bd81a1d --- /dev/null +++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'process_loan_security_shortfall', + 'transactions': [ + { + 'items': ['Loan Security Shortfall'] + } + ] + } \ No newline at end of file diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/test_process_loan_security_shortfall.py b/erpnext/loan_management/doctype/process_loan_security_shortfall/test_process_loan_security_shortfall.py new file mode 100644 index 0000000000..cd379a1bea --- /dev/null +++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/test_process_loan_security_shortfall.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestProcessLoanSecurityShortfall(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/proposed_pledge/__init__.py b/erpnext/loan_management/doctype/proposed_pledge/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json new file mode 100644 index 0000000000..aee7c2ced5 --- /dev/null +++ b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json @@ -0,0 +1,70 @@ +{ + "creation": "2019-08-29 22:29:37.628178", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan_security", + "qty", + "loan_security_price", + "amount", + "haircut", + "post_haircut_amount" + ], + "fields": [ + { + "fieldname": "loan_security_price", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Loan Security Price", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "Company:company:default_currency" + }, + { + "fetch_from": "loan_security.haircut", + "fieldname": "haircut", + "fieldtype": "Percent", + "label": "Haircut %", + "read_only": 1 + }, + { + "fetch_from": "loan_security_pledge.qty", + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Quantity" + }, + { + "fieldname": "loan_security", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Loan Security", + "options": "Loan Security" + }, + { + "fieldname": "post_haircut_amount", + "fieldtype": "Currency", + "label": "Post Haircut Amount", + "options": "Company:company:default_currency", + "read_only": 1 + } + ], + "istable": 1, + "modified": "2019-12-02 10:23:11.498308", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Proposed Pledge", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.py b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.py new file mode 100644 index 0000000000..dfa5c7965a --- /dev/null +++ b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class ProposedPledge(Document): + pass diff --git a/erpnext/loan_management/doctype/repayment_schedule/__init__.py b/erpnext/loan_management/doctype/repayment_schedule/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/doctype/repayment_schedule/repayment_schedule.json b/erpnext/loan_management/doctype/repayment_schedule/repayment_schedule.json similarity index 89% rename from erpnext/hr/doctype/repayment_schedule/repayment_schedule.json rename to erpnext/loan_management/doctype/repayment_schedule/repayment_schedule.json index 5bb2d370fa..7f71bebeba 100644 --- a/erpnext/hr/doctype/repayment_schedule/repayment_schedule.json +++ b/erpnext/loan_management/doctype/repayment_schedule/repayment_schedule.json @@ -1,5 +1,5 @@ { - "creation": "2016-12-20 15:32:25.078334", + "creation": "2019-09-12 12:57:07.940159", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -9,11 +9,10 @@ "interest_amount", "total_payment", "balance_loan_amount", - "paid" + "is_accrued" ], "fields": [ { - "allow_on_submit": 1, "columns": 2, "fieldname": "payment_date", "fieldtype": "Date", @@ -61,17 +60,17 @@ }, { "default": "0", - "fieldname": "paid", + "fieldname": "is_accrued", "fieldtype": "Check", "in_list_view": 1, - "label": "Paid", + "label": "Is Accrued", "read_only": 1 } ], "istable": 1, - "modified": "2019-10-29 11:45:10.694557", + "modified": "2019-09-12 12:57:07.940159", "modified_by": "Administrator", - "module": "HR", + "module": "Loan Management", "name": "Repayment Schedule", "owner": "Administrator", "permissions": [], diff --git a/erpnext/hr/doctype/repayment_schedule/repayment_schedule.py b/erpnext/loan_management/doctype/repayment_schedule/repayment_schedule.py similarity index 71% rename from erpnext/hr/doctype/repayment_schedule/repayment_schedule.py rename to erpnext/loan_management/doctype/repayment_schedule/repayment_schedule.py index 8abee5e089..2aa27b0968 100644 --- a/erpnext/hr/doctype/repayment_schedule/repayment_schedule.py +++ b/erpnext/loan_management/doctype/repayment_schedule/repayment_schedule.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt from __future__ import unicode_literals -import frappe +# import frappe from frappe.model.document import Document class RepaymentSchedule(Document): diff --git a/erpnext/loan_management/doctype/salary_slip_loan/__init__.py b/erpnext/loan_management/doctype/salary_slip_loan/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json new file mode 100644 index 0000000000..ce020fff07 --- /dev/null +++ b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json @@ -0,0 +1,96 @@ +{ + "creation": "2019-08-29 18:11:36.829526", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan", + "loan_type", + "loan_account", + "interest_income_account", + "column_break_4", + "principal_amount", + "interest_amount", + "total_payment", + "loan_repayment_entry" + ], + "fields": [ + { + "fieldname": "loan", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Loan", + "options": "Loan", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "loan_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Loan Account", + "options": "Account", + "read_only": 1 + }, + { + "fieldname": "interest_income_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Interest Income Account", + "options": "Account", + "read_only": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "principal_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Principal Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "interest_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Interest Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "total_payment", + "fieldtype": "Currency", + "label": "Total Payment", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "loan_repayment_entry", + "fieldtype": "Link", + "label": "Loan Repayment Entry", + "options": "Loan Repayment", + "read_only": 1 + }, + { + "fetch_from": "loan.loan_type", + "fieldname": "loan_type", + "fieldtype": "Link", + "label": "Loan Type", + "options": "Loan Type" + } + ], + "istable": 1, + "modified": "2019-10-28 09:15:31.174244", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Salary Slip Loan", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/salary_slip_loan/salary_slip_loan.py b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.py similarity index 71% rename from erpnext/hr/doctype/salary_slip_loan/salary_slip_loan.py rename to erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.py index 83908ce627..9ee0b96dc1 100644 --- a/erpnext/hr/doctype/salary_slip_loan/salary_slip_loan.py +++ b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt from __future__ import unicode_literals -import frappe +# import frappe from frappe.model.document import Document class SalarySlipLoan(Document): diff --git a/erpnext/loan_management/doctype/sanctioned_loan_amount/__init__.py b/erpnext/loan_management/doctype/sanctioned_loan_amount/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.js b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.js new file mode 100644 index 0000000000..5361e7ca2a --- /dev/null +++ b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Sanctioned Loan Amount', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.json b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.json new file mode 100644 index 0000000000..0447cd911c --- /dev/null +++ b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.json @@ -0,0 +1,88 @@ +{ + "actions": [], + "autoname": "LM-SLA-.####", + "creation": "2019-11-23 10:19:06.179736", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "applicant_type", + "applicant", + "column_break_3", + "company", + "sanctioned_amount_limit" + ], + "fields": [ + { + "fieldname": "applicant_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Applicant Type", + "options": "Employee\nMember\nCustomer", + "reqd": 1 + }, + { + "fieldname": "applicant", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Applicant", + "options": "applicant_type", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "sanctioned_amount_limit", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Sanctioned Amount Limit", + "options": "Company:company:default_currency", + "reqd": 1 + } + ], + "links": [], + "modified": "2020-02-25 05:10:52.421193", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Sanctioned Loan Amount", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py new file mode 100644 index 0000000000..74a131015b --- /dev/null +++ b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class SanctionedLoanAmount(Document): + def validate(self): + sanctioned_doc = frappe.db.exists('Sanctioned Loan Amount', {'applicant': self.applicant, 'company': self.company}) + + if sanctioned_doc and sanctioned_doc != self.name: + frappe.throw(_("Sanctioned Loan Amount already exists for {0} against company {1}").format( + frappe.bold(self.applicant), frappe.bold(self.company) + )) diff --git a/erpnext/loan_management/doctype/sanctioned_loan_amount/test_sanctioned_loan_amount.py b/erpnext/loan_management/doctype/sanctioned_loan_amount/test_sanctioned_loan_amount.py new file mode 100644 index 0000000000..ba1372f175 --- /dev/null +++ b/erpnext/loan_management/doctype/sanctioned_loan_amount/test_sanctioned_loan_amount.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestSanctionedLoanAmount(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/unpledge/__init__.py b/erpnext/loan_management/doctype/unpledge/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/unpledge/unpledge.json b/erpnext/loan_management/doctype/unpledge/unpledge.json new file mode 100644 index 0000000000..9e6277d5f8 --- /dev/null +++ b/erpnext/loan_management/doctype/unpledge/unpledge.json @@ -0,0 +1,84 @@ +{ + "creation": "2019-09-21 13:22:19.793797", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan_security", + "against_pledge", + "loan_security_type", + "loan_security_code", + "haircut", + "uom", + "column_break_5", + "qty" + ], + "fields": [ + { + "fieldname": "loan_security", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Loan Security", + "options": "Loan Security", + "reqd": 1 + }, + { + "fetch_from": "loan_security.loan_security_type", + "fieldname": "loan_security_type", + "fieldtype": "Link", + "label": "Loan Security Type", + "options": "Loan Security Type", + "read_only": 1 + }, + { + "fetch_from": "loan_security.loan_security_code", + "fieldname": "loan_security_code", + "fieldtype": "Data", + "label": "Loan Security Code" + }, + { + "fetch_from": "loan_security.unit_of_measure", + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM" + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Quantity", + "reqd": 1 + }, + { + "fieldname": "against_pledge", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Against Pledge", + "options": "Loan Security Pledge", + "reqd": 1 + }, + { + "fetch_from": "loan_security.haircut", + "fieldname": "haircut", + "fieldtype": "Percent", + "label": "Haircut", + "read_only": 1 + } + ], + "istable": 1, + "modified": "2019-10-02 12:48:18.588236", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Unpledge", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/unpledge/unpledge.py b/erpnext/loan_management/doctype/unpledge/unpledge.py new file mode 100644 index 0000000000..205230a308 --- /dev/null +++ b/erpnext/loan_management/doctype/unpledge/unpledge.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class Unpledge(Document): + pass diff --git a/erpnext/hr/loan_common.js b/erpnext/loan_management/loan_common.js similarity index 56% rename from erpnext/hr/loan_common.js rename to erpnext/loan_management/loan_common.js index 3e754fafab..3a47a88cbe 100644 --- a/erpnext/hr/loan_common.js +++ b/erpnext/loan_management/loan_common.js @@ -4,15 +4,28 @@ frappe.ui.form.on(cur_frm.doctype, { refresh: function(frm) { if (!frappe.boot.active_domains.includes("Non Profit")) { - frm.set_df_property('applicant_type', 'options', ['Employee']); + frm.set_df_property('applicant_type', 'options', ['Employee', 'Customer']); frm.refresh_field('applicant_type'); } - }, - applicant_type: function(frm) { - frm.set_value("applicant", null); - frm.set_value("applicant_name", null); + + if (['Loan Disbursement', 'Loan Repayment', 'Loan Interest Accrual'].includes(frm.doc.doctype) + && frm.doc.docstatus == 1) { + + frm.add_custom_button(__("Accounting Ledger"), function() { + frappe.route_options = { + voucher_no: frm.doc.name, + company: frm.doc.company + }; + + frappe.set_route("query-report", "General Ledger"); + },__("View")); + } }, applicant: function(frm) { + if (!["Loan Application", "Loan"].includes(frm.doc.doctype)) { + return; + } + if (frm.doc.applicant) { frappe.model.with_doc(frm.doc.applicant_type, frm.doc.applicant, function() { var applicant = frappe.model.get_doc(frm.doc.applicant_type, frm.doc.applicant); diff --git a/erpnext/loan_management/report/__init__.py b/erpnext/loan_management/report/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/__init__.py b/erpnext/loan_management/report/loan_repayment_and_closure/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.js b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.js new file mode 100644 index 0000000000..ed5e937c99 --- /dev/null +++ b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.js @@ -0,0 +1,41 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Loan Repayment and Closure"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_user_default("Company") + }, + { + "fieldname":"applicant_type", + "label": __("Applicant Type"), + "fieldtype": "Select", + "options": ["Customer", "Employee"], + "reqd": 1, + "default": "Customer", + on_change: function() { + frappe.query_report.set_filter_value('applicant', ""); + } + }, + { + "fieldname": "applicant", + "label": __("Applicant"), + "fieldtype": "Dynamic Link", + "get_options": function() { + var applicant_type = frappe.query_report.get_filter_value('applicant_type'); + var applicant = frappe.query_report.get_filter_value('applicant'); + if(applicant && !applicant_type) { + frappe.throw(__("Please select Applicant Type first")); + } + return applicant_type; + } + + }, + ] +}; diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.json b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.json new file mode 100644 index 0000000000..52d5b2c71a --- /dev/null +++ b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.json @@ -0,0 +1,27 @@ +{ + "add_total_row": 0, + "creation": "2019-09-03 16:54:55.616593", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-02-25 07:16:47.696994", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Repayment and Closure", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Loan Repayment", + "report_name": "Loan Repayment and Closure", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Loan Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py new file mode 100644 index 0000000000..b63cc8ed5a --- /dev/null +++ b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py @@ -0,0 +1,129 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +def execute(filters=None): + columns = get_columns() + data = get_data(filters) + return columns, data + +def get_columns(): + return [ + { + "label": _("Posting Date"), + "fieldtype": "Date", + "fieldname": "posting_date", + "width": 100 + }, + { + "label": _("Loan Repayment"), + "fieldtype": "Link", + "fieldname": "loan_repayment", + "options": "Loan Repayment", + "width": 100 + }, + { + "label": _("Against Loan"), + "fieldtype": "Link", + "fieldname": "against_loan", + "options": "Loan", + "width": 200 + }, + { + "label": _("Applicant"), + "fieldtype": "Data", + "fieldname": "applicant", + "width": 150 + }, + { + "label": _("Payment Type"), + "fieldtype": "Data", + "fieldname": "payment_type", + "width": 150 + }, + { + "label": _("Principal Amount"), + "fieldtype": "Currency", + "fieldname": "principal_amount", + "options": "currency", + "width": 100 + }, + { + "label": _("Interest Amount"), + "fieldtype": "Currency", + "fieldname": "interest", + "options": "currency", + "width": 100 + }, + { + "label": _("Penalty Amount"), + "fieldtype": "Currency", + "fieldname": "penalty", + "options": "currency", + "width": 100 + }, + { + "label": _("Payable Amount"), + "fieldtype": "Currency", + "fieldname": "payable_amount", + "options": "currency", + "width": 100 + }, + { + "label": _("Paid Amount"), + "fieldtype": "Currency", + "fieldname": "paid_amount", + "options": "currency", + "width": 100 + }, + { + "label": _("Currency"), + "fieldtype": "Link", + "fieldname": "currency", + "options": "Currency", + "width": 100 + } + ] + +def get_data(filters): + data = [] + + query_filters = { + "docstatus": 1, + "company": filters.get('company'), + } + + if filters.get('applicant'): + query_filters.update({ + "applicant": filters.get('applicant') + }) + + loan_repayments = frappe.get_all("Loan Repayment", + filters = query_filters, + fields=["posting_date", "applicant", "name", "against_loan", "payment_type", "payable_amount", + "pending_principal_amount", "interest_payable", "penalty_amount", "amount_paid"] + ) + + default_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") + + for repayment in loan_repayments: + row = { + "posting_date": repayment.posting_date, + "loan_repayment": repayment.name, + "applicant": repayment.applicant, + "payment_type": repayment.payment_type, + "against_loan": repayment.against_loan, + "principal_amount": repayment.pending_principal_amount, + "interest": repayment.interest_payable, + "penalty": repayment.penalty_amount, + "payable_amount": repayment.payable_amount, + "paid_amount": repayment.amount_paid, + "currency": default_currency + } + + data.append(row) + + return data \ No newline at end of file diff --git a/erpnext/loan_management/report/loan_security_status/__init__.py b/erpnext/loan_management/report/loan_security_status/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/report/loan_security_status/loan_security_status.js b/erpnext/loan_management/report/loan_security_status/loan_security_status.js new file mode 100644 index 0000000000..6e6191c7e4 --- /dev/null +++ b/erpnext/loan_management/report/loan_security_status/loan_security_status.js @@ -0,0 +1,46 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Loan Security Status"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_user_default("Company") + }, + { + "fieldname":"applicant_type", + "label": __("Applicant Type"), + "fieldtype": "Select", + "options": ["Customer", "Employee"], + "reqd": 1, + "default": "Customer", + on_change: function() { + frappe.query_report.set_filter_value('applicant', ""); + } + }, + { + "fieldname": "applicant", + "label": __("Applicant"), + "fieldtype": "Dynamic Link", + "get_options": function() { + var applicant_type = frappe.query_report.get_filter_value('applicant_type'); + var applicant = frappe.query_report.get_filter_value('applicant'); + if(applicant && !applicant_type) { + frappe.throw(__("Please select Applicant Type first")); + } + return applicant_type; + } + }, + { + "fieldname":"pledge_status", + "label": __("Pledge Status"), + "fieldtype": "Select", + "options": ["", "Requested", "Pledged", "Partially Pledged", "Unpledged"], + }, + ] +}; diff --git a/erpnext/loan_management/report/loan_security_status/loan_security_status.json b/erpnext/loan_management/report/loan_security_status/loan_security_status.json new file mode 100644 index 0000000000..9eb948dec8 --- /dev/null +++ b/erpnext/loan_management/report/loan_security_status/loan_security_status.json @@ -0,0 +1,27 @@ +{ + "add_total_row": 1, + "creation": "2019-10-07 05:57:33.435705", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2019-10-07 13:45:46.793949", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Security Status", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Loan Security", + "report_name": "Loan Security Status", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Loan Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/loan_management/report/loan_security_status/loan_security_status.py b/erpnext/loan_management/report/loan_security_status/loan_security_status.py new file mode 100644 index 0000000000..ea6a2ee645 --- /dev/null +++ b/erpnext/loan_management/report/loan_security_status/loan_security_status.py @@ -0,0 +1,135 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +def execute(filters=None): + columns = get_columns(filters) + data = get_data(filters) + return columns, data + +def get_columns(filters): + columns= [ + { + "label": _("Loan Security Pledge"), + "fieldtype": "Link", + "fieldname": "loan_security_pledge", + "options": "Loan Security Pledge", + "width": 200 + }, + { + "label": _("Loan"), + "fieldtype": "Link", + "fieldname": "loan", + "options": "Loan", + "width": 200 + }, + { + "label": _("Applicant"), + "fieldtype": "Data", + "fieldname": "applicant", + "width": 200 + }, + { + "label": _("Status"), + "fieldtype": "Data", + "fieldname": "status", + "width": 100 + }, + { + "label": _("Pledge Time"), + "fieldtype": "Data", + "fieldname": "pledge_time", + "width": 150 + }, + { + "label": _("Loan Security"), + "fieldtype": "Link", + "fieldname": "loan_security", + "options": "Loan Security", + "width": 150 + }, + { + "label": _("Quantity"), + "fieldtype": "Float", + "fieldname": "qty", + "width": 100 + }, + { + "label": _("Loan Security Price"), + "fieldtype": "Currency", + "fieldname": "loan_security_price", + "options": "currency", + "width": 200 + }, + { + "label": _("Loan Security Value"), + "fieldtype": "Currency", + "fieldname": "loan_security_value", + "options": "currency", + "width": 200 + }, + { + "label": _("Currency"), + "fieldtype": "Link", + "fieldname": "currency", + "options": "Currency", + "width": 50 + } + ] + + return columns + +def get_data(filters): + + loan_security_price_map = frappe._dict(frappe.get_all("Loan Security", + fields=["name", "loan_security_price"], as_list=1 + )) + + data = [] + conditions = get_conditions(filters) + + loan_security_pledges = frappe.db.sql(""" + SELECT + p.name, p.applicant, p.loan, p.status, p.pledge_time, + c.loan_security, c.qty + FROM + `tabLoan Security Pledge` p, `tabPledge` c + WHERE + p.docstatus = 1 + AND c.parent = p.name + AND p.company = %(company)s + {conditions} + """.format(conditions = conditions), (filters), as_dict=1) #nosec + + default_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") + + for pledge in loan_security_pledges: + row = {} + row["loan_security_pledge"] = pledge.name + row["loan"] = pledge.loan + row["applicant"] = pledge.applicant + row["status"] = pledge.status + row["pledge_time"] = pledge.pledge_time + row["loan_security"] = pledge.loan_security + row["qty"] = pledge.qty + row["loan_security_price"] = loan_security_price_map.get(pledge.loan_security) + row["loan_security_value"] = row["loan_security_price"] * pledge.qty + row["currency"] = default_currency + + data.append(row) + + return data + +def get_conditions(filters): + conditions = [] + + if filters.get("applicant"): + conditions.append("p.applicant = %(applicant)s") + + if filters.get("pledge_status"): + conditions.append(" p.status = %(pledge_status)s") + + return "AND {}".format(" AND ".join(conditions)) if conditions else "" diff --git a/erpnext/modules.txt b/erpnext/modules.txt index 316d6de20e..3b347582c3 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -23,4 +23,5 @@ Non Profit Hotels Hub Node Quality Management -Communication \ No newline at end of file +Communication +Loan Management \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 42d537a73c..b0fc7ea59c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -510,7 +510,6 @@ erpnext.patches.v10_0.repost_requested_qty_for_non_stock_uom_items erpnext.patches.v11_0.merge_land_unit_with_location erpnext.patches.v11_0.add_index_on_nestedset_doctypes erpnext.patches.v11_0.remove_modules_setup_page -erpnext.patches.v11_0.rename_employee_loan_to_loan erpnext.patches.v11_0.move_leave_approvers_from_employee #13-06-2018 erpnext.patches.v11_0.update_department_lft_rgt erpnext.patches.v11_0.add_default_email_template_for_leave @@ -634,6 +633,7 @@ execute:frappe.reload_doc('desk', 'doctype','dashboard_chart') erpnext.patches.v12_0.add_default_dashboards erpnext.patches.v12_0.remove_bank_remittance_custom_fields erpnext.patches.v12_0.generate_leave_ledger_entries +execute:frappe.delete_doc_if_exists("Report", "Loan Repayment") erpnext.patches.v12_0.move_credit_limit_to_customer_credit_limit erpnext.patches.v12_0.add_variant_of_in_item_attribute_table erpnext.patches.v12_0.rename_bank_account_field_in_journal_entry_account diff --git a/erpnext/patches/v11_0/rename_employee_loan_to_loan.py b/erpnext/patches/v11_0/rename_employee_loan_to_loan.py deleted file mode 100644 index b2ff6b8c5d..0000000000 --- a/erpnext/patches/v11_0/rename_employee_loan_to_loan.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import unicode_literals -import frappe -from frappe.model.utils.rename_field import rename_field - -def execute(): - if frappe.db.table_exists("Employee Loan Application") and not frappe.db.table_exists("Loan Application"): - frappe.rename_doc("DocType", "Employee Loan Application", "Loan Application", force=True) - - if frappe.db.table_exists("Employee Loan") and not frappe.db.table_exists("Loan"): - frappe.rename_doc("DocType", "Employee Loan", "Loan", force=True) - - frappe.reload_doc("hr", "doctype", "loan_application") - frappe.reload_doc("hr", "doctype", "loan") - frappe.reload_doc("hr", "doctype", "salary_slip_loan") - - for doctype in ['Loan', 'Salary Slip Loan']: - if frappe.db.has_column(doctype, 'employee_loan_account'): - rename_field(doctype, "employee_loan_account", "loan_account") - - columns = {'employee': 'applicant', 'employee_name': 'applicant_name'} - for doctype in ['Loan Application', 'Loan']: - frappe.db.sql(""" update `tab{doctype}` set applicant_type = 'Employee' """ - .format(doctype=doctype)) - for column, new_column in columns.items(): - if frappe.db.has_column(doctype, column): - rename_field(doctype, column, new_column) - - frappe.delete_doc('DocType', doctype) \ No newline at end of file