Merge pull request #851 from pramodn02/MIFOSX-1074

MIFOSX-1074 - Added batch job for accrual accounting journal entry
This commit is contained in:
Vishwas Babu A J
2014-04-11 12:39:22 -07:00
9 changed files with 368 additions and 2 deletions
@@ -0,0 +1,6 @@
ALTER TABLE `m_loan_repayment_schedule`
ADD COLUMN `accrual_interest_derived` DECIMAL(19,6) NULL DEFAULT NULL AFTER `interest_waived_derived`,
ADD COLUMN `accrual_fee_charges_derived` DECIMAL(19,6) NULL DEFAULT NULL AFTER `fee_charges_waived_derived`,
ADD COLUMN `accrual_penalty_charges_derived` DECIMAL(19,6) NULL DEFAULT NULL AFTER `penalty_charges_waived_derived`;
INSERT INTO `job` (`name`, `display_name`, `cron_expression`, `create_time`, `task_priority`, `group_name`, `previous_run_start_time`, `next_run_time`, `job_key`, `initializing_errorlog`, `is_active`, `currently_running`, `updates_allowed`, `scheduler_group`, `is_misfired`) VALUES ('Add Accrual Transactions', 'Add Accrual Transactions', '0 1 0 1/1 * ? *', now(), 5, NULL, NULL, NULL, NULL, NULL, 1, 0, 1, 0, 0);
@@ -12,7 +12,8 @@ public enum JobName {
ACCOUNTING_RUNNING_BALANCE_UPDATE("Update Accounting Running Balances"), //
PAY_DUE_SAVINGS_CHARGES("Pay Due Savings Charges"), //
APPLY_CHARGE_TO_OVERDUE_LOAN_INSTALLMENT("Apply penalty to overdue loans"),
EXECUTE_STANDING_INSTRUCTIONS("Execute Standing Instruction");
EXECUTE_STANDING_INSTRUCTIONS("Execute Standing Instruction"),
ADD_ACCRUAL_ENTRIES("Add Accrual Transactions");
private final String name;
@@ -0,0 +1,94 @@
package org.mifosplatform.portfolio.loanaccount.data;
import java.math.BigDecimal;
import java.util.Date;
import org.joda.time.LocalDate;
import org.mifosplatform.organisation.monetary.data.CurrencyData;
public class LoanScheduleAccrualData {
private final Long loanId;
private final Long officeId;
private final LocalDate dueDate;
private final Long repaymentScheduleId;
private final Long loanProductId;
private final BigDecimal interestIncome;
private final BigDecimal feeIncome;
private final BigDecimal penaltyIncome;
private final BigDecimal accruedInterestIncome;
private final BigDecimal accruedFeeIncome;
private final BigDecimal accruedPenaltyIncome;
private final CurrencyData currencyData;
public LoanScheduleAccrualData(final Long loanId, final Long officeId, final LocalDate dueDate, final Long repaymentScheduleId,
final Long loanProductId, final BigDecimal interestIncome, final BigDecimal feeIncome, final BigDecimal penaltyIncome,
final BigDecimal accruedInterestIncome, final BigDecimal accruedFeeIncome, final BigDecimal accruedPenaltyIncome,
final CurrencyData currencyData) {
this.loanId = loanId;
this.officeId = officeId;
this.dueDate = dueDate;
this.repaymentScheduleId = repaymentScheduleId;
this.loanProductId = loanProductId;
this.interestIncome = interestIncome;
this.feeIncome = feeIncome;
this.penaltyIncome = penaltyIncome;
this.accruedFeeIncome = accruedFeeIncome;
this.accruedInterestIncome = accruedInterestIncome;
this.accruedPenaltyIncome = accruedPenaltyIncome;
this.currencyData = currencyData;
}
public Long getLoanId() {
return this.loanId;
}
public Long getOfficeId() {
return this.officeId;
}
public Date getDueDate() {
return this.dueDate.toDate();
}
public LocalDate getDueDateAsLocaldate() {
return this.dueDate;
}
public Long getRepaymentScheduleId() {
return this.repaymentScheduleId;
}
public Long getLoanProductId() {
return this.loanProductId;
}
public BigDecimal getInterestIncome() {
return this.interestIncome;
}
public BigDecimal getFeeIncome() {
return this.feeIncome;
}
public BigDecimal getPenaltyIncome() {
return this.penaltyIncome;
}
public BigDecimal getAccruedInterestIncome() {
return this.accruedInterestIncome;
}
public BigDecimal getAccruedFeeIncome() {
return this.accruedFeeIncome;
}
public BigDecimal getAccruedPenaltyIncome() {
return this.accruedPenaltyIncome;
}
public CurrencyData getCurrencyData() {
return this.currencyData;
}
}
@@ -34,7 +34,8 @@ public enum LoanTransactionType {
WITHDRAW_TRANSFER(14, "loanTransactionType.withdrawTransfer"), //
REJECT_TRANSFER(15, "loanTransactionType.rejectTransfer"),
REFUND(16, "loanTransactionType.refund"), CHARGE_PAYMENT(17, "loanTransactionType.chargePayment");
REFUND(16, "loanTransactionType.refund"), CHARGE_PAYMENT(17, "loanTransactionType.chargePayment"),
ACCRUAL_ACCOUNTING(17,"loanTransactionType.accrualAccounting");
private final Integer value;
private final String code;
@@ -0,0 +1,10 @@
package org.mifosplatform.portfolio.loanaccount.service;
import org.mifosplatform.infrastructure.jobs.exception.JobExecutionException;
public interface LoanAccrualWritePlatformService {
void addAccrualAccounting() throws JobExecutionException;
}
@@ -0,0 +1,179 @@
package org.mifosplatform.portfolio.loanaccount.service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.mifosplatform.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.mifosplatform.infrastructure.core.service.RoutingDataSource;
import org.mifosplatform.infrastructure.jobs.annotation.CronTarget;
import org.mifosplatform.infrastructure.jobs.exception.JobExecutionException;
import org.mifosplatform.infrastructure.jobs.service.JobName;
import org.mifosplatform.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionEnumData;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionType;
import org.mifosplatform.portfolio.loanproduct.service.LoanEnumerations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.DefaultTransactionDefinition;
@Service
public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlatformService {
private final LoanReadPlatformService loanReadPlatformService;
private final JdbcTemplate jdbcTemplate;
private final DataSource dataSource;
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
private final JpaTransactionManager transactionManager;
@Autowired
public LoanAccrualWritePlatformServiceImpl(final RoutingDataSource dataSource, final LoanReadPlatformService loanReadPlatformService,
final JournalEntryWritePlatformService journalEntryWritePlatformService, final JpaTransactionManager transactionManager) {
this.loanReadPlatformService = loanReadPlatformService;
this.dataSource = dataSource;
this.jdbcTemplate = new JdbcTemplate(this.dataSource);
this.journalEntryWritePlatformService = journalEntryWritePlatformService;
this.transactionManager = transactionManager;
}
@Override
@CronTarget(jobName = JobName.ADD_ACCRUAL_ENTRIES)
public void addAccrualAccounting() throws JobExecutionException {
Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas = this.loanReadPlatformService.retriveScheduleAccrualData();
StringBuilder sb = new StringBuilder();
for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualDatas) {
try {
addAccrualAccounting(accrualData);
} catch (Exception e) {
Throwable realCause = e;
if (e.getCause() != null) {
realCause = e.getCause();
}
sb.append("failed to add accural transaction for repayment with id " + accrualData.getRepaymentScheduleId()
+ " with message " + realCause.getMessage());
}
}
if (sb.length() > 0) { throw new JobExecutionException(sb.toString()); }
}
@Transactional
public void addAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData) throws Exception {
String transactionSql = "INSERT INTO m_loan_transaction (loan_id,office_id,is_reversed,transaction_type_enum,transaction_date,amount,interest_portion_derived,"
+ "fee_charges_portion_derived,penalty_charges_portion_derived) VALUES (?, ?, 0, ?, ?, ?, ?, ?, ?)";
BigDecimal amount = BigDecimal.ZERO;
BigDecimal interestportion = null;
BigDecimal totalAccInterest = null;
if (scheduleAccrualData.getInterestIncome() != null) {
interestportion = scheduleAccrualData.getInterestIncome();
totalAccInterest = interestportion;
if (scheduleAccrualData.getAccruedInterestIncome() != null) {
interestportion = interestportion.subtract(scheduleAccrualData.getAccruedInterestIncome());
}
amount = amount.add(interestportion);
if (interestportion.compareTo(BigDecimal.ZERO) == 0) {
interestportion = null;
}
}
BigDecimal feeportion = null;
BigDecimal totalAccFee = null;
if (scheduleAccrualData.getFeeIncome() != null) {
feeportion = scheduleAccrualData.getFeeIncome();
totalAccFee = feeportion;
if (scheduleAccrualData.getAccruedFeeIncome() != null) {
feeportion.subtract(scheduleAccrualData.getAccruedFeeIncome());
}
amount = amount.add(feeportion);
if (feeportion.compareTo(BigDecimal.ZERO) == 0) {
feeportion = null;
}
}
BigDecimal penaltyportion = null;
BigDecimal totalAccPenalty = null;
if (scheduleAccrualData.getPenaltyIncome() != null) {
penaltyportion = scheduleAccrualData.getPenaltyIncome();
totalAccPenalty = penaltyportion;
if (scheduleAccrualData.getAccruedPenaltyIncome() != null) {
penaltyportion.subtract(scheduleAccrualData.getAccruedPenaltyIncome());
}
amount = amount.add(penaltyportion);
if (penaltyportion.compareTo(BigDecimal.ZERO) == 0) {
penaltyportion = null;
}
}
TransactionStatus transactionStatus = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
this.jdbcTemplate.update(transactionSql, scheduleAccrualData.getLoanId(), scheduleAccrualData.getOfficeId(),
LoanTransactionType.ACCRUAL_ACCOUNTING.getValue(), scheduleAccrualData.getDueDate(), amount, interestportion,
feeportion, penaltyportion);
final Long transactonId = this.jdbcTemplate.queryForLong("SELECT LAST_INSERT_ID()");
Map<String, Object> transactionMap = toMapData(transactonId, amount, interestportion, feeportion, penaltyportion,
scheduleAccrualData);
String repaymetUpdatesql = "UPDATE m_loan_repayment_schedule SET accrual_interest_derived=?, accrual_fee_charges_derived=?, "
+ "accrual_penalty_charges_derived=? WHERE id=?";
this.jdbcTemplate.update(repaymetUpdatesql, totalAccInterest, totalAccFee, totalAccPenalty,
scheduleAccrualData.getRepaymentScheduleId());
final Map<String, Object> accountingBridgeData = deriveAccountingBridgeData(scheduleAccrualData, transactionMap);
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
} catch (Exception e) {
this.transactionManager.rollback(transactionStatus);
throw e;
}
this.transactionManager.commit(transactionStatus);
}
public Map<String, Object> deriveAccountingBridgeData(final LoanScheduleAccrualData loanScheduleAccrualData,
final Map<String, Object> transactionMap) {
final Map<String, Object> accountingBridgeData = new LinkedHashMap<String, Object>();
accountingBridgeData.put("loanId", loanScheduleAccrualData.getLoanId());
accountingBridgeData.put("loanProductId", loanScheduleAccrualData.getLoanProductId());
accountingBridgeData.put("officeId", loanScheduleAccrualData.getOfficeId());
accountingBridgeData.put("currency", loanScheduleAccrualData.getCurrencyData());
accountingBridgeData.put("cashBasedAccountingEnabled", false);
accountingBridgeData.put("accrualBasedAccountingEnabled", true);
final List<Map<String, Object>> newLoanTransactions = new ArrayList<Map<String, Object>>();
newLoanTransactions.add(transactionMap);
accountingBridgeData.put("newLoanTransactions", newLoanTransactions);
return accountingBridgeData;
}
public Map<String, Object> toMapData(final Long id, final BigDecimal amount, final BigDecimal interestportion,
final BigDecimal feeportion, final BigDecimal penaltyportion, final LoanScheduleAccrualData loanScheduleAccrualData) {
final Map<String, Object> thisTransactionData = new LinkedHashMap<String, Object>();
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.ACCRUAL_ACCOUNTING);
thisTransactionData.put("id", id);
thisTransactionData.put("officeId", loanScheduleAccrualData.getOfficeId());
thisTransactionData.put("type", transactionType);
thisTransactionData.put("reversed", false);
thisTransactionData.put("date", loanScheduleAccrualData.getDueDateAsLocaldate());
thisTransactionData.put("currency", loanScheduleAccrualData.getCurrencyData());
thisTransactionData.put("amount", amount);
thisTransactionData.put("principalPortion", null);
thisTransactionData.put("interestPortion", interestportion);
thisTransactionData.put("feeChargesPortion", feeportion);
thisTransactionData.put("penaltyChargesPortion", penaltyportion);
thisTransactionData.put("overPaymentPortion", null);
return thisTransactionData;
}
}
@@ -13,6 +13,7 @@ import org.mifosplatform.portfolio.calendar.data.CalendarData;
import org.mifosplatform.portfolio.group.service.SearchParameters;
import org.mifosplatform.portfolio.loanaccount.data.DisbursementData;
import org.mifosplatform.portfolio.loanaccount.data.LoanAccountData;
import org.mifosplatform.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData;
import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionData;
import org.mifosplatform.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData;
@@ -77,4 +78,6 @@ public interface LoanReadPlatformService {
DisbursementData retrieveLoanDisbursementDetail(Long loanId, Long disbursementId);
Collection<LoanTermVariationsData> retrieveLoanTermVariations(Long loanId, Integer termType);
Collection<LoanScheduleAccrualData> retriveScheduleAccrualData();
}
@@ -16,6 +16,7 @@ import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.joda.time.Days;
import org.joda.time.LocalDate;
import org.mifosplatform.accounting.common.AccountingRuleType;
import org.mifosplatform.infrastructure.codes.data.CodeValueData;
import org.mifosplatform.infrastructure.codes.service.CodeValueReadPlatformService;
import org.mifosplatform.infrastructure.core.data.EnumOptionData;
@@ -53,6 +54,7 @@ import org.mifosplatform.portfolio.group.service.SearchParameters;
import org.mifosplatform.portfolio.loanaccount.data.DisbursementData;
import org.mifosplatform.portfolio.loanaccount.data.LoanAccountData;
import org.mifosplatform.portfolio.loanaccount.data.LoanApplicationTimelineData;
import org.mifosplatform.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.mifosplatform.portfolio.loanaccount.data.LoanStatusEnumData;
import org.mifosplatform.portfolio.loanaccount.data.LoanSummaryData;
import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData;
@@ -62,6 +64,7 @@ import org.mifosplatform.portfolio.loanaccount.data.RepaymentScheduleRelatedLoan
import org.mifosplatform.portfolio.loanaccount.domain.Loan;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepository;
import org.mifosplatform.portfolio.loanaccount.domain.LoanStatus;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTermVariationType;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionRepository;
@@ -1305,4 +1308,69 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
}
@Override
public Collection<LoanScheduleAccrualData> retriveScheduleAccrualData() {
LoanScheduleAccrualMapper mapper = new LoanScheduleAccrualMapper();
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder
.append("select ")
.append(mapper.schema())
.append(" where ((if(ls.fee_charges_waived_derived is null , ls.fee_charges_amount,(ls.fee_charges_amount-ls.fee_charges_waived_derived)) <> if(ls.accrual_fee_charges_derived is null,0, ls.accrual_fee_charges_derived))")
.append(" or (if(ls.penalty_charges_waived_derived is null , ls.penalty_charges_amount,(ls.penalty_charges_amount-ls.penalty_charges_waived_derived)) <> if(ls.accrual_penalty_charges_derived is null,0,ls.accrual_penalty_charges_derived))")
.append(" or (if(ls.interest_waived_derived is null , ls.interest_amount,(ls.interest_amount-ls.interest_waived_derived)) <> if(ls.accrual_interest_derived is null,0,ls.accrual_interest_derived)))")
.append(" and loan.loan_status_id=? and mpl.accounting_type=?");
return this.jdbcTemplate.query(sqlBuilder.toString(), mapper, new Object[] { LoanStatus.ACTIVE.getValue(),
AccountingRuleType.ACCRUAL_BASED.getValue() });
}
private static final class LoanScheduleAccrualMapper implements RowMapper<LoanScheduleAccrualData> {
public String schema() {
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder
.append("loan.id as loanId ,if(loan.client_id is null,mg.office_id,mc.office_id) as officeId,")
.append("ls.duedate as duedate,ls.id as scheduleId,loan.product_id as productId,")
.append("if(ls.interest_waived_derived is null,ls.interest_amount,(ls.interest_amount-ls.interest_waived_derived)) as interest,")
.append("if(ls.penalty_charges_waived_derived is null , ls.penalty_charges_amount,(ls.penalty_charges_amount-ls.penalty_charges_waived_derived)) as penalty,")
.append("if(ls.fee_charges_waived_derived is null , ls.fee_charges_amount,(ls.fee_charges_amount-ls.fee_charges_waived_derived)) as charges,")
.append("ls.accrual_interest_derived as accinterest,ls.accrual_fee_charges_derived as accfeecharege,ls.accrual_penalty_charges_derived as accpenalty,")
.append(" loan.currency_code as currencyCode,loan.currency_digits as currencyDigits,loan.currency_multiplesof as inMultiplesOf,")
.append("curr.display_symbol as currencyDisplaySymbol,curr.name as currencyName,curr.internationalized_name_code as currencyNameCode")
.append(" from m_loan_repayment_schedule ls ").append(" left join m_loan loan on loan.id=ls.loan_id ")
.append(" left join m_product_loan mpl on mpl.id = loan.product_id")
.append(" left join m_client mc on mc.id = loan.client_id ").append(" left join m_group mg on mg.id = loan.group_id")
.append(" left join m_currency curr on curr.code = loan.currency_code");
return sqlBuilder.toString();
}
@Override
public LoanScheduleAccrualData mapRow(ResultSet rs, @SuppressWarnings("unused") int rowNum) throws SQLException {
final Long loanId = rs.getLong("loanId");
final Long officeId = rs.getLong("officeId");
final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "duedate");
final Long repaymentScheduleId = rs.getLong("scheduleId");
final Long loanProductId = rs.getLong("productId");
final BigDecimal interestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "interest");
final BigDecimal feeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "charges");
final BigDecimal penaltyIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "penalty");
final BigDecimal accruedInterestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accinterest");
final BigDecimal accruedFeeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accfeecharege");
final BigDecimal accruedPenaltyIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accpenalty");
final String currencyCode = rs.getString("currencyCode");
final String currencyName = rs.getString("currencyName");
final String currencyNameCode = rs.getString("currencyNameCode");
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
final Integer inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final CurrencyData currencyData = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf,
currencyDisplaySymbol, currencyNameCode);
return new LoanScheduleAccrualData(loanId, officeId, dueDate, repaymentScheduleId, loanProductId, interestIncome, feeIncome,
penaltyIncome, accruedInterestIncome, accruedFeeIncome, accruedPenaltyIncome, currencyData);
}
}
}
@@ -309,6 +309,10 @@ public class LoanEnumerations {
optionData = new LoanTransactionEnumData(LoanTransactionType.CHARGE_PAYMENT.getValue().longValue(),
LoanTransactionType.CHARGE_PAYMENT.getCode(), "Charge Payment");
break;
case ACCRUAL_ACCOUNTING:
optionData = new LoanTransactionEnumData(LoanTransactionType.ACCRUAL_ACCOUNTING.getValue().longValue(),
LoanTransactionType.ACCRUAL_ACCOUNTING.getCode(), "Accrual Accounting");
break;
default:
break;
}