feat: update RBI reserve return form 1 & 9 UI and FD accounting mapping
This commit is contained in:
+15
-7
@@ -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 };
|
||||
|
||||
@@ -27,101 +27,104 @@
|
||||
} @else {
|
||||
<!-- ═══ Rates — only the reserve returns are charged at a percentage ═══ -->
|
||||
@if (showRates) {
|
||||
<div class="rrc-card">
|
||||
<h3 class="rrc-card__title">Reserve rates</h3>
|
||||
<p class="rrc-card__hint">
|
||||
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.
|
||||
</p>
|
||||
<div class="rrc-card">
|
||||
<h3 class="rrc-card__title">Reserve rates</h3>
|
||||
<p class="rrc-card__hint">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div class="rrc-rate-now">
|
||||
@for (type of ['CRR', 'SLR', 'SAVINGS_DEMAND']; track type) {
|
||||
<div class="rrc-rate-now__item">
|
||||
<span class="rrc-rate-now__label">
|
||||
{{ type === 'SAVINGS_DEMAND' ? 'Savings demand share' : type }} in force today
|
||||
</span>
|
||||
@if (currentRate(type); as rate) {
|
||||
<span class="rrc-rate-now__value">{{ rate.percentage }}%</span>
|
||||
@if (rate.seedDefault) {
|
||||
<span class="rrc-rate-now__since rrc-rate-now__since--warn">
|
||||
Placeholder — confirm the rate actually in force
|
||||
</span>
|
||||
} @else {
|
||||
<span class="rrc-rate-now__since">since {{ rate.effectiveFrom }}</span>
|
||||
}
|
||||
} @else {
|
||||
<span class="rrc-rate-now__value rrc-rate-now__value--none">Not set</span>
|
||||
<span class="rrc-rate-now__since">{{ type === 'CRR' ? 'Form 1' : 'Form 9' }} cannot compute</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="rrc-rate-form">
|
||||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__type">
|
||||
<mat-label>Rate</mat-label>
|
||||
<mat-select [(ngModel)]="newRate.rateType">
|
||||
<mat-option value="CRR">CRR — Form 1</mat-option>
|
||||
<mat-option value="SLR">SLR — Form 9</mat-option>
|
||||
<!-- Not a reserve ratio, but set by RBI and effective-dated the same way: the share of
|
||||
savings balances that counts as a demand liability, the rest being time. -->
|
||||
<mat-option value="SAVINGS_DEMAND">Savings — demand share</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__pct">
|
||||
<mat-label>Percentage (%)</mat-label>
|
||||
<input matInput type="number" step="0.0001" min="0" max="100" [(ngModel)]="newRate.percentage" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__date">
|
||||
<mat-label>Effective from</mat-label>
|
||||
<input matInput type="date" [(ngModel)]="newRate.effectiveFrom" />
|
||||
</mat-form-field>
|
||||
|
||||
<button mat-raised-button color="primary" [disabled]="savingRate" (click)="saveRate()">
|
||||
{{ savingRate ? 'Saving…' : 'Save rate' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table class="rrc-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rate</th>
|
||||
<th>Percentage</th>
|
||||
<th>Effective from</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (rate of rates; track rate.id) {
|
||||
<tr>
|
||||
<td><span class="rrc-key">{{ rate.rateType }}</span></td>
|
||||
<td class="rrc-num">{{ rate.percentage }}%</td>
|
||||
<td>{{ rate.effectiveFrom }}</td>
|
||||
<td>
|
||||
@if (isFuture(rate)) {
|
||||
<span class="rrc-badge rrc-badge--future">Future-dated</span>
|
||||
} @else if (currentRate(rate.rateType)?.id === rate.id) {
|
||||
<span class="rrc-badge rrc-badge--active">In force</span>
|
||||
} @else {
|
||||
<span class="rrc-badge rrc-badge--past">Superseded</span>
|
||||
}
|
||||
<div class="rrc-rate-now">
|
||||
@for (type of ['CRR', 'SLR', 'SAVINGS_DEMAND']; track type) {
|
||||
<div class="rrc-rate-now__item">
|
||||
<span class="rrc-rate-now__label">
|
||||
{{ type === 'SAVINGS_DEMAND' ? 'Savings demand share' : type }} in force today
|
||||
</span>
|
||||
@if (currentRate(type); as rate) {
|
||||
<span class="rrc-rate-now__value">{{ rate.percentage }}%</span>
|
||||
@if (rate.seedDefault) {
|
||||
<span class="rrc-badge rrc-badge--seed">Placeholder</span>
|
||||
<span class="rrc-rate-now__since rrc-rate-now__since--warn">
|
||||
Placeholder — confirm the rate actually in force
|
||||
</span>
|
||||
} @else {
|
||||
<span class="rrc-rate-now__since">since {{ rate.effectiveFrom }}</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<button mat-button class="rrc-remove" (click)="deleteRate(rate)" title="Remove this rate">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
} @else {
|
||||
<span class="rrc-rate-now__value rrc-rate-now__value--none">Not set</span>
|
||||
<span class="rrc-rate-now__since">{{ type === 'CRR' ? 'Form 1' : 'Form 9' }} cannot compute</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rrc-rate-form">
|
||||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__type">
|
||||
<mat-label>Rate</mat-label>
|
||||
<mat-select [(ngModel)]="newRate.rateType">
|
||||
<mat-option value="CRR">CRR — Form 1</mat-option>
|
||||
<mat-option value="SLR">SLR — Form 9</mat-option>
|
||||
<!-- Not a reserve ratio, but set by RBI and effective-dated the same way: the share of
|
||||
savings balances that counts as a demand liability, the rest being time. -->
|
||||
<mat-option value="SAVINGS_DEMAND">Savings — demand share</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__pct">
|
||||
<mat-label>Percentage (%)</mat-label>
|
||||
<input matInput type="number" step="0.0001" min="0" max="100" [(ngModel)]="newRate.percentage" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__date">
|
||||
<mat-label>Effective from</mat-label>
|
||||
<input matInput type="date" [(ngModel)]="newRate.effectiveFrom" />
|
||||
</mat-form-field>
|
||||
|
||||
<button mat-raised-button color="primary" [disabled]="savingRate" (click)="saveRate()">
|
||||
{{ savingRate ? 'Saving…' : 'Save rate' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table class="rrc-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rate</th>
|
||||
<th>Percentage</th>
|
||||
<th>Effective from</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (rate of rates; track rate.id) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="rrc-key">{{ rate.rateType }}</span>
|
||||
</td>
|
||||
<td class="rrc-num">{{ rate.percentage }}%</td>
|
||||
<td>{{ rate.effectiveFrom }}</td>
|
||||
<td>
|
||||
@if (isFuture(rate)) {
|
||||
<span class="rrc-badge rrc-badge--future">Future-dated</span>
|
||||
} @else if (currentRate(rate.rateType)?.id === rate.id) {
|
||||
<span class="rrc-badge rrc-badge--active">In force</span>
|
||||
} @else {
|
||||
<span class="rrc-badge rrc-badge--past">Superseded</span>
|
||||
}
|
||||
@if (rate.seedDefault) {
|
||||
<span class="rrc-badge rrc-badge--seed">Placeholder</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<button mat-button class="rrc-remove" (click)="deleteRate(rate)" title="Remove this rate">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ═══ GL accounts ═══ -->
|
||||
@@ -324,6 +327,17 @@
|
||||
<input matInput [(ngModel)]="newRole.description" placeholder="What this head covers" />
|
||||
</mat-form-field>
|
||||
|
||||
<!--
|
||||
The section prefixes are a naming convention rather than a field, so they have to be
|
||||
stated somewhere: an administrator who wants a head on the Memorandum has no way to
|
||||
discover that naming it so is what puts it there.
|
||||
-->
|
||||
<p class="rrc-card__hint rrc-add-role__note">
|
||||
Naming a head <strong>Memorandum - …</strong>, <strong>Annexure A - …</strong> or
|
||||
<strong>Annexure B - …</strong> prints it under that section of the return instead of in the main
|
||||
statement.
|
||||
</p>
|
||||
|
||||
<button mat-raised-button color="primary" [disabled]="addingRole" (click)="addRole()">
|
||||
{{ addingRole ? 'Adding…' : 'Add head' }}
|
||||
</button>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ?? ''}`));
|
||||
}
|
||||
|
||||
+17
-8
@@ -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
|
||||
|
||||
+1
@@ -481,6 +481,7 @@
|
||||
<mifosx-view-savings-accounting-details
|
||||
[accountingRule]="accountingRule"
|
||||
[accountingMappings]="accountingMappings"
|
||||
productLabel="Fixed deposit"
|
||||
></mifosx-view-savings-accounting-details>
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -607,6 +607,7 @@
|
||||
<mifosx-view-savings-accounting-details
|
||||
[accountingRule]="fixedDepositProductData.accountingRule"
|
||||
[accountingMappings]="fixedDepositProductData.accountingMappings"
|
||||
productLabel="Fixed deposit"
|
||||
></mifosx-view-savings-accounting-details>
|
||||
}
|
||||
</section>
|
||||
|
||||
@@ -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<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
return this.http.post(`/rbiformafiling/${filingId}/status`, { status, remarks });
|
||||
}
|
||||
|
||||
deleteSavedReturn(filingId: number): Observable<any> {
|
||||
return this.http.delete(`/rbiformafiling/${filingId}`);
|
||||
}
|
||||
|
||||
private ossParams(quarterEndDate?: string, officeId?: number): HttpParams {
|
||||
let params = new HttpParams();
|
||||
if (quarterEndDate) {
|
||||
|
||||
@@ -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<any> {
|
||||
return this.rbiService.getForm1(reportingDate);
|
||||
@@ -37,5 +38,6 @@ export class Form1CrrComponent extends ReserveReturnBase implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.load();
|
||||
this.loadSaved();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<any> {
|
||||
return this.rbiService.getForm9(reportingDate);
|
||||
@@ -37,5 +38,6 @@ export class Form9SlrComponent extends ReserveReturnBase implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.load();
|
||||
this.loadSaved();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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.
|
||||
|
||||
@@ -32,6 +32,14 @@
|
||||
<input matInput type="date" [(ngModel)]="reportingDate" (change)="onDateChange()" />
|
||||
</mat-form-field>
|
||||
<button mat-stroked-button (click)="resetToToday()">Today</button>
|
||||
<button mat-stroked-button (click)="printReturn()">
|
||||
<i class="fas fa-print"></i>
|
||||
Print
|
||||
</button>
|
||||
<button mat-stroked-button [disabled]="saving || !data.computable" (click)="saveCurrent()">
|
||||
<i class="fas fa-bookmark"></i>
|
||||
{{ saving ? 'Saving…' : 'Save' }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" [disabled]="generating" (click)="exportWorkbook()">
|
||||
<i class="fas fa-file-excel"></i>
|
||||
{{ generating ? 'Generating…' : 'Export' }}
|
||||
@@ -40,269 +48,255 @@
|
||||
</div>
|
||||
|
||||
<!--
|
||||
═══ Tabs ═══
|
||||
The return and the saved list are read at different times and for different reasons, so they
|
||||
are two halves of the screen rather than one page where reaching the list means scrolling past
|
||||
a whole statement and a month of daily rows.
|
||||
-->
|
||||
<div class="rr-tabs rr-noprint">
|
||||
<button class="rr-tab" [class.rr-tab--on]="activeTab === 'report'" (click)="showTab('report')">
|
||||
<i class="fas fa-file-invoice"></i> Report
|
||||
</button>
|
||||
<button class="rr-tab" [class.rr-tab--on]="activeTab === 'saved'" (click)="showTab('saved')">
|
||||
<i class="fas fa-bookmark"></i> Saved returns
|
||||
@if (savedReturns.length) {
|
||||
<span class="rr-tab__count">{{ savedReturns.length }}</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (activeTab === 'report') {
|
||||
<!--
|
||||
Caveats show whenever there are any — not only when the return cannot compute. A return
|
||||
computed on the placeholder rate is arithmetically right and still must not be filed as-is,
|
||||
so hiding that behind the not-computable branch would bury the one thing worth saying.
|
||||
-->
|
||||
@if (data.unmetRequirements.length) {
|
||||
<div class="rr-alert rr-alert--warn">
|
||||
<strong>
|
||||
{{ data.computable ? 'Check this before filing.' : 'This return cannot be computed yet.' }}
|
||||
</strong>
|
||||
<ul class="rr-unmet">
|
||||
@for (reason of data.unmetRequirements; track reason) {
|
||||
<li>{{ reason }}</li>
|
||||
}
|
||||
</ul>
|
||||
<a class="rr-unmet__link" [routerLink]="['/organization/rbi-reserve-config']">
|
||||
Open RBI Reserve Configuration →
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
@if (data.unmetRequirements.length) {
|
||||
<div class="rr-alert rr-alert--warn">
|
||||
<strong>
|
||||
{{ data.computable ? 'Check this before filing.' : 'This return cannot be computed yet.' }}
|
||||
</strong>
|
||||
<ul class="rr-unmet">
|
||||
@for (reason of data.unmetRequirements; track reason) {
|
||||
<li>{{ reason }}</li>
|
||||
}
|
||||
</ul>
|
||||
<a class="rr-unmet__link" [routerLink]="['/organization/rbi-reserve-config']">
|
||||
Open RBI Reserve Configuration →
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ═══ Basis ═══ -->
|
||||
<div class="rr-basis">
|
||||
<div class="rr-basis__row">
|
||||
<div class="rr-basis__item">
|
||||
<span class="rr-basis__label">DTL base as at</span>
|
||||
<span class="rr-basis__value">{{ data.basis.dtlAsOnDate }}</span>
|
||||
</div>
|
||||
<div class="rr-basis__arrow"><i class="fas fa-arrow-right"></i></div>
|
||||
<div class="rr-basis__item">
|
||||
<span class="rr-basis__label">Balances maintained as at</span>
|
||||
<span class="rr-basis__value">{{ data.basis.holdingsAsOnDate }}</span>
|
||||
</div>
|
||||
@if (data.basis.rawLagDate) {
|
||||
<div class="rr-basis__item rr-basis__item--muted">
|
||||
<span class="rr-basis__label">{{ data.basis.lagDays }}-day lag falls on</span>
|
||||
<span class="rr-basis__value">{{ data.basis.rawLagDate }}</span>
|
||||
<!-- ═══ Basis ═══ -->
|
||||
<div class="rr-basis">
|
||||
<div class="rr-basis__row">
|
||||
<div class="rr-basis__item">
|
||||
<span class="rr-basis__label">DTL base as at</span>
|
||||
<span class="rr-basis__value">{{ data.basis.dtlAsOnDate }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="rr-basis__arrow"><i class="fas fa-arrow-right"></i></div>
|
||||
<div class="rr-basis__item">
|
||||
<span class="rr-basis__label">Balances maintained as at</span>
|
||||
<span class="rr-basis__value">{{ data.basis.holdingsAsOnDate }}</span>
|
||||
</div>
|
||||
@if (data.basis.rawLagDate) {
|
||||
<div class="rr-basis__item rr-basis__item--muted">
|
||||
<span class="rr-basis__label">{{ data.basis.lagDays }}-day lag falls on</span>
|
||||
<span class="rr-basis__value">{{ data.basis.rawLagDate }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<p class="rr-basis__note">{{ data.basis.note }}</p>
|
||||
</div>
|
||||
<p class="rr-basis__note">{{ data.basis.note }}</p>
|
||||
</div>
|
||||
|
||||
<!-- ═══ Summary ═══ -->
|
||||
@if (data.computable) {
|
||||
<div class="rr-summary">
|
||||
<!-- NDTL, not DTL: the rate is charged on the net figure, so the base shown must be the one
|
||||
<!-- ═══ Summary ═══ -->
|
||||
@if (data.computable) {
|
||||
<div class="rr-summary">
|
||||
<!-- NDTL, not DTL: the rate is charged on the net figure, so the base shown must be the one
|
||||
the required amount was actually computed from. DTL and what was netted off it are
|
||||
given underneath, so the subtraction is visible rather than implied. -->
|
||||
<div class="rr-stat">
|
||||
<span class="rr-stat__label">NDTL base</span>
|
||||
<span class="rr-stat__value">{{ amount(data.ndtlAmount) }}</span>
|
||||
<span class="rr-stat__sub">
|
||||
DTL {{ amount(data.dtlAmount) }} less {{ amount(data.interBankAssets) }} · as at
|
||||
{{ data.basis.dtlAsOnDate }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="rr-stat" [class.rr-stat--provisional]="data.rateIsSeedDefault">
|
||||
<span class="rr-stat__label">{{ data.rateType }} rate</span>
|
||||
<span class="rr-stat__value">{{ data.ratePercentage }}%</span>
|
||||
@if (data.rateIsSeedDefault) {
|
||||
<span class="rr-stat__sub rr-stat__sub--warn">Placeholder — not confirmed</span>
|
||||
} @else {
|
||||
<span class="rr-stat__sub">in force on {{ data.reportingDate }}</span>
|
||||
}
|
||||
</div>
|
||||
<div class="rr-stat">
|
||||
<span class="rr-stat__label">Required</span>
|
||||
<span class="rr-stat__value">{{ amount(data.requiredAmount) }}</span>
|
||||
<span class="rr-stat__sub">{{ data.ratePercentage }}% of NDTL</span>
|
||||
</div>
|
||||
<div class="rr-stat">
|
||||
<span class="rr-stat__label">Maintained</span>
|
||||
<span class="rr-stat__value">{{ amount(data.maintainedAmount) }}</span>
|
||||
<span class="rr-stat__sub">{{ percent(data.maintainedPercentage) }} of NDTL</span>
|
||||
</div>
|
||||
<div class="rr-stat" [class.rr-stat--bad]="isShortfall" [class.rr-stat--good]="!isShortfall">
|
||||
<span class="rr-stat__label">{{ isShortfall ? 'Shortfall' : 'Surplus' }}</span>
|
||||
<span class="rr-stat__value">{{ amount(shortfallAmount) }}</span>
|
||||
<span class="rr-stat__sub">{{ isShortfall ? 'Below requirement' : 'Above requirement' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ═══ Lines ═══ -->
|
||||
<div class="rr-card">
|
||||
<table class="rr-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Particulars</th>
|
||||
<th>As at</th>
|
||||
<th class="rr-table__num">Amount (INR)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Tracked by position, not by configKey: derived rows (the demand/time split, NDTL)
|
||||
have no config key of their own, so keying on it would collide them into one. -->
|
||||
@for (line of data.lines; track $index) {
|
||||
<tr
|
||||
class="rr-line"
|
||||
[class.rr-line--unconfigured]="!line.configured"
|
||||
[class.rr-line--derived]="line.derived"
|
||||
>
|
||||
<td>
|
||||
<span class="rr-line__label">{{ line.label }}</span>
|
||||
@if (!line.configured) {
|
||||
<span class="rr-badge rr-badge--unmapped">Not configured</span>
|
||||
}
|
||||
</td>
|
||||
<td class="rr-line__date">{{ line.asOnDate }}</td>
|
||||
<td class="rr-table__num">
|
||||
@if (line.configured) {
|
||||
{{ amount(line.amount) }}
|
||||
} @else {
|
||||
<span class="rr-dash">—</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Account-level breakdown, so every total traces back to the accounts behind it. -->
|
||||
@for (account of line.accounts; track account.glAccountId) {
|
||||
<tr class="rr-subline" [class.rr-subline--excluded]="account.excluded">
|
||||
<td>
|
||||
<span class="rr-subline__code">{{ account.glCode }}</span>
|
||||
{{ account.name }}
|
||||
<!-- An account still pinned to the role but out of the total: named here so the
|
||||
line's figure can be read as short by it, rather than looking complete. -->
|
||||
@if (account.excluded) {
|
||||
<span class="rr-subline__excluded">excluded — {{ account.excluded }}</span>
|
||||
}
|
||||
<!-- Where only part of an account is taken, the figure below is not the account's
|
||||
balance and must not be read as one: state the whole balance and the share of
|
||||
it this head takes, so the two can be reconciled without opening the ledger. -->
|
||||
@if (isPartial(account)) {
|
||||
<span class="rr-subline__portion">
|
||||
{{ account.weightPercentage }}% of {{ amount(account.accountBalance) }}
|
||||
</span>
|
||||
}
|
||||
<!-- Which head the portion falls under, on a liability line that carries both. -->
|
||||
@if (account.liabilityClass) {
|
||||
<span class="rr-subline__class">{{ account.liabilityClass | titlecase }}</span>
|
||||
}
|
||||
</td>
|
||||
<td></td>
|
||||
<td class="rr-table__num">{{ amount(account.balance) }}</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
|
||||
@if (data.computable) {
|
||||
<tr class="rr-total">
|
||||
<td>{{ data.rateType }} required ({{ data.ratePercentage }}% of NDTL)</td>
|
||||
<td></td>
|
||||
<td class="rr-table__num">{{ amount(data.requiredAmount) }}</td>
|
||||
</tr>
|
||||
<tr class="rr-total">
|
||||
<td>{{ data.rateType }} maintained</td>
|
||||
<td></td>
|
||||
<td class="rr-table__num">{{ amount(data.maintainedAmount) }}</td>
|
||||
</tr>
|
||||
<tr class="rr-total" [class.rr-total--bad]="isShortfall">
|
||||
<td>{{ isShortfall ? 'Shortfall' : 'Surplus' }}</td>
|
||||
<td></td>
|
||||
<td class="rr-table__num">{{ amount(shortfallAmount) }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
═══ Fortnight maintenance ═══
|
||||
CRR is not met by the closing day's balance: the requirement must be held on average across
|
||||
the fortnight AND a daily minimum on every day of it. A bank can pass the closing position and
|
||||
still fail, so both tests are stated.
|
||||
-->
|
||||
@if (data.maintenance; as m) {
|
||||
<div class="rr-card rr-card--section">
|
||||
<h3 class="rr-section__title">
|
||||
Maintenance over the fortnight
|
||||
@if (!m.fortnightComplete) {
|
||||
<span class="rr-badge rr-badge--partial">In progress — {{ m.dayCount }} days so far</span>
|
||||
}
|
||||
</h3>
|
||||
<p class="rr-section__hint">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div class="rr-summary">
|
||||
<div class="rr-stat" [class.rr-stat--good]="m.averageMet" [class.rr-stat--bad]="!m.averageMet">
|
||||
<span class="rr-stat__label">Average maintained</span>
|
||||
<span class="rr-stat__value">{{ amount(m.average) }}</span>
|
||||
<div class="rr-stat">
|
||||
<span class="rr-stat__label">NDTL base</span>
|
||||
<span class="rr-stat__value">{{ amount(data.ndtlAmount) }}</span>
|
||||
<span class="rr-stat__sub">
|
||||
{{ m.averageMet ? 'Meets' : 'Below' }} required {{ amount(m.requiredAverage) }}
|
||||
DTL {{ amount(data.dtlAmount) }} less {{ amount(data.interBankAssets) }} · as at
|
||||
{{ data.basis.dtlAsOnDate }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="rr-stat" [class.rr-stat--good]="m.dailyMinimumMet" [class.rr-stat--bad]="!m.dailyMinimumMet">
|
||||
<span class="rr-stat__label">Lowest day</span>
|
||||
<span class="rr-stat__value">{{ amount(m.lowest) }}</span>
|
||||
<span class="rr-stat__sub">
|
||||
on {{ m.lowestOn }} · floor {{ amount(m.dailyMinimumAmount) }}
|
||||
</span>
|
||||
<div class="rr-stat" [class.rr-stat--provisional]="data.rateIsSeedDefault">
|
||||
<span class="rr-stat__label">{{ data.rateType }} rate</span>
|
||||
<span class="rr-stat__value">{{ data.ratePercentage }}%</span>
|
||||
@if (data.rateIsSeedDefault) {
|
||||
<span class="rr-stat__sub rr-stat__sub--warn">Placeholder — not confirmed</span>
|
||||
} @else {
|
||||
<span class="rr-stat__sub">in force on {{ data.reportingDate }}</span>
|
||||
}
|
||||
</div>
|
||||
<div class="rr-stat">
|
||||
<span class="rr-stat__label">Days counted</span>
|
||||
<span class="rr-stat__value">{{ m.dayCount }}</span>
|
||||
<span class="rr-stat__sub">{{ m.fortnightComplete ? 'Full fortnight' : 'To reporting date' }}</span>
|
||||
<span class="rr-stat__label">Required</span>
|
||||
<span class="rr-stat__value">{{ amount(data.requiredAmount) }}</span>
|
||||
<span class="rr-stat__sub">{{ data.ratePercentage }}% of NDTL</span>
|
||||
</div>
|
||||
<div class="rr-stat">
|
||||
<span class="rr-stat__label">Maintained</span>
|
||||
<span class="rr-stat__value">{{ amount(data.maintainedAmount) }}</span>
|
||||
<span class="rr-stat__sub">{{ percent(data.maintainedPercentage) }} of NDTL</span>
|
||||
</div>
|
||||
<div class="rr-stat" [class.rr-stat--bad]="isShortfall" [class.rr-stat--good]="!isShortfall">
|
||||
<span class="rr-stat__label">{{ isShortfall ? 'Shortfall' : 'Surplus' }}</span>
|
||||
<span class="rr-stat__value">{{ amount(shortfallAmount) }}</span>
|
||||
<span class="rr-stat__sub">{{ isShortfall ? 'Below requirement' : 'Above requirement' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ═══ Lines ═══ -->
|
||||
<div class="rr-card">
|
||||
<table class="rr-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th class="rr-table__num">Maintained (INR)</th>
|
||||
<th class="rr-table__num">Against floor</th>
|
||||
<th>Particulars</th>
|
||||
<th>As at</th>
|
||||
<th class="rr-table__num">Amount (INR)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (day of m.days; track day.date) {
|
||||
<tr [class.rr-day--short]="day.amount < m.dailyMinimumAmount">
|
||||
<td>{{ day.date }}</td>
|
||||
<td class="rr-table__num">{{ amount(day.amount) }}</td>
|
||||
<td class="rr-table__num">
|
||||
@if (day.amount < m.dailyMinimumAmount) {
|
||||
<span class="rr-badge rr-badge--unmapped">Below floor</span>
|
||||
} @else {
|
||||
<span class="rr-ok">OK</span>
|
||||
<!-- Tracked by position, not by configKey: derived rows (the demand/time split, NDTL)
|
||||
have no config key of their own, so keying on it would collide them into one. -->
|
||||
@for (line of data.lines; track $index) {
|
||||
<tr
|
||||
class="rr-line"
|
||||
[class.rr-line--unconfigured]="!line.configured"
|
||||
[class.rr-line--derived]="line.derived"
|
||||
>
|
||||
<td>
|
||||
<span class="rr-line__label">{{ line.label }}</span>
|
||||
@if (!line.configured) {
|
||||
<span class="rr-badge rr-badge--unmapped">Not configured</span>
|
||||
}
|
||||
</td>
|
||||
<td class="rr-line__date">{{ line.asOnDate }}</td>
|
||||
<td class="rr-table__num">
|
||||
@if (line.configured) {
|
||||
{{ amount(line.amount) }}
|
||||
} @else {
|
||||
<span class="rr-dash">—</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Account-level breakdown, so every total traces back to the accounts behind it. -->
|
||||
@for (account of line.accounts; track account.glAccountId) {
|
||||
<tr class="rr-subline" [class.rr-subline--excluded]="account.excluded">
|
||||
<td>
|
||||
<span class="rr-subline__code">{{ account.glCode }}</span>
|
||||
{{ account.name }}
|
||||
<!-- An account still pinned to the role but out of the total: named here so the
|
||||
line's figure can be read as short by it, rather than looking complete. -->
|
||||
@if (account.excluded) {
|
||||
<span class="rr-subline__excluded">excluded — {{ account.excluded }}</span>
|
||||
}
|
||||
<!-- Where only part of an account is taken, the figure below is not the account's
|
||||
balance and must not be read as one: state the whole balance and the share of
|
||||
it this head takes, so the two can be reconciled without opening the ledger. -->
|
||||
@if (isPartial(account)) {
|
||||
<span class="rr-subline__portion">
|
||||
{{ account.weightPercentage }}% of {{ amount(account.accountBalance) }}
|
||||
</span>
|
||||
}
|
||||
<!-- Which head the portion falls under, on a liability line that carries both. -->
|
||||
@if (account.liabilityClass) {
|
||||
<span class="rr-subline__class">{{ account.liabilityClass | titlecase }}</span>
|
||||
}
|
||||
</td>
|
||||
<td></td>
|
||||
<td class="rr-table__num">{{ amount(account.balance) }}</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
|
||||
@if (data.computable) {
|
||||
<tr class="rr-total">
|
||||
<td>{{ data.rateType }} required ({{ data.ratePercentage }}% of NDTL)</td>
|
||||
<td></td>
|
||||
<td class="rr-table__num">{{ amount(data.requiredAmount) }}</td>
|
||||
</tr>
|
||||
<tr class="rr-total">
|
||||
<td>{{ data.rateType }} maintained</td>
|
||||
<td></td>
|
||||
<td class="rr-table__num">{{ amount(data.maintainedAmount) }}</td>
|
||||
</tr>
|
||||
<tr class="rr-total" [class.rr-total--bad]="isShortfall">
|
||||
<td>{{ isShortfall ? 'Shortfall' : 'Surplus' }}</td>
|
||||
<td></td>
|
||||
<td class="rr-table__num">{{ amount(shortfallAmount) }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ═══ Memorandum and Annexures ═══ -->
|
||||
@for (section of namedSections; track section.title) {
|
||||
@if (section.rows.length) {
|
||||
<!--
|
||||
═══ Fortnight maintenance ═══
|
||||
CRR is not met by the closing day's balance: the requirement must be held on average across
|
||||
the fortnight AND a daily minimum on every day of it. A bank can pass the closing position and
|
||||
still fail, so both tests are stated.
|
||||
-->
|
||||
@if (data.maintenance; as m) {
|
||||
<div class="rr-card rr-card--section">
|
||||
<h3 class="rr-section__title">{{ section.title }}</h3>
|
||||
<p class="rr-section__hint">{{ section.hint }}</p>
|
||||
<h3 class="rr-section__title">
|
||||
Daily maintenance — {{ m.periodLabel || 'this month' }}
|
||||
@if (!m.fortnightComplete) {
|
||||
<span class="rr-badge rr-badge--partial">In progress — {{ m.dayCount }} days so far</span>
|
||||
}
|
||||
</h3>
|
||||
<p class="rr-section__hint">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div class="rr-summary">
|
||||
<div class="rr-stat" [class.rr-stat--good]="m.averageMet" [class.rr-stat--bad]="!m.averageMet">
|
||||
<span class="rr-stat__label">Average maintained</span>
|
||||
<span class="rr-stat__value">{{ amount(m.average) }}</span>
|
||||
<span class="rr-stat__sub">
|
||||
{{ m.averageMet ? 'Meets' : 'Below' }} required {{ amount(m.requiredAverage) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="rr-stat" [class.rr-stat--good]="m.dailyMinimumMet" [class.rr-stat--bad]="!m.dailyMinimumMet">
|
||||
<span class="rr-stat__label">Lowest day</span>
|
||||
<span class="rr-stat__value">{{ amount(m.lowest) }}</span>
|
||||
<span class="rr-stat__sub"> on {{ m.lowestOn }} · floor {{ amount(m.dailyMinimumAmount) }} </span>
|
||||
</div>
|
||||
<div class="rr-stat">
|
||||
<span class="rr-stat__label">Days counted</span>
|
||||
<span class="rr-stat__value">{{ m.dayCount }}</span>
|
||||
<span class="rr-stat__sub">{{ m.fortnightComplete ? 'Full fortnight' : 'To reporting date' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="rr-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Particulars</th>
|
||||
<th class="rr-table__num">Amount (INR)</th>
|
||||
<th>Date</th>
|
||||
<th class="rr-table__num">Required (INR)</th>
|
||||
<th class="rr-table__num">Actual (INR)</th>
|
||||
<th class="rr-table__num">Surplus / (Shortfall)</th>
|
||||
<th class="rr-table__num">Against floor</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of section.rows; track row.configKey) {
|
||||
<tr [class.rr-line--unconfigured]="!row.configured">
|
||||
<td>
|
||||
{{ row.label }}
|
||||
@if (!row.configured) {
|
||||
<span class="rr-badge rr-badge--unmapped">Not configured</span>
|
||||
}
|
||||
</td>
|
||||
@for (day of m.days; track day.date) {
|
||||
<tr [class.rr-day--short]="day.amount < m.dailyMinimumAmount">
|
||||
<td>{{ day.date }}</td>
|
||||
<td class="rr-table__num">{{ amount(day.required) }}</td>
|
||||
<td class="rr-table__num">{{ amount(day.amount) }}</td>
|
||||
<td class="rr-table__num" [class.rr-neg]="day.surplus < 0">{{ amount(day.surplus) }}</td>
|
||||
<td class="rr-table__num">
|
||||
@if (row.configured) {
|
||||
{{ amount(row.amount) }}
|
||||
@if (day.amount < m.dailyMinimumAmount) {
|
||||
<span class="rr-badge rr-badge--unmapped">Below floor</span>
|
||||
} @else {
|
||||
<span class="rr-dash">—</span>
|
||||
<span class="rr-ok">OK</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -311,6 +305,250 @@
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ═══ Memorandum and Annexures ═══ -->
|
||||
@for (section of namedSections; track section.title) {
|
||||
@if (section.rows.length) {
|
||||
<div class="rr-card rr-card--section">
|
||||
<h3 class="rr-section__title">{{ section.title }}</h3>
|
||||
<p class="rr-section__hint">{{ section.hint }}</p>
|
||||
<table class="rr-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Particulars</th>
|
||||
<th class="rr-table__num">Amount (INR)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of section.rows; track row.configKey) {
|
||||
<tr [class.rr-line--unconfigured]="!row.configured">
|
||||
<td>
|
||||
{{ row.label }}
|
||||
@if (!row.configured) {
|
||||
<span class="rr-badge rr-badge--unmapped">Not configured</span>
|
||||
}
|
||||
</td>
|
||||
<td class="rr-table__num">
|
||||
@if (row.configured) {
|
||||
{{ amount(row.amount) }}
|
||||
} @else {
|
||||
<span class="rr-dash">—</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<!--
|
||||
═══ Saved returns ═══
|
||||
Outside the `@if (data)` block so the list stays reachable even when the live return cannot be
|
||||
computed — a saved return from a month when it could is exactly what is wanted then, and a tab
|
||||
that vanishes when the live figures fail would hide exactly the returns that still work.
|
||||
-->
|
||||
@if (activeTab === 'saved') {
|
||||
<div class="rr-card rr-card--section rr-noprint">
|
||||
<h3 class="rr-section__title">
|
||||
Saved returns
|
||||
@if (savedReturns.length) {
|
||||
<span class="rr-badge rr-badge--mapped">{{ savedReturns.length }}</span>
|
||||
}
|
||||
</h3>
|
||||
<p class="rr-section__hint">
|
||||
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.
|
||||
</p>
|
||||
|
||||
@if (data?.computable) {
|
||||
<div class="rr-save-row">
|
||||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rr-save-row__note">
|
||||
<mat-label>Note (optional)</mat-label>
|
||||
<input matInput [(ngModel)]="savedRemarks" placeholder="e.g. provisional filing for this fortnight" />
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" [disabled]="saving" (click)="saveCurrent()">
|
||||
{{ saving ? 'Saving…' : 'Save this return' }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savedLoading) {
|
||||
<div class="rr-loading">Loading saved returns…</div>
|
||||
} @else if (!savedReturns.length) {
|
||||
<div class="rr-empty">Nothing saved yet. Generate a return and press Save to keep it.</div>
|
||||
} @else {
|
||||
<table class="rr-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Reporting date</th>
|
||||
<th>Period</th>
|
||||
<th class="rr-table__num">NDTL</th>
|
||||
<th class="rr-table__num">Required</th>
|
||||
<th class="rr-table__num">Maintained</th>
|
||||
<th>Status</th>
|
||||
<th>Saved</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of savedReturns; track row.id) {
|
||||
<tr [class.rr-saved--open]="viewingSaved?.id === row.id">
|
||||
<td>{{ row.reportingDate }}</td>
|
||||
<td class="rr-saved__period">{{ row.periodStart }} → {{ row.periodEnd }}</td>
|
||||
<td class="rr-table__num">{{ amount(row.ndtlAmount) }}</td>
|
||||
<td class="rr-table__num">{{ amount(row.requiredAmount) }}</td>
|
||||
<td class="rr-table__num">{{ amount(row.maintainedAmount) }}</td>
|
||||
<td>
|
||||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rr-saved__status">
|
||||
<mat-select [value]="row.status" (selectionChange)="updateSavedStatus(row, $event.value)">
|
||||
@for (s of filingStatuses; track s) {
|
||||
<mat-option [value]="s">{{ statusLabel(s) }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</td>
|
||||
<td class="rr-saved__meta">{{ row.createdOn }}<br />{{ row.createdBy }}</td>
|
||||
<td class="rr-saved__actions">
|
||||
<button mat-button class="rr-saved__view" (click)="viewSaved(row)">
|
||||
{{ viewingSaved?.id === row.id ? 'Close' : 'View' }}
|
||||
</button>
|
||||
<button mat-button class="rr-saved__del" (click)="deleteSaved(row)" title="Remove">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!--
|
||||
═══ The opened snapshot ═══
|
||||
Its own section below the list rather than in place of the live return: the reason to open a
|
||||
saved return is almost always to compare it against where the bank stands now, which swapping
|
||||
the live figures out would make impossible.
|
||||
-->
|
||||
@if (viewingSaved && savedData; as _) {
|
||||
<div class="rr-card rr-card--section rr-card--saved" id="rr-saved-view">
|
||||
<div class="rr-saved-head">
|
||||
<div>
|
||||
<h3 class="rr-section__title">
|
||||
Saved return — {{ savedData.reportingDate }}
|
||||
<span class="rr-badge rr-badge--future">{{ statusLabel(viewingSaved.status) }}</span>
|
||||
</h3>
|
||||
<p class="rr-section__hint">
|
||||
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) {
|
||||
<em class="rr-saved__remarks">“{{ viewingSaved.remarks }}”</em>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<button mat-stroked-button (click)="closeSaved()">Close</button>
|
||||
</div>
|
||||
|
||||
<div class="rr-summary">
|
||||
<div class="rr-stat">
|
||||
<span class="rr-stat__label">NDTL base</span>
|
||||
<span class="rr-stat__value">{{ amount(savedData.ndtlAmount ?? savedData.dtlAmount) }}</span>
|
||||
<span class="rr-stat__sub">as at {{ savedData.basis.dtlAsOnDate }}</span>
|
||||
</div>
|
||||
<div class="rr-stat">
|
||||
<span class="rr-stat__label">{{ savedData.rateType }} rate</span>
|
||||
<span class="rr-stat__value">{{ savedData.ratePercentage }}%</span>
|
||||
<span class="rr-stat__sub">as filed</span>
|
||||
</div>
|
||||
<div class="rr-stat">
|
||||
<span class="rr-stat__label">Required</span>
|
||||
<span class="rr-stat__value">{{ amount(savedData.requiredAmount) }}</span>
|
||||
<span class="rr-stat__sub">{{ savedData.ratePercentage }}% of NDTL</span>
|
||||
</div>
|
||||
<div class="rr-stat">
|
||||
<span class="rr-stat__label">Maintained</span>
|
||||
<span class="rr-stat__value">{{ amount(savedData.maintainedAmount) }}</span>
|
||||
<span class="rr-stat__sub">on {{ savedData.basis.holdingsAsOnDate }}</span>
|
||||
</div>
|
||||
<div class="rr-stat" [class.rr-stat--good]="savedData.compliant" [class.rr-stat--bad]="!savedData.compliant">
|
||||
<span class="rr-stat__label">{{ savedData.compliant ? 'Surplus' : 'Shortfall' }}</span>
|
||||
<span class="rr-stat__value">{{ amount(savedSurplus) }}</span>
|
||||
<span class="rr-stat__sub">{{ savedData.compliant ? 'Above requirement' : 'Below requirement' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="rr-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Particulars</th>
|
||||
<th>As at</th>
|
||||
<th class="rr-table__num">Amount (INR)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (line of savedData.lines; track line.configKey) {
|
||||
<tr class="rr-line" [class.rr-line--unconfigured]="!line.configured">
|
||||
<td>
|
||||
<span class="rr-line__label">{{ line.label }}</span>
|
||||
</td>
|
||||
<td class="rr-line__date">{{ line.asOnDate }}</td>
|
||||
<td class="rr-table__num">
|
||||
@if (line.configured) {
|
||||
{{ amount(line.amount) }}
|
||||
} @else {
|
||||
<span class="rr-dash">—</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
@for (account of line.accounts; track account.glAccountId) {
|
||||
<tr class="rr-subline">
|
||||
<td>
|
||||
<span class="rr-subline__code">{{ account.glCode }}</span>
|
||||
{{ account.name }}
|
||||
@if (isPartial(account)) {
|
||||
<span class="rr-subline__portion">
|
||||
{{ account.weightPercentage }}% of {{ amount(account.accountBalance) }}
|
||||
</span>
|
||||
}
|
||||
@if (account.liabilityClass) {
|
||||
<span class="rr-subline__class">{{ account.liabilityClass | titlecase }}</span>
|
||||
}
|
||||
</td>
|
||||
<td></td>
|
||||
<td class="rr-table__num">{{ amount(account.balance) }}</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@if (savedData.maintenance; as sm) {
|
||||
<h4 class="rr-saved__subhead">Daily maintenance as filed — {{ sm.periodLabel || '' }}</h4>
|
||||
<table class="rr-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th class="rr-table__num">Required</th>
|
||||
<th class="rr-table__num">Actual</th>
|
||||
<th class="rr-table__num">Surplus / (Shortfall)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (day of sm.days; track day.date) {
|
||||
<tr [class.rr-day--short]="day.amount < sm.dailyMinimumAmount">
|
||||
<td>{{ day.date }}</td>
|
||||
<td class="rr-table__num">{{ amount(day.required) }}</td>
|
||||
<td class="rr-table__num">{{ amount(day.amount) }}</td>
|
||||
<td class="rr-table__num" [class.rr-neg]="day.surplus < 0">{{ amount(day.surplus) }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
+4
-4
@@ -21,7 +21,7 @@
|
||||
<div class="flex-100 layout-row-wrap responsive-column">
|
||||
<mifosx-gl-account-display
|
||||
class="flex-100"
|
||||
[accountTitle]="'Saving reference'"
|
||||
[accountTitle]="productLabel ? productLabel + ' reference' : 'Saving reference'"
|
||||
[glAccount]="accountingMappings.savingsReferenceAccount"
|
||||
[withTitle]="'40%'"
|
||||
>
|
||||
@@ -72,7 +72,7 @@
|
||||
<div class="flex-100 layout-row-wrap responsive-column">
|
||||
<mifosx-gl-account-display
|
||||
class="flex-100"
|
||||
[accountTitle]="'Saving control'"
|
||||
[accountTitle]="productLabel ? productLabel + ' control' : 'Saving control'"
|
||||
[glAccount]="accountingMappings.savingsControlAccount"
|
||||
[withTitle]="'40%'"
|
||||
>
|
||||
@@ -81,7 +81,7 @@
|
||||
<div class="flex-100 layout-row-wrap responsive-column">
|
||||
<mifosx-gl-account-display
|
||||
class="flex-100"
|
||||
[accountTitle]="'Savings transfers in suspense'"
|
||||
[accountTitle]="productLabel ? 'Transfers in suspense' : 'Savings transfers in suspense'"
|
||||
[glAccount]="accountingMappings.transfersInSuspenseAccount"
|
||||
[withTitle]="'40%'"
|
||||
>
|
||||
@@ -143,7 +143,7 @@
|
||||
<div class="flex-100 layout-row-wrap responsive-column">
|
||||
<mifosx-gl-account-display
|
||||
class="flex-100"
|
||||
[accountTitle]="'Interest on savings'"
|
||||
[accountTitle]="productLabel ? 'Interest on ' + productLabel.toLowerCase() + 's' : 'Interest on savings'"
|
||||
[glAccount]="accountingMappings.interestOnSavingsAccount"
|
||||
[withTitle]="'40%'"
|
||||
>
|
||||
|
||||
+2
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
/* tslint:disable */
|
||||
export default {
|
||||
'mifos_x': {
|
||||
'version': '260924',
|
||||
'hash': 'a6e62201'
|
||||
'version': '260928',
|
||||
'hash': '6cbb31b5'
|
||||
},
|
||||
'allow_switching_backend_instance': true
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user