({
15 | queryKey: ["tasks-results"],
16 | queryFn: fetchTasksResults,
17 | });
18 | }
19 |
20 | export const useStartTask = () => {
21 | const { t } = useTranslation();
22 |
23 | return useMutation({
24 | mutationFn: () => apiClient.post(`/start-task/`),
25 | onSuccess: () => {
26 | notifications.show({
27 | color: "green",
28 | message: t("Task created"),
29 | });
30 | queryClient.invalidateQueries({ queryKey: ["tasks-results"] });
31 | },
32 | onError: () => {
33 | notifications.show({
34 | color: "red",
35 | message: t("Unable to create task"),
36 | });
37 | },
38 | });
39 | };
40 |
--------------------------------------------------------------------------------
/client/src/pages/import/components/ImportSteps/components/TradesImportStep/TradesImportStep.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from "react-i18next";
2 | import { Stack, Title } from "@mantine/core";
3 | import TradesImportFormProvider from "./components/TradesImportForm/TradesImportFormProvider";
4 | import { ICsvTradesRow } from "types/csv";
5 |
6 | interface Props {
7 | trades: ICsvTradesRow[];
8 | portfolioId: number | undefined;
9 | onTradeImported: () => void;
10 | }
11 |
12 | export default function TradesImportStep({
13 | trades,
14 | portfolioId,
15 | onTradeImported,
16 | }: Props) {
17 | const { t } = useTranslation();
18 |
19 | if (!portfolioId) {
20 | return {t("Select a portfolio to import trades.")}
;
21 | }
22 |
23 | if (trades && trades.length > 0) {
24 | console.log(trades);
25 | return (
26 |
27 | {t("Import shares")}
28 | {trades.map((trade) => (
29 |
35 | ))}
36 |
37 | );
38 | }
39 |
40 | return {t("No trades found on the CSV file")}
;
41 | }
42 |
--------------------------------------------------------------------------------
/.github/workflows/docker-build-publish-backend-tag.yml:
--------------------------------------------------------------------------------
1 | name: Docker Build & Publish Backend tag
2 | on:
3 | release:
4 | types:
5 | - created
6 |
7 | jobs:
8 | build-and-publish:
9 | name: Build and Publish Docker Image (tag)
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout code
13 | uses: actions/checkout@v4
14 |
15 | - name: Set up QEMU
16 | uses: docker/setup-qemu-action@v3
17 |
18 | - name: Set up Docker Buildx
19 | uses: docker/setup-buildx-action@v3
20 |
21 | - name: Login to GitHub Container Registry
22 | uses: docker/login-action@v3
23 | with:
24 | registry: ghcr.io
25 | username: ${{ github.actor }}
26 | password: ${{ secrets.GITHUB_TOKEN }}
27 |
28 | - name: Build and Push backend Docker Image (tag)
29 | uses: docker/build-push-action@v5
30 | with:
31 | context: . # Path to your Dockerfile and other build context files
32 | platforms: linux/amd64,linux/arm64
33 | push: true
34 | tags: |
35 | ghcr.io/${{ github.repository }}:${{ github.head_ref || github.ref_name }}
36 | labels: |
37 | org.opencontainers.image.source=https://github.com/bocabitlabs/${{ github.repository }}
38 |
--------------------------------------------------------------------------------
/backend/companies/tests/factory.py:
--------------------------------------------------------------------------------
1 | from factory import Faker, SubFactory, django, post_generation
2 |
3 | from companies.models import Company
4 | from markets.tests.factory import MarketFactory
5 | from portfolios.tests.factory import PortfolioFactory
6 | from sectors.tests.factory import SectorFactory
7 |
8 |
9 | class CompanyFactory(django.DjangoModelFactory):
10 | class Meta:
11 | model = Company
12 |
13 | name = Faker("company")
14 | ticker = Faker("pystr", max_chars=4)
15 | alt_tickers = Faker("paragraph")
16 | description = Faker("paragraph")
17 | url = Faker("url")
18 | color = Faker("color")
19 | broker = Faker("company")
20 | is_closed = Faker("boolean")
21 | country_code = Faker("country_code")
22 |
23 | base_currency = "USD"
24 | dividends_currency = "USD"
25 |
26 | sector = SubFactory(SectorFactory)
27 | market = SubFactory(MarketFactory)
28 | portfolio = SubFactory(PortfolioFactory)
29 |
30 | @post_generation
31 | def set_currencies(self, create, extracted, use_base_currency=False, **kwargs):
32 | if not create:
33 | return
34 | if extracted and use_base_currency:
35 | self.base_currency = extracted["base_currency"]
36 | self.dividends_currency = extracted["base_currency"]
37 |
--------------------------------------------------------------------------------
/backend/stock_prices/views.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from rest_framework import viewsets
4 | from rest_framework.pagination import LimitOffsetPagination
5 |
6 | from stock_prices.models import StockPrice
7 | from stock_prices.serializers import StockPriceSerializer
8 |
9 | logger = logging.getLogger("buho_backend")
10 |
11 |
12 | class ExchangeRateViewSet(viewsets.ModelViewSet):
13 | """Get all the exchange rates from a user"""
14 |
15 | pagination_class = LimitOffsetPagination
16 | serializer_class = StockPriceSerializer
17 |
18 | def get_queryset(self):
19 | sort_by = self.request.query_params.get("sort_by", "transactionDate")
20 | order_by = self.request.query_params.get("order_by", "desc")
21 |
22 | sort_by_fields = {
23 | "transactionDate": "transaction_date",
24 | "transactionPrice": "transaction_price",
25 | "ticker": "ticker",
26 | "priceCurrency": "price_currency",
27 | "price": "price",
28 | }
29 |
30 | # Sort and order the queryset
31 | if order_by == "desc":
32 | queryset = StockPrice.objects.order_by(f"-{sort_by_fields[sort_by]}")
33 | else:
34 | queryset = StockPrice.objects.order_by(f"{sort_by_fields[sort_by]}")
35 |
36 | return queryset
37 |
--------------------------------------------------------------------------------
/.github/workflows/docker-build-publish-client-latest.yml:
--------------------------------------------------------------------------------
1 | name: Docker Build & Publish Client latest
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | jobs:
8 | build-and-publish:
9 | name: Build and Publish Docker Image (latest)
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout code
13 | uses: actions/checkout@v4
14 |
15 | - name: Set up QEMU
16 | uses: docker/setup-qemu-action@v3
17 |
18 | - name: Set up Docker Buildx
19 | uses: docker/setup-buildx-action@v3
20 |
21 | - name: Login to GitHub Container Registry
22 | uses: docker/login-action@v3
23 | with:
24 | registry: ghcr.io
25 | username: ${{ github.actor }}
26 | password: ${{ secrets.GITHUB_TOKEN }}
27 |
28 | - name: Build and Push client Docker Image
29 | uses: docker/build-push-action@v5
30 | with:
31 | context: . # Path to your Dockerfile and other build context files
32 | file: docker.client.Dockerfile
33 | platforms: linux/amd64,linux/arm64
34 | push: true
35 | tags: |
36 | ghcr.io/${{ github.repository }}-client:latest
37 | labels: |
38 | org.opencontainers.image.source=https://github.com/bocabitlabs/${{ github.repository }}
39 |
--------------------------------------------------------------------------------
/client/src/i18n.ts:
--------------------------------------------------------------------------------
1 | import { initReactI18next } from "react-i18next";
2 | import i18n from "i18next";
3 | import english from "locales/en/translation.json";
4 | import spanishSectors from "locales/es/sectors.json";
5 | import spanish from "locales/es/translation.json";
6 |
7 | // the translations
8 | // (tip move them in a JSON file and import them,
9 | // or even better, manage them via a UI: https://react.i18next.com/guides/multiple-translation-files#manage-your-translations-with-a-management-gui)
10 | const resources = {
11 | en: {
12 | translation: { ...english },
13 | },
14 | es: { translation: { ...spanish, ...spanishSectors } },
15 | };
16 |
17 | // eslint-disable-next-line import/no-named-as-default-member
18 | i18n
19 | .use(initReactI18next) // passes i18n down to react-i18next
20 | .init({
21 | resources,
22 | lng: "en", // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources
23 | // you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage
24 | // if you're using a language detector, do not define the lng option
25 | interpolation: {
26 | escapeValue: false, // react already safes from xss
27 | },
28 | });
29 |
30 | export default i18n;
31 |
--------------------------------------------------------------------------------
/backend/exchange_rates/management/commands/get_exchange.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from datetime import datetime
3 |
4 | from django.core.management.base import BaseCommand
5 |
6 | from exchange_rates.services.yfinance_api_client import YFinanceExchangeClient
7 |
8 | logger = logging.getLogger("buho_backend")
9 |
10 |
11 | class Command(BaseCommand):
12 | help = "Gets the exchange rate of a given ticker"
13 |
14 | def add_arguments(self, parser):
15 | parser.add_argument("from_currency", type=str)
16 | parser.add_argument("to_currency", type=str)
17 | parser.add_argument("date", type=str, help="Date in format YYYY-MM-DD")
18 |
19 | def handle(self, *args, **options):
20 | from_currency = options["from_currency"]
21 | to_currency = options["to_currency"]
22 | used_date = options["date"]
23 |
24 | self.stdout.write(
25 | f"Getting data for {from_currency} to {to_currency} on {used_date}"
26 | )
27 | used_date_as_datetime = datetime.strptime(used_date, "%Y-%m-%d")
28 | api_client = YFinanceExchangeClient()
29 | currency = api_client.get_exchange_rate_for_date(
30 | from_currency, to_currency, used_date_as_datetime
31 | )
32 |
33 | self.stdout.write(self.style.SUCCESS(f"{used_date} Data: {currency}"))
34 |
--------------------------------------------------------------------------------
/client/src/pages/import/components/ImportSteps/components/DividendsImportStep/DividendsImportStep.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from "react-i18next";
2 | import { Stack, Title } from "@mantine/core";
3 | import DividendsImportFormProvider from "./components/DividendsImportForm/DividendsImportFormProvider";
4 | import { ICsvDividendRow } from "types/csv";
5 |
6 | interface Props {
7 | dividends: ICsvDividendRow[];
8 | portfolioId: number | undefined;
9 | onDividendImported: () => void;
10 | }
11 |
12 | export default function DividendsImportStep({
13 | dividends,
14 | portfolioId,
15 | onDividendImported,
16 | }: Props) {
17 | const { t } = useTranslation();
18 |
19 | if (!portfolioId) {
20 | return {t("Select a portfolio to import dividends.")}
;
21 | }
22 |
23 | if (dividends && dividends.length > 0) {
24 | return (
25 |
26 | {t("Import dividends")}
27 | {dividends.map((dividend) => (
28 |
34 | ))}
35 |
36 | );
37 | }
38 |
39 | return {t("No dividends found on the CSV file")}
;
40 | }
41 |
--------------------------------------------------------------------------------
/backend/settings/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | from markets.models import TIMEZONES
4 |
5 |
6 | # Create your models here.
7 | class UserSettings(models.Model):
8 | language = models.CharField(max_length=200)
9 | main_portfolio = models.CharField(max_length=200, blank=True, default="")
10 | portfolio_sort_by = models.CharField(max_length=200, blank=True, default="")
11 | portfolio_display_mode = models.CharField(max_length=200, blank=True, default="")
12 | company_sort_by = models.CharField(max_length=200, blank=True, default="")
13 | company_display_mode = models.CharField(max_length=200, blank=True, default="")
14 | timezone = models.CharField(max_length=200, choices=TIMEZONES, default="UTC")
15 | sentry_dsn = models.CharField(max_length=200, blank=True, default="")
16 | sentry_enabled = models.BooleanField(default=False)
17 | display_welcome = models.BooleanField(default=True)
18 |
19 | allow_fetch = models.BooleanField(default=False)
20 |
21 | date_created = models.DateTimeField(auto_now_add=True)
22 | last_updated = models.DateTimeField(auto_now=True)
23 |
24 | class Meta:
25 | verbose_name = "User Settings"
26 | verbose_name_plural = "User Settings"
27 |
28 | def __str__(self):
29 | return f"Language: {self.language}, {self.main_portfolio}"
30 |
--------------------------------------------------------------------------------
/docs/user-guides/deploy-docker-compose.md:
--------------------------------------------------------------------------------
1 | # Deploy using Docker Compose
2 |
3 | You can deploy this application using Docker Compose. To do so, follow these steps.
4 |
5 | ## Requirements
6 |
7 | - Docker
8 |
9 | ## Database
10 |
11 | Please refer to [Choosing a database docs](/docs/development/database-selection) to select and run a database for the application.
12 |
13 | You can choose between `SQLite` and `MySQL`/`MariaDB`.
14 |
15 | ## Create a .env.prod file
16 |
17 | Use the `.env.sample` file and rename it to `.env.prod` (`cp .env.sample .env.prod`) and populate all its values to the desired ones.
18 |
19 | ### Deploy the application with Docker Compose
20 |
21 | ```bash
22 | docker-compose up
23 | ```
24 |
25 | This command will deploy all the containers required by the application (backend, frontend, database, redis and celery).
26 |
27 | It will take the values from the `.env.prod` file.
28 |
29 | ### Configuring the volumes (optional)
30 |
31 | By default, the volumes will be handled automatically by Docker itself (check the `docker-compose.yml` file). If you want to point them to your own paths, you can modify this file to specify it.
32 |
33 | An example pointing to your own path:
34 |
35 | ```yaml
36 | volumes:
37 | - /volume2/buho-stocks/logs:/app/media
38 | ```
39 |
40 | Next: [Initialize the app data](initialize-app-data.md)
--------------------------------------------------------------------------------
/.github/workflows/docker-build-publish-client-tag.yml:
--------------------------------------------------------------------------------
1 | name: Docker Build & Publish Client tag
2 | on:
3 | release:
4 | types:
5 | - created
6 |
7 | jobs:
8 | build-and-publish:
9 | name: Build and Publish Docker Image (tag)
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout code
13 | uses: actions/checkout@v4
14 |
15 | - name: Set up QEMU
16 | uses: docker/setup-qemu-action@v3
17 |
18 | - name: Set up Docker Buildx
19 | uses: docker/setup-buildx-action@v3
20 |
21 | - name: Login to GitHub Container Registry
22 | uses: docker/login-action@v3
23 | with:
24 | registry: ghcr.io
25 | username: ${{ github.actor }}
26 | password: ${{ secrets.GITHUB_TOKEN }}
27 |
28 | - name: Build and Push client Docker Image (tag)
29 | uses: docker/build-push-action@v5
30 | with:
31 | context: . # Path to your Dockerfile and other build context files
32 | file: docker.client.Dockerfile
33 | platforms: linux/amd64,linux/arm64
34 | push: true
35 | tags: |
36 | ghcr.io/${{ github.repository }}-client:${{ github.head_ref || github.ref_name }}
37 | labels: |
38 | org.opencontainers.image.source=https://github.com/bocabitlabs/${{ github.repository }}
39 |
--------------------------------------------------------------------------------
/backend/rights_transactions/management/commands/set_rights_total.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from django.core.management.base import BaseCommand
4 |
5 | from rights_transactions.models import RightsTransaction
6 |
7 | logger = logging.getLogger("buho_backend")
8 |
9 |
10 | class Command(BaseCommand):
11 | help = "Set the rights total amount for all the shares transactions"
12 |
13 | def handle(self, *args, **options):
14 | self.stdout.write("Updating total amount for rights transactions")
15 | # Iterate all the dividends transactions and set the total amount
16 | for transaction in RightsTransaction.objects.all():
17 | transaction.total_amount = (
18 | transaction.count * transaction.gross_price_per_share
19 | )
20 | transaction.save()
21 | self.stdout.write("Updating total amount currency for rights transactions")
22 | # Iterate all the dividends transactions and set the total amount currency to
23 | # the company dividends currency
24 | for transaction in RightsTransaction.objects.all():
25 | transaction.total_amount.currency = transaction.company.base_currency
26 | transaction.save()
27 |
28 | self.stdout.write(
29 | self.style.SUCCESS("Successfully set the rights total amount")
30 | )
31 |
--------------------------------------------------------------------------------
/backend/shares_transactions/management/commands/set_shares_total.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from django.core.management.base import BaseCommand
4 |
5 | from shares_transactions.models import SharesTransaction
6 |
7 | logger = logging.getLogger("buho_backend")
8 |
9 |
10 | class Command(BaseCommand):
11 | help = "Set the shares total amount for all the shares transactions"
12 |
13 | def handle(self, *args, **options):
14 | self.stdout.write("Updating total amount for shares transactions")
15 | # Iterate all the dividends transactions and set the total amount
16 | for transaction in SharesTransaction.objects.all():
17 | transaction.total_amount = (
18 | transaction.count * transaction.gross_price_per_share
19 | )
20 | transaction.save()
21 | self.stdout.write("Updating total amount currency for shares transactions")
22 | # Iterate all the dividends transactions and set the total amount currency
23 | # to the company dividends currency
24 | for transaction in SharesTransaction.objects.all():
25 | transaction.total_amount.currency = transaction.company.base_currency
26 | transaction.save()
27 |
28 | self.stdout.write(
29 | self.style.SUCCESS("Successfully set the shares total amount")
30 | )
31 |
--------------------------------------------------------------------------------
/backend/log_messages/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.7 on 2023-04-07 09:12
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 | initial = True
9 |
10 | dependencies = [
11 | ("portfolios", "0001_initial"),
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name="LogMessage",
17 | fields=[
18 | ("id", models.AutoField(primary_key=True, serialize=False)),
19 | ("message_text", models.CharField(max_length=400)),
20 | ("message_type", models.CharField(max_length=100)),
21 | ("date_created", models.DateTimeField(auto_now_add=True)),
22 | ("last_updated", models.DateTimeField(auto_now=True)),
23 | (
24 | "portfolio",
25 | models.ForeignKey(
26 | on_delete=django.db.models.deletion.CASCADE,
27 | related_name="log_messages",
28 | to="portfolios.portfolio",
29 | ),
30 | ),
31 | ],
32 | options={
33 | "verbose_name": "Log Message",
34 | "verbose_name_plural": "Log Messages",
35 | },
36 | ),
37 | ]
38 |
--------------------------------------------------------------------------------
/client/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { MantineProvider } from "@mantine/core";
3 | import { QueryClientProvider } from "@tanstack/react-query";
4 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
5 | import dayjs from "dayjs";
6 | import customParseFormat from "dayjs/plugin/customParseFormat";
7 | import timezone from "dayjs/plugin/timezone";
8 | import utc from "dayjs/plugin/utc"; // ES 2015
9 | import ReactDOM from "react-dom/client";
10 | import "./i18n";
11 | import queryClient from "api/query-client";
12 | import App from "App";
13 | import "@mantine/core/styles.css";
14 | import "@mantine/dropzone/styles.css";
15 | import "@mantine/dates/styles.css";
16 | import "mantine-react-table/styles.css";
17 | import "@mantine/charts/styles.css";
18 | import "@mantine/tiptap/styles.css";
19 | import "@mantine/notifications/styles.css";
20 |
21 | // dependent on utc plugin
22 | dayjs.extend(utc);
23 | dayjs.extend(timezone);
24 | dayjs.extend(customParseFormat);
25 |
26 | const root = ReactDOM.createRoot(
27 | document.getElementById("root") as HTMLElement,
28 | );
29 |
30 | root.render(
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | ,
39 | );
40 |
--------------------------------------------------------------------------------
/client/src/pages/portfolios/PorfolioChartsPage/components/ChartsList/components/ChartInvestedByCompany/ChartInvestedByCompanyProvider.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from "react-i18next";
2 | import { useParams } from "react-router-dom";
3 | import { Center, Loader, Stack, Title } from "@mantine/core";
4 | import ChartInvestedByCompany from "./ChartInvestedByCompany";
5 | import { usePortfolioYearStatsByCompany } from "hooks/use-stats/use-portfolio-stats";
6 |
7 | type Props = { selectedYear: string; currency: string };
8 |
9 | export default function ChartInvestedByCompanyProvider({
10 | selectedYear,
11 | currency,
12 | }: Props) {
13 | const { id } = useParams();
14 | const { t } = useTranslation();
15 |
16 | // Hooks
17 | const {
18 | data: statsData,
19 | isLoading,
20 | isError,
21 | error,
22 | } = usePortfolioYearStatsByCompany(+id!, selectedYear);
23 |
24 | if (isLoading) {
25 | return ;
26 | }
27 | if (isError) {
28 | return {error.message ? error.message : t("An error occurred")}
;
29 | }
30 |
31 | if (statsData) {
32 | return (
33 |
34 |
35 | {t("Accumulated investment")}
36 |
37 |
38 |
39 |
40 |
41 | );
42 | }
43 | return null;
44 | }
45 |
--------------------------------------------------------------------------------
/client/src/pages/portfolios/PorfolioChartsPage/components/ChartsList/components/ChartSectorsByCompany/ChartSectorsByCompanyProvider.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from "react-i18next";
2 | import { useParams } from "react-router-dom";
3 | import { Center, Loader, Stack, Title } from "@mantine/core";
4 | import { useElementSize } from "@mantine/hooks";
5 | import ChartSectorsByCompany from "./ChartSectorsByCompany";
6 | import { usePortfolioYearStatsByCompany } from "hooks/use-stats/use-portfolio-stats";
7 |
8 | type Props = { selectedYear: string };
9 |
10 | export default function ChartSectorsByCompanyProvider({ selectedYear }: Props) {
11 | const { t } = useTranslation();
12 | const { id } = useParams();
13 | const {
14 | data: statsData,
15 | isLoading,
16 | isError,
17 | error,
18 | } = usePortfolioYearStatsByCompany(+id!, selectedYear);
19 | const { ref, width } = useElementSize();
20 |
21 | if (isLoading) {
22 | return ;
23 | }
24 | if (isError) {
25 | return {error.message ? error.message : t("An error occurred")}
;
26 | }
27 |
28 | if (statsData) {
29 | return (
30 |
31 |
32 | {t("Sectors")}
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 | return null;
41 | }
42 |
--------------------------------------------------------------------------------
/client/src/pages/portfolios/PorfolioChartsPage/components/ChartsList/components/ChartMarketByCompany/ChartMarketsByCompanyProvider.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from "react-i18next";
2 | import { useParams } from "react-router-dom";
3 | import { Center, Loader, Stack, Title } from "@mantine/core";
4 | import { useElementSize } from "@mantine/hooks";
5 | import ChartMarketByCompany from "./ChartMarketByCompany";
6 | import { usePortfolioYearStatsByCompany } from "hooks/use-stats/use-portfolio-stats";
7 |
8 | interface Props {
9 | selectedYear: string;
10 | }
11 |
12 | export default function ChartMarketsByCompanyProvider({ selectedYear }: Props) {
13 | const { t } = useTranslation();
14 | const { id } = useParams();
15 | const {
16 | data: statsData,
17 | isLoading,
18 | isError,
19 | error,
20 | } = usePortfolioYearStatsByCompany(+id!, selectedYear);
21 | const { ref, width } = useElementSize();
22 | if (isLoading) {
23 | return ;
24 | }
25 | if (isError) {
26 | return {error.message ? error.message : t("An error occurred")}
;
27 | }
28 |
29 | if (statsData) {
30 | return (
31 |
32 |
33 | {t("Markets")}
34 |
35 |
36 |
37 |
38 |
39 | );
40 | }
41 | return null;
42 | }
43 |
--------------------------------------------------------------------------------
/client/src/pages/portfolios/PorfolioChartsPage/components/ChartsList/components/ChartInvestedByCompanyYearly/ChartInvestedByCompanyYearlyProvider.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from "react-i18next";
2 | import { useParams } from "react-router-dom";
3 | import { Center, Loader, Stack, Title } from "@mantine/core";
4 | import ChartInvestedByCompanyYearly from "./ChartInvestedByCompanyYearly";
5 | import { usePortfolioYearStatsByCompany } from "hooks/use-stats/use-portfolio-stats";
6 |
7 | type Props = { selectedYear: string; currency: string };
8 |
9 | export default function ChartInvestedByCompanyYearlyProvider({
10 | selectedYear,
11 | currency,
12 | }: Props) {
13 | const { id } = useParams();
14 | const { t } = useTranslation();
15 |
16 | const {
17 | data: statsData,
18 | isLoading,
19 | isError,
20 | error,
21 | } = usePortfolioYearStatsByCompany(+id!, selectedYear);
22 |
23 | if (isLoading) {
24 | return ;
25 | }
26 | if (isError) {
27 | return {error.message ? error.message : t("An error occurred")}
;
28 | }
29 |
30 | if (statsData) {
31 | return (
32 |
33 |
34 | {t("Invested by company yearly")}
35 |
36 |
37 |
38 |
39 |
40 | );
41 | }
42 | return null;
43 | }
44 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "files.associations": {
4 | "*.html": "jinja-html",
5 | "*.xml": "jinja-xml"
6 | },
7 | "editor.codeActionsOnSave": {
8 | "source.fixAll": "explicit"
9 | },
10 | "eslint.validate": [
11 | "javascript"
12 | ],
13 | "isort.check": true,
14 | "isort.args": [
15 | "--profile",
16 | "black"
17 | ],
18 | "[html]": {
19 | "editor.defaultFormatter": "monosans.djlint"
20 | },
21 | "[django-html]": {
22 | "editor.defaultFormatter": "monosans.djlint"
23 | },
24 | "[jinja]": {
25 | "editor.defaultFormatter": "monosans.djlint"
26 | },
27 | "[jinja-html]": {
28 | "editor.defaultFormatter": "monosans.djlint"
29 | },
30 | "python.analysis.autoSearchPaths": true,
31 | "python.analysis.extraPaths": ["backend"],
32 | "python.analysis.ignore": [
33 | "**/site-packages/**/*.py",
34 | "**/migrations/*.py"
35 | ],
36 | "python.autoComplete.extraPaths": [
37 | ".venv",
38 | "backend"
39 | ],
40 | "python.terminal.activateEnvInCurrentTerminal": true,
41 | "python.testing.pytestEnabled": true,
42 | "[python]": {
43 | "editor.codeActionsOnSave": {
44 | "source.organizeImports": "explicit"
45 | }
46 | },
47 | "search.exclude": {
48 | "**/.venv": true,
49 | "**/node_modules": true,
50 | "**/mypy_cache": true,
51 | "**/package-lock.json": true,
52 | "**/poetry.lock": true,
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/client/src/pages/portfolios/PorfolioChartsPage/components/ChartsList/components/ChartBrokerByCompany/ChartBrokerByCompanyProvider.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from "react-i18next";
2 | import { useParams } from "react-router-dom";
3 | import { Center, Loader, Stack, Title } from "@mantine/core";
4 | import { useElementSize } from "@mantine/hooks";
5 | import ChartBrokerByCompany from "./ChartBrokerByCompany";
6 | import { usePortfolioYearStatsByCompany } from "hooks/use-stats/use-portfolio-stats";
7 |
8 | interface Props {
9 | selectedYear: string;
10 | }
11 |
12 | export default function ChartBrokerByCompanyProvider({ selectedYear }: Props) {
13 | const { t } = useTranslation();
14 | const { id } = useParams();
15 |
16 | const {
17 | data: statsData,
18 | isLoading,
19 | isError,
20 | error,
21 | } = usePortfolioYearStatsByCompany(+id!, selectedYear);
22 | const { ref, width } = useElementSize();
23 |
24 | if (isLoading) {
25 | return ;
26 | }
27 | if (isError) {
28 | return {error.message ? error.message : t("An error occurred")}
;
29 | }
30 |
31 | if (statsData) {
32 | return (
33 |
34 |
35 | {t("Brokers")}
36 |
37 |
38 |
39 |
40 |
41 | );
42 | }
43 | return null;
44 | }
45 |
--------------------------------------------------------------------------------
/client/src/pages/portfolios/PorfolioChartsPage/components/ChartsList/components/ChartValueByCompany/ChartValueByCompanyProvider.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useTranslation } from "react-i18next";
3 | import { useParams } from "react-router-dom";
4 | import { Center, Loader, Stack, Title } from "@mantine/core";
5 | import ChartValueByCompany from "./ChartValueByCompany";
6 | import { usePortfolioYearStatsByCompany } from "hooks/use-stats/use-portfolio-stats";
7 |
8 | type Props = { selectedYear: string; currency: string };
9 |
10 | export default function ChartValueByCompanyProvider({
11 | selectedYear,
12 | currency,
13 | }: Props) {
14 | const { id } = useParams();
15 | const { t } = useTranslation();
16 | const {
17 | data: statsData,
18 | isLoading,
19 | isError,
20 | error,
21 | } = usePortfolioYearStatsByCompany(+id!, selectedYear);
22 | if (isLoading) {
23 | return ;
24 | }
25 | if (isError) {
26 | return {error.message ? error.message : t("An error occurred")}
;
27 | }
28 | if (statsData) {
29 | return (
30 |
31 |
32 |
33 | {t("Portfolio value by company (accumulated)")}
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 | }
42 | return null;
43 | }
44 |
--------------------------------------------------------------------------------
/.github/workflows/django.yml:
--------------------------------------------------------------------------------
1 | name: Django CI
2 |
3 | on:
4 | push:
5 | branches: [main, develop]
6 | pull_request:
7 | branches: [main, develop]
8 |
9 | jobs:
10 | build:
11 | env:
12 | DJANGO_ENV: test
13 | runs-on: ubuntu-latest
14 | strategy:
15 | max-parallel: 4
16 | matrix:
17 | python-version: [3.11.9]
18 |
19 | steps:
20 | - uses: actions/checkout@v3
21 | - name: Set up Python ${{ matrix.python-version }}
22 | uses: actions/setup-python@v4
23 | with:
24 | python-version: ${{ matrix.python-version }}
25 | - name: Install poetry
26 | uses: abatilo/actions-poetry@v2
27 | - uses: actions/cache@v3
28 | name: Define a cache for the virtual environment based on the dependencies lock file
29 | with:
30 | path: ./.venv
31 | key: venv-${{ hashFiles('poetry.lock') }}
32 | - name: Install the project dependencies
33 | run: |
34 | mv .env.ci.sample .env
35 | poetry install
36 | - name: Run Tests
37 | run: |
38 | cd backend && poetry run coverage run manage.py test
39 | poetry run coverage report
40 | poetry run coverage xml
41 | - name: Upload coverage to Codecov
42 | uses: codecov/codecov-action@v4
43 | with:
44 | token: ${{ secrets.CODECOV_TOKEN }} # required
45 | verbose: true # optional (default = false)
46 |
--------------------------------------------------------------------------------