DDS: approvals tab, vouchers, payout & close, Sundry GL search, closure tab, fixes
This commit is contained in:
@@ -12,17 +12,25 @@ import { UntypedFormBuilder, UntypedFormGroup, UntypedFormArray, Validators } fr
|
||||
import { Router } from '@angular/router';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component';
|
||||
|
||||
/** Custom Components */
|
||||
import { DayCollectionDialogComponent } from './day-collection-dialog/day-collection-dialog.component';
|
||||
import { NoticeDialogComponent } from './notice-dialog/notice-dialog.component';
|
||||
import {
|
||||
ApplyDdsReceiptDialogComponent,
|
||||
EntryDay,
|
||||
groupByDay
|
||||
} from './apply-dds-receipt-dialog/apply-dds-receipt-dialog.component';
|
||||
|
||||
/** Custom Services */
|
||||
import { OrganizationService } from 'app/organization/organization.service';
|
||||
import { AlertService } from 'app/core/alert/alert.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { announceHeldForApproval, heldForApproval } from 'app/deposits/dds-approvals/dds-approval-notice';
|
||||
|
||||
/**
|
||||
* Daily Transaction Entry component.
|
||||
@@ -45,7 +53,11 @@ import { Dates } from 'app/core/utils/dates';
|
||||
selector: 'mifosx-daily-transaction-entry',
|
||||
templateUrl: './daily-transaction-entry.component.html',
|
||||
styleUrls: ['./daily-transaction-entry.component.scss'],
|
||||
imports: [...STANDALONE_SHARED_IMPORTS]
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatAutocompleteModule,
|
||||
MatIconModule
|
||||
]
|
||||
})
|
||||
export class DailyTransactionEntryComponent implements OnInit {
|
||||
private formBuilder = inject(UntypedFormBuilder);
|
||||
@@ -301,9 +313,11 @@ export class DailyTransactionEntryComponent implements OnInit {
|
||||
row.get('amount')?.setValue(picked.amount);
|
||||
row.get('advanceDays')?.setValue(picked.advanceDays ?? '');
|
||||
row.get('arrearsDays')?.setValue(picked.arrearsDays ?? '');
|
||||
row.get('description')?.setValue(
|
||||
`${picked.days} ${picked.days === 1 ? 'day' : 'days'} ${mode === 'advance' ? 'in advance' : 'arrears'}`
|
||||
);
|
||||
row
|
||||
.get('description')
|
||||
?.setValue(
|
||||
`${picked.days} ${picked.days === 1 ? 'day' : 'days'} ${mode === 'advance' ? 'in advance' : 'arrears'}`
|
||||
);
|
||||
this.recalculateRowBalance(this.collections.length - 1);
|
||||
});
|
||||
}
|
||||
@@ -373,10 +387,11 @@ export class DailyTransactionEntryComponent implements OnInit {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads what this agent has outstanding for the date on the form.
|
||||
* Reads everything this agent has outstanding up to the date on the form, customer by customer.
|
||||
*
|
||||
* Asked again whenever the agent or the date changes, because both pick out a different day's
|
||||
* round - and a list left over from the previous choice is a list the clerk might settle.
|
||||
* Not only that day's: what was written on earlier days and not yet handed in is still owed, and a
|
||||
* clerk checking the round should see it in one list rather than open every date in turn. Each line
|
||||
* carries its own date. Asked again whenever the agent or the date changes.
|
||||
*/
|
||||
loadPendingEntries(): void {
|
||||
const agentId = this.entryForm.get('agentId')?.value;
|
||||
@@ -387,10 +402,12 @@ export class DailyTransactionEntryComponent implements OnInit {
|
||||
}
|
||||
|
||||
this.pendingLoading = true;
|
||||
const on = this.dateUtils.formatDate(date, 'yyyy-MM-dd');
|
||||
this.organizationService.getAgentCollectionDay(agentId, on).subscribe({
|
||||
const upTo = this.dateUtils.formatDate(date, 'yyyy-MM-dd');
|
||||
this.organizationService.getAgentPendingCollections(agentId).subscribe({
|
||||
next: (summary: any) => {
|
||||
this.pendingEntries = (summary?.entries || []).filter((entry: any) => entry.status === 'PENDING');
|
||||
this.pendingEntries = (summary?.entries || []).filter(
|
||||
(entry: any) => entry.status === 'PENDING' && String(entry.entryDate) <= upTo
|
||||
);
|
||||
this.pendingLoading = false;
|
||||
},
|
||||
error: () => {
|
||||
@@ -400,6 +417,138 @@ export class DailyTransactionEntryComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/** The picked agent's own GL account code - where the cashier receipts their cash. */
|
||||
agentGlCode: string | null = null;
|
||||
agentGlName: string | null = null;
|
||||
|
||||
// ── Agent Tran: the agent's money, owed and received ──────────────────────────────────────
|
||||
//
|
||||
// The agent's cash reaches the bank at the cashier's counter, never on this screen. What Agent Tran
|
||||
// shows is where that stands: what the agent owes on everything they wrote down, what the cashier has
|
||||
// taken in against it, and what is left - and from here the receipts are applied to the customers.
|
||||
|
||||
/** What the agent still has outstanding up to the date on the form, a day at a time. */
|
||||
agentDays: EntryDay[] = [];
|
||||
|
||||
/** DDS receipts on the agent's GL account not yet applied, including those still with the manager. */
|
||||
agentReceipts: any[] = [];
|
||||
|
||||
agentMoneyLoading = false;
|
||||
|
||||
/** The day whose lines are open in the Agent Tran list. */
|
||||
agentOpenDay: string | null = null;
|
||||
|
||||
get agentOwed(): number {
|
||||
return this.agentDays.reduce((sum, day) => sum + day.total, 0);
|
||||
}
|
||||
|
||||
get agentReceived(): number {
|
||||
return this.agentReceipts.filter((r) => r.status === 'APPROVED').reduce((sum, r) => sum + Number(r.amount || 0), 0);
|
||||
}
|
||||
|
||||
get agentWaiting(): number {
|
||||
return this.agentReceipts.filter((r) => r.status !== 'APPROVED').reduce((sum, r) => sum + Number(r.amount || 0), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* What is still to be taken from the agent for this date - never below nothing. The receipts are
|
||||
* not tied to a date (cash often reaches the counter the day after the round), so more received than
|
||||
* this date's entries is not a debt the other way; it is cash for another day, said so separately.
|
||||
*/
|
||||
get agentBalance(): number {
|
||||
return Math.max(0, Math.round((this.agentOwed - this.agentReceived) * 100) / 100);
|
||||
}
|
||||
|
||||
/** Received cash beyond this date's entries - most likely for another day's. */
|
||||
get agentReceivedBeyondDay(): number {
|
||||
return Math.max(0, Math.round((this.agentReceived - this.agentOwed) * 100) / 100);
|
||||
}
|
||||
|
||||
loadAgentMoney(): void {
|
||||
const agentId = this.entryForm.get('agentId')?.value;
|
||||
const date = this.entryForm.get('transactionDate')?.value;
|
||||
this.agentOpenDay = null;
|
||||
if (!agentId || !date) {
|
||||
this.agentDays = [];
|
||||
this.agentReceipts = [];
|
||||
return;
|
||||
}
|
||||
this.agentMoneyLoading = true;
|
||||
// Everything still outstanding up to the date on the screen, whatever day it was written: an agent
|
||||
// who comes in after three days brings three days' cash, and the counter has to see all of it to
|
||||
// know what to take.
|
||||
const upTo = this.dateUtils.formatDate(date, 'yyyy-MM-dd');
|
||||
this.organizationService.getAgentPendingCollections(agentId).subscribe({
|
||||
next: (summary: any) => {
|
||||
this.agentDays = groupByDay(
|
||||
(summary?.entries || []).filter((entry: any) => entry.status === 'PENDING' && String(entry.entryDate) <= upTo)
|
||||
);
|
||||
this.agentMoneyLoading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.agentDays = [];
|
||||
this.agentMoneyLoading = false;
|
||||
}
|
||||
});
|
||||
this.organizationService.getAgentCashReceipts(agentId, undefined, true).subscribe({
|
||||
next: (rows: any[]) => (this.agentReceipts = rows || []),
|
||||
error: () => (this.agentReceipts = [])
|
||||
});
|
||||
}
|
||||
|
||||
toggleAgentDay(date: string): void {
|
||||
this.agentOpenDay = this.agentOpenDay === date ? null : date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens Apply DDS Receipt: the cashier's DDS receipt on this agent's GL account, applied to what the
|
||||
* agent still has outstanding. Its own screen, like Apply Additional Shares, so the list above stays
|
||||
* what it is - what to take from the agent - and the applying is done in one deliberate place.
|
||||
*/
|
||||
openApplyReceipt(): void {
|
||||
const agentId = this.entryForm.get('agentId')?.value;
|
||||
if (!agentId) {
|
||||
return;
|
||||
}
|
||||
const picked = this.agents.find((agent: any) => agent.id === agentId);
|
||||
this.dialog
|
||||
.open(ApplyDdsReceiptDialogComponent, {
|
||||
width: '780px',
|
||||
maxWidth: '96vw',
|
||||
panelClass: 'apply-collection-dialog-panel',
|
||||
data: {
|
||||
agentId,
|
||||
agentCode: picked?.agentCode || '',
|
||||
agentName: this.agentInfo.name || picked?.agentNameId || '',
|
||||
glCode: this.agentGlCode,
|
||||
// The day chosen on the screen: its entries are what the receipts settle.
|
||||
entryDate: this.dateUtils.formatDate(this.entryForm.get('transactionDate')?.value, 'yyyy-MM-dd')
|
||||
}
|
||||
})
|
||||
.afterClosed()
|
||||
.subscribe((result: any) => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
const money = Number(result.amount || 0).toLocaleString('en-IN', { minimumFractionDigits: 2 });
|
||||
const vouchers: string[] = result.vouchers || [];
|
||||
if (heldForApproval(result.response)) {
|
||||
announceHeldForApproval(result.response, `Applying ${money}`, this.organizationService, this.alertService);
|
||||
this.loadPendingEntries();
|
||||
return;
|
||||
}
|
||||
this.alertService.alert({
|
||||
type: 'Success',
|
||||
message:
|
||||
`${vouchers.length === 1 ? 'Voucher' : 'Vouchers'} ${vouchers.join(', ')} applied - ` +
|
||||
`${result.entries} ${result.entries === 1 ? 'entry' : 'entries'}, ${money}`
|
||||
});
|
||||
this.loadPendingEntries();
|
||||
this.loadTodaysEntries();
|
||||
this.loadAgentMoney();
|
||||
});
|
||||
}
|
||||
|
||||
get isPartyTran(): boolean {
|
||||
return this.entryForm.get('transactionType').value === 'Party Tran';
|
||||
}
|
||||
@@ -487,6 +636,113 @@ export class DailyTransactionEntryComponent implements OnInit {
|
||||
next: (agents: any[]) => (this.agents = (agents || []).filter((agent: any) => !agent.closeDate)),
|
||||
error: () => (this.agents = [])
|
||||
});
|
||||
|
||||
// Withdraw Deposite: the open accounts either side can be picked from, and a code typed or picked
|
||||
// fills in whose account it is and what it holds, so the clerk checks the transfer against both.
|
||||
this.organizationService.getParties().subscribe({
|
||||
next: (parties: any) => {
|
||||
const all = Array.isArray(parties) ? parties : parties?.pageItems || [];
|
||||
this.openParties = all.filter((party: any) => party.status === 'ACTIVE');
|
||||
this.fillTransferParty('creditor');
|
||||
this.fillTransferParty('debit');
|
||||
},
|
||||
error: () => (this.openParties = [])
|
||||
});
|
||||
this.entryForm.get('creditorPartyCode')?.valueChanges.subscribe(() => this.fillTransferParty('creditor'));
|
||||
this.entryForm.get('debitPartyCode')?.valueChanges.subscribe(() => this.fillTransferParty('debit'));
|
||||
}
|
||||
|
||||
/** Open DDS accounts, for the two sides of a Withdraw Deposite. */
|
||||
openParties: any[] = [];
|
||||
|
||||
/**
|
||||
* The open accounts either side can be picked from. The server moves money only between two
|
||||
* accounts of the agent on the entry, so once an agent is chosen only that agent's accounts are offered.
|
||||
*/
|
||||
private get transferableParties(): any[] {
|
||||
const agentId = Number(this.entryForm.get('agentId')?.value) || null;
|
||||
return agentId ? this.openParties.filter((party: any) => Number(party.agentId) === agentId) : this.openParties;
|
||||
}
|
||||
|
||||
/** The open accounts matching what has been typed on one side - by code, name or account number, any case. */
|
||||
transferPartyOptions(side: 'creditor' | 'debit'): any[] {
|
||||
const typed = String(this.entryForm.get(side + 'PartyCode')?.value || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const other = String(this.entryForm.get(this.otherSide(side) + 'PartyCode')?.value || '')
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
// Once an account is picked the whole list comes back, so another can be chosen without clearing the box.
|
||||
const picked = this.transferableParties.some((party: any) => String(party.partyCode).toLowerCase() === typed);
|
||||
return this.transferableParties
|
||||
.filter((party: any) => String(party.partyCode).toUpperCase() !== other)
|
||||
.filter(
|
||||
(party: any) =>
|
||||
!typed ||
|
||||
picked ||
|
||||
String(party.partyCode).toLowerCase().includes(typed) ||
|
||||
String(party.partyName || '')
|
||||
.toLowerCase()
|
||||
.includes(typed) ||
|
||||
String(party.accountNo || '')
|
||||
.toLowerCase()
|
||||
.includes(typed)
|
||||
)
|
||||
.slice(0, 50);
|
||||
}
|
||||
|
||||
/** True when the same account has been put on both sides. */
|
||||
get transferToItself(): boolean {
|
||||
const creditor = String(this.entryForm.get('creditorPartyCode')?.value || '')
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
const debit = String(this.entryForm.get('debitPartyCode')?.value || '')
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
return this.isWithdrawDeposite && !!creditor && creditor === debit;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the box is left with a name or part of one typed in and only one account matches it, that
|
||||
* account is taken - so typing "amit" and moving on picks Amit's account without opening the list.
|
||||
*/
|
||||
settleTransferParty(side: 'creditor' | 'debit'): void {
|
||||
const control = this.entryForm.get(side + 'PartyCode');
|
||||
const typed = String(control?.value || '').trim();
|
||||
if (!typed || this.partyByCode(typed)) {
|
||||
return;
|
||||
}
|
||||
const matches = this.transferPartyOptions(side);
|
||||
if (matches.length === 1) {
|
||||
control?.setValue(matches[0].partyCode);
|
||||
}
|
||||
}
|
||||
|
||||
private otherSide(side: 'creditor' | 'debit'): 'creditor' | 'debit' {
|
||||
return side === 'creditor' ? 'debit' : 'creditor';
|
||||
}
|
||||
|
||||
private partyByCode(code: string): any {
|
||||
const wanted = String(code || '')
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
return wanted ? this.openParties.find((each: any) => String(each.partyCode).toUpperCase() === wanted) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills a side's name and balance from the code on it, and clears them when it matches no account.
|
||||
* The code is matched in any case and put back as the account has it, so "pt0002" becomes PT0002.
|
||||
*/
|
||||
private fillTransferParty(side: 'creditor' | 'debit'): void {
|
||||
const control = this.entryForm.get(side + 'PartyCode');
|
||||
const party = this.partyByCode(control?.value);
|
||||
if (party && control?.value !== party.partyCode) {
|
||||
control?.setValue(party.partyCode, { emitEvent: false });
|
||||
}
|
||||
this.entryForm.get(side + 'PartyName')?.setValue(party ? party.partyName : '', { emitEvent: false });
|
||||
this.entryForm
|
||||
.get(side + 'PartyBalance')
|
||||
?.setValue(party ? Number(party.currentBalance || 0) : 0, { emitEvent: false });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -527,6 +783,9 @@ export class DailyTransactionEntryComponent implements OnInit {
|
||||
if (!agentId) {
|
||||
this.agentParties = [];
|
||||
this.pendingEntries = [];
|
||||
this.agentGlCode = null;
|
||||
this.agentDays = [];
|
||||
this.agentReceipts = [];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -542,10 +801,20 @@ export class DailyTransactionEntryComponent implements OnInit {
|
||||
guarantor2: guarantors[1]?.guarantorName || '',
|
||||
currentBalance: agent.agentCurrentBalance || 0
|
||||
};
|
||||
this.agentGlCode = agent.glAccountCode || null;
|
||||
this.agentGlName = agent.glAccountName || null;
|
||||
},
|
||||
error: () => {
|
||||
this.agentLookupFailed = true;
|
||||
this.agentInfo = { name: '', address: '', city: '', accountNo: '', guarantor1: '', guarantor2: '', currentBalance: 0 };
|
||||
this.agentInfo = {
|
||||
name: '',
|
||||
address: '',
|
||||
city: '',
|
||||
accountNo: '',
|
||||
guarantor1: '',
|
||||
guarantor2: '',
|
||||
currentBalance: 0
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -559,6 +828,7 @@ export class DailyTransactionEntryComponent implements OnInit {
|
||||
|
||||
this.loadTodaysEntries();
|
||||
this.loadPendingEntries();
|
||||
this.loadAgentMoney();
|
||||
}
|
||||
|
||||
/** This agent's collections already recorded for the date on the screen. */
|
||||
@@ -574,8 +844,8 @@ export class DailyTransactionEntryComponent implements OnInit {
|
||||
next: (transactions: any[]) => {
|
||||
this.todaysEntries = (transactions || []).filter(
|
||||
(transaction: any) =>
|
||||
transaction.transactionType === 'PARTY_TRAN'
|
||||
&& this.dateUtils.formatDate(transaction.transactionDate, 'yyyy-MM-dd') === onDate
|
||||
transaction.transactionType === 'PARTY_TRAN' &&
|
||||
this.dateUtils.formatDate(transaction.transactionDate, 'yyyy-MM-dd') === onDate
|
||||
);
|
||||
},
|
||||
error: () => (this.todaysEntries = [])
|
||||
@@ -672,36 +942,18 @@ export class DailyTransactionEntryComponent implements OnInit {
|
||||
* warn about.
|
||||
*/
|
||||
save(): void {
|
||||
if (!this.isPartyTran) {
|
||||
this.postTransaction();
|
||||
// A Party Tran cannot be saved from here at all. The screen shows the agent's own entries so
|
||||
// the cash can be counted against them; taking that cash in is done where the branch takes cash
|
||||
// in, and two ways to credit the same customers is one way too many. The button is not rendered
|
||||
// for this type either - this is the same rule said again where it cannot be bypassed.
|
||||
// Agent Tran is not saved from here either any more. The agent's cash is taken in at the cashier's
|
||||
// counter, and a second way to take the same cash in is one way too many; Agent Tran shows where it
|
||||
// stands and applies the cashier's receipts.
|
||||
if (this.isPartyTran || this.isAgentTran) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.pendingEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const money = Number(this.pendingTotal).toLocaleString('en-IN', { minimumFractionDigits: 2 });
|
||||
const count = this.pendingEntries.length;
|
||||
|
||||
this.dialog
|
||||
.open(ConfirmationDialogComponent, {
|
||||
data: {
|
||||
type: 'warn',
|
||||
heading: 'Settle these collections',
|
||||
dialogContext:
|
||||
`Take ${money} from ${this.agentInfo.name || 'the agent'} and post `
|
||||
+ `${count} ${count === 1 ? 'collection' : 'collections'}? `
|
||||
+ 'Every customer on the list is credited and the cash is recorded against the agent. '
|
||||
+ 'This cannot be undone.'
|
||||
}
|
||||
})
|
||||
.afterClosed()
|
||||
.subscribe((response: any) => {
|
||||
if (response?.confirm) {
|
||||
this.postTransaction();
|
||||
}
|
||||
});
|
||||
this.postTransaction();
|
||||
}
|
||||
|
||||
private postTransaction(): void {
|
||||
@@ -743,10 +995,17 @@ export class DailyTransactionEntryComponent implements OnInit {
|
||||
// What was just posted, captured before the form is cleared, so the confirmation can name it.
|
||||
const posted = this.postedSummary(raw);
|
||||
|
||||
this.organizationService.createPartyTransaction(payload).subscribe(() => {
|
||||
this.organizationService.createPartyTransaction(payload).subscribe((response: any) => {
|
||||
// On a money screen a silent save is worse than no save: with nothing to confirm it, a clerk
|
||||
// who looks away reasonably assumes it did not go through and posts the whole round again.
|
||||
this.alertService.alert({ type: 'Success', message: posted });
|
||||
//
|
||||
// With maker-checker on, the request comes back with a command id and nothing done: it is
|
||||
// waiting for a checker, and saying it was posted would be telling the clerk the money moved.
|
||||
const awaitingApproval = !!response?.commandId && response?.resourceId == null;
|
||||
this.alertService.alert({
|
||||
type: 'Success',
|
||||
message: awaitingApproval ? `Sent for approval: ${posted}. Nothing moves until a checker approves it.` : posted
|
||||
});
|
||||
|
||||
this.collections.clear();
|
||||
this.partyInfo = null;
|
||||
@@ -794,8 +1053,18 @@ export class DailyTransactionEntryComponent implements OnInit {
|
||||
dateFormat: 'yyyy-MM-dd',
|
||||
locale: this.settingsService.language.code
|
||||
})
|
||||
.subscribe(() => {
|
||||
.subscribe((response: any) => {
|
||||
const money = Number(total).toLocaleString('en-IN', { minimumFractionDigits: 2 });
|
||||
if (heldForApproval(response)) {
|
||||
announceHeldForApproval(
|
||||
response,
|
||||
`${count} ${count === 1 ? 'collection' : 'collections'} for ${agent} (${money})`,
|
||||
this.organizationService,
|
||||
this.alertService
|
||||
);
|
||||
this.loadPendingEntries();
|
||||
return;
|
||||
}
|
||||
this.alertService.alert({
|
||||
type: 'Success',
|
||||
message: count + (count === 1 ? ' collection' : ' collections') + ' posted for ' + agent + ' - ' + money
|
||||
|
||||
Reference in New Issue
Block a user