blob: 268b2e00125cd43381fb4517c7f625806af718db [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.accounting.journalentry.service;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.fineract.accounting.common.AccountingEnumerations;
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
import org.apache.fineract.accounting.journalentry.api.JournalEntryJsonInputParams;
import org.apache.fineract.accounting.journalentry.data.JournalEntryData;
import org.apache.fineract.accounting.journalentry.data.JournalEntryDataValidator;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryType;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.RoutingDataSource;
import org.apache.fineract.infrastructure.jobs.annotation.CronTarget;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.office.domain.OfficeRepository;
import org.apache.fineract.organisation.office.exception.OfficeNotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
@Service
public class JournalEntryRunningBalanceUpdateServiceImpl implements JournalEntryRunningBalanceUpdateService {
private final static Logger logger = LoggerFactory.getLogger(JournalEntryRunningBalanceUpdateServiceImpl.class);
private final JdbcTemplate jdbcTemplate;
private final OfficeRepository officeRepository;
private final JournalEntryDataValidator dataValidator;
private final FromJsonHelper fromApiJsonHelper;
private final GLJournalEntryMapper entryMapper = new GLJournalEntryMapper();
// if a limit is not added to the running balance select statements below and the resultset is more than 400,000,
// the script will eat up all of the server memory
private final String selectRunningBalanceSqlLimit = "limit 0, 10000";
private final String officeRunningBalanceSql = "select je.office_running_balance as runningBalance,je.account_id as accountId from acc_gl_journal_entry je "
+ "inner join (select max(id) as id from acc_gl_journal_entry where office_id=? and entry_date < ? group by account_id,entry_date) je2 "
+ "inner join (select max(entry_date) as date from acc_gl_journal_entry where office_id=? and entry_date < ? group by account_id) je3 "
+ "where je2.id = je.id and je.entry_date = je3.date group by je.id order by je.entry_date DESC " + selectRunningBalanceSqlLimit;
private final String organizationRunningBalanceSql = "select je.organization_running_balance as runningBalance,je.account_id as accountId from acc_gl_journal_entry je "
+ "inner join (select max(id) as id from acc_gl_journal_entry where entry_date < ? group by account_id,entry_date) je2 "
+ "inner join (select max(entry_date) as date from acc_gl_journal_entry where entry_date < ? group by account_id) je3 "
+ "where je2.id = je.id and je.entry_date = je3.date group by je.id order by je.entry_date DESC " + selectRunningBalanceSqlLimit;
private final String officesRunningBalanceSql = "select je.office_running_balance as runningBalance,je.account_id as accountId,je.office_id as officeId "
+ "from acc_gl_journal_entry je "
+ "inner join (select max(id) as id from acc_gl_journal_entry where entry_date < ? group by office_id,account_id,entry_date) je2 "
+ "inner join (select max(entry_date) as date from acc_gl_journal_entry where entry_date < ? group by office_id,account_id) je3 "
+ "where je2.id = je.id and je.entry_date = je3.date group by je.id order by je.entry_date DESC " + selectRunningBalanceSqlLimit;
@Autowired
public JournalEntryRunningBalanceUpdateServiceImpl(final RoutingDataSource dataSource, final OfficeRepository officeRepository,
final JournalEntryDataValidator dataValidator, final FromJsonHelper fromApiJsonHelper) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.officeRepository = officeRepository;
this.dataValidator = dataValidator;
this.fromApiJsonHelper = fromApiJsonHelper;
}
@Override
@CronTarget(jobName = JobName.ACCOUNTING_RUNNING_BALANCE_UPDATE)
public void updateRunningBalance() {
String dateFinder = "select MIN(je.entry_date) as entityDate from acc_gl_journal_entry je "
+ "where je.is_running_balance_calculated=0 ";
try {
Date entityDate = this.jdbcTemplate.queryForObject(dateFinder, Date.class);
updateOrganizationRunningBalance(entityDate);
} catch (EmptyResultDataAccessException e) {
logger.debug("No results found for updation of running balance ");
}
}
@Override
public CommandProcessingResult updateOfficeRunningBalance(JsonCommand command) {
this.dataValidator.validateForUpdateRunningbalance(command);
final Long officeId = this.fromApiJsonHelper.extractLongNamed(JournalEntryJsonInputParams.OFFICE_ID.getValue(),
command.parsedJson());
CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder().withCommandId(command
.commandId());
if (officeId == null) {
updateRunningBalance();
} else {
final Office office = this.officeRepository.findOne(officeId);
if (office == null) { throw new OfficeNotFoundException(officeId); }
String dateFinder = "select MIN(je.entry_date) as entityDate " + "from acc_gl_journal_entry je "
+ "where je.is_running_balance_calculated=0 and je.office_id=?";
try {
Date entityDate = this.jdbcTemplate.queryForObject(dateFinder, Date.class, officeId);
updateRunningBalance(officeId, entityDate);
} catch (EmptyResultDataAccessException e) {
logger.debug("No results found for updation of office running balance with office id:" + officeId);
}
commandProcessingResultBuilder.withOfficeId(officeId);
}
return commandProcessingResultBuilder.build();
}
private void updateOrganizationRunningBalance(Date entityDate) {
Map<Long, BigDecimal> runningBalanceMap = new HashMap<>(5);
Map<Long, Map<Long, BigDecimal>> officesRunningBalance = new HashMap<>();
List<Map<String, Object>> list = jdbcTemplate.queryForList(organizationRunningBalanceSql, entityDate, entityDate);
for (Map<String, Object> entries : list) {
Long accountId = (Long) entries.get("accountId");
if (!runningBalanceMap.containsKey(accountId)) {
runningBalanceMap.put(accountId, (BigDecimal) entries.get("runningBalance"));
}
}
List<Map<String, Object>> officesRunningBalanceList = jdbcTemplate.queryForList(officesRunningBalanceSql, entityDate, entityDate);
for (Map<String, Object> entries : officesRunningBalanceList) {
Long accountId = (Long) entries.get("accountId");
Long officeId = (Long) entries.get("officeId");
Map<Long, BigDecimal> runningBalance = null;
if (officesRunningBalance.containsKey(officeId)) {
runningBalance = officesRunningBalance.get(officeId);
} else {
runningBalance = new HashMap<>();
officesRunningBalance.put(officeId, runningBalance);
}
if (!runningBalance.containsKey(accountId)) {
runningBalance.put(accountId, (BigDecimal) entries.get("runningBalance"));
}
}
List<JournalEntryData> entryDatas = jdbcTemplate.query(entryMapper.organizationRunningBalanceSchema(), entryMapper,
new Object[] { entityDate });
if (entryDatas.size() > 0) {
// run a batch update of 1000 SQL statements at a time
final Integer batchUpdateSize = 1000;
final Integer batchUpdateSizeMinusOne = batchUpdateSize - 1;
String[] updateSql = new String[batchUpdateSize];
int i = 0;
for (JournalEntryData entryData : entryDatas) {
Map<Long, BigDecimal> officeRunningBalanceMap = null;
if (officesRunningBalance.containsKey(entryData.getOfficeId())) {
officeRunningBalanceMap = officesRunningBalance.get(entryData.getOfficeId());
} else {
officeRunningBalanceMap = new HashMap<>();
officesRunningBalance.put(entryData.getOfficeId(), officeRunningBalanceMap);
}
BigDecimal officeRunningBalance = calculateRunningBalance(entryData, officeRunningBalanceMap);
BigDecimal runningBalance = calculateRunningBalance(entryData, runningBalanceMap);
String sql = "UPDATE acc_gl_journal_entry je SET je.is_running_balance_calculated=1, je.organization_running_balance="
+ runningBalance + ",je.office_running_balance=" + officeRunningBalance + " WHERE je.id=" + entryData.getId();
updateSql[i++] = sql;
if (i == batchUpdateSizeMinusOne) {
// run a batch update of the 1000 update SQL statements
this.jdbcTemplate.batchUpdate(updateSql);
// reset counter and string array
i = 0;
updateSql = new String[batchUpdateSize];
}
}
this.jdbcTemplate.batchUpdate(updateSql);
}
}
private void updateRunningBalance(Long officeId, Date entityDate) {
Map<Long, BigDecimal> runningBalanceMap = new HashMap<>(5);
List<Map<String, Object>> list = jdbcTemplate.queryForList(officeRunningBalanceSql, officeId, entityDate, officeId, entityDate);
for (Map<String, Object> entries : list) {
Long accountId = (Long) entries.get("accountId");
if (!runningBalanceMap.containsKey(accountId)) {
runningBalanceMap.put(accountId, (BigDecimal) entries.get("runningBalance"));
}
}
List<JournalEntryData> entryDatas = jdbcTemplate.query(entryMapper.officeRunningBalanceSchema(), entryMapper, new Object[] {
officeId, entityDate });
String[] updateSql = new String[entryDatas.size()];
int i = 0;
for (JournalEntryData entryData : entryDatas) {
BigDecimal runningBalance = calculateRunningBalance(entryData, runningBalanceMap);
String sql = "UPDATE acc_gl_journal_entry je SET je.office_running_balance=" + runningBalance + " WHERE je.id="
+ entryData.getId();
updateSql[i++] = sql;
}
this.jdbcTemplate.batchUpdate(updateSql);
}
private BigDecimal calculateRunningBalance(JournalEntryData entry, Map<Long, BigDecimal> runningBalanceMap) {
BigDecimal runningBalance = BigDecimal.ZERO;
if (runningBalanceMap.containsKey(entry.getGlAccountId())) {
runningBalance = runningBalanceMap.get(entry.getGlAccountId());
}
GLAccountType accounttype = GLAccountType.fromInt(entry.getGlAccountType().getId().intValue());
JournalEntryType entryType = JournalEntryType.fromInt(entry.getEntryType().getId().intValue());
boolean isIncrease = false;
switch (accounttype) {
case ASSET:
if (entryType.isDebitType()) {
isIncrease = true;
}
break;
case EQUITY:
if (entryType.isCreditType()) {
isIncrease = true;
}
break;
case EXPENSE:
if (entryType.isDebitType()) {
isIncrease = true;
}
break;
case INCOME:
if (entryType.isCreditType()) {
isIncrease = true;
}
break;
case LIABILITY:
if (entryType.isCreditType()) {
isIncrease = true;
}
break;
}
if (isIncrease) {
runningBalance = runningBalance.add(entry.getAmount());
} else {
runningBalance = runningBalance.subtract(entry.getAmount());
}
runningBalanceMap.put(entry.getGlAccountId(), runningBalance);
return runningBalance;
}
private static final class GLJournalEntryMapper implements RowMapper<JournalEntryData> {
public String officeRunningBalanceSchema() {
return "select je.id as id,je.account_id as glAccountId,je.type_enum as entryType,je.amount as amount, "
+ "glAccount.classification_enum as classification,je.office_id as officeId "
+ "from acc_gl_journal_entry je , acc_gl_account glAccount " + "where je.account_id = glAccount.id "
+ "and je.office_id=? and je.entry_date >= ? order by je.entry_date,je.id";
}
public String organizationRunningBalanceSchema() {
return "select je.id as id,je.account_id as glAccountId," + "je.type_enum as entryType,je.amount as amount, "
+ "glAccount.classification_enum as classification,je.office_id as officeId "
+ "from acc_gl_journal_entry je , acc_gl_account glAccount " + "where je.account_id = glAccount.id "
+ "and je.entry_date >= ? order by je.entry_date,je.id";
}
@Override
public JournalEntryData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = rs.getLong("id");
final Long glAccountId = rs.getLong("glAccountId");
final Long officeId = rs.getLong("officeId");
final int accountTypeId = JdbcSupport.getInteger(rs, "classification");
final EnumOptionData accountType = AccountingEnumerations.gLAccountType(accountTypeId);
final BigDecimal amount = rs.getBigDecimal("amount");
final int entryTypeId = JdbcSupport.getInteger(rs, "entryType");
final EnumOptionData entryType = AccountingEnumerations.journalEntryType(entryTypeId);
return new JournalEntryData(id, officeId, null, null, glAccountId, null, accountType, null, entryType, amount, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null);
}
}
}