Merge pull request #2692 from alberto-art3ch/WEB-332/advance-accounting-rule-can-be-set-for-each-writeoff-reason

WEB-332: Advanced accounting rule can be set for each writeoff reason
This commit is contained in:
Gopi Kishan
2025-10-06 17:55:16 +05:30
committed by GitHub
23 changed files with 192 additions and 29 deletions
@@ -189,10 +189,7 @@ export class TransactionsTabComponent implements OnInit {
if (hideAccrual || hideReversed) {
transactions = this.transactionsData.filter((t: LoanTransaction) => {
return (
!(hideReversed && t.manuallyReversed) &&
!(hideAccrual && (t.type.accrual || t.type.capitalizedIncomeAmortization))
);
return !(hideReversed && t.manuallyReversed) && !(hideAccrual && this.isAccrualKindOf(t.type));
});
}
this.dataSource = new MatTableDataSource(transactions);
@@ -243,10 +240,12 @@ export class TransactionsTabComponent implements OnInit {
* REAMORTIZE:30
* INTEREST REFUND:33
* CAPITALIZED INCOME:35
* CAPITALIZED_INCOME_AMORTIZATION:36
* CAPITALIZED INCOME ADJUSTMENT:37
* CONTRACT_TERMINATION:38
* BUY_DOWN_FEE:40
* BUY_DOWN_FEE_ADJUSTMENT:41
* BUY_DOWN_FEE_AMORTIZATION:42
*/
showTransaction(transactionsData: LoanTransaction): boolean {
return [
@@ -266,10 +265,12 @@ export class TransactionsTabComponent implements OnInit {
32,
33,
35,
36,
37,
38,
40,
41
41,
42
].includes(transactionsData.type.id);
}
@@ -293,7 +294,7 @@ export class TransactionsTabComponent implements OnInit {
if (transaction.transactionRelations && transaction.transactionRelations.length > 0) {
return 'linked';
}
if (this.isAccrual(transaction.type) || this.isCapitalizedIncomeAmortization(transaction.type)) {
if (this.isAccrualKindOf(transaction.type)) {
return 'accrual';
}
if (this.isChargeOff(transaction.type)) {
@@ -427,6 +428,21 @@ export class TransactionsTabComponent implements OnInit {
return transactionType.capitalizedIncome || transactionType.code === 'loanTransactionType.capitalizedIncome';
}
private isBuyDownFeeAmortization(transactionType: LoanTransactionType): boolean {
return (
transactionType.buyDownFeeAmortizationAdjustment ||
transactionType.code === 'loanTransactionType.buyDownFeeAmortizationAdjustment'
);
}
private isAccrualKindOf(transactionType: LoanTransactionType): boolean {
return (
this.isAccrual(transactionType) ||
this.isCapitalizedIncomeAmortization(transactionType) ||
this.isBuyDownFeeAmortization(transactionType)
);
}
private isCapitalizedIncomeAmortization(transactionType: LoanTransactionType): boolean {
return (
transactionType.capitalizedIncomeAmortization ||
@@ -39,4 +39,5 @@ export interface LoanTransactionType {
contractTermination: boolean;
buyDownFee: boolean;
buyDownFeeAdjustment: boolean;
buyDownFeeAmortizationAdjustment: boolean;
}
@@ -45,7 +45,6 @@ export class CreateCollateralComponent implements OnInit {
) {
this.route.data.subscribe((data: { collateralTemplate: any }) => {
this.organizationService.getCurrencies().subscribe((orgCurrencies: any) => {
console.log('Organization currencies response:', orgCurrencies);
let orgCurrencyList = Array.isArray(orgCurrencies.selectedCurrencyOptions)
? orgCurrencies.selectedCurrencyOptions
: [];
@@ -1236,7 +1236,7 @@
<ng-container matColumnDef="chargeOffReasonCodeValueId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Charge-off reason' | translate }}</th>
<td mat-cell *matCellDef="let chargeOffReasonToExpenseAccountMapping">
{{ chargeOffReasonToExpenseAccountMapping.chargeOffReasonCodeValue.name }}
{{ chargeOffReasonToExpenseAccountMapping.reasonCodeValue.name }}
</td>
</ng-container>
@@ -1253,10 +1253,7 @@
</table>
</div>
<div
*ngIf="buydownFeeClassificationToIncomeAccountMappings?.length > 0"
class="flex-100 layout-row-wrap responsive-column"
>
<div class="flex-100 layout-row-wrap responsive-column">
<h4 class="mat-h4 flex-100">
{{ 'labels.heading.Buydown Fee classifications to Income accounts' | translate }}
</h4>
@@ -1318,6 +1315,32 @@
<tr mat-row *matRowDef="let row; columns: chargeOffReasonExpenseDisplayedColumns"></tr>
</table>
</div>
<div *ngIf="writeOffReasonsToExpenseMappings?.length > 0" class="flex-100 layout-row-wrap responsive-column">
<h4 class="mat-h4 flex-100">
{{ 'labels.heading.WriteOff reasons to Expense accounts' | translate }}
</h4>
<table class="mat-elevation-z1 flex-100" mat-table [dataSource]="writeOffReasonsToExpenseMappings">
<ng-container matColumnDef="chargeOffReasonCodeValueId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.WriteOff Reason' | translate }}</th>
<td mat-cell *matCellDef="let writeOffReasonsToExpenseMapping">
{{ writeOffReasonsToExpenseMapping.reasonCodeValue.name }}
</td>
</ng-container>
<ng-container matColumnDef="expenseAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Expense Account' | translate }}</th>
<td mat-cell *matCellDef="let writeOffReasonsToExpenseMapping">
({{ writeOffReasonsToExpenseMapping.expenseAccount.glCode }})
{{ writeOffReasonsToExpenseMapping.expenseAccount.name }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chargeOffReasonExpenseDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chargeOffReasonExpenseDisplayedColumns"></tr>
</table>
</div>
</div>
</div>
</div>
@@ -117,6 +117,7 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges {
chargeOffReasonToExpenseAccountMappings: ChargeOffReasonToExpenseAccountMapping[] = [];
buydownFeeClassificationToIncomeAccountMappings: ClassificationToIncomeAccountMapping[] = [];
capitalizedIncomeClassificationToIncomeAccountMappings: ClassificationToIncomeAccountMapping[] = [];
writeOffReasonsToExpenseMappings: ChargeOffReasonToExpenseAccountMapping[] = [];
constructor(private accounting: Accounting) {}
@@ -152,6 +153,7 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges {
this.loanProduct.buydownFeeClassificationToIncomeAccountMappings || [];
this.capitalizedIncomeClassificationToIncomeAccountMappings =
this.loanProduct.capitalizedIncomeClassificationToIncomeAccountMappings || [];
this.writeOffReasonsToExpenseMappings = this.loanProduct.writeOffReasonsToExpenseMappings || [];
} else {
this.accountingMappings = {};
@@ -166,6 +168,7 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges {
const assetAndLiabilityAccountData =
this.loanProductsTemplate.accountingMappingOptions.assetAndLiabilityAccountOptions || [];
const chargeOffReasonOptions: any = this.loanProductsTemplate.chargeOffReasonOptions || [];
const writeOffReasonOptions: any = this.loanProductsTemplate.writeOffReasonOptions || [];
const buydownFeeClassificationOptions: any = this.loanProductsTemplate.buydownFeeClassificationOptions || [];
const capitalizedIncomeClassificationOptions: any =
this.loanProductsTemplate.capitalizedIncomeClassificationOptions || [];
@@ -269,7 +272,7 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges {
let optionData = this.optionDataLookUp(m.chargeOffReasonCodeValueId, chargeOffReasonOptions);
this.chargeOffReasonToExpenseAccountMappings.push({
expenseAccount: this.glAccountLookUp(m.expenseAccountId, expenseAccountData),
chargeOffReasonCodeValue: {
reasonCodeValue: {
id: optionData.id,
name: optionData.value
} as CodeValue
@@ -279,8 +282,8 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges {
}
this.buydownFeeClassificationToIncomeAccountMappings = [];
if (this.loanProduct.buydownFeeClassificationToIncomeAccountMappings?.length > 0) {
this.loanProduct.buydownFeeClassificationToIncomeAccountMappings.forEach((m: any) => {
if (this.loanProduct.buydownfeeClassificationToIncomeAccountMappings?.length > 0) {
this.loanProduct.buydownfeeClassificationToIncomeAccountMappings.forEach((m: any) => {
let optionData = this.optionDataLookUp(m.classificationCodeValueId, buydownFeeClassificationOptions);
if (optionData !== null) {
this.buydownFeeClassificationToIncomeAccountMappings.push({
@@ -293,6 +296,7 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges {
}
});
}
this.capitalizedIncomeClassificationToIncomeAccountMappings = [];
if (this.loanProduct.capitalizedIncomeClassificationToIncomeAccountMappings?.length > 0) {
this.loanProduct.capitalizedIncomeClassificationToIncomeAccountMappings.forEach((m: any) => {
@@ -308,6 +312,22 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges {
}
});
}
this.writeOffReasonsToExpenseMappings = [];
if (this.loanProduct.writeOffReasonsToExpenseMappings?.length > 0) {
this.loanProduct.writeOffReasonsToExpenseMappings.forEach((m: any) => {
let optionData = this.optionDataLookUp(m.writeOffReasonCodeValueId, writeOffReasonOptions);
if (optionData !== null) {
this.writeOffReasonsToExpenseMappings.push({
expenseAccount: this.glAccountLookUp(m.expenseAccountId, expenseAccountData),
reasonCodeValue: {
id: optionData.id,
name: optionData.value
} as CodeValue
});
}
});
}
}
if (this.loanProduct.isInterestRecalculationEnabled) {
@@ -618,8 +638,9 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges {
this.loanProduct.feeToIncomeAccountMappings?.length > 0 ||
this.loanProduct.penaltyToIncomeAccountMappings?.length > 0 ||
this.loanProduct.chargeOffReasonToExpenseAccountMappings?.length > 0 ||
this.loanProduct.buydownFeeClassificationToIncomeAccountMappings?.length > 0 ||
this.loanProduct.capitalizedIncomeClassificationToIncomeAccountMappings?.length > 0
this.loanProduct.buydownfeeClassificationToIncomeAccountMappings?.length > 0 ||
this.loanProduct.capitalizedIncomeClassificationToIncomeAccountMappings?.length > 0 ||
this.loanProduct.writeOffReasonsToExpenseMappings?.length > 0
);
}
@@ -96,11 +96,25 @@ export class AdvancedAccountingMappingRuleComponent implements OnInit {
const dialogRef = this.dialog.open(FormDialogComponent, { data });
dialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
const addData: AccountingMappingDTO = {
value: this.getValueData(response.data.value.valueId),
glAccount: this.getGlAccountData(response.data.value.glAccountId)
};
this.tableData.push(addData);
if ([
'ChargeOffReasonExpense',
'WriteOffReasonToExpense'
].includes(this.formType)) {
const addData: AccountingMappingDTO = {
value: this.getValueData(response.data.value.chargeOffReasonCodeValueId),
glAccount: this.getGlAccountData(response.data.value.expenseAccountId)
};
this.addTableData(addData);
} else if ([
'BuydownFeeClassificationToIncome',
'CapitalizedIncomeClassificationToIncome'
].includes(this.formType)) {
const addData: AccountingMappingDTO = {
value: this.getValueData(response.data.value.valueId),
glAccount: this.getGlAccountData(response.data.value.glAccountId)
};
this.addTableData(addData);
}
this.sendParentData();
if (this.formType == 'ChargeOffReasonExpense') {
@@ -110,13 +124,21 @@ export class AdvancedAccountingMappingRuleComponent implements OnInit {
});
}
addTableData(addedData: AccountingMappingDTO): void {
let newData = [
...this.tableData,
addedData
];
this.tableData = newData;
}
delete(index: number) {
const dialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: this.translateService.instant('labels.text.this') }
});
dialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
this.tableData.splice(index, 1);
this.tableData = this.tableData.filter((_, i) => i !== index);
this.sendParentData();
}
});
@@ -159,6 +181,11 @@ export class AdvancedAccountingMappingRuleComponent implements OnInit {
title: 'Capitalized Income classifications to Income accounts',
formfields: this.getClassificationIncomeFormfields(values)
};
case 'WriteOffReasonToExpense':
return {
title: 'Map Write-off reasons to Expense accounts',
formfields: this.getChargeOffReasonExpenseFormfields(values)
};
}
}
@@ -290,8 +317,15 @@ export class AdvancedAccountingMappingRuleComponent implements OnInit {
}
getGlAccountData(valueId: number): any {
return this.incomeAccountData.find((i: any) => {
const glAccountData = [
this.incomeAccountData,
this.expenseAccountData,
this.assetAccountData,
this.liabilityAccountData
];
return glAccountData.find((i: any) => {
return i.id === valueId;
});
return null;
}
}
@@ -536,6 +536,17 @@
[accountingMappingOptions]="capitalizedIncomeClassificationOptions"
(formChangeEvent)="formChangeEvent($event)"
></mifosx-advanced-accounting-mapping-rule>
<mifosx-advanced-accounting-mapping-rule
class="flex-100 m-t-10"
[textField]="'WriteOff Reason'"
[formType]="'WriteOffReasonToExpense'"
[formArray]="writeOffReasonsToExpenseMappings"
[textHeading]="'WriteOff reasons to Expense accounts'"
[expenseAccountData]="expenseAccountData"
[accountingMappingOptions]="writeOffReasonOptions"
(formChangeEvent)="formChangeEvent($event)"
></mifosx-advanced-accounting-mapping-rule>
</div>
</div>
</div>
@@ -86,6 +86,7 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
chargeOffReasonOptions: any;
capitalizedIncomeClassificationOptions: any[] = [];
buydownFeeClassificationOptions: any[] = [];
writeOffReasonOptions: any[] = [];
currentFormValues: any[] = [];
allowAddChargeOffReasonExpense = true;
@@ -134,6 +135,7 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
this.capitalizedIncomeClassificationOptions =
this.loanProductsTemplate.capitalizedIncomeClassificationOptions || [];
this.buydownFeeClassificationOptions = this.loanProductsTemplate.buydownFeeClassificationOptions || [];
this.writeOffReasonOptions = this.loanProductsTemplate.writeOffReasonOptions || [];
this.loanProductAccountingForm.patchValue({
accountingRule: this.loanProductsTemplate.accountingRule.id
@@ -214,7 +216,8 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
this.loanProductsTemplate.penaltyToIncomeAccountMappings ||
this.loanProductsTemplate.chargeOffReasonToExpenseAccountMappings ||
this.loanProductsTemplate.buydownFeeClassificationToIncomeAccountMappings ||
this.loanProductsTemplate.capitalizedIncomeClassificationToIncomeAccountMappings
this.loanProductsTemplate.capitalizedIncomeClassificationToIncomeAccountMappings ||
this.loanProductsTemplate.writeOffReasonsToExpenseMappings
? true
: false
});
@@ -251,7 +254,7 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
this.formBuilder.array(
(this.loanProductsTemplate.chargeOffReasonToExpenseAccountMappings || []).map(
(m: ChargeOffReasonToExpenseAccountMapping) => ({
chargeOffReasonCodeValueId: m.chargeOffReasonCodeValue.id,
chargeOffReasonCodeValueId: m.reasonCodeValue.id,
expenseAccountId: m.expenseAccount.id
})
)
@@ -279,6 +282,17 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
)
)
);
this.loanProductAccountingForm.setControl(
'writeOffReasonsToExpenseMappings',
this.formBuilder.array(
(this.loanProductsTemplate.writeOffReasonsToExpenseMappings || []).map(
(m: ChargeOffReasonToExpenseAccountMapping) => ({
value: m.reasonCodeValue,
glAccount: m.expenseAccount
})
)
)
);
}
}
@@ -384,6 +398,7 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
'capitalizedIncomeClassificationToIncomeAccountMappings',
this.formBuilder.array([])
);
this.loanProductAccountingForm.addControl('writeOffReasonsToExpenseMappings', this.formBuilder.array([]));
} else {
this.loanProductAccountingForm.setControl(
'paymentChannelToFundSourceMappings',
@@ -403,6 +418,7 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
'capitalizedIncomeClassificationToIncomeAccountMappings',
this.formBuilder.array([])
);
this.loanProductAccountingForm.setControl('writeOffReasonsToExpenseMappings', this.formBuilder.array([]));
}
});
} else {
@@ -476,6 +492,10 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
) as UntypedFormArray;
}
get writeOffReasonsToExpenseMappings(): UntypedFormArray {
return this.loanProductAccountingForm.get('writeOffReasonsToExpenseMappings') as UntypedFormArray;
}
setLoanProductAccountingFormDirty() {
if (this.loanProductAccountingForm.pristine) {
this.loanProductAccountingForm.markAsDirty();
@@ -708,7 +728,6 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
}))
)
);
console.log(this.loanProductAccountingForm.value.buydownfeeClassificationToIncomeAccountMappings);
} else if (accountingData.formType === 'CapitalizedIncomeClassificationToIncome') {
this.loanProductAccountingForm.setControl(
'capitalizedIncomeClassificationToIncomeAccountMappings',
@@ -719,7 +738,16 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
}))
)
);
console.log(this.loanProductAccountingForm.value.capitalizedIncomeClassificationToIncomeAccountMappings);
} else if (accountingData.formType === 'WriteOffReasonToExpense') {
this.loanProductAccountingForm.setControl(
'writeOffReasonsToExpenseMappings',
this.formBuilder.array(
(accountingData.values || []).map((m: AccountingMappingDTO) => ({
writeOffReasonCodeValueId: m.value.id,
expenseAccountId: m.glAccount.id
}))
)
);
}
this.setLoanProductAccountingFormDirty();
}
@@ -155,8 +155,10 @@ export interface LoanProduct {
enableAccrualActivityPosting?: boolean;
supportedInterestRefundTypes?: StringEnumOptionData[];
chargeOffBehaviour?: StringEnumOptionData;
buydownfeeClassificationToIncomeAccountMappings?: ClassificationToIncomeAccountMapping[];
buydownFeeClassificationToIncomeAccountMappings?: ClassificationToIncomeAccountMapping[];
capitalizedIncomeClassificationToIncomeAccountMappings?: ClassificationToIncomeAccountMapping[];
writeOffReasonsToExpenseMappings?: ChargeOffReasonToExpenseAccountMapping[];
}
export interface AllowAttributeOverrides {
+3 -1
View File
@@ -41,8 +41,10 @@ export interface PaymentChannelToFundSourceMapping {
export interface ChargeOffReasonToExpenseAccountMapping {
chargeOffReasonCodeValueId?: number;
writeOffReasonCodeValueId?: number;
expenseAccountId?: number;
chargeOffReasonCodeValue?: CodeValue;
reasonCodeValue?: CodeValue;
writeOffReasonCodeValue?: CodeValue;
expenseAccount?: AccountingMapping;
}