A cash deposit posted to the wrong account is moved to the right one in four steps: unpass the voucher, a manager approves, the cashier enters the correct account, a manager approves again. Nothing changes on any account until the final approval. - Voucher Correction page with four-step strip and popups: today's vouchers to unpass (admin or clerk), the unpassed vouchers waiting for their correct account (cashier) with the wrong and correct account side by side and the balance check, and the caller's own requests. A cashier whose customer has used the money closes the request and deposits separately. - Reached from the cashier bar, the clerk bar and the Cashier menu; the manager reviews both steps under Approvals > Voucher Corrections, laid out like the other approval tabs. Routes for Cash Transfers approvals are added to the same tab bar. - A Correction column on My Vouchers, the day book and the voucher register (and in the voucher detail popup) says whether a voucher is unpassed, revoked in favour of another, a correction, or closed, with the reason. - The deposit taken off the wrong account no longer shows on its passbook or transaction list. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
335 lines
9.9 KiB
TypeScript
335 lines
9.9 KiB
TypeScript
/**
|
|
* Copyright since 2025 Mifos Initiative
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
import { Component, inject } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
|
import { MatButtonModule } from '@angular/material/button';
|
|
import { MatIconModule } from '@angular/material/icon';
|
|
import { VoucherStatusComponent, VoucherStatus } from '../voucher-status/voucher-status.component';
|
|
|
|
export interface VoucherDetailDialogData {
|
|
/** Already-loaded detail from CashierService.voucherDetail() — the caller fetches, this dialog
|
|
* only renders, so one loading state lives in the row's own click handler rather than being
|
|
* duplicated inside every dialog that could ever show a voucher. */
|
|
detail: {
|
|
voucherNo: string;
|
|
voucherStatus?: VoucherStatus;
|
|
revokedByVoucherNo?: string | null;
|
|
replacesVoucherNo?: string | null;
|
|
correctionNote?: string | null;
|
|
voucherType: string | null;
|
|
amount: number | null;
|
|
branchName: string | null;
|
|
createdBy: string | null;
|
|
createdAt: string | null;
|
|
totalDebit: number;
|
|
totalCredit: number;
|
|
balanced: boolean;
|
|
lines: {
|
|
lineNo: number;
|
|
entryType: 'DR' | 'CR';
|
|
partyName: string | null;
|
|
accountNo: string | null;
|
|
amount: number;
|
|
description: string | null;
|
|
}[];
|
|
/** GL entries a native platform command posted for the same underlying event, outside this
|
|
* voucher entirely — a share redemption's Dr Share Capital / Cr Share Sundry pair, posted at
|
|
* manager approval, before this voucher's own Dr Sundry / Cr Cash at Vault settlement leg even
|
|
* exists. Empty for every voucher that already is the whole transaction on its own. */
|
|
relatedLines: {
|
|
stage: string;
|
|
entryType: 'DR' | 'CR';
|
|
partyName: string | null;
|
|
accountNo: string | null;
|
|
amount: number;
|
|
description: string | null;
|
|
}[];
|
|
};
|
|
}
|
|
|
|
/** The full slip behind a summarised register row — every debit and credit line a voucher is
|
|
* made of, not just the first side of each that a table row has room for. */
|
|
@Component({
|
|
selector: 'mifosx-voucher-detail-dialog',
|
|
standalone: true,
|
|
imports: [
|
|
CommonModule,
|
|
MatDialogModule,
|
|
MatButtonModule,
|
|
MatIconModule,
|
|
VoucherStatusComponent
|
|
],
|
|
template: `
|
|
<div class="vd-shell">
|
|
<div class="vd-header">
|
|
<div class="vd-header__title">
|
|
<mat-icon class="vd-icon">receipt_long</mat-icon>
|
|
<div>
|
|
<h2>
|
|
{{ detail.voucherNo }}
|
|
<mifosx-voucher-status
|
|
[status]="detail.voucherStatus"
|
|
[revokedBy]="detail.revokedByVoucherNo"
|
|
[replaces]="detail.replacesVoucherNo"
|
|
[note]="detail.correctionNote"
|
|
></mifosx-voucher-status>
|
|
</h2>
|
|
<p>{{ detail.voucherType || 'Voucher' }} · {{ detail.branchName || 'Head Office' }}</p>
|
|
</div>
|
|
</div>
|
|
<button mat-icon-button mat-dialog-close aria-label="Close">
|
|
<mat-icon>close</mat-icon>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="vd-meta">
|
|
<span
|
|
>Raised by <strong>{{ detail.createdBy || '—' }}</strong></span
|
|
>
|
|
@if (detail.createdAt) {
|
|
<span>{{ detail.createdAt | date: 'dd MMM yyyy, h:mm a' }}</span>
|
|
}
|
|
@if (!detail.balanced) {
|
|
<span class="vd-unbalanced">
|
|
<mat-icon inline>warning</mat-icon>
|
|
Debit and credit do not balance
|
|
</span>
|
|
}
|
|
</div>
|
|
|
|
<table class="vd-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Side</th>
|
|
<th>Party</th>
|
|
<th>Account No.</th>
|
|
<th class="num">Amount</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@for (line of detail.lines; track line.lineNo) {
|
|
<tr>
|
|
<td>
|
|
<span
|
|
class="vd-side"
|
|
[class.vd-side--dr]="line.entryType === 'DR'"
|
|
[class.vd-side--cr]="line.entryType === 'CR'"
|
|
>
|
|
{{ line.entryType === 'DR' ? 'Debit' : 'Credit' }}
|
|
</span>
|
|
</td>
|
|
<td>{{ line.partyName || '—' }}</td>
|
|
<td class="vd-acct">{{ line.accountNo || '—' }}</td>
|
|
<td class="num">₹{{ line.amount | number: '1.2-2' }}</td>
|
|
</tr>
|
|
@if (line.description) {
|
|
<tr class="vd-desc-row">
|
|
<td></td>
|
|
<td colspan="3" class="vd-desc">{{ line.description }}</td>
|
|
</tr>
|
|
}
|
|
} @empty {
|
|
<tr>
|
|
<td colspan="4" class="vd-empty">This voucher's individual lines were not recorded.</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
@if (detail.lines.length > 0) {
|
|
<tfoot>
|
|
<tr>
|
|
<td colspan="3">Total</td>
|
|
<td class="num">
|
|
₹{{ detail.totalDebit | number: '1.2-2' }} / ₹{{ detail.totalCredit | number: '1.2-2' }}
|
|
</td>
|
|
</tr>
|
|
</tfoot>
|
|
}
|
|
</table>
|
|
|
|
@if (detail.relatedLines.length > 0) {
|
|
<div class="vd-related">
|
|
<div class="vd-related__title">
|
|
<mat-icon inline>link</mat-icon>
|
|
Also posted for this transaction — not part of this voucher
|
|
</div>
|
|
<table class="vd-table vd-table--related">
|
|
<tbody>
|
|
@for (line of detail.relatedLines; track $index) {
|
|
<tr>
|
|
<td>
|
|
<span
|
|
class="vd-side"
|
|
[class.vd-side--dr]="line.entryType === 'DR'"
|
|
[class.vd-side--cr]="line.entryType === 'CR'"
|
|
>
|
|
{{ line.entryType === 'DR' ? 'Debit' : 'Credit' }}
|
|
</span>
|
|
</td>
|
|
<td>{{ line.partyName || '—' }}</td>
|
|
<td class="vd-acct">{{ line.accountNo || '—' }}</td>
|
|
<td class="num">₹{{ line.amount | number: '1.2-2' }}</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
<p class="vd-related__hint">
|
|
{{ detail.relatedLines[0].stage }} — this and the lines above are the same transaction's full accounting.
|
|
</p>
|
|
</div>
|
|
}
|
|
|
|
<div class="vd-actions">
|
|
<button mat-flat-button color="primary" mat-dialog-close>Close</button>
|
|
</div>
|
|
</div>
|
|
`,
|
|
styles: [
|
|
`
|
|
.vd-shell {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
padding: 20px 24px 16px;
|
|
min-width: 420px;
|
|
max-width: 560px;
|
|
}
|
|
.vd-header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
}
|
|
.vd-header__title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
.vd-header__title h2 {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
}
|
|
.vd-header__title p {
|
|
margin: 2px 0 0;
|
|
font-size: 13px;
|
|
color: rgba(0, 0, 0, 0.6);
|
|
}
|
|
.vd-icon {
|
|
color: #1976d2;
|
|
}
|
|
.vd-meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px 16px;
|
|
font-size: 13px;
|
|
color: rgba(0, 0, 0, 0.65);
|
|
}
|
|
.vd-unbalanced {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
color: #b91c1c;
|
|
font-weight: 600;
|
|
}
|
|
.vd-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 13px;
|
|
}
|
|
.vd-table th {
|
|
text-align: left;
|
|
text-transform: uppercase;
|
|
font-size: 11px;
|
|
letter-spacing: 0.03em;
|
|
color: rgba(0, 0, 0, 0.5);
|
|
padding: 6px 8px;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
}
|
|
.vd-table td {
|
|
padding: 8px;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
}
|
|
.vd-table .num {
|
|
text-align: right;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.vd-acct {
|
|
color: rgba(0, 0, 0, 0.55);
|
|
}
|
|
.vd-side {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
}
|
|
.vd-side--dr {
|
|
background: #fee2e2;
|
|
color: #b91c1c;
|
|
}
|
|
.vd-side--cr {
|
|
background: #dcfce7;
|
|
color: #15803d;
|
|
}
|
|
.vd-desc-row td {
|
|
padding-top: 0;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
}
|
|
.vd-desc {
|
|
font-size: 12px;
|
|
color: rgba(0, 0, 0, 0.5);
|
|
font-style: italic;
|
|
}
|
|
.vd-empty {
|
|
text-align: center;
|
|
color: rgba(0, 0, 0, 0.5);
|
|
padding: 16px 8px;
|
|
}
|
|
.vd-table tfoot td {
|
|
font-weight: 600;
|
|
border-bottom: none;
|
|
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
|
}
|
|
.vd-related {
|
|
border-top: 1px dashed rgba(0, 0, 0, 0.15);
|
|
padding-top: 12px;
|
|
}
|
|
.vd-related__title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03em;
|
|
color: #6d28d9;
|
|
margin-bottom: 8px;
|
|
}
|
|
.vd-table--related td {
|
|
background: #faf5ff;
|
|
}
|
|
.vd-related__hint {
|
|
margin: 6px 0 0;
|
|
font-size: 12px;
|
|
color: rgba(0, 0, 0, 0.5);
|
|
}
|
|
.vd-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
}
|
|
`
|
|
]
|
|
})
|
|
export class VoucherDetailDialogComponent {
|
|
dialogRef = inject(MatDialogRef<VoucherDetailDialogComponent>);
|
|
private data = inject<VoucherDetailDialogData>(MAT_DIALOG_DATA);
|
|
|
|
detail = this.data.detail;
|
|
}
|