From 602f15fe2e85fe558e94648efdf96e6a80024303 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Sat, 26 Jul 2025 00:58:38 +0800 Subject: [PATCH] export reconciliation statements --- src/locales/de.json | 2 + src/locales/en.json | 2 + src/locales/es.json | 2 + src/locales/it.json | 2 + src/locales/ja.json | 2 + src/locales/pt_BR.json | 2 + src/locales/ru.json | 2 + src/locales/uk.json | 2 + src/locales/vi.json | 2 + src/locales/zh_Hans.json | 2 + src/locales/zh_Hant.json | 2 + .../ReconciliationStatementPageBase.ts | 112 +++++++++++++++++- .../dialogs/ReconciliationStatementDialog.vue | 31 ++++- 13 files changed, 162 insertions(+), 3 deletions(-) diff --git a/src/locales/de.json b/src/locales/de.json index 8a134569..b45b622f 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -122,6 +122,8 @@ "exportFilename": "ezBookkeeping_{nickname}_export_data", "defaultExportStatisticsFileName": "ezBookkeeping_statistics_data", "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data", + "defaultExportReconciliationStatementsFileName": "ezBookkeeping_reconciliation_statements", + "exportReconciliationStatementsFileName": "ezBookkeeping_{nickname}_reconciliation_statements", "defaultImportDataMappingFileName": "ezBookkeeping_import_data_mapping", "defaultImportReplaceRuleFileName": "ezBookkeeping_import_replace_rule" }, diff --git a/src/locales/en.json b/src/locales/en.json index 9598009b..7c7e1f9d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -122,6 +122,8 @@ "exportFilename": "ezBookkeeping_{nickname}_export_data", "defaultExportStatisticsFileName": "ezBookkeeping_statistics_data", "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data", + "defaultExportReconciliationStatementsFileName": "ezBookkeeping_reconciliation_statements", + "exportReconciliationStatementsFileName": "ezBookkeeping_{nickname}_reconciliation_statements", "defaultImportDataMappingFileName": "ezBookkeeping_import_data_mapping", "defaultImportReplaceRuleFileName": "ezBookkeeping_import_replace_rule" }, diff --git a/src/locales/es.json b/src/locales/es.json index 26ba9b0c..44db578d 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -122,6 +122,8 @@ "exportFilename": "ezBookkeeping_{nickname}_export_data", "defaultExportStatisticsFileName": "ezBookkeeping_statistics_data", "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data", + "defaultExportReconciliationStatementsFileName": "ezBookkeeping_reconciliation_statements", + "exportReconciliationStatementsFileName": "ezBookkeeping_{nickname}_reconciliation_statements", "defaultImportDataMappingFileName": "ezBookkeeping_import_data_mapping", "defaultImportReplaceRuleFileName": "ezBookkeeping_import_replace_rule" }, diff --git a/src/locales/it.json b/src/locales/it.json index 4a87b15e..b7538f8a 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -122,6 +122,8 @@ "exportFilename": "ezBookkeeping_{nickname}_export_data", "defaultExportStatisticsFileName": "ezBookkeeping_statistics_data", "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data", + "defaultExportReconciliationStatementsFileName": "ezBookkeeping_reconciliation_statements", + "exportReconciliationStatementsFileName": "ezBookkeeping_{nickname}_reconciliation_statements", "defaultImportDataMappingFileName": "ezBookkeeping_import_data_mapping", "defaultImportReplaceRuleFileName": "ezBookkeeping_import_replace_rule" }, diff --git a/src/locales/ja.json b/src/locales/ja.json index a49812d4..2dc8ea17 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -122,6 +122,8 @@ "exportFilename": "ezBookkeeping_{nickname}_エクスポートデータ", "defaultExportStatisticsFileName": "ezBookkeeping_statistics_data", "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data", + "defaultExportReconciliationStatementsFileName": "ezBookkeeping_reconciliation_statements", + "exportReconciliationStatementsFileName": "ezBookkeeping_{nickname}_reconciliation_statements", "defaultImportDataMappingFileName": "ezBookkeeping_import_data_mapping", "defaultImportReplaceRuleFileName": "ezBookkeeping_import_replace_rule" }, diff --git a/src/locales/pt_BR.json b/src/locales/pt_BR.json index 551ed1df..1d7f55e9 100644 --- a/src/locales/pt_BR.json +++ b/src/locales/pt_BR.json @@ -122,6 +122,8 @@ "exportFilename": "ezBookkeeping_{nickname}_export_data", "defaultExportStatisticsFileName": "ezBookkeeping_dados_estatísticos", "exportStatisticsFileName": "ezBookkeeping_{nickname}_dados_estatísticos", + "defaultExportReconciliationStatementsFileName": "ezBookkeeping_reconciliation_statements", + "exportReconciliationStatementsFileName": "ezBookkeeping_{nickname}_reconciliation_statements", "defaultImportDataMappingFileName": "ezBookkeeping_import_data_mapping", "defaultImportReplaceRuleFileName": "ezBookkeeping_import_replace_rule" }, diff --git a/src/locales/ru.json b/src/locales/ru.json index bc2d3ca2..1aa73415 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -122,6 +122,8 @@ "exportFilename": "ezBookkeeping_{nickname}_export_data", "defaultExportStatisticsFileName": "ezBookkeeping_statistics_data", "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data", + "defaultExportReconciliationStatementsFileName": "ezBookkeeping_reconciliation_statements", + "exportReconciliationStatementsFileName": "ezBookkeeping_{nickname}_reconciliation_statements", "defaultImportDataMappingFileName": "ezBookkeeping_import_data_mapping", "defaultImportReplaceRuleFileName": "ezBookkeeping_import_replace_rule" }, diff --git a/src/locales/uk.json b/src/locales/uk.json index a2f0cbf0..002fd3b9 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -122,6 +122,8 @@ "exportFilename": "ezBookkeeping_{nickname}_export_data", "defaultExportStatisticsFileName": "ezBookkeeping_statistics_data", "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data", + "defaultExportReconciliationStatementsFileName": "ezBookkeeping_reconciliation_statements", + "exportReconciliationStatementsFileName": "ezBookkeeping_{nickname}_reconciliation_statements", "defaultImportDataMappingFileName": "ezBookkeeping_import_data_mapping", "defaultImportReplaceRuleFileName": "ezBookkeeping_import_replace_rule" }, diff --git a/src/locales/vi.json b/src/locales/vi.json index 62785260..c54b7e05 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -122,6 +122,8 @@ "exportFilename": "ezBookkeeping_{nickname}_export_data", "defaultExportStatisticsFileName": "ezBookkeeping_statistics_data", "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data", + "defaultExportReconciliationStatementsFileName": "ezBookkeeping_reconciliation_statements", + "exportReconciliationStatementsFileName": "ezBookkeeping_{nickname}_reconciliation_statements", "defaultImportDataMappingFileName": "ezBookkeeping_import_data_mapping", "defaultImportReplaceRuleFileName": "ezBookkeeping_import_replace_rule" }, diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index 5b8c0de8..9e3d754f 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -122,6 +122,8 @@ "exportFilename": "ezBookkeeping_{nickname}_导出数据", "defaultExportStatisticsFileName": "ezBookkeeping_统计数据", "exportStatisticsFileName": "ezBookkeeping_{nickname}_统计数据", + "defaultExportReconciliationStatementsFileName": "ezBookkeeping_对账单", + "exportReconciliationStatementsFileName": "ezBookkeeping_{nickname}_对账单", "defaultImportDataMappingFileName": "ezBookkeeping_导入数据映射文件", "defaultImportReplaceRuleFileName": "ezBookkeeping_导入替换规则文件" }, diff --git a/src/locales/zh_Hant.json b/src/locales/zh_Hant.json index 21b6938d..2e36433f 100644 --- a/src/locales/zh_Hant.json +++ b/src/locales/zh_Hant.json @@ -122,6 +122,8 @@ "exportFilename": "ezBookkeeping_{nickname}_匯出資料", "defaultExportStatisticsFileName": "ezBookkeeping_統計資料", "exportStatisticsFileName": "ezBookkeeping_{nickname}_統計資料", + "defaultExportReconciliationStatementsFileName": "ezBookkeeping_對帳單", + "exportReconciliationStatementsFileName": "ezBookkeeping_{nickname}_對帳單", "defaultImportDataMappingFileName": "ezBookkeeping_匯入資料對應檔案", "defaultImportReplaceRuleFileName": "ezBookkeeping_匯入替換規則檔案" }, diff --git a/src/views/base/transactions/ReconciliationStatementPageBase.ts b/src/views/base/transactions/ReconciliationStatementPageBase.ts index 4a3263b6..6a0299c6 100644 --- a/src/views/base/transactions/ReconciliationStatementPageBase.ts +++ b/src/views/base/transactions/ReconciliationStatementPageBase.ts @@ -7,21 +7,32 @@ import { useUserStore } from '@/stores/user.ts'; import { useAccountsStore } from '@/stores/account.ts'; import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts'; +import { KnownDateTimeFormat } from '@/core/datetime.ts'; import { TransactionType } from '@/core/transaction.ts'; +import { KnownFileType } from '@/core/file.ts'; import type { Account } from '@/models/account.ts'; import type { TransactionCategory } from '@/models/transaction_category.ts'; import type { TransactionReconciliationStatementResponseItem } from '@/models/transaction.ts'; +import { + replaceAll, + removeAll +} from '@/lib/common.ts'; + import { getUtcOffsetByUtcOffsetMinutes, getTimezoneOffsetMinutes, parseDateFromUnixTime, + formatUnixTime, getUnixTime } from '@/lib/datetime.ts'; export function useReconciliationStatementPageBase() { const { + tt, + getCurrentDigitGroupingSymbol, formatUnixTimeToLongDateTime, + formatAmount, formatAmountWithCurrency } = useI18n(); @@ -40,6 +51,18 @@ export function useReconciliationStatementPageBase() { const currentTimezoneOffsetMinutes = computed(() => getTimezoneOffsetMinutes(settingsStore.appSettings.timeZone)); const defaultCurrency = computed(() => userStore.currentUserDefaultCurrency); + const exportFileName = computed(() => { + const nickname = userStore.currentUserNickname; + + if (nickname) { + return tt('dataExport.exportReconciliationStatementsFileName', { + nickname: nickname + }); + } + + return tt('dataExport.defaultExportReconciliationStatementsFileName'); + }); + const allAccountsMap = computed>(() => accountsStore.allAccountsMap); const allCategoriesMap = computed>(() => transactionCategoriesStore.allTransactionCategoriesMap); @@ -183,6 +206,91 @@ export function useReconciliationStatementPageBase() { } } + function getExportedData(fileType: KnownFileType): string { + let separator = ','; + + if (fileType === KnownFileType.TSV) { + separator = '\t'; + } + + let isLiabilityAccount = false; + + if (allAccountsMap.value[accountId.value]) { + isLiabilityAccount = allAccountsMap.value[accountId.value].isLiability; + } + + const digitGroupingSymbol = getCurrentDigitGroupingSymbol(); + const accountBalanceName = isLiabilityAccount ? 'Account Outstanding Balance' : 'Account Balance'; + + const header = [ + tt('Transaction Time'), + tt('Type'), + tt('Category'), + tt('Amount'), + tt('Account'), + tt(accountBalanceName), + tt('Description') + ].join(separator) + '\n'; + + const rows = reconciliationStatements.value.map(transaction => { + const transactionTime = getUnixTime(parseDateFromUnixTime(transaction.time, transaction.utcOffset, currentTimezoneOffsetMinutes.value)); + let type = ''; + let categoryName = allCategoriesMap.value[transaction.categoryId]?.name || ''; + let displayAmount = removeAll(formatAmount(transaction.sourceAmount), digitGroupingSymbol); + let displayAccountName = allAccountsMap.value[transaction.sourceAccountId]?.name || ''; + + if (transaction.type === TransactionType.ModifyBalance) { + type = tt('Modify Balance'); + categoryName = '-'; + } else if (transaction.type === TransactionType.Income) { + type = tt('Income'); + } else if (transaction.type === TransactionType.Expense) { + type = tt('Expense'); + } else if (transaction.type === TransactionType.Transfer && transaction.destinationAccountId === accountId.value) { + type = tt('Transfer In'); + displayAmount = removeAll(formatAmount(transaction.destinationAmount), digitGroupingSymbol); + } else if (transaction.type === TransactionType.Transfer && transaction.sourceAccountId === accountId.value) { + type = tt('Transfer Out'); + } else if (transaction.type === TransactionType.Transfer) { + type = tt('Transfer'); + } else { + type = tt('Unknown'); + } + + if (transaction.type === TransactionType.Transfer && allAccountsMap.value[transaction.destinationAccountId]) { + displayAccountName = displayAccountName + ' → ' + (allAccountsMap.value[transaction.destinationAccountId]?.name || ''); + } + + let displayAccountBalance = ''; + + if (isLiabilityAccount) { + displayAccountBalance = removeAll(formatAmount(-transaction.accountBalance), digitGroupingSymbol); + } else { + displayAccountBalance = removeAll(formatAmount(transaction.accountBalance), digitGroupingSymbol); + } + + let description = transaction.comment || ''; + + if (fileType === KnownFileType.CSV) { + description = replaceAll(description, ',', ' '); + } else if (fileType === KnownFileType.TSV) { + description = replaceAll(description, '\t', ' '); + } + + return [ + formatUnixTime(transactionTime, KnownDateTimeFormat.DefaultDateTime.format), + type, + categoryName, + displayAmount, + displayAccountName, + displayAccountBalance, + description + ].join(separator); + }); + + return header + rows.join('\n'); + } + return { // states accountId, @@ -194,6 +302,7 @@ export function useReconciliationStatementPageBase() { // computed states currentTimezoneOffsetMinutes, defaultCurrency, + exportFileName, allAccountsMap, allCategoriesMap, displayStartDateTime, @@ -208,6 +317,7 @@ export function useReconciliationStatementPageBase() { getDisplayTimezone, getDisplaySourceAmount, getDisplayDestinationAmount, - getDisplayAccountBalance + getDisplayAccountBalance, + getExportedData }; } diff --git a/src/views/desktop/accounts/list/dialogs/ReconciliationStatementDialog.vue b/src/views/desktop/accounts/list/dialogs/ReconciliationStatementDialog.vue index c7cb4708..f5bd80c5 100644 --- a/src/views/desktop/accounts/list/dialogs/ReconciliationStatementDialog.vue +++ b/src/views/desktop/accounts/list/dialogs/ReconciliationStatementDialog.vue @@ -15,6 +15,17 @@ + + + {{ tt('Export to CSV (Comma-separated values) File') }} + + + {{ tt('Export to TSV (Tab-separated values) File') }} + @@ -196,12 +207,17 @@ import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts'; import { useTransactionsStore } from '@/stores/transaction.ts'; import { TransactionType } from '@/core/transaction.ts'; +import { KnownFileType } from '@/core/file.ts'; import { Transaction, type TransactionReconciliationStatementResponseItem } from '@/models/transaction.ts'; +import { startDownloadFile } from '@/lib/ui/common.ts'; + import { mdiArrowRight, mdiDotsVertical, - mdiReceiptTextPlusOutline + mdiReceiptTextPlusOutline, + mdiComma, + mdiKeyboardTab } from '@mdi/js'; type SnackBarType = InstanceType; @@ -228,6 +244,7 @@ const { currentTimezoneOffsetMinutes, allAccountsMap, allCategoriesMap, + exportFileName, displayStartDateTime, displayEndDateTime, displayTotalOutflows, @@ -239,7 +256,8 @@ const { getDisplayTimezone, getDisplaySourceAmount, getDisplayDestinationAmount, - getDisplayAccountBalance + getDisplayAccountBalance, + getExportedData } = useReconciliationStatementPageBase(); const accountsStore = useAccountsStore(); @@ -388,6 +406,15 @@ function addTransaction(): void { }); } +function exportReconciliationStatements(fileType: KnownFileType): void { + if (!reconciliationStatements.value || reconciliationStatements.value.length < 1) { + return; + } + + const exportedData = getExportedData(fileType); + startDownloadFile(fileType.formatFileName(exportFileName.value), fileType.createBlob(exportedData)); +} + function showTransaction(transaction: TransactionReconciliationStatementResponseItem): void { if (transaction.type === TransactionType.ModifyBalance) { return;