feat(rbi-reserve,savings): Form 1/Form 9 returns UI, branch-wise interest posting reports and summary
- Reports > Interest Posting Reports: per branch and product, generate/regenerate at any time, approve; branch column, HO-on-behalf marker, posted txn per account - Reports > Interest Posting Summary (branch-wise) for the head office, with Excel export - Day Close warns and disables Close Day while the branch's interest report is unapproved - Savings product accounting: Interest On Savings (expense), Savings Interest Payable and Paid (liability rows, required) - RBI Form 1 statutory and Form 9 return screens, reserve config updates - Interest posting report GETs exempted from the response cache Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -129,11 +129,30 @@
|
||||
|
||||
<!-- ═══ GL accounts ═══ -->
|
||||
<div class="rrc-card">
|
||||
<h3 class="rrc-card__title">GL accounts behind each figure</h3>
|
||||
<div class="rrc-gl-head">
|
||||
<h3 class="rrc-card__title">GL accounts behind each figure</h3>
|
||||
<!--
|
||||
The two returns share a base but maintain their requirement in different assets, so an
|
||||
administrator setting one up is looking at a different set of heads than one setting up
|
||||
the other. Showing all of them at once buries the few that matter for the task in hand.
|
||||
-->
|
||||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-gl-head__filter">
|
||||
<mat-label>Configuring</mat-label>
|
||||
<mat-select [(ngModel)]="formFilter" (selectionChange)="onFormFilterChange()">
|
||||
@for (f of formFilters; track f.value) {
|
||||
<mat-option [value]="f.value">{{ f.label }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<p class="rrc-card__hint">
|
||||
Every role takes as many accounts as it needs — a bank holds current accounts at several other banks, and each
|
||||
one counts toward what it maintains. Only postable accounts are listed: a header account's balance is the sum of
|
||||
its children, so mapping both would count those children twice.
|
||||
@if (formFilter !== 'ALL') {
|
||||
Heads that build or net the DTL base are shown for either return, since both charge their rate on that same
|
||||
base.
|
||||
}
|
||||
</p>
|
||||
|
||||
@if (unmappedCount) {
|
||||
|
||||
@@ -385,6 +385,20 @@ $red: #c5221f;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
// Title and the return filter on one line: the filter says what the list below is showing, so it
|
||||
// belongs beside the heading rather than buried among the roles.
|
||||
.rrc-gl-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&__filter {
|
||||
width: 260px;
|
||||
}
|
||||
}
|
||||
|
||||
.rrc-remove {
|
||||
min-width: 0 !important;
|
||||
padding: 0 8px !important;
|
||||
|
||||
@@ -64,6 +64,83 @@ export class RbiReserveConfigComponent implements OnInit {
|
||||
/** The roles grouped by the return they feed, in backend order. */
|
||||
roleGroups: { name: string; label: string; roles: any[] }[] = [];
|
||||
|
||||
/**
|
||||
* Which return's configuration is on screen: Form 1, Form 9, or both together.
|
||||
*
|
||||
* The two returns share a base but maintain their requirement in different assets, so an
|
||||
* administrator setting one up is looking at a different set of heads than one setting up the
|
||||
* other. Showing all twenty-six at once buries the four that matter for the task in hand.
|
||||
*/
|
||||
formFilter: 'ALL' | 'FORM_1' | 'FORM_9' | 'FORM_IX' = 'ALL';
|
||||
|
||||
/** Every reserve role the backend returned, before the form filter narrows it. */
|
||||
private allRoles: any[] = [];
|
||||
|
||||
/** Re-slices what is on screen. No round trip - the full set is already held. */
|
||||
onFormFilterChange(): void {
|
||||
this.glRoles = this.rolesForScreen();
|
||||
this.rebuildGroups();
|
||||
}
|
||||
|
||||
/**
|
||||
* The roles this screen shows: those in its own group, narrowed to the return selected above.
|
||||
*
|
||||
* The group scope is widened for Form IX. Its heads were seeded into their own group so they sit
|
||||
* apart on the backend, and a screen fixed to 'RESERVE' would filter every one of them out before
|
||||
* the return filter ever ran - leaving the Form IX option selectable and its list empty.
|
||||
*/
|
||||
private rolesForScreen(): any[] {
|
||||
const wanted = this.formFilter === 'FORM_IX' ? 'FORM9' : this.group;
|
||||
const scoped = wanted ? this.allRoles.filter((r: any) => r.group === wanted) : this.allRoles;
|
||||
return scoped.filter((r: any) => this.belongsToSelectedForm(r));
|
||||
}
|
||||
|
||||
readonly formFilters = [
|
||||
{ value: 'ALL', label: 'All returns' },
|
||||
{ value: 'FORM_1', label: 'Form 1 — CRR (fortnightly)' },
|
||||
{ value: 'FORM_9', label: 'Form 9 — SLR (monthly)' },
|
||||
{ value: 'FORM_IX', label: 'Form IX — Assets & Liabilities (R408)' }
|
||||
];
|
||||
|
||||
/**
|
||||
* Whether a head belongs to the return currently selected.
|
||||
*
|
||||
* Decided from how the return consumes the head rather than from its name: MAINTAINED is met in
|
||||
* cash and balances and so is Form 1's, SLR_ASSET is met in securities and gold and so is Form
|
||||
* 9's, and a head that builds or nets the DTL base belongs to both because both charge their rate
|
||||
* on that same base. A memo head is stated on either return and so shows on both.
|
||||
*/
|
||||
private belongsToSelectedForm(role: any): boolean {
|
||||
if (this.formFilter === 'ALL') {
|
||||
return true;
|
||||
}
|
||||
// Form IX's heads are placed by the group they were seeded into, not by usage. Its usages -
|
||||
// LIABILITY and ASSET - read the same as Form 1's but mean something different: on Form IX they
|
||||
// say which SIDE OF THE BALANCE SHEET a head sits on, not how it enters a ratio. Sixty heads
|
||||
// showing up under Form 1's filter would bury the eight that belong there.
|
||||
const group = role.group || role.roleGroup;
|
||||
if (group === 'FORM9') {
|
||||
return this.formFilter === 'FORM_IX';
|
||||
}
|
||||
if (this.formFilter === 'FORM_IX') {
|
||||
return false;
|
||||
}
|
||||
// The built-in SLR investments role predates the SLR_ASSET usage and still carries MAINTAINED,
|
||||
// so it is placed by name; every other head is placed by how the return consumes it.
|
||||
if (role.configKey === 'SLR_INVESTMENTS') {
|
||||
return this.formFilter === 'FORM_9';
|
||||
}
|
||||
const usage = role.usage || role.roleUsage;
|
||||
if (usage === 'MAINTAINED') {
|
||||
return this.formFilter === 'FORM_1';
|
||||
}
|
||||
if (usage === 'SLR_ASSET') {
|
||||
return this.formFilter === 'FORM_9';
|
||||
}
|
||||
// LIABILITY, ASSET_NET and MEMO all appear on both returns.
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Which group's roles are on screen. Unused when the screen is scoped to one group. */
|
||||
activeGroup = '';
|
||||
rates: any[] = [];
|
||||
@@ -103,6 +180,9 @@ export class RbiReserveConfigComponent implements OnInit {
|
||||
{ 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)' },
|
||||
// Form IX has no ratio and no base: its usages say which side of the balance sheet a head sits
|
||||
// on. LIABILITY above serves its liability side, and this its asset side.
|
||||
{ value: 'ASSET', label: 'Asset — enters Total Assets (Form IX)' },
|
||||
{ value: 'MEMO', label: 'Memo — shown on the return but totals nothing' }
|
||||
];
|
||||
|
||||
@@ -140,10 +220,11 @@ export class RbiReserveConfigComponent implements OnInit {
|
||||
}
|
||||
|
||||
private applyConfig(config: any): void {
|
||||
const allRoles = config.glRoles || [];
|
||||
// Kept so the form filter can re-slice without a round trip.
|
||||
this.allRoles = config.glRoles || [];
|
||||
// Scoped to this screen's return before anything else, so every count, warning and Save on the
|
||||
// page speaks about the roles the user can actually see.
|
||||
this.glRoles = this.group ? allRoles.filter((r: any) => r.group === this.group) : allRoles;
|
||||
this.glRoles = this.rolesForScreen();
|
||||
this.rates = config.rates || [];
|
||||
this.selection = {};
|
||||
for (const role of this.glRoles) {
|
||||
|
||||
@@ -61,12 +61,7 @@
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
[disabled]="savingGl || !isGlDirty(role)"
|
||||
(click)="saveGl(role)"
|
||||
>
|
||||
<button mat-raised-button color="primary" [disabled]="savingGl || !isGlDirty(role)" (click)="saveGl(role)">
|
||||
{{ savingGl ? 'Saving…' : 'Save' }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -78,8 +73,8 @@
|
||||
<div class="rrc-card">
|
||||
<h3 class="rrc-card__title">Financial years</h3>
|
||||
<p class="rrc-card__hint">
|
||||
One entry per year, because the Finance Act fixes TDS per year and a return for an earlier year must keep quoting
|
||||
that year's figures. Add the next year before 01 April, or TDS screens stop computing on that date.
|
||||
One entry per year, because the Finance Act fixes TDS per year and a return for an earlier year must keep
|
||||
quoting that year's figures. Add the next year before 01 April, or TDS screens stop computing on that date.
|
||||
</p>
|
||||
|
||||
<div class="rrc-tabs" role="tablist">
|
||||
|
||||
@@ -37,7 +37,10 @@ interface FieldDef {
|
||||
@Component({
|
||||
selector: 'mifosx-tds-config',
|
||||
templateUrl: './tds-config.component.html',
|
||||
styleUrls: ['../rbi-reserve-config/rbi-reserve-config.component.scss', './tds-config.component.scss'],
|
||||
styleUrls: [
|
||||
'../rbi-reserve-config/rbi-reserve-config.component.scss',
|
||||
'./tds-config.component.scss'
|
||||
],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FormsModule
|
||||
@@ -67,7 +70,12 @@ export class TdsConfigComponent implements OnInit {
|
||||
|
||||
readonly rateFields: FieldDef[] = [
|
||||
{ key: 'thresholdGeneral', label: 'Threshold – general (₹)', kind: 'amount', hint: 'Payee below 60' },
|
||||
{ key: 'thresholdSenior', label: 'Threshold – senior citizen (₹)', kind: 'amount', hint: '60 at any time in the year' },
|
||||
{
|
||||
key: 'thresholdSenior',
|
||||
label: 'Threshold – senior citizen (₹)',
|
||||
kind: 'amount',
|
||||
hint: '60 at any time in the year'
|
||||
},
|
||||
{ key: 'ratePanPercentage', label: 'Rate – valid PAN (%)', kind: 'percent' },
|
||||
{ key: 'rateNoPanPercentage', label: 'Rate – PAN missing / invalid (%)', kind: 'percent' },
|
||||
{ key: 'lateDeductionPercentage', label: 'Late deduction interest (% per month)', kind: 'percent' },
|
||||
@@ -96,7 +104,12 @@ export class TdsConfigComponent implements OnInit {
|
||||
{ value: 'QUARTERLY', label: 'Quarterly' }
|
||||
]
|
||||
},
|
||||
{ key: 'declarationStatementDueDay', label: 'Statement due on day', kind: 'int', hint: 'Of the following month / quarter' }
|
||||
{
|
||||
key: 'declarationStatementDueDay',
|
||||
label: 'Statement due on day',
|
||||
kind: 'int',
|
||||
hint: 'Of the following month / quarter'
|
||||
}
|
||||
];
|
||||
|
||||
readonly statuteFields: FieldDef[] = [
|
||||
|
||||
Reference in New Issue
Block a user