mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-18 20:49:19 +00:00
* feat: initial SPA setup for banking * wip: bring over new banking module * feat: added Espresso design tokens * feat: button styles * fix: add all ink colors * wip: espresso design system changes * feat: button and badge espresso components * fix: button styling for reconcile * feat: Espresso progress bar * feat: Espresso toggle switch * feat: Espresso tabs design * fix: vertical tab support * fix: button sizing across modals * feat: Espresso style table layout * feat: Espresso tooltip * feat: Espresso elevations and checkbox * feat: Dialog with Espresso styles * feat: Espresso textarea * fix: input styles * fix: colors on bank picker * fix: breadcrumb styling * fix: bank picker styling * feat: create doctypes and fields for bank reconciliation * feat: APIs for banking * fix: use date format parser * fix: font styling to match Espresso * wip: settings modal * feat: settings dialog component * fix: icons and invalid requests * feat: preferences tab * fix: adjust icon stroke width to 1.5 * feat: rule configuration in settings * fix: remove sheet component * feat: alert and error banner component * feat: dropdown in Espresso * feat: popover and select in Espresso * fix: cleanup more styles * fix: match size of link fields * feat: command styling * fix: remove unused style tokens * fix: styles for global date picker dropdown * fix: styles for match and reconcile * feat: table Espresso component * feat: remove all other design tokens * fix: remove unused tokens * fix: form elements * fix: remove unused styles and fix filters in bank transaction list * feat: fetch bank rec doctypes for filtering * fix: record payment modal * feat: support for dark mode switching * fix: move bank logos to public folder * feat: add support for RTL * feat: support for RTL * chore: send layout direction in dev boot * fix: make checkbox work in RTL * feat: dark mode support * fix: dark mode style * feat: bank logos in dark mode * feat: dark mode bank logos * chore: use dark mode bank logos everywhere * chore: move rule evaluation to controller * chore: add tests for bank transaction rules * fix: move deps to fix actions errors * fix: move tw-animate-css to deps * fix: remove shadcn * fix: do not open modal if no transactions selected * fix: add translation strings * feat: add banner on existing bank reconciliation tool * feat: bank statement import * fix: translations and layout directions * fix: validation for transaction matching rule * fix: styles * fix: show conflicting transactions in alert * fix: show help text for new banking module forms * feat: show total debits and credits * fix: dark mode colors in automatic config * feat: add keyboard shortcuts help * feat: added keyboard shortcut for settings * fix: decrease size of progress bar * chore: bump packages * feat: add tests for statement import * fix: settings dialog * fix: show banner on small screens * fix: show banner when no bank account set
351 lines
19 KiB
TypeScript
351 lines
19 KiB
TypeScript
import _ from '@/lib/translate'
|
|
import { GetStatementDetailsResponse } from '../import_utils'
|
|
import { flt, formatCurrency } from '@/lib/numbers'
|
|
import { formatDate } from '@/lib/date'
|
|
import { bankRecDateAtom } from '../../BankReconciliation/bankRecAtoms'
|
|
import { AlertCircleIcon, ChevronLeftIcon, ChevronRightIcon, ExternalLinkIcon, InfoIcon, Loader2Icon } from 'lucide-react'
|
|
import { H2, H3, Paragraph } from '@/components/ui/typography'
|
|
import { FileTypeIcon } from '@/components/ui/file-dropzone'
|
|
import { getFileExtension } from '@/lib/file'
|
|
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
|
import { Separator } from '@/components/ui/separator'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
|
import { useFrappeEventListener, useFrappePostCall } from 'frappe-react-sdk'
|
|
import { toast } from 'sonner'
|
|
import ErrorBanner from '@/components/ui/error-banner'
|
|
import { Link, useNavigate } from 'react-router-dom'
|
|
import { useMemo, useState } from 'react'
|
|
import { Progress } from '@/components/ui/progress'
|
|
import { useSetAtom } from 'jotai'
|
|
import { useDirection } from '@/components/ui/direction'
|
|
import BankLogo from '@/components/common/BankLogo'
|
|
import { useGetBankAccounts } from '../../BankReconciliation/utils'
|
|
import { BankStatementImportLog } from '@/types/Accounts/BankStatementImportLog'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
|
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
|
|
|
const parseDateFormat = (dateFormat: string) => {
|
|
|
|
const charMap = {
|
|
"%d": "DD",
|
|
"%m": "MM",
|
|
"%Y": "YYYY",
|
|
"%y": "YY",
|
|
"%b": "MMM",
|
|
"%B": "MMMM",
|
|
}
|
|
|
|
let label = dateFormat
|
|
|
|
Object.keys(charMap).forEach((char) => {
|
|
label = label.replace(char, charMap[char as keyof typeof charMap])
|
|
})
|
|
|
|
return dateFormat
|
|
|
|
}
|
|
|
|
type Props = {
|
|
data: GetStatementDetailsResponse,
|
|
}
|
|
|
|
const StatementDetails = ({ data }: Props) => {
|
|
const dateFormat = parseDateFormat(data.date_format)
|
|
|
|
const { call, loading, error } = useFrappePostCall<{ docs: BankStatementImportLog[] }>('run_doc_method')
|
|
|
|
const navigate = useNavigate()
|
|
|
|
const setDates = useSetAtom(bankRecDateAtom)
|
|
|
|
const direction = useDirection()
|
|
|
|
const onImport = () => {
|
|
|
|
call({
|
|
docs: data.doc,
|
|
method: 'insert_transactions'
|
|
}).then((response) => {
|
|
const doc = response.docs ? response.docs[0] : undefined
|
|
if (doc && doc.start_date && doc.end_date) {
|
|
setDates({
|
|
fromDate: doc.start_date,
|
|
toDate: doc.end_date,
|
|
})
|
|
}
|
|
toast.success(_("Bank statement imported."))
|
|
navigate(`/`)
|
|
}).catch(() => {
|
|
toast.error(_("There was an error while importing the bank statement."))
|
|
})
|
|
|
|
}
|
|
|
|
const [progress, setProgress] = useState(0)
|
|
|
|
useFrappeEventListener("bank-rec-statement-import-progress", (event) => {
|
|
setProgress(event.progress)
|
|
})
|
|
|
|
const file_name = data.doc.file.split("/").pop() ?? ""
|
|
|
|
const { banks } = useGetBankAccounts()
|
|
|
|
const bank = useMemo(() => {
|
|
|
|
return banks?.find((bank) => bank.name === data.doc.bank_account)
|
|
|
|
}, [data.doc.bank_account, banks])
|
|
|
|
return (
|
|
<div className='flex flex-col gap-4'>
|
|
<div className='flex flex-col gap-4'>
|
|
<div className='flex justify-between items-center'>
|
|
<Button size='sm' variant='outline' asChild>
|
|
<Link to="/statement-importer">
|
|
{direction === 'ltr' ? <ChevronLeftIcon /> : <ChevronRightIcon />}
|
|
{_("Back")}
|
|
</Link>
|
|
</Button>
|
|
{data.doc.status === 'Completed' ? <Badge theme='green'>{_("Completed")}</Badge> :
|
|
<Button onClick={onImport} disabled={loading || data.final_transactions?.length === 0} size='sm' type='button'>
|
|
{loading ? <Loader2Icon className='size-4 animate-spin' /> : null}
|
|
{loading ? _("Importing...") : _("Import {0} transactions", [data.final_transactions?.length?.toString() || "0"])}</Button>
|
|
}
|
|
</div>
|
|
<div className='flex items-start gap-4'>
|
|
<div className='flex flex-col gap-1'>
|
|
<H2 className='text-lg border-0 p-0'>{_("Statement Details")}</H2>
|
|
<Paragraph className='text-p-sm'><span>
|
|
{_("We've auto-detected the details of the statement file.")}
|
|
</span><br />
|
|
<span>
|
|
{_("Please review the details below and click the 'Import' button to proceed.")}
|
|
</span>
|
|
</Paragraph>
|
|
</div>
|
|
</div>
|
|
|
|
{progress > 0 && <div className='flex flex-col gap-2'><Progress value={progress} max={100} size="lg" />
|
|
<span className='text-sm'>{_("Importing {0} transactions", [progress.toString()])}
|
|
</span>
|
|
</div>}
|
|
|
|
{error && <ErrorBanner error={error} />}
|
|
|
|
<Table>
|
|
<TableBody>
|
|
<TableRow>
|
|
<TableHead>{_("Bank Account")}</TableHead>
|
|
<TableCell>
|
|
<div className='flex items-center gap-2'>
|
|
<BankLogo bank={bank} />
|
|
<span className="tracking-tight text-sm font-medium">{bank?.account_name}</span>
|
|
<span title="GL Account" className="text-sm">{bank?.account}</span>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableHead>{_("Statement File")}</TableHead>
|
|
<TableCell>
|
|
<div className='flex items-center gap-2'>
|
|
<FileTypeIcon fileType={getFileExtension(file_name)} size='md' showBackground={false} />
|
|
{file_name}
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableHead>{_("Transaction Dates")}</TableHead>
|
|
<TableCell>{_("{0} to {1}", [formatDate(data.doc.start_date, "Do MMMM YYYY"), formatDate(data.doc.end_date, "Do MMMM YYYY")])}</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableHead>{_("Number of Transactions")}</TableHead>
|
|
<TableCell>{data.doc.number_of_transactions}</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableHead>{_("Total Debits")}</TableHead>
|
|
<TableCell><span className='font-numeric'>{formatCurrency(flt(data.doc.total_debits, 2), data.currency)}</span> <span className='text-ink-gray-5 font-sans'>({data.doc.total_debit_transactions} {data.doc.total_debit_transactions === 1 ? _("transaction") : _("transactions")})</span></TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableHead>{_("Total Credits")}</TableHead>
|
|
<TableCell><span className='font-numeric'>{formatCurrency(flt(data.doc.total_credits, 2), data.currency)}</span> <span className='text-ink-gray-5 font-sans'>({data.doc.total_credit_transactions} {data.doc.total_credit_transactions === 1 ? _("transaction") : _("transactions")})</span></TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableHead>{_("Closing Balance as of {}", [formatDate(data.doc.end_date, "Do MMMM YYYY")])}</TableHead>
|
|
<TableCell className='font-numeric'>{formatCurrency(flt(data.doc.closing_balance, 2), data.currency)}</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableHead>
|
|
<div className='flex items-center gap-2'>
|
|
{_("Detected Amount Format")} <Tooltip>
|
|
<TooltipTrigger><InfoIcon size={16} /></TooltipTrigger>
|
|
<TooltipContent>
|
|
{_("The amount format detected in the statement file. This is used to parse the deposit and withdrawal values from each row.")}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
</TableHead>
|
|
<TableCell>{data.doc.detected_amount_format}</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableHead>
|
|
<div className='flex items-center gap-2'>
|
|
{_("Detected Date Format")}
|
|
<Tooltip>
|
|
<TooltipTrigger><InfoIcon size={16} /></TooltipTrigger>
|
|
<TooltipContent>
|
|
{_("The date format detected in the statement file. This is used to parse the date values.")}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
</TableHead>
|
|
<TableCell>
|
|
{dateFormat || data.date_format} (e.g.{" "}
|
|
{formatDate(new Date(), dateFormat || "YYYY-MM-DD")})
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
{data.doc.status === "Not Started" ? <>
|
|
|
|
<ConflictingTransactions transactions={data.conflicting_transactions} />
|
|
|
|
<Separator />
|
|
|
|
<div className='flex flex-col gap-4'>
|
|
<div className='flex flex-col gap-1'>
|
|
<H3 className='text-base border-0 p-0'>{_("Preview Transactions")}</H3>
|
|
{data.final_transactions?.length === 1 ? (
|
|
<Paragraph className='text-p-sm'>{_("We've found 1 transaction in the statement file that will be imported into the system. Please review the details below and click the 'Import' button to proceed.")}</Paragraph>
|
|
) : (
|
|
<Paragraph className='text-p-sm'>{_("{0} transactions will be imported into the system. Please review the details below and click the 'Import' button to proceed.", [data.final_transactions?.length?.toString() || "0"])}</Paragraph>
|
|
)}
|
|
</div>
|
|
<div className='max-h-[400px] overflow-scroll pb-2'>
|
|
<Table>
|
|
<TableCaption>{_("Transactions to be imported into the system")}</TableCaption>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className='w-8'>#</TableHead>
|
|
<TableHead>{_("Date")}</TableHead>
|
|
<TableHead>{_("Description")}</TableHead>
|
|
<TableHead>{_("Ref.")}</TableHead>
|
|
<TableHead className='text-end'>{_("Withdrawal")}</TableHead>
|
|
<TableHead className='text-end'>{_("Deposit")}</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{data.final_transactions?.map((transaction, index) => (
|
|
<TableRow key={index}>
|
|
<TableCell className='w-8'>{index + 1}</TableCell>
|
|
<TableCell>{formatDate(transaction.date)}</TableCell>
|
|
<TableCell className='max-w-[200px] w-fit overflow-hidden text-ellipsis'>{transaction.description}</TableCell>
|
|
<TableCell className='max-w-[100px] w-fit overflow-hidden text-ellipsis'>{transaction.reference}</TableCell>
|
|
<TableCell className='text-end font-numeric'>{formatCurrency(transaction.withdrawal, data.currency)}</TableCell>
|
|
<TableCell className='text-end font-numeric'>{formatCurrency(transaction.deposit, data.currency)}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
</> : null}
|
|
</div>
|
|
|
|
)
|
|
}
|
|
|
|
const ConflictingTransactions = ({ transactions }: { transactions: GetStatementDetailsResponse["conflicting_transactions"] }) => {
|
|
|
|
if (transactions.length === 0) {
|
|
return null
|
|
}
|
|
|
|
return <>
|
|
<Alert theme="red">
|
|
<AlertCircleIcon />
|
|
<AlertTitle>{_("Conflicting Transactions")}</AlertTitle>
|
|
<AlertDescription>
|
|
{transactions.length === 1 ? _("We've found 1 existing transaction in the system that conflicts with the transactions in the statement file. Are you sure you want to proceed with the import?")
|
|
: _("We've found {0} existing transactions in the system that conflict with the transactions in the statement file. Are you sure you want to proceed with the import?", [transactions.length.toString()])}
|
|
|
|
<div className='py-2'>
|
|
<Dialog>
|
|
<DialogTrigger asChild>
|
|
<Button
|
|
size='sm'
|
|
type='button'
|
|
theme='red'
|
|
variant='solid'>
|
|
<span>{transactions.length > 1 ? _("View transactions") : _("View transaction")}</span>
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className='min-w-7xl'>
|
|
<DialogHeader>
|
|
<DialogTitle>{_("Conflicting Transactions")}</DialogTitle>
|
|
<DialogDescription>
|
|
{transactions.length === 1 ? _("We've found 1 existing transaction in the system that conflicts with the transactions in the statement file. Are you sure you want to proceed with the import?")
|
|
: _("We've found {0} existing transactions in the system that conflict with the transactions in the statement file. Are you sure you want to proceed with the import?", [transactions.length.toString()])}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className='max-h-[400px] overflow-scroll pb-2'>
|
|
<Table>
|
|
<TableCaption>{_("Existing transactions in the system belonging to the same bank account and date range")}</TableCaption>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>{_("Date")}</TableHead>
|
|
<TableHead>{_("Description")}</TableHead>
|
|
<TableHead>{_("Ref.")}</TableHead>
|
|
<TableHead className='text-end'>{_("Withdrawal")}</TableHead>
|
|
<TableHead className='text-end'>{_("Deposit")}</TableHead>
|
|
<TableHead></TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{transactions.map((transaction) => (
|
|
<TableRow key={transaction.name}>
|
|
<TableCell>{formatDate(transaction.date)}</TableCell>
|
|
<TableCell title={transaction.description} className='max-w-[200px] w-fit overflow-hidden text-ellipsis'>{transaction.description}</TableCell>
|
|
<TableCell title={transaction.reference_number} className='max-w-[100px] w-fit overflow-hidden text-ellipsis'>{transaction.reference_number ? transaction.reference_number : "-"}</TableCell>
|
|
<TableCell className='text-end font-numeric'>{formatCurrency(transaction.withdrawal, transaction.currency)}</TableCell>
|
|
<TableCell className='text-end font-numeric'>{formatCurrency(transaction.deposit, transaction.currency)}</TableCell>
|
|
<TableCell className='text-end'>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button variant='link' isIconButton asChild className='text-ink-gray-5 hover:text-black p-0 h-4'>
|
|
<a href={`/desk/bank-transaction/${transaction.name}`} target='_blank' rel='noopener noreferrer'>
|
|
<ExternalLinkIcon />
|
|
</a>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
{_("Open {0} in a new tab", [transaction.name])}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
<DialogFooter>
|
|
<DialogClose asChild>
|
|
<Button variant={'outline'} size='md' type='button'>{_("Close")}</Button>
|
|
</DialogClose>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
|
|
</Dialog>
|
|
</div>
|
|
</AlertDescription>
|
|
</Alert>
|
|
</>
|
|
}
|
|
|
|
export default StatementDetails |