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>
251 lines
10 KiB
HTML
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>
|