feat: cash-only registers, day book sections and DD amendments
This commit is contained in:
+5
-1
@@ -26,6 +26,7 @@ import {
|
||||
MatRowDef,
|
||||
MatRow
|
||||
} from '@angular/material/table';
|
||||
import { FacilityChargeFilterService } from 'app/shared/charges/facility-charge-filter.service';
|
||||
import { ChargesFilterPipe } from '../../../../pipes/charges-filter.pipe';
|
||||
import { FormatNumberPipe } from '../../../../pipes/format-number.pipe';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
@@ -55,6 +56,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
export class CurrentProductChargesStepComponent implements OnInit {
|
||||
dialog = inject(MatDialog);
|
||||
private translateService = inject(TranslateService);
|
||||
private facilityChargeFilter = inject(FacilityChargeFilterService);
|
||||
|
||||
@Input() CurrentProductsTemplate: any;
|
||||
@Input() currencyCode: UntypedFormControl;
|
||||
@@ -73,7 +75,9 @@ export class CurrentProductChargesStepComponent implements OnInit {
|
||||
pristine = true;
|
||||
|
||||
ngOnInit() {
|
||||
this.chargeData = this.CurrentProductsTemplate.chargeOptions;
|
||||
this.facilityChargeFilter
|
||||
.excludeFacilityCharges(this.CurrentProductsTemplate.chargeOptions)
|
||||
.subscribe((charges: any[]) => (this.chargeData = charges));
|
||||
|
||||
this.chargesDataSource = this.CurrentProductsTemplate.charges || [];
|
||||
this.pristine = true;
|
||||
|
||||
-15
@@ -174,21 +174,6 @@
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="spl-feature">
|
||||
<span class="spl-feature__label">
|
||||
<i class="fas fa-arrow-right-arrow-left"></i>
|
||||
Withdrawal Facility
|
||||
</span>
|
||||
<select
|
||||
class="spl-select"
|
||||
[(ngModel)]="accountFacilityConfig.withdrawalFacility"
|
||||
(ngModelChange)="markAsDirty()"
|
||||
>
|
||||
<option [ngValue]="false">No</option>
|
||||
<option [ngValue]="true">Yes</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="spl-feature">
|
||||
<span class="spl-feature__label">
|
||||
<i class="fas fa-id-card"></i>
|
||||
|
||||
-2
@@ -24,7 +24,6 @@ type BooleanFeatureKey =
|
||||
| 'neftFacility'
|
||||
| 'rtgsFacility'
|
||||
| 'interBankTotalCapFacility'
|
||||
| 'withdrawalFacility'
|
||||
| 'depositFacilityKyc'
|
||||
| 'virtualDebitCard';
|
||||
|
||||
@@ -76,7 +75,6 @@ export class CurrentProductLimitsStepComponent implements OnChanges {
|
||||
neftFacility: true,
|
||||
rtgsFacility: true,
|
||||
interBankTotalCapFacility: true,
|
||||
withdrawalFacility: true,
|
||||
depositFacilityKyc: true,
|
||||
virtualDebitCard: false
|
||||
};
|
||||
|
||||
-1
@@ -94,7 +94,6 @@ export class CurrentProductPreviewStepComponent implements OnInit, OnChanges {
|
||||
{ key: 'neftFacility', label: 'NEFT Facility' },
|
||||
{ key: 'rtgsFacility', label: 'RTGS Facility' },
|
||||
{ key: 'interBankTotalCapFacility', label: 'Inter-Bank Total Cap Facility' },
|
||||
{ key: 'withdrawalFacility', label: 'Withdrawal Facility' },
|
||||
{ key: 'depositFacilityKyc', label: 'Deposit Facility (KYC)' },
|
||||
{ key: 'virtualDebitCard', label: 'Virtual Debit Card' }
|
||||
];
|
||||
|
||||
+13
-6
@@ -11,6 +11,7 @@ import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms
|
||||
import { MatCheckbox } from '@angular/material/checkbox';
|
||||
import { MatRadioGroup, MatRadioButton } from '@angular/material/radio';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { DORMANCY_DEFAULTS } from '../../../dormancy-defaults';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-current-product-settings-step',
|
||||
@@ -62,9 +63,15 @@ export class CurrentProductSettingsStepComponent implements OnInit {
|
||||
? (this.CurrentProductsTemplate.accountClosedZeroBalanceInDayClosing ?? false)
|
||||
: false,
|
||||
isDormancyTrackingActive: isEditing ? (this.CurrentProductsTemplate.isDormancyTrackingActive ?? true) : true,
|
||||
daysToInactive: isEditing ? this.CurrentProductsTemplate.daysToInactive || 30 : 30,
|
||||
daysToDormancy: isEditing ? this.CurrentProductsTemplate.daysToDormancy || 60 : 60,
|
||||
daysToEscheat: isEditing ? this.CurrentProductsTemplate.daysToEscheat || 90 : 90,
|
||||
daysToInactive: isEditing
|
||||
? this.CurrentProductsTemplate.daysToInactive || DORMANCY_DEFAULTS.daysToInactive
|
||||
: DORMANCY_DEFAULTS.daysToInactive,
|
||||
daysToDormancy: isEditing
|
||||
? this.CurrentProductsTemplate.daysToDormancy || DORMANCY_DEFAULTS.daysToDormancy
|
||||
: DORMANCY_DEFAULTS.daysToDormancy,
|
||||
daysToEscheat: isEditing
|
||||
? this.CurrentProductsTemplate.daysToEscheat || DORMANCY_DEFAULTS.daysToEscheat
|
||||
: DORMANCY_DEFAULTS.daysToEscheat,
|
||||
// Overdraft fields
|
||||
allowOverdraft: isEditing ? (this.CurrentProductsTemplate.allowOverdraft ?? false) : false,
|
||||
overdraftLimit: isEditing ? this.CurrentProductsTemplate.overdraftLimit : '',
|
||||
@@ -113,9 +120,9 @@ export class CurrentProductSettingsStepComponent implements OnInit {
|
||||
accountClosedZeroBalanceInDayClosing: [false],
|
||||
// Dormancy (used internally / by accounting step)
|
||||
isDormancyTrackingActive: [true],
|
||||
daysToInactive: [30],
|
||||
daysToDormancy: [60],
|
||||
daysToEscheat: [90],
|
||||
daysToInactive: [DORMANCY_DEFAULTS.daysToInactive],
|
||||
daysToDormancy: [DORMANCY_DEFAULTS.daysToDormancy],
|
||||
daysToEscheat: [DORMANCY_DEFAULTS.daysToEscheat],
|
||||
// Overdraft (OD) Facility
|
||||
allowOverdraft: [false],
|
||||
overdraftLimit: [
|
||||
|
||||
-1
@@ -89,7 +89,6 @@ export class CurrentProductGeneralTabComponent {
|
||||
{ key: 'neftFacility', label: 'NEFT Facility' },
|
||||
{ key: 'rtgsFacility', label: 'RTGS Facility' },
|
||||
{ key: 'interBankTotalCapFacility', label: 'Inter-Bank Total Cap Facility' },
|
||||
{ key: 'withdrawalFacility', label: 'Withdrawal Facility' },
|
||||
{ key: 'depositFacilityKyc', label: 'Deposit Facility KYC' },
|
||||
{ key: 'virtualDebitCard', label: 'Virtual Debit Card' }
|
||||
];
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default dormancy thresholds for a new product, in days from the last customer transaction.
|
||||
*
|
||||
* RBI's inoperative-accounts rules count the silence from the last *customer-induced* transaction —
|
||||
* a deposit or withdrawal the customer made, not interest the bank posts — and set the marks at a
|
||||
* year, two years and ten:
|
||||
*
|
||||
* 365 → inactive (an internal flag; nothing is restricted)
|
||||
* 730 → inoperative (two years; debits stop, reactivation needs the branch)
|
||||
* 3650 → escheat (ten years; the balance is due to RBI's DEAF)
|
||||
*
|
||||
* These are the values every product stepper starts a new product on, and the backend seeds the
|
||||
* same numbers (see the 0521 and 0524 changelogs). They live here rather than being repeated in
|
||||
* each stepper so a bank that revises its policy changes one file, not four.
|
||||
*
|
||||
* A product can still be configured away from these — they are defaults, not a ceiling — but a
|
||||
* materially shorter period is worth questioning: it would make an ordinary account, dormant for a
|
||||
* few weeks, show as overdue for inactivity.
|
||||
*/
|
||||
export const DORMANCY_DEFAULTS = {
|
||||
daysToInactive: 365,
|
||||
daysToDormancy: 730,
|
||||
daysToEscheat: 3650
|
||||
};
|
||||
+7
-6
@@ -11,6 +11,7 @@ import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { MatRadioGroup, MatRadioButton } from '@angular/material/radio';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { DORMANCY_DEFAULTS } from '../../../dormancy-defaults';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-fixed-deposit-product-currency-step',
|
||||
@@ -54,9 +55,9 @@ export class FixedDepositProductCurrencyStepComponent implements OnInit {
|
||||
2,
|
||||
dormancyAlert: this.fixedDepositProductsTemplate.dormancyAlert ?? false,
|
||||
isDormancyTrackingActive: this.fixedDepositProductsTemplate.isDormancyTrackingActive ?? false,
|
||||
daysToInactive: this.fixedDepositProductsTemplate.daysToInactive ?? 30,
|
||||
daysToDormancy: this.fixedDepositProductsTemplate.daysToDormancy ?? 60,
|
||||
daysToEscheat: this.fixedDepositProductsTemplate.daysToEscheat ?? 90,
|
||||
daysToInactive: this.fixedDepositProductsTemplate.daysToInactive ?? DORMANCY_DEFAULTS.daysToInactive,
|
||||
daysToDormancy: this.fixedDepositProductsTemplate.daysToDormancy ?? DORMANCY_DEFAULTS.daysToDormancy,
|
||||
daysToEscheat: this.fixedDepositProductsTemplate.daysToEscheat ?? DORMANCY_DEFAULTS.daysToEscheat,
|
||||
nominationFacility: this.fixedDepositProductsTemplate.nominationFacility ?? false,
|
||||
autoDebitFacility: this.fixedDepositProductsTemplate.autoDebitFacility ?? false
|
||||
});
|
||||
@@ -77,9 +78,9 @@ export class FixedDepositProductCurrencyStepComponent implements OnInit {
|
||||
],
|
||||
dormancyAlert: [false],
|
||||
isDormancyTrackingActive: [false],
|
||||
daysToInactive: [30],
|
||||
daysToDormancy: [60],
|
||||
daysToEscheat: [90],
|
||||
daysToInactive: [DORMANCY_DEFAULTS.daysToInactive],
|
||||
daysToDormancy: [DORMANCY_DEFAULTS.daysToDormancy],
|
||||
daysToEscheat: [DORMANCY_DEFAULTS.daysToEscheat],
|
||||
nominationFacility: [false],
|
||||
autoDebitFacility: [false]
|
||||
});
|
||||
|
||||
@@ -74,6 +74,17 @@ export class ProductsService {
|
||||
return this.http.put(`/savingsproducts/${savingProductId}`, savingProduct);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a savings product.
|
||||
*
|
||||
* The server refuses this once any account has been opened on the product — a product with a
|
||||
* history behind it cannot be removed without taking those accounts' terms with it — so this is
|
||||
* really only usable on one created by mistake or for a trial.
|
||||
*/
|
||||
deleteSavingProduct(savingProductId: string): Observable<any> {
|
||||
return this.http.delete(`/savingsproducts/${savingProductId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submitting a savings product update (above) puts it in the pending-approval temp table instead of
|
||||
* applying it directly. These calls drive the Approvals -> Pending Savings Product Edits screen.
|
||||
|
||||
+7
-6
@@ -11,6 +11,7 @@ import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule }
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { MatRadioGroup, MatRadioButton } from '@angular/material/radio';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { DORMANCY_DEFAULTS } from '../../../dormancy-defaults';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-recurring-deposit-product-currency-step',
|
||||
@@ -54,9 +55,9 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit {
|
||||
2,
|
||||
dormancyAlert: this.recurringDepositProductsTemplate.dormancyAlert ?? false,
|
||||
isDormancyTrackingActive: this.recurringDepositProductsTemplate.isDormancyTrackingActive ?? false,
|
||||
daysToInactive: this.recurringDepositProductsTemplate.daysToInactive ?? 30,
|
||||
daysToDormancy: this.recurringDepositProductsTemplate.daysToDormancy ?? 60,
|
||||
daysToEscheat: this.recurringDepositProductsTemplate.daysToEscheat ?? 90,
|
||||
daysToInactive: this.recurringDepositProductsTemplate.daysToInactive ?? DORMANCY_DEFAULTS.daysToInactive,
|
||||
daysToDormancy: this.recurringDepositProductsTemplate.daysToDormancy ?? DORMANCY_DEFAULTS.daysToDormancy,
|
||||
daysToEscheat: this.recurringDepositProductsTemplate.daysToEscheat ?? DORMANCY_DEFAULTS.daysToEscheat,
|
||||
nominationFacility: this.recurringDepositProductsTemplate.nominationFacility ?? false,
|
||||
autoDebitFacility: this.recurringDepositProductsTemplate.autoDebitFacility ?? false
|
||||
});
|
||||
@@ -77,9 +78,9 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit {
|
||||
],
|
||||
dormancyAlert: [false],
|
||||
isDormancyTrackingActive: [false],
|
||||
daysToInactive: [30],
|
||||
daysToDormancy: [60],
|
||||
daysToEscheat: [90],
|
||||
daysToInactive: [DORMANCY_DEFAULTS.daysToInactive],
|
||||
daysToDormancy: [DORMANCY_DEFAULTS.daysToDormancy],
|
||||
daysToEscheat: [DORMANCY_DEFAULTS.daysToEscheat],
|
||||
nominationFacility: [false],
|
||||
autoDebitFacility: [false]
|
||||
});
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@
|
||||
<mifosx-saving-product-accounting-step
|
||||
[savingProductsTemplate]="savingProductsTemplate"
|
||||
[accountingRuleData]="accountingRuleData"
|
||||
[isDormancyTrackingActive]="savingProductSettingsForm.get('isDormancyTrackingActive')"
|
||||
[isDormancyTrackingActive]="savingProductCurrencyForm.get('dormancyAlert')"
|
||||
[savingProductFormValid]="savingProductFormValid"
|
||||
[allowOverdraft]="savingProductSettingsForm.get('allowOverdraft')"
|
||||
></mifosx-saving-product-accounting-step>
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@
|
||||
<mifosx-saving-product-accounting-step
|
||||
[savingProductsTemplate]="savingProductAndTemplate"
|
||||
[accountingRuleData]="accountingRuleData"
|
||||
[isDormancyTrackingActive]="savingProductSettingsForm.get('isDormancyTrackingActive')"
|
||||
[isDormancyTrackingActive]="savingProductCurrencyForm.get('dormancyAlert')"
|
||||
[savingProductFormValid]="savingProductFormValidAndNotPristine"
|
||||
[allowOverdraft]="savingProductSettingsForm.get('allowOverdraft')"
|
||||
></mifosx-saving-product-accounting-step>
|
||||
|
||||
+92
-39
@@ -24,6 +24,12 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
type AccountingSectionKey = 'assets' | 'liabilities' | 'expenses' | 'income';
|
||||
|
||||
/** Reserved otherGlMappings account names backing the two dedicated savings-interest GL rows.
|
||||
* They round trip through otherGlMappings because the backend persists only the core mappings
|
||||
* under CASH_BASED accounting, which is the only rule this screen offers. */
|
||||
const SAVINGS_INTEREST_PAYABLE_ACCOUNT_NAME = 'Savings Interest Payable';
|
||||
const SAVINGS_INTEREST_PAID_ACCOUNT_NAME = 'Savings Interest Paid';
|
||||
|
||||
type AccountingRowConfig = {
|
||||
key: string;
|
||||
label: string;
|
||||
@@ -180,13 +186,9 @@ export class SavingProductAccountingStepComponent implements OnInit {
|
||||
this.savingProductAccountingForm.patchValue({
|
||||
savingsReferenceAccountId: mappings.savingsReferenceAccount?.id || '',
|
||||
savingsControlAccountId: mappings.savingsControlAccount?.id || '',
|
||||
transfersInSuspenseAccountId: mappings.transfersInSuspenseAccount?.id || '',
|
||||
interestOnSavingsAccountId: mappings.interestOnSavingsAccount?.id || '',
|
||||
incomeFromFeeAccountId: mappings.incomeFromFeeAccount?.id || '',
|
||||
incomeFromPenaltyAccountId: mappings.incomeFromPenaltyAccount?.id || '',
|
||||
incomeFromInterestId: mappings.incomeFromInterest?.id || '',
|
||||
overdraftPortfolioControlId: mappings.overdraftPortfolioControl?.id || '',
|
||||
writeOffAccountId: mappings.writeOffAccount?.id || '',
|
||||
feesReceivableAccountId: mappings.feeReceivableAccount?.id || '',
|
||||
penaltiesReceivableAccountId: mappings.penaltyReceivableAccount?.id || '',
|
||||
interestPayableAccountId: mappings.interestPayableAccount?.id || '',
|
||||
@@ -200,10 +202,24 @@ export class SavingProductAccountingStepComponent implements OnInit {
|
||||
this.setOtherGlMappings('assetsOtherGlMappings', glMappings?.assets || [], this.assetAccountOptions);
|
||||
this.setOtherGlMappings(
|
||||
'liabilitiesOtherGlMappings',
|
||||
glMappings?.liabilities || [],
|
||||
this.extractNamedGlMapping(
|
||||
glMappings?.liabilities || [],
|
||||
SAVINGS_INTEREST_PAYABLE_ACCOUNT_NAME,
|
||||
'savingsInterestPayableAccountId',
|
||||
this.liabilityAccountOptions
|
||||
),
|
||||
this.liabilityAccountOptions
|
||||
);
|
||||
this.setOtherGlMappings('expensesOtherGlMappings', glMappings?.expenses || [], this.expenseAccountOptions);
|
||||
this.setOtherGlMappings(
|
||||
'expensesOtherGlMappings',
|
||||
this.extractNamedGlMapping(
|
||||
glMappings?.expenses || [],
|
||||
SAVINGS_INTEREST_PAID_ACCOUNT_NAME,
|
||||
'savingsInterestPaidAccountId',
|
||||
this.expenseAccountOptions
|
||||
),
|
||||
this.expenseAccountOptions
|
||||
);
|
||||
this.setOtherGlMappings('incomeOtherGlMappings', glMappings?.income || [], this.incomeAccountOptions);
|
||||
}
|
||||
}
|
||||
@@ -225,6 +241,22 @@ export class SavingProductAccountingStepComponent implements OnInit {
|
||||
return this.savingProductAccountingForm.get(controlName) as UntypedFormArray;
|
||||
}
|
||||
|
||||
/** Pulls a reserved entry out of a saved otherGlMappings list into its own dedicated control and
|
||||
* returns the remaining entries, so it renders as a named row rather than a free-form "Other" row. */
|
||||
private extractNamedGlMapping(
|
||||
otherGlMappings: any[],
|
||||
accountName: string,
|
||||
controlName: string,
|
||||
options: GLAccount[]
|
||||
): any[] {
|
||||
const match = (otherGlMappings || []).find((mapping) => mapping?.accountName === accountName);
|
||||
if (match) {
|
||||
const glAccountId = match.glAccountId || options.find((option) => option.glCode === match.glCode)?.id || '';
|
||||
this.savingProductAccountingForm.get(controlName)?.patchValue(glAccountId);
|
||||
}
|
||||
return (otherGlMappings || []).filter((mapping) => mapping?.accountName !== accountName);
|
||||
}
|
||||
|
||||
private setOtherGlMappings(controlName: string, otherGlMappings: any[], options: GLAccount[]) {
|
||||
const otherGlMappingsArray = this.getOtherGlMappingsArray(controlName);
|
||||
otherGlMappingsArray.clear();
|
||||
@@ -282,10 +314,35 @@ export class SavingProductAccountingStepComponent implements OnInit {
|
||||
}))
|
||||
.filter((item: { accountName: string; glCode: string }) => !!item.accountName || !!item.glCode);
|
||||
|
||||
/** The two named-but-not-backend-persisted rows are emitted as reserved otherGlMappings entries so
|
||||
* they survive a save/reload round trip under Cash accounting. */
|
||||
const namedAsOtherGlMapping = (controlName: string, accountName: string, options: GLAccount[]) => {
|
||||
const glAccountId = this.savingProductAccountingForm.get(controlName)?.value;
|
||||
if (!glAccountId) {
|
||||
return [];
|
||||
}
|
||||
const glCode = options.find((option) => option.id === glAccountId)?.glCode || '';
|
||||
return glCode ? [{ accountName, glCode }] : [];
|
||||
};
|
||||
|
||||
const otherGlMappings = {
|
||||
assets: mapOtherGlMappings('assetsOtherGlMappings', this.assetAccountOptions),
|
||||
liabilities: mapOtherGlMappings('liabilitiesOtherGlMappings', this.liabilityAccountOptions),
|
||||
expenses: mapOtherGlMappings('expensesOtherGlMappings', this.expenseAccountOptions),
|
||||
liabilities: [
|
||||
...namedAsOtherGlMapping(
|
||||
'savingsInterestPayableAccountId',
|
||||
SAVINGS_INTEREST_PAYABLE_ACCOUNT_NAME,
|
||||
this.liabilityAccountOptions
|
||||
),
|
||||
...mapOtherGlMappings('liabilitiesOtherGlMappings', this.liabilityAccountOptions)
|
||||
],
|
||||
expenses: [
|
||||
...namedAsOtherGlMapping(
|
||||
'savingsInterestPaidAccountId',
|
||||
SAVINGS_INTEREST_PAID_ACCOUNT_NAME,
|
||||
this.expenseAccountOptions
|
||||
),
|
||||
...mapOtherGlMappings('expensesOtherGlMappings', this.expenseAccountOptions)
|
||||
],
|
||||
income: mapOtherGlMappings('incomeOtherGlMappings', this.incomeAccountOptions)
|
||||
};
|
||||
|
||||
@@ -320,13 +377,21 @@ export class SavingProductAccountingStepComponent implements OnInit {
|
||||
if (this.accounting.isCashOrAccrualAccountingRuleId(accountingRule)) {
|
||||
addControl('savingsReferenceAccountId', true);
|
||||
addControl('savingsControlAccountId', true);
|
||||
addControl('transfersInSuspenseAccountId', true);
|
||||
addControl('interestOnSavingsAccountId', true);
|
||||
addControl('incomeFromFeeAccountId', true);
|
||||
addControl('incomeFromPenaltyAccountId', true);
|
||||
addControl('incomeFromInterestId', true);
|
||||
addControl('writeOffAccountId', true);
|
||||
addControl('overdraftPortfolioControlId', false);
|
||||
// Savings Interest Payable / Paid are offered under Cash too, but the backend only persists the
|
||||
// core mappings for CASH_BASED, so these two ride along in otherGlMappings (see getOtherGlMappings)
|
||||
// rather than as named accounting params, which would be silently dropped. Optional by design.
|
||||
addControl('savingsInterestPayableAccountId', false);
|
||||
addControl('savingsInterestPaidAccountId', false);
|
||||
// Transfers In Suspense / Income From Interest / Write Off / Overdraft Portfolio Control are
|
||||
// intentionally not offered on this screen, so their controls are never added — leaving them in
|
||||
// with required validators would keep the form invalid with no visible field to fix.
|
||||
removeControl('transfersInSuspenseAccountId');
|
||||
removeControl('incomeFromInterestId');
|
||||
removeControl('writeOffAccountId');
|
||||
removeControl('overdraftPortfolioControlId');
|
||||
|
||||
if (this.accounting.isAccrualAccountingRuleId(accountingRule)) {
|
||||
addControl('feesReceivableAccountId', true);
|
||||
@@ -360,6 +425,8 @@ export class SavingProductAccountingStepComponent implements OnInit {
|
||||
removeControl('interestPayableAccountId');
|
||||
removeControl('interestReceivableAccountId');
|
||||
removeControl('escheatLiabilityId');
|
||||
removeControl('savingsInterestPayableAccountId');
|
||||
removeControl('savingsInterestPaidAccountId');
|
||||
this.clearOtherGlMappings();
|
||||
}
|
||||
}
|
||||
@@ -370,6 +437,9 @@ export class SavingProductAccountingStepComponent implements OnInit {
|
||||
liabilitiesOtherGlMappings,
|
||||
expensesOtherGlMappings,
|
||||
incomeOtherGlMappings,
|
||||
// Carried inside otherGlMappings instead — the backend rejects unknown top-level accounting params.
|
||||
savingsInterestPayableAccountId,
|
||||
savingsInterestPaidAccountId,
|
||||
...savingProductAccounting
|
||||
} = this.savingProductAccountingForm.value;
|
||||
const otherGlMappings = this.getOtherGlMappings();
|
||||
@@ -404,13 +474,6 @@ export class SavingProductAccountingStepComponent implements OnInit {
|
||||
options: this.assetAccountOptions,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
key: 'overdraftPortfolioControlId',
|
||||
label: 'Overdraft Portfolio Control',
|
||||
inputLabel: 'Overdraft portfolio control',
|
||||
controlName: 'overdraftPortfolioControlId',
|
||||
options: this.assetAccountOptions
|
||||
},
|
||||
{
|
||||
key: 'interestReceivableAccountId',
|
||||
label: 'Interest Receivable Account',
|
||||
@@ -454,12 +517,11 @@ export class SavingProductAccountingStepComponent implements OnInit {
|
||||
required: true
|
||||
},
|
||||
{
|
||||
key: 'transfersInSuspenseAccountId',
|
||||
label: 'Transfers In Suspense Account',
|
||||
inputLabel: 'Transfers in suspense account',
|
||||
controlName: 'transfersInSuspenseAccountId',
|
||||
options: this.liabilityAccountOptions,
|
||||
required: true
|
||||
key: 'savingsInterestPayableAccountId',
|
||||
label: 'Savings Interest Payable',
|
||||
inputLabel: 'Savings interest payable',
|
||||
controlName: 'savingsInterestPayableAccountId',
|
||||
options: this.liabilityAccountOptions
|
||||
},
|
||||
{
|
||||
key: 'interestPayableAccountId',
|
||||
@@ -496,12 +558,11 @@ export class SavingProductAccountingStepComponent implements OnInit {
|
||||
required: true
|
||||
},
|
||||
{
|
||||
key: 'writeOffAccountId',
|
||||
label: 'Write Off Account',
|
||||
inputLabel: 'Write off account',
|
||||
controlName: 'writeOffAccountId',
|
||||
options: this.expenseAccountOptions,
|
||||
required: true
|
||||
key: 'savingsInterestPaidAccountId',
|
||||
label: 'Savings Interest Paid',
|
||||
inputLabel: 'Savings interest paid',
|
||||
controlName: 'savingsInterestPaidAccountId',
|
||||
options: this.expenseAccountOptions
|
||||
}
|
||||
],
|
||||
otherMappingsControlName: 'expensesOtherGlMappings',
|
||||
@@ -528,14 +589,6 @@ export class SavingProductAccountingStepComponent implements OnInit {
|
||||
controlName: 'incomeFromPenaltyAccountId',
|
||||
options: this.incomeAccountOptions,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
key: 'incomeFromInterestId',
|
||||
label: 'Income From Interest',
|
||||
inputLabel: 'Income from interest',
|
||||
controlName: 'incomeFromInterestId',
|
||||
options: this.incomeAccountOptions,
|
||||
required: true
|
||||
}
|
||||
],
|
||||
otherMappingsControlName: 'incomeOtherGlMappings',
|
||||
|
||||
+9
-1
@@ -26,6 +26,7 @@ import {
|
||||
MatRowDef,
|
||||
MatRow
|
||||
} from '@angular/material/table';
|
||||
import { FacilityChargeFilterService } from 'app/shared/charges/facility-charge-filter.service';
|
||||
import { ChargesFilterPipe } from '../../../../pipes/charges-filter.pipe';
|
||||
import { FormatNumberPipe } from '../../../../pipes/format-number.pipe';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
@@ -55,6 +56,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
export class SavingProductChargesStepComponent implements OnInit {
|
||||
dialog = inject(MatDialog);
|
||||
private translateService = inject(TranslateService);
|
||||
private facilityChargeFilter = inject(FacilityChargeFilterService);
|
||||
|
||||
@Input() savingProductsTemplate: any;
|
||||
@Input() currencyCode: UntypedFormControl;
|
||||
@@ -73,8 +75,14 @@ export class SavingProductChargesStepComponent implements OnInit {
|
||||
pristine = true;
|
||||
|
||||
ngOnInit() {
|
||||
this.chargeData = this.savingProductsTemplate.chargeOptions;
|
||||
// Facility-priced charges (Cheque Book, ATM, UPI, ...) are levied by the facility mapping itself,
|
||||
// so they must not be offered here as well — see FacilityChargeFilterService.
|
||||
this.facilityChargeFilter
|
||||
.excludeFacilityCharges(this.savingProductsTemplate.chargeOptions)
|
||||
.subscribe((charges: any[]) => (this.chargeData = charges));
|
||||
|
||||
// Already-attached charges are shown as-is: filtering them here would silently drop them
|
||||
// from the payload on save.
|
||||
this.chargesDataSource = this.savingProductsTemplate.charges || [];
|
||||
this.pristine = true;
|
||||
|
||||
|
||||
+91
@@ -63,6 +63,97 @@
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
The periods that will apply to every account opened on this product, shown as soon as
|
||||
tracking is switched on. Read-only until asked for: these are the RBI periods, and a product
|
||||
should get them unless somebody deliberately departs from them.
|
||||
-->
|
||||
@if (dormancyTrackingOn) {
|
||||
<div class="spc-dorm">
|
||||
<div class="spc-dorm__head">
|
||||
<div>
|
||||
<h4>Dormancy Periods</h4>
|
||||
<p>
|
||||
Counted from the last customer transaction — a deposit or withdrawal the customer makes. Interest the
|
||||
bank posts does not reset the clock.
|
||||
</p>
|
||||
</div>
|
||||
<div class="spc-dorm__actions">
|
||||
@if (editingDormancyDays) {
|
||||
<button type="button" class="spc-dorm__btn" (click)="resetDormancyDays()">
|
||||
<fa-icon icon="undo"></fa-icon> RBI defaults
|
||||
</button>
|
||||
}
|
||||
<button type="button" class="spc-dorm__btn" (click)="toggleDormancyDaysEdit()">
|
||||
<fa-icon [icon]="editingDormancyDays ? 'times' : 'edit'"></fa-icon>
|
||||
{{ editingDormancyDays ? 'Cancel' : 'Edit' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spc-dorm__grid">
|
||||
<div class="spc-dorm__item">
|
||||
<span class="spc-dorm__label">Inactive after</span>
|
||||
@if (editingDormancyDays) {
|
||||
<mat-form-field appearance="outline" class="spc-dorm__field">
|
||||
<input matInput type="number" min="1" formControlName="daysToInactive" />
|
||||
<span matSuffix>days</span>
|
||||
</mat-form-field>
|
||||
} @else {
|
||||
<span class="spc-dorm__value">
|
||||
{{ savingProductCurrencyForm.value.daysToInactive }} <small>days</small>
|
||||
</span>
|
||||
}
|
||||
<span class="spc-dorm__note">Flagged internally. Nothing is restricted.</span>
|
||||
</div>
|
||||
|
||||
<div class="spc-dorm__item">
|
||||
<span class="spc-dorm__label">Dormant after</span>
|
||||
@if (editingDormancyDays) {
|
||||
<mat-form-field appearance="outline" class="spc-dorm__field">
|
||||
<input matInput type="number" min="1" formControlName="daysToDormancy" />
|
||||
<span matSuffix>days</span>
|
||||
</mat-form-field>
|
||||
} @else {
|
||||
<span class="spc-dorm__value">
|
||||
{{ savingProductCurrencyForm.value.daysToDormancy }} <small>days</small>
|
||||
</span>
|
||||
}
|
||||
<span class="spc-dorm__note">Debits stop. Credits still land.</span>
|
||||
</div>
|
||||
|
||||
<div class="spc-dorm__item">
|
||||
<span class="spc-dorm__label">Escheat after</span>
|
||||
@if (editingDormancyDays) {
|
||||
<mat-form-field appearance="outline" class="spc-dorm__field">
|
||||
<input matInput type="number" min="1" formControlName="daysToEscheat" />
|
||||
<span matSuffix>days</span>
|
||||
</mat-form-field>
|
||||
} @else {
|
||||
<span class="spc-dorm__value">
|
||||
{{ savingProductCurrencyForm.value.daysToEscheat }} <small>days</small>
|
||||
</span>
|
||||
}
|
||||
<span class="spc-dorm__note">Balance becomes due to RBI's DEAF.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (dormancyDaysOutOfOrder) {
|
||||
<div class="spc-dorm__error">
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
||||
The periods have to run in order: dormant later than inactive, escheat later than dormant. An account
|
||||
cannot reach a later stage before an earlier one.
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="spc-dorm__foot">
|
||||
<fa-icon icon="info-circle"></fa-icon>
|
||||
Accounts opened on this product inherit these periods. An account that receives government benefit credits
|
||||
can be exempted individually, and is then never restricted for inactivity.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="spc-alert">
|
||||
<div class="spc-alert__content">
|
||||
<h4>Nomination Facility</h4>
|
||||
|
||||
+130
@@ -112,3 +112,133 @@ $gray-900: #202124;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Dormancy periods, shown once tracking is switched on ─────────────────── */
|
||||
|
||||
.spc-dorm {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem 1.15rem;
|
||||
border: 1px solid rgb(0 0 0 / 12%);
|
||||
border-left: 3px solid #1565c0;
|
||||
border-radius: 6px;
|
||||
background: rgb(21 101 192 / 3%);
|
||||
}
|
||||
|
||||
.spc-dorm__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.2rem 0 0;
|
||||
font-size: 0.78rem;
|
||||
opacity: 0.7;
|
||||
max-width: 34rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
}
|
||||
|
||||
.spc-dorm__actions {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.spc-dorm__btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.3rem 0.7rem;
|
||||
border: 1px solid rgb(0 0 0 / 20%);
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
font: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: #1565c0;
|
||||
color: #1565c0;
|
||||
}
|
||||
}
|
||||
|
||||
.spc-dorm__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
|
||||
gap: 0.9rem;
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
|
||||
.spc-dorm__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.spc-dorm__label {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.spc-dorm__value {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
small {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.spc-dorm__field {
|
||||
width: 100%;
|
||||
|
||||
/* The stepper's fields are roomier than this panel needs. */
|
||||
::ng-deep .mat-mdc-form-field-subscript-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.spc-dorm__note {
|
||||
font-size: 0.72rem;
|
||||
opacity: 0.65;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
/* Periods that would have an account reach a later stage before an earlier one. */
|
||||
.spc-dorm__error {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
padding: 0.5rem 0.7rem;
|
||||
border-radius: 4px;
|
||||
background: rgb(192 57 43 / 9%);
|
||||
color: #c0392b;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.spc-dorm__foot {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.6rem;
|
||||
border-top: 1px solid rgb(0 0 0 / 8%);
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
+83
-2
@@ -12,6 +12,7 @@ import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { MatRadioGroup, MatRadioButton } from '@angular/material/radio';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { DORMANCY_DEFAULTS } from '../../../dormancy-defaults';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-saving-product-currency-step',
|
||||
@@ -50,10 +51,68 @@ export class SavingProductCurrencyStepComponent implements OnInit {
|
||||
currencyCode: this.savingProductsTemplate.currency?.code || this.currencyData?.[0]?.code || '',
|
||||
digitsAfterDecimal: this.savingProductsTemplate.digitsAfterDecimal ?? 2,
|
||||
dormancyAlert: this.savingProductsTemplate.isDormancyTrackingActive ?? false,
|
||||
nominationFacility: this.savingProductsTemplate.nominationFacility ?? false
|
||||
nominationFacility: this.savingProductsTemplate.nominationFacility ?? false,
|
||||
daysToInactive: this.savingProductsTemplate.daysToInactive ?? DORMANCY_DEFAULTS.daysToInactive,
|
||||
daysToDormancy: this.savingProductsTemplate.daysToDormancy ?? DORMANCY_DEFAULTS.daysToDormancy,
|
||||
daysToEscheat: this.savingProductsTemplate.daysToEscheat ?? DORMANCY_DEFAULTS.daysToEscheat
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the periods are open for editing.
|
||||
*
|
||||
* They start read-only and showing the RBI defaults. A bank that wants its own numbers has to ask
|
||||
* for them, which keeps the regulatory periods as what a product gets unless somebody deliberately
|
||||
* departs from them.
|
||||
*/
|
||||
editingDormancyDays = false;
|
||||
|
||||
get dormancyTrackingOn(): boolean {
|
||||
return !!this.savingProductCurrencyForm.get('dormancyAlert')?.value;
|
||||
}
|
||||
|
||||
toggleDormancyDaysEdit(): void {
|
||||
this.editingDormancyDays = !this.editingDormancyDays;
|
||||
if (!this.editingDormancyDays) {
|
||||
// Leaving edit mode puts back whatever the product had, so a half-typed number is not kept.
|
||||
this.savingProductCurrencyForm.patchValue({
|
||||
daysToInactive: this.savingProductsTemplate.daysToInactive ?? DORMANCY_DEFAULTS.daysToInactive,
|
||||
daysToDormancy: this.savingProductsTemplate.daysToDormancy ?? DORMANCY_DEFAULTS.daysToDormancy,
|
||||
daysToEscheat: this.savingProductsTemplate.daysToEscheat ?? DORMANCY_DEFAULTS.daysToEscheat
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Puts the RBI periods back, for a product that was edited away from them. */
|
||||
resetDormancyDays(): void {
|
||||
this.savingProductCurrencyForm.patchValue({
|
||||
daysToInactive: DORMANCY_DEFAULTS.daysToInactive,
|
||||
daysToDormancy: DORMANCY_DEFAULTS.daysToDormancy,
|
||||
daysToEscheat: DORMANCY_DEFAULTS.daysToEscheat
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the periods no longer run inactive → dormant → escheat in order.
|
||||
*
|
||||
* A dormant threshold at or before the inactive one would have an account reach the later stage
|
||||
* first, which the job cannot act on sensibly, so the step says so rather than saving it.
|
||||
*/
|
||||
get dormancyDaysOutOfOrder(): boolean {
|
||||
const v = this.savingProductCurrencyForm.value;
|
||||
const inactive = Number(v.daysToInactive);
|
||||
const dormant = Number(v.daysToDormancy);
|
||||
const escheat = Number(v.daysToEscheat);
|
||||
if ([
|
||||
inactive,
|
||||
dormant,
|
||||
escheat
|
||||
].some((n) => !Number.isFinite(n) || n <= 0)) {
|
||||
return false;
|
||||
}
|
||||
return dormant <= inactive || escheat <= dormant;
|
||||
}
|
||||
|
||||
createSavingProductCurrencyForm() {
|
||||
this.savingProductCurrencyForm = this.formBuilder.group({
|
||||
currencyCode: [
|
||||
@@ -68,7 +127,21 @@ export class SavingProductCurrencyStepComponent implements OnInit {
|
||||
]
|
||||
],
|
||||
dormancyAlert: [false],
|
||||
nominationFacility: [false]
|
||||
nominationFacility: [false],
|
||||
// The RBI periods, counted from the last customer transaction. Editable, but a product gets
|
||||
// these unless somebody deliberately changes them.
|
||||
daysToInactive: [
|
||||
DORMANCY_DEFAULTS.daysToInactive,
|
||||
Validators.min(1)
|
||||
],
|
||||
daysToDormancy: [
|
||||
DORMANCY_DEFAULTS.daysToDormancy,
|
||||
Validators.min(1)
|
||||
],
|
||||
daysToEscheat: [
|
||||
DORMANCY_DEFAULTS.daysToEscheat,
|
||||
Validators.min(1)
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -82,6 +155,14 @@ export class SavingProductCurrencyStepComponent implements OnInit {
|
||||
nominationFacility: formValue.nominationFacility
|
||||
};
|
||||
|
||||
// The periods only mean anything while tracking is on; sending them with it off would store
|
||||
// thresholds against a product nothing acts on.
|
||||
if (formValue.dormancyAlert) {
|
||||
result.daysToInactive = formValue.daysToInactive;
|
||||
result.daysToDormancy = formValue.daysToDormancy;
|
||||
result.daysToEscheat = formValue.daysToEscheat;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
-15
@@ -174,21 +174,6 @@
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="spl-feature">
|
||||
<span class="spl-feature__label">
|
||||
<i class="fas fa-arrow-right-arrow-left"></i>
|
||||
Withdrawal Facility
|
||||
</span>
|
||||
<select
|
||||
class="spl-select"
|
||||
[(ngModel)]="accountFacilityConfig.withdrawalFacility"
|
||||
(ngModelChange)="markAsDirty()"
|
||||
>
|
||||
<option [ngValue]="false">No</option>
|
||||
<option [ngValue]="true">Yes</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="spl-feature">
|
||||
<span class="spl-feature__label">
|
||||
<i class="fas fa-id-card"></i>
|
||||
|
||||
-2
@@ -24,7 +24,6 @@ type BooleanFeatureKey =
|
||||
| 'neftFacility'
|
||||
| 'rtgsFacility'
|
||||
| 'interBankTotalCapFacility'
|
||||
| 'withdrawalFacility'
|
||||
| 'depositFacilityKyc'
|
||||
| 'virtualDebitCard';
|
||||
|
||||
@@ -76,7 +75,6 @@ export class SavingProductLimitsStepComponent implements OnChanges {
|
||||
neftFacility: true,
|
||||
rtgsFacility: true,
|
||||
interBankTotalCapFacility: true,
|
||||
withdrawalFacility: true,
|
||||
depositFacilityKyc: true,
|
||||
virtualDebitCard: false
|
||||
};
|
||||
|
||||
+40
-72
@@ -475,42 +475,35 @@
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Overdraft portfolio control</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.overdraftPortfolioControlId,
|
||||
savingProductsTemplate?.accountingMappingOptions.assetAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Interest receivable account</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.interestReceivableAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.assetAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Fees receivable account</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.feesReceivableAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.assetAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Penalties receivable account</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.penaltiesReceivableAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.assetAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
@if (isAccrualAccounting()) {
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Interest receivable account</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.interestReceivableAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.assetAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Fees receivable account</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.feesReceivableAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.assetAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Penalties receivable account</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.penaltiesReceivableAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.assetAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="spa-gl-section">
|
||||
@@ -524,24 +517,17 @@
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Transfers in suspense account</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.transfersInSuspenseAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.liabilityAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Interest payable account</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.interestPayableAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.liabilityAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
@if (isAccrualAccounting()) {
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Interest payable account</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.interestPayableAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.liabilityAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingProduct.isDormancyTrackingActive) {
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Escheat liability account</span>
|
||||
@@ -566,15 +552,6 @@
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Write off account</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.writeOffAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.expenseAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spa-gl-section">
|
||||
@@ -597,15 +574,6 @@
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Income from interest</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.incomeFromInterestId,
|
||||
savingProductsTemplate?.accountingMappingOptions.incomeAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
+1
-12
@@ -94,7 +94,6 @@ export class SavingProductPreviewStepComponent implements OnInit, OnChanges {
|
||||
{ key: 'neftFacility', label: 'NEFT Facility' },
|
||||
{ key: 'rtgsFacility', label: 'RTGS Facility' },
|
||||
{ key: 'interBankTotalCapFacility', label: 'Inter-Bank Total Cap Facility' },
|
||||
{ key: 'withdrawalFacility', label: 'Withdrawal Facility' },
|
||||
{ key: 'depositFacilityKyc', label: 'Deposit Facility (KYC)' },
|
||||
{ key: 'virtualDebitCard', label: 'Virtual Debit Card' }
|
||||
];
|
||||
@@ -132,23 +131,14 @@ export class SavingProductPreviewStepComponent implements OnInit, OnChanges {
|
||||
this.savingProduct.savingsReferenceAccountId,
|
||||
assetAccountData
|
||||
),
|
||||
overdraftPortfolioControl: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.overdraftPortfolioControlId,
|
||||
assetAccountData
|
||||
),
|
||||
savingsControlAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.savingsControlAccountId,
|
||||
liabilityAccountData
|
||||
),
|
||||
transfersInSuspenseAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.transfersInSuspenseAccountId,
|
||||
liabilityAccountData
|
||||
),
|
||||
interestOnSavingsAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.interestOnSavingsAccountId,
|
||||
expenseAccountData
|
||||
),
|
||||
writeOffAccount: this.accounting.glAccountLookUp(this.savingProduct.writeOffAccountId, expenseAccountData),
|
||||
incomeFromFeeAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.incomeFromFeeAccountId,
|
||||
incomeAccountData
|
||||
@@ -156,8 +146,7 @@ export class SavingProductPreviewStepComponent implements OnInit, OnChanges {
|
||||
incomeFromPenaltyAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.incomeFromPenaltyAccountId,
|
||||
incomeAccountData
|
||||
),
|
||||
incomeFromInterest: this.accounting.glAccountLookUp(this.savingProduct.incomeFromInterestId, incomeAccountData)
|
||||
)
|
||||
};
|
||||
|
||||
if (this.isAccrualAccounting()) {
|
||||
|
||||
+6
-11
@@ -70,11 +70,7 @@ export class SavingProductSettingsStepComponent implements OnInit {
|
||||
: this.savingProductsTemplate.lockinPeriodFrequency && this.savingProductsTemplate.lockinPeriodFrequency > 0,
|
||||
accountClosedZeroBalanceInDayClosing: isEditing
|
||||
? (this.savingProductsTemplate.accountClosedZeroBalanceInDayClosing ?? false)
|
||||
: false,
|
||||
isDormancyTrackingActive: isEditing ? (this.savingProductsTemplate.isDormancyTrackingActive ?? true) : true,
|
||||
daysToInactive: isEditing ? this.savingProductsTemplate.daysToInactive || 30 : 30,
|
||||
daysToDormancy: isEditing ? this.savingProductsTemplate.daysToDormancy || 60 : 60,
|
||||
daysToEscheat: isEditing ? this.savingProductsTemplate.daysToEscheat || 90 : 90
|
||||
: false
|
||||
});
|
||||
|
||||
if (
|
||||
@@ -115,12 +111,11 @@ export class SavingProductSettingsStepComponent implements OnInit {
|
||||
enableLockinPeriod: [false],
|
||||
lockinPeriodFrequency: [''],
|
||||
lockinPeriodFrequencyType: [''],
|
||||
accountClosedZeroBalanceInDayClosing: [false],
|
||||
// Dormancy tracking fields (required when isDormancyTrackingActive is true)
|
||||
isDormancyTrackingActive: [true],
|
||||
daysToInactive: [30],
|
||||
daysToDormancy: [60],
|
||||
daysToEscheat: [90]
|
||||
accountClosedZeroBalanceInDayClosing: [false]
|
||||
// Dormancy is deliberately absent: it belongs to the Currency step, which is where it is shown
|
||||
// and edited. It was declared here as well, and because this step is spread into the payload
|
||||
// after that one, the numbers an officer typed were silently replaced by these untouched
|
||||
// defaults on the way to the server.
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -86,7 +86,6 @@ export class SavingProductGeneralTabComponent {
|
||||
{ key: 'neftFacility', label: 'NEFT Facility' },
|
||||
{ key: 'rtgsFacility', label: 'RTGS Facility' },
|
||||
{ key: 'interBankTotalCapFacility', label: 'Inter-Bank Total Cap Facility' },
|
||||
{ key: 'withdrawalFacility', label: 'Withdrawal Facility' },
|
||||
{ key: 'depositFacilityKyc', label: 'Deposit Facility (KYC)' },
|
||||
{ key: 'virtualDebitCard', label: 'Virtual Debit Card' }
|
||||
];
|
||||
|
||||
+1
-3
@@ -76,9 +76,7 @@
|
||||
<input matInput type="number" step="0.01" min="0.01" max="100" formControlName="ratePercentage" />
|
||||
<span matSuffix> % </span>
|
||||
</mat-form-field>
|
||||
<p class="dd-hint">
|
||||
Applied to each member's own share investment, pro-rated for how long they held it.
|
||||
</p>
|
||||
<p class="dd-hint">Applied to each member's own share investment, pro-rated for how long they held it.</p>
|
||||
</div>
|
||||
|
||||
@if (loadingPreview) {
|
||||
|
||||
@@ -248,27 +248,67 @@ export class ViewDividendComponent implements OnInit {
|
||||
ws.getCell('A1').alignment = { horizontal: 'center' };
|
||||
|
||||
const heading: [string, any][] = [
|
||||
['Share Product', h?.productName ?? ''],
|
||||
['Financial Year', fy],
|
||||
['Dividend Period', h ? `${h.periodStartDate} to ${h.periodEndDate}` : ''],
|
||||
['Dividend Rate (% per annum)', h?.ratePercentage != null ? Number(h.ratePercentage) : 'Not recorded'],
|
||||
['Declared Amount', h?.declaredAmount != null ? Number(h.declaredAmount) : ''],
|
||||
['Distributed to Members', h?.allocatedAmount != null ? Number(h.allocatedAmount) : ''],
|
||||
['Payment Date', h?.paymentDate ? String(h.paymentDate).slice(0, 10) : 'Not yet approved'],
|
||||
['Status', h?.status === 'APPROVED' ? 'Approved' : 'Awaiting Approval'],
|
||||
['Members Paid', h?.paidMembers ?? ''],
|
||||
['Members Below Minimum', h?.unpaidMembers ?? '']
|
||||
[
|
||||
'Share Product',
|
||||
h?.productName ?? ''
|
||||
],
|
||||
[
|
||||
'Financial Year',
|
||||
fy
|
||||
],
|
||||
[
|
||||
'Dividend Period',
|
||||
h ? `${h.periodStartDate} to ${h.periodEndDate}` : ''
|
||||
],
|
||||
[
|
||||
'Dividend Rate (% per annum)',
|
||||
h?.ratePercentage != null ? Number(h.ratePercentage) : 'Not recorded'
|
||||
],
|
||||
[
|
||||
'Declared Amount',
|
||||
h?.declaredAmount != null ? Number(h.declaredAmount) : ''
|
||||
],
|
||||
[
|
||||
'Distributed to Members',
|
||||
h?.allocatedAmount != null ? Number(h.allocatedAmount) : ''
|
||||
],
|
||||
[
|
||||
'Payment Date',
|
||||
h?.paymentDate ? String(h.paymentDate).slice(0, 10) : 'Not yet approved'
|
||||
],
|
||||
[
|
||||
'Status',
|
||||
h?.status === 'APPROVED' ? 'Approved' : 'Awaiting Approval'
|
||||
],
|
||||
[
|
||||
'Members Paid',
|
||||
h?.paidMembers ?? ''
|
||||
],
|
||||
[
|
||||
'Members Below Minimum',
|
||||
h?.unpaidMembers ?? ''
|
||||
]
|
||||
];
|
||||
heading.forEach(([label, value], i) => {
|
||||
const row = ws.getRow(i + 3);
|
||||
row.getCell(1).value = label;
|
||||
row.getCell(1).font = { bold: true };
|
||||
row.getCell(2).value = value;
|
||||
});
|
||||
heading.forEach(
|
||||
([
|
||||
label,
|
||||
value
|
||||
], i) => {
|
||||
const row = ws.getRow(i + 3);
|
||||
row.getCell(1).value = label;
|
||||
row.getCell(1).font = { bold: true };
|
||||
row.getCell(2).value = value;
|
||||
}
|
||||
);
|
||||
|
||||
const tableStart = heading.length + 5;
|
||||
const header = ws.getRow(tableStart);
|
||||
['Client Name', 'Share Account', 'Dividend Amount', 'Status'].forEach((t, i) => {
|
||||
[
|
||||
'Client Name',
|
||||
'Share Account',
|
||||
'Dividend Amount',
|
||||
'Status'
|
||||
].forEach((t, i) => {
|
||||
header.getCell(i + 1).value = t;
|
||||
header.getCell(i + 1).font = { bold: true };
|
||||
header.getCell(i + 1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE8ECF0' } };
|
||||
@@ -286,7 +326,10 @@ export class ViewDividendComponent implements OnInit {
|
||||
const totalRow = ws.getRow(tableStart + 1 + (this.dataSource?.data?.length ?? 0));
|
||||
totalRow.getCell(2).value = 'Total';
|
||||
totalRow.getCell(2).font = { bold: true };
|
||||
totalRow.getCell(3).value = (this.dataSource?.data ?? []).reduce((s: number, d: any) => s + (Number(d?.amount) || 0), 0);
|
||||
totalRow.getCell(3).value = (this.dataSource?.data ?? []).reduce(
|
||||
(s: number, d: any) => s + (Number(d?.amount) || 0),
|
||||
0
|
||||
);
|
||||
totalRow.getCell(3).numFmt = '#,##0.00';
|
||||
totalRow.getCell(3).font = { bold: true };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user