import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { bankRecSelectedTransactionAtom, bankRecTransferModalAtom, bankRecUnreconcileModalAtom, SelectedBank, selectedBankAccountAtom } from './bankRecAtoms' import { Dialog, DialogContent, DialogHeader, DialogFooter, DialogClose, DialogTitle, DialogDescription } from '@/components/ui/dialog' import _ from '@/lib/translate' import { UnreconciledTransaction, useGetBankAccounts, useGetRuleForTransaction, useRefreshUnreconciledTransactions, useUpdateActionLog } from './utils' import { Button } from '@/components/ui/button' import SelectedTransactionDetails from './SelectedTransactionDetails' import { PaymentEntry } from '@/types/Accounts/PaymentEntry' import { useForm, useFormContext, useWatch } from 'react-hook-form' import { FrappeConfig, FrappeContext, useFrappeGetCall, useFrappePostCall } from 'frappe-react-sdk' import { toast } from 'sonner' import ErrorBanner from '@/components/ui/error-banner' import { H4 } from '@/components/ui/typography' import { cn } from '@/lib/utils' import { ArrowRight, Banknote, BadgeCheck, Calendar, ArrowUpRight, ArrowDownRight, CheckIcon, CheckCircle, ArrowLeft } from 'lucide-react' import { Separator } from '@/components/ui/separator' import { Form } from '@/components/ui/form' import { AccountFormField, DataField, DateField, SmallTextField } from '@/components/ui/form-elements' import SelectedTransactionsTable from './SelectedTransactionsTable' import { useCurrentCompany } from '@/hooks/useCurrentCompany' import { formatDate } from '@/lib/date' import { useContext, useMemo, useState } from 'react' import { formatCurrency } from '@/lib/numbers' import { Label } from '@/components/ui/label' import { FileDropzone } from '@/components/ui/file-dropzone' import FileUploadBanner from '@/components/common/FileUploadBanner' import { BankTransaction } from '@/types/Accounts/BankTransaction' import { useHotkeys } from 'react-hotkeys-hook' import { useDirection } from '@/components/ui/direction' import BankLogo from '@/components/common/BankLogo' const TransferModal = () => { const [isOpen, setIsOpen] = useAtom(bankRecTransferModalAtom) return ( {_("Transfer")} {_("Record an internal transfer to another bank/credit card/cash account.")} ) } const TransferModalContent = () => { const selectedBankAccount = useAtomValue(selectedBankAccountAtom) const selectedTransaction = useAtomValue(bankRecSelectedTransactionAtom(selectedBankAccount?.name ?? '')) if (!selectedTransaction || !selectedBankAccount || selectedTransaction.length === 0) { return
{_("No transaction selected")}
} if (selectedTransaction.length === 1) { return } return } const BulkInternalTransferForm = ({ transactions }: { transactions: UnreconciledTransaction[] }) => { const form = useForm<{ bank_account: string }>() const setIsOpen = useSetAtom(bankRecTransferModalAtom) const { call: createPaymentEntry, loading, error } = useFrappePostCall<{ message: { transaction: BankTransaction, payment_entry: PaymentEntry }[] }>('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_bulk_internal_transfer') const onReconcile = useRefreshUnreconciledTransactions() const addToActionLog = useUpdateActionLog() const onSubmit = (data: { bank_account: string }) => { createPaymentEntry({ bank_transaction_names: transactions.map((transaction) => transaction.name), bank_account: data.bank_account }).then(({ message }) => { addToActionLog({ type: 'transfer', timestamp: (new Date()).getTime(), isBulk: true, items: message.map((item) => ({ bankTransaction: item.transaction, voucher: { reference_doctype: "Payment Entry", reference_name: item.payment_entry.name, posting_date: item.payment_entry.posting_date, doc: item.payment_entry, } })), bulkCommonData: { bank_account: data.bank_account, } }) toast.success(_("Transfer Recorded"), { duration: 4000, closeButton: true, }) onReconcile(transactions[transactions.length - 1]) setIsOpen(false) }) } const onAccountChange = (account: string) => { form.setValue('bank_account', account) } const selectedAccount = useWatch({ control: form.control, name: 'bank_account' }) const currentCompany = useCurrentCompany() const company = transactions && transactions.length > 0 ? transactions[0].company : (currentCompany ?? '') console.log("This is here", transactions) return
{error && }
} interface InternalTransferFormFields extends PaymentEntry { mirror_transaction_name?: string } const InternalTransferForm = ({ selectedBankAccount, selectedTransaction }: { selectedBankAccount: SelectedBank, selectedTransaction: UnreconciledTransaction }) => { const setIsOpen = useSetAtom(bankRecTransferModalAtom) const onClose = () => { setIsOpen(false) } const { data: rule } = useGetRuleForTransaction(selectedTransaction) const isWithdrawal = (selectedTransaction.withdrawal && selectedTransaction.withdrawal > 0) ? true : false const form = useForm({ defaultValues: { payment_type: 'Internal Transfer', company: selectedTransaction?.company, // If the transaction is a withdrawal, set the paid from to the selected bank account paid_from: isWithdrawal ? selectedBankAccount.account : (rule?.account ?? ''), // If the transaction is a deposit, set the paid to to the selected bank account paid_to: !isWithdrawal ? selectedBankAccount.account : (rule?.account ?? ''), // Set the amount to the amount of the selected transaction paid_amount: selectedTransaction.unallocated_amount, received_amount: selectedTransaction.unallocated_amount, reference_date: selectedTransaction.date, posting_date: selectedTransaction.date, reference_no: (selectedTransaction.reference_number || selectedTransaction.description || '').slice(0, 140), } }) const onReconcile = useRefreshUnreconciledTransactions() const { call: createPaymentEntry, loading, error, isCompleted } = useFrappePostCall<{ message: { transaction: BankTransaction, payment_entry: PaymentEntry } }>('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_internal_transfer') const setBankRecUnreconcileModalAtom = useSetAtom(bankRecUnreconcileModalAtom) const addToActionLog = useUpdateActionLog() const { file: frappeFile } = useContext(FrappeContext) as FrappeConfig const [isUploading, setIsUploading] = useState(false) const [uploadProgress, setUploadProgress] = useState(0) const [files, setFiles] = useState([]) const onSubmit = (data: InternalTransferFormFields) => { createPaymentEntry({ bank_transaction_name: selectedTransaction.name, ...data, custom_remarks: data.remarks ? true : false, // Pass this to reconcile both at the same time mirror_transaction_name: data.mirror_transaction_name }).then(async ({ message }) => { addToActionLog({ type: 'transfer', timestamp: (new Date()).getTime(), isBulk: false, items: [ { bankTransaction: message.transaction, voucher: { reference_doctype: "Payment Entry", reference_name: message.payment_entry.name, reference_no: message.payment_entry.reference_no, reference_date: message.payment_entry.reference_date, posting_date: message.payment_entry.posting_date, doc: message.payment_entry, } } ] }) toast.success(_("Transfer Recorded"), { duration: 4000, closeButton: true, action: { label: _("Undo"), onClick: () => setBankRecUnreconcileModalAtom(selectedTransaction.name) }, actionButtonStyle: { backgroundColor: "rgb(0, 138, 46)" } }) if (files.length > 0) { setIsUploading(true) const uploadPromises = files.map(f => { return frappeFile.uploadFile(f, { isPrivate: true, doctype: "Payment Entry", docname: message.payment_entry.name, }, (_bytesUploaded, _totalBytes, progress) => { setUploadProgress((currentProgress) => { //If there are multiple files, we need to add the progress to the current progress return currentProgress + ((progress?.progress ?? 0) / files.length) }) }) }) return Promise.all(uploadPromises).then(() => { setUploadProgress(0) setIsUploading(false) }) } else { return Promise.resolve() } }).then(() => { setUploadProgress(0) setIsUploading(false) onReconcile(selectedTransaction) onClose() }) } useHotkeys('meta+s', () => { form.handleSubmit(onSubmit)() }, { enabled: true, preventDefault: true, enableOnFormTags: true }) const onAccountChange = (account: string, is_mirror: boolean = false) => { //If the transaction is a withdrawal, set the paid to to the selected account - since this is the account where the money is deposited into if (selectedTransaction.withdrawal && selectedTransaction.withdrawal > 0) { form.setValue('paid_to', account) } else { form.setValue('paid_from', account) } if (!is_mirror) { // Reset the mirror transaction name form.setValue('mirror_transaction_name', '') } } const selectedAccount = useWatch({ control: form.control, name: (selectedTransaction.deposit && selectedTransaction.deposit > 0) ? 'paid_from' : 'paid_to' }) const direction = useDirection() if (isUploading && isCompleted) { return } return
{error && }

{isWithdrawal ? _('Transferred to') : _('Transferred from')}

account.name !== selectedBankAccount.account} isRequired />
{direction === 'ltr' ? : }
account.name !== selectedBankAccount.account} />
} const BankOrCashPicker = ({ bankAccount, onAccountChange, selectedAccount, company }: { selectedAccount: string, bankAccount: string, onAccountChange: (account: string) => void, company: string }) => { const { banks } = useGetBankAccounts(undefined, (bank) => bank.name !== bankAccount) return
{banks.map((bank) => (
onAccountChange(bank.account ?? '')} >
{bank.account_name} {bank.bank_account_no && ({bank.bank_account_no})} {bank.account}
))}
} const CashPicker = ({ company, selectedAccount, setSelectedAccount }: { company: string, selectedAccount: string, setSelectedAccount: (account: string) => void }) => { const { data } = useFrappeGetCall('frappe.client.get_value', { doctype: 'Company', filters: company, fieldname: 'default_cash_account' }, undefined, { revalidateOnFocus: false, revalidateIfStale: false, }) const account = data?.message?.default_cash_account if (account) { return
setSelectedAccount(account ?? '')} >
Cash {data?.message?.default_cash_account}
} return null } const RecommendedTransferAccount = ({ transaction, onAccountChange }: { transaction: UnreconciledTransaction, onAccountChange: (account: string, is_mirror: boolean) => void }) => { const { setValue, watch } = useFormContext() const mirrorTransactionName = watch('mirror_transaction_name') const paid_from = watch('paid_from') const paid_to = watch('paid_to') const { data } = useFrappeGetCall('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.search_for_transfer_transaction', { transaction_id: transaction.name }, undefined, { revalidateOnFocus: false, revalidateIfStale: false, }) // Get bank accounts to find the logo const { banks } = useGetBankAccounts() const bank = useMemo(() => { if (data?.message?.bank_account && banks) { return banks.find(bank => bank.name === data.message.bank_account) } return null }, [data?.message?.bank_account, banks]) const selectTransaction = () => { if (data?.message) { setValue('mirror_transaction_name', data.message.name) onAccountChange(data.message.account, true) } } if (data?.message) { const isWithdrawal = data.message.withdrawal && data.message.withdrawal > 0 const amount = isWithdrawal ? data.message.withdrawal : data.message.deposit const currency = data.message.currency const isAccountSelected = isWithdrawal ? paid_from === data.message.account : paid_to === data.message.account const isSuggested = mirrorTransactionName === data?.message?.name && isAccountSelected return (
{_("Suggested Transfer to {0}", [data.message.account])}
{_("The system found a mirror transaction ({0}) in another account with the same amount and date.", [data.message.name])} {_("Accepting the suggestion will reconcile both transactions.")}
{formatDate(data.message.date, 'Do MMM YYYY')}
{data.message.description}
{isWithdrawal ? : } {isWithdrawal ? _('Transferred Out') : _('Received')}
{formatCurrency(amount, currency)}
) } return null } export default TransferModal