Files
cbsnexora/src/app/cashier/voucher-report/voucher-report.component.html
T
niteshandClaude Sonnet 5.5 ab8ed7e67e feat(vouchers): voucher correction screens and a correction status on the voucher registers
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>
2026-10-15 12:28:27 +05:30

251 lines
10 KiB
HTML

<!--
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 vr-no-print">
<div>
<div class="pg-eyebrow">CASHIER · REPORT</div>
<h1 class="pg-title">{{ title }}</h1>
<p class="pg-sub">{{ subtitle }}</p>
</div>
<div class="pg-toolbar">
<label class="pg-field">
<span>Period</span>
<select [(ngModel)]="period" (change)="load()">
<option value="DAY">Day</option>
<option value="MONTH">Month</option>
</select>
</label>
<label class="pg-field">
<span>{{ period === 'DAY' ? 'Date' : 'Month of' }}</span>
<input type="date" [(ngModel)]="anchor" (change)="load()" />
</label>
@if (report?.canSeeAllUsers) {
<label class="pg-field">
<span>User</span>
<input
type="text"
[(ngModel)]="usernameFilter"
(keyup.enter)="load()"
placeholder="All users"
autocomplete="off"
/>
</label>
}
<button type="button" class="pg-btn pg-btn--primary" (click)="load()" [disabled]="loading">
<mat-icon>refresh</mat-icon> Run
</button>
@if (report && count > 0) {
<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 vr-no-print">{{ error }}</div>
}
@if (loading) {
<div class="pg-empty vr-no-print">Loading…</div>
}
@if (report && !loading) {
<div class="vr-period">
{{ report.from | date: 'dd MMM yyyy' }}
@if (report.from !== report.to) {
— {{ report.to | date: 'dd MMM yyyy' }}
}
· {{ count }} {{ noun }}{{ count === 1 ? '' : 's' }} · ₹{{ totalAmount | number: '1.2-2' }}
@if (report.username) {
· {{ report.username }}
}
<!-- Said plainly rather than left to be inferred from a missing filter: a cashier should know
this is their own work and not the branch's. -->
@if (!report.canSeeAllUsers) {
<span class="vr-scope-note">Your own vouchers</span>
}
<!-- The total on a cash register means something narrower than the whole voucher book, so the
screen says which one it is instead of leaving the figure ambiguous. -->
@if (cashOnly) {
<span class="vr-scope-note">Cash only</span>
}
</div>
<!-- A voucher with no CR/DR prefix belongs to neither report. Say so, so the two reports not
adding up to the day's total is explained rather than left to be puzzled over. -->
@if (unclassifiedCount > 0) {
<div class="pg-alert vr-no-print">
{{ unclassifiedCount }} voucher{{ unclassifiedCount === 1 ? '' : 's' }} in this period
{{ unclassifiedCount === 1 ? 'carries' : 'carry' }} no CR or DR prefix, so
{{ unclassifiedCount === 1 ? 'it appears' : 'they appear' }} on neither the Received nor the Payment report.
</div>
}
<!-- Which part of the total has notes and coins counted behind it. Shown on the cash register
as well as the full one: a voucher with no breakdown is still real money, so it stays in
the total, and what the drawer can be checked against is stated instead of assumed. -->
@if (uncountedCount > 0) {
<div class="pg-alert vr-no-print">
{{ uncountedCount }} of these {{ noun }}s {{ uncountedCount === 1 ? 'has' : 'have' }} no denomination breakdown
recorded, totalling ₹{{ uncountedTotal | number: '1.2-2' }}. Some registers pay out without counting notes
against the voucher, so these are still cash — ₹{{ countedTotal | number: '1.2-2' }} of the total has a
breakdown behind it.
</div>
}
<!-- Explains the blank Debit/Credit cells instead of leaving them looking like lost data. -->
@if (missingSidesCount > 0) {
<div class="pg-alert vr-no-print">
{{ missingSidesCount }} of these {{ noun }}s {{ missingSidesCount === 1 ? 'has' : 'have' }} no debit or credit
recorded. The sides are attached when a request is approved, so vouchers raised before that step was added keep
their number and amount but show no accounts. Newly raised {{ noun }}s carry both sides.
</div>
}
@if (count === 0) {
<div class="pg-empty">No {{ noun }}s were raised in this period.</div>
} @else {
<div class="vr-summaries">
<div class="pg-card">
<div class="pos-section-label">By type</div>
<table class="pg-table">
<thead>
<tr>
<th>Type</th>
<th class="num">Count</th>
<th class="num">Amount</th>
</tr>
</thead>
<tbody>
@for (row of byType; track row.type) {
<tr>
<td>{{ row.type }}</td>
<td class="num">{{ row.count }}</td>
<td class="num">{{ row.total | number: '1.2-2' }}</td>
</tr>
}
</tbody>
</table>
</div>
<!-- Only worth showing to someone who can see more than one person's work. -->
@if (report.canSeeAllUsers) {
<div class="pg-card">
<div class="pos-section-label">By user</div>
<table class="pg-table">
<thead>
<tr>
<th>User</th>
<th class="num">Count</th>
<th class="num">Amount</th>
</tr>
</thead>
<tbody>
@for (row of byUser; track row.username) {
<tr>
<td>{{ row.username }}</td>
<td class="num">{{ row.count }}</td>
<td class="num">{{ row.total | number: '1.2-2' }}</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
<div class="pg-card">
<div class="pos-section-label">{{ isReceived ? 'Receipts' : 'Payments' }}</div>
<table class="pg-table vr-rows">
<thead>
<tr>
<th>Voucher No</th>
<th>Type</th>
<th>Debit</th>
<th>Credit</th>
<th class="num">Amount</th>
<th>Correction</th>
<th>User</th>
<!-- Two dates, not one, on the system register.
A voucher's timestamp is the minute it was written; the posting behind it carries
the business date. A nightly run finishing after midnight writes today's voucher
for yesterday's work, so a reader opening this screen today is looking at last
night's postings — and without the ledger date they would take the timestamp for
the day the money moved. -->
@if (systemOnly) {
<th>Ledger date</th>
}
<th>Time</th>
</tr>
</thead>
<tbody>
@for (row of rows; track row.voucherNo) {
<tr>
<td>
{{ row.voucherNo }}
@if (row.verificationStatus === 'EXCEPTION') {
<mat-icon class="vr-flag" title="An exception was raised against this voucher"
>error_outline</mat-icon
>
}
</td>
<td>{{ row.voucherType || '—' }}</td>
<!-- A voucher raised before its sides were recorded has neither. Say that once,
across both cells, rather than showing two dashes that read as "nowhere". -->
@if (!hasSides(row)) {
<td colspan="2" class="vr-nosides">Sides not recorded</td>
} @else {
<td>
<div class="vr-party">{{ row.debitParty || '—' }}</div>
@if (row.debitAccount) {
<div class="vr-acct">{{ row.debitAccount }}</div>
}
</td>
<td>
<div class="vr-party">{{ row.creditParty || '—' }}</div>
@if (row.creditAccount) {
<div class="vr-acct">{{ row.creditAccount }}</div>
}
<!-- A DD with charges, or a compound journal, has sides this row does not show. -->
@if (row.extraLines > 0) {
<div class="vr-more">+{{ row.extraLines }} more on voucher</div>
}
</td>
}
<td class="num">{{ row.amount | number: '1.2-2' }}</td>
<!-- Where the voucher stands after a correction: unpassed, revoked, closed or corrected. -->
<td>
<mifosx-voucher-status
[status]="row.voucherStatus"
[revokedBy]="row.revokedByVoucherNo"
[replaces]="row.replacesVoucherNo"
[note]="row.correctionNote"
></mifosx-voucher-status>
@if (!row.voucherStatus || row.voucherStatus === 'ACTIVE') {
<span class="vr-active">—</span>
}
</td>
<td>{{ row.username || '—' }}</td>
@if (systemOnly) {
<td class="vr-ledgerdate" [class.vr-ledgerdate--prior]="row.postedAfterClose">
{{ row.ledgerDate ? (row.ledgerDate | date: 'dd MMM yyyy') : '—' }}
@if (row.postedAfterClose) {
<span class="vr-priorflag" title="Posted after that day had closed">after close</span>
}
</td>
}
<td>{{ row.createdAt | date: 'dd MMM, HH:mm' }}</td>
</tr>
}
</tbody>
</table>
</div>
}
}
</div>