fix(shares): mandatory GL+voucher entry for advance collection, split entry-fee verification from settlement
- Apply Collection dialog: replace product-config auto-resolved GL account with a required GL + voucher search (mirrored for Share and Entry Fee receipts). The entry-fee receipt is tracked separately from the share receipts and sent as its own field so its amount is never swept into the Sundry-side total or double-posted — it only verifies the fee was paid, since that money already sits wherever the cashier deposited it. - Shows the account's required total (share value + entry fee) and live coverage up front, instead of only after a failed apply. - Apply Additional Shares: same mandatory GL + voucher pattern for its by-voucher lookup, plus a GL account picker fed by the new /shareadvancecollection/glaccounts endpoint. - shares-account-charges-step: fixed a race where the currency-control subscription (meant only for the edit-an-existing-application flow) wiped out product charges that ngOnChanges had just seeded from the same product-selection event, leaving new accounts with no charges. - shares-account-view: hide "Share Certificate" until collection_applied is true, so a certificate can't be printed for shares nobody has actually paid for yet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+122
-21
@@ -15,27 +15,128 @@
|
||||
instalments. The amount moves out of Share Sundry into share capital, and any entry fee is posted to fee income.
|
||||
</p>
|
||||
|
||||
<!-- What this account needs, up front — so the counter knows the full figure, and that it may take
|
||||
a second receipt for the entry fee, before a search ever comes up short. -->
|
||||
@if (requirement) {
|
||||
<div class="requirement-summary">
|
||||
<span class="requirement-line">
|
||||
Required: <b>{{ requirement.shareAmount | number: '1.2-2' }}</b> (Shares)
|
||||
@if (requirement.chargeAmount > 0) {
|
||||
+ <b>{{ requirement.chargeAmount | number: '1.2-2' }}</b> (Entry Fee) =
|
||||
<b>{{ requirement.required | number: '1.2-2' }}</b> Total
|
||||
}
|
||||
</span>
|
||||
@if (selectedIds.length > 0) {
|
||||
<span class="requirement-progress" [class.covered]="shortfall === 0">
|
||||
@if (shortfall === 0) {
|
||||
Share receipts covered
|
||||
} @else {
|
||||
{{ selectedTotal | number: '1.2-2' }} of {{ shareRequired | number: '1.2-2' }} collected —
|
||||
{{ shortfall | number: '1.2-2' }} short
|
||||
}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- For a receipt the list does not carry — one taken on an earlier day, or a slip the member is
|
||||
holding that is quicker to type than to find. -->
|
||||
<div class="voucher-lookup">
|
||||
<input
|
||||
type="text"
|
||||
class="voucher-input"
|
||||
placeholder="Voucher no. (e.g. CR/001/2608/0001)"
|
||||
[(ngModel)]="voucherNo"
|
||||
[disabled]="lookingUp || submitting"
|
||||
(keyup.enter)="lookupVoucher()"
|
||||
/>
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
[disabled]="!voucherNo.trim() || lookingUp || submitting"
|
||||
(click)="lookupVoucher()"
|
||||
>
|
||||
{{ lookingUp ? 'Finding…' : 'Find' }}
|
||||
</button>
|
||||
holding that is quicker to type than to find. Both the GL account the deposit was posted to
|
||||
and the voucher number are required — never resolved from a share product's own accounting
|
||||
setup, so a broken or missing product mapping can never make a genuine voucher unreachable. -->
|
||||
<div class="voucher-section">
|
||||
<div class="voucher-section-label">Share Receipt</div>
|
||||
<div class="gl-select-field">
|
||||
<select class="gl-select" [(ngModel)]="glAccountNo" [disabled]="lookingUp || submitting || loadingGlAccounts">
|
||||
<option value="" disabled>{{ loadingGlAccounts ? 'Loading GL accounts…' : 'Select GL account…' }}</option>
|
||||
@for (gl of glAccounts; track gl.id) {
|
||||
<option [value]="gl.gl_code">{{ gl.gl_code }} — {{ gl.name }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="voucher-lookup">
|
||||
<input
|
||||
type="text"
|
||||
class="voucher-input"
|
||||
placeholder="Voucher no. (e.g. CR/001/2608/0001)"
|
||||
[(ngModel)]="voucherNo"
|
||||
[disabled]="lookingUp || submitting"
|
||||
(keyup.enter)="lookupVoucher()"
|
||||
/>
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
[disabled]="!voucherNo.trim() || !glAccountNo.trim() || lookingUp || submitting"
|
||||
(click)="lookupVoucher()"
|
||||
>
|
||||
{{ lookingUp ? 'Finding…' : 'Find' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shown only when this account actually has an outstanding fee -- a member sometimes pays it as
|
||||
a second receipt into a different GL account rather than folding it into the share-value
|
||||
deposit. This search only ever verifies that receipt exists; it never moves its money -- that
|
||||
stays exactly where the cashier already deposited it, unlike the share receipts above which
|
||||
genuinely move out of Sundry. -->
|
||||
@if (requirement && requirement.chargeAmount > 0) {
|
||||
<div class="voucher-section">
|
||||
<div class="voucher-section-label">Entry Fee Receipt — verification only, no GL entry is posted for it</div>
|
||||
|
||||
@if (chargeReceipt) {
|
||||
<div class="charge-receipt-found" [class.short]="chargeShortfall > 0">
|
||||
<mat-icon>{{ chargeShortfall > 0 ? 'error_outline' : 'check_circle_outline' }}</mat-icon>
|
||||
<span class="charge-receipt-detail">
|
||||
<b class="mono">{{ chargeReceipt.voucher_no || chargeReceipt.receipt_no }}</b>
|
||||
— {{ chargeReceipt.amount | number: '1.2-2' }}
|
||||
@if (chargeShortfall > 0) {
|
||||
(short by {{ chargeShortfall | number: '1.2-2' }})
|
||||
} @else {
|
||||
— entry fee verified
|
||||
}
|
||||
</span>
|
||||
<button mat-button type="button" [disabled]="submitting" (click)="clearChargeReceipt()">Change</button>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="gl-select-field">
|
||||
<select
|
||||
class="gl-select"
|
||||
[(ngModel)]="chargeGlAccountNo"
|
||||
[disabled]="lookingUpCharge || submitting || loadingGlAccounts"
|
||||
>
|
||||
<option value="" disabled>{{ loadingGlAccounts ? 'Loading GL accounts…' : 'Select GL account…' }}</option>
|
||||
@for (gl of glAccounts; track gl.id) {
|
||||
<option [value]="gl.gl_code">{{ gl.gl_code }} — {{ gl.name }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="voucher-lookup">
|
||||
<input
|
||||
type="text"
|
||||
class="voucher-input"
|
||||
placeholder="Voucher no. (e.g. CR/001/2608/0002)"
|
||||
[(ngModel)]="chargeVoucherNo"
|
||||
[disabled]="lookingUpCharge || submitting"
|
||||
(keyup.enter)="lookupChargeVoucher()"
|
||||
/>
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
[disabled]="!chargeVoucherNo.trim() || !chargeGlAccountNo.trim() || lookingUpCharge || submitting"
|
||||
(click)="lookupChargeVoucher()"
|
||||
>
|
||||
{{ lookingUpCharge ? 'Finding…' : 'Find' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="charge-receipt-hint">
|
||||
Leave this blank if the fee was already paid as part of the share receipt above.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (errorMsg) {
|
||||
<div class="alert-error">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
@@ -106,8 +207,8 @@
|
||||
</table>
|
||||
|
||||
<div class="note">
|
||||
Their total must cover the account's share value plus any entry fee — anything paid over that is credited to the
|
||||
member's savings account.
|
||||
Their total must cover the account's share value — plus any entry fee, unless it was verified as its own receipt
|
||||
above. Anything paid over that is credited to the member's savings account.
|
||||
</div>
|
||||
}
|
||||
</mat-dialog-content>
|
||||
@@ -118,7 +219,7 @@
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
[disabled]="selectedIds.length === 0 || submitting"
|
||||
[disabled]="selectedIds.length === 0 || chargeShortfall > 0 || submitting"
|
||||
(click)="apply()"
|
||||
>
|
||||
{{ submitting ? 'Applying…' : 'Apply Collection' }}
|
||||
|
||||
Reference in New Issue
Block a user