feat: cash-only registers, day book sections and DD amendments
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s

This commit is contained in:
2026-09-03 18:26:22 +05:30
parent 6442562df0
commit fbce73fd17
111 changed files with 9076 additions and 3507 deletions
@@ -27,12 +27,23 @@
</div>
</header>
@if (!showGl && rows.length > 0) {
<!-- Shown whenever the day has any cash at all, not only when a denomination breakdown exists:
a cash Demand Draft is recorded without one, and gating on rows hid its money entirely. -->
@if (!showGl && (rows.length > 0 || ddCashTotal > 0)) {
<!-- Summary totals bar -->
<div class="closing-summary">
<div class="closing-summary__item closing-summary__item--in">
<span class="closing-summary__label">Total Deposits (IN)</span>
<span class="closing-summary__value">₹{{ depositTotal | number: '1.2-2' }}</span>
<span class="closing-summary__value">₹{{ depositTotalWithDd | number: '1.2-2' }}</span>
@if (ddCashTotal > 0) {
<!-- Named on the tile as well as in the table: this part of the total carries no
denomination, so without saying so the figures read as failing to add up. -->
<span class="closing-summary__note">
incl. ₹{{ ddCashTotal | number: '1.2-2' }} not itemised ({{ ddCashCount }} cash DD{{
ddCashCount === 1 ? '' : 's'
}})
</span>
}
</div>
<div class="closing-summary__sep">−</div>
<div class="closing-summary__item closing-summary__item--out">
@@ -45,15 +56,57 @@
[class.closing-summary__item--net-pos]="netTotal >= 0"
[class.closing-summary__item--net-neg]="netTotal < 0"
>
<span class="closing-summary__label">Net Closing Cash</span>
<!-- Named for what it is: the counter's own movement. It was labelled "Net Closing Cash",
which it is not -- it carries nothing forward from yesterday and leaves out vault and
inter-cashier legs, so it can differ from the drawer by a large amount. The real
closing figure is in the balance strip below. -->
<span class="closing-summary__label">Net Counter Movement</span>
<span class="closing-summary__value">₹{{ netTotal | number: '1.2-2' }}</span>
</div>
</div>
}
<!-- Opening → closing, the figure a physical count is checked against. Shown only for a
cashier-scoped report; a branch-wide one has no single drawer to open and close. -->
@if (!showGl && scopedToCashier && closingBalance !== null) {
<div class="closing-summary closing-summary--balance">
<div class="closing-summary__item">
<span class="closing-summary__label">Opening Balance</span>
<span class="closing-summary__value">₹{{ openingBalance | number: '1.2-2' }}</span>
</div>
<div class="closing-summary__sep">+</div>
<div
class="closing-summary__item"
[class.closing-summary__item--in]="(cashMovement ?? 0) >= 0"
[class.closing-summary__item--out]="(cashMovement ?? 0) < 0"
>
<span class="closing-summary__label">Cash Movement</span>
<span class="closing-summary__value">₹{{ cashMovement | number: '1.2-2' }}</span>
</div>
<div class="closing-summary__sep">=</div>
<div
class="closing-summary__item"
[class.closing-summary__item--net-pos]="(closingBalance ?? 0) >= 0"
[class.closing-summary__item--net-neg]="(closingBalance ?? 0) < 0"
>
<span class="closing-summary__label">Closing Balance</span>
<span class="closing-summary__value">₹{{ closingBalance | number: '1.2-2' }}</span>
</div>
</div>
@if (cashMovement !== null && netTotal !== cashMovement) {
<!-- The gap between the two is not an error: it is the vault, inter-cashier and exchange
traffic that never crossed the counter. Said plainly, because two different "net"
figures on one screen otherwise read as a bug. -->
<p class="closing-note">
Cash movement differs from counter movement by ₹{{ cashMovement - netTotal | number: '1.2-2' }} — vault,
inter-cashier and exchange legs that did not pass over the counter.
</p>
}
}
@if (!showGl) {
<div class="pg-card">
@if (noteRows.length > 0) {
@if (noteRows.length > 0 || ddCashTotal > 0) {
<div class="pos-section-label">Notes</div>
<table class="pg-table">
<thead>
@@ -93,7 +146,42 @@
</td>
</tr>
}
<!-- Cash taken for a Demand Draft, on the deposit side where it belongs. It has no
denomination to sit against -- the notes were never itemised -- so the piece
counts show a dash rather than a zero, which would read as "no notes". -->
@if (ddCashTotal > 0) {
<tr>
<td>
<span class="pg-chip">Not itemised</span>
<span class="closing-row__hint"> {{ ddCashCount }} cash DD{{ ddCashCount === 1 ? '' : 's' }} </span>
</td>
<td class="num col-in">—</td>
<td class="num col-in">₹{{ ddCashTotal | number: '1.2-2' }}</td>
<td class="num col-out">—</td>
<td class="num col-out">₹0.00</td>
<td class="num col-net">—</td>
<td class="num col-net" style="color: #16a34a">+₹{{ ddCashTotal | number: '1.2-2' }}</td>
</tr>
}
</tbody>
<!-- Totals, so the table reconciles against the summary above rather than having to be
added up by hand — and so the deposit column visibly accounts for the DD row. -->
<tfoot>
<tr>
<td>Total</td>
<td class="num col-in">{{ noteDepositCount }}</td>
<td class="num col-in">₹{{ noteDepositAmount | number: '1.2-2' }}</td>
<td class="num col-out">{{ noteWithdrawalCount }}</td>
<td class="num col-out">₹{{ noteWithdrawalAmount | number: '1.2-2' }}</td>
<td class="num col-net"></td>
<td class="num col-net">
{{ noteNetAmount >= 0 ? '+' : '−' }}₹{{
(noteNetAmount < 0 ? -noteNetAmount : noteNetAmount) | number: '1.2-2'
}}
</td>
</tr>
</tfoot>
</table>
}
@@ -141,8 +229,20 @@
</table>
}
@if (loaded && rows.length === 0 && exchangeRows.length === 0) {
<div class="pg-empty">No cash denomination data for this date.</div>
<!-- Not shown when a Demand Draft row is on the table: there is something to read then, and
an "empty" notice under it contradicts it. -->
@if (loaded && rows.length === 0 && exchangeRows.length === 0 && ddCashTotal === 0) {
<!-- A day with no counter work is not an empty report: the cashier is still holding
whatever they held yesterday, and that balance is shown in the strip above. Saying
only "no data" here read as though the drawer were empty. -->
<div class="pg-empty">
@if (scopedToCashier && closingBalance !== null) {
No cash crossed the counter on this date. The drawer still holds ₹{{ closingBalance | number: '1.2-2' }},
carried forward.
} @else {
No cash denomination data for this date.
}
</div>
}
</div>
}
@@ -38,6 +38,38 @@ export class ClosingCashReportComponent implements OnInit {
loading = false;
loaded = false;
/**
* Cash taken at the counter for a Demand Draft.
*
* A cash DD asks for a denomination breakdown but does not require one, and this report is built
* entirely from breakdowns — so that cash was missing from the drawer figure even though the
* ledger held it and the cashier held the notes. It is counted from the request itself and shown
* on its own line, because the denomination rows below cannot account for it.
*/
ddCashTotal = 0;
ddCashCount = 0;
/** Deposits including the DD cash — the figure the drawer is actually checked against. */
depositTotalWithDd = 0;
/**
* The drawer, opening to closing.
*
* `netTotal` above is only the day's counter movement — deposits less withdrawals. It is not what
* the cashier is holding, because it carries nothing forward from yesterday and ignores vault
* allocations and inter-cashier legs. The closing balance is the figure that has to match a
* physical count at the end of the day, so it is reported separately rather than inferred from
* the counter totals.
*
* Null when the server did not scope the report to one cashier: a branch-wide opening balance is
* the branch day register's business, not this report's.
*/
openingBalance: number | null = null;
closingBalance: number | null = null;
/** Every leg of the day's cash movement, which is wider than deposits less withdrawals. */
cashMovement: number | null = null;
/** Whether the figures above are one cashier's drawer or the whole branch's counter work. */
scopedToCashier = false;
// GL View
showGl = false;
glRows: any[] = [];
@@ -66,6 +98,33 @@ export class ClosingCashReportComponent implements OnInit {
get noteRows(): any[] {
return this.rows.filter((r) => r.kind !== 'coin');
}
/**
* Totals for the notes table, including the un-itemised Demand Draft cash.
*
* The DD row carries an amount but no piece count, so pieces and amounts are summed separately
* rather than one being derived from the other — a denomination times a count would not reach the
* DD's figure, and adding a fictional count to make it balance would misstate the drawer.
*/
get noteDepositCount(): number {
return this.noteRows.reduce((s, r) => s + Number(r.depositCount ?? 0), 0);
}
get noteDepositAmount(): number {
return this.noteRows.reduce((s, r) => s + Number(r.depositAmount ?? 0), 0) + this.ddCashTotal;
}
get noteWithdrawalCount(): number {
return this.noteRows.reduce((s, r) => s + Number(r.withdrawalCount ?? 0), 0);
}
get noteWithdrawalAmount(): number {
return this.noteRows.reduce((s, r) => s + Number(r.withdrawalAmount ?? 0), 0);
}
get noteNetAmount(): number {
return this.noteDepositAmount - this.noteWithdrawalAmount;
}
get coinRows(): any[] {
return this.rows.filter((r) => r.kind === 'coin');
}
@@ -92,6 +151,15 @@ export class ClosingCashReportComponent implements OnInit {
this.depositTotal = Number(res?.depositTotal || 0);
this.withdrawalTotal = Number(res?.withdrawalTotal || 0);
this.netTotal = Number(res?.netTotal || 0);
this.ddCashTotal = Number(res?.ddCashTotal || 0);
this.ddCashCount = Number(res?.ddCashCount || 0);
this.depositTotalWithDd = Number(res?.depositTotalWithDd ?? res?.depositTotal ?? 0);
this.scopedToCashier = res?.scopedToCashier === true;
// Sent only for a cashier-scoped call, and read as absent rather than zero when missing —
// a zero opening balance and no opening balance at all are different statements.
this.openingBalance = res?.openingBalance == null ? null : Number(res.openingBalance);
this.closingBalance = res?.closingBalance == null ? null : Number(res.closingBalance);
this.cashMovement = res?.cashMovement == null ? null : Number(res.cashMovement);
this.loading = false;
this.loaded = true;
},
@@ -101,6 +169,13 @@ export class ClosingCashReportComponent implements OnInit {
this.depositTotal = 0;
this.withdrawalTotal = 0;
this.netTotal = 0;
this.ddCashTotal = 0;
this.ddCashCount = 0;
this.depositTotalWithDd = 0;
this.scopedToCashier = false;
this.openingBalance = null;
this.closingBalance = null;
this.cashMovement = null;
this.loading = false;
this.loaded = true;
}