Skip to content

Commit 94533e5

Browse files
committed
Add calculation and disply for average of incomes and expenses
1 parent 6293d09 commit 94533e5

File tree

4 files changed

+117
-1
lines changed

4 files changed

+117
-1
lines changed

frontend/css/base.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,3 +461,19 @@ td .status-indicator {
461461
.options td:nth-child(2) {
462462
white-space: normal;
463463
}
464+
/*
465+
* Incomes Expenses Average
466+
*/
467+
.inc_exp_avg_row {
468+
display: flex;
469+
flex-wrap: wrap;
470+
}
471+
.inc_exp_avg_column {
472+
flex: 1;
473+
}
474+
.inc_exp_avg_header {
475+
font-weight: 400;
476+
background-color: var(--table-header-background);
477+
border: 1px solid var(--table-border);
478+
text-align: center;
479+
}

src/fava/application.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
"""
1212
from __future__ import annotations
1313

14+
from collections import defaultdict
1415
from dataclasses import fields
1516
from datetime import date
1617
from datetime import datetime
1718
from functools import lru_cache
19+
from functools import reduce
1820
from io import BytesIO
1921
from pathlib import Path
2022
from threading import Lock
@@ -25,6 +27,7 @@
2527
from urllib.parse import urlunparse
2628

2729
import markdown2 # type: ignore[import]
30+
from _decimal import Decimal
2831
from beancount import __version__ as beancount_version
2932
from beancount.utils.text_utils import replace_numbers
3033
from flask import abort
@@ -66,6 +69,9 @@
6669
from flask.wrappers import Response
6770
from werkzeug import Response as WerkzeugResponse
6871

72+
from fava.core.charts import DateAndBalanceWithBudget
73+
from fava.internal_api import ChartData
74+
6975

7076
setup_logging()
7177

@@ -164,8 +170,53 @@ def _setup_template_config(fava_app: Flask) -> None:
164170

165171
@fava_app.context_processor
166172
def _template_context() -> dict[str, FavaLedger | type[ChartApi]]:
173+
incomes_expenses_averages = _calculate_chart_average()
167174
"""Inject variables into the template context."""
168-
return {"ledger": g.ledger, "chart_api": ChartApi}
175+
return {
176+
"ledger": g.ledger,
177+
"chart_api": ChartApi,
178+
"incomes_expenses_averages": incomes_expenses_averages,
179+
}
180+
181+
182+
def _calculate_chart_average() -> (
183+
tuple[dict[str, Decimal], dict[str, Decimal]]
184+
):
185+
income_interval_totals: ChartData = ChartApi.interval_totals(
186+
g.interval, g.ledger.options["name_income"], ""
187+
)
188+
expense_interval_totals: ChartData = ChartApi.interval_totals(
189+
g.interval, g.ledger.options["name_expenses"], ""
190+
)
191+
192+
def sum_balances(
193+
total_balances: dict[str, Decimal], d: DateAndBalanceWithBudget
194+
) -> dict[str, Decimal]:
195+
for key, value in d.balance.items():
196+
total_balances[key] = total_balances[key] + value
197+
return total_balances
198+
199+
income_balances = reduce(
200+
sum_balances,
201+
income_interval_totals.data,
202+
defaultdict(lambda: Decimal(0)),
203+
)
204+
expense_balances = reduce(
205+
sum_balances,
206+
expense_interval_totals.data,
207+
defaultdict(lambda: Decimal(0)),
208+
)
209+
210+
income_averages = {
211+
ib[0]: ib[1] / len(income_interval_totals.data)
212+
for ib in income_balances.items()
213+
}
214+
expense_averages = {
215+
ib[0]: ib[1] / len(expense_interval_totals.data)
216+
for ib in expense_balances.items()
217+
}
218+
219+
return income_averages, expense_averages
169220

170221

171222
def _setup_filters(fava_app: Flask, read_only: bool, incognito: bool) -> None:

src/fava/templates/income_statement.html

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
{% set root_tree = g.filtered.root_tree %}
44
{% set options = ledger.options %}
55
{% set invert = ledger.fava_options.invert_income_liabilities_equity %}
6+
{% set incomesAverage = incomes_expenses_averages[0]%}
7+
{% set expensesAverage = incomes_expenses_averages[1]%}
68

79
<svelte-component type="charts"><script type="application/json">{{
810
[
@@ -14,6 +16,35 @@
1416
]|tojson
1517
}}</script></svelte-component>
1618

19+
<div class="row">
20+
<div class="column">
21+
<div class="inc_exp_avg_header">Incomes Average</div>
22+
{% for inc_average in incomes_expenses_averages[0] %}
23+
<div class="inc_exp_avg_row">
24+
<div class="inc_exp_avg_column">
25+
{{inc_average}}
26+
</div>
27+
<div class="inc_exp_avg_column">
28+
{{incomesAverage[inc_average] | format_currency}}
29+
</div>
30+
</div>
31+
{% endfor %}
32+
</div>
33+
<div class="column">
34+
<div class="inc_exp_avg_header">Expenses Average</div>
35+
{% for exp_average in incomes_expenses_averages[1] %}
36+
<div class="inc_exp_avg_row">
37+
<div class="inc_exp_avg_column">
38+
{{exp_average}}
39+
</div>
40+
<div class="inc_exp_avg_column">
41+
{{expensesAverage[exp_average] | format_currency}}
42+
</div>
43+
</div>
44+
{% endfor %}
45+
</div>
46+
</div>
47+
1748
<div class="row">
1849
<div class="column">
1950
{{ tree_table.tree(root_tree.get(options['name_income']), invert=invert) }}

tests/test_application.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
from beancount import __version__ as beancount_version
99

1010
from fava import __version__ as fava_version
11+
from fava.application import _calculate_chart_average
1112
from fava.application import create_app
1213
from fava.application import SERVER_SIDE_REPORTS
1314
from fava.application import static_url
1415
from fava.context import g
16+
from fava.template_filters import format_currency
1517

1618
if TYPE_CHECKING: # pragma: no cover
1719
from pathlib import Path
@@ -264,3 +266,19 @@ def test_load_extension_reports(test_client: FlaskClient) -> None:
264266
url = "/extension-report/extension/MissingExtension/"
265267
result = test_client.get(url)
266268
assert result.status_code == 404
269+
270+
271+
def test_calculate_average_income_expenses(app: Flask) -> None:
272+
with app.test_request_context("/long-example/?interval=year"):
273+
app.preprocess_request()
274+
averages = _calculate_chart_average()
275+
income_averages = averages[0]
276+
expenses_averages = averages[1]
277+
278+
assert format_currency(income_averages["IRAUSD"]) == "-3147.06"
279+
assert format_currency(income_averages["USD"]) == "-18309.54"
280+
assert format_currency(income_averages["VACHR"]) == "-18.24"
281+
282+
assert format_currency(expenses_averages["IRAUSD"]) == "2723.53"
283+
assert format_currency(expenses_averages["USD"]) == "13142.98"
284+
assert format_currency(expenses_averages["VACHR"]) == "23.06"

0 commit comments

Comments
 (0)