feat: cash-only registers, day book sections and DD amendments
This commit is contained in:
@@ -32,6 +32,7 @@ import {
|
||||
} from '../../shared/customer-search-dialog/customer-search-dialog.component';
|
||||
import { SponsorDDService } from '../../sponsor-dd/sponsor-dd.service';
|
||||
import { OrganizationService } from '../../organization/organization.service';
|
||||
import { CashierIssueDdDialogComponent } from './cashier-issue-dd-dialog.component';
|
||||
|
||||
interface TxnRow {
|
||||
typ: string;
|
||||
@@ -51,6 +52,7 @@ interface TxnRow {
|
||||
routingCode: string;
|
||||
/** DD issuance charges/commission — only used when TYPE=DD, TXN TP=Cash, AC.TP=GL */
|
||||
ddCharges: number | null;
|
||||
sponsorBankId?: 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;
|
||||
@@ -1523,7 +1525,14 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
* the one that actually governs how the settlement credit posts (see submitShareRedemptionTransfer()).
|
||||
*/
|
||||
paymentTypesForRow(row: TxnRow): any[] {
|
||||
const tellerFacing = this.paymentTypes.filter((pt: any) => !pt.isSystemDefined);
|
||||
// Sponsor-bank payment types are excluded. They were added for the Demand Draft module, which
|
||||
// picks its bank from m_sponsor_bank on the DD row itself, and they are not counter instruments
|
||||
// — a clerk choosing "SBI" as the TXN TP. of a withdrawal says nothing about how the money
|
||||
// moves. They are recognised by their description rather than by name so that adding another
|
||||
// sponsor bank does not put it back on this list.
|
||||
const tellerFacing = this.paymentTypes.filter(
|
||||
(pt: any) => !pt.isSystemDefined && !/sponsor bank/i.test(pt.description ?? '')
|
||||
);
|
||||
if (!this.isClerk) {
|
||||
// Cashier counter takes cash only — cheque/transfer/DD instruments go through their own
|
||||
// clerk/back-office approval flows, never straight through the cash drawer.
|
||||
@@ -1541,6 +1550,23 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
if (isShareSettlementCredit) {
|
||||
return tellerFacing.filter((pt: any) => !pt.isCashPayment);
|
||||
}
|
||||
// A withdrawal slip is a cash instruction and nothing else: the clerk raises it, the cashier
|
||||
// counts the notes out against the token. There is no non-cash way to settle one, so cash is
|
||||
// the only choice offered rather than the default among several.
|
||||
if (row.typ === 'Withdrawal Slip') {
|
||||
return tellerFacing.filter((pt: any) => !!pt.isCashPayment);
|
||||
}
|
||||
// A clerk's Demand Draft is never funded by cash they take themselves. Cash for a draft is
|
||||
// counted at the counter, so it comes either from the cashier's own DD screen or from an
|
||||
// approved cashier deposit request the clerk points the draft at; the clerk's own route is to
|
||||
// debit the member's account. Offering cash here let a draft be raised against notes no drawer
|
||||
// had counted, which is exactly how the six cash DDs with no denomination breakdown arose.
|
||||
if (row.typ === 'DD') {
|
||||
return tellerFacing.filter((pt: any) => !pt.isCashPayment);
|
||||
}
|
||||
// Everything else can go either way, so both are offered and the clerk says which. A cheque or
|
||||
// a bank transfer normally settles through the instrument, a disbursement or fund transfer
|
||||
// between accounts — the type alone does not fix it to one or the other.
|
||||
return tellerFacing;
|
||||
}
|
||||
|
||||
@@ -1664,9 +1690,6 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
// wrong for crediting a GL amount into a savings/current account for any other reason (e.g.
|
||||
// funding a newly-activated account's opening balance from a GL source).
|
||||
{ value: 'Fund Transfer', label: 'Fund Transfer' },
|
||||
// NOTE: the exact same TxnRow.typ string is used at every dispatch/default-setting site below —
|
||||
// search for 'Disbursement' whenever this needs to change again, since 'Fund Transfer' always
|
||||
// rides alongside it in every one of those checks rather than having its own separate case.
|
||||
{ value: 'Enter Bank Transfer', label: 'Enter Bank Transfer' },
|
||||
{ value: 'DD', label: 'DD (Demand Draft)' }
|
||||
];
|
||||
@@ -1692,6 +1715,9 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
return this.isClerk ? this.clerkTypOptions : this.cashierTypOptions;
|
||||
}
|
||||
|
||||
sponsorBanks: any[] = [];
|
||||
ddGlMapping: any = null;
|
||||
|
||||
// ── Denomination ──────────────────────────────────────────────────────────
|
||||
notes: Denomination[] = [];
|
||||
coins: Denomination[] = [];
|
||||
@@ -1752,6 +1778,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.loadCashVaultGl();
|
||||
this.loadDdGstPercentage();
|
||||
this.loadSponsorBanks();
|
||||
|
||||
if (this.reviewMode) {
|
||||
this.loadRequestIntoForm(this.reviewRequestId!, 'review');
|
||||
@@ -1842,12 +1869,85 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
.getGlMappings()
|
||||
.pipe(catchError(() => of([])))
|
||||
.subscribe((mappings: any[]) => {
|
||||
const mapping = mappings && mappings.length ? mappings[0] : null;
|
||||
this.ddGstPercentage = Number(mapping?.gstPercentage ?? 18);
|
||||
this.ddLimitAmount = mapping?.ddLimitAmount != null ? Number(mapping.ddLimitAmount) : null;
|
||||
this.ddGlMapping = mappings && mappings.length ? mappings[0] : null;
|
||||
this.ddGstPercentage = Number(this.ddGlMapping?.gstPercentage ?? 18);
|
||||
this.ddLimitAmount = this.ddGlMapping?.ddLimitAmount != null ? Number(this.ddGlMapping.ddLimitAmount) : null;
|
||||
if (this.ddGlMapping?.ddPayableGlAccountId) {
|
||||
if (this.ddGlMapping.ddPayableGlCode) {
|
||||
this.applyDdGlMappingToDdRows();
|
||||
} else {
|
||||
this.http
|
||||
.get<any>(`/glaccounts/${this.ddGlMapping.ddPayableGlAccountId}`)
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((glAcc: any) => {
|
||||
if (glAcc) {
|
||||
this.ddGlMapping.ddPayableGlCode = glAcc.glCode || String(glAcc.id);
|
||||
this.ddGlMapping.ddPayableGlAccountName = glAcc.name;
|
||||
}
|
||||
this.applyDdGlMappingToDdRows();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
applyDdGlMappingToRow(row: TxnRow): void {
|
||||
if (row.typ !== 'DD' || !this.ddGlMapping) return;
|
||||
// Leaves an account-funded DD alone: its A/C NO. is the member's account the clerk typed, and
|
||||
// overwriting it with the DD Payable GL would silently turn the row back into a cash draft.
|
||||
if (this.isDdAccountDebitRow(row)) return;
|
||||
row.acTyp = 'GL';
|
||||
row.dbCr = 'Cr';
|
||||
|
||||
if (this.ddGlMapping.ddPayableGlCode) {
|
||||
row.acNo = this.ddGlMapping.ddPayableGlCode;
|
||||
this.selectedRow = row;
|
||||
this.loadAccountDetails(row.acNo, 'GL');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.ddGlMapping.ddPayableGlAccountId) {
|
||||
this.http
|
||||
.get<any>(`/glaccounts/${this.ddGlMapping.ddPayableGlAccountId}`)
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((glAcc: any) => {
|
||||
if (glAcc) {
|
||||
this.ddGlMapping.ddPayableGlCode = glAcc.glCode || String(glAcc.id);
|
||||
this.ddGlMapping.ddPayableGlAccountName = glAcc.name;
|
||||
row.acNo = this.ddGlMapping.ddPayableGlCode;
|
||||
this.selectedRow = row;
|
||||
this.loadAccountDetails(row.acNo, 'GL');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private applyDdGlMappingToDdRows(): void {
|
||||
for (const row of this.rows) {
|
||||
if (row.typ === 'DD') {
|
||||
this.applyDdGlMappingToRow(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private loadSponsorBanks(): void {
|
||||
this.sponsorDDService
|
||||
.getSponsorBanks()
|
||||
.pipe(catchError(() => of([])))
|
||||
.subscribe((banks: any[]) => {
|
||||
this.sponsorBanks = banks || [];
|
||||
for (const row of this.rows) {
|
||||
if (row.typ === 'DD' && !row.sponsorBankId && this.sponsorBanks.length > 0) {
|
||||
row.sponsorBankId = this.sponsorBanks[0].id;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get isDdGlMappingConfigured(): boolean {
|
||||
return !!(this.ddGlMapping && this.ddGlMapping.ddPayableGlAccountId && this.ddGlMapping.cashGlAccountId);
|
||||
}
|
||||
|
||||
private loadDayStatus(): void {
|
||||
this.cashierService
|
||||
.getCashierDayStatus()
|
||||
@@ -2093,9 +2193,12 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
row.chequeNo = '';
|
||||
row.cheqDate = '';
|
||||
break;
|
||||
// A cheque clears through the bank rather than the drawer, so it opens on the transfer
|
||||
// instrument. Cash is still on its dropdown — a cheque can be cashed over the counter — but
|
||||
// it is the exception, not what the field should start on.
|
||||
case 'Cheque':
|
||||
row.dbCr = 'Dr';
|
||||
row.paymentTypeId = this.findCashPaymentTypeId();
|
||||
row.paymentTypeId = this.findTransferPaymentTypeId();
|
||||
break;
|
||||
case 'Disbursement':
|
||||
case 'Fund Transfer':
|
||||
@@ -2121,16 +2224,25 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
row.chequeNo = '';
|
||||
row.cheqDate = '';
|
||||
break;
|
||||
// A DD is bought with cash: the customer hands over Face Value + Charges + GST, so it is money
|
||||
// coming IN (Cr) against the GL the draft is drawn on. All three of Cr, a cash payment type and
|
||||
// AC.TP = GL are what isDdCashGlRow() looks for -- without them the DD Charges & GST panel never
|
||||
// appears and the row would post as an ordinary entry with the charges silently dropped.
|
||||
// A DD is bought with cash: the purchaser hands over Face Value + Charges + GST, so it is
|
||||
// money coming IN (Cr) against the GL the draft is drawn on. All three of Cr, a cash payment
|
||||
// type and AC.TP = GL are what isDdCashGlRow() looks for -- without them the DD Charges & GST
|
||||
// panel never appears and the row would post as an ordinary entry with the charges silently
|
||||
// dropped.
|
||||
//
|
||||
// A clerk funds a draft through an account or an instrument instead, and their TXN TP. list no
|
||||
// longer offers cash, so defaulting to it would open the field on a value it cannot show.
|
||||
// isDdCashGlRow() still holds for them through AC.TP = GL.
|
||||
case 'DD':
|
||||
row.dbCr = 'Cr';
|
||||
row.paymentTypeId = this.findCashPaymentTypeId();
|
||||
row.acTyp = 'GL';
|
||||
row.paymentTypeId = this.isClerk ? this.findTransferPaymentTypeId() : this.findCashPaymentTypeId();
|
||||
row.chequeNo = '';
|
||||
row.cheqDate = '';
|
||||
if (this.sponsorBanks.length > 0 && !row.sponsorBankId) {
|
||||
row.sponsorBankId = this.sponsorBanks[0].id;
|
||||
}
|
||||
this.applyDdGlMappingToRow(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -2145,10 +2257,33 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
return pt?.id ?? null;
|
||||
}
|
||||
|
||||
/** TYPE=DD + TXN TP=Cash + AC.TP=GL — the only combination that collects DD charges + GST from the customer. */
|
||||
/** TYPE=DD + TXN TP=Cash */
|
||||
isDdCashGlRow(row: TxnRow): boolean {
|
||||
if (this.isDdAccountDebitRow(row)) return false;
|
||||
const pt = this.paymentTypes.find((p: any) => p.id === row.paymentTypeId);
|
||||
return row.typ === 'DD' && !!pt?.isCashPayment && row.acTyp === 'GL';
|
||||
return row.typ === 'DD' && (row.acTyp === 'GL' || !row.acTyp || !!pt?.isCashPayment);
|
||||
}
|
||||
|
||||
/**
|
||||
* TYPE=DD funded by debiting a member's savings/current account rather than by cash.
|
||||
*
|
||||
* Clerk-only. A cashier's counter takes cash and nothing else, so offering them an account debit
|
||||
* would let a draft be raised against an account with no cash to reconcile it against — and the
|
||||
* clerk flow is the one that carries a maker-checker step over the member's balance.
|
||||
*/
|
||||
isDdAccountDebitRow(row: TxnRow): boolean {
|
||||
return this.isClerk && row.typ === 'DD' && (row.acTyp === 'SD' || row.acTyp === 'CA' || row.acTyp === 'CB');
|
||||
}
|
||||
|
||||
/**
|
||||
* Any DD row the charges/GST panel applies to.
|
||||
*
|
||||
* Charges and GST come from the admin's DD configuration and are collected whichever way the
|
||||
* draft is paid for — the difference is only where the money comes from, so the panel is the same
|
||||
* for both and the label on the total changes rather than the arithmetic.
|
||||
*/
|
||||
isDdRow(row: TxnRow): boolean {
|
||||
return this.isDdCashGlRow(row) || this.isDdAccountDebitRow(row);
|
||||
}
|
||||
|
||||
/** GST on DD charges only (never on the DD face value), at the admin-configured rate. */
|
||||
@@ -2179,6 +2314,258 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
return !!this.selectedRow && Math.abs(this.denominationTotal - this.ddTargetTotal) < 0.005;
|
||||
}
|
||||
|
||||
branchNameFor(branchId: number): string {
|
||||
const map: Record<number, string> = {
|
||||
1: '001 · Main',
|
||||
2: '002 · M.G. Road',
|
||||
14: '014 · Station Rd',
|
||||
27: '027 · Industrial'
|
||||
};
|
||||
return map[branchId] || `Branch ${branchId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cashier Walk-in DD creation flow:
|
||||
* 1. Validates one-time persistent GL configuration
|
||||
* 2. Auto-selects next available leaflet
|
||||
* 3. Opens dialog to capture purchaser/beneficiary details
|
||||
* 4. Submits issue request directly with physical denominations for manager approval
|
||||
*/
|
||||
openIssueWalkinDd(row: TxnRow): void {
|
||||
if (!this.isDdGlMappingConfigured) {
|
||||
this.showAlert('error', 'DD GL Mapping Not Configured', [
|
||||
'DD GL mapping is not configured.',
|
||||
'Please ask an administrator to set up the GL accounts in Admin > Sponsor DD GST / GL Settings before creating Demand Drafts.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
const faceValue = Number(row.amount);
|
||||
if (!faceValue || faceValue <= 0) {
|
||||
this.showAlert('error', 'Invalid Amount', ['Please enter the DD Face Value amount before proceeding.']);
|
||||
return;
|
||||
}
|
||||
const chargesAmount = Number(row.ddCharges) || 0;
|
||||
const gstAmount = this.ddGstAmountFor(row);
|
||||
const totalCashPayable = this.ddTotalPayableFor(row);
|
||||
const accountDebit = this.isDdAccountDebitRow(row);
|
||||
|
||||
// An account-funded draft needs the account resolved to a Fineract id before it can be sent:
|
||||
// the server takes a savings/current account id and a client id, not the typed number. Both are
|
||||
// filled in by the A/C NO. lookup, so an unresolved account means the number was never found.
|
||||
let fundingAccountId: number | null = null;
|
||||
let fundingClientId: number | null = null;
|
||||
if (accountDebit) {
|
||||
const acNo = (row.acNo || '').trim();
|
||||
if (!acNo) {
|
||||
this.showAlert('error', 'Account Required', [
|
||||
'Enter the account number the Demand Draft is funded from.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
fundingAccountId = this.rowAccountIds[acNo] ?? null;
|
||||
fundingClientId = this.rowClientIds[acNo] ?? null;
|
||||
if (!fundingAccountId || !fundingClientId) {
|
||||
this.showAlert('error', 'Account Not Found', [
|
||||
`Account ${acNo} could not be resolved.`,
|
||||
'Check the account number and wait for its details to load before raising the draft.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Only a cash draft has notes to reconcile. An account debit takes no cash at the counter, so a
|
||||
// denomination count left over from an earlier row must not be checked against it.
|
||||
if (!accountDebit && this.denominationTotal > 0 && Math.abs(this.denominationTotal - totalCashPayable) > 0.005) {
|
||||
this.showAlert('error', 'Denomination Mismatch', [
|
||||
`Cash denomination counted (₹${this.denominationTotal}) does not match total payable cash (₹${totalCashPayable}).`,
|
||||
'Please verify and match the physical cash denomination count.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
this.sponsorDDService.getNextAvailableLeaflet(row.br, row.sponsorBankId ?? undefined).subscribe({
|
||||
next: (leaflet) => {
|
||||
this.saving = false;
|
||||
if (!leaflet) {
|
||||
this.showAlert('error', 'No Available Leaflets', [
|
||||
'No available Demand Draft leaflets found for this branch and sponsor bank.',
|
||||
'Please register a DD inventory series first.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedBank = this.sponsorBanks.find((b: any) => b.id === row.sponsorBankId);
|
||||
const branchName = this.branchNameFor(row.br) || 'Branch ' + row.br;
|
||||
|
||||
const dialogRef = this.dialog.open(CashierIssueDdDialogComponent, {
|
||||
width: '600px',
|
||||
disableClose: true,
|
||||
data: {
|
||||
leaflet,
|
||||
branchName,
|
||||
sponsorBankName: selectedBank?.name || 'Sponsor Bank',
|
||||
faceValue,
|
||||
chargesAmount,
|
||||
gstPercentage: this.ddGstPercentage,
|
||||
gstAmount,
|
||||
totalCashPayable,
|
||||
paymentMethod: accountDebit ? ('ACCOUNT_DEBIT' as const) : ('CASH' as const),
|
||||
accountNo: accountDebit ? row.acNo : undefined,
|
||||
accountHolderName: accountDebit ? this.rowAccountNames[(row.acNo || '').trim()] : undefined
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (!result) return;
|
||||
if (accountDebit) {
|
||||
this.submitAccountDebitDd(row, leaflet, result, fundingAccountId!, fundingClientId!);
|
||||
} else {
|
||||
this.submitWalkinCashDd(row, leaflet, result);
|
||||
}
|
||||
});
|
||||
},
|
||||
error: (err) => {
|
||||
this.saving = false;
|
||||
this.showAlert('error', 'Error Auto-Selecting DD Leaflet', [err.error?.message || err.message]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Raises a Demand Draft funded by debiting a member's savings or current account.
|
||||
*
|
||||
* Clerk-only, and a maker-checker request like the cash flow: nothing is debited here. The member's
|
||||
* account is only touched when a checker approves, which is also when the balance is re-checked —
|
||||
* so a draft raised against a sufficient balance can still be refused if the balance has since
|
||||
* gone. No denominations are sent, because no cash is taken.
|
||||
*
|
||||
* The account goes in savingsAccountId or currentAccountId, never both: the server insists on
|
||||
* exactly one, since which table the account lives in decides which control GL is debited.
|
||||
*/
|
||||
private submitAccountDebitDd(
|
||||
row: TxnRow,
|
||||
leaflet: any,
|
||||
beneficiaryData: any,
|
||||
fundingAccountId: number,
|
||||
fundingClientId: number
|
||||
): void {
|
||||
const d = new Date();
|
||||
const issuedDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
const isCurrent = row.acTyp === 'CA' || row.acTyp === 'CB';
|
||||
|
||||
const payload = {
|
||||
clientId: fundingClientId,
|
||||
beneficiaryName: beneficiaryData.beneficiaryName,
|
||||
payableAtBranch: beneficiaryData.payableAtBranch,
|
||||
payableAtCity: beneficiaryData.payableAtCity,
|
||||
faceValue: Number(row.amount),
|
||||
chargesAmount: Number(row.ddCharges) || 0,
|
||||
issuingBranchId: row.br,
|
||||
issuedDate,
|
||||
paymentMethod: 'ACCOUNT_DEBIT' as const,
|
||||
savingsAccountId: isCurrent ? undefined : fundingAccountId,
|
||||
currentAccountId: isCurrent ? fundingAccountId : undefined
|
||||
};
|
||||
|
||||
this.saving = true;
|
||||
this.sponsorDDService.submitIssueRequest(leaflet.id, payload).subscribe({
|
||||
next: () => {
|
||||
this.saving = false;
|
||||
this.showAlert('success', 'DD Issuance Request Submitted', [
|
||||
`Demand Draft request for DD No. ${leaflet.ddNumber} (Face Value: ₹${row.amount}) submitted successfully!`,
|
||||
`₹${this.ddTotalPayableFor(row)} will be debited from ${row.acNo} on approval.`,
|
||||
'Sent to Manager / Head Cashier for maker-checker approval.'
|
||||
]);
|
||||
row.amount = null;
|
||||
row.ddCharges = null;
|
||||
row.description = '';
|
||||
},
|
||||
error: (err) => {
|
||||
this.saving = false;
|
||||
this.showAlert('error', 'DD Submission Failed', [err.error?.message || err.message]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private submitWalkinCashDd(row: TxnRow, leaflet: any, purchaserData: any): void {
|
||||
// Read through countFor, which knows the map is keyed by kind:value. Indexing by d.id looked
|
||||
// right but matched nothing, so every cash DD went in with no breakdown at all and its money
|
||||
// vanished from the closing report -- the notes were counted on screen and then dropped.
|
||||
const denominationMap: { [key: string]: number } = {};
|
||||
for (const d of [
|
||||
...this.notes,
|
||||
...this.coins
|
||||
]) {
|
||||
const count = this.countFor(d);
|
||||
if (count > 0) {
|
||||
denominationMap[this.denomKey(d)] = count;
|
||||
}
|
||||
}
|
||||
|
||||
// The breakdown is the only record of which notes were taken, and the server insists it add up
|
||||
// to the cash collected. Checked here so the cashier is told which figure is wrong while the
|
||||
// counts are still on screen, rather than getting a rejection after the fact.
|
||||
//
|
||||
// ddTotalPayableFor rounds up to the whole rupee actually collected, which is what the notes
|
||||
// can add to; the exact figure with paise is accepted too, for a draft that lands on a rupee.
|
||||
const totalCollected = Number(this.ddTotalPayableFor(row));
|
||||
const exactPayable = Number(row.amount || 0) + (Number(row.ddCharges) || 0) + this.ddGstAmountFor(row);
|
||||
const countedTotal = Object.entries(denominationMap).reduce(
|
||||
(sum, [
|
||||
key,
|
||||
count
|
||||
]) => sum + Number(key.split(':')[1]) * count,
|
||||
0
|
||||
);
|
||||
if (countedTotal !== totalCollected && countedTotal !== exactPayable) {
|
||||
this.showAlert('error', 'Denomination Entry Does Not Match', [
|
||||
countedTotal === 0
|
||||
? 'Enter the notes and coins taken from the purchaser before submitting.'
|
||||
: `The notes and coins entered come to ₹${countedTotal.toFixed(2)}, but ₹${totalCollected.toFixed(2)} is collected (face value + charges + GST).`,
|
||||
'The breakdown must equal the cash taken — it is what the closing report and the drawer are checked against.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
const d = new Date();
|
||||
const issuedDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
|
||||
const payload = {
|
||||
walkinPurchaserName: purchaserData.walkinPurchaserName,
|
||||
walkinPurchaserAddress: purchaserData.walkinPurchaserAddress,
|
||||
walkinPurchaserAadhaar: purchaserData.walkinPurchaserAadhaar,
|
||||
beneficiaryName: purchaserData.beneficiaryName,
|
||||
payableAtBranch: purchaserData.payableAtBranch,
|
||||
payableAtCity: purchaserData.payableAtCity,
|
||||
faceValue: Number(row.amount),
|
||||
chargesAmount: Number(row.ddCharges) || 0,
|
||||
issuingBranchId: row.br,
|
||||
issuedDate,
|
||||
paymentMethod: 'CASH' as const,
|
||||
denominationData: Object.keys(denominationMap).length > 0 ? JSON.stringify(denominationMap) : undefined
|
||||
};
|
||||
|
||||
this.saving = true;
|
||||
this.sponsorDDService.submitIssueRequest(leaflet.id, payload).subscribe({
|
||||
next: () => {
|
||||
this.saving = false;
|
||||
this.showAlert('success', 'DD Issuance Request Submitted', [
|
||||
`Demand Draft request for DD No. ${leaflet.ddNumber} (Face Value: ₹${row.amount}, Total Cash: ₹${this.ddTotalPayableFor(row)}) submitted successfully!`,
|
||||
'Sent to Manager / Head Cashier for maker-checker approval.'
|
||||
]);
|
||||
row.amount = null;
|
||||
row.ddCharges = null;
|
||||
row.description = '';
|
||||
this.clearDenominations();
|
||||
},
|
||||
error: (err) => {
|
||||
this.saving = false;
|
||||
this.showAlert('error', 'DD Submission Failed', [err.error?.message || err.message]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits a clerk double-entry request (maker-checker, PENDING until manager approves).
|
||||
* Disbursement: 1 DR [GL] source → N CR savings/current/GL destinations.
|
||||
@@ -2811,16 +3198,23 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
const f = cfg.matchField;
|
||||
// Exact match first, then suffix/partial match. Some account types (e.g. Share — its
|
||||
// list endpoint only accepts offset/limit, no accountNo filter at all) always return the
|
||||
// FULL unfiltered list rather than a server-side-narrowed one, so a partial match can't be
|
||||
// gated on "exactly one row came back" — it never would be for those types.
|
||||
const exactMatch = allItems.find((x: any) => x[f] === acNo);
|
||||
const exactMatch = allItems.find((x: any) => String(x[f] ?? '').trim() === String(acNo).trim());
|
||||
const partialMatch = !exactMatch
|
||||
? (allItems.find((x: any) => x[f]?.endsWith(acNo)) ?? allItems.find((x: any) => x[f]?.includes(acNo)))
|
||||
? (allItems.find((x: any) => String(x[f] ?? '').endsWith(acNo)) ??
|
||||
allItems.find((x: any) => String(x[f] ?? '').includes(acNo)))
|
||||
: null;
|
||||
const match = exactMatch ?? partialMatch ?? null;
|
||||
const idMatch =
|
||||
!exactMatch && !partialMatch && acTyp === 'GL'
|
||||
? allItems.find((x: any) => String(x.id) === String(acNo))
|
||||
: null;
|
||||
const match = exactMatch ?? partialMatch ?? idMatch ?? null;
|
||||
if (match?.id) {
|
||||
const matchedGlCode = match[f] ?? acNo;
|
||||
if (this.selectedRow && this.selectedRow.typ === 'DD' && match.glCode) {
|
||||
this.selectedRow.acNo = match.glCode;
|
||||
}
|
||||
const clientIdFromSearch = acTyp !== 'GL' && match.clientId != null ? String(match.clientId) : null;
|
||||
this.fetchAllDetails(match.id, match[f] ?? acNo, cfg, token, acTyp, clientIdFromSearch);
|
||||
this.fetchAllDetails(match.id, matchedGlCode, cfg, token, acTyp, clientIdFromSearch);
|
||||
return;
|
||||
}
|
||||
this.loadingAccount = false;
|
||||
@@ -3048,7 +3442,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
return false;
|
||||
}
|
||||
const returnedId = acc.glCode ?? acc.accountNo;
|
||||
if (returnedId && returnedId !== acNo) {
|
||||
if (returnedId && returnedId !== acNo && String(acc.id) !== acNo) {
|
||||
this.loadingAccount = false;
|
||||
this.accountError = 'Account mismatch. Please re-enter account number and try again.';
|
||||
return false;
|
||||
@@ -3294,7 +3688,6 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
{ label: 'NEFT Facility', active: !!f.neftFacility },
|
||||
{ label: 'RTGS Facility', active: !!f.rtgsFacility },
|
||||
{ label: 'Inter-Bank Cap', active: !!f.interBankTotalCapFacility },
|
||||
{ label: 'Withdrawal', active: !!f.withdrawalFacility },
|
||||
{ label: 'Deposit (KYC)', active: !!f.depositFacilityKyc },
|
||||
{ label: 'Virtual Debit Card', active: !!f.virtualDebitCard },
|
||||
{ label: 'OD', active: !!this.accountDetails?.allowOverdraft }
|
||||
@@ -4579,6 +4972,10 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
confirmSubmit(): void {
|
||||
this.showConfirmDialog = false;
|
||||
const row = this.selectedRow!;
|
||||
if (row.typ === 'DD') {
|
||||
this.openIssueWalkinDd(row);
|
||||
return;
|
||||
}
|
||||
const amount = this.isDdCashGlRow(row) ? this.ddTotalPayableFor(row) : (row.amount ?? this.denominationTotal);
|
||||
const isDeposit = row.dbCr === 'Cr';
|
||||
const command = isDeposit ? 'deposit' : 'withdrawal';
|
||||
|
||||
Reference in New Issue
Block a user