feat(cashier): add GL denomination tracking, real-time balance, and enhanced closing report
- Save denomination breakdown for GL journal entries via new /cashier/gl-denomination endpoint - Add payment type, date/time, teller name columns to GL Entries closing report - Show GL badge for GL vouchers vs numeric ID for savings transactions - Add payment type required and denomination overdraft validations for GL transactions - Fix GL account number display in confirm dialog Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
010adcb44f
commit
05cbe21542
@@ -542,9 +542,21 @@
|
||||
// GL Table
|
||||
.gl-table {
|
||||
.gl-th--voucher {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.gl-th--date {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.gl-th--pt {
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
.gl-th--teller {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.gl-th--dr {
|
||||
color: #dc2626 !important;
|
||||
}
|
||||
@@ -571,6 +583,51 @@
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.gl-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.gl-date {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.gl-time {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.gl-pt-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 7px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
background: #f0fdf4;
|
||||
color: #15803d;
|
||||
border: 1px solid #bbf7d0;
|
||||
}
|
||||
|
||||
.gl-pt-none {
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.gl-teller {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.gl-amt--dr {
|
||||
color: #dc2626;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -212,8 +212,11 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="gl-th--voucher">Voucher No</th>
|
||||
<th class="gl-th--date">Date</th>
|
||||
<th>A/C No</th>
|
||||
<th>A/C Name</th>
|
||||
<th class="gl-th--pt">Payment Type</th>
|
||||
<th class="gl-th--teller">Teller</th>
|
||||
<th class="num gl-th--dr">DR (₹)</th>
|
||||
<th class="num gl-th--cr">CR (₹)</th>
|
||||
</tr>
|
||||
@@ -221,9 +224,29 @@
|
||||
<tbody>
|
||||
@for (r of glRows; track $index) {
|
||||
<tr [class.gl-row--dr]="r.dr > 0" [class.gl-row--cr]="r.cr > 0">
|
||||
<td class="gl-voucher">{{ r.voucherNo }}</td>
|
||||
<td class="gl-voucher">
|
||||
@if (r.txnSource === 'GL') {
|
||||
<span class="gl-badge">GL</span>
|
||||
} @else {
|
||||
{{ r.voucherNo }}
|
||||
}
|
||||
</td>
|
||||
<td class="gl-date">
|
||||
<div>{{ r.txnDate }}</div>
|
||||
@if (r.txnTime) {
|
||||
<div class="gl-time">{{ r.txnTime }}</div>
|
||||
}
|
||||
</td>
|
||||
<td class="gl-acno">{{ r.acNo }}</td>
|
||||
<td class="gl-acname">{{ r.acName }}</td>
|
||||
<td class="gl-pt">
|
||||
@if (r.paymentType) {
|
||||
<span class="gl-pt-badge">{{ r.paymentType }}</span>
|
||||
} @else {
|
||||
<span class="gl-pt-none">—</span>
|
||||
}
|
||||
</td>
|
||||
<td class="gl-teller">{{ r.tellerName || '—' }}</td>
|
||||
<td class="num gl-amt--dr">
|
||||
@if (r.dr > 0) {
|
||||
₹{{ r.dr | number: '1.2-2' }}
|
||||
@@ -239,7 +262,7 @@
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="gl-foot">
|
||||
<td colspan="3" class="gl-foot__label">Total</td>
|
||||
<td colspan="6" class="gl-foot__label">Total</td>
|
||||
<td class="num gl-amt--dr gl-foot__val">₹{{ glTotalDr | number: '1.2-2' }}</td>
|
||||
<td class="num gl-amt--cr gl-foot__val">₹{{ glTotalCr | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
|
||||
@@ -115,7 +115,12 @@
|
||||
</div>
|
||||
<div class="ws-f ws-f--actyp">
|
||||
<label class="ws-f__lbl" title="Account Type">AC. TP.</label>
|
||||
<select class="ws-f__ctl" [(ngModel)]="row.acTyp" (click)="$event.stopPropagation()">
|
||||
<select
|
||||
class="ws-f__ctl"
|
||||
[(ngModel)]="row.acTyp"
|
||||
(click)="$event.stopPropagation()"
|
||||
(change)="row.acTyp === 'GL' && (row.dbCr = 'Cr')"
|
||||
>
|
||||
@for (a of acTypOptions; track a) {
|
||||
<option [value]="a">{{ a }}</option>
|
||||
}
|
||||
@@ -156,6 +161,7 @@
|
||||
type="button"
|
||||
class="ws-seg__btn ws-seg__btn--dr"
|
||||
[class.ws-seg__btn--active-dr]="row.dbCr === 'Dr'"
|
||||
[disabled]="row.acTyp === 'GL'"
|
||||
(click)="row.dbCr = 'Dr'"
|
||||
>
|
||||
DR
|
||||
@@ -560,7 +566,8 @@
|
||||
</div>
|
||||
<div class="te-panel__body">
|
||||
<div class="te-balance-row">
|
||||
<span class="te-bal-num">1.</span><span class="te-bal-label">Current Balance</span>
|
||||
<span class="te-bal-num">1.</span>
|
||||
<span class="te-bal-label">{{ accountDetails?.glCode ? 'GL Balance' : 'Current Balance' }}</span>
|
||||
<span class="te-bal-value te-bal-value--cr"
|
||||
>{{ currencySymbol }}{{ currentBalance | number: '1.2-2' }}</span
|
||||
>
|
||||
@@ -685,8 +692,12 @@
|
||||
</div>
|
||||
}
|
||||
<div class="te-acs-nameblock">
|
||||
<span class="te-acs-name">{{ clientDetails?.displayName ?? accountDetails?.clientName ?? '—' }}</span>
|
||||
<span class="te-acs-acno">{{ accountDetails?.accountNo }}</span>
|
||||
<span class="te-acs-name">{{
|
||||
accountDetails?.glCode
|
||||
? (accountDetails?.name ?? '—')
|
||||
: (clientDetails?.displayName ?? accountDetails?.clientName ?? '—')
|
||||
}}</span>
|
||||
<span class="te-acs-acno">{{ accountDetails?.glCode ?? accountDetails?.accountNo }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="te-acs-sep"></div>
|
||||
@@ -713,7 +724,13 @@
|
||||
? 'Fixed Dep.'
|
||||
: selectedRow?.acTyp === 'RD'
|
||||
? 'Recurring'
|
||||
: '—')
|
||||
: selectedRow?.acTyp === 'GL'
|
||||
? (accountDetails?.type?.value ?? 'GL Account')
|
||||
: selectedRow?.acTyp === 'SHR'
|
||||
? 'Share'
|
||||
: selectedRow?.acTyp === 'LA'
|
||||
? 'Loan'
|
||||
: '—')
|
||||
}}</span
|
||||
>
|
||||
<span class="te-acs-kv"><b>Branch</b> {{ officeName ?? '—' }}</span>
|
||||
@@ -1083,7 +1100,11 @@
|
||||
</div>
|
||||
<div class="ws-f ws-f--actyp">
|
||||
<label class="ws-f__lbl">AC. TP.</label>
|
||||
<select class="ws-f__ctl" [(ngModel)]="row.acTyp">
|
||||
<select
|
||||
class="ws-f__ctl"
|
||||
[(ngModel)]="row.acTyp"
|
||||
(change)="row.acTyp === 'GL' && (row.dbCr = 'Cr')"
|
||||
>
|
||||
@for (a of acTypOptions; track a) {
|
||||
<option [value]="a">{{ a }}</option>
|
||||
}
|
||||
@@ -1116,6 +1137,7 @@
|
||||
type="button"
|
||||
class="ws-seg__btn ws-seg__btn--dr"
|
||||
[class.ws-seg__btn--active-dr]="row.dbCr === 'Dr'"
|
||||
[disabled]="row.acTyp === 'GL'"
|
||||
(click)="$event.stopPropagation(); row.dbCr = 'Dr'"
|
||||
>
|
||||
DR
|
||||
|
||||
@@ -660,6 +660,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
// Search token — cancels stale async results
|
||||
private searchToken = 0;
|
||||
private acNoInput$ = new Subject<{ row: TxnRow; index: number; value: string }>();
|
||||
cashVaultGlId: number | null = null;
|
||||
|
||||
// Joint holder expanded details (keyed by index)
|
||||
expandedHolder: number | null = null;
|
||||
@@ -767,6 +768,8 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
|
||||
this.loadCashVaultGl();
|
||||
|
||||
if (this.isClerk) {
|
||||
const saved = this.clerkUiState.snapshot.savedAccount;
|
||||
if (saved) {
|
||||
@@ -1041,10 +1044,27 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
this.acNoInput$.next({ row, index, value });
|
||||
}
|
||||
|
||||
private loadCashVaultGl(): void {
|
||||
// Dynamic: vault name > cash name > first ASSET type — no hardcoded IDs
|
||||
this.http
|
||||
.get('/financialactivityaccounts')
|
||||
.pipe(catchError(() => of([])))
|
||||
.subscribe((list: any) => {
|
||||
const items: any[] = Array.isArray(list) ? list : [];
|
||||
const byName = (re: RegExp) => items.find((a: any) => re.test(a.financialActivityData?.name ?? ''));
|
||||
const found =
|
||||
byName(/vault/i) ?? byName(/cash/i) ?? items.find((a: any) => a.glAccountData?.type?.value === 'ASSET');
|
||||
if (found?.glAccountData?.id) {
|
||||
this.cashVaultGlId = found.glAccountData.id;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onRowSelect(row: TxnRow, index: number): void {
|
||||
this.selectedRow = row;
|
||||
// Only reload if account number changed or nothing is loaded yet
|
||||
const alreadyLoaded = this.accountDetails && this.accountDetails.accountNo === row.acNo.trim();
|
||||
const loadedId = this.accountDetails?.glCode ?? this.accountDetails?.accountNo;
|
||||
const alreadyLoaded = this.accountDetails && loadedId === row.acNo.trim();
|
||||
if (row.acNo.trim() && !alreadyLoaded) {
|
||||
this.loadAccountDetails(row.acNo.trim(), row.acTyp);
|
||||
}
|
||||
@@ -1072,22 +1092,25 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
const token = ++this.searchToken; // Every new search gets a unique token
|
||||
const cfg = this.accountTypeConfig(acTyp);
|
||||
|
||||
// Step 1: search by accountNo on the correct endpoint for this account type
|
||||
// Step 1: search on the correct endpoint for this account type
|
||||
this.http
|
||||
.get<any>(`${cfg.list}?accountNo=${encodeURIComponent(acNo)}&offset=0&limit=${cfg.maxLimit}`)
|
||||
.get<any>(
|
||||
`${cfg.list}?${cfg.listQueryParam}=${encodeURIComponent(acNo)}${cfg.supportsPagination ? `&offset=0&limit=${cfg.maxLimit}` : ''}`
|
||||
)
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((res: any) => {
|
||||
if (token !== this.searchToken) return; // stale result — newer search started
|
||||
const allItems: any[] = res?.pageItems ?? (Array.isArray(res) ? res : []);
|
||||
const f = cfg.matchField;
|
||||
// Exact match first, then suffix/partial match (backend may return by last-N-digits)
|
||||
const exactMatch = allItems.find((x: any) => x.accountNo === acNo);
|
||||
const exactMatch = allItems.find((x: any) => x[f] === acNo);
|
||||
const partialMatch =
|
||||
!exactMatch && allItems.length === 1
|
||||
? allItems.find((x: any) => x.accountNo?.endsWith(acNo) || x.accountNo?.includes(acNo))
|
||||
? allItems.find((x: any) => x[f]?.endsWith(acNo) || x[f]?.includes(acNo))
|
||||
: null;
|
||||
const match = exactMatch ?? partialMatch ?? null;
|
||||
if (match?.id) {
|
||||
this.fetchAllDetails(match.id, match.accountNo, cfg, token, acTyp);
|
||||
this.fetchAllDetails(match.id, match[f] ?? acNo, cfg, token, acTyp);
|
||||
return;
|
||||
}
|
||||
this.loadingAccount = false;
|
||||
@@ -1106,6 +1129,10 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
txnSearchSupported: boolean;
|
||||
maxLimit: number;
|
||||
skipGlobalSearch: boolean;
|
||||
listQueryParam: string;
|
||||
matchField: string;
|
||||
detailParams: string;
|
||||
supportsPagination: boolean;
|
||||
} {
|
||||
const t = acTyp?.toUpperCase();
|
||||
if (t === 'LA') {
|
||||
@@ -1118,12 +1145,14 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
txnBase: '/loans',
|
||||
txnSearchSupported: true,
|
||||
maxLimit: 1,
|
||||
skipGlobalSearch: true
|
||||
skipGlobalSearch: true,
|
||||
listQueryParam: 'accountNo',
|
||||
matchField: 'accountNo',
|
||||
detailParams: '?associations=all',
|
||||
supportsPagination: true
|
||||
};
|
||||
}
|
||||
if (t === 'SHR') {
|
||||
// Backend ignores accountNo filter on list — fetch all and match client-side
|
||||
// Global /search?resource=share returns 500 — skip it
|
||||
return {
|
||||
list: '/accounts/share',
|
||||
detail: '/accounts/share',
|
||||
@@ -1133,10 +1162,15 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
txnBase: '/accounts/share',
|
||||
txnSearchSupported: false,
|
||||
maxLimit: 500,
|
||||
skipGlobalSearch: true
|
||||
skipGlobalSearch: true,
|
||||
listQueryParam: 'accountNo',
|
||||
matchField: 'accountNo',
|
||||
detailParams: '?associations=all',
|
||||
supportsPagination: true
|
||||
};
|
||||
}
|
||||
if (t === 'GL') {
|
||||
// GL uses searchParam (not accountNo) and matches on glCode field
|
||||
return {
|
||||
list: '/glaccounts',
|
||||
detail: '/glaccounts',
|
||||
@@ -1146,7 +1180,11 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
txnBase: '/glaccounts',
|
||||
txnSearchSupported: false,
|
||||
maxLimit: 500,
|
||||
skipGlobalSearch: true
|
||||
skipGlobalSearch: true,
|
||||
listQueryParam: 'searchParam',
|
||||
matchField: 'glCode',
|
||||
detailParams: '?fetchRunningBalance=true',
|
||||
supportsPagination: false
|
||||
};
|
||||
}
|
||||
if (t === 'CA' || t === 'CB') {
|
||||
@@ -1159,7 +1197,11 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
txnBase: '/currentaccounts',
|
||||
txnSearchSupported: true,
|
||||
maxLimit: 1,
|
||||
skipGlobalSearch: true
|
||||
skipGlobalSearch: true,
|
||||
listQueryParam: 'accountNo',
|
||||
matchField: 'accountNo',
|
||||
detailParams: '?associations=all',
|
||||
supportsPagination: true
|
||||
};
|
||||
}
|
||||
if (t === 'FD') {
|
||||
@@ -1172,7 +1214,11 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
txnBase: '/savingsaccounts',
|
||||
txnSearchSupported: true,
|
||||
maxLimit: 1,
|
||||
skipGlobalSearch: true
|
||||
skipGlobalSearch: true,
|
||||
listQueryParam: 'accountNo',
|
||||
matchField: 'accountNo',
|
||||
detailParams: '?associations=all',
|
||||
supportsPagination: true
|
||||
};
|
||||
}
|
||||
if (t === 'RD') {
|
||||
@@ -1185,7 +1231,11 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
txnBase: '/savingsaccounts',
|
||||
txnSearchSupported: true,
|
||||
maxLimit: 1,
|
||||
skipGlobalSearch: true
|
||||
skipGlobalSearch: true,
|
||||
listQueryParam: 'accountNo',
|
||||
matchField: 'accountNo',
|
||||
detailParams: '?associations=all',
|
||||
supportsPagination: true
|
||||
};
|
||||
}
|
||||
// SD — savings
|
||||
@@ -1198,7 +1248,11 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
txnBase: '/savingsaccounts',
|
||||
txnSearchSupported: true,
|
||||
maxLimit: 1,
|
||||
skipGlobalSearch: true
|
||||
skipGlobalSearch: true,
|
||||
listQueryParam: 'accountNo',
|
||||
matchField: 'accountNo',
|
||||
detailParams: '?associations=all',
|
||||
supportsPagination: true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1219,12 +1273,13 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
siAccountType: number;
|
||||
txnBase: string;
|
||||
txnSearchSupported: boolean;
|
||||
detailParams?: string;
|
||||
},
|
||||
token: number,
|
||||
acTyp: string = ''
|
||||
): void {
|
||||
this.http
|
||||
.get<any>(`${cfg.detail}/${accountId}?associations=all`)
|
||||
.get<any>(`${cfg.detail}/${accountId}${cfg.detailParams ?? '?associations=all'}`)
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((acc: any) => {
|
||||
if (token !== this.searchToken) return; // stale response — newer search active
|
||||
@@ -1233,17 +1288,18 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
this.accountError = 'Could not load account details.';
|
||||
return;
|
||||
}
|
||||
// Cross-check: returned accountNo must match what cashier searched
|
||||
if (acc.accountNo && acc.accountNo !== acNo) {
|
||||
// Cross-check: returned identifier must match what cashier searched
|
||||
const returnedId = acc.glCode ?? acc.accountNo;
|
||||
if (returnedId && returnedId !== acNo) {
|
||||
this.loadingAccount = false;
|
||||
this.accountError = `Account mismatch. Please re-enter account number and try again.`;
|
||||
return;
|
||||
}
|
||||
this.accountDetails = acc;
|
||||
this.jointHolders = acc.jointHolders ?? [];
|
||||
// Auto-fill ECS A/C NO. with savings account number (SHR) or account number
|
||||
// Auto-fill ECS A/C NO.: GL → glCode, SHR → savingsAccountNumber, others → accountNo
|
||||
if (this.selectedRow) {
|
||||
this.selectedRow.ecsAcNo = acc.savingsAccountNumber ?? acc.accountNo ?? '';
|
||||
this.selectedRow.ecsAcNo = acc.glCode ?? acc.savingsAccountNumber ?? acc.accountNo ?? '';
|
||||
}
|
||||
if (this.isClerk) this.clerkUiState.setFacilities(this.facilities, acNo, acTyp);
|
||||
// Compute same-day compliance indicators from included transactions
|
||||
@@ -1484,7 +1540,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
// ── Balance getters ────────────────────────────────────────────────────────
|
||||
|
||||
get currentBalance(): number {
|
||||
return this.accountDetails?.summary?.accountBalance ?? 0;
|
||||
return this.accountDetails?.summary?.accountBalance ?? this.accountDetails?.organizationRunningBalance ?? 0;
|
||||
}
|
||||
|
||||
get clearBalance(): number {
|
||||
@@ -1935,6 +1991,55 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
private proceedAfterValidation(row: TxnRow, amount: number, isDeposit: boolean, selectedPtEarly: any): void {
|
||||
const isWithdrawal = !isDeposit;
|
||||
const isCash = !!selectedPtEarly?.isCashPayment;
|
||||
const isGlType = row.acTyp === 'GL';
|
||||
|
||||
// GL transactions post as journal entries — skip account-level facility/balance checks
|
||||
if (isGlType) {
|
||||
if (!this.cashVaultGlId) {
|
||||
this.showAlert('error', 'Cash Vault GL Not Configured', [
|
||||
'Financial Activity Account (CASH_AT_TELLER) is not configured.',
|
||||
'Please configure it in System → Financial Activity Accounts.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Payment type required (same as savings)
|
||||
if (!row.paymentTypeId) {
|
||||
this.showAlert('error', 'Payment Type Required', ['Please select Payment Type (Cash / Cheque / NEFT etc.)']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Denomination overdraft check for GL withdrawal (same as savings)
|
||||
if (isWithdrawal && this.denominationTotal > 0 && this.hasWithdrawalDenomOverdraft) {
|
||||
const lines = this.denominationOverdrafts.map(
|
||||
(o) => `${o.label}: Need ${o.requested}, Available ${o.available} (Short by ${o.shortfall})`
|
||||
);
|
||||
this.showAlert('error', 'Insufficient Denomination Stock', [
|
||||
'Your cash drawer does not have enough notes/coins:',
|
||||
...lines,
|
||||
'Adjust denomination or request vault replenishment.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
const AML_GL = 200000;
|
||||
if (amount > AML_GL && isCash) {
|
||||
this.showConfirmAlert(
|
||||
'warning',
|
||||
'AML Alert — Large Cash Transaction',
|
||||
[
|
||||
`Transaction Amount: ₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2 })}`,
|
||||
'This transaction requires manager approval.',
|
||||
'Have you obtained approval to proceed?'
|
||||
],
|
||||
'Yes, Proceed',
|
||||
() => this.openConfirmDialog(row, amount, isDeposit, selectedPtEarly)
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.openConfirmDialog(row, amount, isDeposit, selectedPtEarly);
|
||||
return;
|
||||
}
|
||||
|
||||
// 0d. Withdrawal facility
|
||||
if (isWithdrawal) {
|
||||
@@ -2028,7 +2133,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
const balanceAfterTxn = isDeposit ? this.currentBalance + amount : this.currentBalance - amount;
|
||||
this.confirmData = {
|
||||
type: isDeposit ? 'DEPOSIT' : 'WITHDRAWAL',
|
||||
accountNo: this.accountDetails.accountNo,
|
||||
accountNo: this.accountDetails.glCode ?? this.accountDetails.accountNo,
|
||||
clientName: this.clientName,
|
||||
amount,
|
||||
currentBalance: this.currentBalance,
|
||||
@@ -2049,6 +2154,12 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
const accountId = this.accountDetails.id;
|
||||
const cfg = this.accountTypeConfig(row.acTyp);
|
||||
|
||||
// GL type → post as manual journal entry
|
||||
if (row.acTyp === 'GL') {
|
||||
this.submitGlJournalEntry(row, amount, isDeposit);
|
||||
return;
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
const txnDate = `${String(today.getDate()).padStart(2, '0')}/${String(today.getMonth() + 1).padStart(2, '0')}/${today.getFullYear()}`;
|
||||
|
||||
@@ -2134,6 +2245,102 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
this.addRow();
|
||||
}
|
||||
|
||||
private submitGlJournalEntry(row: TxnRow, amount: number, isDeposit: boolean): void {
|
||||
if (!this.cashVaultGlId) {
|
||||
this.showAlert('error', 'Cash Vault GL Not Configured', [
|
||||
'Financial Activity Account (CASH_AT_TELLER) is not configured.',
|
||||
'Please configure it in System → Financial Activity Accounts.'
|
||||
]);
|
||||
this.saving = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
const txnDate = `${String(today.getDate()).padStart(2, '0')}/${String(today.getMonth() + 1).padStart(2, '0')}/${today.getFullYear()}`;
|
||||
const txnDateIso = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
||||
|
||||
const selectedGlId = this.accountDetails.id;
|
||||
const contraGlId = this.cashVaultGlId;
|
||||
|
||||
// isDeposit (CR button) = cashier receives cash → credit selected GL, debit contra (cash vault)
|
||||
// isWithdrawal (DR button) = cashier pays out cash → debit selected GL, credit contra (cash vault)
|
||||
const credits = isDeposit ? [{ glAccountId: selectedGlId, amount }] : [{ glAccountId: contraGlId, amount }];
|
||||
const debits = isDeposit ? [{ glAccountId: contraGlId, amount }] : [{ glAccountId: selectedGlId, amount }];
|
||||
|
||||
const payload: any = {
|
||||
officeId: row.br || 1,
|
||||
transactionDate: txnDate,
|
||||
dateFormat: 'dd/MM/yyyy',
|
||||
locale: 'en',
|
||||
currencyCode: this.accountDetails?.currency?.code ?? 'INR',
|
||||
comments: row.description || null,
|
||||
credits,
|
||||
debits,
|
||||
paymentTypeId: row.paymentTypeId
|
||||
};
|
||||
|
||||
// Check if cash payment so we can record denomination after journal entry
|
||||
const selectedPt = this.paymentTypes.find((p: any) => p.id === row.paymentTypeId);
|
||||
const isCashPayment = !!selectedPt?.isCashPayment;
|
||||
|
||||
this.saving = true;
|
||||
this.http
|
||||
.post('/journalentries', payload)
|
||||
.pipe(
|
||||
catchError((err: any) => {
|
||||
const msg =
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ??
|
||||
err?.error?.developerMessage ??
|
||||
'Journal entry failed. Please try again.';
|
||||
this.showAlert('error', 'Transaction Failed', [msg]);
|
||||
this.saving = false;
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
.subscribe((res: any) => {
|
||||
this.saving = false;
|
||||
if (res) {
|
||||
// Record denomination so cashier position stays accurate
|
||||
// Same condition as savings: cash + denom entered + denom matches posted amount
|
||||
if (isCashPayment && this.denominationTotal > 0 && this.denominationTotal === amount) {
|
||||
const denomBreakdown = [
|
||||
...this.notes,
|
||||
...this.coins
|
||||
]
|
||||
.filter((d) => this.countFor(d) > 0)
|
||||
.map((d) => ({
|
||||
denominationId: d.id,
|
||||
denominationValue: d.value,
|
||||
kind: d.kind,
|
||||
count: this.countFor(d),
|
||||
subtotal: this.subtotalFor(d)
|
||||
}));
|
||||
this.http
|
||||
.post('/cashier/gl-denomination', {
|
||||
txnId: res.transactionId ?? res.resourceId,
|
||||
accountNo: this.accountDetails?.glCode ?? row.acNo,
|
||||
accountName: this.accountDetails?.name ?? '',
|
||||
txnType: isDeposit ? 'DEPOSIT' : 'WITHDRAWAL',
|
||||
txnDate: txnDateIso,
|
||||
paymentTypeName: selectedPt?.name ?? 'Cash',
|
||||
denominationBreakdown: denomBreakdown
|
||||
})
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
const ref = res.transactionId ?? res.resourceId ?? 'N/A';
|
||||
this.showAlert('success', 'GL Transaction Posted', [
|
||||
`Journal entry of ₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2 })} posted successfully.`,
|
||||
`Transaction ID: ${ref}`
|
||||
]);
|
||||
this.clearDenominations();
|
||||
this.loadOpeningPosition();
|
||||
this.loadAccountDetails(row.acNo.trim(), row.acTyp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSearch(): void {
|
||||
if (this.selectedRow?.acNo.trim()) {
|
||||
this.loadAccountDetails(this.selectedRow.acNo.trim(), this.selectedRow.acTyp);
|
||||
|
||||
Reference in New Issue
Block a user