diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.ts index a8ebfa8de..666f126d5 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.ts @@ -334,17 +334,18 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan const frequencyId = this.resolveFrequencyId(this.tenurePeriodUnit); if (this.principalAmount && this.principalAmount > 0) { - // Convert tenure to months for backend API. A Days tenure under 30 days rounds down to 0 - // months, which the backend rejects -- send it as tenureInDays instead in that case. + // Years and Months convert exactly; a Days tenure is sent as days and never converted. let tenureInMonths: number | undefined; let tenureInDays: number | undefined; if (this.tenurePeriodUnit === 'Years') { tenureInMonths = this.tenureValue * 12; } else if (this.tenurePeriodUnit === 'Months') { tenureInMonths = this.tenureValue; - } else if (enteredDays >= 30) { - tenureInMonths = Math.floor(enteredDays / 30); } else { + // Sent as days, whatever the count. Converting to whole months silently dropped the + // remainder — a 200-day deposit quoted as six months lost 20 days of interest, and the + // screen still said "200 Days" above the figure. The backend takes days directly and + // works the calendar out itself. tenureInDays = enteredDays; } @@ -423,10 +424,17 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan } else { const n = 12 / compMonths; const effectiveMonths = tenureInMonths ?? enteredDays / 30; - const totalCompoundings = Math.floor(effectiveMonths / compMonths); const rPerCompounding = rate / (n * 100); - result.maturityAmount = - Math.round(this.principalAmount * Math.pow(1 + rPerCompounding, totalCompoundings) * 100) / 100; + // Whole compounding periods, then simple interest on the days left over. Flooring the + // periods and stopping there dropped the remainder entirely: a 200-day deposit + // compounding monthly quoted only six months of interest and lost the other twenty + // days, which is the depositor's money. + const wholePeriods = Math.floor(effectiveMonths / compMonths); + const compounded = this.principalAmount * Math.pow(1 + rPerCompounding, wholePeriods); + const leftoverDays = enteredDays - wholePeriods * compMonths * 30; + const leftoverInterest = + leftoverDays > 0 ? (compounded * rate * leftoverDays) / (100 * daysInYearVal) : 0; + result.maturityAmount = Math.round((compounded + leftoverInterest) * 100) / 100; } result.interestEarned = Math.round((result.maturityAmount - this.principalAmount) * 100) / 100; this.matchResult = { ...result }; diff --git a/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.html b/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.html index 0e5f05cb9..9c8f007c2 100644 --- a/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.html +++ b/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.html @@ -27,101 +27,104 @@ } @else { @if (showRates) { -
-

Reserve rates

-

- Rates are effective-dated rather than overwritten: a return filed for an earlier period keeps reporting the rate - that was actually in force then. Saving against a date that already has a rate edits it; a new date adds one. -

+
+

Reserve rates

+

+ Rates are effective-dated rather than overwritten: a return filed for an earlier period keeps reporting the + rate that was actually in force then. Saving against a date that already has a rate edits it; a new date adds + one. +

-
- @for (type of ['CRR', 'SLR', 'SAVINGS_DEMAND']; track type) { -
- - {{ type === 'SAVINGS_DEMAND' ? 'Savings demand share' : type }} in force today - - @if (currentRate(type); as rate) { - {{ rate.percentage }}% - @if (rate.seedDefault) { - - Placeholder — confirm the rate actually in force - - } @else { - since {{ rate.effectiveFrom }} - } - } @else { - Not set - {{ type === 'CRR' ? 'Form 1' : 'Form 9' }} cannot compute - } -
- } -
- -
- - Rate - - CRR — Form 1 - SLR — Form 9 - - Savings — demand share - - - - - Percentage (%) - - - - - Effective from - - - - -
- - - - - - - - - - - - - @for (rate of rates; track rate.id) { - - - - - - - + } @else { + Not set + {{ type === 'CRR' ? 'Form 1' : 'Form 9' }} cannot compute + } + } - -
RatePercentageEffective fromStatus
{{ rate.rateType }}{{ rate.percentage }}%{{ rate.effectiveFrom }} - @if (isFuture(rate)) { - Future-dated - } @else if (currentRate(rate.rateType)?.id === rate.id) { - In force - } @else { - Superseded - } +
+ @for (type of ['CRR', 'SLR', 'SAVINGS_DEMAND']; track type) { +
+ + {{ type === 'SAVINGS_DEMAND' ? 'Savings demand share' : type }} in force today + + @if (currentRate(type); as rate) { + {{ rate.percentage }}% @if (rate.seedDefault) { - Placeholder + + Placeholder — confirm the rate actually in force + + } @else { + since {{ rate.effectiveFrom }} } -
- -
-
+
+ +
+ + Rate + + CRR — Form 1 + SLR — Form 9 + + Savings — demand share + + + + + Percentage (%) + + + + + Effective from + + + + +
+ + + + + + + + + + + + + @for (rate of rates; track rate.id) { + + + + + + + + } + +
RatePercentageEffective fromStatus
+ {{ rate.rateType }} + {{ rate.percentage }}%{{ rate.effectiveFrom }} + @if (isFuture(rate)) { + Future-dated + } @else if (currentRate(rate.rateType)?.id === rate.id) { + In force + } @else { + Superseded + } + @if (rate.seedDefault) { + Placeholder + } + + +
+ } @@ -324,6 +327,17 @@ + +

+ Naming a head Memorandum - …, Annexure A - … or + Annexure B - … prints it under that section of the return instead of in the main + statement. +

+ diff --git a/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.scss b/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.scss index 4b597578c..2660489ab 100644 --- a/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.scss +++ b/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.scss @@ -297,7 +297,7 @@ $red: #c5221f; &--active { border-color: $primary; - background: rgba(19, 83, 160, 0.06); + background: rgb(19 83 160 / 6%); color: $primary; } @@ -306,13 +306,13 @@ $red: #c5221f; font-weight: 500; padding: 1px 7px; border-radius: 10px; - background: rgba(176, 96, 0, 0.12); + background: rgb(176 96 0 / 12%); color: $amber; // A fully mapped return is worth stating as plainly as an incomplete one, so the tab row can // be read at a glance instead of by elimination. &--done { - background: rgba(19, 115, 51, 0.12); + background: rgb(19 115 51 / 12%); color: $green; } } @@ -379,6 +379,12 @@ $red: #c5221f; } } +// Spans the add-head form's columns rather than sitting in one of them. +.rrc-add-role__note { + flex-basis: 100%; + margin: 0 !important; +} + .rrc-remove { min-width: 0 !important; padding: 0 8px !important; @@ -397,7 +403,7 @@ $red: #c5221f; padding: 3px 10px; } -@media (max-width: 720px) { +@media (width <= 720px) { .rrc-rate-form__type, .rrc-rate-form__pct, .rrc-rate-form__date, @@ -419,7 +425,7 @@ $red: #c5221f; font-size: 0.72rem; letter-spacing: 0.04em; text-transform: uppercase; - color: rgba(0, 0, 0, 0.55); + color: rgb(0 0 0 / 55%); font-weight: 600; padding: 0 0.5rem 0.25rem 0; } @@ -447,7 +453,7 @@ $red: #c5221f; /* An incomplete row is marked where it is, rather than only by the disabled Save button, so it is clear which row is holding the save back. */ &__row--invalid td { - background: rgba(214, 106, 0, 0.06); + background: rgb(214 106 0 / 6%); } } @@ -460,7 +466,7 @@ $red: #c5221f; .rrc-add { margin-top: 1.25rem; padding-top: 1rem; - border-top: 1px dashed rgba(0, 0, 0, 0.12); + border-top: 1px dashed rgb(0 0 0 / 12%); &__form { display: flex; diff --git a/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.ts b/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.ts index 3499691c4..73a9c7de2 100644 --- a/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.ts +++ b/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.ts @@ -91,12 +91,19 @@ export class RbiReserveConfigComponent implements OnInit { { value: 'TIME', label: 'Time' } ]; - /** How a head an administrator adds is consumed by the return. */ + /** + * How a head an administrator adds is consumed by the return. + * + * MAINTAINED and SLR_ASSET are separate because the two returns admit different assets: CRR is + * met in cash and balances with banks, SLR in cash, gold and approved securities. One option for + * both would let a head meant for Form 1 quietly satisfy Form 9. + */ readonly roleUsages = [ - { value: 'LIABILITY', label: 'Liability — adds to the DTL base' }, - { value: 'ASSET_NET', label: 'Asset — netted off the base (NDTL)' }, - { value: 'MAINTAINED', label: 'Counts toward what is maintained' }, - { value: 'MEMO', label: 'Memo — shown but totals nothing' } + { value: 'LIABILITY', label: 'Liability — adds to the DTL base (Form 1 & 9)' }, + { value: 'ASSET_NET', label: 'Asset — netted off the base to give NDTL (Form 1 & 9)' }, + { value: 'MAINTAINED', label: 'Counts toward CRR maintained (Form 1)' }, + { value: 'SLR_ASSET', label: 'Counts toward SLR maintained (Form 9)' }, + { value: 'MEMO', label: 'Memo — shown on the return but totals nothing' } ]; /** New-head form. */ @@ -168,10 +175,12 @@ export class RbiReserveConfigComponent implements OnInit { } byGroup.get(group)!.roles.push(role); } - this.roleGroups = [...byGroup.entries()].map(([ - name, - entry - ]) => ({ name, label: entry.label, roles: entry.roles })); + this.roleGroups = [...byGroup.entries()].map( + ([ + name, + entry + ]) => ({ name, label: entry.label, roles: entry.roles }) + ); // Keep the open tab if it still exists, so saving a role does not throw the user back to the // first tab with their place lost. @@ -237,8 +246,7 @@ export class RbiReserveConfigComponent implements OnInit { // a bank look compliant when it is not. this.selection[role.configKey] = [ ...(this.selection[role.configKey] || []), - { glAccountId: null as any, weightPercentage: 100, liabilityClass: role.classified ? 'TIME' : null } - ]; + { glAccountId: null as any, weightPercentage: 100, liabilityClass: role.classified ? 'TIME' : null }]; } removeMapping(role: any, index: number): void { @@ -252,9 +260,7 @@ export class RbiReserveConfigComponent implements OnInit { */ availableAccounts(role: any, index: number): any[] { const rows = this.selection[role.configKey] || []; - const taken = new Set( - rows.filter((_, i) => i !== index).map((m) => `${m.glAccountId}/${m.liabilityClass ?? ''}`) - ); + const taken = new Set(rows.filter((_, i) => i !== index).map((m) => `${m.glAccountId}/${m.liabilityClass ?? ''}`)); const current = rows[index]; return this.glAccountOptions.filter((o) => !taken.has(`${o.id}/${current?.liabilityClass ?? ''}`)); } diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.ts b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.ts index 921f9773e..0f6bb338f 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.ts +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.ts @@ -308,6 +308,11 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha setRequired('incomeFromPenaltyAccountId', cashOrAccrual); setRequired('feesReceivableAccountId', accrualOnly); setRequired('penaltiesReceivableAccountId', accrualOnly); + // Required only on accrual, where there is no posting without it. On cash it stays optional but + // is offered, because interest posting routes through it when the product names one (see + // AccountingProcessorHelper.createInterestPostingJournalEntriesForSavings): the payable carries + // interest declared but not yet credited, and a product that names none collapses straight to + // Savings Control instead. setRequired('interestPayableAccountId', accrualOnly); setRequired('escheatLiabilityId', cashOrAccrual && this.isDormancyTrackingEnabled()); @@ -326,8 +331,8 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha rows: [ { key: 'savingsReferenceAccountId', - label: 'Savings Reference Account', - inputLabel: 'Savings reference account', + label: 'Fixed Deposit Reference Account', + inputLabel: 'Fixed deposit reference account', controlName: 'savingsReferenceAccountId', options: this.assetAccountOptions, required: true @@ -360,8 +365,8 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha rows: [ { key: 'savingsControlAccountId', - label: 'Savings Control Account', - inputLabel: 'Savings control account', + label: 'Fixed Deposit Control Account', + inputLabel: 'Fixed deposit control account', controlName: 'savingsControlAccountId', options: this.liabilityAccountOptions, required: true @@ -375,12 +380,16 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha required: true }, { + // Offered on cash accounting too, not only accrual: interest posting routes through + // this account whichever basis the product uses, so that interest declared but not yet + // credited to the member reads as the bank's own liability rather than appearing only + // when it is paid. Optional on cash - a product naming none posts straight to Savings + // Control as before. key: 'interestPayableAccountId', label: 'Interest Payable Account', inputLabel: 'Interest payable account', controlName: 'interestPayableAccountId', - options: this.liabilityAccountOptions, - accrualOnly: true + options: this.liabilityAccountOptions }, { key: 'escheatLiabilityId', @@ -402,8 +411,8 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha rows: [ { key: 'interestOnSavingsAccountId', - label: 'Interest On Savings Account', - inputLabel: 'Interest on savings account', + label: 'Interest On Fixed Deposits', + inputLabel: 'Interest on fixed deposits', controlName: 'interestOnSavingsAccountId', options: this.expenseAccountOptions, required: true diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component.html b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component.html index e92b052fd..8a6431b90 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component.html +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component.html @@ -481,6 +481,7 @@ } diff --git a/src/app/products/fixed-deposit-products/view-fixed-deposit-product/fixed-deposit-general-tab/fixed-deposit-general-tab.component.html b/src/app/products/fixed-deposit-products/view-fixed-deposit-product/fixed-deposit-general-tab/fixed-deposit-general-tab.component.html index cb3324ea9..458eece5b 100644 --- a/src/app/products/fixed-deposit-products/view-fixed-deposit-product/fixed-deposit-general-tab/fixed-deposit-general-tab.component.html +++ b/src/app/products/fixed-deposit-products/view-fixed-deposit-product/fixed-deposit-general-tab/fixed-deposit-general-tab.component.html @@ -607,6 +607,7 @@ } diff --git a/src/app/rbi-circulars/rbi-circulars.service.ts b/src/app/rbi-circulars/rbi-circulars.service.ts index 6558ba987..38fd0c260 100644 --- a/src/app/rbi-circulars/rbi-circulars.service.ts +++ b/src/app/rbi-circulars/rbi-circulars.service.ts @@ -121,6 +121,47 @@ export class RbiCircularsService { return this.http.get('/ossreturn/oss2', { params: this.ossParams(quarterEndDate, officeId) }); } + // ── Saved returns ── + + /** + * Returns saved earlier, newest first. A saved return is served from the snapshot taken when it + * was saved, never recomputed — a backdated entry or a changed mapping would otherwise produce a + * different statement from the one that was reviewed. + */ + getSavedReturns(formCode?: string): Observable { + let params = new HttpParams(); + if (formCode) { + params = params.set('formCode', formCode); + } + return this.http.get('/rbiformafiling', { params }); + } + + /** One saved return, with the stored snapshot, so the screen renders what was saved. */ + getSavedReturn(filingId: number): Observable { + return this.http.get(`/rbiformafiling/${filingId}`); + } + + /** Keeps the return currently on screen. Re-saving the same form and date replaces it. */ + saveReturn(formCode: string, reportingDate: string, report: any, remarks?: string): Observable { + return this.http.post('/rbiformafiling', { + formCode, + reportingDate, + report, + remarks, + locale: 'en', + dateFormat: 'yyyy-MM-dd' + }); + } + + /** Moves a saved return along its filing lifecycle; the snapshot itself is untouched. */ + updateSavedReturnStatus(filingId: number, status: string, remarks?: string): Observable { + return this.http.post(`/rbiformafiling/${filingId}/status`, { status, remarks }); + } + + deleteSavedReturn(filingId: number): Observable { + return this.http.delete(`/rbiformafiling/${filingId}`); + } + private ossParams(quarterEndDate?: string, officeId?: number): HttpParams { let params = new HttpParams(); if (quarterEndDate) { diff --git a/src/app/rbi-circulars/reserve-return/form1-crr.component.ts b/src/app/rbi-circulars/reserve-return/form1-crr.component.ts index 2db6c5c69..c5935ff16 100644 --- a/src/app/rbi-circulars/reserve-return/form1-crr.component.ts +++ b/src/app/rbi-circulars/reserve-return/form1-crr.component.ts @@ -30,6 +30,7 @@ import { ReserveReturnBase } from './reserve-return.base'; }) export class Form1CrrComponent extends ReserveReturnBase implements OnInit { protected readonly exportStem = 'RBI_Form1_CRR'; + protected readonly formCode = 'FORM_1'; protected fetch(reportingDate: string): Observable { return this.rbiService.getForm1(reportingDate); @@ -37,5 +38,6 @@ export class Form1CrrComponent extends ReserveReturnBase implements OnInit { ngOnInit(): void { this.load(); + this.loadSaved(); } } diff --git a/src/app/rbi-circulars/reserve-return/form9-slr.component.ts b/src/app/rbi-circulars/reserve-return/form9-slr.component.ts index 4dee07ffe..656b1fdc3 100644 --- a/src/app/rbi-circulars/reserve-return/form9-slr.component.ts +++ b/src/app/rbi-circulars/reserve-return/form9-slr.component.ts @@ -30,6 +30,7 @@ import { ReserveReturnBase } from './reserve-return.base'; }) export class Form9SlrComponent extends ReserveReturnBase implements OnInit { protected readonly exportStem = 'RBI_Form9_SLR'; + protected readonly formCode = 'FORM_9'; protected fetch(reportingDate: string): Observable { return this.rbiService.getForm9(reportingDate); @@ -37,5 +38,6 @@ export class Form9SlrComponent extends ReserveReturnBase implements OnInit { ngOnInit(): void { this.load(); + this.loadSaved(); } } diff --git a/src/app/rbi-circulars/reserve-return/reserve-return.base.ts b/src/app/rbi-circulars/reserve-return/reserve-return.base.ts index 65642760c..efa7f5962 100644 --- a/src/app/rbi-circulars/reserve-return/reserve-return.base.ts +++ b/src/app/rbi-circulars/reserve-return/reserve-return.base.ts @@ -64,7 +64,12 @@ export interface SectionRow { * and topped up on the last one fails on the daily test while passing on the closing position. */ export interface FortnightMaintenance { - days: { date: string; amount: number }[]; + /** Each day of the month, with the requirement it was measured against and the gap. */ + days: { date: string; required: number; amount: number; surplus: number }[]; + /** The month the table covers. */ + periodStart?: string; + periodEnd?: string; + periodLabel?: string; dayCount: number; average: number; lowest: number; @@ -146,6 +151,9 @@ export abstract class ReserveReturnBase { /** Filename stem for the Excel export. */ protected abstract readonly exportStem: string; + /** Which return this screen is, so saved returns list only this one's. */ + protected abstract readonly formCode: string; + data: ReserveReturn | null = null; loading = true; errorMsg = ''; @@ -158,6 +166,33 @@ export abstract class ReserveReturnBase { toastVisible = false; private toastTimer: any; + /** Returns saved earlier, listed under the live one. */ + savedReturns: any[] = []; + savedLoading = false; + saving = false; + savedRemarks = ''; + + /** + * Which half of the screen is showing: the return itself, or the ones kept on record. + * + * Two tabs rather than one long page: the live return and the saved list are read at different + * times and for different reasons, and stacking them meant scrolling past the whole statement - + * including a month of daily rows - to reach the list at all. + */ + activeTab: 'report' | 'saved' = 'report'; + + showTab(tab: 'report' | 'saved'): void { + this.activeTab = tab; + } + + /** The saved return currently open below the list, or null when none is. */ + viewingSaved: any = null; + /** + * The opened snapshot's own statement, rendered in its own section. Held separately from `data` so + * the live return stays on screen alongside it and the two can be compared directly. + */ + savedData: ReserveReturn | null = null; + load(): void { this.loading = true; this.errorMsg = ''; @@ -224,6 +259,167 @@ export abstract class ReserveReturnBase { return Math.abs(this.data?.surplusOrShortfall ?? 0); } + // ── Saved returns ── + + loadSaved(): void { + this.savedLoading = true; + this.rbiService + .getSavedReturns(this.formCode) + .pipe(catchError(() => of([]))) + .subscribe((rows: any[]) => { + this.savedLoading = false; + this.savedReturns = rows || []; + }); + } + + /** Keeps the return on screen, so it can be read again later exactly as it stands now. */ + saveCurrent(): void { + if (!this.data) { + return; + } + this.saving = true; + this.rbiService + .saveReturn(this.data.formCode, this.data.reportingDate, this.data, this.savedRemarks || undefined) + .pipe( + catchError((err: any) => { + this.saving = false; + this.toast(err?.error?.errors?.[0]?.defaultUserMessage ?? 'Could not save this return.'); + return of(null); + }) + ) + .subscribe((rows: any[] | null) => { + this.saving = false; + if (!rows) { + return; + } + this.savedReturns = rows; + this.savedRemarks = ''; + this.toast('Return saved.'); + }); + } + + /** + * Opens a saved return in its own section below the list, leaving the live return on screen. + * + * Deliberately not in place of the live one: the reason to open a saved return is almost always to + * compare it against where the bank stands now, and swapping the live figures out makes that + * comparison impossible without clicking back and forth from memory. + */ + viewSaved(row: any): void { + // Clicking the open one closes it, so the button toggles rather than re-fetching. + if (this.viewingSaved?.id === row.id) { + this.closeSaved(); + return; + } + this.rbiService + .getSavedReturn(row.id) + .pipe( + catchError(() => { + this.toast('Could not open this saved return.'); + return of(null); + }) + ) + .subscribe((saved: any) => { + if (!saved?.report) { + this.toast('This saved return has no stored statement.'); + return; + } + this.viewingSaved = saved; + this.savedData = saved.report as ReserveReturn; + // Opens on the Saved tab, where the list it was chosen from is - never jumping the user to + // the live return, which is a different statement. + this.activeTab = 'saved'; + setTimeout(() => document.getElementById('rr-saved-view')?.scrollIntoView({ behavior: 'smooth' }), 0); + }); + } + + /** Closes the opened snapshot. The live return was never hidden, so nothing is restored. */ + closeSaved(): void { + this.viewingSaved = null; + this.savedData = null; + } + + /** Kept for callers that still name the old action. */ + backToLive(): void { + this.closeSaved(); + } + + /** Magnitude of the opened snapshot's gap, so the sign lives in its label rather than the number. */ + get savedSurplus(): number { + return Math.abs(this.savedData?.surplusOrShortfall ?? 0); + } + + updateSavedStatus(row: any, status: string): void { + this.rbiService + .updateSavedReturnStatus(row.id, status) + .pipe( + catchError((err: any) => { + this.toast(err?.error?.errors?.[0]?.defaultUserMessage ?? 'Could not update the status.'); + return of(null); + }) + ) + .subscribe((rows: any[] | null) => { + if (!rows) { + return; + } + this.savedReturns = rows; + if (this.viewingSaved?.id === row.id) { + this.viewingSaved = { ...this.viewingSaved, status }; + } + this.toast(`Marked ${this.statusLabel(status)}.`); + }); + } + + deleteSaved(row: any): void { + this.rbiService + .deleteSavedReturn(row.id) + .pipe( + catchError(() => { + this.toast('Could not remove this saved return.'); + return of(null); + }) + ) + .subscribe((rows: any[] | null) => { + if (!rows) { + return; + } + this.savedReturns = rows; + if (this.viewingSaved?.id === row.id) { + this.backToLive(); + } + this.toast('Saved return removed.'); + }); + } + + /** The stages a return moves through, in order, for the status menu. */ + readonly filingStatuses = [ + 'DRAFT', + 'GENERATED', + 'CHECKED', + 'APPROVED', + 'PROVISIONAL_SUBMITTED', + 'FINAL_SUBMITTED', + 'REVISED' + ]; + + statusLabel(status: string): string { + const labels: Record = { + DRAFT: 'Draft', + GENERATED: 'Generated', + CHECKED: 'Checked', + APPROVED: 'Approved', + PROVISIONAL_SUBMITTED: 'Provisional submitted', + FINAL_SUBMITTED: 'Final submitted', + REVISED: 'Revised' + }; + return labels[status] ?? status; + } + + /** Hands the rendered return to the browser's print dialog, which also prints to PDF. */ + printReturn(): void { + window.print(); + } + /** * True when a head takes only part of an account, so the printed figure is not that account's * balance. Anything at or above 100, or unset, is the whole balance and needs no explanation. @@ -268,17 +464,25 @@ export abstract class ReserveReturnBase { try { const wb = newWorkbook(); const sheet = wb.addWorksheet(this.data.formCode.replace('_', ' ')); + // Five columns because the daily table is the widest block on the sheet; the statement above + // it uses the first three and leaves the rest empty. sheet.columns = [ { width: 52 }, { width: 22 }, - { width: 20 }]; + { width: 20 }, + { width: 22 }, + { width: 16 }]; addBanner(sheet, `${this.data.formName} — ${this.data.frequency}`, 'C'); let start = sheet.rowCount + 1; this.addKeyValue(sheet, 'Reporting date', this.data.reportingDate); if (this.data.fortnightLabel) { - this.addKeyValue(sheet, 'Fortnight', `${this.data.fortnightLabel} (${this.data.fortnightStart} to ${this.data.fortnightEnd})`); + this.addKeyValue( + sheet, + 'Fortnight', + `${this.data.fortnightLabel} (${this.data.fortnightStart} to ${this.data.fortnightEnd})` + ); } this.addKeyValue(sheet, 'DTL base as at', this.data.basis.dtlAsOnDate); this.addKeyValue(sheet, 'Balances maintained as at', this.data.basis.holdingsAsOnDate); @@ -388,16 +592,26 @@ export abstract class ReserveReturnBase { this.addKeyValue(sheet, 'Average maintained', this.amount(m.average)); this.addKeyValue(sheet, 'Required average', this.amount(m.requiredAverage)); this.addKeyValue(sheet, 'Average test', m.averageMet ? 'Met' : 'NOT MET'); - this.addKeyValue(sheet, `Daily floor (${m.dailyMinimumPercent}% of requirement)`, this.amount(m.dailyMinimumAmount)); + this.addKeyValue( + sheet, + `Daily floor (${m.dailyMinimumPercent}% of requirement)`, + this.amount(m.dailyMinimumAmount) + ); this.addKeyValue(sheet, 'Lowest day', `${this.amount(m.lowest)} on ${m.lowestOn}`); this.addKeyValue(sheet, 'Daily minimum test', m.dailyMinimumMet ? 'Met' : 'NOT MET'); - this.addKeyValue(sheet, 'Days counted', `${m.dayCount}${m.fortnightComplete ? ' (full fortnight)' : ' (to reporting date)'}`); + this.addKeyValue( + sheet, + 'Days counted', + `${m.dayCount}${m.fortnightComplete ? ' (full fortnight)' : ' (to reporting date)'}` + ); boxAllCells(sheet, start, sheet.rowCount, 2); sheet.addRow([]); const dayHead = sheet.addRow([ 'Date', - 'Maintained (INR)', + 'Required (INR)', + 'Actual (INR)', + 'Surplus / (Shortfall)', 'Against floor' ]); dayHead.font = { bold: true }; @@ -406,13 +620,21 @@ export abstract class ReserveReturnBase { for (const day of m.days) { const row = sheet.addRow([ day.date, + day.required, day.amount, + day.surplus, day.amount < m.dailyMinimumAmount ? 'Below floor' : 'OK' ]); - row.getCell(2).numFmt = '#,##0.00'; - row.getCell(2).alignment = { horizontal: 'right' }; + for (const col of [ + 2, + 3, + 4 + ]) { + row.getCell(col).numFmt = '#,##0.00'; + row.getCell(col).alignment = { horizontal: 'right' }; + } } - boxAllCells(sheet, dayStart, sheet.rowCount, 3); + boxAllCells(sheet, dayStart, sheet.rowCount, 5); } // The Memorandum and Annexures, each under its own heading. diff --git a/src/app/rbi-circulars/reserve-return/reserve-return.component.html b/src/app/rbi-circulars/reserve-return/reserve-return.component.html index 5dcc3af53..2f8dbc1a9 100644 --- a/src/app/rbi-circulars/reserve-return/reserve-return.component.html +++ b/src/app/rbi-circulars/reserve-return/reserve-return.component.html @@ -32,6 +32,14 @@ + + + + + + @if (activeTab === 'report') { + - @if (data.unmetRequirements.length) { -
- - {{ data.computable ? 'Check this before filing.' : 'This return cannot be computed yet.' }} - -
    - @for (reason of data.unmetRequirements; track reason) { -
  • {{ reason }}
  • - } -
- - Open RBI Reserve Configuration → - -
- } + @if (data.unmetRequirements.length) { +
+ + {{ data.computable ? 'Check this before filing.' : 'This return cannot be computed yet.' }} + +
    + @for (reason of data.unmetRequirements; track reason) { +
  • {{ reason }}
  • + } +
+ + Open RBI Reserve Configuration → + +
+ } - -
-
-
- DTL base as at - {{ data.basis.dtlAsOnDate }} -
-
-
- Balances maintained as at - {{ data.basis.holdingsAsOnDate }} -
- @if (data.basis.rawLagDate) { -
- {{ data.basis.lagDays }}-day lag falls on - {{ data.basis.rawLagDate }} + +
+
+
+ DTL base as at + {{ data.basis.dtlAsOnDate }}
- } +
+
+ Balances maintained as at + {{ data.basis.holdingsAsOnDate }} +
+ @if (data.basis.rawLagDate) { +
+ {{ data.basis.lagDays }}-day lag falls on + {{ data.basis.rawLagDate }} +
+ } +
+

