Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f3c97a205 |
@@ -117,6 +117,8 @@
|
|||||||
<select
|
<select
|
||||||
class="ws-f__ctl"
|
class="ws-f__ctl"
|
||||||
[(ngModel)]="row.typ"
|
[(ngModel)]="row.typ"
|
||||||
|
[disabled]="isTypLocked(row)"
|
||||||
|
[title]="isTypLocked(row) ? 'Token withdrawal - TYPE is fixed to Payment' : ''"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
(change)="onTypChange(row)"
|
(change)="onTypChange(row)"
|
||||||
>
|
>
|
||||||
@@ -177,13 +179,7 @@
|
|||||||
class="ws-f__ctl"
|
class="ws-f__ctl"
|
||||||
[(ngModel)]="row.acTyp"
|
[(ngModel)]="row.acTyp"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
(change)="
|
(change)="isGlCreditOnly(row) && (row.dbCr = 'Cr')"
|
||||||
row.acTyp === 'GL' &&
|
|
||||||
row.typ !== 'Disbursement' &&
|
|
||||||
row.typ !== 'Fund Transfer' &&
|
|
||||||
row.typ !== 'Enter Bank Transfer' &&
|
|
||||||
(row.dbCr = 'Cr')
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
@for (a of acTypOptionsFor(row); track a) {
|
@for (a of acTypOptionsFor(row); track a) {
|
||||||
<option [value]="a">{{ a }}</option>
|
<option [value]="a">{{ a }}</option>
|
||||||
@@ -266,12 +262,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="ws-seg__btn ws-seg__btn--dr"
|
class="ws-seg__btn ws-seg__btn--dr"
|
||||||
[class.ws-seg__btn--active-dr]="row.dbCr === 'Dr'"
|
[class.ws-seg__btn--active-dr]="row.dbCr === 'Dr'"
|
||||||
[disabled]="
|
[disabled]="isGlCreditOnly(row)"
|
||||||
row.acTyp === 'GL' &&
|
|
||||||
row.typ !== 'Disbursement' &&
|
|
||||||
row.typ !== 'Fund Transfer' &&
|
|
||||||
row.typ !== 'Enter Bank Transfer'
|
|
||||||
"
|
|
||||||
(click)="row.dbCr = 'Dr'"
|
(click)="row.dbCr = 'Dr'"
|
||||||
>
|
>
|
||||||
DR
|
DR
|
||||||
@@ -280,6 +271,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="ws-seg__btn ws-seg__btn--cr"
|
class="ws-seg__btn ws-seg__btn--cr"
|
||||||
[class.ws-seg__btn--active-cr]="row.dbCr === 'Cr'"
|
[class.ws-seg__btn--active-cr]="row.dbCr === 'Cr'"
|
||||||
|
[disabled]="row.typ === 'Withdrawal Slip'"
|
||||||
(click)="row.dbCr = 'Cr'"
|
(click)="row.dbCr = 'Cr'"
|
||||||
>
|
>
|
||||||
CR
|
CR
|
||||||
@@ -711,8 +703,18 @@
|
|||||||
<circle cx="12" cy="8" r="4" />
|
<circle cx="12" cy="8" r="4" />
|
||||||
<path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" />
|
<path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" />
|
||||||
</svg>
|
</svg>
|
||||||
Personal Information
|
{{ isGlAccount ? 'GL Account Information' : 'Personal Information' }}
|
||||||
</div>
|
</div>
|
||||||
|
@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>
|
||||||
|
} @else {
|
||||||
<div class="te-panel__body">
|
<div class="te-panel__body">
|
||||||
<div class="te-field">
|
<div class="te-field">
|
||||||
<span class="te-label">Branch</span><span class="te-value">{{ officeName }}</span>
|
<span class="te-label">Branch</span><span class="te-value">{{ officeName }}</span>
|
||||||
@@ -775,6 +777,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Joint Holders + Nominees -->
|
<!-- Joint Holders + Nominees -->
|
||||||
@@ -929,6 +932,7 @@
|
|||||||
|
|
||||||
<!-- Other Info + KYC + Account Balances -->
|
<!-- Other Info + KYC + Account Balances -->
|
||||||
<div class="te-panel">
|
<div class="te-panel">
|
||||||
|
@if (!isGlAccount) {
|
||||||
<div class="te-panel__header">
|
<div class="te-panel__header">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<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" />
|
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||||
@@ -997,8 +1001,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</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">
|
<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" />
|
<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" />
|
<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
|
<select
|
||||||
class="ws-f__ctl"
|
class="ws-f__ctl"
|
||||||
[(ngModel)]="row.typ"
|
[(ngModel)]="row.typ"
|
||||||
|
[disabled]="isTypLocked(row)"
|
||||||
|
[title]="isTypLocked(row) ? 'Token withdrawal - TYPE is fixed to Payment' : ''"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
(change)="onTypChange(row)"
|
(change)="onTypChange(row)"
|
||||||
>
|
>
|
||||||
@@ -1627,13 +1634,7 @@
|
|||||||
class="ws-f__ctl"
|
class="ws-f__ctl"
|
||||||
[(ngModel)]="row.acTyp"
|
[(ngModel)]="row.acTyp"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
(change)="
|
(change)="isGlCreditOnly(row) && (row.dbCr = 'Cr')"
|
||||||
row.acTyp === 'GL' &&
|
|
||||||
row.typ !== 'Disbursement' &&
|
|
||||||
row.typ !== 'Fund Transfer' &&
|
|
||||||
row.typ !== 'Enter Bank Transfer' &&
|
|
||||||
(row.dbCr = 'Cr')
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
@for (a of acTypOptionsFor(row); track a) {
|
@for (a of acTypOptionsFor(row); track a) {
|
||||||
<option [value]="a">{{ a }}</option>
|
<option [value]="a">{{ a }}</option>
|
||||||
@@ -1704,12 +1705,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="ws-seg__btn ws-seg__btn--dr"
|
class="ws-seg__btn ws-seg__btn--dr"
|
||||||
[class.ws-seg__btn--active-dr]="row.dbCr === 'Dr'"
|
[class.ws-seg__btn--active-dr]="row.dbCr === 'Dr'"
|
||||||
[disabled]="
|
[disabled]="isGlCreditOnly(row)"
|
||||||
row.acTyp === 'GL' &&
|
|
||||||
row.typ !== 'Disbursement' &&
|
|
||||||
row.typ !== 'Fund Transfer' &&
|
|
||||||
row.typ !== 'Enter Bank Transfer'
|
|
||||||
"
|
|
||||||
(click)="$event.stopPropagation(); row.dbCr = 'Dr'"
|
(click)="$event.stopPropagation(); row.dbCr = 'Dr'"
|
||||||
>
|
>
|
||||||
DR
|
DR
|
||||||
@@ -1718,6 +1714,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="ws-seg__btn ws-seg__btn--cr"
|
class="ws-seg__btn ws-seg__btn--cr"
|
||||||
[class.ws-seg__btn--active-cr]="row.dbCr === 'Cr'"
|
[class.ws-seg__btn--active-cr]="row.dbCr === 'Cr'"
|
||||||
|
[disabled]="row.typ === 'Withdrawal Slip'"
|
||||||
(click)="$event.stopPropagation(); row.dbCr = 'Cr'"
|
(click)="$event.stopPropagation(); row.dbCr = 'Cr'"
|
||||||
>
|
>
|
||||||
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--ghost" type="button" (click)="onSearch()">Search</button>
|
||||||
<button class="ws-btn ws-btn--mode" type="button">Change Tr.Mode</button>
|
<button class="ws-btn ws-btn--mode" type="button">Change Tr.Mode</button>
|
||||||
@if (isClerk) {
|
@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">
|
<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="12" y1="5" x2="12" y2="19" />
|
||||||
<line x1="5" y1="12" x2="19" y2="12" />
|
<line x1="5" y1="12" x2="19" y2="12" />
|
||||||
|
|||||||
@@ -1658,6 +1658,8 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
// Full account + client data
|
// Full account + client data
|
||||||
accountDetails: any = null;
|
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;
|
clientDetails: any = null;
|
||||||
kycDetails: any = null;
|
kycDetails: any = null;
|
||||||
nominees: any[] = [];
|
nominees: any[] = [];
|
||||||
@@ -1685,10 +1687,12 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
private searchToken = 0;
|
private searchToken = 0;
|
||||||
private lastLoadedAcNo = '';
|
private lastLoadedAcNo = '';
|
||||||
private acNoInput$ = new Subject<{ row: TxnRow; index: number; value: string }>();
|
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;
|
cashVaultGlId: number | null = null;
|
||||||
/** The Cash Vault GL account's own code and name — the denomination breakdown for a manual GL
|
/** 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
|
* 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
|
* 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. */
|
* its cash/vault account (see loadCashVaultGl) — never a literal account name or code. */
|
||||||
cashVaultGlCode: string | null = null;
|
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)
|
distinctUntilChanged((a, b) => a.value === b.value && a.row === b.row)
|
||||||
)
|
)
|
||||||
.subscribe(({ row, index, value }) => {
|
.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.selectedRow = row;
|
||||||
this.loadAccountDetails(value.trim(), row.acTyp);
|
this.loadAccountDetails(value.trim(), row.acTyp);
|
||||||
}
|
}
|
||||||
@@ -2323,8 +2328,40 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
this.rows.push(row);
|
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 */
|
/** 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 {
|
onTypChange(row: TxnRow): void {
|
||||||
|
if (this.isTypLocked(row)) {
|
||||||
|
row.typ = 'Payment';
|
||||||
|
return;
|
||||||
|
}
|
||||||
switch (row.typ) {
|
switch (row.typ) {
|
||||||
case 'Withdrawal Slip':
|
case 'Withdrawal Slip':
|
||||||
row.dbCr = 'Dr';
|
row.dbCr = 'Dr';
|
||||||
@@ -2440,7 +2477,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
* such ambiguity — the account itself says what it's for.
|
* such ambiguity — the account itself says what it's for.
|
||||||
*/
|
*/
|
||||||
showsEntryType(row: TxnRow): boolean {
|
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',
|
SAVINGS: 'SD',
|
||||||
CURRENT: 'CA',
|
CURRENT: 'CA',
|
||||||
FIXED_DEPOSIT: 'FD',
|
FIXED_DEPOSIT: 'FD',
|
||||||
RECURRING_DEPOSIT: 'RD'
|
RECURRING_DEPOSIT: 'RD',
|
||||||
|
GL: 'GL'
|
||||||
};
|
};
|
||||||
|
|
||||||
openCustomerSearch(): void {
|
openCustomerSearch(): void {
|
||||||
@@ -4243,6 +4281,17 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.accountDetails = acc;
|
this.accountDetails = acc;
|
||||||
this.jointHolders = acc.jointHolders ?? [];
|
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)
|
// 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.
|
// — FD/RD/Current/Share/GL have no such row, so there's nothing to ask for.
|
||||||
if (acTyp?.toUpperCase() === 'SD' && acc.id) {
|
if (acTyp?.toUpperCase() === 'SD' && acc.id) {
|
||||||
@@ -4813,6 +4862,40 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
return this.accountDetails?.officeName ?? this.clientDetails?.officeName ?? '—';
|
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 {
|
get openingDate(): string {
|
||||||
const raw =
|
const raw =
|
||||||
this.accountDetails?.timeline?.submittedOnDate ??
|
this.accountDetails?.timeline?.submittedOnDate ??
|
||||||
@@ -5847,10 +5930,14 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GL withdrawal → direct journal entry (no maker-checker)
|
// A GL withdrawal slip is raised by the clerk and paid by the cashier against its token, exactly like a
|
||||||
// GL deposit → falls through to maker-checker block below
|
// savings withdrawal (the clerk branch and the token branch below). Nothing posts from this screen directly:
|
||||||
if (row.acTyp === 'GL' && !isDeposit) {
|
// manual journal entries are switched off on the server.
|
||||||
this.submitGlJournalEntry(row, amount, isDeposit);
|
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;
|
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 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 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 {
|
return {
|
||||||
accountNo: this.accountDetails.accountNo,
|
accountNo: isGl ? (this.accountDetails.glCode ?? this.accountDetails.name) : this.accountDetails.accountNo,
|
||||||
accountType: this.creditEntryAccountType(row.acTyp),
|
accountType: this.creditEntryAccountType(row.acTyp),
|
||||||
savingsAccountId: this.accountDetails.id,
|
savingsAccountId: isGl ? null : this.accountDetails.id,
|
||||||
clientId: this.accountDetails.clientId ?? this.clientDetails?.id ?? null,
|
// GL-specific fields: the GL being paid out of and the cash GL it is paid from (null for other types)
|
||||||
clientName: this.clientDetails?.displayName ?? this.accountDetails?.clientName ?? null,
|
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,
|
amount,
|
||||||
txnDate: txnDateIso,
|
txnDate: txnDateIso,
|
||||||
paymentTypeId: row.paymentTypeId || null,
|
paymentTypeId: row.paymentTypeId || null,
|
||||||
paymentTypeName: selectedPt?.name ?? 'Cash',
|
paymentTypeName: selectedPt?.name ?? 'Cash',
|
||||||
description: row.description || null,
|
description: description || null,
|
||||||
chequeNo: row.chequeNo || null,
|
chequeNo: row.chequeNo || null,
|
||||||
bankName: row.bankName || null,
|
bankName: row.bankName || null,
|
||||||
tokNo: row.tokNo?.trim() || null,
|
tokNo: row.tokNo?.trim() || null,
|
||||||
@@ -6425,106 +6529,6 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
this.addRow();
|
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 {
|
onSearch(): void {
|
||||||
if (this.selectedRow?.acNo.trim()) {
|
if (this.selectedRow?.acNo.trim()) {
|
||||||
this.loadAccountDetails(this.selectedRow.acNo.trim(), this.selectedRow.acTyp);
|
this.loadAccountDetails(this.selectedRow.acNo.trim(), this.selectedRow.acTyp);
|
||||||
|
|||||||
Reference in New Issue
Block a user