mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-14 18:51:21 +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
255 lines
12 KiB
TypeScript
255 lines
12 KiB
TypeScript
import BankPicker from "@/components/features/BankReconciliation/BankPicker"
|
|
import { selectedBankAccountAtom } from "@/components/features/BankReconciliation/bankRecAtoms"
|
|
import CompanySelector from "@/components/features/BankReconciliation/CompanySelector"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
|
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"
|
|
import ErrorBanner from "@/components/ui/error-banner"
|
|
import { FileDropzone } from "@/components/ui/file-dropzone"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
|
import { H3, Paragraph } from "@/components/ui/typography"
|
|
import { useCurrentCompany } from "@/hooks/useCurrentCompany"
|
|
import { formatDate } from "@/lib/date"
|
|
import { flt, formatCurrency } from "@/lib/numbers"
|
|
import _ from "@/lib/translate"
|
|
import { cn } from "@/lib/utils"
|
|
import { BankStatementImportLog } from "@/types/Accounts/BankStatementImportLog"
|
|
import { useFrappeCreateDoc, useFrappeFileUpload, useFrappeGetDocList } from "frappe-react-sdk"
|
|
import { useAtom, useAtomValue } from "jotai"
|
|
import { ListIcon, Loader2Icon } from "lucide-react"
|
|
import { useState } from "react"
|
|
import { useNavigate } from "react-router"
|
|
|
|
|
|
const BankStatementImporter = () => {
|
|
|
|
const selectedCompany = useCurrentCompany()
|
|
|
|
const [selectedBankAccount] = useAtom(selectedBankAccountAtom)
|
|
|
|
const [files, setFiles] = useState<File[]>([])
|
|
|
|
const { upload, error, loading } = useFrappeFileUpload()
|
|
|
|
const navigate = useNavigate()
|
|
const { createDoc, loading: createLoading, error: createError } = useFrappeCreateDoc<BankStatementImportLog>()
|
|
|
|
const onUpload = () => {
|
|
|
|
if (!selectedBankAccount) {
|
|
return
|
|
}
|
|
|
|
const id = `new-bank-statement-import-log-${Date.now()}`
|
|
|
|
upload(files[0], {
|
|
isPrivate: true,
|
|
doctype: "Bank Statement Import Log",
|
|
docname: id,
|
|
fieldname: 'file'
|
|
}).then((file) => {
|
|
return createDoc("Bank Statement Import Log",
|
|
// @ts-expect-error - not filling everything else
|
|
{
|
|
name: id,
|
|
file: file.file_url,
|
|
bank_account: selectedBankAccount.name
|
|
})
|
|
}).then((doc) => {
|
|
navigate(`/statement-importer/${doc.name}`)
|
|
})
|
|
}
|
|
|
|
return (
|
|
<div className="flex px-4">
|
|
<div className="w-[52%]">
|
|
{error && <ErrorBanner error={error} />}
|
|
{createError && <ErrorBanner error={createError} />}
|
|
<div className="py-2 flex flex-col gap-6">
|
|
<div className="flex flex-col gap-2">
|
|
<Label>{_("Company")}<span className="text-ink-red-3">*</span></Label>
|
|
<div className="min-w-56 w-fit flex flex-col">
|
|
<CompanySelector />
|
|
</div>
|
|
</div>
|
|
{selectedCompany && <div className="flex flex-col gap-2">
|
|
<Label>{_("Bank Account")}<span className="text-ink-red-3">*</span></Label>
|
|
<div className="">
|
|
<BankPicker className="w-full flex-wrap" />
|
|
</div>
|
|
</div>
|
|
}
|
|
{selectedBankAccount && <div className="flex flex-col gap-4 pe-4">
|
|
<div className="flex justify-between">
|
|
<div className="flex flex-col gap-2">
|
|
<Label>{_("Bank Statement")}<span className="text-ink-red-3">*</span></Label>
|
|
<p
|
|
data-slot="form-description"
|
|
className={cn("text-ink-gray-5 text-xs")}
|
|
>
|
|
{_("Upload your bank statement file to start the import process. We support CSV, and XLSX files.")}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<StatementInstructions />
|
|
</div>
|
|
</div>
|
|
|
|
<FileDropzone
|
|
setFiles={setFiles}
|
|
files={files}
|
|
className="p-8"
|
|
accept={{
|
|
'text/csv': ['.csv'],
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
|
|
'application/vnd.ms-excel': ['.xls'],
|
|
// 'application/xml': ['.xml'],
|
|
}}
|
|
multiple={false}
|
|
/>
|
|
</div>}
|
|
<div className="flex justify-end px-4">
|
|
<Button
|
|
onClick={onUpload}
|
|
size='md'
|
|
disabled={files.length === 0 || loading || createLoading || !selectedBankAccount || !selectedCompany}>
|
|
{loading || createLoading ? <Loader2Icon className="size-4 animate-spin" /> : null}
|
|
{loading || createLoading ? _("Uploading...") : _("Upload")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="w-[48%] border-s border-outline-gray-2 ps-4">
|
|
{selectedBankAccount && <StatementImportLog />}
|
|
</div>
|
|
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const StatementInstructions = () => {
|
|
return <Dialog>
|
|
<DialogTrigger asChild>
|
|
<Button variant='outline' size='sm'>{_("View Instructions")}</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="min-w-7xl">
|
|
<DialogHeader>
|
|
<DialogTitle>{_("Statement Import Instructions")}</DialogTitle>
|
|
<DialogDescription>{_("We support uploading CSV, XLSX and XLS files. Please make sure the file contains the correct columns.")}</DialogDescription>
|
|
</DialogHeader>
|
|
<Paragraph className="text-sm">{_("The file should contain the following columns with a distinct header row. You can upload most bank statements as is without changing the columns.")}</Paragraph>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>{_("Column Name")}</TableHead>
|
|
<TableHead>{_("Maps To")}</TableHead>
|
|
<TableHead>{_("Description")}</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
<TableRow>
|
|
<TableCell>Date/Transaction Date/Value Date</TableCell>
|
|
<TableCell>{_("Date")}</TableCell>
|
|
<TableCell className="text-ink-gray-5">{_("The date of the transaction")}</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableCell>Amount</TableCell>
|
|
<TableCell>{_("Amount")}</TableCell>
|
|
<TableCell className="text-ink-gray-5">{_('This can contain "CR"/"DR" values or positive/negative values. You could also have a separate column for CR/DR.')}</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableCell>Withdrawal/Deposit</TableCell>
|
|
<TableCell>{_("Withdrawal")}/{_("Deposit")}</TableCell>
|
|
<TableCell className="text-ink-gray-5">{_("The withdrawal or deposit amounts - only required if there's no amount column.")}</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableCell>Description/Particulars/Remarks/Narration/Detail</TableCell>
|
|
<TableCell>{_("Description")}</TableCell>
|
|
<TableCell className="text-ink-gray-5">{_("The description of the transaction")}</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableCell>Reference/Ref/Transaction ID/Cheque/Check</TableCell>
|
|
<TableCell>{_("Reference")}</TableCell>
|
|
<TableCell className="text-ink-gray-5">{_("The reference number of the transaction")}</TableCell>
|
|
</TableRow>
|
|
</TableBody>
|
|
</Table>
|
|
<DialogFooter>
|
|
<DialogClose asChild>
|
|
<Button variant='outline'>{_("Close")}</Button>
|
|
</DialogClose>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
}
|
|
|
|
const StatementImportLog = () => {
|
|
|
|
const bankAccount = useAtomValue(selectedBankAccountAtom)
|
|
|
|
const { data, error } = useFrappeGetDocList<BankStatementImportLog>("Bank Statement Import Log", {
|
|
fields: ["name", "file", "status", "number_of_transactions", "start_date", "end_date", "closing_balance", "creation"],
|
|
filters: [["bank_account", "=", bankAccount?.name ?? ""]],
|
|
orderBy: {
|
|
field: "creation",
|
|
order: "desc"
|
|
},
|
|
limit: 10
|
|
}, bankAccount ? undefined : null, {
|
|
revalidateOnFocus: false
|
|
})
|
|
|
|
const navigate = useNavigate()
|
|
|
|
const onViewDetails = (name: string) => {
|
|
navigate(`/statement-importer/${name}`)
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<H3 className="text-base">{_("Previous Imports")}</H3>
|
|
|
|
{error && <ErrorBanner error={error} />}
|
|
|
|
{data && data.length > 0 ? (
|
|
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>{_("Imported On")}</TableHead>
|
|
<TableHead>{_("Status")}</TableHead>
|
|
<TableHead>{_("Transaction Dates")}</TableHead>
|
|
<TableHead className="text-end">{_("Number of Transactions")}</TableHead>
|
|
<TableHead className="text-end">{_("Closing Balance")}</TableHead>
|
|
<TableHead>{_("File")}</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{data?.map((item) => (
|
|
<TableRow key={item.name} onClick={() => onViewDetails(item.name)} className="cursor-pointer hover:bg-surface-gray-2">
|
|
<TableCell>{formatDate(item.creation, 'Do MMM YYYY')}</TableCell>
|
|
<TableCell><Badge theme={item.status === "Completed" ? "green" : "gray"}>{item.status}</Badge></TableCell>
|
|
<TableCell>{formatDate(item.start_date, 'Do MMM YYYY')} to {formatDate(item.end_date, 'Do MMM YYYY')}</TableCell>
|
|
<TableCell className="text-end">{item.number_of_transactions}</TableCell>
|
|
<TableCell className="text-end font-numeric">{formatCurrency(flt(item.closing_balance, 2))}</TableCell>
|
|
<TableCell><a
|
|
href={item.file}
|
|
target="_blank" className="underline underline-offset-4">{item.file.split('/').pop()}</a></TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>)
|
|
: <Empty>
|
|
<EmptyHeader>
|
|
<EmptyMedia>
|
|
<ListIcon />
|
|
</EmptyMedia>
|
|
<EmptyTitle>{_("No bank statements imported yet")}</EmptyTitle>
|
|
</EmptyHeader>
|
|
</Empty>}
|
|
</div>
|
|
)
|
|
}
|
|
export default BankStatementImporter |