feat: updates for cashier, tds reports, rbi reserve and dashboard
This commit is contained in:
@@ -10,11 +10,11 @@ import { Component, OnInit, inject } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import * as ExcelJS from 'exceljs';
|
||||
|
||||
import { BranchScopeService } from '../core/branch-scope.service';
|
||||
import { FY_TAX_CONFIG, Quarter, QUARTER_LABEL } from '../letter-mgmt/certificates/form-15gh/tds-15gh.compute';
|
||||
import { TdsDeductee, setTdsDeductees, tdsFys } from './tds-reports-data';
|
||||
import { buildTdsInterestWorkbook } from './tds-interest-export';
|
||||
import { TdsReportsService } from './tds-reports.service';
|
||||
import {
|
||||
NEAR_THRESHOLD_BAND,
|
||||
@@ -26,6 +26,9 @@ import {
|
||||
tdsInterestSummary
|
||||
} from './tds-reports.compute';
|
||||
|
||||
/** The three answers the register gives: TDS taken, not taken because of a declaration, none due. */
|
||||
type TdsTab = 'paying' | 'declared' | 'not-paying';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-tds-interest-report',
|
||||
templateUrl: './tds-interest-report.component.html',
|
||||
@@ -70,7 +73,7 @@ export class TdsInterestReportComponent implements OnInit {
|
||||
/** Which customer's quarter-wise detail is open. */
|
||||
expandedId: string | null = null;
|
||||
exporting = false;
|
||||
selectedTab: 'paying' | 'not-paying' = 'paying';
|
||||
selectedTab: TdsTab = 'paying';
|
||||
|
||||
/** Rows per page; 0 shows the whole list. */
|
||||
readonly pageSizeOptions = [
|
||||
@@ -92,6 +95,7 @@ export class TdsInterestReportComponent implements OnInit {
|
||||
private allRows: TdsInterestRow[] = [];
|
||||
private paying: TdsInterestRow[] = [];
|
||||
private notPaying: TdsInterestRow[] = [];
|
||||
private declared: TdsInterestRow[] = [];
|
||||
private summaryCacheKey = '';
|
||||
private cachedSummary: TdsInterestSummary | null = null;
|
||||
|
||||
@@ -219,8 +223,28 @@ export class TdsInterestReportComponent implements OnInit {
|
||||
if (!row.trackedAccounts) {
|
||||
return { label: 'Not tracked — no FD/RD', tone: 'muted' };
|
||||
}
|
||||
if (row.hasDeclaration) {
|
||||
const beforeForm = Math.round(row.schedule.totalTds - row.actualTds);
|
||||
if (beforeForm > 0) {
|
||||
return { label: `Form ${row.form} — ₹${beforeForm.toLocaleString('en-IN')} due from before the form, not yet deducted`, tone: 'warn' };
|
||||
}
|
||||
if (row.tdsSaved > 0) {
|
||||
return { label: `Form ${row.form} — TDS not deducted`, tone: 'ok' };
|
||||
}
|
||||
return { label: `Form ${row.form} on record — below the limit so far`, tone: 'ok' };
|
||||
}
|
||||
if (row.schedule.totalTds > 0) {
|
||||
return { label: 'Over the limit — TDS due', tone: 'warn' };
|
||||
const gap = Math.round(row.schedule.totalTds - row.actualTds);
|
||||
if (gap > 0) {
|
||||
return { label: `Over the limit — ₹${gap.toLocaleString('en-IN')} not yet deducted`, tone: 'warn' };
|
||||
}
|
||||
if (gap < 0) {
|
||||
return { label: `Deducted ₹${(-gap).toLocaleString('en-IN')} more than due — check`, tone: 'warn' };
|
||||
}
|
||||
return { label: 'Over the limit — TDS deducted', tone: 'ok' };
|
||||
}
|
||||
if (row.actualTds > 0) {
|
||||
return { label: `₹${Math.round(row.actualTds).toLocaleString('en-IN')} deducted but none due — check`, tone: 'warn' };
|
||||
}
|
||||
if (row.hasDeclaration && row.crossed) {
|
||||
return { label: 'Over the limit — 15G/15H, no TDS', tone: 'ok' };
|
||||
@@ -247,8 +271,12 @@ export class TdsInterestReportComponent implements OnInit {
|
||||
}
|
||||
this.rowsCacheKey = key;
|
||||
this.allRows = tdsInterestRows(this.filter);
|
||||
this.paying = this.allRows.filter((r) => r.schedule.totalTds > 0);
|
||||
this.notPaying = this.allRows.filter((r) => r.schedule.totalTds === 0);
|
||||
// Three plain answers: whose TDS was taken, whose was not because they gave a declaration, and who
|
||||
// owed none. A declarant whose tax was partly taken before the form is shown with the declarants,
|
||||
// where both figures sit side by side.
|
||||
this.declared = this.allRows.filter((r) => r.hasDeclaration);
|
||||
this.paying = this.allRows.filter((r) => !r.hasDeclaration && (r.schedule.totalTds > 0 || r.actualTds > 0));
|
||||
this.notPaying = this.allRows.filter((r) => !r.hasDeclaration && r.schedule.totalTds === 0 && r.actualTds === 0);
|
||||
}
|
||||
|
||||
get rows(): TdsInterestRow[] {
|
||||
@@ -266,6 +294,11 @@ export class TdsInterestReportComponent implements OnInit {
|
||||
return this.notPaying;
|
||||
}
|
||||
|
||||
get declaredRows(): TdsInterestRow[] {
|
||||
this.ensureRows();
|
||||
return this.declared;
|
||||
}
|
||||
|
||||
get summary(): TdsInterestSummary {
|
||||
const key = JSON.stringify(this.filter);
|
||||
if (key !== this.summaryCacheKey || !this.cachedSummary) {
|
||||
@@ -277,6 +310,9 @@ export class TdsInterestReportComponent implements OnInit {
|
||||
|
||||
/** Every row of the tab on screen, before paging. */
|
||||
get visibleRows(): TdsInterestRow[] {
|
||||
if (this.selectedTab === 'declared') {
|
||||
return this.declaredRows;
|
||||
}
|
||||
return this.selectedTab === 'paying' ? this.payingRows : this.notPayingRows;
|
||||
}
|
||||
|
||||
@@ -337,7 +373,7 @@ export class TdsInterestReportComponent implements OnInit {
|
||||
this.expandedId = null;
|
||||
}
|
||||
|
||||
selectTab(tab: 'paying' | 'not-paying'): void {
|
||||
selectTab(tab: TdsTab): void {
|
||||
this.selectedTab = tab;
|
||||
this.onFilterChanged();
|
||||
}
|
||||
@@ -385,105 +421,17 @@ export class TdsInterestReportComponent implements OnInit {
|
||||
async exportWorkbook(): Promise<void> {
|
||||
this.exporting = true;
|
||||
try {
|
||||
const wb = new ExcelJS.Workbook();
|
||||
const ws = wb.addWorksheet(`TDS Interest FY ${this.filter.fy}`);
|
||||
|
||||
ws.addRow([`Deposit Interest & TDS — FY ${this.filter.fy}`]);
|
||||
ws.addRow([`Scope: ${this.scopeLabel}`]);
|
||||
ws.addRow([]);
|
||||
ws.addRow([
|
||||
'Customer',
|
||||
'CIF',
|
||||
'PAN',
|
||||
'Branch',
|
||||
'Accounts tracked for TDS',
|
||||
'Accounts not tracked',
|
||||
'Status',
|
||||
'Form',
|
||||
'Band',
|
||||
'Age',
|
||||
'Q1',
|
||||
'Q2',
|
||||
'Q3',
|
||||
'Q4',
|
||||
'Interest (FD/RD)',
|
||||
'Savings interest (exempt)',
|
||||
'Threshold',
|
||||
'Crossed in',
|
||||
'Rate %',
|
||||
'TDS payable',
|
||||
'Declaration',
|
||||
'UIN',
|
||||
'TDS saved',
|
||||
'Paid without deduction'
|
||||
]);
|
||||
|
||||
for (const r of this.rows) {
|
||||
ws.addRow([
|
||||
r.deductee.name,
|
||||
r.deductee.customerId,
|
||||
r.deductee.pan || 'NOT ON RECORD',
|
||||
r.branchCode,
|
||||
this.trackedOf(r)
|
||||
.map((a) => `${a.accountNo} (${a.type})`)
|
||||
.join(', '),
|
||||
this.untrackedOf(r)
|
||||
.map((a) => `${a.accountNo} (${a.type})`)
|
||||
.join(', '),
|
||||
this.statusOf(r).label,
|
||||
r.form === '121' ? 'Form 121' : `Form ${r.form}`,
|
||||
r.band === 'SENIOR' ? 'Senior citizen' : 'General',
|
||||
r.age,
|
||||
r.quarters[1],
|
||||
r.quarters[2],
|
||||
r.quarters[3],
|
||||
r.quarters[4],
|
||||
r.tdsApplicableInterest,
|
||||
r.exemptInterest,
|
||||
r.threshold,
|
||||
r.crossedInQuarter ? `Q${r.crossedInQuarter}` : '—',
|
||||
r.rateApplied * 100,
|
||||
r.schedule.totalTds,
|
||||
r.hasDeclaration ? 'Yes' : 'No',
|
||||
r.declarationUin ?? '',
|
||||
r.tdsSaved,
|
||||
r.paidWithoutDeduction
|
||||
]);
|
||||
}
|
||||
|
||||
const s = this.summary;
|
||||
ws.addRow([]);
|
||||
ws.addRow([
|
||||
'TOTAL',
|
||||
`${s.deductees} deductees`,
|
||||
'',
|
||||
'',
|
||||
`${s.trackedCustomers} tracked`,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
s.byQuarter[0].interest,
|
||||
s.byQuarter[1].interest,
|
||||
s.byQuarter[2].interest,
|
||||
s.byQuarter[3].interest,
|
||||
s.applicableInterest,
|
||||
s.exemptInterest,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
s.totalTds,
|
||||
`${s.declarations} filed`,
|
||||
'',
|
||||
s.tdsSaved,
|
||||
s.paidWithoutDeduction
|
||||
]);
|
||||
|
||||
ws.getRow(1).font = { bold: true, size: 13 };
|
||||
ws.getRow(4).font = { bold: true };
|
||||
ws.columns.forEach((c) => (c.width = 18));
|
||||
|
||||
const wb = buildTdsInterestWorkbook({
|
||||
fy: this.filter.fy,
|
||||
scopeLabel: this.scopeLabel,
|
||||
rules: this.rules,
|
||||
statement: this.statute.statement,
|
||||
// What the screen shows under the current filters, every tab included.
|
||||
rows: this.rows,
|
||||
declaredRows: this.declaredRows,
|
||||
summary: this.summary,
|
||||
statusOf: (row) => this.statusOf(row)
|
||||
});
|
||||
const buffer = await wb.xlsx.writeBuffer();
|
||||
const blob = new Blob([buffer], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
@@ -491,7 +439,7 @@ export class TdsInterestReportComponent implements OnInit {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `TDS-Interest-FY${this.filter.fy}.xlsx`;
|
||||
a.download = `TDS-Register-FY${this.filter.fy}.xlsx`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user