From fa9f2129dde722183a09f019a1244bf0261e4ee2 Mon Sep 17 00:00:00 2001 From: Vishwas Babu AJ Date: Thu, 13 Dec 2012 02:09:48 +0530 Subject: [PATCH] accounting wip --- .../patch-05_accounting_tables_updates.sql | 40 ++ .../noncore/ReadReportingServiceImpl.java | 411 ++++++++++++++++++ .../accounting/AccountingConstants.java | 12 +- .../accounting/api/GLClosuresApiResource.java | 8 +- .../api/GLJournalEntriesApiResource.java | 123 ++++++ .../api/commands/GLJournalEntryCommand.java | 71 +++ .../SingleDebitOrCreditEntryCommand.java | 52 +++ .../accounting/api/data/GLClosureData.java | 27 +- .../api/data/GLJournalEntryData.java | 76 ++++ .../api/data/JournalEntryIdentifier.java | 43 ++ .../AccountingApiDataConversionService.java | 3 + .../AccountingApiJsonSerializerService.java | 10 + .../api/infrastructure/DateParam.java | 43 ++ ...ccountingApiDataConversionServiceImpl.java | 70 ++- ...sonAccountingApiJsonSerializerService.java | 47 +- .../accounting/domain/GLAccount.java | 2 +- .../accounting/domain/GLClosure.java | 11 +- .../domain/GLClosureRepository.java | 6 +- .../accounting/domain/GLJournalEntry.java | 125 ++++++ .../domain/GLJournalEntryRepository.java | 15 + .../GLJournalEntryRepositoryCustom.java | 8 + .../domain/GLJournalEntryRepositoryImpl.java | 24 + .../GLAccountInvalidDeleteException.java | 7 + .../GLClosureInvalidDeleteException.java | 16 + .../exceptions/GLClosureInvalidException.java | 34 ++ .../GLJournalEntriesNotFoundException.java | 18 + .../GLJournalEntryInvalidException.java | 46 ++ .../service/GLAccountCommandValidator.java | 2 + .../GLJournalEntryCommandValidator.java | 35 ++ .../GLJournalEntryReadPlatformService.java | 15 + .../GLJournalEntryWritePlatformService.java | 14 + .../GLAccountReadPlatformServiceImpl.java | 28 +- ...WritePlatformServiceJpaRepositoryImpl.java | 68 ++- .../GLClosureReadPlatformServiceImpl.java | 29 +- ...WritePlatformServiceJpaRepositoryImpl.java | 26 +- ...GLJournalEntryReadPlatformServiceImpl.java | 151 +++++++ ...WritePlatformServiceJpaRepositoryImpl.java | 199 +++++++++ .../service/ReadReportingServiceImpl.java | 5 +- 38 files changed, 1860 insertions(+), 60 deletions(-) create mode 100755 mifosng-db/patches/patch-05_accounting_tables_updates.sql create mode 100755 mifosng-provider/src/main/java/org/mifosng/platform/noncore/ReadReportingServiceImpl.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/accounting/api/GLJournalEntriesApiResource.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/accounting/api/commands/GLJournalEntryCommand.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/accounting/api/commands/SingleDebitOrCreditEntryCommand.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/accounting/api/data/GLJournalEntryData.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/accounting/api/data/JournalEntryIdentifier.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/accounting/api/infrastructure/DateParam.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLJournalEntry.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLJournalEntryRepository.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLJournalEntryRepositoryCustom.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLJournalEntryRepositoryImpl.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/accounting/exceptions/GLClosureInvalidDeleteException.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/accounting/exceptions/GLClosureInvalidException.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/accounting/exceptions/GLJournalEntriesNotFoundException.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/accounting/exceptions/GLJournalEntryInvalidException.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/accounting/service/GLJournalEntryCommandValidator.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/accounting/service/GLJournalEntryReadPlatformService.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/accounting/service/GLJournalEntryWritePlatformService.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLJournalEntryReadPlatformServiceImpl.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLJournalEntryWritePlatformServiceJpaRepositoryImpl.java diff --git a/mifosng-db/patches/patch-05_accounting_tables_updates.sql b/mifosng-db/patches/patch-05_accounting_tables_updates.sql new file mode 100755 index 000000000..f7a60c03d --- /dev/null +++ b/mifosng-db/patches/patch-05_accounting_tables_updates.sql @@ -0,0 +1,40 @@ +DROP TABLE IF EXISTS `acc_gl_journal_entry`; + +CREATE TABLE `acc_gl_journal_entry` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `account_id` BIGINT(20) NOT NULL, + `office_id` BIGINT(20) NOT NULL, + `reversal_id` BIGINT(20) NULL DEFAULT NULL, + `transaction_id` VARCHAR(50) NOT NULL, + `reversed` TINYINT(1) NOT NULL DEFAULT '0', + `portfolio_generated` TINYINT(1) NOT NULL DEFAULT '0', + `entry_date` DATE NOT NULL, + `type` VARCHAR(50) NOT NULL, + `amount` DECIMAL(19,6) NOT NULL, + `description` VARCHAR(500) NULL DEFAULT NULL, + `entity_type` VARCHAR(50) NULL DEFAULT NULL, + `entity_id` BIGINT(20) NULL DEFAULT NULL, + `createdby_id` BIGINT(20) NOT NULL, + `lastmodifiedby_id` BIGINT(20) NOT NULL, + `created_date` DATETIME NOT NULL, + `lastmodified_date` DATETIME NOT NULL, + PRIMARY KEY (`id`), + INDEX `FK_acc_gl_journal_entry_m_office` (`office_id`), + INDEX `FK_acc_gl_journal_entry_m_appuser` (`createdby_id`), + INDEX `FK_acc_gl_journal_entry_m_appuser_2` (`lastmodifiedby_id`), + INDEX `FK_acc_gl_journal_entry_acc_gl_journal_entry` (`reversal_id`), + INDEX `FK_acc_gl_journal_entry_acc_gl_account` (`account_id`), + CONSTRAINT `FK_acc_gl_journal_entry_acc_gl_account` FOREIGN KEY (`account_id`) REFERENCES `acc_gl_account` (`id`), + CONSTRAINT `FK_acc_gl_journal_entry_acc_gl_journal_entry` FOREIGN KEY (`reversal_id`) REFERENCES `acc_gl_journal_entry` (`id`), + CONSTRAINT `FK_acc_gl_journal_entry_m_appuser` FOREIGN KEY (`createdby_id`) REFERENCES `m_appuser` (`id`), + CONSTRAINT `FK_acc_gl_journal_entry_m_appuser_2` FOREIGN KEY (`lastmodifiedby_id`) REFERENCES `m_appuser` (`id`), + CONSTRAINT `FK_acc_gl_journal_entry_m_office` FOREIGN KEY (`office_id`) REFERENCES `m_office` (`id`) +) +COLLATE='utf8_general_ci' +ENGINE=InnoDB DEFAULT CHARSET=utf8; + +ALTER TABLE `acc_gl_account` + ALTER `description` DROP DEFAULT; +ALTER TABLE `acc_gl_account` + CHANGE COLUMN `description` `description` VARCHAR(500) NULL AFTER `classification`; + diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/noncore/ReadReportingServiceImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/noncore/ReadReportingServiceImpl.java new file mode 100755 index 000000000..c35385846 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/noncore/ReadReportingServiceImpl.java @@ -0,0 +1,411 @@ +package org.mifosplatform.infrastructure.dataqueries.service; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.sql.Date; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.sql.rowset.CachedRowSet; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.StreamingOutput; + +import org.apache.commons.lang.StringUtils; +import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; +import org.mifosplatform.infrastructure.core.service.FileUtils; +import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil; +import org.mifosplatform.infrastructure.dataqueries.data.GenericResultsetData; +import org.mifosplatform.infrastructure.dataqueries.data.ResultsetColumnHeader; +import org.mifosplatform.infrastructure.dataqueries.data.ResultsetDataRow; +import org.mifosplatform.infrastructure.dataqueries.exception.ReportNotFoundException; +import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; +import org.mifosplatform.useradministration.domain.AppUser; +import org.pentaho.reporting.engine.classic.core.ClassicEngineBoot; +import org.pentaho.reporting.engine.classic.core.MasterReport; +import org.pentaho.reporting.engine.classic.core.ReportProcessingException; +import org.pentaho.reporting.engine.classic.core.modules.output.pageable.pdf.PdfReportUtil; +import org.pentaho.reporting.engine.classic.core.modules.output.table.csv.CSVReportUtil; +import org.pentaho.reporting.engine.classic.core.modules.output.table.html.HtmlReportUtil; +import org.pentaho.reporting.engine.classic.core.modules.output.table.xls.ExcelReportUtil; +import org.pentaho.reporting.engine.classic.core.parameters.ParameterDefinitionEntry; +import org.pentaho.reporting.engine.classic.core.parameters.ReportParameterDefinition; +import org.pentaho.reporting.engine.classic.core.util.ReportParameterValues; +import org.pentaho.reporting.libraries.resourceloader.Resource; +import org.pentaho.reporting.libraries.resourceloader.ResourceException; +import org.pentaho.reporting.libraries.resourceloader.ResourceManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.lowagie.text.Document; +import com.lowagie.text.PageSize; +import com.lowagie.text.pdf.PdfPTable; +import com.lowagie.text.pdf.PdfWriter; + +@Service +public class ReadReportingServiceImpl implements ReadReportingService { + + private final PlatformSecurityContext context; + + private final static Logger logger = LoggerFactory.getLogger(ReadReportingServiceImpl.class); + + private Boolean noPentaho = false; + + @Autowired + public ReadReportingServiceImpl(final PlatformSecurityContext context) { + // kick off pentaho reports server + ClassicEngineBoot.getInstance().start(); + noPentaho = false; + + this.context = context; + } + + @Autowired + private GenericDataService genericDataService; + + @Override + public StreamingOutput retrieveReportCSV(final String name, final String type, final Map queryParams) { + + return new StreamingOutput() { + + @Override + public void write(OutputStream out) { + try { + + GenericResultsetData result = retrieveGenericResultset(name, type, queryParams); + StringBuffer sb = generateCsvFileBuffer(result); + + InputStream in = new ByteArrayInputStream(sb.toString().getBytes("UTF-8")); + + byte[] outputByte = new byte[4096]; + Integer readLen = in.read(outputByte, 0, 4096); + + while (readLen != -1) { + out.write(outputByte, 0, readLen); + readLen = in.read(outputByte, 0, 4096); + } + // in.close(); + // out.flush(); + // out.close(); + } catch (Exception e) { + throw new PlatformDataIntegrityException("error.msg.exception.error", e.getMessage()); + } + } + }; + + } + + private static StringBuffer generateCsvFileBuffer(GenericResultsetData result) { + StringBuffer writer = new StringBuffer(); + + List columnHeaders = result.getColumnHeaders(); + logger.info("NO. of Columns: " + columnHeaders.size()); + Integer chSize = columnHeaders.size(); + for (int i = 0; i < chSize; i++) { + writer.append('"' + columnHeaders.get(i).getColumnName() + '"'); + if (i < (chSize - 1)) writer.append(","); + } + writer.append('\n'); + + List data = result.getData(); + List row; + Integer rSize; + // String currCol; + String currColType; + String currVal; + logger.info("NO. of Rows: " + data.size()); + for (int i = 0; i < data.size(); i++) { + row = data.get(i).getRow(); + rSize = row.size(); + for (int j = 0; j < rSize; j++) { + // currCol = columnHeaders.get(j).getColumnName(); + currColType = columnHeaders.get(j).getColumnType(); + currVal = row.get(j); + if (currVal != null) { + if (currColType.equals("DECIMAL") || currColType.equals("DOUBLE") || currColType.equals("BIGINT") + || currColType.equals("SMALLINT") || currColType.equals("INT")) + writer.append(currVal); + else + writer.append('"' + currVal + '"'); + } + if (j < (rSize - 1)) writer.append(","); + } + writer.append('\n'); + } + + return writer; + } + + @Override + public GenericResultsetData retrieveGenericResultset(final String name, final String type, final Map queryParams) { + + long startTime = System.currentTimeMillis(); + logger.info("STARTING REPORT: " + name + " Type: " + type); + + String sql; + if (name.equals(".")) { + // this is to support api /reports - which isn't an important + // call. It isn't used in the default reporting UI. But there is a + // need to provide an api that does bring back 'permitted' reports + // PERMITTED REPORTS SQL + sql = "select r.report_id, r.report_name, r.report_type, r.report_subtype, r.report_category," + + " rp.parameter_id, rp.report_parameter_name, p.parameter_name" + " from stretchy_report r" + + " left join stretchy_report_parameter rp on rp.report_id = r.report_id" + + " left join stretchy_parameter p on p.parameter_id = rp.parameter_id" + " where exists" + " (select 'f'" + + " from m_appuser_role ur " + " join m_role r on r.id = ur.role_id" + + " left join m_role_permission rp on rp.role_id = r.id" + " left join m_permission p on p.id = rp.permission_id" + + " where ur.appuser_id = " + context.authenticatedUser().getId() + + " and (p.code in ('ALL_FUNCTIONS', 'ALL_FUNCTIONS_READ') or p.code = concat('READ_', r.report_name))) " + + " order by r.report_name, rp.parameter_id"; + } else { + sql = getSQLtoRun(name, type, queryParams); + } + + GenericResultsetData result = genericDataService.fillGenericResultSet(sql); + + long elapsed = System.currentTimeMillis() - startTime; + logger.info("FINISHING Report/Request Name: " + name + " - " + type + " Elapsed Time: " + elapsed); + return result; + } + + private String getSQLtoRun(final String name, final String type, final Map queryParams) { + + String sql = getSql(name, type); + + Set keys = queryParams.keySet(); + for (String key : keys) { + String pValue = queryParams.get(key); + // logger.info("(" + key + " : " + pValue + ")"); + sql = genericDataService.replace(sql, key, pValue); + } + + AppUser currentUser = context.authenticatedUser(); + // Allows sql query to restrict data by office hierarchy if required + sql = genericDataService.replace(sql, "${currentUserHierarchy}", currentUser.getOffice().getHierarchy()); + // Allows sql query to restrict data by current user Id if required + // (typically used to return report lists containing only reports + // permitted to be run by the user + sql = genericDataService.replace(sql, "${currentUserId}", currentUser.getId().toString()); + + sql = genericDataService.wrapSQL(sql); + + return sql; + + } + + private String getSql(String name, String type) { + + String inputSql = "select " + type + "_sql as the_sql from stretchy_" + type + " where " + type + "_name = '" + name + "'"; + inputSql = genericDataService.wrapSQL(inputSql); + + String sqlErrorMsg = "Sql: " + inputSql; + CachedRowSet rs = genericDataService.getCachedResultSet(inputSql, sqlErrorMsg); + + try { + if (rs.next()) { return rs.getString("the_sql"); } + throw new ReportNotFoundException(inputSql); + + } catch (SQLException e) { + logger.info("msg: " + e.getMessage() + " sql: " + inputSql); + throw new PlatformDataIntegrityException("error.msg.sql.error", e.getMessage(), sqlErrorMsg); + } + + } + + @Override + public String getReportType(String reportName) { + String sql = "SELECT ifnull(report_type,'') as report_type FROM `stretchy_report` where report_name = '" + reportName + "'"; + + sql = genericDataService.wrapSQL(sql); + + String sqlErrorMsg = "Report Name: " + reportName + " Sql: " + sql; + CachedRowSet rs = genericDataService.getCachedResultSet(sql, sqlErrorMsg); + + try { + + if (rs.next()) { return rs.getString("report_type"); } + throw new ReportNotFoundException(sql); + + } catch (SQLException e) { + throw new PlatformDataIntegrityException("error.msg.sql.error", e.getMessage(), sqlErrorMsg); + } + + } + + @Override + public Response processPentahoRequest(String reportName, String outputTypeParam, Map queryParams) { + + String outputType = "HTML"; + if (StringUtils.isNotBlank(outputTypeParam)) outputType = outputTypeParam; + + if (!(outputType.equalsIgnoreCase("HTML") || outputType.equalsIgnoreCase("PDF") || outputType.equalsIgnoreCase("XLS") || outputType + .equalsIgnoreCase("CSV"))) + throw new PlatformDataIntegrityException("error.msg.invalid.outputType", "No matching Output Type: " + outputType); + + if (noPentaho) + throw new PlatformDataIntegrityException("error.msg.no.pentaho", "Pentaho is not enabled", "Pentaho is not enabled"); + + // TODO - use pentaho location finder like Pawel does in Mifos + // String reportPath = + // "C:\\dev\\apache-tomcat-7.0.25\\webapps\\ROOT\\PentahoReports\\" + // + reportName + ".prpt"; + String reportPath = FileUtils.MIFOSX_BASE_DIR + File.separator + + ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim() + File.separator + "reports" + File.separator + + reportName + ".prpt"; + logger.info("Report path: " + reportPath); + + // load report definition + ResourceManager manager = new ResourceManager(); + manager.registerDefaults(); + Resource res; + + try { + res = manager.createDirectly(reportPath, MasterReport.class); + MasterReport masterReport = (MasterReport) res.getResource(); + + addParametersToReport(masterReport, queryParams); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + if ("PDF".equalsIgnoreCase(outputType)) { + PdfReportUtil.createPDF(masterReport, baos); + return Response.ok().entity(baos.toByteArray()).type("application/pdf").build(); + } + + if ("XLS".equalsIgnoreCase(outputType)) { + ExcelReportUtil.createXLS(masterReport, baos); + return Response.ok().entity(baos.toByteArray()).type("application/vnd.ms-excel") + .header("Content-Disposition", "attachment;filename=" + reportName.replaceAll(" ", "") + ".xls").build(); + } + + if ("CSV".equalsIgnoreCase(outputType)) { + CSVReportUtil.createCSV(masterReport, baos, "UTF-8"); + return Response.ok().entity(baos.toByteArray()).type("application/x-msdownload") + .header("Content-Disposition", "attachment;filename=" + reportName.replaceAll(" ", "") + ".csv").build(); + } + + if ("HTML".equalsIgnoreCase(outputType)) { + HtmlReportUtil.createStreamHTML(masterReport, baos); + return Response.ok().entity(baos.toByteArray()).type("text/html").build(); + } + } catch (ResourceException e) { + throw new PlatformDataIntegrityException("error.msg.reporting.error", e.getMessage()); + } catch (ReportProcessingException e) { + throw new PlatformDataIntegrityException("error.msg.reporting.error", e.getMessage()); + } catch (IOException e) { + throw new PlatformDataIntegrityException("error.msg.reporting.error", e.getMessage()); + } + + throw new PlatformDataIntegrityException("error.msg.invalid.outputType", "No matching Output Type: " + outputType); + + } + + private void addParametersToReport(MasterReport report, Map queryParams) { + + try { + ReportParameterValues rptParamValues = report.getParameterValues(); + ReportParameterDefinition paramsDefinition = report.getParameterDefinition(); + + /* + * only allow integer and string parameter types and assume all + * mandatory - could go more detailed like Pawel did in Mifos later + * and could match incoming and pentaho parameters better... + * currently assuming they come in ok... and if not an error + */ + for (ParameterDefinitionEntry paramDefEntry : paramsDefinition.getParameterDefinitions()) { + String paramName = paramDefEntry.getName(); + String pValue = queryParams.get(paramName); + if (StringUtils.isBlank(pValue)) + throw new PlatformDataIntegrityException("error.msg.reporting.error", "Pentaho Parameter: " + paramName + + " - not Provided"); + + Class clazz = paramDefEntry.getValueType(); + logger.info("addParametersToReport(" + paramName + " : " + pValue + " : " + clazz.getCanonicalName() + ")"); + + if (clazz.getCanonicalName().equalsIgnoreCase("java.lang.Integer")) + rptParamValues.put(paramName, Integer.parseInt(pValue)); + else if (clazz.getCanonicalName().equalsIgnoreCase("java.sql.Date")) + rptParamValues.put(paramName, Date.valueOf(pValue)); + else + rptParamValues.put(paramName, pValue); + } + + } catch (Exception e) { + throw new PlatformDataIntegrityException("error.msg.reporting.error", e.getMessage()); + } + } + + @Override + public String retrieveReportPDF(final String reportName, final String type, final Map queryParams) { + + String fileLocation = FileUtils.MIFOSX_BASE_DIR + File.separator + ""; + if (!new File(fileLocation).isDirectory()) { + new File(fileLocation).mkdirs(); + } + + String genaratePdf = fileLocation + File.separator + reportName + ".pdf"; + + try { + GenericResultsetData result = retrieveGenericResultset(reportName, type, queryParams); + + List columnHeaders = result.getColumnHeaders(); + List data = result.getData(); + List row; + + logger.info("NO. of Columns: " + columnHeaders.size()); + Integer chSize = columnHeaders.size(); + + Document document = new Document(PageSize.B0.rotate()); + + PdfWriter.getInstance(document, new FileOutputStream(new File(fileLocation + reportName + ".pdf"))); + document.open(); + + PdfPTable table = new PdfPTable(chSize); + table.setWidthPercentage(100); + + for (int i = 0; i < chSize; i++) { + + table.addCell(columnHeaders.get(i).getColumnName()); + + } + table.completeRow(); + + Integer rSize; + String currColType; + String currVal; + logger.info("NO. of Rows: " + data.size()); + for (int i = 0; i < data.size(); i++) { + row = data.get(i).getRow(); + rSize = row.size(); + for (int j = 0; j < rSize; j++) { + currColType = columnHeaders.get(j).getColumnType(); + currVal = row.get(j); + if (currVal != null) { + if (currColType.equals("DECIMAL") || currColType.equals("DOUBLE") || currColType.equals("BIGINT") + || currColType.equals("SMALLINT") || currColType.equals("INT")) { + + table.addCell(currVal.toString()); + } else { + table.addCell(currVal.toString()); + } + } + } + } + table.completeRow(); + document.add(table); + document.close(); + return genaratePdf; + } + catch (Exception e) { + throw new PlatformDataIntegrityException("error.msg.exception.error", e.getMessage()); + } + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/AccountingConstants.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/AccountingConstants.java index 78c396dce..7b69011ab 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/AccountingConstants.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/AccountingConstants.java @@ -4,7 +4,17 @@ public class AccountingConstants { /*** Categories for Accounting **/ public static enum GL_ACCOUNT_CLASSIFICATION { - ASSETS, LIABILITIES, INCOME, EXPENDITURE, EQUITY; + ASSET, LIABILITY, INCOME, EXPENSE, EQUITY; + + @Override + public String toString() { + return name().toString(); + } + } + + /** Types of Journal Entries **/ + public static enum JOURNAL_ENTRY_TYPE { + DEBIT, CREDIT; @Override public String toString() { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/GLClosuresApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/GLClosuresApiResource.java index e75df75ea..b86930a13 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/GLClosuresApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/GLClosuresApiResource.java @@ -1,5 +1,6 @@ package org.mifosplatform.accounting.api; +import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -26,6 +27,8 @@ import org.mifosplatform.accounting.service.GLClosureWritePlatformService; import org.mifosplatform.infrastructure.core.api.ApiParameterHelper; import org.mifosplatform.infrastructure.core.data.EntityIdentifier; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; +import org.mifosplatform.organisation.office.data.OfficeLookup; +import org.mifosplatform.organisation.office.service.OfficeReadPlatformService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; @@ -41,6 +44,9 @@ public class GLClosuresApiResource { @Autowired private GLClosureWritePlatformService glClosureWritePlatformService; + @Autowired + private OfficeReadPlatformService officeReadPlatformService; + @Autowired private AccountingApiDataConversionService accountingApiDataConversionService; @@ -84,7 +90,7 @@ public class GLClosuresApiResource { final GLClosureData glClosureData = this.glClosureReadPlatformService.retrieveGLClosureById(glClosureId); if (template) { - // TODO: define templates + glClosureData.setAllowedOffices(new ArrayList(officeReadPlatformService.retrieveAllOfficesForLookup())); } return this.apiJsonSerializerService.serializeGLClosureDataToJson(prettyPrint, responseParameters, glClosureData); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/GLJournalEntriesApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/GLJournalEntriesApiResource.java new file mode 100755 index 000000000..7a16871bc --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/GLJournalEntriesApiResource.java @@ -0,0 +1,123 @@ +package org.mifosplatform.accounting.api; + +import java.util.Date; +import java.util.List; +import java.util.Set; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; + +import org.mifosplatform.accounting.api.commands.GLJournalEntryCommand; +import org.mifosplatform.accounting.api.data.GLJournalEntryData; +import org.mifosplatform.accounting.api.data.JournalEntryIdentifier; +import org.mifosplatform.accounting.api.infrastructure.AccountingApiDataConversionService; +import org.mifosplatform.accounting.api.infrastructure.AccountingApiJsonSerializerService; +import org.mifosplatform.accounting.api.infrastructure.DateParam; +import org.mifosplatform.accounting.service.GLJournalEntryReadPlatformService; +import org.mifosplatform.accounting.service.GLJournalEntryWritePlatformService; +import org.mifosplatform.infrastructure.core.api.ApiParameterHelper; +import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Path("/journalentries") +@Component +@Scope("singleton") +public class GLJournalEntriesApiResource { + + @Autowired + private GLJournalEntryReadPlatformService glJournalEntryReadPlatformService; + + @Autowired + private GLJournalEntryWritePlatformService glJournalEntryWritePlatformService; + + @Autowired + private AccountingApiDataConversionService accountingApiDataConversionService; + + @Autowired + private AccountingApiJsonSerializerService apiJsonSerializerService; + + private final String entityType = "JOURNAL_ENTRY"; + + @Autowired + private PlatformSecurityContext context; + + // private final static Logger logger = + // LoggerFactory.getLogger(GLJournalEntriesApiResource.class); + + @GET + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveAllJournalEntries(@Context final UriInfo uriInfo, @QueryParam("officeId") final Long officeId, + @QueryParam("glAccountId") final Long glAccountId, + @QueryParam("portfolioGenerated") final Boolean portfolioGenerated, @QueryParam("fromDate") final DateParam fromDateParam, + @QueryParam("toDate") final DateParam toDateParam) { + + // TODO: Vishwas Add pagination, approach at + // http://www.javaworld.com/community/node/8295 looks good + + context.authenticatedUser().validateHasReadPermission(entityType); + + final Set responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters()); + final boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters()); + List glJournalEntryDatas = null; + // get dates from date params + Date fromDate = null; + if (fromDateParam != null) { + fromDate = fromDateParam.getDate(); + } + Date toDate = null; + if (toDateParam != null) { + toDate = toDateParam.getDate(); + } + glJournalEntryDatas = this.glJournalEntryReadPlatformService.retrieveAllGLJournalEntries(officeId, glAccountId, + portfolioGenerated, fromDate, toDate); + return this.apiJsonSerializerService.serializeGLJournalEntryDataToJson(prettyPrint, responseParameters, glJournalEntryDatas); + } + + @GET + @Path("{glJournalEntryId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retreiveAccount(@PathParam("glJournalEntryId") final Long glJournalEntryId, @Context final UriInfo uriInfo) { + + context.authenticatedUser().validateHasReadPermission(entityType); + + final Set responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters()); + final boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters()); + + final GLJournalEntryData glJournalEntryData = this.glJournalEntryReadPlatformService.retrieveGLJournalEntryById(glJournalEntryId); + return this.apiJsonSerializerService.serializeGLJournalEntryDataToJson(prettyPrint, responseParameters, glJournalEntryData); + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String createGLJournalEntry(final String jsonRequestBody) { + + final GLJournalEntryCommand command = this.accountingApiDataConversionService.convertJsonToGLJournalEntryCommand(jsonRequestBody); + + final String transactionId = glJournalEntryWritePlatformService.createJournalEntry(command); + return this.apiJsonSerializerService.serializeJournalEntryIdentifier(new JournalEntryIdentifier(transactionId)); + } + + @POST + @Path("{transactionId}/reversal") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String createReversalJournalEntry(@PathParam("transactionId") final String transactionId) { + + this.glJournalEntryWritePlatformService.revertJournalEntry(transactionId); + + return this.apiJsonSerializerService.serializeJournalEntryIdentifier(new JournalEntryIdentifier(transactionId)); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/commands/GLJournalEntryCommand.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/commands/GLJournalEntryCommand.java new file mode 100755 index 000000000..0ef36caca --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/commands/GLJournalEntryCommand.java @@ -0,0 +1,71 @@ +package org.mifosplatform.accounting.api.commands; + +import java.util.Set; + +import org.joda.time.LocalDate; + +/** + * Immutable command for adding an accounting closure + */ +public class GLJournalEntryCommand { + + private final Long officeId; + private final LocalDate entryDate; + private final String comments; + + private final SingleDebitOrCreditEntryCommand[] credits; + private final SingleDebitOrCreditEntryCommand[] debits; + + private final Set parametersPassedInRequest; + + public GLJournalEntryCommand(Set parametersPassedInRequest, Long officeId, LocalDate entryDate, String comments, + SingleDebitOrCreditEntryCommand[] credits, SingleDebitOrCreditEntryCommand[] debits) { + this.officeId = officeId; + this.entryDate = entryDate; + this.comments = comments; + this.credits = credits; + this.debits = debits; + this.parametersPassedInRequest = parametersPassedInRequest; + } + + public boolean isOfficeIdChanged() { + return this.parametersPassedInRequest.contains("officeId"); + } + + public boolean isEntryDateChanged() { + return this.parametersPassedInRequest.contains("entryDate"); + } + + public boolean isCommentsChanged() { + return this.parametersPassedInRequest.contains("comments"); + } + + public boolean isCreditsChanged() { + return this.parametersPassedInRequest.contains("credits"); + } + + public boolean isDebitsChanged() { + return this.parametersPassedInRequest.contains("debits"); + } + + public Long getOfficeId() { + return this.officeId; + } + + public LocalDate getEntryDate() { + return this.entryDate; + } + + public String getComments() { + return this.comments; + } + + public SingleDebitOrCreditEntryCommand[] getCredits() { + return this.credits; + } + + public SingleDebitOrCreditEntryCommand[] getDebits() { + return this.debits; + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/commands/SingleDebitOrCreditEntryCommand.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/commands/SingleDebitOrCreditEntryCommand.java new file mode 100755 index 000000000..8926cc72e --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/commands/SingleDebitOrCreditEntryCommand.java @@ -0,0 +1,52 @@ +package org.mifosplatform.accounting.api.commands; + +import java.math.BigDecimal; +import java.util.Set; + +/** + * Immutable command for adding a "Credit" entry to the Journal + */ +public class SingleDebitOrCreditEntryCommand { + + private final Long glAccountId; + private final BigDecimal amount; + private final String comments; + + private final Set parametersPassedInRequest; + + public SingleDebitOrCreditEntryCommand(Set parametersPassedInRequest, Long glAccountId, BigDecimal amount, String comments) { + this.parametersPassedInRequest = parametersPassedInRequest; + this.glAccountId = glAccountId; + this.amount = amount; + this.comments = comments; + } + + public boolean isGlAccountIdChanged() { + return this.parametersPassedInRequest.contains("glAccountId"); + } + + public boolean isGlAmountChanged() { + return this.parametersPassedInRequest.contains("amount"); + } + + public boolean isCommentsChanged() { + return this.parametersPassedInRequest.contains("comments"); + } + + public Long getGlAccountId() { + return this.glAccountId; + } + + public BigDecimal getAmount() { + return this.amount; + } + + public String getComments() { + return this.comments; + } + + public Set getParametersPassedInRequest() { + return this.parametersPassedInRequest; + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/data/GLClosureData.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/data/GLClosureData.java index c70b156f3..9e71d8ea6 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/data/GLClosureData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/data/GLClosureData.java @@ -1,6 +1,10 @@ package org.mifosplatform.accounting.api.data; +import java.util.ArrayList; +import java.util.List; + import org.joda.time.LocalDate; +import org.mifosplatform.organisation.office.data.OfficeLookup; /** * Immutable object representing a General Ledger Account @@ -25,14 +29,21 @@ public class GLClosureData { @SuppressWarnings("unused") private final LocalDate lastUpdatedDate; @SuppressWarnings("unused") - private final Long creatingByUserId; + private final Long createdByUserId; + @SuppressWarnings("unused") + private final String createdByUsername; @SuppressWarnings("unused") private final Long lastUpdatedByUserId; @SuppressWarnings("unused") + private final String lastUpdatedByUsername; + @SuppressWarnings("unused") private final String comments; + private List allowedOffices = new ArrayList(); + public GLClosureData(Long id, Long officeId, String officeName, LocalDate closingDate, boolean deleted, LocalDate createdDate, - LocalDate lastUpdatedDate, Long creatingByUserId, Long lastUpdatedByUserId, String comments) { + LocalDate lastUpdatedDate, Long createdByUserId, String createdByUsername, Long lastUpdatedByUserId, + String lastUpdatedByUsername, String comments) { this.id = id; this.officeId = officeId; this.officeName = officeName; @@ -40,9 +51,19 @@ public class GLClosureData { this.deleted = deleted; this.createdDate = createdDate; this.lastUpdatedDate = lastUpdatedDate; - this.creatingByUserId = creatingByUserId; + this.createdByUserId = createdByUserId; + this.createdByUsername = createdByUsername; this.lastUpdatedByUserId = lastUpdatedByUserId; + this.lastUpdatedByUsername = lastUpdatedByUsername; this.comments = comments; } + public List getAllowedOffices() { + return this.allowedOffices; + } + + public void setAllowedOffices(List allowedOffices) { + this.allowedOffices = allowedOffices; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/data/GLJournalEntryData.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/data/GLJournalEntryData.java new file mode 100755 index 000000000..e26065bc1 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/data/GLJournalEntryData.java @@ -0,0 +1,76 @@ +package org.mifosplatform.accounting.api.data; + +import java.math.BigDecimal; + +import org.joda.time.LocalDate; + +/** + * Immutable object representing a General Ledger Account + * + * Note: no getter/setters required as google will produce json from fields of + * object. + */ +public class GLJournalEntryData { + + @SuppressWarnings("unused") + private final Long id; + @SuppressWarnings("unused") + private final Long officeId; + @SuppressWarnings("unused") + private final String officeName; + @SuppressWarnings("unused") + private final String glAccountName; + @SuppressWarnings("unused") + private final Long glAccountId; + @SuppressWarnings("unused") + private final String glAccountClassification; + @SuppressWarnings("unused") + private final LocalDate entryDate; + @SuppressWarnings("unused") + private final String entryType; + @SuppressWarnings("unused") + private final BigDecimal amount; + @SuppressWarnings("unused") + private final String transactionId; + @SuppressWarnings("unused") + private final Boolean portfolioGenerated; + @SuppressWarnings("unused") + private final String entityType; + @SuppressWarnings("unused") + private final Long entityId; + @SuppressWarnings("unused") + private final Long createdByUserId; + @SuppressWarnings("unused") + private final LocalDate createdDate; + @SuppressWarnings("unused") + private final String createdByUserName; + @SuppressWarnings("unused") + private final String comments; + @SuppressWarnings("unused") + private final Boolean reversed; + + public GLJournalEntryData(Long id, Long officeId, String officeName, String glAccountName, Long glAccountId, + String glAccountClassification, LocalDate entryDate, String entryType, BigDecimal amount, String transactionId, + Boolean portfolioGenerated, String entityType, Long entityId, Long createdByUserId, LocalDate createdDate, + String createdByUserName, String comments, Boolean reversed) { + this.id = id; + this.officeId = officeId; + this.officeName = officeName; + this.glAccountName = glAccountName; + this.glAccountId = glAccountId; + this.glAccountClassification = glAccountClassification; + this.entryDate = entryDate; + this.entryType = entryType; + this.amount = amount; + this.transactionId = transactionId; + this.portfolioGenerated = portfolioGenerated; + this.entityType = entityType; + this.entityId = entityId; + this.createdByUserId = createdByUserId; + this.createdDate = createdDate; + this.createdByUserName = createdByUserName; + this.comments = comments; + this.reversed = reversed; + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/data/JournalEntryIdentifier.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/data/JournalEntryIdentifier.java new file mode 100755 index 000000000..4d678604c --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/data/JournalEntryIdentifier.java @@ -0,0 +1,43 @@ +package org.mifosplatform.accounting.api.data; + +/** + * Represents the successful result of an REST API call. + */ +public class JournalEntryIdentifier { + + private String entityId; + + // TODO - Rename variable to commandId or taskId or something that shows + // this is the id of a command in a table/queue for processing. + @SuppressWarnings("unused") + private Long makerCheckerId; + + public static JournalEntryIdentifier makerChecker(final Long makerCheckerId) { + return new JournalEntryIdentifier(null, makerCheckerId); + } + + public static JournalEntryIdentifier makerChecker(final String resourceId, final Long makerCheckerId) { + return new JournalEntryIdentifier(resourceId, makerCheckerId); + } + + public JournalEntryIdentifier() { + // + } + + public JournalEntryIdentifier(final String entityId) { + this.entityId = entityId; + } + + private JournalEntryIdentifier(final String entityId, final Long makerCheckerId) { + this.entityId = entityId; + this.makerCheckerId = makerCheckerId; + } + + public String getEntityId() { + return this.entityId; + } + + public void setEntityId(final String entityId) { + this.entityId = entityId; + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/infrastructure/AccountingApiDataConversionService.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/infrastructure/AccountingApiDataConversionService.java index 63d8f9020..ca20f63b5 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/infrastructure/AccountingApiDataConversionService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/infrastructure/AccountingApiDataConversionService.java @@ -2,6 +2,7 @@ package org.mifosplatform.accounting.api.infrastructure; import org.mifosplatform.accounting.api.commands.GLAccountCommand; import org.mifosplatform.accounting.api.commands.GLClosureCommand; +import org.mifosplatform.accounting.api.commands.GLJournalEntryCommand; public interface AccountingApiDataConversionService { @@ -9,4 +10,6 @@ public interface AccountingApiDataConversionService { GLClosureCommand convertJsonToGLClosureCommand(Long resourceIdentifier, String json); + GLJournalEntryCommand convertJsonToGLJournalEntryCommand(String json); + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/infrastructure/AccountingApiJsonSerializerService.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/infrastructure/AccountingApiJsonSerializerService.java index f8f196156..32726cd27 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/infrastructure/AccountingApiJsonSerializerService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/infrastructure/AccountingApiJsonSerializerService.java @@ -5,12 +5,17 @@ import java.util.Set; import org.mifosplatform.accounting.api.data.GLAccountData; import org.mifosplatform.accounting.api.data.GLClosureData; +import org.mifosplatform.accounting.api.data.GLJournalEntryData; +import org.mifosplatform.accounting.api.data.JournalEntryIdentifier; import org.mifosplatform.infrastructure.core.data.EntityIdentifier; + public interface AccountingApiJsonSerializerService { String serializeEntityIdentifier(final EntityIdentifier identifier); + String serializeJournalEntryIdentifier(final JournalEntryIdentifier identifier); + String serializeGLAccountDataToJson(boolean prettyPrint, Set responseParameters, GLAccountData accountData); String serializeGLAccountDataToJson(boolean prettyPrint, Set responseParameters, Collection accountDatas); @@ -19,4 +24,9 @@ public interface AccountingApiJsonSerializerService { String serializeGLClosureDataToJson(boolean prettyPrint, Set responseParameters, Collection closureDatas); + String serializeGLJournalEntryDataToJson(boolean prettyPrint, Set responseParameters, GLJournalEntryData journalEntryData); + + String serializeGLJournalEntryDataToJson(boolean prettyPrint, Set responseParameters, + Collection journalEntryDatas); + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/infrastructure/DateParam.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/infrastructure/DateParam.java new file mode 100755 index 000000000..0d99ae138 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/infrastructure/DateParam.java @@ -0,0 +1,43 @@ +package org.mifosplatform.accounting.api.infrastructure; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.apache.commons.lang.StringUtils; + +/** + * Class for parsing dates sent as query parameters + * + * TODO: Vishwas Should move this class to a more generic package + * + * @author + * + */ +public class DateParam { + + private final Date date; + + public DateParam(String dateStr) throws WebApplicationException { + if (StringUtils.isEmpty(dateStr)) { + this.date = null; + return; + } + final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + try { + this.date = dateFormat.parse(dateStr); + } catch (ParseException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST) + .entity("Couldn't parse date string: Expected format yyyy-mm-dd " + e.getMessage()).build()); + } + } + + public Date getDate() { + return date; + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/infrastructure/impl/AccountingApiDataConversionServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/infrastructure/impl/AccountingApiDataConversionServiceImpl.java index e969a61be..725f4b22d 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/infrastructure/impl/AccountingApiDataConversionServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/infrastructure/impl/AccountingApiDataConversionServiceImpl.java @@ -1,10 +1,12 @@ package org.mifosplatform.accounting.api.infrastructure.impl; import java.lang.reflect.Type; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -12,6 +14,8 @@ import org.apache.commons.lang.StringUtils; import org.joda.time.LocalDate; import org.mifosplatform.accounting.api.commands.GLAccountCommand; import org.mifosplatform.accounting.api.commands.GLClosureCommand; +import org.mifosplatform.accounting.api.commands.GLJournalEntryCommand; +import org.mifosplatform.accounting.api.commands.SingleDebitOrCreditEntryCommand; import org.mifosplatform.accounting.api.infrastructure.AccountingApiDataConversionService; import org.mifosplatform.infrastructure.core.exception.InvalidJsonException; import org.mifosplatform.infrastructure.core.exception.UnsupportedParameterException; @@ -19,7 +23,9 @@ import org.mifosplatform.infrastructure.core.serialization.JsonParserHelper; import org.springframework.stereotype.Service; import com.google.gson.Gson; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.reflect.TypeToken; @@ -69,7 +75,7 @@ public class AccountingApiDataConversionServiceImpl implements AccountingApiData final String glCode = helper.extractStringNamed("glCode", element, requestParamatersDetected); final Boolean disabled = helper.extractBooleanNamed("disabled", element, requestParamatersDetected); final Boolean manualEntriesAllowed = helper.extractBooleanNamed("manualEntriesAllowed", element, requestParamatersDetected); - final String category = helper.extractStringNamed("category", element, requestParamatersDetected); + final String category = helper.extractStringNamed("classification", element, requestParamatersDetected); final Boolean headerAccount = helper.extractBooleanNamed("headerAccount", element, requestParamatersDetected); final String description = helper.extractStringNamed("description", element, requestParamatersDetected); @@ -98,4 +104,66 @@ public class AccountingApiDataConversionServiceImpl implements AccountingApiData return new GLClosureCommand(requestParamatersDetected, resourceIdentifier, officeId, closingDate, comments); } + + @Override + public GLJournalEntryCommand convertJsonToGLJournalEntryCommand(String json) { + final Type typeOfMap = new TypeToken>() {}.getType(); + final Map requestMap = gsonConverter.fromJson(json, typeOfMap); + + final Set supportedParams = new HashSet(Arrays.asList("officeId", "entryDate", "comments", "credits", "debits", + "locale", "dateFormat")); + + checkForUnsupportedParameters(requestMap, supportedParams); + + final JsonParser parser = new JsonParser(); + final JsonElement element = parser.parse(json); + final JsonParserHelper helper = new JsonParserHelper(); + + final Set requestParamatersDetected = new HashSet(); + final Long officeId = helper.extractLongNamed("officeId", element, requestParamatersDetected); + final String comments = helper.extractStringNamed("comments", element, requestParamatersDetected); + final LocalDate entryDate = helper.extractLocalDateNamed("entryDate", element, requestParamatersDetected); + + final JsonObject topLevelJsonElement = element.getAsJsonObject(); + final Locale locale = helper.extractLocaleParameter(topLevelJsonElement); + + SingleDebitOrCreditEntryCommand[] credits = null; + SingleDebitOrCreditEntryCommand[] debits = null; + if (element.isJsonObject()) { + if (topLevelJsonElement.has("credits") && topLevelJsonElement.get("credits").isJsonArray()) { + credits = populateCreditsOrDebitsArray(topLevelJsonElement, helper, requestParamatersDetected, locale, credits, "credits"); + } + if (topLevelJsonElement.has("debits") && topLevelJsonElement.get("debits").isJsonArray()) { + debits = populateCreditsOrDebitsArray(topLevelJsonElement, helper, requestParamatersDetected, locale, debits, "debits"); + } + } + return new GLJournalEntryCommand(requestParamatersDetected, officeId, entryDate, comments, credits, debits); + } + + /** + * @param helper + * @param requestParamatersDetected + * @param comments + * @param topLevelJsonElement + * @param locale + */ + private SingleDebitOrCreditEntryCommand[] populateCreditsOrDebitsArray(final JsonObject topLevelJsonElement, + final JsonParserHelper helper, final Set requestParamatersDetected, final Locale locale, + SingleDebitOrCreditEntryCommand[] debitOrCredits, String paramName) { + requestParamatersDetected.add(paramName); + final JsonArray array = topLevelJsonElement.get(paramName).getAsJsonArray(); + debitOrCredits = new SingleDebitOrCreditEntryCommand[array.size()]; + for (int i = 0; i < array.size(); i++) { + + final JsonObject creditElement = array.get(i).getAsJsonObject(); + final Set parametersPassedInForCreditsCommand = new HashSet(); + + final Long glAccountId = helper.extractLongNamed("glAccountId", creditElement, parametersPassedInForCreditsCommand); + final String comments = helper.extractStringNamed("comments", creditElement, parametersPassedInForCreditsCommand); + final BigDecimal amount = helper.extractBigDecimalNamed("amount", creditElement, locale, parametersPassedInForCreditsCommand); + + debitOrCredits[i] = new SingleDebitOrCreditEntryCommand(parametersPassedInForCreditsCommand, glAccountId, amount, comments); + } + return debitOrCredits; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/infrastructure/impl/GoogleGsonAccountingApiJsonSerializerService.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/infrastructure/impl/GoogleGsonAccountingApiJsonSerializerService.java index 276f15e3a..d537eab15 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/infrastructure/impl/GoogleGsonAccountingApiJsonSerializerService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/api/infrastructure/impl/GoogleGsonAccountingApiJsonSerializerService.java @@ -7,6 +7,8 @@ import java.util.Set; import org.mifosplatform.accounting.api.data.GLAccountData; import org.mifosplatform.accounting.api.data.GLClosureData; +import org.mifosplatform.accounting.api.data.GLJournalEntryData; +import org.mifosplatform.accounting.api.data.JournalEntryIdentifier; import org.mifosplatform.accounting.api.infrastructure.AccountingApiJsonSerializerService; import org.mifosplatform.infrastructure.core.api.PortfolioApiJsonSerializerService; import org.mifosplatform.infrastructure.core.data.EntityIdentifier; @@ -23,11 +25,18 @@ import com.google.gson.Gson; @Service public class GoogleGsonAccountingApiJsonSerializerService implements AccountingApiJsonSerializerService { + private static final Set DATA_PARAMETERS = new HashSet(Arrays.asList("entityId")); + private static final Set GL_ACCOUNT_DATA_PARAMETERS = new HashSet(Arrays.asList("id", "name", "parentId", "glCode", "disabled", "manualEntriesAllowed", "classification", "headerAccount", "description")); - private static final Set GL_ACCOUNT_CLOSURE_PARAMETERS = new HashSet(Arrays.asList("id", "officeId", "officeName", - "closingDate", "deleted", "createdDate", "lastUpdatedDate", "creatingByUserId", "lastUpdatedByUserId", "comments")); + private static final Set GL_ACCOUNT_CLOSURE_DATA_PARAMETERS = new HashSet(Arrays.asList("id", "officeId", "officeName", + "closingDate", "deleted", "createdDate", "lastUpdatedDate", "createdByUserId", "createdByUsername", "lastUpdatedByUserId", + "lastUpdatedByUsername", "comments", "allowedOffices")); + + private static final Set GL_JOURNAL_ENTRY_DATA_PARAMETERS = new HashSet(Arrays.asList("id", "officeId", "officeName", + "glAccountName", "glAccountId", "glAccountClassification", "entryDate", "entryType", "amount", "transactionId", + "portfolioGenerated", "entityType", "entityId", "createdByUserId", "createdDate", "createdByUserName", "comments", "reversed")); private final GoogleGsonSerializerHelper helper; @@ -38,7 +47,13 @@ public class GoogleGsonAccountingApiJsonSerializerService implements AccountingA @Override public String serializeEntityIdentifier(final EntityIdentifier identifier) { - final Set DATA_PARAMETERS = new HashSet(Arrays.asList("entityId")); + final Gson gsonDeserializer = helper.createGsonBuilderWithParameterExclusionSerializationStrategy(DATA_PARAMETERS, false, + DATA_PARAMETERS); + return helper.serializedJsonFrom(gsonDeserializer, identifier); + } + + @Override + public String serializeJournalEntryIdentifier(final JournalEntryIdentifier identifier) { final Gson gsonDeserializer = helper.createGsonBuilderWithParameterExclusionSerializationStrategy(DATA_PARAMETERS, false, DATA_PARAMETERS); return helper.serializedJsonFrom(gsonDeserializer, identifier); @@ -61,15 +76,31 @@ public class GoogleGsonAccountingApiJsonSerializerService implements AccountingA @Override public String serializeGLClosureDataToJson(boolean prettyPrint, Set responseParameters, GLClosureData closureData) { - final Gson gsonDeserializer = helper.createGsonBuilderWithParameterExclusionSerializationStrategy(GL_ACCOUNT_CLOSURE_PARAMETERS, - prettyPrint, responseParameters); + final Gson gsonDeserializer = helper.createGsonBuilderWithParameterExclusionSerializationStrategy( + GL_ACCOUNT_CLOSURE_DATA_PARAMETERS, prettyPrint, responseParameters); return helper.serializedJsonFrom(gsonDeserializer, closureData); } @Override public String serializeGLClosureDataToJson(boolean prettyPrint, Set responseParameters, Collection closureDatas) { - final Gson gsonDeserializer = helper.createGsonBuilderWithParameterExclusionSerializationStrategy(GL_ACCOUNT_DATA_PARAMETERS, - prettyPrint, responseParameters); - return helper.serializedJsonFrom(gsonDeserializer, closureDatas.toArray(new GLAccountData[closureDatas.size()])); + final Gson gsonDeserializer = helper.createGsonBuilderWithParameterExclusionSerializationStrategy( + GL_ACCOUNT_CLOSURE_DATA_PARAMETERS, prettyPrint, responseParameters); + return helper.serializedJsonFrom(gsonDeserializer, closureDatas.toArray(new GLClosureData[closureDatas.size()])); } + + @Override + public String serializeGLJournalEntryDataToJson(boolean prettyPrint, Set responseParameters, GLJournalEntryData journalEntryData) { + final Gson gsonDeserializer = helper.createGsonBuilderWithParameterExclusionSerializationStrategy(GL_JOURNAL_ENTRY_DATA_PARAMETERS, + prettyPrint, responseParameters); + return helper.serializedJsonFrom(gsonDeserializer, journalEntryData); + } + + @Override + public String serializeGLJournalEntryDataToJson(boolean prettyPrint, Set responseParameters, + Collection journalEntryDatas) { + final Gson gsonDeserializer = helper.createGsonBuilderWithParameterExclusionSerializationStrategy(GL_JOURNAL_ENTRY_DATA_PARAMETERS, + prettyPrint, responseParameters); + return helper.serializedJsonFrom(gsonDeserializer, journalEntryDatas.toArray(new GLJournalEntryData[journalEntryDatas.size()])); + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLAccount.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLAccount.java index 9f554956c..3a2029840 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLAccount.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLAccount.java @@ -43,7 +43,7 @@ public class GLAccount extends AbstractPersistable { private boolean disabled = false; @SuppressWarnings("unused") - @Column(name = "manual_entries_allowed", nullable = false) + @Column(name = "manual_journal_entries_allowed", nullable = false) private boolean manualEntriesAllowed = true; @SuppressWarnings("unused") diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLClosure.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLClosure.java index 4b921acb3..93803538e 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLClosure.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLClosure.java @@ -22,7 +22,6 @@ import org.mifosplatform.useradministration.domain.AppUser; @Table(name = "acc_gl_closure", uniqueConstraints = { @UniqueConstraint(columnNames = { "office_id", "closing_date" }, name = "office_id_closing_date") }) public class GLClosure extends AbstractAuditableCustom { - @SuppressWarnings("unused") @ManyToOne @JoinColumn(name = "office_id", nullable = false) private Office office; @@ -31,7 +30,6 @@ public class GLClosure extends AbstractAuditableCustom { @Column(name = "is_deleted", nullable = false) private boolean deleted = true; - @SuppressWarnings("unused") @Column(name = "closing_date") @Temporal(TemporalType.DATE) private Date closingDate; @@ -64,4 +62,13 @@ public class GLClosure extends AbstractAuditableCustom { this.comments = command.getComments().trim(); } } + + public Date getClosingDate() { + return this.closingDate; + } + + public Office getOffice() { + return this.office; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLClosureRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLClosureRepository.java index 570a1f23f..b02efd9ec 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLClosureRepository.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLClosureRepository.java @@ -2,7 +2,11 @@ package org.mifosplatform.accounting.domain; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface GLClosureRepository extends JpaRepository, JpaSpecificationExecutor { - // no added behaviour + + @Query("from GLClosure closure where closure.closingDate = (select max(closure1.closingDate) from GLClosure closure1 where closure1.office.id=:officeId) and closure.office.id= :officeId") + GLClosure getLatestGLClosureByBranch(@Param("officeId") Long officeId); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLJournalEntry.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLJournalEntry.java new file mode 100755 index 000000000..cb666002d --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLJournalEntry.java @@ -0,0 +1,125 @@ +package org.mifosplatform.accounting.domain; + +import java.math.BigDecimal; +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.apache.commons.lang.StringUtils; +import org.mifosplatform.infrastructure.core.domain.AbstractAuditableCustom; +import org.mifosplatform.organisation.office.domain.Office; +import org.mifosplatform.useradministration.domain.AppUser; + +@Entity +@Table(name = "acc_gl_journal_entry") +public class GLJournalEntry extends AbstractAuditableCustom { + + @ManyToOne + @JoinColumn(name = "office_id", nullable = false) + private Office office; + + @ManyToOne + @JoinColumn(name = "account_id", nullable = false) + private GLAccount glAccount; + + @SuppressWarnings("unused") + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "reversal_id") + private GLJournalEntry reversalJournalEntry; + + @SuppressWarnings("unused") + @Column(name = "transaction_id", nullable = false, length = 50) + private String transactionId; + + @SuppressWarnings("unused") + @Column(name = "reversed", nullable = false) + private boolean reversed = false; + + @SuppressWarnings("unused") + @Column(name = "portfolio_generated", nullable = false) + private boolean portfolioGenerated = false; + + @Column(name = "entry_date") + @Temporal(TemporalType.DATE) + private Date entryDate; + + @Column(name = "type", nullable = false, length = 50) + private String type; + + @Column(name = "amount", scale = 6, precision = 19, nullable = false) + private BigDecimal amount; + + @SuppressWarnings("unused") + @Column(name = "description", length = 500) + private String description; + + @SuppressWarnings("unused") + @Column(name = "entity_type", length = 50) + private String entityType; + + @SuppressWarnings("unused") + @Column(name = "entity_id") + private Long entityId; + + public static GLJournalEntry createNew(Office office, GLAccount glAccount, String transactionId, boolean portfolioGenerated, + Date entryDate, String type, BigDecimal amount, String description, String entityType, Long entityId) { + return new GLJournalEntry(office, glAccount, transactionId, portfolioGenerated, entryDate, type, amount, description, entityType, + entityId); + } + + protected GLJournalEntry() { + // + } + + public GLJournalEntry(Office office, GLAccount glAccount, String transactionId, boolean portfolioGenerated, Date entryDate, + String type, BigDecimal amount, String description, String entityType, Long entityId) { + this.office = office; + this.glAccount = glAccount; + this.reversalJournalEntry = null; + this.transactionId = transactionId; + this.reversed = false; + this.portfolioGenerated = portfolioGenerated; + this.entryDate = entryDate; + this.type = type; + this.amount = amount; + this.description = StringUtils.defaultIfEmpty(description, null); + this.entityType = StringUtils.defaultIfEmpty(entityType, null); + this.entityId = entityId; + } + + public String getType() { + return this.type; + } + + public Office getOffice() { + return this.office; + } + + public GLAccount getGlAccount() { + return this.glAccount; + } + + public Date getEntryDate() { + return this.entryDate; + } + + public BigDecimal getAmount() { + return this.amount; + } + + public void setReversalJournalEntry(GLJournalEntry reversalJournalEntry) { + this.reversalJournalEntry = reversalJournalEntry; + } + + public void setReversed(boolean reversed) { + this.reversed = reversed; + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLJournalEntryRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLJournalEntryRepository.java new file mode 100755 index 000000000..3739d13a2 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLJournalEntryRepository.java @@ -0,0 +1,15 @@ +package org.mifosplatform.accounting.domain; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface GLJournalEntryRepository extends JpaRepository, JpaSpecificationExecutor, + GLJournalEntryRepositoryCustom { + + @Query("from GLJournalEntry journalEntry where journalEntry.transactionId= :transactionId and journalEntry.reversed is false and journalEntry.portfolioGenerated is false") + List findUnReversedManualJournalEntriesByTransactionId(@Param("transactionId") String transactionId); +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLJournalEntryRepositoryCustom.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLJournalEntryRepositoryCustom.java new file mode 100755 index 000000000..4a17d63b6 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLJournalEntryRepositoryCustom.java @@ -0,0 +1,8 @@ +package org.mifosplatform.accounting.domain; + +import java.util.List; + +public interface GLJournalEntryRepositoryCustom { + + List findFirstJournalEntryForAccount(long glAccountId); +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLJournalEntryRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLJournalEntryRepositoryImpl.java new file mode 100755 index 000000000..31a8f304a --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/domain/GLJournalEntryRepositoryImpl.java @@ -0,0 +1,24 @@ +package org.mifosplatform.accounting.domain; + +import java.util.List; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.springframework.stereotype.Repository; + +@Repository +public class GLJournalEntryRepositoryImpl implements GLJournalEntryRepositoryCustom { + + @PersistenceContext + private EntityManager entityManager; + + @Override + @SuppressWarnings("unchecked") + public List findFirstJournalEntryForAccount(long glAccountId) { + List journalEntries = entityManager + .createQuery("SELECT journalEntry FROM GLJournalEntry journalEntry where journalEntry.glAccount.id= :glAccountId") + .setParameter("glAccountId", glAccountId).setFirstResult(0).setMaxResults(1).getResultList(); + return journalEntries; + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/exceptions/GLAccountInvalidDeleteException.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/exceptions/GLAccountInvalidDeleteException.java index 29af52dee..c2965e2bb 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/exceptions/GLAccountInvalidDeleteException.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/exceptions/GLAccountInvalidDeleteException.java @@ -18,6 +18,13 @@ public class GLAccountInvalidDeleteException extends AbstractPlatformDomainRuleE } else if (name().toString().equalsIgnoreCase("HAS_CHILDREN")) { return "Cannot delete this Header GL Account without first deleting or reassinging its children"; } return name().toString(); } + + public String errorCode() { + if (name().toString().equalsIgnoreCase("TRANSANCTIONS_LOGGED")) { + return "error.msg.glaccount.glcode.invalid.delete.transactions.logged"; + } else if (name().toString().equalsIgnoreCase("HAS_CHILDREN")) { return "error.msg.glaccount.glcode.invalid.delete.has.children"; } + return name().toString(); + } } public GLAccountInvalidDeleteException(final GL_ACCOUNT_INVALID_DELETE_REASON reason, final Long glAccountId) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/exceptions/GLClosureInvalidDeleteException.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/exceptions/GLClosureInvalidDeleteException.java new file mode 100755 index 000000000..c31854a5d --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/exceptions/GLClosureInvalidDeleteException.java @@ -0,0 +1,16 @@ +package org.mifosplatform.accounting.exceptions; + +import java.util.Date; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +/** + * A {@link RuntimeException} thrown when a GL Closure Delte command is invalid + */ +public class GLClosureInvalidDeleteException extends AbstractPlatformDomainRuleException { + + public GLClosureInvalidDeleteException(final Long officeId, final String officeName, final Date latestclosureDate) { + super("error.msg.glclosure.invalid.delete", "The latest closure for office with Id " + officeId + " and name " + officeName + " is on " + + latestclosureDate.toString() + ", please delete this closure first", latestclosureDate); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/exceptions/GLClosureInvalidException.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/exceptions/GLClosureInvalidException.java new file mode 100755 index 000000000..6aa3e6fa3 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/exceptions/GLClosureInvalidException.java @@ -0,0 +1,34 @@ +package org.mifosplatform.accounting.exceptions; + +import java.util.Date; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +/** + * A {@link RuntimeException} thrown when a GL Closure is Invalid + */ +public class GLClosureInvalidException extends AbstractPlatformDomainRuleException { + + /*** enum of reasons for invalid Accounting Closure **/ + public static enum GL_CLOSURE_INVALID_REASON { + FUTURE_DATE, ACCOUNTING_CLOSED; + + public String errorMessage() { + if (name().toString().equalsIgnoreCase("FUTURE_DATE")) { + return "Accounting closures cannot be made for a future date"; + } else if (name().toString().equalsIgnoreCase("ACCOUNTING_CLOSED")) { return "Accounting Closure for this branch has already been defined for a greater date"; } + return name().toString(); + } + + public String errorCode() { + if (name().toString().equalsIgnoreCase("FUTURE_DATE")) { + return "error.msg.glclosure.invalid.future.date"; + } else if (name().toString().equalsIgnoreCase("ACCOUNTING_CLOSED")) { return "error.msg.glclosure.invalid.accounting.closed"; } + return name().toString(); + } + } + + public GLClosureInvalidException(final GL_CLOSURE_INVALID_REASON reason, final Date date) { + super(reason.errorCode(), reason.errorMessage(), date); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/exceptions/GLJournalEntriesNotFoundException.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/exceptions/GLJournalEntriesNotFoundException.java new file mode 100755 index 000000000..c366bb6ea --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/exceptions/GLJournalEntriesNotFoundException.java @@ -0,0 +1,18 @@ +package org.mifosplatform.accounting.exceptions; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +/** + * A {@link RuntimeException} thrown when journal entry resources are not found. + */ +public class GLJournalEntriesNotFoundException extends AbstractPlatformResourceNotFoundException { + + public GLJournalEntriesNotFoundException(final String transactionId) { + super("error.msg.journalEntries.transactionId.invalid", "Journal Entries with transaction Identifier " + transactionId + + " does not exist or are not system generated/reversible ", transactionId); + } + + public GLJournalEntriesNotFoundException(final Long entryId) { + super("error.msg.journalEntries.id.invalid", "Journal Entry with entry Id " + entryId + " does not exist ", entryId); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/exceptions/GLJournalEntryInvalidException.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/exceptions/GLJournalEntryInvalidException.java new file mode 100755 index 000000000..0743e6c30 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/exceptions/GLJournalEntryInvalidException.java @@ -0,0 +1,46 @@ +package org.mifosplatform.accounting.exceptions; + +import java.util.Date; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +/** + * A {@link RuntimeException} thrown when a GL Journal Entry is Invalid + */ +public class GLJournalEntryInvalidException extends AbstractPlatformDomainRuleException { + + /*** enum of reasons for invalid Journal Entry **/ + public static enum GL_JOURNAL_ENTRY_INVALID_REASON { + FUTURE_DATE, ACCOUNTING_CLOSED, NO_DEBITS_OR_CREDITS, DEBIT_CREDIT_SUM_MISMATCH, DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY; + + public String errorMessage() { + if (name().toString().equalsIgnoreCase("FUTURE_DATE")) { + return "The journal entry cannot be made for a future date"; + } else if (name().toString().equalsIgnoreCase("ACCOUNTING_CLOSED")) { + return "Journal entry cannot be made prior to last account closing date for the branch"; + } else if (name().toString().equalsIgnoreCase("NO_DEBITS_OR_CREDITS")) { + return "Journal Entry must have atleast one Debit and one Credit"; + } else if (name().toString().equalsIgnoreCase("DEBIT_CREDIT_SUM_MISMATCH")) { + return "Sum of All Debits must equal the sum of all Credits for a Journal Entry"; + } else if (name().toString().equalsIgnoreCase("DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY")) { return "Both account and amount must be specified for all Debits and Credits"; } + return name().toString(); + } + + public String errorCode() { + if (name().toString().equalsIgnoreCase("FUTURE_DATE")) { + return "error.msg.glJournalEntry.invalid.future.date"; + } else if (name().toString().equalsIgnoreCase("ACCOUNTING_CLOSED")) { + return "error.msg.glJournalEntry.invalid.accounting.closed"; + } else if (name().toString().equalsIgnoreCase("NO_DEBITS_OR_CREDITS")) { + return "error.msg.glJournalEntry.invalid.no.debits.or.credits"; + } else if (name().toString().equalsIgnoreCase("DEBIT_CREDIT_SUM_MISMATCH")) { + return "error.msg.glJournalEntry.invalid.mismatch.debits.credits"; + } else if (name().toString().equalsIgnoreCase("DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY")) { return "error.msg.glJournalEntry.invalid.empty.account.or.amount"; } + return name().toString(); + } + } + + public GLJournalEntryInvalidException(final GL_JOURNAL_ENTRY_INVALID_REASON reason, final Date date) { + super(reason.errorCode(), reason.errorMessage(), date); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/GLAccountCommandValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/GLAccountCommandValidator.java index 069d42dea..0324d2248 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/GLAccountCommandValidator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/GLAccountCommandValidator.java @@ -31,6 +31,8 @@ public class GLAccountCommandValidator { baseDataValidator.reset().parameter("classification").value(command.getClassification()).notBlank().notExceedingLengthOf(45); baseDataValidator.reset().parameter("description").value(command.getDescription()).ignoreIfNull().notExceedingLengthOf(500); + + baseDataValidator.reset().parameter("manualEntriesAllowed").value(command.getManualEntriesAllowed()).notBlank(); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/GLJournalEntryCommandValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/GLJournalEntryCommandValidator.java new file mode 100755 index 000000000..67301d097 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/GLJournalEntryCommandValidator.java @@ -0,0 +1,35 @@ +package org.mifosplatform.accounting.service; + +import java.util.ArrayList; +import java.util.List; + +import org.mifosplatform.accounting.api.commands.GLJournalEntryCommand; +import org.mifosplatform.infrastructure.core.data.ApiParameterError; +import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder; +import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; + +public class GLJournalEntryCommandValidator { + + private final GLJournalEntryCommand command; + + public GLJournalEntryCommandValidator(GLJournalEntryCommand command) { + this.command = command; + } + + public void validateForCreate() { + + List dataValidationErrors = new ArrayList(); + + DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("GLJournalEntry"); + + baseDataValidator.reset().parameter("entryDate").value(command.getEntryDate()).notBlank(); + + baseDataValidator.reset().parameter("officeId").value(command.getOfficeId()).notNull().integerGreaterThanZero(); + + baseDataValidator.reset().parameter("comments").value(command.getComments()).ignoreIfNull().notExceedingLengthOf(500); + + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", + "Validation errors exist.", dataValidationErrors); } + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/GLJournalEntryReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/GLJournalEntryReadPlatformService.java new file mode 100755 index 000000000..891b0eb47 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/GLJournalEntryReadPlatformService.java @@ -0,0 +1,15 @@ +package org.mifosplatform.accounting.service; + +import java.util.Date; +import java.util.List; + +import org.mifosplatform.accounting.api.data.GLJournalEntryData; + +public interface GLJournalEntryReadPlatformService { + + List retrieveAllGLJournalEntries(Long officeId, Long glAccountId, Boolean portfolioGenerated, Date fromDate, + Date toDate); + + GLJournalEntryData retrieveGLJournalEntryById(long glJournalEntryId); + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/GLJournalEntryWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/GLJournalEntryWritePlatformService.java new file mode 100755 index 000000000..4194f54c6 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/GLJournalEntryWritePlatformService.java @@ -0,0 +1,14 @@ +package org.mifosplatform.accounting.service; + +import org.mifosplatform.accounting.api.commands.GLJournalEntryCommand; +import org.springframework.security.access.prepost.PreAuthorize; + +public interface GLJournalEntryWritePlatformService { + + @PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'ORGANISATION_ADMINISTRATION_SUPER_USER', 'CREATE_JOURNAL_ENTRY')") + String createJournalEntry(GLJournalEntryCommand journalEntryCommand); + + @PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'ORGANISATION_ADMINISTRATION_SUPER_USER', 'REVERT_JOURNAL_ENTRY')") + String revertJournalEntry(String transactionId); + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLAccountReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLAccountReadPlatformServiceImpl.java index bc2bf5033..7504a1082 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLAccountReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLAccountReadPlatformServiceImpl.java @@ -2,6 +2,7 @@ package org.mifosplatform.accounting.service.impl; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.Arrays; import java.util.List; import org.apache.commons.lang.StringUtils; @@ -11,7 +12,6 @@ import org.mifosplatform.accounting.exceptions.GLAccountInvalidClassificationExc import org.mifosplatform.accounting.exceptions.GLAccountNotFoundException; import org.mifosplatform.accounting.service.GLAccountReadPlatformService; import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; -import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; @@ -22,19 +22,16 @@ import org.springframework.stereotype.Service; public class GLAccountReadPlatformServiceImpl implements GLAccountReadPlatformService { private final JdbcTemplate jdbcTemplate; - @SuppressWarnings("unused") - private final PlatformSecurityContext context; @Autowired - public GLAccountReadPlatformServiceImpl(final PlatformSecurityContext context, final TenantAwareRoutingDataSource dataSource) { - this.context = context; + public GLAccountReadPlatformServiceImpl(final TenantAwareRoutingDataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } private static final class GLAccountMapper implements RowMapper { public String schema() { - return " id as id, name as name, parent_id as parentId, gl_code as glCode, disabled as disabled, manual_entries_allowed as manualEntriesAllowed, " + return " id as id, name as name, parent_id as parentId, gl_code as glCode, disabled as disabled, manual_journal_entries_allowed as manualEntriesAllowed, " + "classification as classification, header_account as headerAccount, description as description " + "from acc_gl_account"; } @@ -65,15 +62,30 @@ public class GLAccountReadPlatformServiceImpl implements GLAccountReadPlatformSe GLAccountMapper rm = new GLAccountMapper(); String sql = "select " + rm.schema(); + Object[] paramaterArray = new Object[3]; + int arrayPos = 0; if (StringUtils.isNotBlank(accountClassification) && searchParam != null) { sql += " where classification like ? and ( name like %?% or glCode like %?% )"; + paramaterArray[arrayPos] = accountClassification; + arrayPos = arrayPos++; + paramaterArray[arrayPos] = searchParam; + arrayPos = arrayPos++; + paramaterArray[arrayPos] = searchParam; + arrayPos = arrayPos++; } else if (StringUtils.isNotBlank(accountClassification)) { sql += " where classification like ?"; + paramaterArray[arrayPos] = accountClassification; + arrayPos++; } else if (searchParam != null) { sql += " where ( name like %?% or glCode like %?% )"; + paramaterArray[arrayPos] = searchParam; + arrayPos++; + paramaterArray[arrayPos] = searchParam; + arrayPos++; } sql = sql + " order by glCode"; - return this.jdbcTemplate.query(sql, rm, new Object[] { accountClassification, searchParam }); + Object[] finalObjectArray = Arrays.copyOf(paramaterArray, arrayPos); + return this.jdbcTemplate.query(sql, rm, finalObjectArray); } @Override @@ -93,7 +105,7 @@ public class GLAccountReadPlatformServiceImpl implements GLAccountReadPlatformSe private static boolean checkValidGLAccountClassification(final String entityType) { for (GL_ACCOUNT_CLASSIFICATION classification : GL_ACCOUNT_CLASSIFICATION.values()) { - if (classification.name().equalsIgnoreCase(entityType)) { return true; } + if (classification.name().toString().equalsIgnoreCase(entityType)) { return true; } } return false; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLAccountWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLAccountWritePlatformServiceJpaRepositoryImpl.java index a35beaf7c..8fc31c8d3 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLAccountWritePlatformServiceJpaRepositoryImpl.java @@ -1,13 +1,20 @@ package org.mifosplatform.accounting.service.impl; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.mifosplatform.accounting.AccountingConstants.GL_ACCOUNT_CLASSIFICATION; import org.mifosplatform.accounting.api.commands.GLAccountCommand; import org.mifosplatform.accounting.domain.GLAccount; import org.mifosplatform.accounting.domain.GLAccountRepository; +import org.mifosplatform.accounting.domain.GLJournalEntry; +import org.mifosplatform.accounting.domain.GLJournalEntryRepository; import org.mifosplatform.accounting.exceptions.GLAccountDuplicateException; +import org.mifosplatform.accounting.exceptions.GLAccountInvalidClassificationException; import org.mifosplatform.accounting.exceptions.GLAccountInvalidDeleteException; +import org.mifosplatform.accounting.exceptions.GLAccountInvalidDeleteException.GL_ACCOUNT_INVALID_DELETE_REASON; import org.mifosplatform.accounting.exceptions.GLAccountInvalidParentException; import org.mifosplatform.accounting.exceptions.GLAccountNotFoundException; -import org.mifosplatform.accounting.exceptions.GLAccountInvalidDeleteException.GL_ACCOUNT_INVALID_DELETE_REASON; import org.mifosplatform.accounting.service.GLAccountCommandValidator; import org.mifosplatform.accounting.service.GLAccountWritePlatformService; import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; @@ -24,10 +31,13 @@ public class GLAccountWritePlatformServiceJpaRepositoryImpl implements GLAccount private final static Logger logger = LoggerFactory.getLogger(GLAccountWritePlatformServiceJpaRepositoryImpl.class); private final GLAccountRepository glAccountRepository; + private final GLJournalEntryRepository glJournalEntryRepository; @Autowired - public GLAccountWritePlatformServiceJpaRepositoryImpl(final GLAccountRepository glAccountRepository) { + public GLAccountWritePlatformServiceJpaRepositoryImpl(final GLAccountRepository glAccountRepository, + final GLJournalEntryRepository glJournalEntryRepository) { this.glAccountRepository = glAccountRepository; + this.glJournalEntryRepository = glJournalEntryRepository; } @Transactional @@ -37,12 +47,17 @@ public class GLAccountWritePlatformServiceJpaRepositoryImpl implements GLAccount GLAccountCommandValidator validator = new GLAccountCommandValidator(command); validator.validateForCreate(); + if (StringUtils.isNotBlank(command.getClassification())) { + if (!checkValidGLAccountClassification(command.getClassification())) { throw new GLAccountInvalidClassificationException( + command.getClassification()); } + } + // check parent is valid GLAccount parentGLAccount = null; if (command.getParentId() != null) { parentGLAccount = validateParentGLAccount(command); } - GLAccount glAccount = GLAccount.createNew(parentGLAccount, command.getName(), command.getGlCode(), command.getDisabled(), + GLAccount glAccount = GLAccount.createNew(parentGLAccount, command.getName(), command.getGlCode(), false, command.getManualEntriesAllowed(), command.getClassification(), command.getHeaderAccount(), command.getDescription()); this.glAccountRepository.saveAndFlush(glAccount); @@ -70,6 +85,12 @@ public class GLAccountWritePlatformServiceJpaRepositoryImpl implements GLAccount parentGLAccount = validateParentGLAccount(command); } + // validate new classification + if (command.isClassificationChanged()) { + if (!checkValidGLAccountClassification(command.getClassification())) { throw new GLAccountInvalidClassificationException( + command.getClassification()); } + } + glAccount.update(command, parentGLAccount); this.glAccountRepository.saveAndFlush(glAccount); @@ -77,6 +98,26 @@ public class GLAccountWritePlatformServiceJpaRepositoryImpl implements GLAccount return glAccount.getId(); } + @Transactional + @Override + public Long deleteGLAccount(Long glAccountId) { + final GLAccount glAccount = this.glAccountRepository.findOne(glAccountId); + + if (glAccount == null) { throw new GLAccountNotFoundException(glAccountId); } + + // validate this isn't a header account that has children + if (glAccount.isHeaderAccount() && glAccount.getChildren().size() > 0) { throw new GLAccountInvalidDeleteException( + GL_ACCOUNT_INVALID_DELETE_REASON.HAS_CHILDREN, glAccountId); } + + // TODO: does this account have transactions logged against it + List journalEntriesForAccount = glJournalEntryRepository.findFirstJournalEntryForAccount(glAccountId); + if (journalEntriesForAccount.size() > 0) { throw new GLAccountInvalidDeleteException( + GL_ACCOUNT_INVALID_DELETE_REASON.TRANSANCTIONS_LOGGED, glAccountId); } + this.glAccountRepository.delete(glAccount); + + return glAccountId; + } + /** * @param command * @return @@ -106,21 +147,10 @@ public class GLAccountWritePlatformServiceJpaRepositoryImpl implements GLAccount "Unknown data integrity issue with resource GL Account: " + realCause.getMessage()); } - @Transactional - @Override - public Long deleteGLAccount(Long glAccountId) { - final GLAccount glAccount = this.glAccountRepository.findOne(glAccountId); - - if (glAccount == null) { throw new GLAccountNotFoundException(glAccountId); } - - // validate this isn't a header account that has children - if (glAccount.isHeaderAccount() && glAccount.getChildren().size() > 0) { throw new GLAccountInvalidDeleteException( - GL_ACCOUNT_INVALID_DELETE_REASON.HAS_CHILDREN, glAccountId); } - - // TODO: does this account have transactions logged against it - - this.glAccountRepository.delete(glAccount); - - return glAccountId; + private static boolean checkValidGLAccountClassification(final String entityType) { + for (GL_ACCOUNT_CLASSIFICATION classification : GL_ACCOUNT_CLASSIFICATION.values()) { + if (classification.name().toString().equalsIgnoreCase(entityType)) { return true; } + } + return false; } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLClosureReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLClosureReadPlatformServiceImpl.java index 4dce802d4..9f01998b4 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLClosureReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLClosureReadPlatformServiceImpl.java @@ -2,6 +2,7 @@ package org.mifosplatform.accounting.service.impl; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.Arrays; import java.util.List; import org.joda.time.LocalDate; @@ -10,7 +11,6 @@ import org.mifosplatform.accounting.exceptions.GLClosureNotFoundException; import org.mifosplatform.accounting.service.GLClosureReadPlatformService; import org.mifosplatform.infrastructure.core.domain.JdbcSupport; import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; -import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; @@ -21,12 +21,9 @@ import org.springframework.stereotype.Service; public class GLClosureReadPlatformServiceImpl implements GLClosureReadPlatformService { private final JdbcTemplate jdbcTemplate; - @SuppressWarnings("unused") - private final PlatformSecurityContext context; @Autowired - public GLClosureReadPlatformServiceImpl(final PlatformSecurityContext context, final TenantAwareRoutingDataSource dataSource) { - this.context = context; + public GLClosureReadPlatformServiceImpl(final TenantAwareRoutingDataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } @@ -36,9 +33,9 @@ public class GLClosureReadPlatformServiceImpl implements GLClosureReadPlatformSe return " glClosure.id as id, glClosure.office_id as officeId,office.name as officeName ,glClosure.closing_date as closingDate," + " glClosure.is_deleted as isDeleted, creatingUser.id as creatingUserId,creatingUser.username as creatingUserName," + " updatingUser.id as updatingUserId,updatingUser.username as updatingUserName, glClosure.created_date as createdDate," - + " glClosure.lastmodified_date as updatedDate from acc_gl_Closure as glClosure, m_appuser as creatingUser, " - + " glClosure.comments as comments " - + " m_appuser as updatingUser,m_office as office where glClosure.createdby_id=creatingUser.id and " + + " glClosure.lastmodified_date as updatedDate, glClosure.comments as comments " + + " from acc_gl_Closure as glClosure, m_appuser as creatingUser, m_appuser as updatingUser,m_office as office" + + " where glClosure.createdby_id=creatingUser.id and " + " glClosure.lastmodifiedby_id=updatingUser.id and glClosure.office_id=office.id"; } @@ -53,11 +50,13 @@ public class GLClosureReadPlatformServiceImpl implements GLClosureReadPlatformSe LocalDate createdDate = JdbcSupport.getLocalDate(rs, "createdDate"); LocalDate lastUpdatedDate = JdbcSupport.getLocalDate(rs, "updatedDate"); Long creatingByUserId = rs.getLong("creatingUserId"); + String createdByUserName = rs.getString("creatingUserName"); Long lastUpdatedByUserId = rs.getLong("updatingUserId"); + String lastUpdatedByUserName = rs.getString("updatingUserName"); String comments = rs.getString("comments"); return new GLClosureData(id, officeId, officeName, closingDate, deleted, createdDate, lastUpdatedDate, creatingByUserId, - lastUpdatedByUserId, comments); + createdByUserName, lastUpdatedByUserId, lastUpdatedByUserName, comments); } } @@ -66,12 +65,18 @@ public class GLClosureReadPlatformServiceImpl implements GLClosureReadPlatformSe GLClosureMapper rm = new GLClosureMapper(); String sql = "select " + rm.schema() + " and glClosure.is_deleted = 0"; + Object[] objectArray = new Object[1]; + int arrayPos = 0; if (officeId != null && officeId != 0) { sql += " and glClosure.office_id = ?"; + objectArray[arrayPos] = officeId; + arrayPos = arrayPos + 1; } - sql = sql + " glClosure.closing_date"; - return this.jdbcTemplate.query(sql, rm, new Object[] { officeId }); + sql = sql + " order by glClosure.closing_date"; + + Object[] finalObjectArray = Arrays.copyOf(objectArray, arrayPos); + return this.jdbcTemplate.query(sql, rm, finalObjectArray); } @Override @@ -79,7 +84,7 @@ public class GLClosureReadPlatformServiceImpl implements GLClosureReadPlatformSe try { GLClosureMapper rm = new GLClosureMapper(); - String sql = "select " + rm.schema() + " and glClosure,id = ?"; + String sql = "select " + rm.schema() + " and glClosure.id = ?"; GLClosureData glAccountData = this.jdbcTemplate.queryForObject(sql, rm, new Object[] { glClosureId }); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLClosureWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLClosureWritePlatformServiceJpaRepositoryImpl.java index 5f0ae0e73..582cc207e 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLClosureWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLClosureWritePlatformServiceJpaRepositoryImpl.java @@ -1,9 +1,15 @@ package org.mifosplatform.accounting.service.impl; + +import java.util.Date; + import org.mifosplatform.accounting.api.commands.GLClosureCommand; import org.mifosplatform.accounting.domain.GLClosure; import org.mifosplatform.accounting.domain.GLClosureRepository; import org.mifosplatform.accounting.exceptions.GLClosureDuplicateException; +import org.mifosplatform.accounting.exceptions.GLClosureInvalidDeleteException; +import org.mifosplatform.accounting.exceptions.GLClosureInvalidException; +import org.mifosplatform.accounting.exceptions.GLClosureInvalidException.GL_CLOSURE_INVALID_REASON; import org.mifosplatform.accounting.exceptions.GLClosureNotFoundException; import org.mifosplatform.accounting.service.GLClosureCommandValidator; import org.mifosplatform.accounting.service.GLClosureWritePlatformService; @@ -44,6 +50,16 @@ public class GLClosureWritePlatformServiceJpaRepositoryImpl implements GLClosure final Office office = this.officeRepository.findOne(command.getOfficeId()); if (office == null) { throw new OfficeNotFoundException(command.getOfficeId()); } + // ensure closure date is not in the future + Date todaysDate = new Date(); + Date closureDate = command.getClosingDate().toDateMidnight().toDate(); + if (closureDate.after(todaysDate)) { throw new GLClosureInvalidException(GL_CLOSURE_INVALID_REASON.FUTURE_DATE, closureDate); } + // shouldn't be before an existing accounting closure + GLClosure latestGLClosure = glClosureRepository.getLatestGLClosureByBranch(command.getOfficeId()); + if (latestGLClosure != null) { + if (latestGLClosure.getClosingDate().after(closureDate)) { throw new GLClosureInvalidException( + GL_CLOSURE_INVALID_REASON.ACCOUNTING_CLOSED, latestGLClosure.getClosingDate()); } + } GLClosure glClosure = GLClosure.createNew(office, command.getClosingDate(), command.getComments()); this.glClosureRepository.saveAndFlush(glClosure); @@ -79,8 +95,14 @@ public class GLClosureWritePlatformServiceJpaRepositoryImpl implements GLClosure if (glClosure == null) { throw new GLClosureNotFoundException(glClosureId); } - // TODO: check if any closures are present for this branch at a later - // date than this closure + /** + * check if any closures are present for this branch at a later date + * than this closure date + **/ + Date closureDate = glClosure.getClosingDate(); + GLClosure latestGLClosure = glClosureRepository.getLatestGLClosureByBranch(glClosure.getOffice().getId()); + if (latestGLClosure.getClosingDate().after(closureDate)) { throw new GLClosureInvalidDeleteException(latestGLClosure.getOffice() + .getId(), latestGLClosure.getOffice().getName(), latestGLClosure.getClosingDate()); } this.glClosureRepository.delete(glClosure); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLJournalEntryReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLJournalEntryReadPlatformServiceImpl.java new file mode 100755 index 000000000..836fa1435 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLJournalEntryReadPlatformServiceImpl.java @@ -0,0 +1,151 @@ +package org.mifosplatform.accounting.service.impl; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import org.joda.time.LocalDate; +import org.mifosplatform.accounting.api.data.GLJournalEntryData; +import org.mifosplatform.accounting.exceptions.GLJournalEntriesNotFoundException; +import org.mifosplatform.accounting.service.GLJournalEntryReadPlatformService; +import org.mifosplatform.infrastructure.core.domain.JdbcSupport; +import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; +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 GLJournalEntryReadPlatformServiceImpl implements GLJournalEntryReadPlatformService { + + private final JdbcTemplate jdbcTemplate; + + @Autowired + public GLJournalEntryReadPlatformServiceImpl(final TenantAwareRoutingDataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + private static final class GLJournalEntryMapper implements RowMapper { + + public String schema() { + return " journalEntry.id as journalEntryId, glAccount.classification as classification ," + + " glAccount.name as glAccountName, journalEntry.account_id as glAccountId," + + " journalEntry.office_id as officeId, office.name as officeName, " + + " journalEntry.portfolio_generated as portfolioGenerated,journalEntry.entry_date as entryDate, " + + " journalEntry.type as entryType,journalEntry.amount as amount, journalEntry.transaction_id as transactionId," + + " journalEntry.entity_type as entityType, journalEntry.entity_id as entityId, creatingUser.id as createdByUserId, " + + " creatingUser.username as createdByUserName, journalEntry.description as comments, " + + " journalEntry.created_date as createdDate, journalEntry.reversed as reversed " + + " from acc_gl_journal_entry journalEntry, acc_gl_account glAccount, m_office office, m_appuser creatingUser " + + " where journalEntry.account_id = glAccount.id " + + " and journalEntry.office_id = office.id and journalEntry.createdby_id = creatingUser.id "; + } + + @Override + public GLJournalEntryData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + Long journalEntryId = rs.getLong("journalEntryId"); + Long officeId = rs.getLong("officeId"); + String officeName = rs.getString("officeName"); + String glAccountName = rs.getString("glAccountName"); + Long glAccountId = rs.getLong("glAccountId"); + String glAccountClassification = rs.getString("classification"); + LocalDate entryDate = JdbcSupport.getLocalDate(rs, "entryDate"); + Boolean portfolioGenerated = rs.getBoolean("portfolioGenerated"); + BigDecimal amount = rs.getBigDecimal("amount"); + String entryType = rs.getString("entryType"); + String transactionId = rs.getString("transactionId"); + String entityType = rs.getString("entityType"); + Long entityId = rs.getLong("entityId"); + Long createdByUserId = rs.getLong("createdByUserId"); + LocalDate createdDate = JdbcSupport.getLocalDate(rs, "createdDate"); + String createdByUserName = rs.getString("createdByUserName"); + String comments = rs.getString("comments"); + Boolean reversed = rs.getBoolean("reversed"); + + return new GLJournalEntryData(journalEntryId, officeId, officeName, glAccountName, glAccountId, glAccountClassification, + entryDate, entryType, amount, transactionId, portfolioGenerated, entityType, entityId, createdByUserId, createdDate, + createdByUserName, comments, reversed); + } + } + + @Override + public List retrieveAllGLJournalEntries(Long officeId, Long glAccountId, Boolean portfolioGenerated, Date fromDate, + Date toDate) { + GLJournalEntryMapper rm = new GLJournalEntryMapper(); + + String sql = "select " + rm.schema(); + Object[] objectArray = new Object[4]; + int arrayPos = 0; + + if (officeId != null && officeId != 0) { + sql += " and journalEntry.office_id = ?"; + objectArray[arrayPos] = officeId; + arrayPos = arrayPos + 1; + } + + if (glAccountId != null && glAccountId != 0) { + sql += " and journalEntry.account_id = ?"; + objectArray[arrayPos] = glAccountId; + arrayPos = arrayPos + 1; + } + + if (fromDate != null || toDate != null) { + DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + String fromDateString = null; + String toDateString = null; + if (fromDate != null && toDate != null) { + sql += " and journalEntry.entry_date between ? and ? "; + fromDateString = df.format(fromDate); + toDateString = df.format(toDate); + objectArray[arrayPos] = fromDateString; + arrayPos = arrayPos + 1; + objectArray[arrayPos] = toDateString; + arrayPos = arrayPos + 1; + } else if (fromDate != null) { + sql += " and journalEntry.entry_date >= ? "; + fromDateString = df.format(fromDate); + objectArray[arrayPos] = fromDateString; + arrayPos = arrayPos + 1; + } else if (toDate != null) { + sql += " and journalEntry.entry_date <= ? "; + toDateString = df.format(toDate); + objectArray[arrayPos] = toDateString; + arrayPos = arrayPos + 1; + } + } + + if (portfolioGenerated != null) { + if (portfolioGenerated) { + sql += " and journalEntry.portfolio_generated = 1"; + } else { + sql += " and journalEntry.portfolio_generated = 0"; + } + } + + Object[] finalObjectArray = Arrays.copyOf(objectArray, arrayPos); + return this.jdbcTemplate.query(sql, rm, finalObjectArray); + } + + @Override + public GLJournalEntryData retrieveGLJournalEntryById(long glJournalEntryId) { + try { + + GLJournalEntryMapper rm = new GLJournalEntryMapper(); + String sql = "select " + rm.schema() + " and journalEntry.id = ?"; + + GLJournalEntryData glAccountData = this.jdbcTemplate.queryForObject(sql, rm, new Object[] { glJournalEntryId }); + + return glAccountData; + } catch (EmptyResultDataAccessException e) { + throw new GLJournalEntriesNotFoundException(glJournalEntryId); + } + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLJournalEntryWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLJournalEntryWritePlatformServiceJpaRepositoryImpl.java new file mode 100755 index 000000000..6f0591769 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/service/impl/GLJournalEntryWritePlatformServiceJpaRepositoryImpl.java @@ -0,0 +1,199 @@ +package org.mifosplatform.accounting.service.impl; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +import org.apache.commons.lang.RandomStringUtils; +import org.apache.commons.lang.StringUtils; +import org.joda.time.LocalDate; +import org.mifosplatform.accounting.AccountingConstants.JOURNAL_ENTRY_TYPE; +import org.mifosplatform.accounting.api.commands.GLJournalEntryCommand; +import org.mifosplatform.accounting.api.commands.SingleDebitOrCreditEntryCommand; +import org.mifosplatform.accounting.domain.GLAccount; +import org.mifosplatform.accounting.domain.GLAccountRepository; +import org.mifosplatform.accounting.domain.GLClosure; +import org.mifosplatform.accounting.domain.GLClosureRepository; +import org.mifosplatform.accounting.domain.GLJournalEntry; +import org.mifosplatform.accounting.domain.GLJournalEntryRepository; +import org.mifosplatform.accounting.exceptions.GLAccountNotFoundException; +import org.mifosplatform.accounting.exceptions.GLJournalEntriesNotFoundException; +import org.mifosplatform.accounting.exceptions.GLJournalEntryInvalidException; +import org.mifosplatform.accounting.exceptions.GLJournalEntryInvalidException.GL_JOURNAL_ENTRY_INVALID_REASON; +import org.mifosplatform.accounting.service.GLJournalEntryCommandValidator; +import org.mifosplatform.accounting.service.GLJournalEntryWritePlatformService; +import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; +import org.mifosplatform.organisation.office.domain.Office; +import org.mifosplatform.organisation.office.domain.OfficeRepository; +import org.mifosplatform.organisation.office.exception.OfficeNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class GLJournalEntryWritePlatformServiceJpaRepositoryImpl implements GLJournalEntryWritePlatformService { + + private final static Logger logger = LoggerFactory.getLogger(GLJournalEntryWritePlatformServiceJpaRepositoryImpl.class); + + private final GLClosureRepository glClosureRepository; + private final GLAccountRepository glAccountRepository; + private final GLJournalEntryRepository glJournalEntryRepository; + private final OfficeRepository officeRepository; + + @Autowired + public GLJournalEntryWritePlatformServiceJpaRepositoryImpl(final GLClosureRepository glClosureRepository, + final GLAccountRepository glAccountRepository, final GLJournalEntryRepository glJournalEntryRepository, + final OfficeRepository officeRepository) { + this.glClosureRepository = glClosureRepository; + this.officeRepository = officeRepository; + this.glAccountRepository = glAccountRepository; + this.glJournalEntryRepository = glJournalEntryRepository; + } + + @Transactional + @Override + public String createJournalEntry(GLJournalEntryCommand command) { + try { + GLJournalEntryCommandValidator validator = new GLJournalEntryCommandValidator(command); + validator.validateForCreate(); + + // check office is valid + final Office office = this.officeRepository.findOne(command.getOfficeId()); + if (office == null) { throw new OfficeNotFoundException(command.getOfficeId()); } + + validateBusinessRulesForJournalEntries(command); + + /** Set a transaction Id and save these Journal entries **/ + Date entryDate = command.getEntryDate().toDateMidnight().toDate(); + String transactionId = generateTransactionId(); + + saveAllDebitOrCreditEntries(command, office, entryDate, command.getDebits(), transactionId, JOURNAL_ENTRY_TYPE.DEBIT); + + saveAllDebitOrCreditEntries(command, office, entryDate, command.getCredits(), transactionId, JOURNAL_ENTRY_TYPE.CREDIT); + + return transactionId; + } catch (DataIntegrityViolationException dve) { + handleJournalEntryDataIntegrityIssues(dve); + return null; + } + } + + @Transactional + @Override + public String revertJournalEntry(String transactionId) { + + // is the transaction Id valid + List journalEntries = glJournalEntryRepository.findUnReversedManualJournalEntriesByTransactionId(transactionId); + + if (journalEntries.size() <= 1) { throw new GLJournalEntriesNotFoundException(transactionId); } + + String reversalTransactionId = generateTransactionId(); + + for (GLJournalEntry journalEntry : journalEntries) { + GLJournalEntry reversalJournalEntry; + String reversalComment = "Reversal entry for Journal Entry with Entry Id :" + journalEntry.getId() + " and transaction Id " + + transactionId; + if (journalEntry.getType().equalsIgnoreCase(JOURNAL_ENTRY_TYPE.DEBIT.toString())) { + reversalJournalEntry = GLJournalEntry.createNew(journalEntry.getOffice(), journalEntry.getGlAccount(), + reversalTransactionId, false, journalEntry.getEntryDate(), JOURNAL_ENTRY_TYPE.CREDIT.toString(), + journalEntry.getAmount(), reversalComment, null, null); + } else { + reversalJournalEntry = GLJournalEntry.createNew(journalEntry.getOffice(), journalEntry.getGlAccount(), + reversalTransactionId, false, journalEntry.getEntryDate(), JOURNAL_ENTRY_TYPE.DEBIT.toString(), + journalEntry.getAmount(), reversalComment, null, null); + } + // save the reversal entry + this.glJournalEntryRepository.saveAndFlush(reversalJournalEntry); + journalEntry.setReversed(true); + journalEntry.setReversalJournalEntry(reversalJournalEntry); + // save the updated journal entry + this.glJournalEntryRepository.saveAndFlush(journalEntry); + } + return reversalTransactionId; + } + + private void validateBusinessRulesForJournalEntries(GLJournalEntryCommand command) { + + /** check if date of Journal entry is valid ***/ + final LocalDate entryLocalDate = command.getEntryDate(); + Date entryDate = entryLocalDate.toDateMidnight().toDate(); + // shouldn't be in the future + Date todaysDate = new Date(); + if (entryDate.after(todaysDate)) { throw new GLJournalEntryInvalidException(GL_JOURNAL_ENTRY_INVALID_REASON.FUTURE_DATE, entryDate); } + // shouldn't be before an accounting closure + GLClosure latestGLClosure = glClosureRepository.getLatestGLClosureByBranch(command.getOfficeId()); + if (latestGLClosure != null) { + if (latestGLClosure.getClosingDate().after(entryDate)) { throw new GLJournalEntryInvalidException( + GL_JOURNAL_ENTRY_INVALID_REASON.ACCOUNTING_CLOSED, latestGLClosure.getClosingDate()); } + } + /*** check if credits and debits are valid **/ + SingleDebitOrCreditEntryCommand[] credits = command.getCredits(); + SingleDebitOrCreditEntryCommand[] debits = command.getDebits(); + + // atleast one debit or credit must be present + if ((credits == null || credits.length <= 0) || (debits == null || debits.length <= 0)) { throw new GLJournalEntryInvalidException( + GL_JOURNAL_ENTRY_INVALID_REASON.NO_DEBITS_OR_CREDITS, null); } + + // sum of all debits must be = sum of all credits + BigDecimal creditsSum = BigDecimal.ZERO; + BigDecimal debitsSum = BigDecimal.ZERO; + for (SingleDebitOrCreditEntryCommand creditEntryCommand : credits) { + if (creditEntryCommand.getAmount() == null || creditEntryCommand.getGlAccountId() == null) { throw new GLJournalEntryInvalidException( + GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null); } + creditsSum = creditsSum.add(creditEntryCommand.getAmount()); + } + for (SingleDebitOrCreditEntryCommand debitEntryCommand : debits) { + if (debitEntryCommand.getAmount() == null || debitEntryCommand.getGlAccountId() == null) { throw new GLJournalEntryInvalidException( + GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null); } + debitsSum = debitsSum.add(debitEntryCommand.getAmount()); + } + if (creditsSum.compareTo(debitsSum) != 0) { throw new GLJournalEntryInvalidException( + GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_SUM_MISMATCH, null); } + } + + /** + * @param command + * @param office + * @param entryDate + * @param singleDebitOrCreditEntryCommands + * @param transactionId + */ + private void saveAllDebitOrCreditEntries(GLJournalEntryCommand command, final Office office, Date entryDate, + SingleDebitOrCreditEntryCommand[] singleDebitOrCreditEntryCommands, String transactionId, JOURNAL_ENTRY_TYPE type) { + for (SingleDebitOrCreditEntryCommand singleDebitOrCreditEntryCommand : singleDebitOrCreditEntryCommands) { + GLAccount glAccount = glAccountRepository.findOne(singleDebitOrCreditEntryCommand.getGlAccountId()); + if (glAccount == null) { throw new GLAccountNotFoundException(singleDebitOrCreditEntryCommand.getGlAccountId()); } + String comments = command.getComments(); + if (!StringUtils.isBlank(singleDebitOrCreditEntryCommand.getComments())) { + comments = singleDebitOrCreditEntryCommand.getComments(); + } + GLJournalEntry glJournalEntry = GLJournalEntry.createNew(office, glAccount, transactionId, false, entryDate, type.toString(), + singleDebitOrCreditEntryCommand.getAmount(), comments, null, null); + glJournalEntryRepository.saveAndFlush(glJournalEntry); + } + } + + /** + * @param command + * @param dve + */ + private void handleJournalEntryDataIntegrityIssues(DataIntegrityViolationException dve) { + 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.. + * + * @return + */ + private String generateTransactionId() { + return RandomStringUtils.random(15, "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadReportingServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadReportingServiceImpl.java index 1b3b39683..c35385846 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadReportingServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadReportingServiceImpl.java @@ -20,6 +20,7 @@ import javax.ws.rs.core.StreamingOutput; import org.apache.commons.lang.StringUtils; import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; import org.mifosplatform.infrastructure.core.service.FileUtils; +import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil; import org.mifosplatform.infrastructure.dataqueries.data.GenericResultsetData; import org.mifosplatform.infrastructure.dataqueries.data.ResultsetColumnHeader; import org.mifosplatform.infrastructure.dataqueries.data.ResultsetDataRow; @@ -256,7 +257,9 @@ public class ReadReportingServiceImpl implements ReadReportingService { // String reportPath = // "C:\\dev\\apache-tomcat-7.0.25\\webapps\\ROOT\\PentahoReports\\" // + reportName + ".prpt"; - String reportPath = "/var/lib/tomcat7/webapps/ROOT/PentahoReports/" + reportName + ".prpt"; + String reportPath = FileUtils.MIFOSX_BASE_DIR + File.separator + + ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim() + File.separator + "reports" + File.separator + + reportName + ".prpt"; logger.info("Report path: " + reportPath); // load report definition