Skip to content

Commit d6323be

Browse files
somasorosdpcadamsaghy
authored andcommitted
FINERACT-2358: Allow to configure advanced accounting rules based on write-off reason
1 parent 641f755 commit d6323be

File tree

20 files changed

+634
-105
lines changed

20 files changed

+634
-105
lines changed

fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMapping.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ public class ProductToGLAccountMapping extends AbstractPersistableCustom<Long> {
6868
@JoinColumn(name = "charge_off_reason_id", nullable = true)
6969
private CodeValue chargeOffReason;
7070

71+
@ManyToOne
72+
@JoinColumn(name = "write_off_reason_id", nullable = true)
73+
private CodeValue writeOffReason;
74+
7175
@ManyToOne
7276
@JoinColumn(name = "capitalized_income_classification_id", nullable = true)
7377
private CodeValue capitalizedIncomeClassification;

fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ ProductToGLAccountMapping findProductIdAndProductTypeAndFinancialAccountTypeAndC
3535
@Param("productType") int productType, @Param("financialAccountType") int financialAccountType,
3636
@Param("chargeId") Long ChargeId);
3737

38-
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=:financialAccountType and mapping.paymentType is NULL and mapping.charge is NULL and mapping.chargeOffReason is NULL and mapping.capitalizedIncomeClassification is NULL and mapping.buydownFeeClassification is NULL")
38+
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=:financialAccountType and mapping.paymentType is NULL and mapping.charge is NULL and mapping.chargeOffReason is NULL and mapping.writeOffReason is NULL and mapping.capitalizedIncomeClassification is NULL and mapping.buydownFeeClassification is NULL")
3939
ProductToGLAccountMapping findCoreProductToFinAccountMapping(@Param("productId") Long productId, @Param("productType") int productType,
4040
@Param("financialAccountType") int financialAccountType);
4141

