- Already holds
- ₹{{ glTarget.toLocaleString('en-IN') }}
- — count the drawer to exactly this amount.
+ This branch has already opened today's business day — counting the drawer again here would only be rejected
+ on submit. Deposits, withdrawals and closing today's day happen from Day Register instead.
- } @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 (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
index 6dcf31aa0..c68d75abd 100644
--- a/src/app/accounting/migrate-cash-denomination/migrate-cash-denomination.component.scss
+++ b/src/app/accounting/migrate-cash-denomination/migrate-cash-denomination.component.scss
@@ -115,6 +115,10 @@
color: #64748b;
line-height: 1.4;
}
+
+ button {
+ margin-top: 10px;
+ }
}
.mcd-grid {
@@ -171,7 +175,11 @@
}
&__input {
- width: 48px;
+ /* Width is set inline per row (see countInputWidthCh()), growing with the digit count typed —
+ min-width here only covers the fallback before Angular has bound anything. */
+ min-width: 48px;
+ max-width: 96px;
+ transition: width 0.1s ease-out;
text-align: center;
font-variant-numeric: tabular-nums;
border: 1px solid #e2e8f0;
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
index a66fb5651..2509a0fce 100644
--- a/src/app/accounting/migrate-cash-denomination/migrate-cash-denomination.component.ts
+++ b/src/app/accounting/migrate-cash-denomination/migrate-cash-denomination.component.ts
@@ -66,6 +66,14 @@ export class MigrateCashDenominationComponent implements OnInit {
glStatus: any = null;
glError = '';
+ /** Set once per office change: whether TODAY's business day is already open for this office. This
+ * screen is a one-time "day 1" tool (see the class doc) - once a day has actually been opened,
+ * reopening the same one is exactly what BranchDayRegisterApiResource#migrateCashDenomination
+ * itself refuses (its own openDayInternal call rejects a day that is already open), so this is
+ * checked here too, before the user counts an entire drawer only to have the submit rejected. */
+ todayAlreadyOpen = false;
+ todayOpenInfo: any = null;
+
notes: Denom[] = [];
coins: Denom[] = [];
counts: Record = {};
@@ -118,6 +126,8 @@ export class MigrateCashDenominationComponent implements OnInit {
this.result = null;
this.successMsg = '';
this.errorMsg = '';
+ this.todayAlreadyOpen = false;
+ this.todayOpenInfo = null;
if (!this.officeId) return;
this.glLoading = true;
@@ -131,6 +141,15 @@ export class MigrateCashDenominationComponent implements OnInit {
this.glError = res.message || 'No cash GL account is configured for this tenant yet.';
}
});
+
+ this.service
+ .branchDayRegister(this.officeId)
+ .pipe(catchError(() => of(null)))
+ .subscribe((res: any) => {
+ const openDay = res?.openDay;
+ this.todayAlreadyOpen = !!openDay && openDay.businessDate === res?.today;
+ this.todayOpenInfo = this.todayAlreadyOpen ? openDay : null;
+ });
}
countFor(d: Denom): number {
@@ -149,6 +168,12 @@ export class MigrateCashDenominationComponent implements OnInit {
this.counts[d.id] = Math.max(0, (this.counts[d.id] || 0) + delta);
}
+ /** Widens the count box as the typed count grows more digits, so a large count (hundreds,
+ * thousands) is never clipped or hard to read inside a field sized for one or two digits. */
+ countInputWidthCh(d: Denom): number {
+ return Math.max(3, String(this.countFor(d)).length + 2);
+ }
+
get countedTotal(): number {
return this.allDenoms.reduce((s, d) => s + this.subtotalFor(d), 0);
}
diff --git a/src/app/accounting/migrate-opening-balances/migrate-opening-balances.component.html b/src/app/accounting/migrate-opening-balances/migrate-opening-balances.component.html
index 728b7cf77..ac06c467a 100644
--- a/src/app/accounting/migrate-opening-balances/migrate-opening-balances.component.html
+++ b/src/app/accounting/migrate-opening-balances/migrate-opening-balances.component.html
@@ -162,7 +162,18 @@
diff --git a/src/app/accounting/migrate-opening-balances/migrate-opening-balances.component.scss b/src/app/accounting/migrate-opening-balances/migrate-opening-balances.component.scss
index 14cf2ca5a..578f9a0ee 100644
--- a/src/app/accounting/migrate-opening-balances/migrate-opening-balances.component.scss
+++ b/src/app/accounting/migrate-opening-balances/migrate-opening-balances.component.scss
@@ -219,6 +219,33 @@
word-break: break-word;
}
+ /* Cash at Vault / Cash at Teller — locked, see MigrateOpeningBalancesComponent.isDenominationManaged. */
+ &__lock {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ margin-left: 8px;
+ padding: 1px 8px 1px 6px;
+ border-radius: 999px;
+ background: #fff7ed;
+ color: #9a3412;
+ font-size: 11px;
+ font-weight: 600;
+ white-space: nowrap;
+ vertical-align: middle;
+
+ mat-icon {
+ font-size: 13px;
+ width: 13px;
+ height: 13px;
+ }
+
+ a {
+ color: inherit;
+ text-decoration: underline;
+ }
+ }
+
&__amt {
text-align: right;
}
diff --git a/src/app/accounting/migrate-opening-balances/migrate-opening-balances.component.ts b/src/app/accounting/migrate-opening-balances/migrate-opening-balances.component.ts
index b1636d9df..0a4bcf291 100644
--- a/src/app/accounting/migrate-opening-balances/migrate-opening-balances.component.ts
+++ b/src/app/accounting/migrate-opening-balances/migrate-opening-balances.component.ts
@@ -17,6 +17,7 @@ import {
} from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { MatIconModule } from '@angular/material/icon';
+import { MatTooltipModule } from '@angular/material/tooltip';
/** Custom Services */
import { AccountingService } from '../accounting.service';
@@ -43,7 +44,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
...STANDALONE_SHARED_IMPORTS,
GlAccountDisplayComponent,
CurrencyPipe,
- MatIconModule
+ MatIconModule,
+ MatTooltipModule
]
})
export class MigrateOpeningBalancesComponent implements OnInit, AfterViewInit {
@@ -74,6 +76,13 @@ export class MigrateOpeningBalancesComponent implements OnInit, AfterViewInit {
debitsSum = 0;
/** Sum total of credits. */
creditsSum = 0;
+ /** GL account ids mapped to Financial Activity 101 (Cash at Vault) / 102 (Cash at Teller) — the
+ * account(s) Migrate Cash Denomination owns. Their opening balance is shown here for visibility
+ * but locked against editing: redefining it as a bare number here, instead of through a counted
+ * denomination breakdown, would let the GL figure drift away from what is physically in the till.
+ * The backend enforces the same rule independently (see defineOpeningBalance's
+ * validateNoDenominationManagedCashAccounts) — this is a convenience, not the source of truth. */
+ denominationManagedGlAccountIds: number[] = [];
/* Reference of search form */
@ViewChild('searchFormRef') searchFormRef: ElementRef;
@@ -95,6 +104,22 @@ export class MigrateOpeningBalancesComponent implements OnInit, AfterViewInit {
this.officeData = data.offices;
this.currencyData = data.currencies.selectedCurrencyOptions;
});
+ // Office-independent: which GL account(s) this tenant has mapped to Cash at Vault / Cash at
+ // Teller, so their row can be locked the moment the office's accounts are drawn.
+ this.accountingService.getFinancialActivityAccounts().subscribe((mappings: any[]) => {
+ this.denominationManagedGlAccountIds = (mappings || [])
+ .filter((mapping) => mapping.financialActivityData?.id === 101 || mapping.financialActivityData?.id === 102)
+ .map((mapping) => mapping.glAccountData?.id)
+ .filter((id) => id != null);
+ // Retrieve may already have run before this (fast) request came back — lock any row it
+ // missed rather than leaving it editable for the rest of this visit.
+ this.glAccountEntries?.controls.forEach((control) => {
+ const glAccountId = control.get('glAccountId')?.value;
+ if (this.isDenominationManaged(glAccountId) && control.enabled) {
+ control.disable();
+ }
+ });
+ });
}
/**
@@ -136,14 +161,31 @@ export class MigrateOpeningBalancesComponent implements OnInit, AfterViewInit {
* @returns {FormGroup} GL Account entry form.
*/
createGLAccountEntryForm(glAccount: any): UntypedFormGroup {
- return this.formBuilder.group(
+ // A never-touched account arrives as a placeholder row (amount/entryType both null); one that
+ // already carries an opening balance arrives with its real, committed figure — show that
+ // instead of leaving the field looking blank.
+ const existingDebit = glAccount.entryType?.value === 'DEBIT' ? glAccount.amount : null;
+ const existingCredit = glAccount.entryType?.value === 'CREDIT' ? glAccount.amount : null;
+ const group = this.formBuilder.group(
{
glAccountId: [glAccount.glAccountId],
- debit: [null],
- credit: [null]
+ debit: [existingDebit],
+ credit: [existingCredit]
},
{ validator: onlyOneOfTheFieldsIsRequiredValidator }
);
+ if (this.isDenominationManaged(glAccount.glAccountId)) {
+ // Disabling excludes this row from the form's own .value, so it is never resubmitted here —
+ // the account keeps whatever Migrate Cash Denomination last set it to.
+ group.disable();
+ }
+ return group;
+ }
+
+ /** Whether this GL account is Cash at Vault / Cash at Teller — locked here, see
+ * denominationManagedGlAccountIds. */
+ isDenominationManaged(glAccountId: number): boolean {
+ return this.denominationManagedGlAccountIds.includes(glAccountId);
}
/**
@@ -176,17 +218,30 @@ export class MigrateOpeningBalancesComponent implements OnInit, AfterViewInit {
this.openingBalancesData = openingBalancesData;
- entry.valueChanges.subscribe(() => {
- this.debitsSum = 0;
- this.creditsSum = 0;
- entry.controls.forEach((value) => {
- this.debitsSum += value.value.debit;
- this.creditsSum += value.value.credit;
- });
- });
+ entry.valueChanges.subscribe(() => this.recomputeSums());
+ // Runs once immediately too: valueChanges only fires on a later edit, and the locked cash
+ // row's own prefilled figure (set once, above, never through a further value change) would
+ // otherwise never be reflected in the banner at all.
+ this.recomputeSums();
});
}
+ /** Total Debit / Total Credit shown in the balance banner. A locked (denomination-managed) row is
+ * excluded here exactly as it is excluded from the form's own .value used at submit time — this
+ * screen only needs to balance the accounts it is itself about to (re)define, not the one
+ * Migrate Cash Denomination already settled and this form can never touch. */
+ private recomputeSums(): void {
+ this.debitsSum = 0;
+ this.creditsSum = 0;
+ this.glAccountEntries.controls.forEach((control) => {
+ if (control.disabled) {
+ return;
+ }
+ this.debitsSum += control.value.debit || 0;
+ this.creditsSum += control.value.credit || 0;
+ });
+ }
+
/**
* Submits the opening balances form and defines opening balances,
* if successful redirects to view created transaction.
diff --git a/src/app/accounting/search-journal-entry/search-journal-entry.component.html b/src/app/accounting/search-journal-entry/search-journal-entry.component.html
index f8dacebff..99e81b425 100644
--- a/src/app/accounting/search-journal-entry/search-journal-entry.component.html
+++ b/src/app/accounting/search-journal-entry/search-journal-entry.component.html
@@ -53,10 +53,13 @@
Refresh
-
+
+ @if (!disabledFeatures.createJournalEntries) {
+
+ }
diff --git a/src/app/accounting/search-journal-entry/search-journal-entry.component.ts b/src/app/accounting/search-journal-entry/search-journal-entry.component.ts
index 39c8e1694..cf8330511 100644
--- a/src/app/accounting/search-journal-entry/search-journal-entry.component.ts
+++ b/src/app/accounting/search-journal-entry/search-journal-entry.component.ts
@@ -14,6 +14,7 @@ import { ActivatedRoute, RouterLink } from '@angular/router';
import { merge } from 'rxjs';
import { tap, startWith, map, distinctUntilChanged, debounceTime } from 'rxjs/operators';
import { AccountingService } from '../accounting.service';
+import { DISABLED_ACCOUNTING_FEATURES } from '../disabled-features';
import { SettingsService } from 'app/settings/settings.service';
import { JournalEntriesDataSource } from './journal-entry.datasource';
import { Dates } from 'app/core/utils/dates';
@@ -90,6 +91,8 @@ export class SearchJournalEntryComponent implements OnInit, OnDestroy, AfterView
// ── Misc ──────────────────────────────────────────────────────────────────
minDate = new Date(2000, 0, 1);
maxDate = new Date();
+ /** Accounting features switched off for this installation — see disabled-features.ts. */
+ disabledFeatures = DISABLED_ACCOUNTING_FEATURES;
// ── Filter controls ───────────────────────────────────────────────────────
officeName = new UntypedFormControl();
diff --git a/src/app/cashier/branch-day/branch-day.component.html b/src/app/cashier/branch-day/branch-day.component.html
index e5db06861..65d27eb6e 100644
--- a/src/app/cashier/branch-day/branch-day.component.html
+++ b/src/app/cashier/branch-day/branch-day.component.html
@@ -49,6 +49,18 @@
+ lock
+
+ This branch has never opened a business day, and its Cash at Vault GL account has no opening balance yet. Count
+ the vault and open the first business day together from
+ Migrate Cash Denomination — Open Day here is
+ locked until that is done, so today's cash tracking never starts out of step with the ledger.
+
+
- @if (d.cashDifference == null) {
+ @if (!hasValue(d.cashDifference)) {
—
} @else if (d.cashDifference === 0) {
Tallied
diff --git a/src/app/cashier/branch-day/branch-day.component.scss b/src/app/cashier/branch-day/branch-day.component.scss
index fa49ed69d..c97aebcc9 100644
--- a/src/app/cashier/branch-day/branch-day.component.scss
+++ b/src/app/cashier/branch-day/branch-day.component.scss
@@ -30,6 +30,36 @@
}
}
+/* Blocks Open Day until the branch's first-ever open has gone through Migrate Cash Denomination —
+ see BranchDayComponent.checkFirstOpenCashMigration(). Same shape as .ve-note, in the error palette
+ the rest of this screen already uses for a missed close (.bd-missed). */
+.bd-locked {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ padding: 10px 14px;
+ margin-bottom: 14px;
+ background: #fef2f2;
+ border: 1px solid #fecaca;
+ border-radius: 8px;
+ color: #b91c1c;
+ font-size: 12px;
+ line-height: 1.5;
+
+ mat-icon {
+ flex: none;
+ font-size: 18px;
+ width: 18px;
+ height: 18px;
+ }
+
+ a {
+ color: inherit;
+ font-weight: 600;
+ text-decoration: underline;
+ }
+}
+
.bd-success {
padding: 10px 14px;
margin-bottom: 14px;
diff --git a/src/app/cashier/branch-day/branch-day.component.ts b/src/app/cashier/branch-day/branch-day.component.ts
index 579a3364a..da476f658 100644
--- a/src/app/cashier/branch-day/branch-day.component.ts
+++ b/src/app/cashier/branch-day/branch-day.component.ts
@@ -10,7 +10,7 @@ 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 } from '@angular/router';
+import { ActivatedRoute, RouterLink } from '@angular/router';
import { catchError, forkJoin, map, of, switchMap } from 'rxjs';
import { CashierService } from '../cashier.service';
@@ -61,7 +61,8 @@ interface BranchDay {
imports: [
CommonModule,
FormsModule,
- MatIconModule
+ MatIconModule,
+ RouterLink
]
})
export class BranchDayComponent implements OnInit {
@@ -85,6 +86,12 @@ export class BranchDayComponent implements OnInit {
loaded = false;
errorMsg = '';
successMsg = '';
+ /** True only for a branch that has never opened a business day and whose Cash at Vault / Cash at
+ * Teller GL account has never had anything posted to it — Open Day here would let day-to-day cash
+ * tracking begin while the ledger silently stays wherever it already was. The backend enforces
+ * this independently too (see BranchDayRegisterApiResource#requireCashMigrationBeforeFirstEverOpen);
+ * this flag only steers the user to the right screen before they hit that rejection. */
+ firstOpenNeedsCashMigration = false;
// Open-day form
showOpenForm = false;
@@ -202,6 +209,7 @@ export class BranchDayComponent implements OnInit {
this.today = res?.today ?? '';
this.canOperate = !!res?.canOperate;
this.openDate = this.today;
+ this.checkFirstOpenCashMigration();
});
this.service
@@ -212,9 +220,42 @@ export class BranchDayComponent implements OnInit {
});
}
+ /**
+ * Only relevant the moment this branch has never opened a business day before — every later
+ * open/close on this screen is untouched, so there is nothing to re-check once history exists.
+ */
+ private checkFirstOpenCashMigration(): void {
+ if (this.days.length > 0 || this.openDay) {
+ this.firstOpenNeedsCashMigration = false;
+ return;
+ }
+ this.service.cashGlStatus().subscribe({
+ next: (status: any) => {
+ // configured === false means this tenant has no Cash at Vault / Cash at Teller account
+ // mapped at all — nothing for this check to reconcile against, so it does not block.
+ this.firstOpenNeedsCashMigration = status?.configured !== false && !status?.hasBalance;
+ },
+ error: () => {
+ this.firstOpenNeedsCashMigration = false;
+ }
+ });
+ }
+
/** True when the branch has no open day at all, so today can be opened. */
get canOpenToday(): boolean {
- return this.canOperate && !this.openDay;
+ return this.canOperate && !this.openDay && !this.firstOpenNeedsCashMigration;
+ }
+
+ /**
+ * A still-open day's closing/book figures are dropped from the API response entirely (the
+ * backend's Gson instance omits a null map value rather than sending it as JSON null), so the
+ * field arrives as undefined, not null. Checking only `!== null` against that — which a stricter
+ * pass of this file made the mistake of doing — leaves undefined on the "has a value" branch,
+ * which then renders `'₹' + null` (the number pipe's own null output, string-concatenated) as the
+ * literal text "₹null". This checks for either, the way an open day actually shows up.
+ */
+ hasValue(value: number | null | undefined): boolean {
+ return value !== null && value !== undefined;
}
startOpen(): void {
diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.ts b/src/app/cashier/transaction-entry/transaction-entry.component.ts
index 5a49e956d..6f33ad242 100644
--- a/src/app/cashier/transaction-entry/transaction-entry.component.ts
+++ b/src/app/cashier/transaction-entry/transaction-entry.component.ts
@@ -3189,7 +3189,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
// Step 1: search on the correct endpoint for this account type
this.http
.get(
- `${cfg.list}?${cfg.listQueryParam}=${encodeURIComponent(acNo)}${cfg.supportsPagination ? `&offset=0&limit=${cfg.maxLimit}` : ''}`
+ `${cfg.list}?${cfg.listQueryParam}=${encodeURIComponent(acNo)}${cfg.supportsPagination ? `&offset=0&limit=${cfg.maxLimit}` : ''}${cfg.extraListQueryParams ? `&${cfg.extraListQueryParams}` : ''}`
)
.pipe(catchError(() => of(null)))
.subscribe((res: any) => {
@@ -3278,6 +3278,10 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
matchField: string;
detailParams: string;
supportsPagination: boolean;
+ /** Extra fixed query params (e.g. "usage=1") appended to every list-search request for this
+ * account type — used to keep Header GL accounts out of the results a clerk/cashier can pick
+ * for a transaction; a Header can never receive a posting, so it should never be offered. */
+ extraListQueryParams?: string;
} {
const t = acTyp?.toUpperCase();
if (t === 'LA') {
@@ -3331,7 +3335,10 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
listQueryParam: 'searchParam',
matchField: 'glCode',
detailParams: '?fetchRunningBalance=true',
- supportsPagination: false
+ supportsPagination: false,
+ // Detail-only: a Header account is a pure grouping node and can never receive a
+ // transaction, so it must never be a selectable match here in the first place.
+ extraListQueryParams: 'usage=1'
};
}
if (t === 'CA' || t === 'CB') {
diff --git a/src/app/clients/edit-client/contact-otp-block/contact-otp-block.component.ts b/src/app/clients/edit-client/contact-otp-block/contact-otp-block.component.ts
index 7d01f68d9..99cbd5959 100644
--- a/src/app/clients/edit-client/contact-otp-block/contact-otp-block.component.ts
+++ b/src/app/clients/edit-client/contact-otp-block/contact-otp-block.component.ts
@@ -164,6 +164,13 @@ export class ContactOtpBlockComponent implements OnChanges, OnDestroy {
request$.subscribe({
next: (res: any) => {
this.sending = false;
+ // The backend reports failure to actually dispatch the OTP (gateway error, flagged
+ // number, SMTP failure...) as a 200 response with success:false and a real reason in
+ // `message` — never a thrown error. Surface that reason instead of treating it as sent.
+ if (res?.success === false) {
+ this.errorMessage = res?.message || 'Could not send OTP.';
+ return;
+ }
this.otpId = res?.otpId ?? res?.resourceId ?? null;
this.otpSent = true;
this.clearAllDigits();
diff --git a/src/app/core/shell/sidenav/sidenav.component.html b/src/app/core/shell/sidenav/sidenav.component.html
index 95778bb7c..a8ae77c84 100644
--- a/src/app/core/shell/sidenav/sidenav.component.html
+++ b/src/app/core/shell/sidenav/sidenav.component.html
@@ -299,32 +299,38 @@
{{ 'labels.menus.Notifications' | translate }}
-
-
-
-
- {{ 'labels.menus.Frequent Postings' | translate }}
-
-
-
-
-
- {{ 'labels.menus.Create Journal Entry' | translate }}
-
+
+ @if (!disabledFeatures.frequentPostings) {
+
+
+
+
+ {{ 'labels.menus.Frequent Postings' | translate }}
+
+ }
+ @if (!disabledFeatures.createJournalEntries) {
+
+
+
+
+ {{ 'labels.menus.Create Journal Entry' | translate }}
+
+ }
Holiday Calendar
- Financial Year / Closing Entries
+
+ @if (!disabledFeatures.closingEntries) {
+ Financial Year / Closing Entries
+ }
Business Date (Day Register)
diff --git a/src/app/organization/institution-profile/institution-profile.component.ts b/src/app/organization/institution-profile/institution-profile.component.ts
index 41e8b41e5..2d04f9480 100644
--- a/src/app/organization/institution-profile/institution-profile.component.ts
+++ b/src/app/organization/institution-profile/institution-profile.component.ts
@@ -12,6 +12,7 @@ import { catchError, of } from 'rxjs';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { OrganizationService } from '../organization.service';
import { SettingsService } from 'app/settings/settings.service';
+import { DISABLED_ACCOUNTING_FEATURES } from '../../accounting/disabled-features';
const BANK_TYPES = [
{ value: 'COOPERATIVE', label: 'Co-operative Bank' },
@@ -63,6 +64,8 @@ export class InstitutionProfileComponent implements OnInit {
private organizationService = inject(OrganizationService);
private settingsService = inject(SettingsService);
+ /** Accounting features switched off for this installation — see accounting/disabled-features.ts. */
+ disabledFeatures = DISABLED_ACCOUNTING_FEATURES;
bankTypes = BANK_TYPES;
ownershipTypes = OWNERSHIP_TYPES;
licenseStatuses = LICENSE_STATUSES;
diff --git a/src/app/platform-admin/menu-access/menu-access.component.ts b/src/app/platform-admin/menu-access/menu-access.component.ts
index e98bcfebd..c5607a687 100644
--- a/src/app/platform-admin/menu-access/menu-access.component.ts
+++ b/src/app/platform-admin/menu-access/menu-access.component.ts
@@ -104,7 +104,8 @@ export class PlatformMenuAccessComponent {
'X-Api-Key': this.apiKey,
// Any real tenant satisfies the platform's tenant-resolution filter; the target tenant for
// read/write is this header's own value, so it is set to whichever tenant is actually in play.
- 'Fineract-Platform-TenantId': tenantForFilter || this.selectedTenant || (this.tenants[0]?.identifier ?? 'default')
+ 'Fineract-Platform-TenantId':
+ tenantForFilter || this.selectedTenant || this.tenants[0]?.identifier || environment.fineractPlatformTenantId
};
}
@@ -126,7 +127,9 @@ export class PlatformMenuAccessComponent {
this.rememberKey();
this.loadingTenants = true;
try {
- const res = await fetch(this.apiUrl('/tenant-menu-access/tenants'), { headers: this.headers('default') });
+ const res = await fetch(this.apiUrl('/tenant-menu-access/tenants'), {
+ headers: this.headers(environment.fineractPlatformTenantId)
+ });
if (!res.ok) {
throw new Error(
res.status === 403 || res.status === 401 ? 'Invalid or inactive API key.' : `Server error (${res.status}).`
diff --git a/src/app/products/share-products/create-share-product/create-share-product.component.html b/src/app/products/share-products/create-share-product/create-share-product.component.html
index 89efc79c2..3d5ee4a18 100644
--- a/src/app/products/share-products/create-share-product/create-share-product.component.html
+++ b/src/app/products/share-products/create-share-product/create-share-product.component.html
@@ -110,8 +110,7 @@
[shareProductsTemplate]="shareProductsTemplate"
[accountingRuleData]="accountingRuleData"
[shareProduct]="shareProduct"
- [taskPermission]="'CREATE_SHAREPRODUCT'"
- (submitEvent)="submit()"
+ [showActions]="false"
>
diff --git a/src/app/products/share-products/share-product-stepper/share-product-preview-step/share-product-preview-step.component.html b/src/app/products/share-products/share-product-stepper/share-product-preview-step/share-product-preview-step.component.html
index 91c9f9ec0..4bc274f94 100644
--- a/src/app/products/share-products/share-product-stepper/share-product-preview-step/share-product-preview-step.component.html
+++ b/src/app/products/share-products/share-product-stepper/share-product-preview-step/share-product-preview-step.component.html
@@ -250,11 +250,13 @@
-
-
-
-
+@if (showActions) {
+
+
+
+
+}
diff --git a/src/app/products/share-products/share-product-stepper/share-product-preview-step/share-product-preview-step.component.ts b/src/app/products/share-products/share-product-stepper/share-product-preview-step/share-product-preview-step.component.ts
index 1c1fc2c11..c725721bb 100644
--- a/src/app/products/share-products/share-product-stepper/share-product-preview-step/share-product-preview-step.component.ts
+++ b/src/app/products/share-products/share-product-stepper/share-product-preview-step/share-product-preview-step.component.ts
@@ -52,6 +52,10 @@ export class ShareProductPreviewStepComponent {
@Input() accountingRuleData: any;
@Input() shareProduct: any;
@Input() taskPermission: string;
+ /** Set to false when the host page already renders its own Cancel/Submit bar for this step
+ * (the Create flow's shared bottom action bar) — Edit's stepper has no such bar of its own, so
+ * this defaults to true and keeps rendering the button there. */
+ @Input() showActions = true;
@Output() submitEvent = new EventEmitter();
marketPriceDisplayedColumns: string[] = [
diff --git a/src/app/savings/create-savings-account/create-savings-account.component.html b/src/app/savings/create-savings-account/create-savings-account.component.html
index dfbf67e7c..f90406dab 100644
--- a/src/app/savings/create-savings-account/create-savings-account.component.html
+++ b/src/app/savings/create-savings-account/create-savings-account.component.html
@@ -91,16 +91,10 @@
[savingsAccountNominees]="savingsAccountCurrencyStep?.nominees ?? []"
[submitting]="submitting"
(submitEvent)="submit()"
+ (previousEvent)="prev()"
>
}
-
-
}
diff --git a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.scss b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.scss
index cffbf47eb..8c772a802 100644
--- a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.scss
+++ b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.scss
@@ -13,17 +13,21 @@
padding: 0.5rem 0;
}
-/* Submit + Cancel row at the bottom of the preview. Previous lives in the
- parent wizard's tab action bar. */
+/* Previous, Cancel & Submit row at the bottom of the preview. */
.preview-submit-row {
display: flex;
- justify-content: flex-end;
+ justify-content: space-between;
align-items: center;
- gap: 12px;
margin-top: 18px;
padding-top: 16px;
border-top: 1px solid #f1f5f9;
+ &__right {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ }
+
button {
height: 42px !important;
border-radius: 10px !important;
diff --git a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.ts
index 880e51d90..c86dc9198 100644
--- a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.ts
+++ b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.ts
@@ -116,6 +116,9 @@ export class SavingsAccountPreviewStepComponent implements OnChanges {
/** Form submission event */
@Output() submitEvent = new EventEmitter();
+ /** Goes back a tab in the parent wizard — kept here so Previous renders in the same row as
+ * Cancel/Submit instead of a separate action bar underneath. */
+ @Output() previousEvent = new EventEmitter();
ngOnChanges(): void {
if (this.activeClientMembers?.length > 0) {
diff --git a/src/app/shares/shares-account-view/shares-account-view.component.ts b/src/app/shares/shares-account-view/shares-account-view.component.ts
index e8318cff0..84103260f 100644
--- a/src/app/shares/shares-account-view/shares-account-view.component.ts
+++ b/src/app/shares/shares-account-view/shares-account-view.component.ts
@@ -149,6 +149,13 @@ export class SharesAccountViewComponent implements OnInit {
// capital that was never issued.
if (!this.isSettled) {
this.buttonConfig.removeOption('Share Certificate');
+ } else {
+ // "Apply Collection" settles the account's ORIGINAL application funding -- a one-time
+ // step. Once collectionApplied is true there is nothing left to settle this way; any
+ // further share purchases on this account go through "Apply Additional Shares" instead,
+ // which raises and settles its own tranche. Leaving this button up after settlement invited
+ // reopening the dialog against an account with no unapplied receipt to find.
+ this.buttonConfig.removeButton('Apply Collection');
}
}
}
diff --git a/src/app/sponsor-dd/gl-settings-dialog/gl-settings-dialog.component.ts b/src/app/sponsor-dd/gl-settings-dialog/gl-settings-dialog.component.ts
index 3426b8bda..b4516b3c4 100644
--- a/src/app/sponsor-dd/gl-settings-dialog/gl-settings-dialog.component.ts
+++ b/src/app/sponsor-dd/gl-settings-dialog/gl-settings-dialog.component.ts
@@ -206,7 +206,9 @@ export class GlSettingsDialogComponent implements OnInit {
loadGlAccountsAndMapping(): void {
this.loading = true;
- this.http.get('/glaccounts?manualEntriesAllowed=true&disabled=false').subscribe({
+ // usage=1 (Detail only): these settings map real Sponsor DD transactions onto a GL account, and
+ // a Header account can never receive one, so it must never be offered here.
+ this.http.get('/glaccounts?manualEntriesAllowed=true&disabled=false&usage=1').subscribe({
next: (accounts) => {
this.glAccounts = accounts || [];
this.sponsorDDService.getGlMappings().subscribe({
diff --git a/src/assets/env.js b/src/assets/env.js
index 3b7c7551c..e7ec0e8c1 100644
--- a/src/assets/env.js
+++ b/src/assets/env.js
@@ -20,8 +20,8 @@
// Tenants the login screen offers, and which one it starts on. Every identifier here has to exist
// in the tenants table of the tenant-store database, or the platform rejects the request as an
// unknown tenant.
- window["env"]["fineractPlatformTenantId"] = 'default';
- window["env"]["fineractPlatformTenantIds"] = 'default,abc_bank,socity_bank';
+ window["env"]["fineractPlatformTenantId"] = 'lincpay';
+ window["env"]["fineractPlatformTenantIds"] = 'lincpay';
window['env']['tenantLogoUrl'] = '';
window['env']['tenantLogoUrlDark'] = '';
diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts
index cffbec166..d7885aac8 100644
--- a/src/environments/environment.prod.ts
+++ b/src/environments/environment.prod.ts
@@ -19,8 +19,8 @@ export const environment = {
version: env.mifos_x.version,
hash: env.mifos_x.hash,
// For connecting to server running elsewhere update the tenant identifier
- fineractPlatformTenantId: loadedEnv['fineractPlatformTenantId'] || 'default',
- fineractPlatformTenantIds: loadedEnv['fineractPlatformTenantIds'] || 'default',
+ fineractPlatformTenantId: loadedEnv['fineractPlatformTenantId'] || 'lincpay',
+ fineractPlatformTenantIds: loadedEnv['fineractPlatformTenantIds'] || 'lincpay',
// For connecting to others servers running elsewhere update the base API URL
baseApiUrls:
loadedEnv['fineractApiUrls'] || 'https://demo.mifos.community,https://localhost:8443,' + window.location.origin,
diff --git a/src/environments/environment.ts b/src/environments/environment.ts
index f4cf45cb8..700ff10f9 100644
--- a/src/environments/environment.ts
+++ b/src/environments/environment.ts
@@ -22,8 +22,8 @@ export const environment = {
version: env.mifos_x.version,
hash: env.mifos_x.hash,
// For connecting to server running elsewhere update the tenant identifier
- fineractPlatformTenantId: loadedEnv.fineractPlatformTenantId || 'default',
- fineractPlatformTenantIds: loadedEnv.fineractPlatformTenantIds || 'default',
+ fineractPlatformTenantId: loadedEnv.fineractPlatformTenantId || 'lincpay',
+ fineractPlatformTenantIds: loadedEnv.fineractPlatformTenantIds || 'lincpay',
// For connecting to others servers running elsewhere update the base API URL
baseApiUrls:
loadedEnv.fineractApiUrls || 'https://demo.mifos.community,https://localhost:8443,' + window.location.origin,