Skip to content

Commit

Permalink
feat(budget): manage your daily expenses (N26 only) (#135)
Browse files Browse the repository at this point in the history
* feat(budget): manage your daily expenses! (N26 only)

* refactor(budget): move table render in separate file

* feat(budget): interactively set expense fields

* fix: remove default imports to budget

* refactor: rearrange budget methods to fetch/render/review

* fix(budget): exit review gracefully on CTRL+C

* feat(budget): add command line & Assistant options

* refactor: inherit N26 from SourceBaseExpense

* build: add budget dependencies

* build: add pytz dependency

* chore: update readme with screenshots

* feat: add budget summary console panel

* feat: add monthly mean to summary

* refactor(n26): prompt user credentials like Finary

* test: disable finary tests for now

* feat: hide expense table when empty
  • Loading branch information
MadeInPierre authored Jul 15, 2023
1 parent 8b9f7ee commit 6209b40
Show file tree
Hide file tree
Showing 21 changed files with 1,553 additions and 137 deletions.
32 changes: 22 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<br>
</div>

Finalynx is your "Finary Assistant", a command-line tool to organize your investments portfolio and get automated monthly investment recommendations based on your future life goals.
Finalynx is your "Finary Assistant", a command-line (and experimental web dashboard) tool to organize your investments portfolio and get automated monthly investment recommendations based on your future life goals.
This tool synchronizes with your [Finary](https://finary.com/) account to show real-time investment values.

Don't have Finary yet? You can sign up using my [referral link](https://finary.com/referral/f8d349c922d1e1c8f0d2) 🌹 (or through the [default](https://finary.com/signup) page).
Expand All @@ -29,6 +29,27 @@ Don't have Finary yet? You can sign up using my [referral link](https://finary.c
<img src="https://raw.githubusercontent.com/MadeInPierre/finalynx/main/docs/_static/screenshot_demo_frameless.png" width="600" />
</p>

<details>
<summary>
<div align="center">
<strong>[Click]</strong> Additional screenshots 📸
</div>
</summary>

| Recommendations | Web dashboard |
|---|---|
| <img src="https://raw.githubusercontent.com/MadeInPierre/finalynx/main/docs/_static/screenshot_recommendations.png" width="600" /> | <img src="https://raw.githubusercontent.com/MadeInPierre/finalynx/main/docs/_static/screenshot_dashboard.png" width="600" /> |

Finalynx also includes a daily budget manager to classify your expenses and show monthly & yearly statistics:

<img src="https://raw.githubusercontent.com/MadeInPierre/finalynx/main/docs/_static/budget.png"/>

<img src="https://raw.githubusercontent.com/MadeInPierre/finalynx/main/docs/_static/budget_review.png"/>

Statistics and visualizations will be added soon!

</details>

## ✨ Features

1. **✅ Portfolio:** Organize your assets, set targets, and sync with your Finary account.
Expand All @@ -39,15 +60,6 @@ Don't have Finary yet? You can sign up using my [referral link](https://finary.c

You can check the [current development status](https://github.com/users/MadeInPierre/projects/4). Contributions are warmly welcome!

<details>
<summary><strong>[Click]</strong> Additional screenshots 📸</summary>

| Recommendations | Web dashboard |
|---|---|
| <img src="https://raw.githubusercontent.com/MadeInPierre/finalynx/main/docs/_static/screenshot_recommendations.png" width="600" /> | <img src="https://raw.githubusercontent.com/MadeInPierre/finalynx/main/docs/_static/screenshot_dashboard.png" width="600" /> |

</details>

## 🚀 Installation
If you don't plan on touching the code, simply run (with python >=3.10 and pip installed):
```sh
Expand Down
Binary file added docs/_static/budget.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/_static/budget_review.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 38 additions & 23 deletions finalynx/assistant.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import os
from datetime import date
from typing import Any
from typing import List
from typing import Optional
from typing import Tuple
Expand All @@ -11,10 +12,11 @@
from finalynx import Dashboard
from finalynx import Fetch
from finalynx import Portfolio
from finalynx.budget.budget import Budget
from finalynx.config import get_active_theme as TH
from finalynx.config import set_active_theme
from finalynx.copilot.recommendations import render_recommendations
from finalynx.fetch.source_base import SourceBase
from finalynx.fetch.source_base_line import SourceBaseLine
from finalynx.fetch.source_finary import SourceFinary
from finalynx.portfolio.bucket import Bucket
from finalynx.portfolio.envelope import Envelope
Expand Down Expand Up @@ -79,6 +81,8 @@ def __init__(
active_sources: Optional[List[str]] = None,
theme: Optional[finalynx.theme.Theme] = None,
sidecars: Optional[List[Sidecar]] = None,
check_budget: bool = False,
interactive: bool = False,
ignore_argv: bool = False,
):
self.portfolio = portfolio
Expand All @@ -98,6 +102,8 @@ def __init__(
self.export_dir = export_dir
self.active_sources = active_sources if active_sources else ["finary"]
self.sidecars = sidecars if sidecars else []
self.check_budget = check_budget
self.interactive = interactive

# Set the global color theme if specified
if theme:
Expand All @@ -109,8 +115,9 @@ def __init__(

# Create the fetching manager instance
self._fetch = Fetch(self.portfolio, self.clear_cache, self.ignore_orphans)
self.budget = Budget()

def add_source(self, source: SourceBase) -> None:
def add_source(self, source: SourceBaseLine) -> None:
"""Register a source, either defined in your own config or from the available Finalynx sources
using `from finalynx.fetch.source_any import SourceAny`."""
self._fetch.add_source(source)
Expand All @@ -135,6 +142,12 @@ def _parse_args(self) -> None:
self.show_data = True
if args["dashboard"]:
self.launch_dashboard = True
if args["budget"]:
self.check_budget = True
if args["--interactive"]:
if not self.check_budget:
console.log("[red][bold]Error:[/] --interactive can only be used with budget, ignoring.")
self.interactive = True
if args["--format"]:
self.output_format = args["--format"]
if args["--sidecar"]:
Expand Down Expand Up @@ -184,9 +197,17 @@ def run(self) -> None:
# Fetch from the online sources and process the portfolio
fetched_tree = self.initialize()

# Fetch the budget from N26 if enabled
if self.check_budget:
fetched_tree.add(self.budget.fetch(self.clear_cache, self.force_signin))
console.log("[bold]Tip:[/] run again with -I or --interactive review the expenses 👀")

# Render the console elements
main_frame = self.render_mainframe()
panels = self.render_panels()
renders: List[Any] = [main_frame, panels]
if self.check_budget:
renders.append(self.budget.render_expenses())

# Save the current portfolio to a file. Useful for statistics later
if self.enable_export:
Expand All @@ -197,13 +218,12 @@ def run(self) -> None:
console.print(Panel(fetched_tree, title="Fetched data"))

# Display the entire portfolio and associated recommendations
console.print(
"\n\n",
main_frame,
"\n\n",
panels,
"\n",
)
for render in renders:
console.print("\n\n", render)

# Interactive review of the budget expenses if enabled
if self.check_budget and self.interactive:
self.budget.interactive_review()

# Host a local webserver with the running dashboard
if self.launch_dashboard:
Expand Down Expand Up @@ -250,25 +270,20 @@ def render_mainframe(self) -> Columns:
def render_panels(self) -> Columns:
"""Renders the default set of panels used in the default console view when calling run()."""

def panel(title: str, content: Any) -> Panel:
return Panel(content, title=title, padding=(1, 2), expand=False, border_style=TH().PANEL)

# Final set of results to be displayed
panels: List[ConsoleRenderable] = [
Text(" "),
Panel(
render_recommendations(self.portfolio, self.envelopes),
title="Recommendations",
padding=(1, 2),
expand=False,
border_style=TH().PANEL,
),
Panel(
self.render_performance_report(),
title="Performance",
padding=(1, 2),
expand=False,
border_style=TH().PANEL,
),
panel("Recommendations", render_recommendations(self.portfolio, self.envelopes)),
panel("Performance", self.render_performance_report()),
]

# Add the budget panel if enabled
if self.check_budget:
panels.append(panel("Budget", self.budget.render_summary()))

return Columns(panels, padding=(2, 2))

def render_performance_report(self) -> Tree:
Expand Down
6 changes: 6 additions & 0 deletions finalynx/budget/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# flake8: noqa
from .budget import Budget
from .expense import Constraint
from .expense import Expense
from .expense import Period
from .expense import Status
87 changes: 87 additions & 0 deletions finalynx/budget/_render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""
This module contains the logic for displaying a table of expenses. It is only used
internally by the `Budget` class.
"""
from datetime import datetime
from typing import List
from typing import Optional

from rich import box
from rich.table import Table

from ..config import get_active_theme as TH
from .expense import Constraint
from .expense import Expense
from .expense import Period
from .expense import Status


def _render_expenses_table(
expenses: List[Expense],
title: str = "",
caption: str = "",
focus: Optional[int] = None,
) -> Table:
"""Generate a rich console table from a list of expenses."""
table = Table(title=title, box=box.MINIMAL, caption=caption, caption_justify="right", expand=True)
table.add_column("#", justify="center", style=TH().TEXT)
table.add_column("Date", justify="left", style="orange1")
table.add_column("Time", justify="left", style="orange1")
table.add_column("Amount", justify="right", style="red")
table.add_column("Merchant", justify="left", style="cyan")
table.add_column("Category", justify="left", style=TH().HINT)
table.add_column("Status", justify="left", style=TH().TEXT)
table.add_column("I Paid", justify="right", style="red")
table.add_column("Payback", justify="left", style=TH().TEXT)
table.add_column("Type", justify="left", style=TH().TEXT)
table.add_column("Period", justify="left", style=TH().TEXT)
table.add_column("Comment", justify="left", style=TH().HINT)

for i, t in enumerate(expenses):
# Format date and time
ts_date = t.as_datetime()
day_name_str = ts_date.strftime("%A")[:3]
day_nth_str = ts_date.strftime("%d")
month_str = ts_date.strftime("%B")
time_str = ts_date.strftime("%H:%M")
date_str = f"{day_name_str} {day_nth_str} {month_str[:3]}"

# Format amount with colors
amount_color = "" if t.amount < 0 else "[green]"
amount_str = f"{amount_color}{t.amount:.2f} €"

# Format i_paid with colors just as amount
i_paid_color = "[green]" if t.i_paid is not None and t.i_paid > 0 else ""
i_paid_str = f"{i_paid_color}{t.i_paid:.2f} €" if t.i_paid is not None else ""

# Add a separator between months
separator = False
if len(expenses) > 1 and i < len(expenses) - 1:
np1_month_str = datetime.utcfromtimestamp(int(expenses[i + 1].timestamp) / 1000).strftime("%B")
separator = bool(month_str != np1_month_str)

# If focus is set, highlight the corresponding row and dim the others
if focus is not None:
style = "bold" if i == focus else "dim"
else:
style = "dim" if "(transfer)" in t.merchant_name else "none"

# Add the row to the table
table.add_row(
str(t.cell_number),
date_str,
time_str,
amount_str,
t.merchant_name,
t.merchant_category[:30],
str(t.status.value).capitalize() if t.status != Status.UNKNOWN else "",
i_paid_str,
t.payback,
str(t.constraint.value).capitalize() if t.constraint != Constraint.UNKNOWN else "",
str(t.period.value).capitalize() if t.period != Period.UNKNOWN else "",
t.comment,
style=style,
end_section=separator,
)

return table
Loading

0 comments on commit 6209b40

Please sign in to comment.