Browse Source

Feature/add support for liabilities (#1789)

* Add support for liabilities

* Update changelog
pull/2068/head
Thomas Kaul 2 years ago
committed by GitHub
parent
commit
7931e6950d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      CHANGELOG.md
  2. 13
      apps/api/src/app/order/order.service.ts
  3. 1
      apps/api/src/app/portfolio/portfolio.controller.ts
  4. 44
      apps/api/src/app/portfolio/portfolio.service.ts
  5. 20
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
  6. 27
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  7. 13
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
  8. 1
      libs/common/src/lib/interfaces/portfolio-summary.interface.ts
  9. 10
      libs/ui/src/lib/activities-table/activities-table.component.html
  10. 4
      libs/ui/src/lib/activities-table/activities-table.component.scss
  11. 2
      prisma/migrations/20230614345544_added_liability_to_order_type/migration.sql
  12. 1
      prisma/schema.prisma

6
CHANGELOG.md

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Added support for liabilities
## 1.279.0 - 2023-06-10 ## 1.279.0 - 2023-06-10
### Added ### Added

13
apps/api/src/app/order/order.service.ts

@ -96,7 +96,7 @@ export class OrderService {
const updateAccountBalance = data.updateAccountBalance ?? false; const updateAccountBalance = data.updateAccountBalance ?? false;
const userId = data.userId; const userId = data.userId;
if (data.type === 'ITEM') { if (data.type === 'ITEM' || data.type === 'LIABILITY') {
const assetClass = data.assetClass; const assetClass = data.assetClass;
const assetSubClass = data.assetSubClass; const assetSubClass = data.assetSubClass;
currency = data.SymbolProfile.connectOrCreate.create.currency; currency = data.SymbolProfile.connectOrCreate.create.currency;
@ -129,7 +129,10 @@ export class OrderService {
} }
}); });
const isDraft = isAfter(data.date as Date, endOfToday()); const isDraft =
data.type === 'LIABILITY'
? false
: isAfter(data.date as Date, endOfToday());
if (!isDraft) { if (!isDraft) {
// Gather symbol data of order in the background, if not draft // Gather symbol data of order in the background, if not draft
@ -320,7 +323,11 @@ export class OrderService {
}) })
) )
.filter((order) => { .filter((order) => {
return withExcludedAccounts || order.Account?.isExcluded === false; return (
withExcludedAccounts ||
!order.Account ||
order.Account?.isExcluded === false
);
}) })
.map((order) => { .map((order) => {
const value = new Big(order.quantity).mul(order.unitPrice).toNumber(); const value = new Big(order.quantity).mul(order.unitPrice).toNumber();

1
apps/api/src/app/portfolio/portfolio.controller.ts

@ -162,6 +162,7 @@ export class PortfolioController {
'excludedAccountsAndActivities', 'excludedAccountsAndActivities',
'fees', 'fees',
'items', 'items',
'liabilities',
'netWorth', 'netWorth',
'totalBuy', 'totalBuy',
'totalSell' 'totalSell'

44
apps/api/src/app/portfolio/portfolio.service.ts

@ -1302,12 +1302,11 @@ export class PortfolioService {
}: { }: {
activities: OrderWithAccount[]; activities: OrderWithAccount[];
date?: Date; date?: Date;
userCurrency: string; userCurrency: string;
}) { }) {
return activities return activities
.filter((activity) => { .filter((activity) => {
// Filter out all activities before given date and type dividend // Filter out all activities before given date (drafts) and type dividend
return ( return (
isBefore(date, new Date(activity.date)) && isBefore(date, new Date(activity.date)) &&
activity.type === TypeOfOrder.DIVIDEND activity.type === TypeOfOrder.DIVIDEND
@ -1431,7 +1430,7 @@ export class PortfolioService {
}) { }) {
return activities return activities
.filter((activity) => { .filter((activity) => {
// Filter out all activities before given date // Filter out all activities before given date (drafts)
return isBefore(date, new Date(activity.date)); return isBefore(date, new Date(activity.date));
}) })
.map(({ fee, SymbolProfile }) => { .map(({ fee, SymbolProfile }) => {
@ -1478,19 +1477,37 @@ export class PortfolioService {
}; };
} }
private getItems(orders: OrderWithAccount[], date = new Date(0)) { private getItems(activities: OrderWithAccount[], date = new Date(0)) {
return orders return activities
.filter((order) => { .filter((activity) => {
// Filter out all orders before given date and type item // Filter out all activities before given date (drafts) and type item
return ( return (
isBefore(date, new Date(order.date)) && isBefore(date, new Date(activity.date)) &&
order.type === TypeOfOrder.ITEM activity.type === TypeOfOrder.ITEM
); );
}) })
.map((order) => { .map(({ quantity, SymbolProfile, unitPrice }) => {
return this.exchangeRateDataService.toCurrency( return this.exchangeRateDataService.toCurrency(
new Big(order.quantity).mul(order.unitPrice).toNumber(), new Big(quantity).mul(unitPrice).toNumber(),
order.SymbolProfile.currency, SymbolProfile.currency,
this.request.user.Settings.settings.baseCurrency
);
})
.reduce(
(previous, current) => new Big(previous).plus(current),
new Big(0)
);
}
private getLiabilities(activities: OrderWithAccount[]) {
return activities
.filter(({ type }) => {
return type === TypeOfOrder.LIABILITY;
})
.map(({ quantity, SymbolProfile, unitPrice }) => {
return this.exchangeRateDataService.toCurrency(
new Big(quantity).mul(unitPrice).toNumber(),
SymbolProfile.currency,
this.request.user.Settings.settings.baseCurrency this.request.user.Settings.settings.baseCurrency
); );
}) })
@ -1601,6 +1618,7 @@ export class PortfolioService {
const fees = this.getFees({ activities, userCurrency }).toNumber(); const fees = this.getFees({ activities, userCurrency }).toNumber();
const firstOrderDate = activities[0]?.date; const firstOrderDate = activities[0]?.date;
const items = this.getItems(activities).toNumber(); const items = this.getItems(activities).toNumber();
const liabilities = this.getLiabilities(activities).toNumber();
const totalBuy = this.getTotalByType(activities, userCurrency, 'BUY'); const totalBuy = this.getTotalByType(activities, userCurrency, 'BUY');
const totalSell = this.getTotalByType(activities, userCurrency, 'SELL'); const totalSell = this.getTotalByType(activities, userCurrency, 'SELL');
@ -1633,6 +1651,7 @@ export class PortfolioService {
.plus(performanceInformation.performance.currentValue) .plus(performanceInformation.performance.currentValue)
.plus(items) .plus(items)
.plus(excludedAccountsAndActivities) .plus(excludedAccountsAndActivities)
.minus(liabilities)
.toNumber(); .toNumber();
const daysInMarket = differenceInDays(new Date(), firstOrderDate); const daysInMarket = differenceInDays(new Date(), firstOrderDate);
@ -1659,6 +1678,7 @@ export class PortfolioService {
fees, fees,
firstOrderDate, firstOrderDate,
items, items,
liabilities,
netWorth, netWorth,
totalBuy, totalBuy,
totalSell, totalSell,

20
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html

@ -194,6 +194,26 @@
<div class="row"> <div class="row">
<div class="col"><hr /></div> <div class="col"><hr /></div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row">
<div class="flex-grow-1 text-truncate" i18n>Liabilities</div>
<div class="d-flex justify-content-end">
<span
*ngIf="summary?.liabilities || summary?.liabilities === 0"
class="mr-1"
>-</span
>
<gf-value
class="justify-content-end"
[isCurrency]="true"
[locale]="locale"
[unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.liabilities"
></gf-value>
</div>
</div>
<div class="row">
<div class="col"><hr /></div>
</div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
<div class="flex-grow-1 font-weight-bold text-truncate" i18n>Net Worth</div> <div class="flex-grow-1 font-weight-bold text-truncate" i18n>Net Worth</div>
<div class="justify-content-end"> <div class="justify-content-end">

27
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -300,6 +300,33 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.activityForm.controls['searchSymbol'].updateValueAndValidity(); this.activityForm.controls['searchSymbol'].updateValueAndValidity();
this.activityForm.controls['updateAccountBalance'].disable(); this.activityForm.controls['updateAccountBalance'].disable();
this.activityForm.controls['updateAccountBalance'].setValue(false); this.activityForm.controls['updateAccountBalance'].setValue(false);
} else if (type === 'LIABILITY') {
this.activityForm.controls['accountId'].removeValidators(
Validators.required
);
this.activityForm.controls['accountId'].updateValueAndValidity();
this.activityForm.controls['currency'].setValue(
this.data.user.settings.baseCurrency
);
this.activityForm.controls['currencyOfFee'].setValue(
this.data.user.settings.baseCurrency
);
this.activityForm.controls['currencyOfUnitPrice'].setValue(
this.data.user.settings.baseCurrency
);
this.activityForm.controls['dataSource'].removeValidators(
Validators.required
);
this.activityForm.controls['dataSource'].updateValueAndValidity();
this.activityForm.controls['name'].setValidators(Validators.required);
this.activityForm.controls['name'].updateValueAndValidity();
this.activityForm.controls['quantity'].setValue(1);
this.activityForm.controls['searchSymbol'].removeValidators(
Validators.required
);
this.activityForm.controls['searchSymbol'].updateValueAndValidity();
this.activityForm.controls['updateAccountBalance'].disable();
this.activityForm.controls['updateAccountBalance'].setValue(false);
} else { } else {
this.activityForm.controls['accountId'].setValidators( this.activityForm.controls['accountId'].setValidators(
Validators.required Validators.required

13
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html

@ -14,6 +14,7 @@
<mat-option i18n value="BUY">Buy</mat-option> <mat-option i18n value="BUY">Buy</mat-option>
<mat-option i18n value="DIVIDEND">Dividend</mat-option> <mat-option i18n value="DIVIDEND">Dividend</mat-option>
<mat-option i18n value="ITEM">Item</mat-option> <mat-option i18n value="ITEM">Item</mat-option>
<mat-option i18n value="LIABILITY">Liability</mat-option>
<mat-option i18n value="SELL">Sell</mat-option> <mat-option i18n value="SELL">Sell</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
@ -116,7 +117,10 @@
<mat-datepicker #date disabled="false"></mat-datepicker> <mat-datepicker #date disabled="false"></mat-datepicker>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="mb-3"> <div
class="mb-3"
[ngClass]="{ 'd-none': activityForm.controls['type']?.value === 'ITEM' || activityForm.controls['type']?.value === 'LIABILITY' }"
>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Quantity</mat-label> <mat-label i18n>Quantity</mat-label>
<input formControlName="quantity" matInput type="number" /> <input formControlName="quantity" matInput type="number" />
@ -130,6 +134,7 @@
>Dividend</ng-container >Dividend</ng-container
> >
<ng-container *ngSwitchCase="'ITEM'" i18n>Value</ng-container> <ng-container *ngSwitchCase="'ITEM'" i18n>Value</ng-container>
<ng-container *ngSwitchCase="'LIABILITY'" i18n>Value</ng-container>
<ng-container *ngSwitchDefault i18n>Unit Price</ng-container> <ng-container *ngSwitchDefault i18n>Unit Price</ng-container>
</ng-container> </ng-container>
</mat-label> </mat-label>
@ -177,6 +182,7 @@
>Dividend</ng-container >Dividend</ng-container
> >
<ng-container *ngSwitchCase="'ITEM'" i18n>Value</ng-container> <ng-container *ngSwitchCase="'ITEM'" i18n>Value</ng-container>
<ng-container *ngSwitchCase="'LIABILITY'" i18n>Value</ng-container>
<ng-container *ngSwitchDefault i18n>Unit Price</ng-container> <ng-container *ngSwitchDefault i18n>Unit Price</ng-container>
</ng-container> </ng-container>
</mat-label> </mat-label>
@ -186,7 +192,10 @@
> >
</mat-form-field> </mat-form-field>
</div> </div>
<div class="mb-3"> <div
class="mb-3"
[ngClass]="{ 'd-none': activityForm.controls['type']?.value === 'ITEM' || activityForm.controls['type']?.value === 'LIABILITY' }"
>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Fee</mat-label> <mat-label i18n>Fee</mat-label>
<input formControlName="feeInCustomCurrency" matInput type="number" /> <input formControlName="feeInCustomCurrency" matInput type="number" />

1
libs/common/src/lib/interfaces/portfolio-summary.interface.ts

@ -10,6 +10,7 @@ export interface PortfolioSummary extends PortfolioPerformance {
fees: number; fees: number;
firstOrderDate: Date; firstOrderDate: Date;
items: number; items: number;
liabilities: number;
netWorth: number; netWorth: number;
ordersCount: number; ordersCount: number;
totalBuy: number; totalBuy: number;

10
libs/ui/src/lib/activities-table/activities-table.component.html

@ -162,6 +162,7 @@
buy: element.type === 'BUY', buy: element.type === 'BUY',
dividend: element.type === 'DIVIDEND', dividend: element.type === 'DIVIDEND',
item: element.type === 'ITEM', item: element.type === 'ITEM',
liability: element.type === 'LIABILITY',
sell: element.type === 'SELL' sell: element.type === 'SELL'
}" }"
> >
@ -173,6 +174,10 @@
*ngIf="element.type === 'ITEM'" *ngIf="element.type === 'ITEM'"
name="cube-outline" name="cube-outline"
></ion-icon> ></ion-icon>
<ion-icon
*ngIf="element.type === 'LIABILITY'"
name="flame-outline"
></ion-icon>
<ion-icon <ion-icon
*ngIf="element.type === 'SELL'" *ngIf="element.type === 'SELL'"
name="arrow-down-circle-outline" name="arrow-down-circle-outline"
@ -538,7 +543,10 @@
mat-row mat-row
[ngClass]="{ [ngClass]="{
'cursor-pointer': 'cursor-pointer':
hasPermissionToOpenDetails && !row.isDraft && row.type !== 'ITEM' hasPermissionToOpenDetails &&
!row.isDraft &&
row.type !== 'ITEM' &&
row.type !== 'LIABILITY'
}" }"
(click)="onClickActivity(row)" (click)="onClickActivity(row)"
></tr> ></tr>

4
libs/ui/src/lib/activities-table/activities-table.component.scss

@ -37,6 +37,10 @@
color: var(--purple); color: var(--purple);
} }
&.liability {
color: var(--red);
}
&.sell { &.sell {
color: var(--orange); color: var(--orange);
} }

2
prisma/migrations/20230614345544_added_liability_to_order_type/migration.sql

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "Type" ADD VALUE 'LIABILITY';

1
prisma/schema.prisma

@ -238,6 +238,7 @@ enum Type {
BUY BUY
DIVIDEND DIVIDEND
ITEM ITEM
LIABILITY
SELL SELL
} }

Loading…
Cancel
Save