+
+
+
+
+ inventory_2
+
+ Count the branch's opening cash into notes and coins. If the Cash GL account has no balance yet, this count
+ defines it; if it already carries one, the count must match it exactly. Either way, this also opens the
+ branch's first business day with the same count.
+
+
+
+
+ Branch
+
+ @for (office of officeData; track office) {
+ {{ office.name }}
+ }
+
+
+
+ Business Date
+
+
+
+
+
+
+
+
+ @if (glLoading) {
+
Checking the cash GL account…
+ }
+
+ @if (glError) {
+
+
error
+
{{ glError }}
+
+ }
+
+ @if (glStatus && !glError) {
+
+
+
account_balance
+
+
Cash GL Account
+
({{ glStatus.glCode }}) {{ glStatus.glAccountName }}
+ @if (glHasBalance) {
+
+ Already holds
+ ₹{{ glTarget.toLocaleString('en-IN') }}
+ — count the drawer to exactly this amount.
+
+ } @else {
+
+ No opening balance posted yet — whatever you count below becomes this account's opening balance.
+
+ }
+
+
+
+
+
+
+
+
Denomination
+
Count
+
Amount
+
+ @for (d of allDenoms; track d.id) {
+
+
+ {{ d.label }} {{ d.kind }}
+
+
+
+
+
+
+
₹{{ subtotalFor(d).toLocaleString('en-IN') }}
+
+ }
+
+
+
+
+
+
+ Counted Total
+ ₹{{ countedTotal.toLocaleString('en-IN') }}
+
+ @if (glHasBalance) {
+
+ GL Target
+ ₹{{ glTarget.toLocaleString('en-IN') }}
+
+ }
+
+ @if (countedTotal <= 0) {
+ edit_note
+ Count each denomination above
+ } @else if (isReconciled) {
+ check_circle
+ {{ glHasBalance ? 'Matches the GL balance' : 'Will define the GL opening balance' }}
+ } @else {
+ error
+ Off by ₹{{
+ (countedTotal - glTarget < 0 ? glTarget - countedTotal : countedTotal - glTarget).toLocaleString('en-IN')
+ }}
+ }
+
+
+
+ @if (needsDifferenceReason) {
+
+
+
+ Why does the count differ from the GL balance?
+
+
+
+
+ }
+
+ @if (errorMsg) {
+
{{ errorMsg }}
+ }
+
+ @if (successMsg) {
+
+
check_circle
+
+ {{ successMsg }}
+
+
+
+
+
+ }
+
+ @if (!successMsg) {
+
+
+
+ }
+ }
+
diff --git a/src/app/accounting/migrate-cash-denomination/migrate-cash-denomination.component.scss b/src/app/accounting/migrate-cash-denomination/migrate-cash-denomination.component.scss
new file mode 100644
index 000000000..6dcf31aa0
--- /dev/null
+++ b/src/app/accounting/migrate-cash-denomination/migrate-cash-denomination.component.scss
@@ -0,0 +1,308 @@
+/**
+ * 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/.
+ */
+
+.mcd {
+ max-width: 760px;
+}
+
+.mcd-card {
+ margin-bottom: 14px;
+ border-radius: 8px;
+
+ &--flush mat-card-content {
+ padding: 0;
+ }
+}
+
+.mcd-intro {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ padding: 10px 14px;
+ margin-bottom: 14px;
+ background: #eff6ff;
+ border: 1px solid #bfdbfe;
+ border-radius: 8px;
+ color: #1e40af;
+ font-size: 12px;
+ line-height: 1.5;
+
+ mat-icon {
+ font-size: 18px;
+ width: 18px;
+ height: 18px;
+ flex: none;
+ }
+}
+
+.mcd-row {
+ display: flex;
+ align-items: flex-start;
+ gap: 16px;
+ flex-wrap: wrap;
+}
+
+.mcd-office {
+ flex: 1 1 280px;
+}
+
+.mcd-field {
+ flex: 1 1 200px;
+}
+
+.mcd-loading {
+ padding: 10px 14px;
+ margin-bottom: 14px;
+ font-size: 12px;
+ color: #64748b;
+}
+
+.mcd-gl {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ padding: 10px 14px;
+ margin-bottom: 16px;
+ background: #f8fafc;
+ border: 1px solid #e2e8f0;
+ border-left: 4px solid #0ea5e9;
+ border-radius: 8px;
+
+ mat-icon {
+ color: #0ea5e9;
+ margin-top: 2px;
+ }
+
+ &--fresh {
+ border-left-color: #f59e0b;
+
+ mat-icon {
+ color: #f59e0b;
+ }
+ }
+
+ &--error {
+ border-left-color: #dc2626;
+ background: #fef2f2;
+
+ mat-icon {
+ color: #dc2626;
+ }
+ }
+
+ &__label {
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.02em;
+ color: #64748b;
+ }
+
+ &__value {
+ font-size: 13px;
+ font-weight: 600;
+ color: #0d1b2a;
+ margin: 2px 0 4px;
+ }
+
+ &__hint {
+ font-size: 11.5px;
+ color: #64748b;
+ line-height: 1.4;
+ }
+}
+
+.mcd-grid {
+ display: grid;
+ grid-template-columns: 1fr 160px 120px;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 14px;
+
+ &--head {
+ background: #f8fafc;
+ border-bottom: 1px solid #e2e8f0;
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.02em;
+ color: #64748b;
+ }
+
+ &--row {
+ border-bottom: 1px solid #f1f5f9;
+ }
+
+ &__denom {
+ font-size: 13px;
+ font-weight: 600;
+ color: #0d1b2a;
+ }
+
+ &__kind {
+ font-size: 10px;
+ font-weight: 500;
+ color: #94a3b8;
+ margin-left: 6px;
+ text-transform: uppercase;
+ }
+
+ &__count {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+
+ button {
+ width: 28px;
+ height: 28px;
+ line-height: 28px;
+
+ mat-icon {
+ font-size: 16px;
+ width: 16px;
+ height: 16px;
+ }
+ }
+ }
+
+ &__input {
+ width: 48px;
+ text-align: center;
+ font-variant-numeric: tabular-nums;
+ border: 1px solid #e2e8f0;
+ border-radius: 4px;
+ padding: 4px 2px;
+ font-size: 13px;
+ }
+
+ &__amt {
+ font-variant-numeric: tabular-nums;
+ font-weight: 600;
+ font-size: 13px;
+ text-align: right;
+ color: #0d1b2a;
+ }
+}
+
+.mcd-balance {
+ display: flex;
+ align-items: center;
+ gap: 24px;
+ flex-wrap: wrap;
+ padding: 12px 16px;
+ margin-bottom: 14px;
+ background: #fef2f2;
+ border: 1px solid #fecaca;
+ border-left: 4px solid #dc2626;
+ border-radius: 8px;
+
+ &--ok {
+ background: #f0fdf4;
+ border-color: #bbf7d0;
+ border-left-color: #16a34a;
+ }
+
+ &__figure {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ }
+
+ &__label {
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.02em;
+ color: #64748b;
+ }
+
+ &__amt {
+ font-variant-numeric: tabular-nums;
+ font-weight: 700;
+ font-size: 16px;
+ white-space: nowrap;
+ color: #0d1b2a;
+ }
+
+ &__status {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-left: auto;
+ font-size: 13px;
+ font-weight: 600;
+ color: #b91c1c;
+
+ mat-icon {
+ font-size: 20px;
+ width: 20px;
+ height: 20px;
+ }
+ }
+
+ &--ok &__status {
+ color: #15803d;
+ }
+}
+
+.mcd-reason {
+ width: 100%;
+}
+
+.mcd-error {
+ padding: 10px 14px;
+ margin-bottom: 14px;
+ background: #fef2f2;
+ border: 1px solid #fecaca;
+ border-radius: 8px;
+ color: #b91c1c;
+ font-size: 12.5px;
+}
+
+.mcd-success {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ padding: 14px 16px;
+ margin-bottom: 14px;
+ background: #f0fdf4;
+ border: 1px solid #bbf7d0;
+ border-radius: 8px;
+ color: #14532d;
+ font-size: 13px;
+
+ mat-icon {
+ color: #16a34a;
+ margin-top: 1px;
+ }
+
+ &__actions {
+ margin-top: 10px;
+ }
+}
+
+.mcd-actions {
+ display: flex;
+ justify-content: flex-end;
+ padding-bottom: 24px;
+}
+
+@media (width <= 560px) {
+ .mcd-grid {
+ grid-template-columns: 1fr auto;
+ row-gap: 4px;
+
+ &--head {
+ display: none;
+ }
+
+ &__amt {
+ grid-column: 1 / -1;
+ text-align: left;
+ }
+ }
+}
diff --git a/src/app/accounting/migrate-cash-denomination/migrate-cash-denomination.component.ts b/src/app/accounting/migrate-cash-denomination/migrate-cash-denomination.component.ts
new file mode 100644
index 000000000..a66fb5651
--- /dev/null
+++ b/src/app/accounting/migrate-cash-denomination/migrate-cash-denomination.component.ts
@@ -0,0 +1,237 @@
+/**
+ * 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, OnInit, inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { MatIconModule } from '@angular/material/icon';
+import { ActivatedRoute, Router } from '@angular/router';
+import { catchError, of } from 'rxjs';
+import { CashierService } from '../../cashier/cashier.service';
+import { SettingsService } from 'app/settings/settings.service';
+import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
+
+interface Denom {
+ id: number;
+ value: number;
+ kind: 'note' | 'coin';
+ label: string;
+}
+
+/**
+ * Migrate Cash Denomination — the vault-side counterpart to Migrate Opening Balances.
+ *
+ * A freshly set-up institution's chart of accounts carries a "Cash at Vault" GL balance (defined via
+ * Migrate Opening Balances, or not yet defined at all), but that figure is meaningless to a cashier
+ * standing at the drawer until it is broken into actual notes and coins, and the branch has nowhere
+ * to track that breakdown until its first business day is open. This screen is the one place both of
+ * those happen together: count the drawer, and it either defines the cash GL's opening balance from
+ * that count (nothing posted to it yet) or checks the count against what is already posted, then
+ * opens the business day with that same breakdown - so a new bank goes from "GL seeded" straight to
+ * "vault countable" without a separate trip through Manage Tellers or Open Business Day first.
+ */
+@Component({
+ selector: 'mifosx-migrate-cash-denomination',
+ templateUrl: './migrate-cash-denomination.component.html',
+ styleUrls: ['./migrate-cash-denomination.component.scss'],
+ standalone: true,
+ imports: [
+ CommonModule,
+ FormsModule,
+ MatIconModule,
+ ...STANDALONE_SHARED_IMPORTS
+ ]
+})
+export class MigrateCashDenominationComponent implements OnInit {
+ private service = inject(CashierService);
+ private settingsService = inject(SettingsService);
+ private route = inject(ActivatedRoute);
+ private router = inject(Router);
+
+ officeData: any[] = [];
+ officeId: number | null = null;
+ /** Bound to the datepicker as a Date, converted to a strict yyyy-MM-dd string only when
+ * submitting — the branchday endpoints parse this with LocalDate.parse() directly, with no
+ * locale/dateFormat hint alongside it, so a locale-formatted string (what dateUtils.formatDate
+ * produces) is rejected outright. */
+ businessDate: Date = new Date();
+ maxDate = new Date();
+
+ glLoading = false;
+ glStatus: any = null;
+ glError = '';
+
+ notes: Denom[] = [];
+ coins: Denom[] = [];
+ counts: Record