diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/LoanTransactionDTO.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/LoanTransactionDTO.java deleted file mode 100755 index c32c1a5f7..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/LoanTransactionDTO.java +++ /dev/null @@ -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; - } - -} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/LoanDTO.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/LoanDTO.java similarity index 77% rename from mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/LoanDTO.java rename to mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/LoanDTO.java index a7752a557..6345d14c5 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/LoanDTO.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/LoanDTO.java @@ -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 newLoanTransactions; - public LoanDTO(final Long loanId, final Long loanProductId, final Long officeId, final boolean cashBasedAccountingEnabled, - final boolean accrualBasedAccountingEnabled, final List newLoanTransactions) { + public LoanDTO(final Long loanId, final Long loanProductId, final Long officeId, final BigDecimal calculatedInterest, + final boolean cashBasedAccountingEnabled, final boolean accrualBasedAccountingEnabled, + final List 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 newLoanTransactions) { this.newLoanTransactions = newLoanTransactions; } + + public BigDecimal getCalculatedInterest() { + return this.calculatedInterest; + } + + public void setCalculatedInterest(BigDecimal calculatedInterest) { + this.calculatedInterest = calculatedInterest; + } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/LoanTransactionDTO.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/LoanTransactionDTO.java new file mode 100755 index 000000000..4b0ef2c0c --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/LoanTransactionDTO.java @@ -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; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccountingProcessorForLoan.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccountingProcessorForLoan.java new file mode 100755 index 000000000..321b8af53 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccountingProcessorForLoan.java @@ -0,0 +1,9 @@ +package org.mifosplatform.accounting.journalentry.service; + +import org.mifosplatform.accounting.journalentry.data.LoanDTO; + +public interface AccountingProcessorForLoan { + + void createJournalEntriesForLoan(LoanDTO loanDTO); + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccountingProcessorForLoanFactory.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccountingProcessorForLoanFactory.java new file mode 100755 index 000000000..8ca4c7d31 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccountingProcessorForLoanFactory.java @@ -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; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccountingProcessorHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccountingProcessorHelper.java new file mode 100755 index 000000000..d1121617f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccountingProcessorHelper.java @@ -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 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 newLoanTransactions = new ArrayList(); + + @SuppressWarnings("unchecked") + final List> newTransactionsMap = (List>) accountingBridgeData.get("newLoanTransactions"); + + for (final Map 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); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java new file mode 100755 index 000000000..90cf457fb --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java @@ -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) + * + *
+ * + * 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
+ * + * Principal Write off: Debits "Losses Written Off" and Credits + * "Loan Portfolio"
+ * + * Interest Write off:Debits "Losses Written off" and and Credits + * "Receivable Interest"
+ * + * Fee Write off:Debits "Losses Written off" and and Credits + * "Receivable Fees"
+ * + * Penalty Write off: Debits "Losses Written off" and and Credits + * "Receivable Penalties"
+ * + * @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); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java new file mode 100755 index 000000000..d603073b0 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java @@ -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
+ * + * 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); + } + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java index 8a6230eac..5c45dd29f 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java @@ -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 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 newLoanTransactions = new ArrayList(); - - @SuppressWarnings("unchecked") - final List> newTransactionsMap = (List>) accountingBridgeData.get("newLoanTransactions"); - - for (final Map 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()); + } + } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTransactionEnumData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTransactionEnumData.java index fcb6854e3..560611bd7 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTransactionEnumData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTransactionEnumData.java @@ -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; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java index fb341a6b2..8449a96d6 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java @@ -2067,6 +2067,7 @@ public class Loan extends AbstractPersistable { 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()); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanSummary.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanSummary.java index 646bc7faf..9d2499376 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanSummary.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanSummary.java @@ -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 totalFeeChargesDueAtDisbursement should be diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java index bc796a3aa..8087d475e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java @@ -208,7 +208,7 @@ public final class LoanTransaction extends AbstractPersistable { 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 { 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);