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:
@@ -109,6 +109,22 @@
|
||||
Share Sundry against them, and approving this request moves it into share capital.
|
||||
</p>
|
||||
|
||||
<div class="gl-select-field">
|
||||
<select
|
||||
class="gl-select"
|
||||
[(ngModel)]="glAccountNo"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[disabled]="lookingUpVoucher || 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
|
||||
#voucherInput
|
||||
@@ -121,7 +137,7 @@
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
[disabled]="!voucherInput.value.trim() || lookingUpVoucher || submitting"
|
||||
[disabled]="!voucherInput.value.trim() || !glAccountNo.trim() || lookingUpVoucher || submitting"
|
||||
(click)="lookupVoucher(voucherInput.value)"
|
||||
>
|
||||
{{ lookingUpVoucher ? 'Finding…' : 'Find' }}
|
||||
|
||||
@@ -49,6 +49,30 @@ $danger: #b3261e;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.gl-select-field {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.gl-select {
|
||||
width: 100%;
|
||||
padding: 9px 11px;
|
||||
border: 1px solid $border;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: $primary;
|
||||
background: #fff;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $primary;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #f4f7fb;
|
||||
color: $muted;
|
||||
}
|
||||
}
|
||||
|
||||
.voucher-lookup {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
|
||||
import { UntypedFormGroup, UntypedFormBuilder, Validators, FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
@@ -55,6 +55,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
styleUrls: ['./apply-shares.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FormsModule,
|
||||
MatRadioModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatIconModule
|
||||
@@ -99,6 +100,13 @@ export class ApplySharesComponent implements OnInit {
|
||||
lookingUpVoucher = false;
|
||||
voucherError = '';
|
||||
|
||||
/** GL account the deposit was posted to — required, and always picked by hand. Never resolved
|
||||
* from a share product's accounting setup, so a broken or missing product mapping can never
|
||||
* make a genuine voucher unreachable. */
|
||||
glAccountNo = '';
|
||||
glAccounts: any[] = [];
|
||||
loadingGlAccounts = true;
|
||||
|
||||
/** What the member's savings can cover, for the savings route. */
|
||||
savingsEligibility: {
|
||||
eligible: boolean;
|
||||
@@ -120,6 +128,14 @@ export class ApplySharesComponent implements OnInit {
|
||||
this.createApplySharesAccountForm();
|
||||
this.applySharesForm.get('unitPrice').patchValue(this.sharesAccountData.currentMarketPrice || '');
|
||||
this.applySharesForm.get('amount').patchValue(this.amount || '');
|
||||
|
||||
this.cashierService
|
||||
.getShareAdvanceGlAccounts()
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((res: any) => {
|
||||
this.glAccounts = res?.glAccounts || [];
|
||||
this.loadingGlAccounts = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -266,14 +282,15 @@ export class ApplySharesComponent implements OnInit {
|
||||
*/
|
||||
lookupVoucher(voucherNo: string): void {
|
||||
const trimmed = (voucherNo || '').trim();
|
||||
if (!trimmed || this.lookingUpVoucher) {
|
||||
const glAccountNo = this.glAccountNo.trim();
|
||||
if (!trimmed || !glAccountNo || this.lookingUpVoucher) {
|
||||
return;
|
||||
}
|
||||
this.lookingUpVoucher = true;
|
||||
this.voucherError = '';
|
||||
|
||||
this.cashierService
|
||||
.getShareAdvanceByVoucher(trimmed, this.sharesAccountData.clientId)
|
||||
.getShareAdvanceByVoucher(trimmed, glAccountNo, this.sharesAccountData.clientId)
|
||||
.pipe(
|
||||
catchError((err: any) => {
|
||||
this.voucherError = this.readError(err, `Voucher ${trimmed} could not be found.`);
|
||||
|
||||
Reference in New Issue
Block a user