wip: refactored accounting and added charge applied event

This commit is contained in:
Vishwas Babu AJ
2013-03-17 21:25:09 +05:30
parent 759b832360
commit b7e1ff3fa8
10 changed files with 372 additions and 281 deletions
@@ -85,34 +85,103 @@ public class AccountingProcessorHelper {
newLoanTransactions);
}
public GLAccount getLinkedCashAccountsForLoanProduct(final Long loanProductId, final CASH_ACCOUNTS_FOR_LOAN finAccountType) {
final ProductToGLAccountMapping accountMapping = this.accountMappingRepository
.findByProductIdAndProductTypeAndFinancialAccountType(loanProductId, PortfolioProductType.LOAN.getValue(),
finAccountType.getValue());
return accountMapping.getGlAccount();
/**
* Convenience method that creates a pair of related Debits and Credits for
* Accrual Based accounting.
*
* The target accounts for debits and credits are switched in case of a
* reversal
*
* @param office
* @param accountTypeToBeDebited
* Enum of the placeholder GLAccount to be debited
* @param accountTypeToBeCredited
* Enum of the placeholder of the GLAccount to be credited
* @param loanProductId
* @param loanId
* @param transactionId
* @param transactionDate
* @param amount
* @param isReversal
*/
public void createAccrualBasedJournalEntriesAndReversalsForLoan(final Office office,
final ACCRUAL_ACCOUNTS_FOR_LOAN accountTypeToBeDebited, final ACCRUAL_ACCOUNTS_FOR_LOAN accountTypeToBeCredited,
final Long loanProductId, final Long loanId, final String transactionId, final Date transactionDate, final BigDecimal amount,
final Boolean isReversal) {
int accountTypeToDebitId = accountTypeToBeDebited.getValue();
int accountTypeToCreditId = accountTypeToBeCredited.getValue();
// reverse debits and credits for reversals
if (isReversal) {
accountTypeToDebitId = accountTypeToBeCredited.getValue();
accountTypeToCreditId = accountTypeToBeDebited.getValue();
}
createJournalEntriesForLoan(office, accountTypeToDebitId, accountTypeToCreditId, loanProductId, loanId, transactionId,
transactionDate, amount);
}
public GLAccount getLinkedAccrualAccountsForLoanProduct(final Long loanProductId, final ACCRUAL_ACCOUNTS_FOR_LOAN finAccountType) {
final ProductToGLAccountMapping accountMapping = this.accountMappingRepository
.findByProductIdAndProductTypeAndFinancialAccountType(loanProductId, PortfolioProductType.LOAN.getValue(),
finAccountType.getValue());
return accountMapping.getGlAccount();
/**
* Convenience method that creates a pair of related Debits and Credits for
* Cash Based accounting.
*
* The target accounts for debits and credits are switched in case of a
* reversal
*
* @param office
* @param accountTypeToBeDebited
* Enum of the placeholder GLAccount to be debited
* @param accountTypeToBeCredited
* Enum of the placeholder of the GLAccount to be credited
* @param loanProductId
* @param loanId
* @param transactionId
* @param transactionDate
* @param amount
* @param isReversal
*/
public void createCashBasedJournalEntriesAndReversalsForLoan(final Office office, final CASH_ACCOUNTS_FOR_LOAN accountTypeToBeDebited,
final CASH_ACCOUNTS_FOR_LOAN accountTypeToBeCredited, final Long loanProductId, final Long loanId, final String transactionId,
final Date transactionDate, final BigDecimal amount, final Boolean isReversal) {
int accountTypeToDebitId = accountTypeToBeDebited.getValue();
int accountTypeToCreditId = accountTypeToBeCredited.getValue();
// reverse debits and credits for reversals
if (isReversal) {
accountTypeToDebitId = accountTypeToBeCredited.getValue();
accountTypeToCreditId = accountTypeToBeDebited.getValue();
}
createJournalEntriesForLoan(office, accountTypeToDebitId, accountTypeToCreditId, loanProductId, loanId, transactionId,
transactionDate, amount);
}
public void createCreditJournalEntryForLoanProduct(final Office office, final GLAccount account, final Long loanId,
final String transactionId, final Date transactionDate, final BigDecimal amount) {
final boolean manualEntry = false;
final JournalEntry journalEntry = JournalEntry.createNew(office, account, transactionId, manualEntry, transactionDate,
JournalEntryType.CREDIT, amount, null, PortfolioProductType.LOAN.getValue(), loanId);
this.glJournalEntryRepository.saveAndFlush(journalEntry);
public void createCreditJournalEntryOrReversalForLoan(final Office office, final CASH_ACCOUNTS_FOR_LOAN accountMappingType,
final Long loanProductId, final Long loanId, final String transactionId, final Date transactionDate, final BigDecimal amount,
final Boolean isReversal) {
int accountMappingTypeId = accountMappingType.getValue();
createCreditJournalEntryOrReversalForLoan(office, accountMappingTypeId, loanProductId, loanId, transactionId, transactionDate,
amount, isReversal);
}
public void createDebitJournalEntryForLoanProduct(final Office office, final GLAccount account, final Long loanId,
final String transactionId, final Date transactionDate, final BigDecimal amount) {
final boolean manualEntry = false;
final JournalEntry journalEntry = JournalEntry.createNew(office, account, transactionId, manualEntry, transactionDate,
JournalEntryType.DEBIT, amount, null, PortfolioProductType.LOAN.getValue(), loanId);
this.glJournalEntryRepository.saveAndFlush(journalEntry);
public void createCreditJournalEntryOrReversalForLoan(final Office office, final ACCRUAL_ACCOUNTS_FOR_LOAN accountMappingType,
final Long loanProductId, final Long loanId, final String transactionId, final Date transactionDate, final BigDecimal amount,
final Boolean isReversal) {
int accountMappingTypeId = accountMappingType.getValue();
createCreditJournalEntryOrReversalForLoan(office, accountMappingTypeId, loanProductId, loanId, transactionId, transactionDate,
amount, isReversal);
}
public void createDebitJournalEntryOrReversalForLoan(final Office office, final CASH_ACCOUNTS_FOR_LOAN accountMappingType,
final Long loanProductId, final Long loanId, final String transactionId, final Date transactionDate, final BigDecimal amount,
final Boolean isReversal) {
int accountMappingTypeId = accountMappingType.getValue();
createDebitJournalEntryOrReversalForLoan(office, accountMappingTypeId, loanProductId, loanId, transactionId, transactionDate,
amount, isReversal);
}
public void createDebitJournalEntryOrReversalForLoan(final Office office, final ACCRUAL_ACCOUNTS_FOR_LOAN accountMappingType,
final Long loanProductId, final Long loanId, final String transactionId, final Date transactionDate, final BigDecimal amount,
final Boolean isReversal) {
int accountMappingTypeId = accountMappingType.getValue();
createDebitJournalEntryOrReversalForLoan(office, accountMappingTypeId, loanProductId, loanId, transactionId, transactionDate,
amount, isReversal);
}
/**
@@ -138,4 +207,55 @@ public class AccountingProcessorHelper {
return this.officeRepository.findOne(officeId);
}
private void createJournalEntriesForLoan(final Office office, final int accountTypeToDebitId, final int accountTypeToCreditId,
final Long loanProductId, final Long loanId, final String transactionId, final Date transactionDate, final BigDecimal amount) {
GLAccount debitAccount = getLinkedGLAccountForLoanProduct(loanProductId, accountTypeToDebitId);
GLAccount creditAccount = getLinkedGLAccountForLoanProduct(loanProductId, accountTypeToCreditId);
createCreditJournalEntryForLoan(office, debitAccount, loanId, transactionId, transactionDate, amount);
createDebitJournalEntryForLoan(office, creditAccount, loanId, transactionId, transactionDate, amount);
}
private void createDebitJournalEntryOrReversalForLoan(final Office office, final int accountMappingTypeId, final Long loanProductId,
final Long loanId, final String transactionId, final Date transactionDate, final BigDecimal amount, final Boolean isReversal) {
GLAccount account = getLinkedGLAccountForLoanProduct(loanProductId, accountMappingTypeId);
if (isReversal) {
createCreditJournalEntryForLoan(office, account, loanId, transactionId, transactionDate, amount);
} else {
createDebitJournalEntryForLoan(office, account, loanId, transactionId, transactionDate, amount);
}
}
private void createCreditJournalEntryOrReversalForLoan(final Office office, final int accountMappingTypeId, final Long loanProductId,
final Long loanId, final String transactionId, final Date transactionDate, final BigDecimal amount, final Boolean isReversal) {
GLAccount account = getLinkedGLAccountForLoanProduct(loanProductId, accountMappingTypeId);
if (isReversal) {
createDebitJournalEntryForLoan(office, account, loanId, transactionId, transactionDate, amount);
} else {
createCreditJournalEntryForLoan(office, account, loanId, transactionId, transactionDate, amount);
}
}
private void createCreditJournalEntryForLoan(final Office office, final GLAccount account, final Long loanId,
final String transactionId, final Date transactionDate, final BigDecimal amount) {
final boolean manualEntry = false;
final JournalEntry journalEntry = JournalEntry.createNew(office, account, transactionId, manualEntry, transactionDate,
JournalEntryType.CREDIT, amount, null, PortfolioProductType.LOAN.getValue(), loanId);
this.glJournalEntryRepository.saveAndFlush(journalEntry);
}
private void createDebitJournalEntryForLoan(final Office office, final GLAccount account, final Long loanId,
final String transactionId, final Date transactionDate, final BigDecimal amount) {
final boolean manualEntry = false;
final JournalEntry journalEntry = JournalEntry.createNew(office, account, transactionId, manualEntry, transactionDate,
JournalEntryType.DEBIT, amount, null, PortfolioProductType.LOAN.getValue(), loanId);
this.glJournalEntryRepository.saveAndFlush(journalEntry);
}
private GLAccount getLinkedGLAccountForLoanProduct(final Long loanProductId, final int accountMappingTypeId) {
final ProductToGLAccountMapping accountMapping = this.accountMappingRepository
.findByProductIdAndProductTypeAndFinancialAccountType(loanProductId, PortfolioProductType.LOAN.getValue(),
accountMappingTypeId);
return accountMapping.getGlAccount();
}
}
@@ -5,7 +5,6 @@ import java.util.Date;
import org.mifosplatform.accounting.closure.domain.GLClosure;
import org.mifosplatform.accounting.common.AccountingConstants.ACCRUAL_ACCOUNTS_FOR_LOAN;
import org.mifosplatform.accounting.glaccount.domain.GLAccount;
import org.mifosplatform.accounting.journalentry.data.LoanDTO;
import org.mifosplatform.accounting.journalentry.data.LoanTransactionDTO;
import org.mifosplatform.organisation.office.domain.Office;
@@ -36,7 +35,9 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
}
/*** Handle Apply charges ***/
// TODO
else if (loanTransactionDTO.getTransactionType().isApplyCharges()) {
createJournalEntriesForApplyCharges(loanDTO, loanTransactionDTO, office);
}
/***
* Handle repayments, repayments at disbursement and reversal of
@@ -44,13 +45,13 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
***/
else if (loanTransactionDTO.getTransactionType().isRepayment()
|| loanTransactionDTO.getTransactionType().isRepaymentAtDisbursement()) {
createJournalEntriesForRepayments(loanDTO, loanTransactionDTO, office);
createJournalEntriesForRepaymentsAndWriteOffs(loanDTO, loanTransactionDTO, office, false);
}
/** Handle Write Offs and waivers **/
/** Handle Write Offs, waivers and thier reversals **/
else if ((loanTransactionDTO.getTransactionType().isWriteOff() || loanTransactionDTO.getTransactionType().isWaiveInterest() || loanTransactionDTO
.getTransactionType().isWaiveCharges())) {
createJournalEntriesForWriteOff(loanDTO, loanTransactionDTO, office);
createJournalEntriesForRepaymentsAndWriteOffs(loanDTO, loanTransactionDTO, office, true);
}
}
}
@@ -79,136 +80,38 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
// create journal entries for the disbursement (or disbursement
// reversal)
final GLAccount loanPortfolioAccount = helper.getLinkedAccrualAccountsForLoanProduct(loanProductId,
ACCRUAL_ACCOUNTS_FOR_LOAN.LOAN_PORTFOLIO);
final GLAccount fundSourceAccount = helper.getLinkedAccrualAccountsForLoanProduct(loanProductId,
ACCRUAL_ACCOUNTS_FOR_LOAN.FUND_SOURCE);
if (isReversed) {
helper.createCreditJournalEntryForLoanProduct(office, loanPortfolioAccount, loanId, transactionId, transactionDate,
disbursalAmount);
helper.createDebitJournalEntryForLoanProduct(office, fundSourceAccount, loanId, transactionId, transactionDate, disbursalAmount);
} else {
helper.createDebitJournalEntryForLoanProduct(office, loanPortfolioAccount, loanId, transactionId, transactionDate,
disbursalAmount);
helper.createCreditJournalEntryForLoanProduct(office, fundSourceAccount, loanId, transactionId, transactionDate,
disbursalAmount);
}
helper.createAccrualBasedJournalEntriesAndReversalsForLoan(office, ACCRUAL_ACCOUNTS_FOR_LOAN.LOAN_PORTFOLIO,
ACCRUAL_ACCOUNTS_FOR_LOAN.FUND_SOURCE, loanProductId, loanId, transactionId, transactionDate, disbursalAmount, isReversed);
// create journal entries for recognising interest (or reversal)
BigDecimal interestApplied = loanDTO.getCalculatedInterest();
if (interestApplied != null && !(interestApplied.compareTo(BigDecimal.ZERO) == 0)) {
final GLAccount interestReceivableAccount = helper.getLinkedAccrualAccountsForLoanProduct(loanProductId,
ACCRUAL_ACCOUNTS_FOR_LOAN.INTEREST_RECEIVABLE);
final GLAccount interestOnLoansAccount = helper.getLinkedAccrualAccountsForLoanProduct(loanProductId,
ACCRUAL_ACCOUNTS_FOR_LOAN.INTEREST_ON_LOANS);
if (isReversed) {
helper.createDebitJournalEntryForLoanProduct(office, interestOnLoansAccount, loanId, transactionId, transactionDate,
interestApplied);
helper.createCreditJournalEntryForLoanProduct(office, interestReceivableAccount, loanId, transactionId, transactionDate,
interestApplied);
} else {
helper.createDebitJournalEntryForLoanProduct(office, interestReceivableAccount, loanId, transactionId, transactionDate,
interestApplied);
helper.createCreditJournalEntryForLoanProduct(office, interestOnLoansAccount, loanId, transactionId, transactionDate,
interestApplied);
}
helper.createAccrualBasedJournalEntriesAndReversalsForLoan(office, ACCRUAL_ACCOUNTS_FOR_LOAN.INTEREST_RECEIVABLE,
ACCRUAL_ACCOUNTS_FOR_LOAN.INTEREST_ON_LOANS, loanProductId, loanId, transactionId, transactionDate, disbursalAmount,
isReversed);
}
}
/**
* Create a single Debit for fund source and multiple credits if applicable
* (loan portfolio for principal repayments, Interest receivable for
* interest repayments, Fee receivable for fees payment and Penalty
* Receivable from penalties for penalty payment)
*
* Handles repayments using the following posting rules <br/>
* <br/>
* <br/>
*
* In case the loan transaction has been reversed, all debits are turned
* into credits and vice versa
*/
private void createJournalEntriesForRepayments(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, final Office office) {
// loan properties
final Long loanProductId = loanDTO.getLoanProductId();
final Long loanId = loanDTO.getLoanId();
// transaction properties
final String transactionId = loanTransactionDTO.getTransactionId();
final Date transactionDate = loanTransactionDTO.getTransactionDate();
final BigDecimal principalAmount = loanTransactionDTO.getPrincipal();
final BigDecimal interestAmount = loanTransactionDTO.getInterest();
final BigDecimal feesAmount = loanTransactionDTO.getFees();
final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties();
final boolean isReversed = loanTransactionDTO.isReversed();
BigDecimal totalDebitAmount = new BigDecimal(0);
if (principalAmount != null && !(principalAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(principalAmount);
final GLAccount loanPortfolioAccount = helper.getLinkedAccrualAccountsForLoanProduct(loanProductId,
ACCRUAL_ACCOUNTS_FOR_LOAN.LOAN_PORTFOLIO);
if (isReversed) {
helper.createDebitJournalEntryForLoanProduct(office, loanPortfolioAccount, loanId, transactionId, transactionDate,
principalAmount);
} else {
helper.createCreditJournalEntryForLoanProduct(office, loanPortfolioAccount, loanId, transactionId, transactionDate,
principalAmount);
}
}
if (interestAmount != null && !(interestAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(interestAmount);
final GLAccount interestAccount = helper.getLinkedAccrualAccountsForLoanProduct(loanProductId,
ACCRUAL_ACCOUNTS_FOR_LOAN.INTEREST_RECEIVABLE);
if (isReversed) {
helper.createDebitJournalEntryForLoanProduct(office, interestAccount, loanId, transactionId, transactionDate,
interestAmount);
} else {
helper.createCreditJournalEntryForLoanProduct(office, interestAccount, loanId, transactionId, transactionDate,
interestAmount);
}
}
if (feesAmount != null && !(feesAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(feesAmount);
final GLAccount incomeFromFeesAccount = helper.getLinkedAccrualAccountsForLoanProduct(loanProductId,
ACCRUAL_ACCOUNTS_FOR_LOAN.FEES_RECEIVABLE);
if (isReversed) {
helper.createDebitJournalEntryForLoanProduct(office, incomeFromFeesAccount, loanId, transactionId, transactionDate,
feesAmount);
} else {
helper.createCreditJournalEntryForLoanProduct(office, incomeFromFeesAccount, loanId, transactionId, transactionDate,
feesAmount);
}
}
if (penaltiesAmount != null && !(penaltiesAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(penaltiesAmount);
final GLAccount incomeFromPenaltiesAccount = helper.getLinkedAccrualAccountsForLoanProduct(loanProductId,
ACCRUAL_ACCOUNTS_FOR_LOAN.PENALTIES_RECEIVABLE);
if (isReversed) {
helper.createDebitJournalEntryForLoanProduct(office, incomeFromPenaltiesAccount, loanId, transactionId, transactionDate,
penaltiesAmount);
} else {
helper.createCreditJournalEntryForLoanProduct(office, incomeFromPenaltiesAccount, loanId, transactionId, transactionDate,
penaltiesAmount);
}
}
final GLAccount fundSourceAccount = helper.getLinkedAccrualAccountsForLoanProduct(loanProductId,
ACCRUAL_ACCOUNTS_FOR_LOAN.FUND_SOURCE);
if (isReversed) {
helper.createCreditJournalEntryForLoanProduct(office, fundSourceAccount, loanId, transactionId, transactionDate,
totalDebitAmount);
} else {
helper.createDebitJournalEntryForLoanProduct(office, fundSourceAccount, loanId, transactionId, transactionDate,
totalDebitAmount);
}
}
/**
* Handles write offs using the following posting rules <br/>
* <b>Principal Repayment</b>: Debits "Fund Source" and Credits
* "Loan Portfolio"<br/>
*
* <b>Interest Repayment</b>:Debits "Fund Source" and and Credits
* "Receivable Interest" <br/>
*
* <b>Fee Write Repayment</b>:Debits "Fund Source" and and Credits
* "Receivable Fees" <br/>
*
* <b>Penalty Repayment</b>: Debits "Fund Source" and and Credits
* "Receivable Penalties" <br/>
* <br/>
* Handles write offs using the following posting rules <br/>
* <br/>
* <b>Principal Write off</b>: Debits "Losses Written Off" and Credits
* "Loan Portfolio"<br/>
*
@@ -220,12 +123,18 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
*
* <b>Penalty Write off</b>: Debits "Losses Written off" and and Credits
* "Receivable Penalties" <br/>
* <br/>
* <br/>
* In case the loan transaction has been reversed, all debits are turned
* into credits and vice versa
*
* @param loanTransactionDTO
* @param loanDTO
* @param office
*
*/
private void createJournalEntriesForWriteOff(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, final Office office) {
private void createJournalEntriesForRepaymentsAndWriteOffs(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
final Office office, final boolean writeOff) {
// loan properties
final Long loanProductId = loanDTO.getLoanProductId();
final Long loanId = loanDTO.getLoanId();
@@ -237,71 +146,90 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
final BigDecimal interestAmount = loanTransactionDTO.getInterest();
final BigDecimal feesAmount = loanTransactionDTO.getFees();
final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties();
final boolean isReversed = loanTransactionDTO.isReversed();
final boolean isReversal = loanTransactionDTO.isReversed();
BigDecimal totalDebitAmount = new BigDecimal(0);
// handle principle payment or writeOff (and reversals)
if (principalAmount != null && !(principalAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(principalAmount);
final GLAccount loanPortfolioAccount = helper.getLinkedAccrualAccountsForLoanProduct(loanProductId,
ACCRUAL_ACCOUNTS_FOR_LOAN.LOAN_PORTFOLIO);
if (isReversed) {
helper.createDebitJournalEntryForLoanProduct(office, loanPortfolioAccount, loanId, transactionId, transactionDate,
principalAmount);
} else {
helper.createCreditJournalEntryForLoanProduct(office, loanPortfolioAccount, loanId, transactionId, transactionDate,
principalAmount);
}
helper.createCreditJournalEntryOrReversalForLoan(office, ACCRUAL_ACCOUNTS_FOR_LOAN.LOAN_PORTFOLIO, loanProductId, loanId,
transactionId, transactionDate, principalAmount, isReversal);
}
// handle interest payment of writeOff (and reversals)
if (interestAmount != null && !(interestAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(interestAmount);
final GLAccount interestAccount = helper.getLinkedAccrualAccountsForLoanProduct(loanProductId,
ACCRUAL_ACCOUNTS_FOR_LOAN.INTEREST_RECEIVABLE);
if (isReversed) {
helper.createDebitJournalEntryForLoanProduct(office, interestAccount, loanId, transactionId, transactionDate,
interestAmount);
} else {
helper.createCreditJournalEntryForLoanProduct(office, interestAccount, loanId, transactionId, transactionDate,
interestAmount);
}
helper.createCreditJournalEntryOrReversalForLoan(office, ACCRUAL_ACCOUNTS_FOR_LOAN.INTEREST_RECEIVABLE, loanProductId, loanId,
transactionId, transactionDate, interestAmount, isReversal);
}
// handle fees payment of writeOff (and reversals)
if (feesAmount != null && !(feesAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(feesAmount);
final GLAccount incomeFromFeesAccount = helper.getLinkedAccrualAccountsForLoanProduct(loanProductId,
ACCRUAL_ACCOUNTS_FOR_LOAN.FEES_RECEIVABLE);
if (isReversed) {
helper.createDebitJournalEntryForLoanProduct(office, incomeFromFeesAccount, loanId, transactionId, transactionDate,
feesAmount);
} else {
helper.createCreditJournalEntryForLoanProduct(office, incomeFromFeesAccount, loanId, transactionId, transactionDate,
feesAmount);
}
helper.createCreditJournalEntryOrReversalForLoan(office, ACCRUAL_ACCOUNTS_FOR_LOAN.FEES_RECEIVABLE, loanProductId, loanId,
transactionId, transactionDate, feesAmount, isReversal);
}
// handle penalties payment of writeOff (and reversals)
if (penaltiesAmount != null && !(penaltiesAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(penaltiesAmount);
final GLAccount incomeFromPenaltiesAccount = helper.getLinkedAccrualAccountsForLoanProduct(loanProductId,
ACCRUAL_ACCOUNTS_FOR_LOAN.PENALTIES_RECEIVABLE);
if (isReversed) {
helper.createDebitJournalEntryForLoanProduct(office, incomeFromPenaltiesAccount, loanId, transactionId, transactionDate,
penaltiesAmount);
} else {
helper.createCreditJournalEntryForLoanProduct(office, incomeFromPenaltiesAccount, loanId, transactionId, transactionDate,
penaltiesAmount);
}
helper.createCreditJournalEntryOrReversalForLoan(office, ACCRUAL_ACCOUNTS_FOR_LOAN.PENALTIES_RECEIVABLE, loanProductId, loanId,
transactionId, transactionDate, penaltiesAmount, isReversal);
}
final GLAccount fundSourceAccount = helper.getLinkedAccrualAccountsForLoanProduct(loanProductId,
ACCRUAL_ACCOUNTS_FOR_LOAN.LOSSES_WRITTEN_OFF);
if (isReversed) {
helper.createCreditJournalEntryForLoanProduct(office, fundSourceAccount, loanId, transactionId, transactionDate,
totalDebitAmount);
/**
* Single DEBIT transaction for write-offs or Repayments (and their
* reversals)
***/
if (writeOff) {
helper.createDebitJournalEntryOrReversalForLoan(office, ACCRUAL_ACCOUNTS_FOR_LOAN.LOSSES_WRITTEN_OFF, loanProductId, loanId,
transactionId, transactionDate, totalDebitAmount, isReversal);
} else {
helper.createDebitJournalEntryForLoanProduct(office, fundSourceAccount, loanId, transactionId, transactionDate,
totalDebitAmount);
helper.createDebitJournalEntryOrReversalForLoan(office, ACCRUAL_ACCOUNTS_FOR_LOAN.FUND_SOURCE, loanProductId, loanId,
transactionId, transactionDate, totalDebitAmount, isReversal);
}
}
/**
* Create Journal Entries for application of charges (fees and payments)
* using the following rules <br/>
*
* <b>Fees:</b> Debit <i>Fees Receivable</i> and credit <i>Income from
* Fees</i> <br/>
*
* <b>Penalties:</b> Debit <i>Penalties Receivable</i> and credit <i>Income
* from Penalties</i>
*
* Also handles reversals for both fees and payment applications
*
* @param loanDTO
* @param loanTransactionDTO
* @param office
*/
private void createJournalEntriesForApplyCharges(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, final Office office) {
// loan properties
final Long loanProductId = loanDTO.getLoanProductId();
final Long loanId = loanDTO.getLoanId();
// transaction properties
final String transactionId = loanTransactionDTO.getTransactionId();
final Date transactionDate = loanTransactionDTO.getTransactionDate();
final BigDecimal feesAmount = loanTransactionDTO.getFees();
final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties();
final boolean isReversed = loanTransactionDTO.isReversed();
// create journal entries for the fees application (or reversal)
if (feesAmount != null && !(feesAmount.compareTo(BigDecimal.ZERO) == 0)) {
helper.createAccrualBasedJournalEntriesAndReversalsForLoan(office, ACCRUAL_ACCOUNTS_FOR_LOAN.LOAN_PORTFOLIO,
ACCRUAL_ACCOUNTS_FOR_LOAN.FUND_SOURCE, loanProductId, loanId, transactionId, transactionDate, feesAmount, isReversed);
}
// create journal entries for the penalties application (or reversal)
else if (penaltiesAmount != null && !(penaltiesAmount.compareTo(BigDecimal.ZERO) == 0)) {
helper.createAccrualBasedJournalEntriesAndReversalsForLoan(office, ACCRUAL_ACCOUNTS_FOR_LOAN.PENALTIES_RECEIVABLE,
ACCRUAL_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES, loanProductId, loanId, transactionId, transactionDate, feesAmount,
isReversed);
}
}
@@ -5,7 +5,6 @@ import java.util.Date;
import org.mifosplatform.accounting.closure.domain.GLClosure;
import org.mifosplatform.accounting.common.AccountingConstants.CASH_ACCOUNTS_FOR_LOAN;
import org.mifosplatform.accounting.glaccount.domain.GLAccount;
import org.mifosplatform.accounting.journalentry.data.LoanDTO;
import org.mifosplatform.accounting.journalentry.data.LoanTransactionDTO;
import org.mifosplatform.organisation.office.domain.Office;
@@ -53,15 +52,9 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF
**/
else if (loanTransactionDTO.getTransactionType().isWriteOff() && !loanTransactionDTO.isReversed()) {
final BigDecimal principalAmount = loanTransactionDTO.getPrincipal();
final GLAccount lossesWrittenOffAccount = helper.getLinkedCashAccountsForLoanProduct(loanProductId,
CASH_ACCOUNTS_FOR_LOAN.LOSSES_WRITTEN_OFF);
helper.createDebitJournalEntryForLoanProduct(office, lossesWrittenOffAccount, loanId, transactionId, transactionDate,
principalAmount);
final GLAccount loanPortfolioAccount = helper.getLinkedCashAccountsForLoanProduct(loanProductId,
CASH_ACCOUNTS_FOR_LOAN.LOAN_PORTFOLIO);
helper.createCreditJournalEntryForLoanProduct(office, loanPortfolioAccount, loanId, transactionId, transactionDate,
principalAmount);
helper.createCashBasedJournalEntriesAndReversalsForLoan(office, CASH_ACCOUNTS_FOR_LOAN.LOSSES_WRITTEN_OFF,
CASH_ACCOUNTS_FOR_LOAN.LOAN_PORTFOLIO, loanProductId, loanId, transactionId, transactionDate, principalAmount,
false);
}
}
}
@@ -87,22 +80,10 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF
final String transactionId = loanTransactionDTO.getTransactionId();
final Date transactionDate = loanTransactionDTO.getTransactionDate();
final BigDecimal disbursalAmount = loanTransactionDTO.getAmount();
final boolean isReversed = loanTransactionDTO.isReversed();
final boolean isReversal = loanTransactionDTO.isReversed();
final GLAccount loanPortfolioAccount = helper.getLinkedCashAccountsForLoanProduct(loanProductId,
CASH_ACCOUNTS_FOR_LOAN.LOAN_PORTFOLIO);
final GLAccount fundSourceAccount = helper.getLinkedCashAccountsForLoanProduct(loanProductId, CASH_ACCOUNTS_FOR_LOAN.FUND_SOURCE);
if (isReversed) {
helper.createDebitJournalEntryForLoanProduct(office, fundSourceAccount, loanId, transactionId, transactionDate, disbursalAmount);
helper.createCreditJournalEntryForLoanProduct(office, loanPortfolioAccount, loanId, transactionId, transactionDate,
disbursalAmount);
} else {
helper.createDebitJournalEntryForLoanProduct(office, loanPortfolioAccount, loanId, transactionId, transactionDate,
disbursalAmount);
helper.createCreditJournalEntryForLoanProduct(office, fundSourceAccount, loanId, transactionId, transactionDate,
disbursalAmount);
}
helper.createCashBasedJournalEntriesAndReversalsForLoan(office, CASH_ACCOUNTS_FOR_LOAN.LOAN_PORTFOLIO,
CASH_ACCOUNTS_FOR_LOAN.FUND_SOURCE, loanProductId, loanId, transactionId, transactionDate, disbursalAmount, isReversal);
}
/**
@@ -132,63 +113,30 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF
if (principalAmount != null && !(principalAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(principalAmount);
final GLAccount loanPortfolioAccount = helper.getLinkedCashAccountsForLoanProduct(loanProductId,
CASH_ACCOUNTS_FOR_LOAN.LOAN_PORTFOLIO);
if (isReversal) {
helper.createDebitJournalEntryForLoanProduct(office, loanPortfolioAccount, loanId, transactionId, transactionDate,
principalAmount);
} else {
helper.createCreditJournalEntryForLoanProduct(office, loanPortfolioAccount, loanId, transactionId, transactionDate,
principalAmount);
}
helper.createCreditJournalEntryOrReversalForLoan(office, CASH_ACCOUNTS_FOR_LOAN.LOAN_PORTFOLIO, loanProductId, loanId,
transactionId, transactionDate, principalAmount, isReversal);
}
if (interestAmount != null && !(interestAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(interestAmount);
final GLAccount interestAccount = helper.getLinkedCashAccountsForLoanProduct(loanProductId,
CASH_ACCOUNTS_FOR_LOAN.INTEREST_ON_LOANS);
if (isReversal) {
helper.createDebitJournalEntryForLoanProduct(office, interestAccount, loanId, transactionId, transactionDate,
interestAmount);
} else {
helper.createCreditJournalEntryForLoanProduct(office, interestAccount, loanId, transactionId, transactionDate,
interestAmount);
}
helper.createCreditJournalEntryOrReversalForLoan(office, CASH_ACCOUNTS_FOR_LOAN.INTEREST_ON_LOANS, loanProductId, loanId,
transactionId, transactionDate, interestAmount, isReversal);
}
if (feesAmount != null && !(feesAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(feesAmount);
final GLAccount incomeFromFeesAccount = helper.getLinkedCashAccountsForLoanProduct(loanProductId,
CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_FEES);
if (isReversal) {
helper.createDebitJournalEntryForLoanProduct(office, incomeFromFeesAccount, loanId, transactionId, transactionDate,
feesAmount);
} else {
helper.createCreditJournalEntryForLoanProduct(office, incomeFromFeesAccount, loanId, transactionId, transactionDate,
feesAmount);
}
helper.createCreditJournalEntryOrReversalForLoan(office, CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_FEES, loanProductId, loanId,
transactionId, transactionDate, feesAmount, isReversal);
}
if (penaltiesAmount != null && !(penaltiesAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(penaltiesAmount);
helper.createCreditJournalEntryOrReversalForLoan(office, CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES, loanProductId, loanId,
transactionId, transactionDate, penaltiesAmount, isReversal);
}
final GLAccount incomeFromPenaltiesAccount = helper.getLinkedCashAccountsForLoanProduct(loanProductId,
CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES);
if (isReversal) {
helper.createDebitJournalEntryForLoanProduct(office, incomeFromPenaltiesAccount, loanId, transactionId, transactionDate,
penaltiesAmount);
} else {
helper.createCreditJournalEntryForLoanProduct(office, incomeFromPenaltiesAccount, loanId, transactionId, transactionDate,
penaltiesAmount);
}
}
final GLAccount fundSourceAccount = helper.getLinkedCashAccountsForLoanProduct(loanProductId, CASH_ACCOUNTS_FOR_LOAN.FUND_SOURCE);
if (isReversal) {
helper.createCreditJournalEntryForLoanProduct(office, fundSourceAccount, loanId, transactionId, transactionDate,
totalDebitAmount);
} else {
helper.createDebitJournalEntryForLoanProduct(office, fundSourceAccount, loanId, transactionId, transactionDate,
totalDebitAmount);
}
/*** create a single debit entry (or reversal) for the entire amount **/
helper.createDebitJournalEntryOrReversalForLoan(office, CASH_ACCOUNTS_FOR_LOAN.FUND_SOURCE, loanProductId, loanId, transactionId,
transactionDate, totalDebitAmount, isReversal);
}
}
@@ -21,6 +21,7 @@ public class LoanTransactionEnumData {
private final boolean contra;
private final boolean waiveInterest;
private final boolean waiveCharges;
private final boolean applyCharges;
private final boolean writeOff;
private final boolean recoveryRepayment;
@@ -34,6 +35,7 @@ public class LoanTransactionEnumData {
this.contra = Long.valueOf(3).equals(this.id);
this.waiveInterest = Long.valueOf(4).equals(this.id);
this.waiveCharges = Long.valueOf(9).equals(this.id);
this.applyCharges = Long.valueOf(10).equals(this.id);
this.writeOff = Long.valueOf(6).equals(this.id);
this.recoveryRepayment = Long.valueOf(8).equals(this.id);
}
@@ -70,4 +72,8 @@ public class LoanTransactionEnumData {
return this.recoveryRepayment;
}
public boolean isApplyCharges() {
return this.applyCharges;
}
}
@@ -378,7 +378,8 @@ public class Loan extends AbstractPersistable<Long> {
this.accountNumberRequiresAutoGeneration = accountNumberRequiresAutoGeneration;
}
public void addLoanCharge(final LoanCharge loanCharge) {
public LoanTransaction addLoanCharge(final LoanCharge loanCharge, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds) {
validateLoanIsNotClosed(loanCharge);
@@ -420,6 +421,53 @@ public class Loan extends AbstractPersistable<Long> {
}
updateLoanSummaryDerivedFields();
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
final LoanTransaction applyLoanChargeTransaction = getChargeAppliedTransaction(loanCharge, null);
return applyLoanChargeTransaction;
}
/**
* Creates a loanTransaction for "Apply Charge Event" with transaction date
* set to "suppliedTransactionDate"
*
* If "suppliedTransactionDate" is not passed Id, the transaction date is
* set to the loans due date if the due date is lesser than todays date. If
* not, the transaction date is set to todays date
*
* @param loanCharge
* @param suppliedTransactionDate
* @return
*/
private LoanTransaction getChargeAppliedTransaction(final LoanCharge loanCharge, LocalDate suppliedTransactionDate) {
final Money chargeAmount = loanCharge.getAmount(getCurrency());
Money feeCharges = chargeAmount;
Money penaltyCharges = Money.zero(loanCurrency());
if (loanCharge.isPenaltyCharge()) {
penaltyCharges = chargeAmount;
feeCharges = Money.zero(loanCurrency());
}
LocalDate transactionDate = null;
if (suppliedTransactionDate != null) {
transactionDate = suppliedTransactionDate;
} else {
transactionDate = loanCharge.getDueLocalDate();
LocalDate currentDate = DateUtils.getLocalDateOfTenant();
// if loan charge is to be applied on a future date, the loan
// transaction would show todays date as applied date
if (currentDate.isBefore(transactionDate)) {
transactionDate = currentDate;
}
}
final LoanTransaction applyLoanChargeTransaction = LoanTransaction.applyLoanCharge(this, chargeAmount, transactionDate, feeCharges,
penaltyCharges);
return applyLoanChargeTransaction;
}
private void validateLoanIsNotClosed(final LoanCharge loanCharge) {
@@ -542,10 +590,10 @@ public class Loan extends AbstractPersistable<Long> {
changes.put("amount", amountWaived.getAmount());
Money feeChargesWaived = Money.of(loanCurrency(), loanCharge.amount());
Money feeChargesWaived = amountWaived;
Money penaltyChargesWaived = Money.zero(loanCurrency());
if (loanCharge.isPenaltyCharge()) {
penaltyChargesWaived = Money.of(loanCurrency(), loanCharge.amount());
penaltyChargesWaived = amountWaived;
feeChargesWaived = Money.zero(loanCurrency());
}
@@ -1244,9 +1292,17 @@ public class Loan extends AbstractPersistable<Long> {
chargesPayment.updateLoan(this);
this.loanTransactions.add(chargesPayment);
/**
* all Charges repaid at disbursal is marked as repaid and
* "APPLY Charge" transactions are created for all other fees (
* which are created during disbursal but not repaid)
**/
for (LoanCharge charge : setOfLoanCharges()) {
if (charge.isDueAtDisbursement()) {
charge.markAsFullyPaid();
} else {
LoanTransaction loanChargeAppliedTransaction = getChargeAppliedTransaction(charge, disbursedOn);
this.loanTransactions.add(loanChargeAppliedTransaction);
}
}
}
@@ -1843,7 +1899,7 @@ public class Loan extends AbstractPersistable<Long> {
public boolean isSubmittedOnDateAfter(final LocalDate compareDate) {
return this.submittedOnDate == null ? false : (new LocalDate(submittedOnDate)).isAfter(compareDate);
}
private LocalDate getSubmittedOnDate() {
return (LocalDate) ObjectUtils.defaultIfNull(new LocalDate(this.submittedOnDate), null);
}
@@ -2080,7 +2136,7 @@ public class Loan extends AbstractPersistable<Long> {
break;
}
if (lastAssignmentRecordLatestEndDate == null){
if (lastAssignmentRecordLatestEndDate == null) {
lastAssignmentRecordLatestEndDate = historyRecord;
} else if (historyRecord.isEndDateAfter(lastAssignmentRecordLatestEndDate.getEndDate())
&& !historyRecord.isSameLoanOfficer(newLoanOfficer)) {
@@ -466,7 +466,7 @@ public class LoanCharge extends AbstractPersistable<Long> {
return Money.of(currency, this.amountPaid).plus(amountWaivedOrWrittenOff).isGreaterThanZero();
}
private Money getAmount(final MonetaryCurrency currency) {
public Money getAmount(final MonetaryCurrency currency) {
return Money.of(currency, this.amount);
}
@@ -85,6 +85,14 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
public static LoanTransaction waiver(final Loan loan, final Money waived, final LocalDate waiveDate) {
return new LoanTransaction(loan, LoanTransactionType.WAIVE_INTEREST, waived.getAmount(), waiveDate);
}
public static LoanTransaction applyLoanCharge(final Loan loan, final Money amount, final LocalDate applyDate,
final Money feeCharges, final Money penaltyCharges) {
LoanTransaction applyCharge = new LoanTransaction(loan, LoanTransactionType.APPLY_CHARGES, amount.getAmount(), applyDate);
applyCharge.updateChargesComponents(feeCharges, penaltyCharges);
return applyCharge;
}
public static LoanTransaction waiveLoanCharge(final Loan loan, final Money waived, final LocalDate waiveDate,
final Money feeChargesWaived, final Money penaltyChargesWaived) {
@@ -13,13 +13,15 @@ public enum LoanTransactionType {
CONTRA(3, "loanTransactionType.contra"), //
WAIVE_INTEREST(4, "loanTransactionType.waiver"), //
REPAYMENT_AT_DISBURSEMENT(5, "loanTransactionType.repaymentAtDisbursement"), //
WRITEOFF(6, "loanTransactionType.writeOff"), MARKED_FOR_RESCHEDULING(7, "loanTransactionType.marked.for.rescheduling"), //
WRITEOFF(6, "loanTransactionType.writeOff"), //
MARKED_FOR_RESCHEDULING(7, "loanTransactionType.marked.for.rescheduling"), //
/**
* This type of transactions is allowed on written-off loans where mfi still
* attempts to recover payments from applicant after writing-off.
*/
RECOVERY_REPAYMENT(8, "loanTransactionType.recoveryRepayment"), //
WAIVE_CHARGES(9, "loanTransactionType.waiveCharges");
WAIVE_CHARGES(9, "loanTransactionType.waiveCharges"), //
APPLY_CHARGES(10, "loanTransactionType.applyCharges");
private final Integer value;
private final String code;
@@ -70,6 +72,9 @@ public enum LoanTransactionType {
case 9:
loanTransactionType = LoanTransactionType.WAIVE_CHARGES;
break;
case 10:
loanTransactionType = LoanTransactionType.APPLY_CHARGES;
break;
default:
loanTransactionType = LoanTransactionType.INVALID;
break;
@@ -101,6 +106,10 @@ public enum LoanTransactionType {
return this.value.equals(LoanTransactionType.WAIVE_CHARGES.getValue());
}
public boolean isApplyCharges() {
return this.value.equals(LoanTransactionType.APPLY_CHARGES.getValue());
}
public boolean isWriteOff() {
return this.value.equals(LoanTransactionType.WRITEOFF.getValue());
}
@@ -489,11 +489,25 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
throw new InvalidCurrencyException("loanCharge", "attach.to.loan", errorMessage);
}
final List<Long> existingTransactionIds = new ArrayList<Long>();
final List<Long> existingReversedTransactionIds = new ArrayList<Long>();
this.loanChargeRepository.save(loanCharge);
loan.addLoanCharge(loanCharge);
LoanTransaction applyChargeTransaction = loan.addLoanCharge(loanCharge, existingTransactionIds, existingReversedTransactionIds);
// we want to apply charge transactions only for those loans charges
// that are applied when a loan is active
if (loan.status().isActive()) {
this.loanTransactionRepository.save(applyChargeTransaction);
}
this.loanRepository.save(loan);
// we post Journal entries only for loans in active status
if (loan.status().isActive()) {
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loanCharge.getId()) //
@@ -206,8 +206,7 @@ public class LoanEnumerations {
switch (type) {
case INVALID:
optionData = new LoanTransactionEnumData(LoanTransactionType.INVALID.getValue().longValue(),
LoanTransactionType.INVALID.getCode(),
"Invalid");
LoanTransactionType.INVALID.getCode(), "Invalid");
break;
case DISBURSEMENT:
optionData = new LoanTransactionEnumData(LoanTransactionType.DISBURSEMENT.getValue().longValue(),
@@ -223,8 +222,7 @@ public class LoanEnumerations {
break;
case CONTRA:
optionData = new LoanTransactionEnumData(LoanTransactionType.CONTRA.getValue().longValue(),
LoanTransactionType.CONTRA.getCode(),
"Reversal");
LoanTransactionType.CONTRA.getCode(), "Reversal");
break;
case WAIVE_INTEREST:
optionData = new LoanTransactionEnumData(LoanTransactionType.WAIVE_INTEREST.getValue().longValue(),
@@ -246,6 +244,10 @@ public class LoanEnumerations {
optionData = new LoanTransactionEnumData(LoanTransactionType.WAIVE_CHARGES.getValue().longValue(),
LoanTransactionType.WAIVE_CHARGES.getCode(), "Waive loan charges");
break;
case APPLY_CHARGES:
optionData = new LoanTransactionEnumData(LoanTransactionType.APPLY_CHARGES.getValue().longValue(),
LoanTransactionType.APPLY_CHARGES.getCode(), "Apply Charge");
break;
}
return optionData;
}