done
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
|
||||
<div class="pg">
|
||||
<header class="pg-head vv-no-print">
|
||||
<div>
|
||||
<div class="pg-eyebrow">CASHIER · VOUCHER</div>
|
||||
<h1 class="pg-title">Voucher</h1>
|
||||
<p class="pg-sub">
|
||||
Look up a voucher by its number to see the entry behind it — which accounts were debited, which were credited,
|
||||
and for how much.
|
||||
</p>
|
||||
</div>
|
||||
<div class="pg-toolbar">
|
||||
<label class="pg-field">
|
||||
<span>Voucher No</span>
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="voucherNo"
|
||||
(keyup.enter)="search()"
|
||||
placeholder="CR/2708/0001"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</label>
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="search()" [disabled]="loading">
|
||||
<mat-icon>search</mat-icon> Find
|
||||
</button>
|
||||
@if (voucher) {
|
||||
<button type="button" class="pg-btn" (click)="print()">
|
||||
<mat-icon>print</mat-icon> Print
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@if (error) {
|
||||
<div class="pg-alert pg-alert--error vv-no-print">{{ error }}</div>
|
||||
}
|
||||
|
||||
@if (loading) {
|
||||
<div class="pg-empty vv-no-print">Loading…</div>
|
||||
}
|
||||
|
||||
@if (voucher && !loading) {
|
||||
<div class="pg-card vv-slip">
|
||||
<div class="vv-slip-head">
|
||||
<div>
|
||||
<div class="vv-no">{{ voucher.voucherNo }}</div>
|
||||
<div class="vv-meta">
|
||||
{{ voucher.voucherType || '—' }}
|
||||
@if (voucher.branchName) {
|
||||
· {{ voucher.branchName }}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="vv-slip-head-right">
|
||||
<div class="vv-amount">₹{{ voucher.amount | number: '1.2-2' }}</div>
|
||||
<div class="vv-meta">
|
||||
@if (voucher.createdAt) {
|
||||
{{ voucher.createdAt | date: 'dd MMM yyyy, HH:mm' }}
|
||||
}
|
||||
@if (voucher.createdBy) {
|
||||
· {{ voucher.createdBy }}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (voucher.verificationStatus) {
|
||||
<div class="vv-status" [class.vv-status--exception]="voucher.verificationStatus === 'EXCEPTION'">
|
||||
<mat-icon>{{ voucher.verificationStatus === 'EXCEPTION' ? 'error_outline' : 'verified' }}</mat-icon>
|
||||
{{ voucher.verificationStatus === 'EXCEPTION' ? 'Exception raised against this voucher' : 'Verified against the slip' }}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- A voucher raised before its sides were recorded: the number is valid, the posting behind it
|
||||
simply was never captured, so it is shown as an older record rather than as an error. -->
|
||||
@if (hasNoLines) {
|
||||
<div class="pg-empty">
|
||||
No debit or credit detail was recorded for this voucher. It was raised before the entry sides were kept.
|
||||
</div>
|
||||
} @else {
|
||||
<table class="pg-table vv-lines">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Particulars</th>
|
||||
<th>Account</th>
|
||||
<th class="num">Debit</th>
|
||||
<th class="num">Credit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (line of debitLines; track line.lineNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<div class="vv-party">{{ line.partyName || '—' }}</div>
|
||||
@if (line.description) {
|
||||
<div class="vv-desc">{{ line.description }}</div>
|
||||
}
|
||||
</td>
|
||||
<td>{{ line.accountNo || '—' }}</td>
|
||||
<td class="num">{{ line.amount | number: '1.2-2' }}</td>
|
||||
<td class="num">—</td>
|
||||
</tr>
|
||||
}
|
||||
@for (line of creditLines; track line.lineNo) {
|
||||
<tr>
|
||||
<td class="vv-indent">
|
||||
<div class="vv-party">{{ line.partyName || '—' }}</div>
|
||||
@if (line.description) {
|
||||
<div class="vv-desc">{{ line.description }}</div>
|
||||
}
|
||||
</td>
|
||||
<td>{{ line.accountNo || '—' }}</td>
|
||||
<td class="num">—</td>
|
||||
<td class="num">{{ line.amount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="2">Total</td>
|
||||
<td class="num">{{ voucher.totalDebit | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ voucher.totalCredit | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<!-- Stated rather than assumed: a voucher whose sides were written before the balance check
|
||||
existed, or repaired by hand, should read as unbalanced rather than pass as a valid slip. -->
|
||||
@if (!voucher.balanced) {
|
||||
<div class="pg-alert pg-alert--error">
|
||||
This voucher does not balance — debits and credits differ. Refer it to accounts before relying on it.
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
/* Page chrome and the table come from cashier-shared.scss. Only the slip itself is defined here. */
|
||||
|
||||
.vv-slip {
|
||||
max-width: 860px;
|
||||
}
|
||||
|
||||
.vv-slip-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding-bottom: 14px;
|
||||
margin-bottom: 14px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.vv-slip-head-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.vv-no {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.4px;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.vv-amount {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.vv-meta {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.vv-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 14px;
|
||||
background: #ecfdf5;
|
||||
border: 1px solid #a7f3d0;
|
||||
border-radius: 8px;
|
||||
color: #065f46;
|
||||
font-size: 12px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* An exception is a warning about this voucher, not a state of it — it has to stop the eye. */
|
||||
&.vv-status--exception {
|
||||
background: #fef2f2;
|
||||
border-color: #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
}
|
||||
|
||||
.vv-lines {
|
||||
/* Credits sit under their debits the way they do on a written slip, so the indent carries the
|
||||
relationship that the DR/CR columns alone would leave to the reader to work out. */
|
||||
.vv-indent {
|
||||
padding-left: 28px;
|
||||
}
|
||||
|
||||
.vv-party {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.vv-desc {
|
||||
margin-top: 2px;
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
tfoot td {
|
||||
font-weight: 600;
|
||||
border-top: 2px solid #e5e7eb;
|
||||
}
|
||||
}
|
||||
|
||||
/* Printing a voucher should put the slip on paper and nothing else — the search box and buttons are
|
||||
how you reached it, not part of it. */
|
||||
@media print {
|
||||
.vv-no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.vv-slip {
|
||||
max-width: none;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 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 { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { catchError, of } from 'rxjs';
|
||||
import { CashierService } from '../cashier.service';
|
||||
|
||||
/** One side of a voucher: a debit or a credit against a single account. */
|
||||
interface VoucherLine {
|
||||
lineNo: number;
|
||||
entryType: 'DR' | 'CR';
|
||||
glAccountId: number | null;
|
||||
savingsAccountId: number | null;
|
||||
loanAccountId: number | null;
|
||||
partyName: string | null;
|
||||
accountNo: string | null;
|
||||
amount: number;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
interface Voucher {
|
||||
voucherNo: string;
|
||||
voucherType: string | null;
|
||||
referenceType: string | null;
|
||||
referenceId: number | null;
|
||||
amount: number | null;
|
||||
branchOfficeId: number | null;
|
||||
branchName: string | null;
|
||||
createdBy: string | null;
|
||||
createdAt: string | null;
|
||||
verificationStatus: string | null;
|
||||
lines: VoucherLine[];
|
||||
totalDebit: number;
|
||||
totalCredit: number;
|
||||
balanced: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* One voucher, as a slip: which accounts were debited, which credited, and for how much.
|
||||
*
|
||||
* Looked up by voucher number rather than listed, because that is how a voucher is reached — from a
|
||||
* paper slip in someone's hand, or from a number quoted on a statement.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-voucher-view',
|
||||
templateUrl: './voucher-view.component.html',
|
||||
styleUrls: [
|
||||
'../cashier-shared.scss',
|
||||
'./voucher-view.component.scss'
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatIconModule
|
||||
]
|
||||
})
|
||||
export class VoucherViewComponent {
|
||||
private service = inject(CashierService);
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
|
||||
voucherNo = '';
|
||||
voucher: Voucher | null = null;
|
||||
loading = false;
|
||||
/** Shown in place of the slip — a bad number and a server error read differently to the user. */
|
||||
error: string | null = null;
|
||||
|
||||
constructor() {
|
||||
// A voucher number in the URL means someone arrived from a link or a statement rather than by
|
||||
// typing, so the slip is fetched straight away instead of showing them an empty search box.
|
||||
const fromUrl = this.route.snapshot.queryParamMap.get('voucherNo');
|
||||
if (fromUrl) {
|
||||
this.voucherNo = fromUrl;
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
|
||||
search(): void {
|
||||
const trimmed = (this.voucherNo || '').trim();
|
||||
if (!trimmed) {
|
||||
this.error = 'Enter a voucher number.';
|
||||
this.voucher = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
// Kept in the URL so the slip can be linked to and survives a refresh.
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { voucherNo: trimmed },
|
||||
replaceUrl: true
|
||||
});
|
||||
|
||||
this.service
|
||||
.voucherDetail(trimmed)
|
||||
.pipe(
|
||||
catchError((err: any) => {
|
||||
this.error =
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ??
|
||||
err?.error?.developerMessage ??
|
||||
`Voucher ${trimmed} could not be loaded.`;
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
.subscribe((res: Voucher | null) => {
|
||||
this.loading = false;
|
||||
this.voucher = res;
|
||||
});
|
||||
}
|
||||
|
||||
/** Debits first, then credits — the order a slip is read in. */
|
||||
get debitLines(): VoucherLine[] {
|
||||
return (this.voucher?.lines ?? []).filter((l) => l.entryType === 'DR');
|
||||
}
|
||||
|
||||
get creditLines(): VoucherLine[] {
|
||||
return (this.voucher?.lines ?? []).filter((l) => l.entryType === 'CR');
|
||||
}
|
||||
|
||||
/**
|
||||
* A voucher raised before its sides were recorded. The voucher is real and its number valid — only
|
||||
* the posting behind it was never captured — so this reads as an older record, not a broken one.
|
||||
*/
|
||||
get hasNoLines(): boolean {
|
||||
return !!this.voucher && this.voucher.lines.length === 0;
|
||||
}
|
||||
|
||||
print(): void {
|
||||
window.print();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user