mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-23 20:16:40 +00:00
fix(banking): use custom renderer for translated strings and parser for formula evaluation
356 lines
15 KiB
TypeScript
356 lines
15 KiB
TypeScript
import { useAtomValue } from "jotai"
|
|
import { MissingFiltersBanner } from "./MissingFiltersBanner"
|
|
import { bankRecDateAtom, SelectedBank, selectedBankAccountAtom } from "./bankRecAtoms"
|
|
import { useCurrentCompany } from "@/hooks/useCurrentCompany"
|
|
import type { ColumnDef } from "@tanstack/react-table"
|
|
import { useCallback, useMemo, useState } from "react"
|
|
import { useFrappeGetCall, useFrappePostCall, useSWRConfig } from "frappe-react-sdk"
|
|
import { QueryReportReturnType } from "@/types/custom/Reports"
|
|
import { formatDate } from "@/lib/date"
|
|
import { ListView, type ListViewColumnMeta } from "@/components/ui/list-view"
|
|
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
|
import { formatCurrency } from "@/lib/numbers"
|
|
import { getCompanyCurrency } from "@/lib/company"
|
|
import { slug } from "@/lib/frappe"
|
|
import { CheckCircle2, ReceiptTextIcon, XCircle } from "lucide-react"
|
|
import ErrorBanner from "@/components/ui/error-banner"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import _ from "@/lib/translate"
|
|
import { useCopyToClipboard } from "usehooks-ts"
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
|
import { toast } from "sonner"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
|
import { Form } from "@/components/ui/form"
|
|
import { useForm } from "react-hook-form"
|
|
import { DateField } from "@/components/ui/form-elements"
|
|
import { Empty, EmptyMedia, EmptyHeader, EmptyTitle, EmptyDescription } from "@/components/ui/empty"
|
|
import MarkdownRenderer from "@/components/ui/markdown"
|
|
|
|
const BankClearanceSummary = () => {
|
|
const bankAccount = useAtomValue(selectedBankAccountAtom)
|
|
const dates = useAtomValue(bankRecDateAtom)
|
|
|
|
if (!bankAccount) {
|
|
return <MissingFiltersBanner text={_("Please select a bank account to view the bank clearance summary.")} />
|
|
}
|
|
|
|
if (!dates) {
|
|
return <MissingFiltersBanner text={_("Please select dates to view the bank clearance summary.")} />
|
|
}
|
|
|
|
return <BankClearanceSummaryView />
|
|
}
|
|
interface BankClearanceSummaryEntry {
|
|
payment_document_type: string
|
|
payment_entry: string
|
|
posting_date: string,
|
|
cheque_no?: string,
|
|
amount: number,
|
|
against: string,
|
|
clearance_date: string,
|
|
}
|
|
|
|
const BankClearanceSummaryView = () => {
|
|
|
|
const companyID = useCurrentCompany()
|
|
const bankAccount = useAtomValue(selectedBankAccountAtom)
|
|
const dates = useAtomValue(bankRecDateAtom)
|
|
|
|
const filters = useMemo(() => {
|
|
return JSON.stringify({
|
|
account: bankAccount?.account,
|
|
from_date: dates.fromDate,
|
|
to_date: dates.toDate
|
|
})
|
|
}, [bankAccount, dates])
|
|
|
|
const { data, error, mutate } = useFrappeGetCall<{ message: QueryReportReturnType<BankClearanceSummaryEntry> }>('frappe.desk.query_report.run', {
|
|
report_name: 'Bank Clearance Summary',
|
|
filters,
|
|
ignore_prepared_report: 1,
|
|
are_default_filters: false,
|
|
}, `Report-Bank Clearance Summary-${filters}`, { keepPreviousData: true, revalidateOnFocus: false }, 'POST')
|
|
|
|
const formattedFromDate = formatDate(dates.fromDate)
|
|
const formattedToDate = formatDate(dates.toDate)
|
|
|
|
const [, copyToClipboard] = useCopyToClipboard()
|
|
|
|
const onCopy = useCallback(
|
|
(text: string) => {
|
|
copyToClipboard(text).then(() => {
|
|
toast.success(_("Copied to clipboard"))
|
|
})
|
|
},
|
|
[copyToClipboard],
|
|
)
|
|
|
|
const accountCurrency = useMemo(
|
|
() => bankAccount?.account_currency ?? getCompanyCurrency(companyID),
|
|
[bankAccount?.account_currency, companyID],
|
|
)
|
|
|
|
const clearanceColumns = useMemo<ColumnDef<BankClearanceSummaryEntry, unknown>[]>(
|
|
() => [
|
|
{
|
|
accessorKey: "payment_document_type",
|
|
header: _("Document Type"),
|
|
size: 140,
|
|
cell: ({ row }) => _(row.original.payment_document_type),
|
|
},
|
|
{
|
|
id: "payment_entry",
|
|
header: _("Payment Document"),
|
|
size: 160,
|
|
meta: {
|
|
getTooltipText: (r) => {
|
|
const x = r as BankClearanceSummaryEntry
|
|
return [x.payment_document_type, x.payment_entry].filter(Boolean).join(" · ") || undefined
|
|
},
|
|
} satisfies ListViewColumnMeta,
|
|
cell: ({ row }) => (
|
|
<a
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="text-ink-gray-8 block min-w-0 w-full underline underline-offset-4"
|
|
href={`/desk/${slug(row.original.payment_document_type)}/${row.original.payment_entry}`}
|
|
>
|
|
{row.original.payment_entry}
|
|
</a>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: "posting_date",
|
|
header: _("Posting Date"),
|
|
size: 118,
|
|
meta: { tabularNums: true } satisfies ListViewColumnMeta,
|
|
cell: ({ row }) => formatDate(row.original.posting_date),
|
|
},
|
|
{
|
|
accessorKey: "cheque_no",
|
|
header: _("Cheque/Reference Number"),
|
|
size: 160,
|
|
cell: ({ row }) => {
|
|
const ref = row.original.cheque_no ?? ""
|
|
return (
|
|
<Tooltip delayDuration={500}>
|
|
<TooltipTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="text-ink-gray-8 hover:underline min-w-0 w-full cursor-pointer truncate text-start underline-offset-4"
|
|
onClick={() => onCopy(ref)}
|
|
>
|
|
{ref}
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
{ref}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
|
|
)
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "clearance_date",
|
|
header: _("Clearance Date"),
|
|
size: 118,
|
|
meta: { tabularNums: true } satisfies ListViewColumnMeta,
|
|
cell: ({ row }) => formatDate(row.original.clearance_date),
|
|
},
|
|
{
|
|
accessorKey: "against",
|
|
header: _("Against Account"),
|
|
size: 250,
|
|
},
|
|
{
|
|
accessorKey: "amount",
|
|
header: _("Amount"),
|
|
size: 150,
|
|
meta: { align: "right" } satisfies ListViewColumnMeta,
|
|
cell: ({ row }) => <span className="font-numeric">{formatCurrency(row.original.amount, accountCurrency)}</span>,
|
|
},
|
|
{
|
|
id: "status",
|
|
header: _("Status"),
|
|
size: 200,
|
|
meta: { truncate: false, truncateTooltip: false } satisfies ListViewColumnMeta,
|
|
cell: ({ row }) => {
|
|
const r = row.original
|
|
return r.clearance_date ? (
|
|
<Badge theme="green">
|
|
<CheckCircle2 />
|
|
{_("Cleared")}
|
|
</Badge>
|
|
) : (
|
|
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
<Badge theme="red">
|
|
<XCircle />
|
|
{_("Not Cleared")}
|
|
</Badge>
|
|
<SetClearanceDateButton
|
|
voucher={r}
|
|
bankAccount={bankAccount}
|
|
companyID={companyID}
|
|
mutate={mutate}
|
|
/>
|
|
</div>
|
|
)
|
|
},
|
|
},
|
|
],
|
|
[accountCurrency, bankAccount, companyID, mutate, onCopy],
|
|
)
|
|
|
|
const content = _("Below is a list of all accounting entries posted against the bank account {0} between {1} and {2}.", [`<strong>${bankAccount?.account}</strong>`, `<strong>${formattedFromDate}</strong>`, `<strong>${formattedToDate}</strong>`])
|
|
|
|
return <div className="space-y-4 py-2">
|
|
|
|
<div>
|
|
<span className="text-p-sm">
|
|
<MarkdownRenderer content={content} />
|
|
</span>
|
|
</div>
|
|
|
|
{error && <ErrorBanner error={error} />}
|
|
|
|
{data && data.message.result.length > 0 ? (
|
|
<ListView
|
|
data={data.message.result}
|
|
columns={clearanceColumns}
|
|
getRowId={(row) => `${row.payment_entry}-${row.posting_date}`}
|
|
maxHeight="calc(100vh - 200px)"
|
|
scrollAreaClassName="min-h-[calc(100vh-200px)]"
|
|
emptyState={_("No rows to display.")}
|
|
/>
|
|
) : null}
|
|
|
|
{data && data.message.result.length == 0 &&
|
|
<Empty>
|
|
<EmptyMedia>
|
|
<ReceiptTextIcon />
|
|
</EmptyMedia>
|
|
<EmptyHeader>
|
|
<EmptyTitle>{_("No entries found")}</EmptyTitle>
|
|
<EmptyDescription>{_("There are no accounting entries in the system for the selected account and dates.")}</EmptyDescription>
|
|
</EmptyHeader>
|
|
</Empty>
|
|
}
|
|
|
|
|
|
</div>
|
|
}
|
|
|
|
const SetClearanceDateButton = ({ voucher, bankAccount, companyID, mutate }: { voucher: BankClearanceSummaryEntry, bankAccount: SelectedBank | null, companyID: string, mutate: VoidFunction }) => {
|
|
|
|
const [open, setOpen] = useState(false)
|
|
|
|
const onClose = () => {
|
|
setOpen(false)
|
|
mutate()
|
|
}
|
|
|
|
return <Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogTrigger disabled={!bankAccount}>
|
|
<Tooltip delayDuration={500}>
|
|
<TooltipTrigger>
|
|
<Button variant='link' size="sm" className="px-0" theme="red">{_("Force Clear")}</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent align='start'>
|
|
{_("Set the clearance date for this voucher without reconciling with a bank transaction.")}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</DialogTrigger>
|
|
<DialogContent className="min-w-2xl">
|
|
{bankAccount && <ForceClearVoucherForm voucher={voucher} bankAccount={bankAccount} companyID={companyID} onClose={onClose} />}
|
|
</DialogContent>
|
|
</Dialog>
|
|
}
|
|
|
|
const ForceClearVoucherForm = ({ voucher, bankAccount, companyID, onClose }: { voucher: BankClearanceSummaryEntry, bankAccount: SelectedBank, companyID: string, onClose: () => void }) => {
|
|
|
|
const { mutate } = useSWRConfig()
|
|
|
|
const dates = useAtomValue(bankRecDateAtom)
|
|
const form = useForm<{ clearance_date: string }>({
|
|
defaultValues: {
|
|
clearance_date: voucher.posting_date,
|
|
}
|
|
})
|
|
|
|
const { call, loading, error } = useFrappePostCall('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.update_clearance_date')
|
|
|
|
const onSubmit = (data: { clearance_date: string }) => {
|
|
call({
|
|
payment_document: voucher.payment_document_type,
|
|
payment_entry: voucher.payment_entry,
|
|
account: bankAccount.account,
|
|
clearance_date: data.clearance_date,
|
|
})
|
|
.then(() => {
|
|
toast.success(_("Clearance date updated"))
|
|
onClose()
|
|
mutate(`bank-reconciliation-account-closing-balance-${bankAccount?.name}-${dates.toDate}`)
|
|
})
|
|
}
|
|
|
|
return <Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
|
|
<div className='flex flex-col gap-4'>
|
|
|
|
<DialogHeader>
|
|
<DialogTitle>{_("Force Clear Voucher")}</DialogTitle>
|
|
<DialogDescription>
|
|
{_("Set the clearance date for this voucher without reconciling with a bank transaction.")}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
{error && <ErrorBanner error={error} />}
|
|
<div>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>{_("Payment Document")}</TableHead>
|
|
<TableCell><a target="_blank" className="underline underline-offset-4"
|
|
href={`/desk/${slug(voucher.payment_document_type)}/${voucher.payment_entry}`}>{_(voucher.payment_document_type)} : {voucher.payment_entry}</a></TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableHead>{_("Posting Date")}</TableHead>
|
|
<TableCell>{formatDate(voucher.posting_date)}</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableHead>{_("Cheque/Reference Number")}</TableHead>
|
|
<TableCell title={voucher.cheque_no}>{voucher.cheque_no?.slice(0, 40)}{voucher.cheque_no?.length && voucher.cheque_no?.length > 40 ? "..." : ""}</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableHead>{_("Amount")}</TableHead>
|
|
<TableCell className="text-end">{formatCurrency(voucher.amount, bankAccount?.account_currency ?? getCompanyCurrency(companyID))}</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableHead>{_("Against Account")}</TableHead>
|
|
<TableCell><a target="_blank" className="underline underline-offset-4" href={`/desk/account/${voucher.against}`}>{voucher.against}</a></TableCell>
|
|
</TableRow>
|
|
</TableHeader>
|
|
</Table>
|
|
</div>
|
|
<DateField
|
|
name='clearance_date'
|
|
label={_("Clearance Date")}
|
|
isRequired
|
|
inputProps={{ autoFocus: true }}
|
|
/>
|
|
|
|
<DialogFooter>
|
|
<DialogClose asChild>
|
|
<Button variant={'outline'} disabled={loading} size='md'>{_("Cancel")}</Button>
|
|
</DialogClose>
|
|
<Button type='submit' disabled={loading} size='md'>{_("Submit")}</Button>
|
|
</DialogFooter>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
}
|
|
|
|
export default BankClearanceSummary
|