wip:initial work for accrual accounting and refactoring

This commit is contained in:
Vishwas Babu AJ
2013-03-06 15:13:27 +05:30
parent f846286858
commit 3850f22140
13 changed files with 815 additions and 362 deletions
@@ -1,143 +0,0 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.accounting.glaccount.data;
import java.math.BigDecimal;
import java.util.Date;
public class LoanTransactionDTO {
private String transactionId;
private Date transactionDate;
private BigDecimal amount;
/*** Breakup of amounts in case of repayments **/
private BigDecimal principal;
private BigDecimal interest;
private BigDecimal fees;
private BigDecimal penalties;
/*** Boolean values determine type of loan transaction ***/
private boolean disbursement;
private boolean repayment;
private boolean repaymentAtDisbursement;
private boolean contra;
private boolean writeOff;
public LoanTransactionDTO(final String transactionId, final Date transactionDate, final BigDecimal amount, final BigDecimal principal,
final BigDecimal interest, final BigDecimal fees, final BigDecimal penalties, final boolean disbursement,
final boolean repayment, final boolean repaymentAtDisbursement, final boolean contra, final boolean writeOff) {
this.transactionId = transactionId;
this.transactionDate = transactionDate;
this.amount = amount;
this.principal = principal;
this.interest = interest;
this.fees = fees;
this.penalties = penalties;
this.disbursement = disbursement;
this.repayment = repayment;
this.repaymentAtDisbursement = repaymentAtDisbursement;
this.contra = contra;
this.writeOff = writeOff;
}
public Date getTransactionDate() {
return this.transactionDate;
}
public void setTransactionDate(final Date transactionDate) {
this.transactionDate = transactionDate;
}
public String getTransactionId() {
return this.transactionId;
}
public void setTransactionId(final String transactionId) {
this.transactionId = transactionId;
}
public BigDecimal getAmount() {
return this.amount;
}
public void setAmount(final BigDecimal amount) {
this.amount = amount;
}
public BigDecimal getPrincipal() {
return this.principal;
}
public void setPrincipal(final BigDecimal principal) {
this.principal = principal;
}
public BigDecimal getInterest() {
return this.interest;
}
public void setInterest(final BigDecimal interest) {
this.interest = interest;
}
public BigDecimal getFees() {
return this.fees;
}
public void setFees(final BigDecimal fees) {
this.fees = fees;
}
public BigDecimal getPenalties() {
return this.penalties;
}
public void setPenalties(final BigDecimal penalties) {
this.penalties = penalties;
}
public boolean isDisbursement() {
return this.disbursement;
}
public void setDisbursement(final boolean disbursement) {
this.disbursement = disbursement;
}
public boolean isRepayment() {
return this.repayment;
}
public void setRepayment(final boolean repayment) {
this.repayment = repayment;
}
public boolean isContra() {
return this.contra;
}
public void setContra(final boolean contra) {
this.contra = contra;
}
public boolean isWriteOff() {
return this.writeOff;
}
public void setWriteOff(final boolean writeOff) {
this.writeOff = writeOff;
}
public boolean isRepaymentAtDisbursement() {
return this.repaymentAtDisbursement;
}
public void setRepaymentAtDisbursement(final boolean repaymentAtDisbursement) {
this.repaymentAtDisbursement = repaymentAtDisbursement;
}
}
@@ -3,8 +3,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.accounting.glaccount.data;
package org.mifosplatform.accounting.journalentry.data;
import java.math.BigDecimal;
import java.util.List;
public class LoanDTO {
@@ -12,15 +13,18 @@ public class LoanDTO {
private Long loanId;
private Long loanProductId;
private Long officeId;
private BigDecimal calculatedInterest;
private boolean cashBasedAccountingEnabled;
private boolean accrualBasedAccountingEnabled;
private List<LoanTransactionDTO> newLoanTransactions;
public LoanDTO(final Long loanId, final Long loanProductId, final Long officeId, final boolean cashBasedAccountingEnabled,
final boolean accrualBasedAccountingEnabled, final List<LoanTransactionDTO> newLoanTransactions) {
public LoanDTO(final Long loanId, final Long loanProductId, final Long officeId, final BigDecimal calculatedInterest,
final boolean cashBasedAccountingEnabled, final boolean accrualBasedAccountingEnabled,
final List<LoanTransactionDTO> newLoanTransactions) {
this.loanId = loanId;
this.loanProductId = loanProductId;
this.officeId = officeId;
this.calculatedInterest = calculatedInterest;
this.cashBasedAccountingEnabled = cashBasedAccountingEnabled;
this.accrualBasedAccountingEnabled = accrualBasedAccountingEnabled;
this.newLoanTransactions = newLoanTransactions;
@@ -73,4 +77,12 @@ public class LoanDTO {
public void setNewLoanTransactions(final List<LoanTransactionDTO> newLoanTransactions) {
this.newLoanTransactions = newLoanTransactions;
}
public BigDecimal getCalculatedInterest() {
return this.calculatedInterest;
}
public void setCalculatedInterest(BigDecimal calculatedInterest) {
this.calculatedInterest = calculatedInterest;
}
}
@@ -0,0 +1,80 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.accounting.journalentry.data;
import java.math.BigDecimal;
import java.util.Date;
import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionEnumData;
public class LoanTransactionDTO {
private final String transactionId;
private final Date transactionDate;
private final LoanTransactionEnumData transactionType;
private final BigDecimal amount;
/*** Breakup of amounts in case of repayments **/
private final BigDecimal principal;
private final BigDecimal interest;
private final BigDecimal fees;
private final BigDecimal penalties;
/*** Boolean values determines if the transaction is reversed ***/
private final boolean reversed;
public LoanTransactionDTO(final String transactionId, final Date transactionDate, final LoanTransactionEnumData transactionType,
final BigDecimal amount, final BigDecimal principal, final BigDecimal interest, final BigDecimal fees,
final BigDecimal penalties, final boolean reversed) {
this.transactionId = transactionId;
this.transactionDate = transactionDate;
this.amount = amount;
this.principal = principal;
this.interest = interest;
this.fees = fees;
this.penalties = penalties;
this.reversed = reversed;
this.transactionType = transactionType;
}
public String getTransactionId() {
return this.transactionId;
}
public Date getTransactionDate() {
return this.transactionDate;
}
public LoanTransactionEnumData getTransactionType() {
return this.transactionType;
}
public BigDecimal getAmount() {
return this.amount;
}
public BigDecimal getPrincipal() {
return this.principal;
}
public BigDecimal getInterest() {
return this.interest;
}
public BigDecimal getFees() {
return this.fees;
}
public BigDecimal getPenalties() {
return this.penalties;
}
public boolean isReversed() {
return this.reversed;
}
}
@@ -0,0 +1,9 @@
package org.mifosplatform.accounting.journalentry.service;
import org.mifosplatform.accounting.journalentry.data.LoanDTO;
public interface AccountingProcessorForLoan {
void createJournalEntriesForLoan(LoanDTO loanDTO);
}
@@ -0,0 +1,35 @@
package org.mifosplatform.accounting.journalentry.service;
import org.mifosplatform.accounting.journalentry.data.LoanDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
@Component
public class AccountingProcessorForLoanFactory {
private ApplicationContext applicationContext;
@Autowired
public AccountingProcessorForLoanFactory(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
public AccountingProcessorForLoan determineProcessor(final LoanDTO loanDTO) {
AccountingProcessorForLoan accountingProcessorForLoan = null;
if (loanDTO.isCashBasedAccountingEnabled()) {
accountingProcessorForLoan = applicationContext
.getBean("cashBasedAccountingProcessorForLoan", AccountingProcessorForLoan.class);
}
else if (loanDTO.isAccrualBasedAccountingEnabled()) {
accountingProcessorForLoan = applicationContext.getBean("accrualBasedAccountingProcessorForLoan",
AccountingProcessorForLoan.class);
}
return accountingProcessorForLoan;
}
}
@@ -0,0 +1,141 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.accounting.journalentry.service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.joda.time.LocalDate;
import org.mifosplatform.accounting.closure.domain.GLClosure;
import org.mifosplatform.accounting.closure.domain.GLClosureRepository;
import org.mifosplatform.accounting.common.AccountingConstants.ACCRUAL_ACCOUNTS_FOR_LOAN;
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.accounting.journalentry.domain.JournalEntry;
import org.mifosplatform.accounting.journalentry.domain.JournalEntryRepository;
import org.mifosplatform.accounting.journalentry.domain.JournalEntryType;
import org.mifosplatform.accounting.journalentry.exception.JournalEntryInvalidException;
import org.mifosplatform.accounting.journalentry.exception.JournalEntryInvalidException.GL_JOURNAL_ENTRY_INVALID_REASON;
import org.mifosplatform.accounting.producttoaccountmapping.domain.PortfolioProductType;
import org.mifosplatform.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping;
import org.mifosplatform.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
import org.mifosplatform.organisation.office.domain.Office;
import org.mifosplatform.organisation.office.domain.OfficeRepository;
import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionEnumData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AccountingProcessorHelper {
private final JournalEntryRepository glJournalEntryRepository;
private final ProductToGLAccountMappingRepository accountMappingRepository;
private final GLClosureRepository closureRepository;
private final OfficeRepository officeRepository;
@Autowired
public AccountingProcessorHelper(final JournalEntryRepository glJournalEntryRepository,
final ProductToGLAccountMappingRepository accountMappingRepository, final GLClosureRepository closureRepository,
final OfficeRepository officeRepository) {
this.glJournalEntryRepository = glJournalEntryRepository;
this.accountMappingRepository = accountMappingRepository;
this.closureRepository = closureRepository;
this.officeRepository = officeRepository;
}
public LoanDTO populateLoanDtoFromMap(final Map<String, Object> accountingBridgeData, final boolean cashBasedAccountingEnabled,
final boolean accrualBasedAccountingEnabled) {
final Long loanId = (Long) accountingBridgeData.get("loanId");
final Long loanProductId = (Long) accountingBridgeData.get("loanProductId");
final Long officeId = (Long) accountingBridgeData.get("officeId");
final BigDecimal calculatedInterest = (BigDecimal) accountingBridgeData.get("calculatedInterest");
final List<LoanTransactionDTO> newLoanTransactions = new ArrayList<LoanTransactionDTO>();
@SuppressWarnings("unchecked")
final List<Map<String, Object>> newTransactionsMap = (List<Map<String, Object>>) accountingBridgeData.get("newLoanTransactions");
for (final Map<String, Object> map : newTransactionsMap) {
final String transactionId = ((Long) map.get("id")).toString();
final Date transactionDate = ((LocalDate) map.get("date")).toDate();
final LoanTransactionEnumData transactionType = (LoanTransactionEnumData) map.get("type");
final BigDecimal amount = (BigDecimal) map.get("amount");
final BigDecimal principal = (BigDecimal) map.get("principalPortion");
final BigDecimal interest = (BigDecimal) map.get("interestPortion");
final BigDecimal fees = (BigDecimal) map.get("feeChargesPortion");
final BigDecimal penalties = (BigDecimal) map.get("penaltyChargesPortion");
final boolean reversed = (Boolean) map.get("reversed");
final LoanTransactionDTO transaction = new LoanTransactionDTO(transactionId, transactionDate, transactionType, amount,
principal, interest, fees, penalties, reversed);
newLoanTransactions.add(transaction);
}
return new LoanDTO(loanId, loanProductId, officeId, calculatedInterest, cashBasedAccountingEnabled, accrualBasedAccountingEnabled,
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();
}
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();
}
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 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);
}
/**
* @param latestGLClosure
* @param transactionDate
*/
public void checkForBranchClosures(final GLClosure latestGLClosure, final Date transactionDate) {
/**
* check if an accounting closure has happened for this branch after the
* transaction Date
**/
if (latestGLClosure != null) {
if (latestGLClosure.getClosingDate().after(transactionDate) || latestGLClosure.getClosingDate().equals(transactionDate)) { throw new JournalEntryInvalidException(
GL_JOURNAL_ENTRY_INVALID_REASON.ACCOUNTING_CLOSED, latestGLClosure.getClosingDate(), null, null); }
}
}
public GLClosure getLatestClosureByBranch(long officeId) {
return this.closureRepository.getLatestGLClosureByBranch(officeId);
}
public Office getOfficeById(long officeId) {
return this.officeRepository.findOne(officeId);
}
}
@@ -0,0 +1,281 @@
package org.mifosplatform.accounting.journalentry.service;
import java.math.BigDecimal;
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.infrastructure.core.service.FileUtils;
import org.mifosplatform.organisation.office.domain.Office;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class AccrualBasedAccountingProcessorForLoan implements AccountingProcessorForLoan {
private final AccountingProcessorHelper helper;
@Autowired
public AccrualBasedAccountingProcessorForLoan(final AccountingProcessorHelper accountingProcessorHelper) {
this.helper = accountingProcessorHelper;
}
@Override
public void createJournalEntriesForLoan(LoanDTO loanDTO) {
final GLClosure latestGLClosure = this.helper.getLatestClosureByBranch(loanDTO.getOfficeId());
final Office office = this.helper.getOfficeById(loanDTO.getOfficeId());
for (final LoanTransactionDTO loanTransactionDTO : loanDTO.getNewLoanTransactions()) {
final Date transactionDate = loanTransactionDTO.getTransactionDate();
helper.checkForBranchClosures(latestGLClosure, transactionDate);
/** Handle Disbursements **/
if (loanTransactionDTO.getTransactionType().isDisbursement()) {
createJournalEntriesForDisbursements(loanDTO, loanTransactionDTO, office);
}
/*** Handle Apply charges ***/
// TODO
/***
* Handle repayments, repayments at disbursement and reversal of
* Repayments and Repayments at disbursement
***/
else if (loanTransactionDTO.getTransactionType().isRepayment()
|| loanTransactionDTO.getTransactionType().isRepaymentAtDisbursement()) {
createJournalEntriesForRepayments(loanDTO, loanTransactionDTO, office);
}
/** Handle Write Offs and waivers **/
else if ((loanTransactionDTO.getTransactionType().isWriteOff() || loanTransactionDTO.getTransactionType().isWaiveInterest() || loanTransactionDTO
.getTransactionType().isWaiveCharges()) && !loanTransactionDTO.isReversed()) {
createJournalEntriesForWriteOff(loanDTO, loanTransactionDTO, office);
}
}
}
/**
* Debit loan Portfolio and credit Fund source for Disbursement. Also
* recognise the receivable interest (Debit "Interest Receivable" and credit
* "Income from Interest")
*
* @param loanDTO
* @param loanTransactionDTO
* @param office
*/
private void createJournalEntriesForDisbursements(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
final Office office) {
// TODO: remove the junk transaction Id
String junkTransactionId = "junk" + FileUtils.generateRandomString();
// 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 disbursalAmount = loanTransactionDTO.getAmount();
final boolean isReversed = loanTransactionDTO.isReversed();
// 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);
}
// 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, junkTransactionId, transactionDate,
interestApplied);
helper.createCreditJournalEntryForLoanProduct(office, interestReceivableAccount, loanId, junkTransactionId,
transactionDate, interestApplied);
} else {
helper.createDebitJournalEntryForLoanProduct(office, interestReceivableAccount, loanId, junkTransactionId, transactionDate,
interestApplied);
helper.createCreditJournalEntryForLoanProduct(office, interestOnLoansAccount, loanId, junkTransactionId, transactionDate,
interestApplied);
}
}
}
/**
* 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)
*
* <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 Write off</b>: Debits "Losses Written Off" and Credits
* "Loan Portfolio"<br/>
*
* <b>Interest Write off</b>:Debits "Losses Written off" and and Credits
* "Receivable Interest" <br/>
*
* <b>Fee Write off</b>:Debits "Losses Written off" and and Credits
* "Receivable Fees" <br/>
*
* <b>Penalty Write off</b>: Debits "Losses Written off" and and Credits
* "Receivable Penalties" <br/>
*
* @param loanTransactionDTO
* @param loanDTO
* @param office
*/
private void createJournalEntriesForWriteOff(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();
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);
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);
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);
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);
helper.createCreditJournalEntryForLoanProduct(office, incomeFromPenaltiesAccount, loanId, transactionId, transactionDate,
penaltiesAmount);
}
final GLAccount fundSourceAccount = helper.getLinkedAccrualAccountsForLoanProduct(loanProductId,
ACCRUAL_ACCOUNTS_FOR_LOAN.LOSSES_WRITTEN_OFF);
helper.createDebitJournalEntryForLoanProduct(office, fundSourceAccount, loanId, transactionId, transactionDate, totalDebitAmount);
}
}
@@ -0,0 +1,194 @@
package org.mifosplatform.accounting.journalentry.service;
import java.math.BigDecimal;
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;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class CashBasedAccountingProcessorForLoan implements AccountingProcessorForLoan {
private final AccountingProcessorHelper helper;
@Autowired
public CashBasedAccountingProcessorForLoan(final AccountingProcessorHelper accountingProcessorHelper) {
this.helper = accountingProcessorHelper;
}
@Override
public void createJournalEntriesForLoan(LoanDTO loanDTO) {
final GLClosure latestGLClosure = this.helper.getLatestClosureByBranch(loanDTO.getOfficeId());
final Office office = this.helper.getOfficeById(loanDTO.getOfficeId());
final Long loanProductId = loanDTO.getLoanProductId();
for (final LoanTransactionDTO loanTransactionDTO : loanDTO.getNewLoanTransactions()) {
final Date transactionDate = loanTransactionDTO.getTransactionDate();
final String transactionId = loanTransactionDTO.getTransactionId();
final Long loanId = loanDTO.getLoanId();
helper.checkForBranchClosures(latestGLClosure, transactionDate);
/** Handle Disbursements and reversals of disbursements **/
if (loanTransactionDTO.getTransactionType().isDisbursement() && !loanTransactionDTO.isReversed()) {
createJournalEntriesForDisbursements(loanDTO, loanTransactionDTO, office);
}
/***
* Logic for repayments, repayments at disbursement and reversal of
* Repayments and Repayments at disbursement
***/
else if (loanTransactionDTO.getTransactionType().isRepayment()
|| loanTransactionDTO.getTransactionType().isRepaymentAtDisbursement()) {
createJournalEntriesForRepayments(loanDTO, loanTransactionDTO, office);
}
/***
* Only principal write off affects cash based accounting (interest
* and fee write off need not be considered). Debit losses written
* off and credit Loan Portfolio
**/
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);
}
}
}
/**
* Debit loan Portfolio and credit Fund source for a Disbursement <br/>
*
* All debits are turned into credits and vice versa in case of disbursement
* reversals
*
*
* @param loanDTO
* @param loanTransactionDTO
* @param office
*/
private void createJournalEntriesForDisbursements(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 disbursalAmount = loanTransactionDTO.getAmount();
final boolean isReversed = 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);
}
}
/**
* Create a single Debit to fund source and multiple credits if applicable
* (loan portfolio for principal repayments, Interest on loans for interest
* repayments, Income from fees for fees payment and Income from penalties
* for penalty payment)
*
* In case the loan transaction is a reversal, 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 isReversal = loanTransactionDTO.isReversed();
BigDecimal totalDebitAmount = new BigDecimal(0);
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);
}
}
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);
}
}
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);
}
}
if (penaltiesAmount != null && !(penaltiesAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(penaltiesAmount);
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);
}
}
}
@@ -6,7 +6,6 @@
package org.mifosplatform.accounting.journalentry.service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -16,15 +15,13 @@ import org.apache.commons.lang.StringUtils;
import org.joda.time.LocalDate;
import org.mifosplatform.accounting.closure.domain.GLClosure;
import org.mifosplatform.accounting.closure.domain.GLClosureRepository;
import org.mifosplatform.accounting.common.AccountingConstants.CASH_ACCOUNTS_FOR_LOAN;
import org.mifosplatform.accounting.glaccount.data.LoanDTO;
import org.mifosplatform.accounting.glaccount.data.LoanTransactionDTO;
import org.mifosplatform.accounting.glaccount.domain.GLAccount;
import org.mifosplatform.accounting.glaccount.domain.GLAccountRepository;
import org.mifosplatform.accounting.glaccount.exception.GLAccountNotFoundException;
import org.mifosplatform.accounting.journalentry.api.JournalEntryJsonInputParams;
import org.mifosplatform.accounting.journalentry.command.JournalEntryCommand;
import org.mifosplatform.accounting.journalentry.command.SingleDebitOrCreditEntryCommand;
import org.mifosplatform.accounting.journalentry.data.LoanDTO;
import org.mifosplatform.accounting.journalentry.domain.JournalEntry;
import org.mifosplatform.accounting.journalentry.domain.JournalEntryRepository;
import org.mifosplatform.accounting.journalentry.domain.JournalEntryType;
@@ -32,9 +29,6 @@ import org.mifosplatform.accounting.journalentry.exception.JournalEntriesNotFoun
import org.mifosplatform.accounting.journalentry.exception.JournalEntryInvalidException;
import org.mifosplatform.accounting.journalentry.exception.JournalEntryInvalidException.GL_JOURNAL_ENTRY_INVALID_REASON;
import org.mifosplatform.accounting.journalentry.serialization.JournalEntryCommandFromApiJsonDeserializer;
import org.mifosplatform.accounting.producttoaccountmapping.domain.PortfolioProductType;
import org.mifosplatform.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping;
import org.mifosplatform.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder;
@@ -58,20 +52,23 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
private final GLAccountRepository glAccountRepository;
private final JournalEntryRepository glJournalEntryRepository;
private final OfficeRepository officeRepository;
private final ProductToGLAccountMappingRepository accountMappingRepository;
private final AccountingProcessorForLoanFactory accountingProcessorForLoanFactory;
private final AccountingProcessorHelper helper;
private final JournalEntryCommandFromApiJsonDeserializer fromApiJsonDeserializer;
@Autowired
public JournalEntryWritePlatformServiceJpaRepositoryImpl(final GLClosureRepository glClosureRepository,
final GLAccountRepository glAccountRepository, final JournalEntryRepository glJournalEntryRepository,
final OfficeRepository officeRepository, final ProductToGLAccountMappingRepository accountMappingRepository,
final JournalEntryCommandFromApiJsonDeserializer fromApiJsonDeserializer) {
final JournalEntryRepository glJournalEntryRepository, final OfficeRepository officeRepository,
final GLAccountRepository glAccountRepository, final JournalEntryCommandFromApiJsonDeserializer fromApiJsonDeserializer,
final AccountingProcessorHelper accountingProcessorHelper,
final AccountingProcessorForLoanFactory accountingProcessorForLoanFactory) {
this.glClosureRepository = glClosureRepository;
this.officeRepository = officeRepository;
this.glAccountRepository = glAccountRepository;
this.glJournalEntryRepository = glJournalEntryRepository;
this.accountMappingRepository = accountMappingRepository;
this.fromApiJsonDeserializer = fromApiJsonDeserializer;
this.glAccountRepository = glAccountRepository;
this.accountingProcessorForLoanFactory = accountingProcessorForLoanFactory;
this.helper = accountingProcessorHelper;
}
@Transactional
@@ -148,195 +145,15 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
final boolean cashBasedAccountingEnabled = (Boolean) accountingBridgeData.get("cashBasedAccountingEnabled");
final boolean accrualBasedAccountingEnabled = (Boolean) accountingBridgeData.get("accrualBasedAccountingEnabled");
if (cashBasedAccountingEnabled) {
final LoanDTO loanDTO = populateDtoFromMap(accountingBridgeData, cashBasedAccountingEnabled, accrualBasedAccountingEnabled);
createJournalEntriesUsingCashRules(loanDTO);
} else if (accrualBasedAccountingEnabled) {
// TODO Vishwas: add accrual based accounting rules
if (cashBasedAccountingEnabled || accrualBasedAccountingEnabled) {
final LoanDTO loanDTO = helper.populateLoanDtoFromMap(accountingBridgeData, cashBasedAccountingEnabled,
accrualBasedAccountingEnabled);
final AccountingProcessorForLoan accountingProcessorForLoan = this.accountingProcessorForLoanFactory
.determineProcessor(loanDTO);
accountingProcessorForLoan.createJournalEntriesForLoan(loanDTO);
}
}
private LoanDTO populateDtoFromMap(final Map<String, Object> accountingBridgeData, final boolean cashBasedAccountingEnabled,
final boolean accrualBasedAccountingEnabled) {
final Long loanId = (Long) accountingBridgeData.get("loanId");
final Long loanProductId = (Long) accountingBridgeData.get("loanProductId");
final Long officeId = (Long) accountingBridgeData.get("officeId");
final List<LoanTransactionDTO> newLoanTransactions = new ArrayList<LoanTransactionDTO>();
@SuppressWarnings("unchecked")
final List<Map<String, Object>> newTransactionsMap = (List<Map<String, Object>>) accountingBridgeData.get("newLoanTransactions");
for (final Map<String, Object> map : newTransactionsMap) {
final String transactionId = ((Long) map.get("id")).toString();
final Date transactionDate = ((LocalDate) map.get("date")).toDate();
final BigDecimal amount = (BigDecimal) map.get("amount");
final BigDecimal principal = (BigDecimal) map.get("principalPortion");
final BigDecimal interest = (BigDecimal) map.get("interestPortion");
final BigDecimal fees = (BigDecimal) map.get("feeChargesPortion");
final BigDecimal penalties = (BigDecimal) map.get("penaltyChargesPortion");
final boolean disbursement = (Boolean) map.get("disbursement");
final boolean repayment = (Boolean) map.get("repayment");
final boolean repaymentAtDisbursement = (Boolean) map.get("repaymentAtDisbursement");
final boolean contra = (Boolean) map.get("contra");
final boolean writeOff = (Boolean) map.get("writeOff");
final LoanTransactionDTO transaction = new LoanTransactionDTO(transactionId, transactionDate, amount, principal, interest,
fees, penalties, disbursement, repayment, repaymentAtDisbursement, contra, writeOff);
newLoanTransactions.add(transaction);
}
return new LoanDTO(loanId, loanProductId, officeId, cashBasedAccountingEnabled, accrualBasedAccountingEnabled, newLoanTransactions);
}
private void createJournalEntriesUsingCashRules(final LoanDTO loanDTO) {
final GLClosure latestGLClosure = this.glClosureRepository.getLatestGLClosureByBranch(loanDTO.getOfficeId());
final Office office = this.officeRepository.findOne(loanDTO.getOfficeId());
final Long loanProductId = loanDTO.getLoanProductId();
for (final LoanTransactionDTO loanTransactionDTO : loanDTO.getNewLoanTransactions()) {
final Date transactionDate = loanTransactionDTO.getTransactionDate();
final String transactionId = loanTransactionDTO.getTransactionId();
final Long loanId = loanDTO.getLoanId();
/**
* check if an accounting closure has happened for this branch after
* the transaction Date
**/
if (latestGLClosure != null) {
if (latestGLClosure.getClosingDate().after(transactionDate) || latestGLClosure.getClosingDate().equals(transactionDate)) { throw new JournalEntryInvalidException(
GL_JOURNAL_ENTRY_INVALID_REASON.ACCOUNTING_CLOSED, latestGLClosure.getClosingDate(), null, null); }
}
/*** Debit loan Portfolio and credit Fund source for Disbursal **/
if (loanTransactionDTO.isDisbursement() && !loanTransactionDTO.isContra()) {
final GLAccount loanPortfolioAccount = getLinkedFinAccountForLoanProduct(loanProductId,
CASH_ACCOUNTS_FOR_LOAN.LOAN_PORTFOLIO);
final GLAccount fundSourceAccount = getLinkedFinAccountForLoanProduct(loanProductId, CASH_ACCOUNTS_FOR_LOAN.FUND_SOURCE);
final BigDecimal disbursalAmount = loanTransactionDTO.getAmount();
createDebitJournalEntryForLoanProduct(office, loanPortfolioAccount, loanId, transactionId, transactionDate, disbursalAmount);
createCreditJournalEntryForLoanProduct(office, fundSourceAccount, loanId, transactionId, transactionDate, disbursalAmount);
} else if (loanTransactionDTO.isRepayment() || loanTransactionDTO.isRepaymentAtDisbursement() || loanTransactionDTO.isContra()) {
createJournalEntriesForRepaymentOrContraUsingCashRules(office, loanProductId, loanTransactionDTO, transactionDate,
transactionId, loanId);
}/***
* Only principal write off affects cash based accounting (interest
* and fee write off need not be considered). Debit losses written
* off and credit Loan Portfolio
**/
else if (loanTransactionDTO.isWriteOff()) {
final BigDecimal principalAmount = loanTransactionDTO.getPrincipal();
final GLAccount lossesWrittenOffAccount = getLinkedFinAccountForLoanProduct(loanProductId,
CASH_ACCOUNTS_FOR_LOAN.LOSSES_WRITTEN_OFF);
createDebitJournalEntryForLoanProduct(office, lossesWrittenOffAccount, loanId, transactionId, transactionDate,
principalAmount);
final GLAccount loanPortfolioAccount = getLinkedFinAccountForLoanProduct(loanProductId,
CASH_ACCOUNTS_FOR_LOAN.LOAN_PORTFOLIO);
createCreditJournalEntryForLoanProduct(office, loanPortfolioAccount, loanId, transactionId, transactionDate,
principalAmount);
}
}
}
/**
* Create a single Debit to fund source and multiple credits if applicable
* (loan portfolio for principal repayment, Interest on loans for interest
* repayments, Income from fees for fees payment and Income from penalties
* for penalty payment)
*
* In case the loan transaction is a contra, all debits are turned into
* credits and vice versa
*/
private void createJournalEntriesForRepaymentOrContraUsingCashRules(final Office office, final Long loanProductId,
final LoanTransactionDTO loanTransactionDTO, final Date transactionDate, String transactionId, final Long loanId) {
final BigDecimal principalAmount = loanTransactionDTO.getPrincipal();
final BigDecimal interestAmount = loanTransactionDTO.getInterest();
final BigDecimal feesAmount = loanTransactionDTO.getFees();
final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties();
final boolean isContraTransaction = loanTransactionDTO.isContra();
BigDecimal totalDebitAmount = new BigDecimal(0);
if (principalAmount != null && !(principalAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(principalAmount);
final GLAccount loanPortfolioAccount = getLinkedFinAccountForLoanProduct(loanProductId, CASH_ACCOUNTS_FOR_LOAN.LOAN_PORTFOLIO);
if (isContraTransaction) {
createDebitJournalEntryForLoanProduct(office, loanPortfolioAccount, loanId, transactionId, transactionDate, principalAmount);
} else {
createCreditJournalEntryForLoanProduct(office, loanPortfolioAccount, loanId, transactionId, transactionDate,
principalAmount);
}
}
if (interestAmount != null && !(interestAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(interestAmount);
final GLAccount interestAccount = getLinkedFinAccountForLoanProduct(loanProductId, CASH_ACCOUNTS_FOR_LOAN.INTEREST_ON_LOANS);
if (isContraTransaction) {
createDebitJournalEntryForLoanProduct(office, interestAccount, loanId, transactionId, transactionDate, interestAmount);
} else {
createCreditJournalEntryForLoanProduct(office, interestAccount, loanId, transactionId, transactionDate, interestAmount);
}
}
if (feesAmount != null && !(feesAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(feesAmount);
final GLAccount incomeFromFeesAccount = getLinkedFinAccountForLoanProduct(loanProductId,
CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_FEES);
if (isContraTransaction) {
createDebitJournalEntryForLoanProduct(office, incomeFromFeesAccount, loanId, transactionId, transactionDate, feesAmount);
} else {
createCreditJournalEntryForLoanProduct(office, incomeFromFeesAccount, loanId, transactionId, transactionDate, feesAmount);
}
}
if (penaltiesAmount != null && !(penaltiesAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(penaltiesAmount);
final GLAccount incomeFromPenaltiesAccount = getLinkedFinAccountForLoanProduct(loanProductId,
CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES);
if (isContraTransaction) {
createDebitJournalEntryForLoanProduct(office, incomeFromPenaltiesAccount, loanId, transactionId, transactionDate,
penaltiesAmount);
} else {
createCreditJournalEntryForLoanProduct(office, incomeFromPenaltiesAccount, loanId, transactionId, transactionDate,
penaltiesAmount);
}
}
final GLAccount fundSourceAccount = getLinkedFinAccountForLoanProduct(loanProductId, CASH_ACCOUNTS_FOR_LOAN.FUND_SOURCE);
if (isContraTransaction) {
createCreditJournalEntryForLoanProduct(office, fundSourceAccount, loanId, transactionId, transactionDate, totalDebitAmount);
} else {
createDebitJournalEntryForLoanProduct(office, fundSourceAccount, loanId, transactionId, transactionDate, totalDebitAmount);
}
}
private GLAccount getLinkedFinAccountForLoanProduct(final Long loanProductId, final CASH_ACCOUNTS_FOR_LOAN finAccountType) {
final ProductToGLAccountMapping accountMapping = this.accountMappingRepository
.findByProductIdAndProductTypeAndFinancialAccountType(loanProductId, PortfolioProductType.LOAN.getValue(),
finAccountType.getValue());
return accountMapping.getGlAccount();
}
private 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);
}
private 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);
}
private void validateBusinessRulesForJournalEntries(final JournalEntryCommand command) {
/** check if date of Journal entry is valid ***/
final LocalDate entryLocalDate = command.getTransactionDate();
@@ -405,13 +222,6 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
}
}
private void handleJournalEntryDataIntegrityIssues(final DataIntegrityViolationException dve) {
final Throwable realCause = dve.getMostSpecificCause();
logger.error(dve.getMessage(), dve);
throw new PlatformDataIntegrityException("error.msg.glJournalEntry.unknown.data.integrity.issue",
"Unknown data integrity issue with resource Journal Entry: " + realCause.getMessage());
}
/**
* TODO: Need a better implementation with guaranteed uniqueness (but not a
* long UUID)...maybe something tied to system clock..
@@ -419,4 +229,12 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
private String generateTransactionId() {
return RandomStringUtils.random(15, "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890");
}
private void handleJournalEntryDataIntegrityIssues(final DataIntegrityViolationException dve) {
final Throwable realCause = dve.getMostSpecificCause();
logger.error(dve.getMessage(), dve);
throw new PlatformDataIntegrityException("error.msg.glJournalEntry.unknown.data.integrity.issue",
"Unknown data integrity issue with resource Journal Entry: " + realCause.getMessage());
}
}
@@ -41,4 +41,33 @@ public class LoanTransactionEnumData {
public Long id() {
return this.id;
}
public boolean isDisbursement() {
return this.disbursement;
}
public boolean isRepaymentAtDisbursement() {
return this.repaymentAtDisbursement;
}
public boolean isRepayment() {
return this.repayment;
}
public boolean isWaiveInterest() {
return this.waiveInterest;
}
public boolean isWaiveCharges() {
return this.waiveCharges;
}
public boolean isWriteOff() {
return this.writeOff;
}
public boolean isRecoveryRepayment() {
return this.recoveryRepayment;
}
}
@@ -2067,6 +2067,7 @@ public class Loan extends AbstractPersistable<Long> {
accountingBridgeData.put("loanId", this.getId());
accountingBridgeData.put("loanProductId", this.productId());
accountingBridgeData.put("officeId", this.getOfficeId());
accountingBridgeData.put("calculatedInterest", this.summary.getTotalInterestCharged());
accountingBridgeData.put("cashBasedAccountingEnabled", this.isCashBasedAccountingEnabledOnLoanProduct());
accountingBridgeData.put("accrualBasedAccountingEnabled", this.isAccrualBasedAccountingEnabledOnLoanProduct());
@@ -130,6 +130,10 @@ public final class LoanSummary {
public boolean isRepaidInFull(final MonetaryCurrency currency) {
return getTotalOutstanding(currency).isZero();
}
public BigDecimal getTotalInterestCharged() {
return this.totalInterestCharged;
}
/**
* All fields but <code>totalFeeChargesDueAtDisbursement</code> should be
@@ -208,7 +208,7 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
return LoanTransactionType.REPAYMENT_AT_DISBURSEMENT.equals(getTypeOf()) && isNotReversed();
}
private boolean isRecoveryRepayment() {
public boolean isRecoveryRepayment() {
return LoanTransactionType.RECOVERY_REPAYMENT.equals(getTypeOf()) && isNotReversed();
}
@@ -285,15 +285,7 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
thisTransactionData.put("id", this.getId());
thisTransactionData.put("type", transactionType);
thisTransactionData.put("disbursement", Boolean.valueOf(this.isDisbursement()));
thisTransactionData.put("repaymentAtDisbursement", Boolean.valueOf(this.isRepaymentAtDisbursement()));
thisTransactionData.put("repayment", Boolean.valueOf(this.isRepayment()));
thisTransactionData.put("recoveryRepayment", Boolean.valueOf(this.isRecoveryRepayment()));
thisTransactionData.put("contra", Boolean.valueOf(this.isReversed()));
thisTransactionData.put("waiveInterest", Boolean.valueOf(this.isInterestWaiver()));
thisTransactionData.put("waiveCharges", Boolean.valueOf(this.isChargesWaiver()));
thisTransactionData.put("writeOff", Boolean.valueOf(this.isWriteOff()));
thisTransactionData.put("reversed", Boolean.valueOf(this.isReversed()));
thisTransactionData.put("date", this.getTransactionDate());
thisTransactionData.put("currency", currencyData);
thisTransactionData.put("amount", this.amount);