{{ data.basis.note }}

-

{{ data.basis.note }}

-
- - @if (data.computable) { -
- + @if (data.computable) { +
+ -
- NDTL base - {{ amount(data.ndtlAmount) }} - - DTL {{ amount(data.dtlAmount) }} less {{ amount(data.interBankAssets) }} · as at - {{ data.basis.dtlAsOnDate }} - -
-
- {{ data.rateType }} rate - {{ data.ratePercentage }}% - @if (data.rateIsSeedDefault) { - Placeholder — not confirmed - } @else { - in force on {{ data.reportingDate }} - } -
-
- Required - {{ amount(data.requiredAmount) }} - {{ data.ratePercentage }}% of NDTL -
-
- Maintained - {{ amount(data.maintainedAmount) }} - {{ percent(data.maintainedPercentage) }} of NDTL -
-
- {{ isShortfall ? 'Shortfall' : 'Surplus' }} - {{ amount(shortfallAmount) }} - {{ isShortfall ? 'Below requirement' : 'Above requirement' }} -
-
- } - - -
- - - - - - - - - - - @for (line of data.lines; track $index) { - - - - - - - @for (account of line.accounts; track account.glAccountId) { - - - - - - } - } - - @if (data.computable) { - - - - - - - - - - - - - - - - } - -
ParticularsAs atAmount (INR)
- {{ line.label }} - @if (!line.configured) { - Not configured - } - {{ line.asOnDate }} - @if (line.configured) { - {{ amount(line.amount) }} - } @else { - — - } -
- {{ account.glCode }} - {{ account.name }} - - @if (account.excluded) { - excluded — {{ account.excluded }} - } - - @if (isPartial(account)) { - - {{ account.weightPercentage }}% of {{ amount(account.accountBalance) }} - - } - - @if (account.liabilityClass) { - {{ account.liabilityClass | titlecase }} - } - {{ amount(account.balance) }}
{{ data.rateType }} required ({{ data.ratePercentage }}% of NDTL){{ amount(data.requiredAmount) }}
{{ data.rateType }} maintained{{ amount(data.maintainedAmount) }}
{{ isShortfall ? 'Shortfall' : 'Surplus' }}{{ amount(shortfallAmount) }}
-
- - - @if (data.maintenance; as m) { -
-

- Maintenance over the fortnight - @if (!m.fortnightComplete) { - In progress — {{ m.dayCount }} days so far - } -

-

- CRR is reckoned over the whole fortnight, not on one day: the full requirement must be held - on average, and at least {{ m.dailyMinimumPercent }}% of it on every single day. -

- -
-
- Average maintained - {{ amount(m.average) }} +
+ NDTL base + {{ amount(data.ndtlAmount) }} - {{ m.averageMet ? 'Meets' : 'Below' }} required {{ amount(m.requiredAverage) }} + DTL {{ amount(data.dtlAmount) }} less {{ amount(data.interBankAssets) }} · as at + {{ data.basis.dtlAsOnDate }}
-
- Lowest day - {{ amount(m.lowest) }} - - on {{ m.lowestOn }} · floor {{ amount(m.dailyMinimumAmount) }} - +
+ {{ data.rateType }} rate + {{ data.ratePercentage }}% + @if (data.rateIsSeedDefault) { + Placeholder — not confirmed + } @else { + in force on {{ data.reportingDate }} + }
- Days counted - {{ m.dayCount }} - {{ m.fortnightComplete ? 'Full fortnight' : 'To reporting date' }} + Required + {{ amount(data.requiredAmount) }} + {{ data.ratePercentage }}% of NDTL +
+
+ Maintained + {{ amount(data.maintainedAmount) }} + {{ percent(data.maintainedPercentage) }} of NDTL +
+
+ {{ isShortfall ? 'Shortfall' : 'Surplus' }} + {{ amount(shortfallAmount) }} + {{ isShortfall ? 'Below requirement' : 'Above requirement' }}
+ } + +
- - - + + + - @for (day of m.days; track day.date) { - - - - + + + + + + @for (account of line.accounts; track account.glAccountId) { + + + + + + } + } + + @if (data.computable) { + + + + + + + + + + + + + + }
DateMaintained (INR)Against floorParticularsAs atAmount (INR)
{{ day.date }}{{ amount(day.amount) }} - @if (day.amount < m.dailyMinimumAmount) { - Below floor - } @else { - OK + + @for (line of data.lines; track $index) { +
+ {{ line.label }} + @if (!line.configured) { + Not configured } {{ line.asOnDate }} + @if (line.configured) { + {{ amount(line.amount) }} + } @else { + — + } +
+ {{ account.glCode }} + {{ account.name }} + + @if (account.excluded) { + excluded — {{ account.excluded }} + } + + @if (isPartial(account)) { + + {{ account.weightPercentage }}% of {{ amount(account.accountBalance) }} + + } + + @if (account.liabilityClass) { + {{ account.liabilityClass | titlecase }} + } + {{ amount(account.balance) }}
{{ data.rateType }} required ({{ data.ratePercentage }}% of NDTL){{ amount(data.requiredAmount) }}
{{ data.rateType }} maintained{{ amount(data.maintainedAmount) }}
{{ isShortfall ? 'Shortfall' : 'Surplus' }}{{ amount(shortfallAmount) }}
- } - - @for (section of namedSections; track section.title) { - @if (section.rows.length) { + + @if (data.maintenance; as m) {
-

{{ section.title }}

-

{{ section.hint }}

+

+ Daily maintenance — {{ m.periodLabel || 'this month' }} + @if (!m.fortnightComplete) { + In progress — {{ m.dayCount }} days so far + } +

+

+ Every day of the month, against the requirement in force. CRR is reckoned over the period rather than on one + day: the full requirement must be held on average, and at least + {{ m.dailyMinimumPercent }}% of it on every single day. +

+ +
+
+ Average maintained + {{ amount(m.average) }} + + {{ m.averageMet ? 'Meets' : 'Below' }} required {{ amount(m.requiredAverage) }} + +
+
+ Lowest day + {{ amount(m.lowest) }} + on {{ m.lowestOn }} · floor {{ amount(m.dailyMinimumAmount) }} +
+
+ Days counted + {{ m.dayCount }} + {{ m.fortnightComplete ? 'Full fortnight' : 'To reporting date' }} +
+
+ - - + + + + + - @for (row of section.rows; track row.configKey) { - - + @for (day of m.days; track day.date) { + + + + + @@ -311,6 +305,250 @@
ParticularsAmount (INR)DateRequired (INR)Actual (INR)Surplus / (Shortfall)Against floor
- {{ row.label }} - @if (!row.configured) { - Not configured - } -
{{ day.date }}{{ amount(day.required) }}{{ amount(day.amount) }}{{ amount(day.surplus) }} - @if (row.configured) { - {{ amount(row.amount) }} + @if (day.amount < m.dailyMinimumAmount) { + Below floor } @else { - — + OK }
} + + + @for (section of namedSections; track section.title) { + @if (section.rows.length) { +
+

{{ section.title }}

+

{{ section.hint }}

+ + + + + + + + + @for (row of section.rows; track row.configKey) { + + + + + } + +
ParticularsAmount (INR)
+ {{ row.label }} + @if (!row.configured) { + Not configured + } + + @if (row.configured) { + {{ amount(row.amount) }} + } @else { + — + } +
+
+ } + } + } + } + + + @if (activeTab === 'saved') { +
+

+ Saved returns + @if (savedReturns.length) { + {{ savedReturns.length }} + } +

+

+ Returns kept for the record. Each opens exactly as it was saved — the stored statement, not a re-run of today's + ledger, so a figure already reported cannot change underneath it. +

+ + @if (data?.computable) { +
+ + Note (optional) + + + +
+ } + + @if (savedLoading) { +
Loading saved returns…
+ } @else if (!savedReturns.length) { +
Nothing saved yet. Generate a return and press Save to keep it.
+ } @else { + + + + + + + + + + + + + + + @for (row of savedReturns; track row.id) { + + + + + + + + + + + } + +
Reporting datePeriodNDTLRequiredMaintainedStatusSaved
{{ row.reportingDate }}{{ row.periodStart }} → {{ row.periodEnd }}{{ amount(row.ndtlAmount) }}{{ amount(row.requiredAmount) }}{{ amount(row.maintainedAmount) }} + + + @for (s of filingStatuses; track s) { + {{ statusLabel(s) }} + } + + + {{ row.createdOn }}
{{ row.createdBy }}
+ + +
+ } +
+ + + @if (viewingSaved && savedData; as _) { +
+
+
+

+ Saved return — {{ savedData.reportingDate }} + {{ statusLabel(viewingSaved.status) }} +

+

+ Saved {{ viewingSaved.createdOn }} by {{ viewingSaved.createdBy || 'unknown' }}. These are the figures as + they stood when it was saved — not today's ledger, which may have moved since. + @if (viewingSaved.remarks) { + “{{ viewingSaved.remarks }}” + } +

+
+ +
+ +
+
+ NDTL base + {{ amount(savedData.ndtlAmount ?? savedData.dtlAmount) }} + as at {{ savedData.basis.dtlAsOnDate }} +
+
+ {{ savedData.rateType }} rate + {{ savedData.ratePercentage }}% + as filed +
+
+ Required + {{ amount(savedData.requiredAmount) }} + {{ savedData.ratePercentage }}% of NDTL +
+
+ Maintained + {{ amount(savedData.maintainedAmount) }} + on {{ savedData.basis.holdingsAsOnDate }} +
+
+ {{ savedData.compliant ? 'Surplus' : 'Shortfall' }} + {{ amount(savedSurplus) }} + {{ savedData.compliant ? 'Above requirement' : 'Below requirement' }} +
+
+ + + + + + + + + + + @for (line of savedData.lines; track line.configKey) { + + + + + + @for (account of line.accounts; track account.glAccountId) { + + + + + + } + } + +
ParticularsAs atAmount (INR)
+ {{ line.label }} + {{ line.asOnDate }} + @if (line.configured) { + {{ amount(line.amount) }} + } @else { + — + } +
+ {{ account.glCode }} + {{ account.name }} + @if (isPartial(account)) { + + {{ account.weightPercentage }}% of {{ amount(account.accountBalance) }} + + } + @if (account.liabilityClass) { + {{ account.liabilityClass | titlecase }} + } + {{ amount(account.balance) }}
+ + @if (savedData.maintenance; as sm) { +

Daily maintenance as filed — {{ sm.periodLabel || '' }}

+ + + + + + + + + + + @for (day of sm.days; track day.date) { + + + + + + + } + +
DateRequiredActualSurplus / (Shortfall)
{{ day.date }}{{ amount(day.required) }}{{ amount(day.amount) }}{{ amount(day.surplus) }}
+ } +
} } diff --git a/src/app/rbi-circulars/reserve-return/reserve-return.component.scss b/src/app/rbi-circulars/reserve-return/reserve-return.component.scss index 6577a52db..cdc132990 100644 --- a/src/app/rbi-circulars/reserve-return/reserve-return.component.scss +++ b/src/app/rbi-circulars/reserve-return/reserve-return.component.scss @@ -129,6 +129,52 @@ $red: #c5221f; width: 180px; } +// ── Tabs ── + +.rr-tabs { + display: flex; + gap: 4px; + border-bottom: 1px solid $border; + margin-bottom: 16px; +} + +.rr-tab { + display: inline-flex; + align-items: center; + gap: 8px; + background: none; + border: none; + border-bottom: 2px solid transparent; + padding: 10px 16px; + font-size: 13px; + font-weight: 600; + color: $gray-500; + cursor: pointer; + font-family: inherit; + + i { + font-size: 12px; + } + + &:hover { + color: $gray-900; + } + + &--on { + color: $primary; + border-bottom-color: $primary; + } + + &__count { + font-size: 10.5px; + font-weight: 700; + background: #e8f0fe; + color: $primary; + border-radius: 20px; + padding: 1px 8px; + } +} + // ── Basis ── .rr-basis { @@ -297,7 +343,7 @@ $red: #c5221f; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; - padding: 10px 10px; + padding: 10px; border-bottom: 1px solid $border; white-space: nowrap; } @@ -446,6 +492,154 @@ $red: #c5221f; font-weight: 600; } +// A shortfall on a day reads as one, rather than as a number with a minus sign in front. +.rr-neg { + color: $red; + font-weight: 600; +} + +// ── Saved returns ── + +.rr-empty { + padding: 22px; + text-align: center; + color: $gray-500; + font-size: 12.5px; +} + +.rr-save-row { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 16px; + + &__note { + flex: 1 1 340px; + } +} + +.rr-saved { + &__period, + &__meta { + font-size: 11px; + color: $gray-500; + white-space: nowrap; + } + + &__status { + width: 170px; + } + + &__actions { + white-space: nowrap; + } + + &__view { + font-size: 12px !important; + min-width: 0 !important; + color: $primary !important; + } + + &__del { + min-width: 0 !important; + padding: 0 8px !important; + color: $gray-500 !important; + + &:hover { + color: $red !important; + } + } + + &__remarks { + display: block; + margin-top: 4px; + color: $gray-500; + } + + &--open td { + background: #e8f0fe; + } +} + +// The opened snapshot, in its own section. Tinted and edged so it reads as a different document +// from the live return above it rather than as more of the same page. +.rr-card--saved { + border-color: #c6dafc; + background: #fbfdff; + border-left: 4px solid $primary; +} + +.rr-saved-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} + +.rr-saved__subhead { + margin: 20px 0 8px; + font-size: 13px; + font-weight: 600; + color: $gray-900; +} + +// ── Print ── +// The browser's print dialog is also its save-as-PDF, so one stylesheet serves both. Controls, +// navigation and the saved-returns list are dropped: a printed return is a statement, and a page +// carrying dead buttons reads as a screenshot of software rather than as a filing. +@media print { + .rr-noprint, + .rr-header__actions, + .rr-unmet__link { + display: none !important; + } + + .rr-container { + max-width: none; + padding: 0; + } + + .rr-card, + .rr-basis, + .rr-stat { + border-color: #999 !important; + box-shadow: none !important; + break-inside: avoid; + } + + .rr-card { + overflow: visible !important; + } + + // Header rows repeat on every printed page, so a table spilling over a page break stays readable. + .rr-table thead { + display: table-header-group; + } + + .rr-table tr { + break-inside: avoid; + } + + .rr-toast { + display: none !important; + } + + // Backgrounds carry meaning here — a day below the floor is shaded, not merely labelled — so they + // must survive the print engine's default of dropping them to save ink. + .rr-container, + .rr-card, + .rr-basis, + .rr-stat, + .rr-table, + .rr-table th, + .rr-table td { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } +} + .rr-badge { display: inline-block; margin-left: 8px; @@ -479,7 +673,7 @@ $red: #c5221f; z-index: 1000; } -@media (max-width: 720px) { +@media (width <= 720px) { .rr-header__actions { width: 100%; } diff --git a/src/app/shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component.html b/src/app/shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component.html index e121e3be3..7084e2a16 100644 --- a/src/app/shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component.html +++ b/src/app/shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component.html @@ -21,7 +21,7 @@
@@ -72,7 +72,7 @@
@@ -81,7 +81,7 @@
@@ -143,7 +143,7 @@
diff --git a/src/app/shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component.ts b/src/app/shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component.ts index 769771faf..0b3948c00 100644 --- a/src/app/shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component.ts +++ b/src/app/shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component.ts @@ -26,6 +26,8 @@ export class ViewSavingsAccountingDetailsComponent { @Input() accountingRule: OptionData; @Input() accountingMappings: any[] = []; + /** Product name for GL row titles (e.g. 'Fixed deposit'); omitted = savings wording. */ + @Input() productLabel?: string; isCashOrAccrualAccounting(): boolean { if (this.accountingRule) { diff --git a/src/assets/translations/en-US.json b/src/assets/translations/en-US.json index 9160611c2..2ce8822a5 100644 --- a/src/assets/translations/en-US.json +++ b/src/assets/translations/en-US.json @@ -2236,6 +2236,9 @@ "Interest free period": "Interest free period", "Interest method": "Interest method", "Interest on savings": "Interest on savings", + "Fixed deposit control": "Fixed deposit control", + "Fixed deposit reference": "Fixed deposit reference", + "Interest on fixed deposits": "Interest on fixed deposits", "Interest rate": "Interest Rate", "Interest recalculation compounding on": "Interest recalculation compounding on", "Interest waived on": "Interest waived on", @@ -2793,6 +2796,7 @@ "Savings Product": "Savings Product", "Savings SubType": "Savings SubType", "Savings transfers in suspense": "Savings transfers in suspense", + "Transfers in suspense": "Transfers in suspense", "Schedule Date": "Schedule Date", "Share Details": "Share Details", "Scheduler Jobs": "Scheduler Jobs", diff --git a/src/environments/.env.ts b/src/environments/.env.ts index 44f95bdbb..6a375a6e4 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -9,8 +9,8 @@ /* tslint:disable */ export default { 'mifos_x': { - 'version': '260924', - 'hash': 'a6e62201' + 'version': '260928', + 'hash': '6cbb31b5' }, 'allow_switching_backend_instance': true };