Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.accounting.retainedearning.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.infrastructure.core.domain.ExternalId;

/**
* Entity for retained earning summary.
*/
@Getter
@Setter
@Entity
@Table(name = "acc_gl_journal_entry_annual_summary")
public class AccountGLJournalEntryAnnualSummary extends AbstractAuditableWithUTCDateTimeCustom<Long> {

@Column(name = "gl_code")
private String glCode;

@Column(name = "product_id")
private Long productId;

@Column(name = "office_id")
private Long officeId;

@Column(name = "opening_balance_amount")
private BigDecimal openingBalanceAmount;

@Column(name = "currency_code", nullable = false, length = 3)
private String currencyCode;

@Column(name = "owner_external_id")
private ExternalId ownerExternalId;

@Column(name = "manual_entry", nullable = false)
private Boolean manualEntry = false;

@Column(name = "year_end_date")
private LocalDate yearEndDate;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.accounting.retainedearning.domain;

import java.time.LocalDate;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

/**
* Repository for retained earning summary entity.
*/
public interface AccountGLJournalEntryAnnualSummaryRepository extends JpaRepository<AccountGLJournalEntryAnnualSummary, Long> {

List<AccountGLJournalEntryAnnualSummary> findByYearEndDate(LocalDate yerEndDate);
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ public final class GlobalConfigurationConstants {
public static final String FORCE_PASSWORD_RESET_ON_FIRST_LOGIN = "force-password-reset-on-first-login";
public static final String ALLOW_CASH_AND_NON_CASH_ACCRUAL = "allow-cash-and-non-cash-accrual";
public static final String BLOCK_TRANSACTIONS_ON_CLOSED_OVERPAID_LOANS = "block-transactions-on-closed-overpaid-loans";
public static final String INCOME_EXPENSE_GL_ACCOUNTS = "income-expense-gl-accounts";
public static final String LAST_DAY_OF_FINANCIAL_YEAR = "last-day-of-financial-year";
public static final String LAST_MONTH_OF_FINANCIAL_YEAR = "last-month-of-financial-year";
public static final String RETAINED_EARNING_GL_ACCOUNT = "retained-gl-account";
public static final String RETAINED_EARNING_USED_BY_REPORT_NAME = "retained-earning-used-by-report-name";
public static final String OFFICE_ID = "office-id";

private GlobalConfigurationConstants() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,18 @@ public interface ConfigurationDomainService {

Integer retrieveMaxLoginRetries();

String getIncomeExpenseGlAccounts();

String getRetainedEarningGlAccount();

Long getLastDayOfFinancialYear();

Long getLastMonthOfFinancialYear();

String getRetainedEarningUsedByReportName();

Long getOfficeId();

boolean isAllowCashAndNonCashAccrual();

boolean isBlockTransactionsOnClosedOverpaidLoansEnabled();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ public static class FineractJobProperties {
private int stuckRetryThreshold;
private boolean loanCobEnabled;
private FineractJournalEntryAggregationProperties journalEntryAggregation;
private int retainedEarningChunkSize;
}

@Getter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public enum JobName {
ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS("Add Accrual Transactions For Savings"), //
JOURNAL_ENTRY_AGGREGATION("Journal Entry Aggregation"), //
WORKING_CAPITAL_LOAN_COB_JOB("Working Capital Loan COB"), //
RETAINED_EARNING("Retained Earning Job"), //
; //

private final String name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public enum DefaultJob implements Job {
ADD_ACCRUAL_TRANSACTIONS_FOR_LOANS_WITH_INCOME_POSTED_AS_TRANSACTIONS(
"Add Accrual Transactions For Loans With Income Posted As Transactions", "LA_AATR"), //
RECALCULATE_INTEREST_FOR_LOANS("Recalculate Interest For Loans", "LA_RINT"), //
WORKING_CAPITAL_LOAN_COB("Working Capital Loan COB", "WC_COB"); //
WORKING_CAPITAL_LOAN_COB("Working Capital Loan COB", "WC_COB"), //
RETAINED_EARNING("Retained Earning Job", "RE_ERNG");

private final String customName;
private final String shortName;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.test.stepdef.common;

import static org.assertj.core.api.Assertions.assertThat;

import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.client.feign.FineractFeignClient;
import org.apache.fineract.client.models.PostOfficesResponse;
import org.apache.fineract.test.stepdef.AbstractStepDef;
import org.apache.fineract.test.support.TestContextKey;
import org.springframework.jdbc.core.JdbcTemplate;

@RequiredArgsConstructor
public class AnnualSummaryStepDef extends AbstractStepDef {

private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.ENGLISH);
private static final String OFFICE_ID_CONFIG = "office-id";

private final JdbcTemplate testJdbcTemplate;
private final FineractFeignClient fineractClient;

@Given("any existing year-end retained earnings close for fiscal year ending {string} is removed")
public void removeRetainedEarningsCloseForFiscalYear(final String yearEndDateStr) {
final LocalDate yearEndDate = LocalDate.parse(yearEndDateStr, FORMATTER);
testJdbcTemplate.update("DELETE FROM acc_gl_journal_entry_annual_summary WHERE year_end_date = ?", yearEndDate);
}

@When("Admin points the Retained Earning Job at the last created office")
public void pointRetainedEarningJobAtLastCreatedOffice() {
// Each scenario runs the close against its own freshly created office so the trial balance only contains
// that scenario's loans - this is what isolates repeated runs from each other. office-id is a numeric
// config, so it is set through the internal configuration API.
final PostOfficesResponse office = testContext().get(TestContextKey.OFFICE_CREATE_RESPONSE);
assertThat(office).as("No office was created. Use 'Admin creates a new office' step first.").isNotNull();
fineractClient.defaultApi().updateInternalGlobalConfiguration(OFFICE_ID_CONFIG, office.getOfficeId());
}

@Then("The journal entry annual summary table contains {int} row(s) for GL code {string} with year end date {string}")
public void annualSummaryTableContainsRows(final int expectedCount, final String glCode, final String yearEndDateStr) {
final LocalDate yearEndDate = LocalDate.parse(yearEndDateStr, FORMATTER);
final PostOfficesResponse office = testContext().get(TestContextKey.OFFICE_CREATE_RESPONSE);
assertThat(office).as("No office was created. Use 'Admin creates a new office' step first.").isNotNull();
final Integer actualCount = testJdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM acc_gl_journal_entry_annual_summary WHERE gl_code = ? AND year_end_date = ? AND office_id = ?",
Integer.class, glCode, yearEndDate, office.getOfficeId());
assertThat(actualCount)
.as("acc_gl_journal_entry_annual_summary: expected %d row(s) for gl_code '%s', year_end_date %s, office %s but found %d",
expectedCount, glCode, yearEndDate, office.getOfficeId(), actualCount)
.isEqualTo(expectedCount);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ public void runWorkingCapitalLoanCOB() {
jobService.executeAndWait(DefaultJob.WORKING_CAPITAL_LOAN_COB);
}

@When("Admin runs the Retained Earning Job")
public void runRetainedEarning() {
jobService.executeAndWait(DefaultJob.RETAINED_EARNING);
}

@Then("Admin verifies scheduler job {string} has display name {string}")
public void verifyJobDisplayName(String shortName, String expectedDisplayName) {
GetJobsResponse response = ok(() -> fineractClient.schedulerJob().retrieveByShortName(shortName, Map.of()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
public class ReportingStepDef extends AbstractStepDef {

private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.ENGLISH);
private static final String TRIAL_BALANCE_REPORT = "Trial Balance Summary Report with Asset Owner";
private final FineractFeignClient fineractClient;

@Then("Transaction Summary Report for date {string} has the following data:")
Expand All @@ -65,6 +66,25 @@ public void transactionSummaryReportWithAssetOwnerColumnEmpty(final String dateS
verifyColumnNullability("Transaction Summary Report with Asset Owner", dateStr, columnName, true);
}

@Then("Trial Balance Summary Report with Asset Owner for date {string} has a row for GL account {string} with non-zero ending balance")
public void trialBalanceHasNonZeroEndingBalanceForGlAccount(final String dateStr, final String glCode) {
final BigDecimal ending = sumColumnForGlAccount(executeReport(TRIAL_BALANCE_REPORT, dateStr), glCode, "endingbalance");
assertThat(ending).as("Trial Balance for %s: expected GL account '%s' to be present", dateStr, glCode).isNotNull();
assertThat(ending.signum())
.as("Trial Balance for %s: expected GL account '%s' ending balance to be non-zero but was %s", dateStr, glCode, ending)
.isNotEqualTo(0);
}

@Then("Trial Balance Summary Report with Asset Owner for date {string} shows GL account {string} closed out")
public void trialBalanceShowsGlAccountClosedOut(final String dateStr, final String glCode) {
final BigDecimal ending = sumColumnForGlAccount(executeReport(TRIAL_BALANCE_REPORT, dateStr), glCode, "endingbalance");
if (ending != null) {
assertThat(ending.signum()).as(
"Trial Balance for %s: expected GL account '%s' to be closed out (absent or zero ending balance) but it has ending balance %s",
dateStr, glCode, ending).isEqualTo(0);
}
}

private void verifyReportData(final String reportName, final String dateStr, final DataTable dataTable) {
final RunReportsResponse response = executeReport(reportName, dateStr);

Expand Down Expand Up @@ -135,6 +155,13 @@ private RunReportsResponse executeReport(final String reportName, final String d
return response;
}

private BigDecimal sumColumnForGlAccount(final RunReportsResponse response, final String glCode, final String columnName) {
final int glIdx = findColumnIndex(response.getColumnHeaders(), "glacct");
final int colIdx = findColumnIndex(response.getColumnHeaders(), columnName);
return response.getData().stream().filter(r -> r.getRow() != null && glCode.equals(stringify(r.getRow().get(glIdx))))
.map(r -> new BigDecimal(Objects.toString(r.getRow().get(colIdx), "0"))).reduce(BigDecimal::add).orElse(null);
}

private boolean valuesMatch(final String expected, final String actual) {
if (Objects.equals(expected, actual)) {
return true;
Expand Down
Loading