From 94533e5938efbcab09d04b8839209fc3982e559d Mon Sep 17 00:00:00 2001 From: Vahid <24546202+vahidmah@users.noreply.github.com> Date: Fri, 26 May 2023 22:17:14 -0400 Subject: [PATCH] Add calculation and disply for average of incomes and expenses --- frontend/css/base.css | 16 +++++++ src/fava/application.py | 53 +++++++++++++++++++++++- src/fava/templates/income_statement.html | 31 ++++++++++++++ tests/test_application.py | 18 ++++++++ 4 files changed, 117 insertions(+), 1 deletion(-) diff --git a/frontend/css/base.css b/frontend/css/base.css index e244aa6c8..5b92369dd 100644 --- a/frontend/css/base.css +++ b/frontend/css/base.css @@ -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; +} diff --git a/src/fava/application.py b/src/fava/application.py index ff03fceac..ce8315f74 100644 --- a/src/fava/application.py +++ b/src/fava/application.py @@ -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 @@ -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 @@ -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() @@ -164,8 +170,53 @@ def _setup_template_config(fava_app: Flask) -> None: @fava_app.context_processor def _template_context() -> dict[str, FavaLedger | type[ChartApi]]: + incomes_expenses_averages = _calculate_chart_average() """Inject variables into the template context.""" - return {"ledger": g.ledger, "chart_api": ChartApi} + 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 = reduce( + sum_balances, + income_interval_totals.data, + defaultdict(lambda: Decimal(0)), + ) + expense_balances = 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: diff --git a/src/fava/templates/income_statement.html b/src/fava/templates/income_statement.html index 9ecb80431..bdcfaf18a 100644 --- a/src/fava/templates/income_statement.html +++ b/src/fava/templates/income_statement.html @@ -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]%} +
+
+
Incomes Average
+ {% for inc_average in incomes_expenses_averages[0] %} +
+
+ {{inc_average}} +
+
+ {{incomesAverage[inc_average] | format_currency}} +
+
+ {% endfor %} +
+
+
Expenses Average
+ {% for exp_average in incomes_expenses_averages[1] %} +
+
+ {{exp_average}} +
+
+ {{expensesAverage[exp_average] | format_currency}} +
+
+ {% endfor %} +
+
+
{{ tree_table.tree(root_tree.get(options['name_income']), invert=invert) }} diff --git a/tests/test_application.py b/tests/test_application.py index 9d2ad40aa..62fe716cf 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -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 @@ -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"