import payee field as tags when importing a QIF file (#356)

This commit is contained in:
MaysWind
2025-11-25 00:55:36 +08:00
parent de27c8e6c5
commit 9ff1334584
64 changed files with 1353 additions and 871 deletions
+5 -1
View File
@@ -168,7 +168,11 @@ export const SUPPORTED_IMPORT_FILE_CATEGORY_AND_TYPES: ImportFileCategoryAndType
type: 'qif_dmy',
name: 'Day-month-year format',
}
]
],
supportedAdditionalOptions: {
payeeAsTag: false,
payeeAsDescription: true
}
},
{
type: 'iif',
+5
View File
@@ -75,6 +75,11 @@ export interface NameNumeralValue {
readonly value: number;
}
export interface KeyAndName {
readonly key: string;
readonly name: string;
}
export interface TypeAndName {
readonly type: number;
readonly name: string;
+10
View File
@@ -68,6 +68,14 @@ export interface ImportFileCategoryAndTypes {
readonly fileTypes: ImportFileType[];
}
export interface ImportFileTypeSupportedAdditionalOptions extends Record<string, boolean | undefined> {
readonly payeeAsTag?: boolean;
readonly payeeAsDescription?: boolean;
readonly memberAsTag?: boolean;
readonly projectAsTag?: boolean;
readonly merchantAsTag?: boolean;
}
export interface ImportFileType extends ImportFileTypeAndExtensions {
readonly type: string;
readonly name: string;
@@ -75,6 +83,7 @@ export interface ImportFileType extends ImportFileTypeAndExtensions {
readonly subTypes?: ImportFileTypeSubType[];
readonly supportedEncodings?: string[];
readonly dataFromTextbox?: boolean;
readonly supportedAdditionalOptions?: ImportFileTypeSupportedAdditionalOptions;
readonly document?: {
readonly supportMultiLanguages: boolean | string;
readonly anchor: string;
@@ -99,6 +108,7 @@ export interface LocalizedImportFileType extends ImportFileTypeAndExtensions {
readonly subTypes?: LocalizedImportFileTypeSubType[];
readonly supportedEncodings?: LocalizedImportFileTypeSupportedEncodings[];
readonly dataFromTextbox?: boolean;
readonly supportedAdditionalOptions?: ImportFileTypeSupportedAdditionalOptions;
readonly document?: LocalizedImportFileDocument;
}
+12 -2
View File
@@ -8,6 +8,9 @@ import type {
import type {
VersionInfo
} from '@/core/version.ts';
import type {
ImportFileTypeSupportedAdditionalOptions
} from '@/core/file.ts';
import {
TransactionType
} from '@/core/transaction.ts';
@@ -160,7 +163,8 @@ import {
import {
isDefined,
isBoolean
isBoolean,
objectFieldWithValueToArrayItem
} from './common.ts';
import {
getGoogleMapAPIKey,
@@ -588,11 +592,16 @@ export default {
timeout: DEFAULT_UPLOAD_API_TIMEOUT
} as ApiRequestConfig);
},
parseImportTransaction: ({ fileType, fileEncoding, importFile, columnMapping, transactionTypeMapping, hasHeaderLine, timeFormat, timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoSeparator, geoOrder, tagSeparator }: { fileType: string, fileEncoding?: string, importFile: File, columnMapping?: Record<number, number>, transactionTypeMapping?: Record<string, TransactionType>, hasHeaderLine?: boolean, timeFormat?: string, timezoneFormat?: string, amountDecimalSeparator?: string, amountDigitGroupingSymbol?: string, geoSeparator?: string, geoOrder?: string, tagSeparator?: string }): ApiResponsePromise<ImportTransactionResponsePageWrapper> => {
parseImportTransaction: ({ fileType, additionalOptions, fileEncoding, importFile, columnMapping, transactionTypeMapping, hasHeaderLine, timeFormat, timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoSeparator, geoOrder, tagSeparator }: { fileType: string, additionalOptions?: ImportFileTypeSupportedAdditionalOptions, fileEncoding?: string, importFile: File, columnMapping?: Record<number, number>, transactionTypeMapping?: Record<string, TransactionType>, hasHeaderLine?: boolean, timeFormat?: string, timezoneFormat?: string, amountDecimalSeparator?: string, amountDigitGroupingSymbol?: string, geoSeparator?: string, geoOrder?: string, tagSeparator?: string }): ApiResponsePromise<ImportTransactionResponsePageWrapper> => {
let textualAdditionalOptions: string | undefined = undefined;
let textualColumnMapping: string | undefined = undefined;
let textualTransactionTypeMapping: string | undefined = undefined;
let textualHasHeaderLine: string | undefined = undefined;
if (additionalOptions) {
textualAdditionalOptions = objectFieldWithValueToArrayItem(additionalOptions, true).join(',');
}
if (columnMapping) {
textualColumnMapping = JSON.stringify(columnMapping);
}
@@ -607,6 +616,7 @@ export default {
return axios.postForm<ApiResponse<ImportTransactionResponsePageWrapper>>('v1/transactions/parse_import.json', {
fileType: fileType,
options: textualAdditionalOptions,
fileEncoding: fileEncoding,
file: importFile,
columnMapping: textualColumnMapping,
+3
View File
@@ -1885,6 +1885,9 @@
"Custom Script": "Custom Script",
"Execute Custom Script": "Execute Custom Script",
"Execute Custom Script to Parse Data": "Execute Custom Script to Parse Data",
"Additional Options": "Additional Options",
"Parse Payee as Tag": "Parse Payee as Tag",
"Parse Payee as Description": "Parse Payee as Description",
"Data File": "Datendatei",
"Data to import": "Data to import",
"Please select a file to import": "Bitte wählen Sie eine Datei zum Importieren aus",
+3
View File
@@ -1885,6 +1885,9 @@
"Custom Script": "Custom Script",
"Execute Custom Script": "Execute Custom Script",
"Execute Custom Script to Parse Data": "Execute Custom Script to Parse Data",
"Additional Options": "Additional Options",
"Parse Payee as Tag": "Parse Payee as Tag",
"Parse Payee as Description": "Parse Payee as Description",
"Data File": "Data File",
"Data to import": "Data to import",
"Please select a file to import": "Please select a file to import",
+3
View File
@@ -1885,6 +1885,9 @@
"Custom Script": "Custom Script",
"Execute Custom Script": "Execute Custom Script",
"Execute Custom Script to Parse Data": "Execute Custom Script to Parse Data",
"Additional Options": "Additional Options",
"Parse Payee as Tag": "Parse Payee as Tag",
"Parse Payee as Description": "Parse Payee as Description",
"Data File": "Archivo de datos",
"Data to import": "Data to import",
"Please select a file to import": "Please select a file to import",
+3
View File
@@ -1885,6 +1885,9 @@
"Custom Script": "Script personnalisé",
"Execute Custom Script": "Exécuter le script personnalisé",
"Execute Custom Script to Parse Data": "Exécuter le script personnalisé pour analyser les données",
"Additional Options": "Additional Options",
"Parse Payee as Tag": "Parse Payee as Tag",
"Parse Payee as Description": "Parse Payee as Description",
"Data File": "Fichier de données",
"Data to import": "Données à importer",
"Please select a file to import": "Veuillez sélectionner un fichier à importer",
+1
View File
@@ -1525,6 +1525,7 @@ export function useI18n() {
subTypes: subTypes.length ? subTypes : undefined,
supportedEncodings: supportedEncodings.length ? supportedEncodings : undefined,
dataFromTextbox: fileType.dataFromTextbox,
supportedAdditionalOptions: fileType.supportedAdditionalOptions,
document: document
};
+3
View File
@@ -1885,6 +1885,9 @@
"Custom Script": "Custom Script",
"Execute Custom Script": "Execute Custom Script",
"Execute Custom Script to Parse Data": "Execute Custom Script to Parse Data",
"Additional Options": "Additional Options",
"Parse Payee as Tag": "Parse Payee as Tag",
"Parse Payee as Description": "Parse Payee as Description",
"Data File": "File dati",
"Data to import": "Dati da importare",
"Please select a file to import": "Seleziona un file da importare",
+3
View File
@@ -1885,6 +1885,9 @@
"Custom Script": "Custom Script",
"Execute Custom Script": "Execute Custom Script",
"Execute Custom Script to Parse Data": "Execute Custom Script to Parse Data",
"Additional Options": "Additional Options",
"Parse Payee as Tag": "Parse Payee as Tag",
"Parse Payee as Description": "Parse Payee as Description",
"Data File": "データファイル",
"Data to import": "インポートするデータ",
"Please select a file to import": "インポートするファイルを選択してください",
+3
View File
@@ -1885,6 +1885,9 @@
"Custom Script": "사용자 정의 스크립트",
"Execute Custom Script": "사용자 정의 스크립트 실행",
"Execute Custom Script to Parse Data": "데이터 구문 분석을 위한 사용자 정의 스크립트 실행",
"Additional Options": "Additional Options",
"Parse Payee as Tag": "Parse Payee as Tag",
"Parse Payee as Description": "Parse Payee as Description",
"Data File": "데이터 파일",
"Data to import": "가져올 데이터",
"Please select a file to import": "가져올 파일을 선택하십시오",
+3
View File
@@ -1885,6 +1885,9 @@
"Custom Script": "Custom Script",
"Execute Custom Script": "Execute Custom Script",
"Execute Custom Script to Parse Data": "Execute Custom Script to Parse Data",
"Additional Options": "Additional Options",
"Parse Payee as Tag": "Parse Payee as Tag",
"Parse Payee as Description": "Parse Payee as Description",
"Data File": "Gegevensbestand",
"Data to import": "Te importeren gegevens",
"Please select a file to import": "Selecteer een bestand om te importeren",
+3
View File
@@ -1885,6 +1885,9 @@
"Custom Script": "Custom Script",
"Execute Custom Script": "Execute Custom Script",
"Execute Custom Script to Parse Data": "Execute Custom Script to Parse Data",
"Additional Options": "Additional Options",
"Parse Payee as Tag": "Parse Payee as Tag",
"Parse Payee as Description": "Parse Payee as Description",
"Data File": "Arquivo de Dados",
"Data to import": "Dados para importar",
"Please select a file to import": "Por favor, selecione um arquivo para importar",
+3
View File
@@ -1885,6 +1885,9 @@
"Custom Script": "Custom Script",
"Execute Custom Script": "Execute Custom Script",
"Execute Custom Script to Parse Data": "Execute Custom Script to Parse Data",
"Additional Options": "Additional Options",
"Parse Payee as Tag": "Parse Payee as Tag",
"Parse Payee as Description": "Parse Payee as Description",
"Data File": "Файл данных",
"Data to import": "Data to import",
"Please select a file to import": "Please select a file to import",
+3
View File
@@ -1885,6 +1885,9 @@
"Custom Script": "สคริปต์กำหนดเอง",
"Execute Custom Script": "เรียกใช้สคริปต์กำหนดเอง",
"Execute Custom Script to Parse Data": "เรียกใช้สคริปต์กำหนดเองเพื่อแยกข้อมูล",
"Additional Options": "Additional Options",
"Parse Payee as Tag": "Parse Payee as Tag",
"Parse Payee as Description": "Parse Payee as Description",
"Data File": "ไฟล์ข้อมูล",
"Data to import": "ข้อมูลที่จะนำเข้า",
"Please select a file to import": "กรุณาเลือกไฟล์เพื่อนำเข้า",
+3
View File
@@ -1885,6 +1885,9 @@
"Custom Script": "Custom Script",
"Execute Custom Script": "Execute Custom Script",
"Execute Custom Script to Parse Data": "Execute Custom Script to Parse Data",
"Additional Options": "Additional Options",
"Parse Payee as Tag": "Parse Payee as Tag",
"Parse Payee as Description": "Parse Payee as Description",
"Data File": "Файл даних",
"Data to import": "Дані для імпорту",
"Please select a file to import": "Будь ласка, виберіть файл для імпорту",
+3
View File
@@ -1885,6 +1885,9 @@
"Custom Script": "Custom Script",
"Execute Custom Script": "Execute Custom Script",
"Execute Custom Script to Parse Data": "Execute Custom Script to Parse Data",
"Additional Options": "Additional Options",
"Parse Payee as Tag": "Parse Payee as Tag",
"Parse Payee as Description": "Parse Payee as Description",
"Data File": "Tệp dữ liệu",
"Data to import": "Data to import",
"Please select a file to import": "Please select a file to import",
+3
View File
@@ -1885,6 +1885,9 @@
"Custom Script": "自定义脚本",
"Execute Custom Script": "执行自定义脚本",
"Execute Custom Script to Parse Data": "执行自定义脚本解析数据",
"Additional Options": "附加选项",
"Parse Payee as Tag": "将收款人解析为标签",
"Parse Payee as Description": "将收款人解析为描述",
"Data File": "数据文件",
"Data to import": "要导入的数据",
"Please select a file to import": "请选择要导入的文件",
+3
View File
@@ -1885,6 +1885,9 @@
"Custom Script": "自訂腳本",
"Execute Custom Script": "執行自訂腳本",
"Execute Custom Script to Parse Data": "執行自訂腳本來解析資料",
"Additional Options": "附加選項",
"Parse Payee as Tag": "將收款人解析為標籤",
"Parse Payee as Description": "將收款人解析為描述",
"Data File": "資料檔案",
"Data to import": "要匯入的資料",
"Please select a file to import": "請選擇要匯入的檔案",
+3 -2
View File
@@ -12,6 +12,7 @@ import { useExchangeRatesStore } from './exchangeRates.ts';
import { type BeforeResolveFunction, itemAndIndex, entries, keys } from '@/core/base.ts';
import { type TextualYearMonth, DateRange } from '@/core/datetime.ts';
import { CategoryType } from '@/core/category.ts';
import type { ImportFileTypeSupportedAdditionalOptions } from '@/core/file.ts';
import { TransactionType, TransactionTagFilterType } from '@/core/transaction.ts';
import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts';
import {
@@ -1264,9 +1265,9 @@ export const useTransactionsStore = defineStore('transactions', () => {
});
}
function parseImportTransaction({ fileType, fileEncoding, importFile, columnMapping, transactionTypeMapping, hasHeaderLine, timeFormat, timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoSeparator, geoOrder, tagSeparator }: { fileType: string, fileEncoding?: string, importFile: File, columnMapping?: Record<number, number>, transactionTypeMapping?: Record<string, TransactionType>, hasHeaderLine?: boolean, timeFormat?: string, timezoneFormat?: string, amountDecimalSeparator?: string, amountDigitGroupingSymbol?: string, geoSeparator?: string, geoOrder?: string, tagSeparator?: string }): Promise<ImportTransactionResponsePageWrapper> {
function parseImportTransaction({ fileType, additionalOptions, fileEncoding, importFile, columnMapping, transactionTypeMapping, hasHeaderLine, timeFormat, timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoSeparator, geoOrder, tagSeparator }: { fileType: string, additionalOptions?: ImportFileTypeSupportedAdditionalOptions, fileEncoding?: string, importFile: File, columnMapping?: Record<number, number>, transactionTypeMapping?: Record<string, TransactionType>, hasHeaderLine?: boolean, timeFormat?: string, timezoneFormat?: string, amountDecimalSeparator?: string, amountDigitGroupingSymbol?: string, geoSeparator?: string, geoOrder?: string, tagSeparator?: string }): Promise<ImportTransactionResponsePageWrapper> {
return new Promise((resolve, reject) => {
services.parseImportTransaction({ fileType, fileEncoding, importFile, columnMapping, transactionTypeMapping, hasHeaderLine, timeFormat, timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoSeparator, geoOrder, tagSeparator }).then(response => {
services.parseImportTransaction({ fileType, additionalOptions, fileEncoding, importFile, columnMapping, transactionTypeMapping, hasHeaderLine, timeFormat, timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoSeparator, geoOrder, tagSeparator }).then(response => {
const data = response.data;
if (!data || !data.success || !data.result) {
@@ -145,6 +145,31 @@
/>
</v-col>
<v-col cols="12" md="12" v-if="supportedAdditionalOptions">
<v-select
:disabled="submitting"
:label="tt('Additional Options')"
:placeholder="tt('Additional Options')"
v-model="fileType"
v-model:menu="additionalOptionsMenuState"
>
<template #selection>
<span class="cursor-pointer">{{ displaySelectedAdditionalOptions }}</span>
</template>
<template #no-data>
<v-list class="py-0">
<template v-for="item in allSupportedAdditionalOptions">
<v-list-item :key="item.key"
:append-icon="importAdditionalOptions[item.key] ? mdiCheck : undefined"
@click="importAdditionalOptions[item.key] = !importAdditionalOptions[item.key]"
v-if="isDefined(supportedAdditionalOptions[item.key])">{{ tt(item.name) }}</v-list-item>
</template>
</v-list>
</template>
</v-select>
</v-col>
<v-col cols="12" md="12" v-if="!isImportDataFromTextbox">
<v-text-field
readonly
@@ -257,15 +282,20 @@ import { useTransactionsStore } from '@/stores/transaction.ts';
import { useOverviewStore } from '@/stores/overview.ts';
import { useStatisticsStore } from '@/stores/statistics.ts';
import { itemAndIndex } from '@/core/base.ts';
import { type KeyAndName, itemAndIndex } from '@/core/base.ts';
import { type NumeralSystem } from '@/core/numeral.ts';
import { TransactionType } from '@/core/transaction.ts';
import { KnownFileType } from '@/core/file.ts';
import type { LocalizedImportFileCategoryAndTypes, LocalizedImportFileType, LocalizedImportFileTypeSubType, LocalizedImportFileTypeSupportedEncodings } from '@/core/file.ts';
import {
type ImportFileTypeSupportedAdditionalOptions,
type LocalizedImportFileCategoryAndTypes,
type LocalizedImportFileType,
type LocalizedImportFileTypeSubType,
type LocalizedImportFileTypeSupportedEncodings,
KnownFileType
} from '@/core/file.ts';
import { ImportTransaction } from '@/models/imported_transaction.ts';
import { isNumber } from '@/lib/common.ts';
import { isDefined, isNumber } from '@/lib/common.ts';
import { findExtensionByType, isFileExtensionSupported } from '@/lib/file.ts';
import { generateRandomUUID } from '@/lib/misc.ts';
import logger from '@/lib/logger.ts';
@@ -297,6 +327,7 @@ defineProps<{
const {
tt,
joinMultiText,
getCurrentNumeralSystemType,
getAllSupportedImportFileCagtegoryAndTypes,
formatNumberToLocalizedNumerals
@@ -316,7 +347,19 @@ const importTransactionExecuteCustomScriptTab = useTemplateRef<ImportTransaction
const importTransactionCheckDataTab = useTemplateRef<ImportTransactionCheckDataTabType>('importTransactionCheckDataTab');
const fileInput = useTemplateRef<HTMLInputElement>('fileInput');
const allSupportedAdditionalOptions: KeyAndName[] = [
{
key: 'payeeAsTag',
name: 'Parse Payee as Tag'
},
{
key: 'payeeAsDescription',
name: 'Parse Payee as Description'
}
];
const showState = ref<boolean>(false);
const additionalOptionsMenuState = ref<boolean>(false);
const clientSessionId = ref<string>('');
const currentStep = ref<ImportTransactionDialogStep>('uploadFile');
const importProcess = ref<number>(0);
@@ -326,6 +369,7 @@ const fileEncoding = ref<string>('utf-8');
const processDSVMethod = ref<ImportDSVProcessMethod>(ImportDSVProcessMethod.ColumnMapping);
const importFile = ref<File | null>(null);
const importData = ref<string>('');
const importAdditionalOptions = ref<ImportFileTypeSupportedAdditionalOptions>({});
const parsedFileData = ref<string[][] | undefined>(undefined);
const importTransactions = ref<ImportTransaction[] | undefined>(undefined);
@@ -342,6 +386,7 @@ const allSupportedImportFileCategoryAndTypes = computed<LocalizedImportFileCateg
const allFileSubTypes = computed<LocalizedImportFileTypeSubType[] | undefined>(() => allSupportedImportFileTypesMap.value[fileType.value]?.subTypes);
const allSupportedEncodings = computed<LocalizedImportFileTypeSupportedEncodings[] | undefined>(() => allSupportedImportFileTypesMap.value[fileType.value]?.supportedEncodings);
const isImportDataFromTextbox = computed<boolean>(() => allSupportedImportFileTypesMap.value[fileType.value]?.dataFromTextbox ?? false);
const supportedAdditionalOptions = computed<ImportFileTypeSupportedAdditionalOptions | undefined>(() => allSupportedImportFileTypesMap.value[fileType.value]?.supportedAdditionalOptions);
const allSteps = computed<StepBarItem[]>(() => {
const steps: StepBarItem[] = [
@@ -408,6 +453,26 @@ const supportedImportFileExtensions = computed<string | undefined>(() => {
return allSupportedImportFileTypesMap.value[fileType.value]?.extensions;
});
const displaySelectedAdditionalOptions = computed<string>(() => {
if (!supportedAdditionalOptions.value) {
return tt('None');
}
const selectedOptions: string[] = [];
for (const option of allSupportedAdditionalOptions) {
if (isDefined(supportedAdditionalOptions.value[option.key]) && importAdditionalOptions.value[option.key]) {
selectedOptions.push(tt(option.name));
}
}
if (selectedOptions.length < 1) {
return tt('None');
}
return joinMultiText(selectedOptions);
});
const exportFileGuideDocumentUrl = computed<string | undefined>(() => {
const document = allSupportedImportFileTypesMap.value[fileType.value]?.document;
@@ -437,6 +502,7 @@ function open(): Promise<void> {
importProcess.value = 0;
importFile.value = null;
importData.value = '';
importAdditionalOptions.value = Object.assign({}, supportedAdditionalOptions.value ?? {});
parsedFileData.value = undefined;
importTransactionDefineColumnTab.value?.reset();
importTransactionExecuteCustomScriptTab.value?.reset();
@@ -614,6 +680,7 @@ function parseData(): void {
transactionsStore.parseImportTransaction({
fileType: type,
additionalOptions: importAdditionalOptions.value,
fileEncoding: encoding,
importFile: uploadFile,
columnMapping: columnMapping,
@@ -773,6 +840,7 @@ watch(fileType, () => {
importFile.value = null;
parsedFileData.value = undefined;
importAdditionalOptions.value = Object.assign({}, supportedAdditionalOptions.value ?? {});
importTransactions.value = undefined;
});