Browse Source

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>
pull/5481/merge
Sven Günther 2 days ago
committed by GitHub
parent
commit
1daa980824
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 29
      apps/api/src/app/import/import.service.ts
  3. 20
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  4. 53
      apps/client/src/app/services/import-activities.service.ts
  5. 2
      test/import/ok/penthouse-apartment.csv

1
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

29
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<SymbolProfile> = { 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}")`

20
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({

53
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({

2
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,
1 Date Code DataSource Currency Price Quantity Action Fee Note
2 01.01.2022 Penthouse Apartment MANUAL USD 500000.0 1 buy 0.00
Loading…
Cancel
Save