@@ -66,11 +66,18 @@ List<ProductToGLAccountMapping> findAllPenaltyToIncomeAccountMappings(@Param("pr
6666
List<ProductToGLAccountMapping> findAllChargeOffReasonsMappings(@Param("productId") Long productId,
6767
@Param("productType") int productType);
6868

69+
List<ProductToGLAccountMapping> findAllProductToGLAccountMappingsByProductIdAndProductTypeAndFinancialAccountType(Long productId,
70+
int productType, int financialAccountType);
71+
72+
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.writeOffReason is not NULL")
73+
List<ProductToGLAccountMapping> findAllWriteOffReasonsMappings(@Param("productId") Long productId,
74+
@Param("productType") int productType);
75+
6976
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.chargeOffReason.id =:chargeOffReasonId AND mapping.productId =:productId AND mapping.productType =:productType")
7077
ProductToGLAccountMapping findChargeOffReasonMapping(@Param("productId") Long productId, @Param("productType") Integer productType,
7178
@Param("chargeOffReasonId") Long chargeOffReasonId);
7279

73-
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId AND mapping.productType =:productType AND mapping.charge IS NULL AND mapping.paymentType IS NULL AND mapping.chargeOffReason IS NULL AND mapping.capitalizedIncomeClassification is NULL AND mapping.buydownFeeClassification is NULL")
80+
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId AND mapping.productType =:productType AND mapping.charge IS NULL AND mapping.paymentType IS NULL AND mapping.chargeOffReason IS NULL AND mapping.writeOffReason IS NULL AND mapping.capitalizedIncomeClassification is NULL AND mapping.buydownFeeClassification is NULL")
7481
List<ProductToGLAccountMapping> findAllRegularMappings(@Param("productId") Long productId, @Param("productType") Integer productType);
7582

7683
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.paymentType is not NULL")

fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java

Lines changed: 112 additions & 60 deletions
Large diffs are not rendered by default.

fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper;
2626
import org.apache.fineract.accounting.producttoaccountmapping.data.ClassificationToGLAccountData;
2727
import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper;
28+
import org.apache.fineract.accounting.producttoaccountmapping.data.WriteOffReasonsToExpenseAccountMapper;
2829

2930
public interface ProductToGLAccountMappingReadPlatformService {
3031

@@ -52,6 +53,8 @@ public interface ProductToGLAccountMappingReadPlatformService {
5253

5354
List<ChargeOffReasonToGLAccountMapper> fetchChargeOffReasonMappingsForLoanProduct(Long loanProductId);
5455

56+
List<WriteOffReasonsToExpenseAccountMapper> fetchWriteOffReasonMappingsForLoanProduct(Long loanProductId);
57+
5558
List<ClassificationToGLAccountData> fetchClassificationMappingsForLoanProduct(Long loanProductId,
5659
LoanProductAccountingParams classificationParameter);
5760
}

fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper;
4141
import org.apache.fineract.accounting.producttoaccountmapping.data.ClassificationToGLAccountData;
4242
import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper;
43+
import org.apache.fineract.accounting.producttoaccountmapping.data.WriteOffReasonsToExpenseAccountMapper;
4344
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping;
4445
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
4546
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
@@ -293,6 +294,22 @@ private List<ChargeOffReasonToGLAccountMapper> fetchChargeOffReasonMappings(fina
293294
return chargeOffReasonToGLAccountMappers;
294295
}
295296

297+
private List<WriteOffReasonsToExpenseAccountMapper> fetchWriteOffReasonMappings(final PortfolioProductType portfolioProductType,
298+
final Long loanProductId) {
299+
final List<ProductToGLAccountMapping> mappings = productToGLAccountMappingRepository.findAllWriteOffReasonsMappings(loanProductId,
300+
portfolioProductType.getValue());
301+
List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonsToExpenseAccountMappers = mappings.isEmpty() ? null : new ArrayList<>();
302+
for (final ProductToGLAccountMapping mapping : mappings) {
303+
final String glCode = String.valueOf(mapping.getGlAccount().getId());
304+
final String writeOffReasonId = String.valueOf(mapping.getWriteOffReason().getId());
305+
306+
final WriteOffReasonsToExpenseAccountMapper writeOffReasonToGLAccountMapper = new WriteOffReasonsToExpenseAccountMapper()
307+
.setWriteOffReasonCodeValueId(writeOffReasonId).setExpenseAccountId(glCode);
308+
writeOffReasonsToExpenseAccountMappers.add(writeOffReasonToGLAccountMapper);
309+
}
310+
return writeOffReasonsToExpenseAccountMappers;
311+
}
312+
296313
private List<ClassificationToGLAccountData> fetchClassificationMappings(final PortfolioProductType portfolioProductType,
297314
final Long loanProductId, LoanProductAccountingParams classificationParameter) {
298315
final List<ProductToGLAccountMapping> mappings = classificationParameter
@@ -367,6 +384,11 @@ public List<ChargeOffReasonToGLAccountMapper> fetchChargeOffReasonMappingsForLoa
367384
return fetchChargeOffReasonMappings(PortfolioProductType.LOAN, loanProductId);
368385
}
369386

387+
@Override
388+
public List<WriteOffReasonsToExpenseAccountMapper> fetchWriteOffReasonMappingsForLoanProduct(Long loanProductId) {
389+
return fetchWriteOffReasonMappings(PortfolioProductType.LOAN, loanProductId);
390+
}
391+
370392
@Override
371393
public List<ClassificationToGLAccountData> fetchClassificationMappingsForLoanProduct(Long loanProductId,
372394
LoanProductAccountingParams classificationParameter) {

fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,10 @@ public enum LoanProductAccountingParams {
181181
INCOME_FROM_GOODWILL_CREDIT_FEES("incomeFromGoodwillCreditFeesAccountId"), //
182182
INCOME_FROM_GOODWILL_CREDIT_PENALTY("incomeFromGoodwillCreditPenaltyAccountId"), //
183183
CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS("chargeOffReasonToExpenseAccountMappings"), //
184+
WRITE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS("writeOffReasonsToExpenseMappings"), //
184185
EXPENSE_GL_ACCOUNT_ID("expenseAccountId"), //
185186
CHARGE_OFF_REASON_CODE_VALUE_ID("chargeOffReasonCodeValueId"), //
187+
WRITE_OFF_REASON_CODE_VALUE_ID("writeOffReasonCodeValueId"), //
186188
DEFERRED_INCOME_LIABILITY("deferredIncomeLiabilityAccountId"), //
187189
INCOME_FROM_CAPITALIZATION("incomeFromCapitalizationAccountId"), //
188190
BUY_DOWN_EXPENSE("buyDownExpenseAccountId"), //
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.accounting.producttoaccountmapping.data;
20+
21+
import java.io.Serial;
22+
import java.io.Serializable;
23+
import lombok.Data;
24+
import lombok.NoArgsConstructor;
25+
import lombok.experimental.Accessors;
26+
27+
@Data
28+
@NoArgsConstructor
29+
@Accessors(chain = true)
30+
public class WriteOffReasonsToExpenseAccountMapper implements Serializable {
31+
32+
@Serial
33+
private static final long serialVersionUID = 1L;
34+
private String writeOffReasonCodeValueId;
35+
private String expenseAccountId;
36+
}

fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -531,13 +531,20 @@ public DataValidatorBuilder longGreaterThanZero() {
531531
}
532532

533533
if (this.value != null) {
534-
final long number = Long.parseLong(this.value.toString());
535-
if (number < 1) {
536-
String validationErrorCode = "validation.msg." + this.resource + "." + this.parameter + ".not.greater.than.zero";
537-
String defaultEnglishMessage = "The parameter `" + this.parameter + "` must be greater than 0.";
538-
final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode, defaultEnglishMessage, this.parameter,
539-
number, 0);
540-
this.dataValidationErrors.add(error);
534+
try {
535+
final long number = Long.parseLong(this.value.toString());
536+
if (number < 1) {
537+
String validationErrorCode = "validation.msg." + this.resource + "." + this.parameter + ".not.greater.than.zero";
538+
String defaultEnglishMessage = "The parameter `" + this.parameter + "` must be greater than 0.";
539+
final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode, defaultEnglishMessage,
540+
this.parameter, number, 0);
541+
this.dataValidationErrors.add(error);
542+
}
543+
} catch (NumberFormatException e) {
544+
String validationErrorCode = "validation.msg." + this.resource + "." + this.parameter + ".not.a.number";
545+
String defaultEnglishMessage = "The parameter `" + this.parameter + "` must be a number.";
546+
this.dataValidationErrors.add(ApiParameterError.parameterError(validationErrorCode, defaultEnglishMessage, this.parameter));
547+
throwValidationErrors();
541548
}
542549
}
543550
return this;

fineract-loan/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/LoanProductToGLAccountMappingHelper.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepository;
3131
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
3232
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
33+
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping;
3334
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
3435
import org.apache.fineract.accounting.producttoaccountmapping.exception.ProductToGLAccountMappingInvalidException;
3536
import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingHelper;
@@ -141,12 +142,39 @@ public void saveChargesToIncomeAccountMappings(final JsonCommand command, final
141142

142143
public void saveChargeOffReasonToExpenseAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
143144
final Map<String, Object> changes) {
144-
saveChargeOffReasonToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN);
145+
saveReasonToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN,
146+
LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS,
147+
LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID, CashAccountsForLoan.CHARGE_OFF_EXPENSE);
148+
}
149+
150+
public void saveWriteOffReasonToExpenseAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
151+
final Map<String, Object> changes) {
152+
saveReasonToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN,
153+
LoanProductAccountingParams.WRITE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS,
154+
LoanProductAccountingParams.WRITE_OFF_REASON_CODE_VALUE_ID, CashAccountsForLoan.LOSSES_WRITTEN_OFF);
155+
}
156+
157+
public void updateWriteOffReasonToExpenseAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
158+
final Map<String, Object> changes) {
159+
final List<ProductToGLAccountMapping> existingWriteOffReasonToGLAccountMappings = this.accountMappingRepository
160+
.findAllWriteOffReasonsMappings(productId, PortfolioProductType.LOAN.getValue());
161+
LoanProductAccountingParams reasonToExpenseAccountMappingsParam = LoanProductAccountingParams.WRITE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS;
162+
LoanProductAccountingParams reasonCodeValueIdParam = LoanProductAccountingParams.WRITE_OFF_REASON_CODE_VALUE_ID;
163+
CashAccountsForLoan cashAccountsForLoan = CashAccountsForLoan.LOSSES_WRITTEN_OFF;
164+
updateReasonToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN,
165+
existingWriteOffReasonToGLAccountMappings, reasonToExpenseAccountMappingsParam, reasonCodeValueIdParam,
166+
cashAccountsForLoan);
145167
}
146168

147169
public void updateChargeOffReasonToExpenseAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
148170
final Map<String, Object> changes) {
149-
updateChargeOffReasonToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN);
171+
final List<ProductToGLAccountMapping> chargeOffReasonsMappings = this.accountMappingRepository
172+
.findAllChargeOffReasonsMappings(productId, PortfolioProductType.LOAN.getValue());
173+
LoanProductAccountingParams reasonToExpenseAccountMappingsParam = LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS;
174+
LoanProductAccountingParams reasonCodeValueIdParam = LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID;
175+
CashAccountsForLoan cashAccountsForLoan = CashAccountsForLoan.CHARGE_OFF_EXPENSE;
176+
updateReasonToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN, chargeOffReasonsMappings,
177+
reasonToExpenseAccountMappingsParam, reasonCodeValueIdParam, cashAccountsForLoan);
150178
}
151179

152180
public void saveCapitalizedIncomeClassificationToIncomeAccountMappings(final JsonCommand command, final JsonElement element,

0 commit comments

Comments
 (0)