feat(cashier): GL withdrawal slips, GL account panel with parent GL, and Payment lock on token withdrawals
Transaction Entry:
- GL withdrawal slip: a GL Withdrawal Slip is always a debit. The clerk raises it as a
request (GL, cash vault GL, office and currency ride on the payload), the manager
approves it and the cashier pays it against its token; nothing posts directly from this
screen any more (submitGlJournalEntry removed). Cashiers without an approved token are
told to enter one. GL credit-only rules moved into isGlCreditOnly(), and extra rows are
not allowed while a Withdrawal Slip is selected.
- A/C NO. now starts the automatic account search after six characters.
- Clerk: with AC. TP. = GL show a "GL Account Information" panel (code, name, type, usage,
normal balance, parent GL, manual entries, status, description) instead of the empty
client panels; the parent GL name is looked up from /glaccounts/{parentId} for GL only.
- Cashier: once a token (withdrawal / closure request) is verified, TYPE is disabled and
fixed to Payment, and onTypChange refuses to change it.
Requires the backend change "GL withdrawal slips through request, approval and cashier payment".
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -117,6 +117,8 @@
|
||||
<select
|
||||
class="ws-f__ctl"
|
||||
[(ngModel)]="row.typ"
|
||||
[disabled]="isTypLocked(row)"
|
||||
[title]="isTypLocked(row) ? 'Token withdrawal - TYPE is fixed to Payment' : ''"
|
||||
(click)="$event.stopPropagation()"
|
||||
(change)="onTypChange(row)"
|
||||
>
|
||||
@@ -177,13 +179,7 @@
|
||||
class="ws-f__ctl"
|
||||
[(ngModel)]="row.acTyp"
|
||||
(click)="$event.stopPropagation()"
|
||||
(change)="
|
||||
row.acTyp === 'GL' &&
|
||||
row.typ !== 'Disbursement' &&
|
||||
row.typ !== 'Fund Transfer' &&
|
||||
row.typ !== 'Enter Bank Transfer' &&
|
||||
(row.dbCr = 'Cr')
|
||||
"
|
||||
(change)="isGlCreditOnly(row) && (row.dbCr = 'Cr')"
|
||||
>
|
||||
@for (a of acTypOptionsFor(row); track a) {
|
||||
<option [value]="a">{{ a }}</option>
|
||||
@@ -266,12 +262,7 @@
|
||||
type="button"
|
||||
class="ws-seg__btn ws-seg__btn--dr"
|
||||
[class.ws-seg__btn--active-dr]="row.dbCr === 'Dr'"
|
||||
[disabled]="
|
||||
row.acTyp === 'GL' &&
|
||||
row.typ !== 'Disbursement' &&
|
||||
row.typ !== 'Fund Transfer' &&
|
||||
row.typ !== 'Enter Bank Transfer'
|
||||
"
|
||||
[disabled]="isGlCreditOnly(row)"
|
||||
(click)="row.dbCr = 'Dr'"
|
||||
>
|
||||
DR
|
||||
@@ -280,6 +271,7 @@
|
||||
type="button"
|
||||
class="ws-seg__btn ws-seg__btn--cr"
|
||||
[class.ws-seg__btn--active-cr]="row.dbCr === 'Cr'"
|
||||
[disabled]="row.typ === 'Withdrawal Slip'"
|
||||
(click)="row.dbCr = 'Cr'"
|
||||
>
|
||||
CR
|
||||
@@ -711,70 +703,81 @@
|
||||
<circle cx="12" cy="8" r="4" />
|
||||
<path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" />
|
||||
</svg>
|
||||
Personal Information
|
||||
{{ isGlAccount ? 'GL Account Information' : 'Personal Information' }}
|
||||
</div>
|
||||
<div class="te-panel__body">
|
||||
<div class="te-field">
|
||||
<span class="te-label">Branch</span><span class="te-value">{{ officeName }}</span>
|
||||
@if (isGlAccount) {
|
||||
<div class="te-panel__body">
|
||||
@for (f of glInfo; track f.label) {
|
||||
<div class="te-field">
|
||||
<span class="te-label">{{ f.label }}</span
|
||||
><span class="te-value te-value-wrap" [class.te-mono]="f.mono">{{ f.value }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">A/c No.</span
|
||||
><span class="te-value te-mono">{{ accountDetails.accountNo }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Name</span><span class="te-value te-value-name">{{ clientName }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">S/D/W/H/O</span><span class="te-value">{{ guardianName }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Address</span><span class="te-value te-value-wrap">{{ clientAddress }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Email</span><span class="te-value">{{ clientEmail }}</span>
|
||||
</div>
|
||||
<div class="te-field-row">
|
||||
} @else {
|
||||
<div class="te-panel__body">
|
||||
<div class="te-field">
|
||||
<span class="te-label">Last Txn</span><span class="te-value">{{ lastTransactionDate }}</span>
|
||||
<span class="te-label">Branch</span><span class="te-value">{{ officeName }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Type</span
|
||||
><span class="te-value">{{ accountDetails.depositType?.value ?? selectedRow?.acTyp ?? '—' }}</span>
|
||||
<span class="te-label">A/c No.</span
|
||||
><span class="te-value te-mono">{{ accountDetails.accountNo }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Name</span><span class="te-value te-value-name">{{ clientName }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">S/D/W/H/O</span><span class="te-value">{{ guardianName }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Address</span><span class="te-value te-value-wrap">{{ clientAddress }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Email</span><span class="te-value">{{ clientEmail }}</span>
|
||||
</div>
|
||||
<div class="te-field-row">
|
||||
<div class="te-field">
|
||||
<span class="te-label">Last Txn</span><span class="te-value">{{ lastTransactionDate }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Type</span
|
||||
><span class="te-value">{{ accountDetails.depositType?.value ?? selectedRow?.acTyp ?? '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="te-field-row">
|
||||
<div class="te-field">
|
||||
<span class="te-label">Opening Dt</span><span class="te-value">{{ openingDate }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Status</span
|
||||
><span class="te-value">{{ accountDetails.status?.value ?? '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">CIF No.</span><span class="te-value te-mono">{{ cifNumber }}</span>
|
||||
</div>
|
||||
@if (isNonIndividual) {
|
||||
<div class="te-field">
|
||||
<span class="te-label">Constitution</span>
|
||||
<span class="te-value">{{
|
||||
nonPersonDetails?.constitution?.name ?? nonPersonDetails?.constitutionType?.name ?? '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Incorp. No.</span>
|
||||
<span class="te-value te-mono">{{
|
||||
nonPersonDetails?.incorpNo ?? nonPersonDetails?.incorporationNo ?? '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Main Business</span>
|
||||
<span class="te-value">{{
|
||||
nonPersonDetails?.mainBusinessLine?.name ?? nonPersonDetails?.mainBusinessLineType?.name ?? '—'
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="te-field-row">
|
||||
<div class="te-field">
|
||||
<span class="te-label">Opening Dt</span><span class="te-value">{{ openingDate }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Status</span
|
||||
><span class="te-value">{{ accountDetails.status?.value ?? '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">CIF No.</span><span class="te-value te-mono">{{ cifNumber }}</span>
|
||||
</div>
|
||||
@if (isNonIndividual) {
|
||||
<div class="te-field">
|
||||
<span class="te-label">Constitution</span>
|
||||
<span class="te-value">{{
|
||||
nonPersonDetails?.constitution?.name ?? nonPersonDetails?.constitutionType?.name ?? '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Incorp. No.</span>
|
||||
<span class="te-value te-mono">{{
|
||||
nonPersonDetails?.incorpNo ?? nonPersonDetails?.incorporationNo ?? '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Main Business</span>
|
||||
<span class="te-value">{{
|
||||
nonPersonDetails?.mainBusinessLine?.name ?? nonPersonDetails?.mainBusinessLineType?.name ?? '—'
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Joint Holders + Nominees -->
|
||||
@@ -929,76 +932,78 @@
|
||||
|
||||
<!-- Other Info + KYC + Account Balances -->
|
||||
<div class="te-panel">
|
||||
<div class="te-panel__header">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||
<line x1="16" y1="2" x2="16" y2="6" />
|
||||
<line x1="8" y1="2" x2="8" y2="6" />
|
||||
<line x1="3" y1="10" x2="21" y2="10" />
|
||||
</svg>
|
||||
Other Information
|
||||
</div>
|
||||
<div class="te-panel__body">
|
||||
<div class="te-field">
|
||||
<span class="te-label">Phone No.</span><span class="te-value">{{ clientPhone }}</span>
|
||||
@if (!isGlAccount) {
|
||||
<div class="te-panel__header">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||
<line x1="16" y1="2" x2="16" y2="6" />
|
||||
<line x1="8" y1="2" x2="8" y2="6" />
|
||||
<line x1="3" y1="10" x2="21" y2="10" />
|
||||
</svg>
|
||||
Other Information
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Client ID</span
|
||||
><span class="te-value te-mono">{{ accountDetails.clientId }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">PAN</span><span class="te-value te-mono">{{ panNumber }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">KYC Status</span>
|
||||
<span class="te-value" [style.color]="kycDetails?.kycStatus === 300 ? '#059669' : '#f59e0b'">{{
|
||||
kycStatus
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">CKYC No.</span><span class="te-value te-mono">{{ ckycNumber }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Aadhaar</span><span class="te-value te-mono">{{ aadhaarNumber }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Product</span>
|
||||
<span class="te-value">{{
|
||||
accountDetails.savingsProductName ||
|
||||
accountDetails.depositProductName ||
|
||||
accountDetails.productName ||
|
||||
'—'
|
||||
}}</span>
|
||||
</div>
|
||||
@if (selectedRow?.acTyp === 'SHR') {
|
||||
<div class="te-panel__body">
|
||||
<div class="te-field">
|
||||
<span class="te-label">No. of Shares Held</span
|
||||
><span class="te-value te-mono">{{ accountDetails.summary?.totalApprovedShares ?? 0 }}</span>
|
||||
<span class="te-label">Phone No.</span><span class="te-value">{{ clientPhone }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Market Price / Share</span
|
||||
><span class="te-value te-mono"
|
||||
>{{ currencySymbol }}{{ accountDetails.currentMarketPrice | number: '1.2-2' }}</span
|
||||
>
|
||||
<span class="te-label">Client ID</span
|
||||
><span class="te-value te-mono">{{ accountDetails.clientId }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Resolution No.</span
|
||||
><span class="te-value te-mono">{{ accountDetails.resolutionNumber || '—' }}</span>
|
||||
<span class="te-label">PAN</span><span class="te-value te-mono">{{ panNumber }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Linked Savings A/C No.</span
|
||||
><span class="te-value te-mono">{{ accountDetails.savingsAccountNumber || '—' }}</span>
|
||||
<span class="te-label">KYC Status</span>
|
||||
<span class="te-value" [style.color]="kycDetails?.kycStatus === 300 ? '#059669' : '#f59e0b'">{{
|
||||
kycStatus
|
||||
}}</span>
|
||||
</div>
|
||||
@if (accountDetails.linkedLoanAccountNo) {
|
||||
<div class="te-field">
|
||||
<span class="te-label">CKYC No.</span><span class="te-value te-mono">{{ ckycNumber }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Aadhaar</span><span class="te-value te-mono">{{ aadhaarNumber }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Product</span>
|
||||
<span class="te-value">{{
|
||||
accountDetails.savingsProductName ||
|
||||
accountDetails.depositProductName ||
|
||||
accountDetails.productName ||
|
||||
'—'
|
||||
}}</span>
|
||||
</div>
|
||||
@if (selectedRow?.acTyp === 'SHR') {
|
||||
<div class="te-field">
|
||||
<span class="te-label">Linked Loan A/C No.</span
|
||||
><span class="te-value te-mono">{{ accountDetails.linkedLoanAccountNo }}</span>
|
||||
<span class="te-label">No. of Shares Held</span
|
||||
><span class="te-value te-mono">{{ accountDetails.summary?.totalApprovedShares ?? 0 }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Market Price / Share</span
|
||||
><span class="te-value te-mono"
|
||||
>{{ currencySymbol }}{{ accountDetails.currentMarketPrice | number: '1.2-2' }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Resolution No.</span
|
||||
><span class="te-value te-mono">{{ accountDetails.resolutionNumber || '—' }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Linked Savings A/C No.</span
|
||||
><span class="te-value te-mono">{{ accountDetails.savingsAccountNumber || '—' }}</span>
|
||||
</div>
|
||||
@if (accountDetails.linkedLoanAccountNo) {
|
||||
<div class="te-field">
|
||||
<span class="te-label">Linked Loan A/C No.</span
|
||||
><span class="te-value te-mono">{{ accountDetails.linkedLoanAccountNo }}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="te-panel__header te-panel__header--mt">
|
||||
<div class="te-panel__header" [class.te-panel__header--mt]="!isGlAccount">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="1" x2="12" y2="23" />
|
||||
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
|
||||
@@ -1567,6 +1572,8 @@
|
||||
<select
|
||||
class="ws-f__ctl"
|
||||
[(ngModel)]="row.typ"
|
||||
[disabled]="isTypLocked(row)"
|
||||
[title]="isTypLocked(row) ? 'Token withdrawal - TYPE is fixed to Payment' : ''"
|
||||
(click)="$event.stopPropagation()"
|
||||
(change)="onTypChange(row)"
|
||||
>
|
||||
@@ -1627,13 +1634,7 @@
|
||||
class="ws-f__ctl"
|
||||
[(ngModel)]="row.acTyp"
|
||||
(click)="$event.stopPropagation()"
|
||||
(change)="
|
||||
row.acTyp === 'GL' &&
|
||||
row.typ !== 'Disbursement' &&
|
||||
row.typ !== 'Fund Transfer' &&
|
||||
row.typ !== 'Enter Bank Transfer' &&
|
||||
(row.dbCr = 'Cr')
|
||||
"
|
||||
(change)="isGlCreditOnly(row) && (row.dbCr = 'Cr')"
|
||||
>
|
||||
@for (a of acTypOptionsFor(row); track a) {
|
||||
<option [value]="a">{{ a }}</option>
|
||||
@@ -1704,12 +1705,7 @@
|
||||
type="button"
|
||||
class="ws-seg__btn ws-seg__btn--dr"
|
||||
[class.ws-seg__btn--active-dr]="row.dbCr === 'Dr'"
|
||||
[disabled]="
|
||||
row.acTyp === 'GL' &&
|
||||
row.typ !== 'Disbursement' &&
|
||||
row.typ !== 'Fund Transfer' &&
|
||||
row.typ !== 'Enter Bank Transfer'
|
||||
"
|
||||
[disabled]="isGlCreditOnly(row)"
|
||||
(click)="$event.stopPropagation(); row.dbCr = 'Dr'"
|
||||
>
|
||||
DR
|
||||
@@ -1718,6 +1714,7 @@
|
||||
type="button"
|
||||
class="ws-seg__btn ws-seg__btn--cr"
|
||||
[class.ws-seg__btn--active-cr]="row.dbCr === 'Cr'"
|
||||
[disabled]="row.typ === 'Withdrawal Slip'"
|
||||
(click)="$event.stopPropagation(); row.dbCr = 'Cr'"
|
||||
>
|
||||
CR
|
||||
@@ -2043,7 +2040,13 @@
|
||||
<button class="ws-btn ws-btn--ghost" type="button" (click)="onSearch()">Search</button>
|
||||
<button class="ws-btn ws-btn--mode" type="button">Change Tr.Mode</button>
|
||||
@if (isClerk) {
|
||||
<button class="ws-btn ws-btn--add" type="button" (click)="addRow()" title="Add another transaction row">
|
||||
<button
|
||||
class="ws-btn ws-btn--add"
|
||||
type="button"
|
||||
[disabled]="selectedRow?.typ === 'Withdrawal Slip'"
|
||||
(click)="addRow()"
|
||||
title="Add another transaction row"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
|
||||
@@ -1658,6 +1658,8 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Full account + client data
|
||||
accountDetails: any = null;
|
||||
/** Parent (header) GL of the selected GL account, shown in the GL Account Information panel. */
|
||||
glParent: { glCode?: string; name?: string } | null = null;
|
||||
clientDetails: any = null;
|
||||
kycDetails: any = null;
|
||||
nominees: any[] = [];
|
||||
@@ -1685,10 +1687,12 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
private searchToken = 0;
|
||||
private lastLoadedAcNo = '';
|
||||
private acNoInput$ = new Subject<{ row: TxnRow; index: number; value: string }>();
|
||||
/** How many characters of the A/C NO. start the automatic account search. */
|
||||
private static readonly AC_NO_AUTO_SEARCH_MIN_LENGTH = 6;
|
||||
cashVaultGlId: number | null = null;
|
||||
/** The Cash Vault GL account's own code and name — the denomination breakdown for a manual GL
|
||||
* entry has to be filed against THIS account, not whichever account the teller typed in, since
|
||||
* that account is never the one physical notes actually moved through (see submitGlJournalEntry).
|
||||
* that account is never the one physical notes actually moved through.
|
||||
* Both resolved dynamically off whatever this tenant's own financial-activity mapping names as
|
||||
* its cash/vault account (see loadCashVaultGl) — never a literal account name or code. */
|
||||
cashVaultGlCode: string | null = null;
|
||||
@@ -1903,7 +1907,8 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
distinctUntilChanged((a, b) => a.value === b.value && a.row === b.row)
|
||||
)
|
||||
.subscribe(({ row, index, value }) => {
|
||||
if (value.trim().length >= 4) {
|
||||
// Searches on its own once six characters are typed, for clerk and cashier alike.
|
||||
if (value.trim().length >= TransactionEntryComponent.AC_NO_AUTO_SEARCH_MIN_LENGTH) {
|
||||
this.selectedRow = row;
|
||||
this.loadAccountDetails(value.trim(), row.acTyp);
|
||||
}
|
||||
@@ -2323,8 +2328,40 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
this.rows.push(row);
|
||||
}
|
||||
|
||||
/**
|
||||
* A GL row is credit-only (money received into the account) except on the transfer types, which pick
|
||||
* either side, and on a Withdrawal Slip, which is always a debit: the GL is debited and the cash
|
||||
* vault credited when cash is paid out against it (a clerk's request, approved, then paid by the cashier).
|
||||
*/
|
||||
isGlCreditOnly(row: TxnRow): boolean {
|
||||
return (
|
||||
row.acTyp === 'GL' &&
|
||||
row.typ !== 'Disbursement' &&
|
||||
row.typ !== 'Fund Transfer' &&
|
||||
row.typ !== 'Enter Bank Transfer' &&
|
||||
row.typ !== 'Withdrawal Slip'
|
||||
);
|
||||
}
|
||||
|
||||
/** Called when the TYPE dropdown changes — sets sensible defaults for TXN TP and DR/CR */
|
||||
/**
|
||||
* A cashier row that was filled from an approved token (withdrawal / account-closure) is cash
|
||||
* leaving the branch to the member, so its TYPE is fixed to Payment and cannot be changed.
|
||||
* Never true for the clerk screen or for rows keyed in by hand.
|
||||
*/
|
||||
isTypLocked(row: TxnRow): boolean {
|
||||
return (
|
||||
!this.isClerk &&
|
||||
!!row.tokNo?.trim() &&
|
||||
!!(this.activeWithdrawalRequest || this.activeDividendClaimRequest || this.activeClosureRequest)
|
||||
);
|
||||
}
|
||||
|
||||
onTypChange(row: TxnRow): void {
|
||||
if (this.isTypLocked(row)) {
|
||||
row.typ = 'Payment';
|
||||
return;
|
||||
}
|
||||
switch (row.typ) {
|
||||
case 'Withdrawal Slip':
|
||||
row.dbCr = 'Dr';
|
||||
@@ -2440,7 +2477,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
* such ambiguity — the account itself says what it's for.
|
||||
*/
|
||||
showsEntryType(row: TxnRow): boolean {
|
||||
return row.acTyp === 'GL' && row.dbCr === 'Cr';
|
||||
return row.acTyp === 'GL';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3918,7 +3955,8 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
SAVINGS: 'SD',
|
||||
CURRENT: 'CA',
|
||||
FIXED_DEPOSIT: 'FD',
|
||||
RECURRING_DEPOSIT: 'RD'
|
||||
RECURRING_DEPOSIT: 'RD',
|
||||
GL: 'GL'
|
||||
};
|
||||
|
||||
openCustomerSearch(): void {
|
||||
@@ -4243,6 +4281,17 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.accountDetails = acc;
|
||||
this.jointHolders = acc.jointHolders ?? [];
|
||||
// GL only: look up the parent (header) GL's name. Never runs for any other account type.
|
||||
this.glParent = null;
|
||||
if (acTyp?.toUpperCase() === 'GL' && acc.parentId) {
|
||||
this.http
|
||||
.get<any>(`/glaccounts/${acc.parentId}`)
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((p: any) => {
|
||||
if (token !== this.searchToken) return;
|
||||
this.glParent = p ? { glCode: p.glCode, name: p.name } : null;
|
||||
});
|
||||
}
|
||||
// Cash Hold / Clearing Hold breakdown only exists for plain savings accounts (m_savings_account)
|
||||
// — FD/RD/Current/Share/GL have no such row, so there's nothing to ask for.
|
||||
if (acTyp?.toUpperCase() === 'SD' && acc.id) {
|
||||
@@ -4813,6 +4862,40 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
return this.accountDetails?.officeName ?? this.clientDetails?.officeName ?? '—';
|
||||
}
|
||||
|
||||
/** True when the loaded account is a General Ledger account (/glaccounts), which has no client behind it. */
|
||||
get isGlAccount(): boolean {
|
||||
return !!this.accountDetails?.glCode && this.selectedRow?.acTyp === 'GL';
|
||||
}
|
||||
|
||||
/** The GL account's own details, shown in place of the client panels. */
|
||||
get glInfo(): { label: string; value: string; mono?: boolean }[] {
|
||||
const gl = this.accountDetails;
|
||||
if (!gl) return [];
|
||||
const type = gl.type?.value ?? '—';
|
||||
const nature = [
|
||||
'ASSET',
|
||||
'EXPENSE'
|
||||
].includes(String(type).toUpperCase()) ? 'Debit' : 'Credit';
|
||||
return [
|
||||
{ label: 'GL Code', value: gl.glCode ?? '—', mono: true },
|
||||
{ label: 'GL Name', value: gl.name ?? '—' },
|
||||
{ label: 'Account Type', value: type },
|
||||
{ label: 'Usage', value: gl.usage?.value ?? '—' },
|
||||
{ label: 'Normal Balance', value: nature },
|
||||
{
|
||||
label: 'Parent GL',
|
||||
value: this.glParent?.name
|
||||
? `${this.glParent.glCode ? this.glParent.glCode.trim() + ' - ' : ''}${this.glParent.name.trim()}`
|
||||
: gl.parentId != null
|
||||
? `ID ${gl.parentId}`
|
||||
: '—'
|
||||
},
|
||||
{ label: 'Manual Entries', value: gl.manualEntriesAllowed ? 'Allowed' : 'Not allowed' },
|
||||
{ label: 'Status', value: gl.disabled ? 'Disabled' : 'Enabled' },
|
||||
{ label: 'Description', value: gl.description || '—' }
|
||||
];
|
||||
}
|
||||
|
||||
get openingDate(): string {
|
||||
const raw =
|
||||
this.accountDetails?.timeline?.submittedOnDate ??
|
||||
@@ -5847,10 +5930,14 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
// GL withdrawal → direct journal entry (no maker-checker)
|
||||
// GL deposit → falls through to maker-checker block below
|
||||
if (row.acTyp === 'GL' && !isDeposit) {
|
||||
this.submitGlJournalEntry(row, amount, isDeposit);
|
||||
// A GL withdrawal slip is raised by the clerk and paid by the cashier against its token, exactly like a
|
||||
// savings withdrawal (the clerk branch and the token branch below). Nothing posts from this screen directly:
|
||||
// manual journal entries are switched off on the server.
|
||||
if (row.acTyp === 'GL' && !isDeposit && !this.clerkMode && !this.activeWithdrawalRequest) {
|
||||
this.showAlert('error', 'GL Withdrawal', [
|
||||
'A GL withdrawal is raised by a clerk and paid by the cashier against its token.',
|
||||
'Enter the token number of the approved request to pay it.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6223,17 +6310,34 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
const txnDateIso = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
||||
const selectedPt = this.paymentTypes.find((p: any) => p.id === row.paymentTypeId);
|
||||
|
||||
const isGl = row.acTyp === 'GL';
|
||||
// The Entry Type picked for a GL slip has no column of its own on the request, so it rides on the narration.
|
||||
const entryTypeName = isGl ? this.entryTypeOptions.find((e) => e.id === row.entryTypeCvId)?.name : undefined;
|
||||
const description = [
|
||||
row.description,
|
||||
entryTypeName ? `[Entry Type: ${entryTypeName}]` : ''
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return {
|
||||
accountNo: this.accountDetails.accountNo,
|
||||
accountNo: isGl ? (this.accountDetails.glCode ?? this.accountDetails.name) : this.accountDetails.accountNo,
|
||||
accountType: this.creditEntryAccountType(row.acTyp),
|
||||
savingsAccountId: this.accountDetails.id,
|
||||
clientId: this.accountDetails.clientId ?? this.clientDetails?.id ?? null,
|
||||
clientName: this.clientDetails?.displayName ?? this.accountDetails?.clientName ?? null,
|
||||
savingsAccountId: isGl ? null : this.accountDetails.id,
|
||||
// GL-specific fields: the GL being paid out of and the cash GL it is paid from (null for other types)
|
||||
glAccountId: isGl ? this.accountDetails.id : null,
|
||||
contraGlId: isGl ? this.cashVaultGlId : null,
|
||||
officeId: isGl ? row.br || 1 : null,
|
||||
currencyCode: isGl ? (this.accountDetails?.currency?.code ?? 'INR') : null,
|
||||
clientId: isGl ? null : (this.accountDetails.clientId ?? this.clientDetails?.id ?? null),
|
||||
clientName: isGl
|
||||
? (this.accountDetails?.name ?? null)
|
||||
: (this.clientDetails?.displayName ?? this.accountDetails?.clientName ?? null),
|
||||
amount,
|
||||
txnDate: txnDateIso,
|
||||
paymentTypeId: row.paymentTypeId || null,
|
||||
paymentTypeName: selectedPt?.name ?? 'Cash',
|
||||
description: row.description || null,
|
||||
description: description || null,
|
||||
chequeNo: row.chequeNo || null,
|
||||
bankName: row.bankName || null,
|
||||
tokNo: row.tokNo?.trim() || null,
|
||||
@@ -6425,106 +6529,6 @@ 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 = businessToday();
|
||||
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)
|
||||
}));
|
||||
// Filed against the Cash Vault GL, not the account the teller selected on screen: that
|
||||
// selected account (e.g. Sundry Creditors) is the other leg of the journal entry, and
|
||||
// never the one whose physical notes actually changed hands. Tagging the breakdown to it
|
||||
// instead of the vault is why this cash never reached the cashier/vault position.
|
||||
this.http
|
||||
.post('/cashier/gl-denomination', {
|
||||
txnId: res.transactionId ?? res.resourceId,
|
||||
accountNo: this.cashVaultGlCode ?? row.acNo,
|
||||
accountName: this.cashVaultGlName ?? 'Cash at Vault',
|
||||
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