From 1daa980824a7771da7f96115395fe05943c85e6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sven=20G=C3=BCnther?= Date: Fri, 17 Oct 2025 22:12:21 +0200 Subject: [PATCH] Bugfix/import of activity with MANUAL data source (CSV file) (#5749) * Fix import of activity with MANUAL data source * Update changelog --------- Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> --- CHANGELOG.md | 1 + apps/api/src/app/import/import.service.ts | 29 ++++++---- .../import-activities-dialog.component.ts | 20 ++++--- .../app/services/import-activities.service.ts | 53 +++++++++++++++++-- test/import/ok/penthouse-apartment.csv | 2 + 5 files changed, 82 insertions(+), 23 deletions(-) create mode 100644 test/import/ok/penthouse-apartment.csv diff --git a/CHANGELOG.md b/CHANGELOG.md index c0f6bd0e1..8f72750ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fixed an issue in the `csv` file import where custom asset profiles failed due to validation errors - Fixed an issue with the total buy and sell calculation in the summary related to activities in a custom currency - Respected the include indices flag in the search functionality of the _Financial Modeling Prep_ service - Fixed an issue where the scroll position was not restored when changing pages diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 69ec781c3..2725747aa 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -743,14 +743,27 @@ export class ImportService { } if (!assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })]) { - const assetProfile = { - currency, - ...( + if (['FEE', 'INTEREST', 'LIABILITY'].includes(type)) { + // Skip asset profile validation for FEE, INTEREST, and LIABILITY + // as these activity types don't require asset profiles + assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] = { + currency, + dataSource, + symbol + }; + + continue; + } + + let assetProfile: Partial = { currency }; + + try { + assetProfile = ( await this.dataProviderService.getAssetProfiles([ { dataSource, symbol } ]) - )?.[symbol] - }; + )?.[symbol]; + } catch {} if (!assetProfile?.name) { const assetProfileInImport = assetProfilesWithMarketDataDto?.find( @@ -787,11 +800,7 @@ export class ImportService { } } - if ( - (dataSource !== 'MANUAL' && type === 'BUY') || - type === 'DIVIDEND' || - type === 'SELL' - ) { + if (!['FEE', 'INTEREST', 'LIABILITY'].includes(type)) { if (!assetProfile?.name) { throw new Error( `activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")` diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts index 2439a4b65..0c0054e9b 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts @@ -346,6 +346,7 @@ export class GfImportActivitiesDialogComponent implements OnDestroy { isDryRun: true, tags: content.tags }); + this.activities = activities; this.dataSource = new MatTableDataSource(activities.reverse()); this.pageIndex = 0; @@ -360,15 +361,18 @@ export class GfImportActivitiesDialogComponent implements OnDestroy { const content = fileContent.split('\n').slice(1); try { - const data = await this.importActivitiesService.importCsv({ - fileContent, - isDryRun: true, - userAccounts: this.data.user.accounts - }); - this.activities = data.activities; - this.dataSource = new MatTableDataSource(data.activities.reverse()); + const { activities, assetProfiles } = + await this.importActivitiesService.importCsv({ + fileContent, + isDryRun: true, + userAccounts: this.data.user.accounts + }); + + this.activities = activities; + this.assetProfiles = assetProfiles; + this.dataSource = new MatTableDataSource(activities.reverse()); this.pageIndex = 0; - this.totalItems = data.activities.length; + this.totalItems = activities.length; } catch (error) { console.error(error); this.handleImportError({ diff --git a/apps/client/src/app/services/import-activities.service.ts b/apps/client/src/app/services/import-activities.service.ts index 27a34652b..323f07a5b 100644 --- a/apps/client/src/app/services/import-activities.service.ts +++ b/apps/client/src/app/services/import-activities.service.ts @@ -45,6 +45,7 @@ export class ImportActivitiesService { userAccounts: Account[]; }): Promise<{ activities: Activity[]; + assetProfiles: CreateAssetProfileWithMarketDataDto[]; }> { const content = csvToJson(fileContent, { dynamicTyping: true, @@ -53,23 +54,65 @@ export class ImportActivitiesService { }).data; const activities: CreateOrderDto[] = []; + const assetProfiles: CreateAssetProfileWithMarketDataDto[] = []; + for (const [index, item] of content.entries()) { + const currency = this.parseCurrency({ content, index, item }); + const dataSource = this.parseDataSource({ item }); + const symbol = this.parseSymbol({ content, index, item }); + const type = this.parseType({ content, index, item }); + activities.push({ + currency, + dataSource, + symbol, + type, accountId: this.parseAccount({ item, userAccounts }), comment: this.parseComment({ item }), - currency: this.parseCurrency({ content, index, item }), - dataSource: this.parseDataSource({ item }), date: this.parseDate({ content, index, item }), fee: this.parseFee({ content, index, item }), quantity: this.parseQuantity({ content, index, item }), - symbol: this.parseSymbol({ content, index, item }), - type: this.parseType({ content, index, item }), unitPrice: this.parseUnitPrice({ content, index, item }), updateAccountBalance: false }); + + if ( + dataSource === DataSource.MANUAL && + !['FEE', 'INTEREST', 'LIABILITY'].includes(type) + ) { + // Create synthetic asset profile for MANUAL data source + // (except for FEE, INTEREST, and LIABILITY which don't require asset profiles) + assetProfiles.push({ + currency, + symbol, + assetClass: null, + assetSubClass: null, + comment: null, + countries: [], + cusip: null, + dataSource: DataSource.MANUAL, + figi: null, + figiComposite: null, + figiShareClass: null, + holdings: [], + isActive: true, + isin: null, + marketData: [], + name: symbol, + scraperConfiguration: null, + sectors: [], + symbolMapping: {}, + url: null + }); + } } - return await this.importJson({ activities, isDryRun }); + const result = await this.importJson({ + activities, + assetProfiles, + isDryRun + }); + return { ...result, assetProfiles }; } public importJson({ diff --git a/test/import/ok/penthouse-apartment.csv b/test/import/ok/penthouse-apartment.csv new file mode 100644 index 000000000..27eb5bf1c --- /dev/null +++ b/test/import/ok/penthouse-apartment.csv @@ -0,0 +1,2 @@ +Date,Code,DataSource,Currency,Price,Quantity,Action,Fee,Note +01.01.2022,Penthouse Apartment,MANUAL,USD,500000.0,1,buy,0.00,