From 44c9e484794e5df666b84815b408afe1c37b4558 Mon Sep 17 00:00:00 2001 From: Binny G Sreevas Date: Fri, 21 Nov 2014 20:41:54 +0530 Subject: [PATCH] Opening Balances: initial commit --- .../glaccount/data/GLAccountData.java | 9 ++ .../api/JournalEntriesApiResource.java | 21 ++- .../journalentry/data/JournalEntryData.java | 38 +++++ .../data/OfficeOpeningBalancesData.java | 51 +++++++ .../journalentry/domain/JournalEntry.java | 4 + .../domain/JournalEntryRepository.java | 6 + .../DefineOpeningBalanceCommandHandler.java | 25 +++ .../JournalEntryReadPlatformService.java | 3 + .../JournalEntryReadPlatformServiceImpl.java | 142 +++++++++++++++++- .../JournalEntryWritePlatformService.java | 3 + ...WritePlatformServiceJpaRepositoryImpl.java | 132 +++++++++++++++- .../commands/domain/CommandWrapper.java | 8 + .../service/CommandWrapperBuilder.java | 16 ++ .../SynchronousCommandProcessingService.java | 2 + .../domain/ConfigurationDomainService.java | 2 + .../domain/ConfigurationDomainServiceJpa.java | 7 + .../core_db/V219__opening_balaces_setup.sql | 34 +++++ 17 files changed, 497 insertions(+), 6 deletions(-) create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/OfficeOpeningBalancesData.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/handler/DefineOpeningBalanceCommandHandler.java create mode 100644 mifosng-provider/src/main/resources/sql/migrations/core_db/V219__opening_balaces_setup.sql diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountData.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountData.java index 017c23064..7cc8d1c2e 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountData.java @@ -173,4 +173,13 @@ public class GLAccountData { return this.glCode; } + public EnumOptionData getType() { + return this.type; + } + + public Integer getTypeId() { + if (this.type != null) { return this.type.getId().intValue(); } + return null; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/api/JournalEntriesApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/api/JournalEntriesApiResource.java index 683405a88..c670a2f02 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/api/JournalEntriesApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/api/JournalEntriesApiResource.java @@ -24,6 +24,7 @@ import javax.ws.rs.core.UriInfo; import org.apache.commons.lang.StringUtils; import org.mifosplatform.accounting.journalentry.data.JournalEntryAssociationParametersData; import org.mifosplatform.accounting.journalentry.data.JournalEntryData; +import org.mifosplatform.accounting.journalentry.data.OfficeOpeningBalancesData; import org.mifosplatform.accounting.journalentry.service.JournalEntryReadPlatformService; import org.mifosplatform.commands.domain.CommandWrapper; import org.mifosplatform.commands.service.CommandWrapperBuilder; @@ -53,7 +54,7 @@ public class JournalEntriesApiResource { private final String resourceNameForPermission = "JOURNALENTRY"; private final JournalEntryReadPlatformService journalEntryReadPlatformService; - private final DefaultToApiJsonSerializer apiJsonSerializerService; + private final DefaultToApiJsonSerializer apiJsonSerializerService; private final ApiRequestParameterHelper apiRequestParameterHelper; private final PlatformSecurityContext context; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; @@ -61,7 +62,7 @@ public class JournalEntriesApiResource { @Autowired public JournalEntriesApiResource(final PlatformSecurityContext context, final JournalEntryReadPlatformService journalEntryReadPlatformService, - final DefaultToApiJsonSerializer toApiJsonSerializer, + final DefaultToApiJsonSerializer toApiJsonSerializer, final ApiRequestParameterHelper apiRequestParameterHelper, final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService) { this.context = context; @@ -131,6 +132,10 @@ public class JournalEntriesApiResource { final CommandWrapper commandRequest = new CommandWrapperBuilder().updateRunningBalanceForJournalEntry() .withJson(jsonRequestBody).build(); result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "defineOpeningBalance")) { + final CommandWrapper commandRequest = new CommandWrapperBuilder().defineOpeningBalanceForJournalEntry() + .withJson(jsonRequestBody).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); } else { final CommandWrapper commandRequest = new CommandWrapperBuilder().createJournalEntry().withJson(jsonRequestBody).build(); result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); @@ -155,6 +160,18 @@ public class JournalEntriesApiResource { return this.apiJsonSerializerService.serialize(result); } + @GET + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Path("openingbalance") + public String retrieveOpeningBalance(@Context final UriInfo uriInfo, @QueryParam("officeId") final Long officeId) { + + this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermission); + final OfficeOpeningBalancesData officeOpeningBalancesData = this.journalEntryReadPlatformService.retrieveOfficeOpeningBalances(officeId); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.apiJsonSerializerService.serialize(settings, officeOpeningBalancesData); + } + private boolean is(final String commandParam, final String commandValue) { return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/JournalEntryData.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/JournalEntryData.java index 5a26ed0d6..d23168422 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/JournalEntryData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/JournalEntryData.java @@ -8,6 +8,7 @@ package org.mifosplatform.accounting.journalentry.data; import java.math.BigDecimal; import org.joda.time.LocalDate; +import org.mifosplatform.accounting.glaccount.data.GLAccountData; import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.organisation.monetary.data.CurrencyData; @@ -99,6 +100,39 @@ public class JournalEntryData { this.currency = currency; } + public static JournalEntryData fromGLAccountData(final GLAccountData glAccountData) { + + final Long id = null; + final Long officeId = null; + final String officeName = null; + final String glAccountName = glAccountData.getName(); + final Long glAccountId = glAccountData.getId(); + final String glAccountCode = glAccountData.getGlCode(); + final EnumOptionData glAccountClassification = glAccountData.getType(); + final LocalDate transactionDate = null; + final EnumOptionData entryType = null; + final BigDecimal amount = null; + final String transactionId = null; + final Boolean manualEntry = null; + final EnumOptionData entityType = null; + final Long entityId = null; + final Long createdByUserId = null; + final LocalDate createdDate = null; + final String createdByUserName = null; + final String comments = null; + final Boolean reversed = null; + final String referenceNumber = null; + final BigDecimal officeRunningBalance = null; + final BigDecimal organizationRunningBalance = null; + final Boolean runningBalanceComputed = null; + final TransactionDetailData transactionDetailData = null; + final CurrencyData currency = null; + return new JournalEntryData(id, officeId, officeName, glAccountName, glAccountId, glAccountCode, glAccountClassification, + transactionDate, entryType, amount, transactionId, manualEntry, entityType, entityId, createdByUserId, createdDate, + createdByUserName, comments, reversed, referenceNumber, officeRunningBalance, organizationRunningBalance, + runningBalanceComputed, transactionDetailData, currency); + } + public Long getId() { return this.id; } @@ -122,4 +156,8 @@ public class JournalEntryData { public Long getOfficeId() { return this.officeId; } + + public String getTransactionId() { + return transactionId; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/OfficeOpeningBalancesData.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/OfficeOpeningBalancesData.java new file mode 100644 index 000000000..46c75f4bf --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/OfficeOpeningBalancesData.java @@ -0,0 +1,51 @@ +package org.mifosplatform.accounting.journalentry.data; + +import java.util.List; + +import org.joda.time.LocalDate; +import org.mifosplatform.accounting.glaccount.data.GLAccountData; + +public class OfficeOpeningBalancesData { + + @SuppressWarnings("unused") + private final Long officeId; + @SuppressWarnings("unused") + private final String officeName; + @SuppressWarnings("unused") + private final LocalDate transactionDate; + @SuppressWarnings("unused") + private final GLAccountData contraAccount; + @SuppressWarnings("unused") + private final List assetAccountOpeningBalances; + @SuppressWarnings("unused") + private final List liabityAccountOpeningBalances; + @SuppressWarnings("unused") + private final List incomeAccountOpeningBalances; + @SuppressWarnings("unused") + private final List equityAccountOpeningBalances; + @SuppressWarnings("unused") + private final List expenseAccountOpeningBalances; + + private OfficeOpeningBalancesData(final Long officeId, final String officeName, final LocalDate transactionDate, + final GLAccountData contraAccount, final List assetAccountOpeningBalances, + final List liabityAccountOpeningBalances, final List incomeAccountOpeningBalances, + final List equityAccountOpeningBalances, final List expenseAccountOpeningBalances) { + this.officeId = officeId; + this.officeName = officeName; + this.transactionDate = transactionDate; + this.contraAccount = contraAccount; + this.assetAccountOpeningBalances = assetAccountOpeningBalances; + this.liabityAccountOpeningBalances = liabityAccountOpeningBalances; + this.incomeAccountOpeningBalances = incomeAccountOpeningBalances; + this.equityAccountOpeningBalances = equityAccountOpeningBalances; + this.expenseAccountOpeningBalances = expenseAccountOpeningBalances; + } + + public static OfficeOpeningBalancesData createNew(final Long officeId, final String officeName, final LocalDate transactionDate, + final GLAccountData contraAccount, final List assetAccountOpeningBalances, + final List liabityAccountOpeningBalances, final List incomeAccountOpeningBalances, + final List equityAccountOpeningBalances, final List expenseAccountOpeningBalances) { + return new OfficeOpeningBalancesData(officeId, officeName, transactionDate, contraAccount, assetAccountOpeningBalances, + liabityAccountOpeningBalances, incomeAccountOpeningBalances, equityAccountOpeningBalances, expenseAccountOpeningBalances); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/domain/JournalEntry.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/domain/JournalEntry.java index 0643b1fd3..912e0523f 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/domain/JournalEntry.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/domain/JournalEntry.java @@ -177,4 +177,8 @@ public class JournalEntry extends AbstractAuditableCustom { return this.paymentDetail; } + public String getTransactionId() { + return transactionId; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/domain/JournalEntryRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/domain/JournalEntryRepository.java index 1bfddc564..c43fea5cb 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/domain/JournalEntryRepository.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/domain/JournalEntryRepository.java @@ -17,4 +17,10 @@ public interface JournalEntryRepository extends JpaRepository findUnReversedManualJournalEntriesByTransactionId(@Param("transactionId") String transactionId); + + @Query("select DISTINCT j.transactionId from JournalEntry j where j.transactionId not in (select DISTINCT je.transactionId from JournalEntry je where je.glAccount.id = :contraId)") + List findNonContraTansactionIds(@Param("contraId") Long contraId); + + @Query("select DISTINCT j.transactionId from JournalEntry j where j.office.id = :officeId and j.glAccount.id = :contraId and j.reversed is false and j.transactionId not in (select DISTINCT je.reversalJournalEntry.transactionId from JournalEntry je where je.reversed is true)") + List findNonReversedContraTansactionIds(@Param("contraId") Long contraId, @Param("officeId") Long officeId); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/handler/DefineOpeningBalanceCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/handler/DefineOpeningBalanceCommandHandler.java new file mode 100644 index 000000000..e90b3188d --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/handler/DefineOpeningBalanceCommandHandler.java @@ -0,0 +1,25 @@ +package org.mifosplatform.accounting.journalentry.handler; + +import org.mifosplatform.accounting.journalentry.service.JournalEntryWritePlatformService; +import org.mifosplatform.commands.handler.NewCommandSourceHandler; +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class DefineOpeningBalanceCommandHandler implements NewCommandSourceHandler { + + private final JournalEntryWritePlatformService writePlatformService; + + @Autowired + public DefineOpeningBalanceCommandHandler(final JournalEntryWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Override + public CommandProcessingResult processCommand(JsonCommand command) { + return this.writePlatformService.defineOpeningBalance(command); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryReadPlatformService.java index c581ab149..567252549 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryReadPlatformService.java @@ -9,6 +9,7 @@ import java.util.Date; import org.mifosplatform.accounting.journalentry.data.JournalEntryAssociationParametersData; import org.mifosplatform.accounting.journalentry.data.JournalEntryData; +import org.mifosplatform.accounting.journalentry.data.OfficeOpeningBalancesData; import org.mifosplatform.infrastructure.core.service.Page; import org.mifosplatform.portfolio.group.service.SearchParameters; @@ -19,4 +20,6 @@ public interface JournalEntryReadPlatformService { Page retrieveAll(SearchParameters searchParameters, Long glAccountId, Boolean onlyManualEntries, Date fromDate, Date toDate, String transactionId, Integer entityType, JournalEntryAssociationParametersData associationParametersData); + OfficeOpeningBalancesData retrieveOfficeOpeningBalances(Long officeId); + } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java index 7cfaed464..04935e1f7 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java @@ -10,24 +10,35 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Arrays; import java.util.Date; +import java.util.List; import org.apache.commons.lang.StringUtils; import org.joda.time.LocalDate; import org.mifosplatform.accounting.common.AccountingEnumerations; +import org.mifosplatform.accounting.glaccount.data.GLAccountData; +import org.mifosplatform.accounting.glaccount.domain.GLAccountType; +import org.mifosplatform.accounting.glaccount.service.GLAccountReadPlatformService; import org.mifosplatform.accounting.journalentry.data.JournalEntryAssociationParametersData; import org.mifosplatform.accounting.journalentry.data.JournalEntryData; +import org.mifosplatform.accounting.journalentry.data.OfficeOpeningBalancesData; import org.mifosplatform.accounting.journalentry.data.TransactionDetailData; import org.mifosplatform.accounting.journalentry.data.TransactionTypeEnumData; import org.mifosplatform.accounting.journalentry.exception.JournalEntriesNotFoundException; import org.mifosplatform.infrastructure.codes.data.CodeValueData; +import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomainService; import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.infrastructure.core.domain.JdbcSupport; +import org.mifosplatform.infrastructure.core.exception.GeneralPlatformDomainRuleException; +import org.mifosplatform.infrastructure.core.service.DateUtils; import org.mifosplatform.infrastructure.core.service.Page; import org.mifosplatform.infrastructure.core.service.PaginationHelper; import org.mifosplatform.infrastructure.core.service.RoutingDataSource; import org.mifosplatform.organisation.monetary.data.CurrencyData; +import org.mifosplatform.organisation.office.data.OfficeData; +import org.mifosplatform.organisation.office.service.OfficeReadPlatformService; import org.mifosplatform.portfolio.account.PortfolioAccountType; import org.mifosplatform.portfolio.group.service.SearchParameters; import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionEnumData; @@ -41,17 +52,26 @@ import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; @Service public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlatformService { private final JdbcTemplate jdbcTemplate; + private final ConfigurationDomainService configurationDomainService; + private final GLAccountReadPlatformService glAccountReadPlatformService; + private final OfficeReadPlatformService officeReadPlatformService; private final PaginationHelper paginationHelper = new PaginationHelper<>(); @Autowired - public JournalEntryReadPlatformServiceImpl(final RoutingDataSource dataSource) { + public JournalEntryReadPlatformServiceImpl(final RoutingDataSource dataSource, + final ConfigurationDomainService configurationDomainService, final GLAccountReadPlatformService glAccountReadPlatformService, + final OfficeReadPlatformService officeReadPlatformService) { this.jdbcTemplate = new JdbcTemplate(dataSource); + this.configurationDomainService = configurationDomainService; + this.glAccountReadPlatformService = glAccountReadPlatformService; + this.officeReadPlatformService = officeReadPlatformService; } private static final class GLJournalEntryMapper implements RowMapper { @@ -333,4 +353,124 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat } } + @Override + public OfficeOpeningBalancesData retrieveOfficeOpeningBalances(final Long officeId) { + + /** + * Global configuration 'office-opening-balances-contra-account' + * property value must not be null and it should be a equity account. + */ + final Long contraId = this.configurationDomainService.retrieveOpeningBalancesContraAccount(); + if (contraId == null) { throw new GeneralPlatformDomainRuleException( + "error.msg.configuration.opening.balance.contra.account.cannot.be.null", + "Configuration property 'office-opening-balances-contra-account' value can not be null", + "office-opening-balances-contra-account"); } + + final JournalEntryAssociationParametersData associationParametersData = new JournalEntryAssociationParametersData(); + final GLAccountData contraAccount = this.glAccountReadPlatformService.retrieveGLAccountById(contraId, associationParametersData); + if (!GLAccountType.fromInt(contraAccount.getTypeId()).isEquityType()) { throw new GeneralPlatformDomainRuleException( + "error.msg.configuration.opening.balance.contra.account.value.is.invalid.account.type", + "Global configuration 'office-opening-balances-contra-account' value is not an equity type account", contraId); } + + final OfficeData officeData = this.officeReadPlatformService.retrieveOffice(officeId); + final List allOpeningTransactions = populateAllTransactionsFromGLAccounts(contraId); + final String contraTransactionId = retrieveContraAccountTransactionId(officeId, contraId); + + List existingOpeningBalanceTransactions = new ArrayList<>(); + if (StringUtils.isNotBlank(contraTransactionId)) { + existingOpeningBalanceTransactions = retrieveOfficeBalanceTransactions(officeId, contraTransactionId); + } + final List transactions = populateOpeningBalances(existingOpeningBalanceTransactions, allOpeningTransactions); + final List assetAccountOpeningBalances = new ArrayList<>(); + final List liabityAccountOpeningBalances = new ArrayList<>(); + final List incomeAccountOpeningBalances = new ArrayList<>(); + final List equityAccountOpeningBalances = new ArrayList<>(); + final List expenseAccountOpeningBalances = new ArrayList<>(); + + for (final JournalEntryData journalEntryData : transactions) { + final GLAccountType type = GLAccountType.fromInt(journalEntryData.getGlAccountType().getId().intValue()); + if (type.isAssetType()) { + assetAccountOpeningBalances.add(journalEntryData); + } else if (type.isLiabilityType()) { + liabityAccountOpeningBalances.add(journalEntryData); + } else if (type.isEquityType()) { + equityAccountOpeningBalances.add(journalEntryData); + } else if (type.isIncomeType()) { + incomeAccountOpeningBalances.add(journalEntryData); + } else if (type.isExpenseType()) { + expenseAccountOpeningBalances.add(journalEntryData); + } + } + + final LocalDate transactionDate = DateUtils.getLocalDateOfTenant(); + + final OfficeOpeningBalancesData officeOpeningBalancesData = OfficeOpeningBalancesData.createNew(officeId, officeData.name(), + transactionDate, contraAccount, assetAccountOpeningBalances, liabityAccountOpeningBalances, incomeAccountOpeningBalances, + equityAccountOpeningBalances, expenseAccountOpeningBalances); + return officeOpeningBalancesData; + } + + private List populateOpeningBalances(final List existingOpeningBalanceTransactions, + final List allOpeningTransactions) { + final List allOpeningBalnceTransactions = new ArrayList<>(allOpeningTransactions.size()); + for (final JournalEntryData newOpeningBalanceTransaction : allOpeningTransactions) { + boolean isNewTransactionAddedToCollection = false; + for (final JournalEntryData existingOpeningBalanceTransaction : existingOpeningBalanceTransactions) { + if (newOpeningBalanceTransaction.getGlAccountId().equals(existingOpeningBalanceTransaction.getGlAccountId())) { + allOpeningBalnceTransactions.add(existingOpeningBalanceTransaction); + isNewTransactionAddedToCollection = true; + break; + } + } + if (!isNewTransactionAddedToCollection) { + allOpeningBalnceTransactions.add(newOpeningBalanceTransaction); + } + } + return allOpeningBalnceTransactions; + } + + private List populateAllTransactionsFromGLAccounts(final Long contraId) { + final List glAccounts = this.glAccountReadPlatformService.retrieveAllEnabledDetailGLAccounts(); + final List openingBalanceTransactions = new ArrayList<>(glAccounts.size()); + + for (final GLAccountData glAccountData : glAccounts) { + if (!contraId.equals(glAccountData.getId())) { + final JournalEntryData openingBalanceTransaction = JournalEntryData.fromGLAccountData(glAccountData); + openingBalanceTransactions.add(openingBalanceTransaction); + } + } + return openingBalanceTransactions; + } + + private List retrieveOfficeBalanceTransactions(final Long officeId, final String transactionId) { + final Long contraId = null; + return retrieveContraTransactions(officeId, contraId, transactionId).getPageItems(); + } + + private String retrieveContraAccountTransactionId(final Long officeId, final Long contraId) { + final String transactionId = ""; + final Page contraJournalEntries = retrieveContraTransactions(officeId, contraId, transactionId); + if (!CollectionUtils.isEmpty(contraJournalEntries.getPageItems())) { + final JournalEntryData contraTransaction = contraJournalEntries.getPageItems().get(contraJournalEntries.getPageItems().size()-1); + return contraTransaction.getTransactionId(); + } + return transactionId; + } + + private Page retrieveContraTransactions(final Long officeId, final Long contraId, final String transactionId) { + final Integer offset = 0; + final Integer limit = null; + final String orderBy = "journalEntry.id"; + final String sortOrder = "ASC"; + final Integer entityType = null; + final Boolean onlyManualEntries = null; + final Date fromDate = null; + final Date toDate = null; + final JournalEntryAssociationParametersData associationParametersData = null; + + final SearchParameters searchParameters = SearchParameters.forJournalEntries(officeId, offset, limit, orderBy, sortOrder); + return retrieveAll(searchParameters, contraId, onlyManualEntries, fromDate, toDate, transactionId, entityType, + associationParametersData); + + } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryWritePlatformService.java index ebb3055db..1fd51a739 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryWritePlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryWritePlatformService.java @@ -19,4 +19,7 @@ public interface JournalEntryWritePlatformService { void createJournalEntriesForLoan(Map accountingBridgeData); void createJournalEntriesForSavings(Map accountingBridgeData); + + CommandProcessingResult defineOpeningBalance(JsonCommand command); + } 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 64ea21d64..4fd4ded0c 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 @@ -19,6 +19,7 @@ import org.mifosplatform.accounting.closure.domain.GLClosureRepository; import org.mifosplatform.accounting.glaccount.data.GLAccountDataForLookup; import org.mifosplatform.accounting.glaccount.domain.GLAccount; import org.mifosplatform.accounting.glaccount.domain.GLAccountRepository; +import org.mifosplatform.accounting.glaccount.domain.GLAccountType; import org.mifosplatform.accounting.glaccount.exception.GLAccountNotFoundException; import org.mifosplatform.accounting.glaccount.service.GLAccountReadPlatformService; import org.mifosplatform.accounting.journalentry.api.JournalEntryJsonInputParams; @@ -36,9 +37,11 @@ import org.mifosplatform.accounting.journalentry.serialization.JournalEntryComma import org.mifosplatform.accounting.rule.domain.AccountingRule; import org.mifosplatform.accounting.rule.domain.AccountingRuleRepository; import org.mifosplatform.accounting.rule.exception.AccountingRuleNotFoundException; +import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomainService; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder; +import org.mifosplatform.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.mifosplatform.organisation.office.domain.Office; @@ -54,6 +57,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; @Service public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements JournalEntryWritePlatformService { @@ -73,6 +77,7 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa private final OrganisationCurrencyRepositoryWrapper organisationCurrencyRepository; private final PlatformSecurityContext context; private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; + private final ConfigurationDomainService configurationDomainService; @Autowired public JournalEntryWritePlatformServiceJpaRepositoryImpl(final GLClosureRepository glClosureRepository, @@ -83,7 +88,8 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa final AccountingProcessorForSavingsFactory accountingProcessorForSavingsFactory, final GLAccountReadPlatformService glAccountReadPlatformService, final OrganisationCurrencyRepositoryWrapper organisationCurrencyRepository, final PlatformSecurityContext context, - final PaymentDetailWritePlatformService paymentDetailWritePlatformService) { + final PaymentDetailWritePlatformService paymentDetailWritePlatformService, + final ConfigurationDomainService configurationDomainService) { this.glClosureRepository = glClosureRepository; this.officeRepository = officeRepository; this.glJournalEntryRepository = glJournalEntryRepository; @@ -97,6 +103,7 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa this.organisationCurrencyRepository = organisationCurrencyRepository; this.context = context; this.paymentDetailWritePlatformService = paymentDetailWritePlatformService; + this.configurationDomainService = configurationDomainService; } @Transactional @@ -276,6 +283,12 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa .getTransactionId()); if (journalEntries.size() <= 1) { throw new JournalEntriesNotFoundException(command.getTransactionId()); } + final String reversalTransactionId = revertJournalEntry(journalEntries); + + return new CommandProcessingResultBuilder().withTransactionId(reversalTransactionId).build(); + } + + private String revertJournalEntry(List journalEntries) { final Long officeId = journalEntries.get(0).getOffice().getId(); final String reversalTransactionId = generateTransactionId(officeId); final boolean manualEntry = true; @@ -283,7 +296,7 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa for (final JournalEntry journalEntry : journalEntries) { JournalEntry reversalJournalEntry; final String reversalComment = "Reversal entry for Journal Entry with Entry Id :" + journalEntry.getId() - + " and transaction Id " + command.getTransactionId(); + + " and transaction Id " + journalEntry.getTransactionId(); if (journalEntry.isDebitEntry()) { reversalJournalEntry = JournalEntry.createNew(journalEntry.getOffice(), journalEntry.getPaymentDetails(), journalEntry.getGlAccount(), journalEntry.getCurrencyCode(), reversalTransactionId, manualEntry, @@ -302,7 +315,7 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa // save the updated journal entry this.glJournalEntryRepository.saveAndFlush(journalEntry); } - return new CommandProcessingResultBuilder().withTransactionId(reversalTransactionId).build(); + return null; } @Transactional @@ -410,4 +423,117 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa "Unknown data integrity issue with resource Journal Entry: " + realCause.getMessage()); } + @Transactional + @Override + public CommandProcessingResult defineOpeningBalance(final JsonCommand command) { + try { + final JournalEntryCommand journalEntryCommand = this.fromApiJsonDeserializer.commandFromApiJson(command.json()); + journalEntryCommand.validateForCreate(); + + /** + * Global configuration 'office-opening-balances-contra-account' + * property value must not be null and it should be a equity + * account. + */ + final Long contraId = this.configurationDomainService.retrieveOpeningBalancesContraAccount(); + if (contraId == null) { throw new GeneralPlatformDomainRuleException( + "error.msg.configuration.opening.balance.contra.account.cannot.be.null", + "Configuration property 'office-opening-balances-contra-account' value can not be null", + "office-opening-balances-contra-account"); } + + validateJournalEntriesArePostedBefore(contraId); + + // check office is valid + final Long officeId = command.longValueOfParameterNamed(JournalEntryJsonInputParams.OFFICE_ID.getValue()); + final Office office = this.officeRepository.findOne(officeId); + if (office == null) { throw new OfficeNotFoundException(officeId); } + + final String currencyCode = command.stringValueOfParameterNamed(JournalEntryJsonInputParams.CURRENCY_CODE.getValue()); + + validateBusinessRulesForJournalEntries(journalEntryCommand); + + /** + * revert old journal entries + */ + final List transactionIdsToBeReversed = this.glJournalEntryRepository.findNonReversedContraTansactionIds(contraId, + officeId); + for (String transactionId : transactionIdsToBeReversed) { + final List journalEntries = this.glJournalEntryRepository + .findUnReversedManualJournalEntriesByTransactionId(transactionId); + revertJournalEntry(journalEntries); + } + + /** Set a transaction Id and save these Journal entries **/ + final Date transactionDate = command.DateValueOfParameterNamed(JournalEntryJsonInputParams.TRANSACTION_DATE.getValue()); + final String transactionId = generateTransactionId(officeId); + + saveAllDebitOrCreditOpeningBalanceEntries(journalEntryCommand, office, currencyCode, transactionDate, + journalEntryCommand.getDebits(), transactionId, JournalEntryType.DEBIT, contraId); + + saveAllDebitOrCreditOpeningBalanceEntries(journalEntryCommand, office, currencyCode, transactionDate, + journalEntryCommand.getCredits(), transactionId, JournalEntryType.CREDIT, contraId); + + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withOfficeId(officeId) + .withTransactionId(transactionId).build(); + } catch (final DataIntegrityViolationException dve) { + handleJournalEntryDataIntegrityIssues(dve); + return null; + } + } + + private void saveAllDebitOrCreditOpeningBalanceEntries(final JournalEntryCommand command, final Office office, + final String currencyCode, final Date transactionDate, + final SingleDebitOrCreditEntryCommand[] singleDebitOrCreditEntryCommands, final String transactionId, + final JournalEntryType type, final Long contraAccountId) { + + final boolean manualEntry = true; + final GLAccount contraAccount = this.glAccountRepository.findOne(contraAccountId); + if (contraAccount == null) { throw new GLAccountNotFoundException(contraAccountId); } + if (!GLAccountType.fromInt(contraAccount.getType()).isEquityType()) { throw new GeneralPlatformDomainRuleException( + "error.msg.configuration.opening.balance.contra.account.value.is.invalid.account.type", + "Global configuration 'office-opening-balances-contra-account' value is not an equity type account", contraAccountId); } + validateGLAccountForTransaction(contraAccount); + final JournalEntryType contraType = getContraType(type); + String comments = command.getComments(); + + /** Validate current code is appropriate **/ + this.organisationCurrencyRepository.findOneWithNotFoundDetection(currencyCode); + + for (final SingleDebitOrCreditEntryCommand singleDebitOrCreditEntryCommand : singleDebitOrCreditEntryCommands) { + final GLAccount glAccount = this.glAccountRepository.findOne(singleDebitOrCreditEntryCommand.getGlAccountId()); + if (glAccount == null) { throw new GLAccountNotFoundException(singleDebitOrCreditEntryCommand.getGlAccountId()); } + + validateGLAccountForTransaction(glAccount); + + if (!StringUtils.isBlank(singleDebitOrCreditEntryCommand.getComments())) { + comments = singleDebitOrCreditEntryCommand.getComments(); + } + + final JournalEntry glJournalEntry = JournalEntry.createNew(office, null, glAccount, currencyCode, transactionId, manualEntry, + transactionDate, type, singleDebitOrCreditEntryCommand.getAmount(), comments, null, null, null, null, null); + this.glJournalEntryRepository.saveAndFlush(glJournalEntry); + + final JournalEntry contraEntry = JournalEntry.createNew(office, null, contraAccount, currencyCode, transactionId, manualEntry, + transactionDate, contraType, singleDebitOrCreditEntryCommand.getAmount(), comments, null, null, null, null, null); + this.glJournalEntryRepository.saveAndFlush(contraEntry); + } + } + + private JournalEntryType getContraType(final JournalEntryType type) { + final JournalEntryType contraType; + if (type.isCreditType()) { + contraType = JournalEntryType.DEBIT; + } else { + contraType = JournalEntryType.CREDIT; + } + return contraType; + } + + private void validateJournalEntriesArePostedBefore(final Long contraId) { + final List transactionIds = this.glJournalEntryRepository.findNonContraTansactionIds(contraId); + if (!CollectionUtils.isEmpty(transactionIds)) { throw new GeneralPlatformDomainRuleException( + "error.msg.journalentry.defining.openingbalance.not.allowed", + "Defining Opening balances not allowed after journal entries posted", transactionIds); } + } + } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java index 6c2f3aa4b..a0bb48747 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java @@ -312,6 +312,14 @@ public class CommandWrapper { return this.actionName.equalsIgnoreCase("UPDATERUNNINGBALANCE") && this.entityName.equalsIgnoreCase("JOURNALENTRY"); } + public boolean isDefineOpeningalance() { + return this.actionName.equalsIgnoreCase("DEFINEOPENINGBALANCE") && this.entityName.equalsIgnoreCase("JOURNALENTRY"); + } + + public boolean isUpdateOpeningbalance() { + return this.actionName.equalsIgnoreCase("UPDATEOPENINGBALANCE") && this.entityName.equalsIgnoreCase("JOURNALENTRY"); + } + public boolean isFundResource() { return this.entityName.equalsIgnoreCase("FUND"); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java index 9f15a214e..a1e38db08 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java @@ -890,6 +890,22 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder defineOpeningBalanceForJournalEntry() { + this.actionName = "DEFINEOPENINGBALANCE"; + this.entityName = "JOURNALENTRY"; + this.entityId = null; + this.href = "/journalentries/update"; + return this; + } + + public CommandWrapperBuilder updateOpeningBalanceForJournalEntry() { + this.actionName = "UPDATEOPENINGBALANCE"; + this.entityName = "JOURNALENTRY"; + this.entityId = null; + this.href = "/journalentries/update"; + return this; + } + public CommandWrapperBuilder createSavingProduct() { this.actionName = "CREATE"; this.entityName = "SAVINGSPRODUCT"; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java index bb146845e..3a1b9989f 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java @@ -435,6 +435,8 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer handler = this.applicationContext.getBean("reverseJournalEntryCommandHandler", NewCommandSourceHandler.class); } else if (wrapper.isUpdateRunningbalance()) { handler = this.applicationContext.getBean("updateRunningBalanceCommandHandler", NewCommandSourceHandler.class); + } else if (wrapper.isDefineOpeningalance()) { + handler = this.applicationContext.getBean("defineOpeningBalanceCommandHandler", NewCommandSourceHandler.class); } else { throw new UnsupportedCommandException(wrapper.commandName()); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainService.java index e7146a302..7be6f157a 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainService.java @@ -35,6 +35,8 @@ public interface ConfigurationDomainService { Long retrieveGraceOnPenaltyPostingPeriod(); + Long retrieveOpeningBalancesContraAccount(); + boolean isSavingsInterestPostingAtCurrentPeriodEnd(); Integer retrieveFinancialYearBeginningMonth(); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java index 3b025b6dd..d57443dfa 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java @@ -135,6 +135,13 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService return property.getValue(); } + @Override + public Long retrieveOpeningBalancesContraAccount() { + final String propertyName = "office-opening-balances-contra-account"; + final GlobalConfigurationProperty property = this.globalConfigurationRepository.findOneByNameWithNotFoundDetection(propertyName); + return property.getValue(); + } + @Override public boolean isSavingsInterestPostingAtCurrentPeriodEnd() { final String propertyName = "savings-interest-posting-current-period-end"; diff --git a/mifosng-provider/src/main/resources/sql/migrations/core_db/V219__opening_balaces_setup.sql b/mifosng-provider/src/main/resources/sql/migrations/core_db/V219__opening_balaces_setup.sql new file mode 100644 index 000000000..a28a2c20b --- /dev/null +++ b/mifosng-provider/src/main/resources/sql/migrations/core_db/V219__opening_balaces_setup.sql @@ -0,0 +1,34 @@ +-- Example: INSERT INTO `acc_gl_account` +-- ( +-- `name`, `parent_id`, `hierarchy`, `gl_code`, `disabled`, `manual_journal_entries_allowed`, +-- `account_usage`, `classification_enum`, `tag_id`, `description` +-- ) +-- VALUES +-- ( +-- 'Opening Balances Contra Account', NULL, '.', 'OBCA', 0, 1, +-- 1, 3, NULL, NULL +-- ); + +INSERT INTO `c_configuration` + ( + `name`, + `value`, + `enabled` + ) + VALUES + ( + 'office-opening-balances-contra-account', + 0, -- Or Example: (SELECT id FROM acc_gl_account WHERE gl_code = 'OBCA' ), + 1); + +ALTER TABLE `c_configuration` + ADD UNIQUE INDEX `name_UNIQUE` (`name`); + +INSERT INTO `m_permission` + ( + `grouping`, `code`, `entity_name`, `action_name` + ) + VALUES + ( + 'accounting', 'DEFINEOPENINGBALANCE_JOURNALENTRY', 'JOURNALENTRY', 'DEFINEOPENINGBALANCE' + );