Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add calculation and disply for average of incomes and expenses #1619

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions frontend/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -461,3 +461,19 @@ td .status-indicator {
.options td:nth-child(2) {
white-space: normal;
}
/*
* Incomes Expenses Average
*/
.inc_exp_avg_row {
display: flex;
flex-wrap: wrap;
}
.inc_exp_avg_column {
flex: 1;
}
.inc_exp_avg_header {
font-weight: 400;
background-color: var(--table-header-background);
border: 1px solid var(--table-border);
text-align: center;
}
63 changes: 61 additions & 2 deletions src/fava/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
"""
from __future__ import annotations

from collections import defaultdict
from dataclasses import fields
from datetime import date
from datetime import datetime
from functools import lru_cache
from functools import reduce
from io import BytesIO
from pathlib import Path
from threading import Lock
Expand All @@ -25,6 +27,7 @@
from urllib.parse import urlunparse

import markdown2 # type: ignore[import]
from _decimal import Decimal
from beancount import __version__ as beancount_version
from beancount.utils.text_utils import replace_numbers
from flask import abort
Expand Down Expand Up @@ -66,6 +69,9 @@
from flask.wrappers import Response
from werkzeug import Response as WerkzeugResponse

from fava.core.charts import DateAndBalanceWithBudget
from fava.internal_api import ChartData


setup_logging()

Expand Down Expand Up @@ -163,9 +169,62 @@ def _setup_template_config(fava_app: Flask) -> None:
fava_app.add_template_global(get_ledger_data, "get_ledger_data")

@fava_app.context_processor
def _template_context() -> dict[str, FavaLedger | type[ChartApi]]:
def _template_context() -> (
dict[
str,
FavaLedger
| type[ChartApi]
| tuple[dict[str, Decimal], dict[str, Decimal]],
]
):
"""Inject variables into the template context."""
return {"ledger": g.ledger, "chart_api": ChartApi}
incomes_expenses_averages = _calculate_chart_average()

return {
"ledger": g.ledger,
"chart_api": ChartApi,
"incomes_expenses_averages": incomes_expenses_averages,
}


def _calculate_chart_average() -> (
tuple[dict[str, Decimal], dict[str, Decimal]]
):
income_interval_totals: ChartData = ChartApi.interval_totals(
g.interval, g.ledger.options["name_income"], ""
)
expense_interval_totals: ChartData = ChartApi.interval_totals(
g.interval, g.ledger.options["name_expenses"], ""
)

def sum_balances(
total_balances: dict[str, Decimal], d: DateAndBalanceWithBudget
) -> dict[str, Decimal]:
for key, value in d.balance.items():
total_balances[key] = total_balances[key] + value
return total_balances

income_balances: dict[str, Decimal] = reduce(
sum_balances,
income_interval_totals.data,
defaultdict(lambda: Decimal(0)),
)
expense_balances: dict[str, Decimal] = reduce(
sum_balances,
expense_interval_totals.data,
defaultdict(lambda: Decimal(0)),
)

income_averages = {
ib[0]: ib[1] / len(income_interval_totals.data)
for ib in income_balances.items()
}
expense_averages = {
ib[0]: ib[1] / len(expense_interval_totals.data)
for ib in expense_balances.items()
}

return income_averages, expense_averages


def _setup_filters(fava_app: Flask, read_only: bool, incognito: bool) -> None:
Expand Down
31 changes: 31 additions & 0 deletions src/fava/templates/income_statement.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
{% set root_tree = g.filtered.root_tree %}
{% set options = ledger.options %}
{% set invert = ledger.fava_options.invert_income_liabilities_equity %}
{% set incomesAverage = incomes_expenses_averages[0]%}
{% set expensesAverage = incomes_expenses_averages[1]%}

<svelte-component type="charts"><script type="application/json">{{
[
Expand All @@ -14,6 +16,35 @@
]|tojson
}}</script></svelte-component>

<div class="row">
<div class="column">
<div class="inc_exp_avg_header">Incomes Average</div>
{% for inc_average in incomes_expenses_averages[0] %}
<div class="inc_exp_avg_row">
<div class="inc_exp_avg_column">
{{inc_average}}
</div>
<div class="inc_exp_avg_column">
{{incomesAverage[inc_average] | format_currency}}
</div>
</div>
{% endfor %}
</div>
<div class="column">
<div class="inc_exp_avg_header">Expenses Average</div>
{% for exp_average in incomes_expenses_averages[1] %}
<div class="inc_exp_avg_row">
<div class="inc_exp_avg_column">
{{exp_average}}
</div>
<div class="inc_exp_avg_column">
{{expensesAverage[exp_average] | format_currency}}
</div>
</div>
{% endfor %}
</div>
</div>

<div class="row">
<div class="column">
{{ tree_table.tree(root_tree.get(options['name_income']), invert=invert) }}
Expand Down
18 changes: 18 additions & 0 deletions tests/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
from beancount import __version__ as beancount_version

from fava import __version__ as fava_version
from fava.application import _calculate_chart_average
from fava.application import create_app
from fava.application import SERVER_SIDE_REPORTS
from fava.application import static_url
from fava.context import g
from fava.template_filters import format_currency

if TYPE_CHECKING: # pragma: no cover
from pathlib import Path
Expand Down Expand Up @@ -264,3 +266,19 @@ def test_load_extension_reports(test_client: FlaskClient) -> None:
url = "/extension-report/extension/MissingExtension/"
result = test_client.get(url)
assert result.status_code == 404


def test_calculate_average_income_expenses(app: Flask) -> None:
with app.test_request_context("/long-example/?interval=year"):
app.preprocess_request()
averages = _calculate_chart_average()
income_averages = averages[0]
expenses_averages = averages[1]

assert format_currency(income_averages["IRAUSD"]) == "-3147.06"
assert format_currency(income_averages["USD"]) == "-18309.54"
assert format_currency(income_averages["VACHR"]) == "-18.24"

assert format_currency(expenses_averages["IRAUSD"]) == "2723.53"
assert format_currency(expenses_averages["USD"]) == "13142.98"
assert format_currency(expenses_averages["VACHR"]) == "23.06"