feat(shares, cashier): Entry Type tagging on Transaction Entry, real GL details on receipts
Adds an Entry Type field to Transaction Entry's GL-crediting rows (Receipt / Collection), fetched from the backend's GLEntryType code, so the counter can flag what a GL credit is actually for (General / Share Collection / DD Issue) instead of leaving every GL account picker throughout Share Advance Collection guessing from the whole chart of accounts. Apply Share / Additional Share screens: - GL Account picker narrowed to accounts that actually have an eligible receipt, and only auto-selected when exactly one candidate exists -- no more defaulting to whichever account sorts first (Cash at Vault). - Voucher tables show Description and Collected By, and a Detail view exposes the receipt's real Dr/Cr GL legs instead of the GL Account dropdown's current unrelated selection. - Additional Share funds from a single voucher (radio-style selection); ticking a receipt syncs the GL Account picker to its own actual account. - Checker's Additional Share Request review dialog gets the same Description + real GL entry detail, expandable per voucher, so approval is never a guess about what will post. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -401,6 +401,12 @@ export class CashierService {
|
||||
return this.http.post(`/shareadvancecollection/${collectionId}/apply`, data);
|
||||
}
|
||||
|
||||
/** The receipt's own real Dr/Cr GL legs — never the GL account currently picked in a search box,
|
||||
* which is unrelated screen state that can name a different account than this receipt sits in. */
|
||||
getShareAdvanceGlEntries(collectionId: number): Observable<any> {
|
||||
return this.http.get(`/shareadvancecollection/${collectionId}/gl-entries`);
|
||||
}
|
||||
|
||||
/** Every additional-share tranche awaiting approval, across every share account. */
|
||||
getShareAdditionRequests(): Observable<any> {
|
||||
return this.http.get('/shareadditionrequests');
|
||||
@@ -413,4 +419,14 @@ export class CashierService {
|
||||
getShareAdditionRequest(transactionId: number): Observable<any> {
|
||||
return this.http.get(`/shareadditionrequests/${transactionId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry Type options for a GL credit (General / Share Collection / DD Issue / …) — the
|
||||
* "GLEntryType" code under Admin > System > Manage Codes, so an admin can add another type later
|
||||
* without a deployment. Empty on failure rather than erroring, since a row missing this dropdown's
|
||||
* options should still be usable for every other field.
|
||||
*/
|
||||
getEntryTypeOptions(): Observable<any[]> {
|
||||
return this.http.get<any[]>('/codes/name/GLEntryType/codevalues').pipe(catchError(() => of([])));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +191,25 @@
|
||||
</select>
|
||||
}
|
||||
</div>
|
||||
@if (showsEntryType(row)) {
|
||||
<div class="ws-f ws-f--paytype">
|
||||
<label
|
||||
class="ws-f__lbl ws-f__lbl--accent"
|
||||
title="What this money was actually collected for — checked before a share application or DD issuance can adopt this voucher"
|
||||
>ENTRY TYPE</label
|
||||
>
|
||||
<select
|
||||
class="ws-f__ctl ws-f__ctl--accent"
|
||||
[(ngModel)]="row.entryTypeCvId"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<option [ngValue]="null">Select…</option>
|
||||
@for (t of entryTypeOptions; track t.id) {
|
||||
<option [ngValue]="t.id">{{ t.name }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1562,6 +1581,25 @@
|
||||
</select>
|
||||
}
|
||||
</div>
|
||||
@if (showsEntryType(row)) {
|
||||
<div class="ws-f ws-f--paytype">
|
||||
<label
|
||||
class="ws-f__lbl ws-f__lbl--accent"
|
||||
title="What this money was actually collected for — checked before a share application or DD issuance can adopt this voucher"
|
||||
>ENTRY TYPE</label
|
||||
>
|
||||
<select
|
||||
class="ws-f__ctl ws-f__ctl--accent"
|
||||
[(ngModel)]="row.entryTypeCvId"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<option [ngValue]="null">Select…</option>
|
||||
@for (t of entryTypeOptions; track t.id) {
|
||||
<option [ngValue]="t.id">{{ t.name }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ws-vdivider"></div>
|
||||
|
||||
@@ -53,6 +53,19 @@ interface TxnRow {
|
||||
/** DD issuance charges/commission — only used when TYPE=DD, TXN TP=Cash, AC.TP=GL */
|
||||
ddCharges: number | null;
|
||||
sponsorBankId?: number | null;
|
||||
/**
|
||||
* What a GL credit is actually for — the id of a code value under "GLEntryType" (General, Share
|
||||
* Collection, DD Issue, or whatever an admin has added under Admin > System > Manage Codes).
|
||||
*
|
||||
* A cashier Receipt (or a clerk Collection) into a GL account is the counter picking, by hand,
|
||||
* any account to post cash into — deliberately, so a broken product mapping can never block a
|
||||
* genuine voucher. That freedom is also the hole: two unrelated flows can land in the same GL
|
||||
* account with nothing on the row itself saying which one the money was collected for, and a
|
||||
* manager applying a share request can pick up a voucher that was really meant to fund a DD. This
|
||||
* tag is what a day-end reconciliation groups by, and what Share Advance Collection / Sponsor DD
|
||||
* issuance check before adopting a row — only shown/sent for AC.TP=GL, DR./CR.=Cr rows.
|
||||
*/
|
||||
entryTypeCvId?: number | null;
|
||||
/** Only used when this row is AC.TP=SHR inside an Enter Bank Transfer to its linked savings
|
||||
* account — governs the share redemption (see submitShareRedemptionTransfer()). */
|
||||
resolutionNumber?: string;
|
||||
@@ -1864,6 +1877,12 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
this.loadCashVaultGl();
|
||||
this.loadDdGstPercentage();
|
||||
this.loadSponsorBanks();
|
||||
this.cashierService
|
||||
.getEntryTypeOptions()
|
||||
.pipe(catchError(() => of([])))
|
||||
.subscribe((options: any[]) => {
|
||||
this.entryTypeOptions = options ?? [];
|
||||
});
|
||||
|
||||
if (this.reviewMode) {
|
||||
this.loadRequestIntoForm(this.reviewRequestId!, 'review');
|
||||
@@ -2379,6 +2398,23 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
return this.isDdCashGlRow(row) || this.isDdAccountDebitRow(row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this row needs an Entry Type tag — a credit into a GL account, which is exactly the
|
||||
* case where the money's real purpose isn't otherwise recorded anywhere (see TxnRow.entryTypeCvId).
|
||||
* A debit out of a GL account, or any entry against a savings/current/share account, carries no
|
||||
* such ambiguity — the account itself says what it's for.
|
||||
*/
|
||||
showsEntryType(row: TxnRow): boolean {
|
||||
return row.acTyp === 'GL' && row.dbCr === 'Cr';
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry Type choices for a GL credit — the "GLEntryType" code (Admin > System > Manage Codes),
|
||||
* fetched once in ngOnInit(). A code value, not a free-text field, so the day-end "group by type"
|
||||
* this exists for stays reliable; an admin extends the list there, not in this file.
|
||||
*/
|
||||
entryTypeOptions: { id: number; name: string }[] = [];
|
||||
|
||||
/** GST on DD charges only (never on the DD face value), at the admin-configured rate. */
|
||||
ddGstAmountFor(row: TxnRow): number {
|
||||
const charges = Number(row.ddCharges) || 0;
|
||||
@@ -2848,7 +2884,10 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
// Without this the backend defaults every request from this screen to DISBURSEMENT — a
|
||||
// Collection or Fund Transfer submitted here was being saved (and shown in the checker's
|
||||
// inbox) as a plain Disbursement.
|
||||
requestType: isCollectionMode ? 'COLLECTION' : isFundTransfer ? 'FUND_TRANSFER' : 'DISBURSEMENT'
|
||||
requestType: isCollectionMode ? 'COLLECTION' : isFundTransfer ? 'FUND_TRANSFER' : 'DISBURSEMENT',
|
||||
// Same ambiguity as a cashier Receipt — a Collection credits a GL account picked by hand.
|
||||
// See TxnRow.entryTypeCvId.
|
||||
...(isCollectionMode ? { entryTypeCvId: crRows[0]?.entryTypeCvId || null } : {})
|
||||
};
|
||||
|
||||
this.saving = true;
|
||||
@@ -5905,6 +5944,10 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
chequeNo: row.chequeNo || null,
|
||||
bankName: row.bankName || null,
|
||||
denominationData: isCash && this.denominationTotal > 0 ? JSON.stringify(this.denominationCounts) : null,
|
||||
// What this GL credit is actually for — see TxnRow.entryTypeCvId. Sent for every GL deposit
|
||||
// (the backend defaults to "General" when the counter didn't pick one), so Share Advance
|
||||
// Collection and Sponsor DD issuance can tell whose voucher this really is before adopting it.
|
||||
...(isGl ? { entryTypeCvId: row.entryTypeCvId || null } : {}),
|
||||
// DD + Cash + GL breakdown — persisted so the Sponsor DD issuance screen can read the face
|
||||
// value/charges/GST back once this request is approved, instead of retyping them.
|
||||
...(this.isDdCashGlRow(row)
|
||||
|
||||
Reference in New Issue
Block a user