rustfava 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rustfava/__init__.py +30 -0
- rustfava/_ctx_globals_class.py +55 -0
- rustfava/api_models.py +36 -0
- rustfava/application.py +534 -0
- rustfava/beans/__init__.py +6 -0
- rustfava/beans/abc.py +327 -0
- rustfava/beans/account.py +79 -0
- rustfava/beans/create.py +377 -0
- rustfava/beans/flags.py +20 -0
- rustfava/beans/funcs.py +38 -0
- rustfava/beans/helpers.py +52 -0
- rustfava/beans/ingest.py +75 -0
- rustfava/beans/load.py +31 -0
- rustfava/beans/prices.py +151 -0
- rustfava/beans/protocols.py +82 -0
- rustfava/beans/str.py +454 -0
- rustfava/beans/types.py +63 -0
- rustfava/cli.py +187 -0
- rustfava/context.py +13 -0
- rustfava/core/__init__.py +729 -0
- rustfava/core/accounts.py +161 -0
- rustfava/core/attributes.py +145 -0
- rustfava/core/budgets.py +207 -0
- rustfava/core/charts.py +301 -0
- rustfava/core/commodities.py +37 -0
- rustfava/core/conversion.py +229 -0
- rustfava/core/documents.py +87 -0
- rustfava/core/extensions.py +132 -0
- rustfava/core/fava_options.py +255 -0
- rustfava/core/file.py +542 -0
- rustfava/core/filters.py +484 -0
- rustfava/core/group_entries.py +97 -0
- rustfava/core/ingest.py +509 -0
- rustfava/core/inventory.py +167 -0
- rustfava/core/misc.py +105 -0
- rustfava/core/module_base.py +18 -0
- rustfava/core/number.py +106 -0
- rustfava/core/query.py +180 -0
- rustfava/core/query_shell.py +301 -0
- rustfava/core/tree.py +265 -0
- rustfava/core/watcher.py +219 -0
- rustfava/ext/__init__.py +232 -0
- rustfava/ext/auto_commit.py +61 -0
- rustfava/ext/portfolio_list/PortfolioList.js +34 -0
- rustfava/ext/portfolio_list/__init__.py +29 -0
- rustfava/ext/portfolio_list/templates/PortfolioList.html +15 -0
- rustfava/ext/rustfava_ext_test/RustfavaExtTest.js +42 -0
- rustfava/ext/rustfava_ext_test/__init__.py +207 -0
- rustfava/ext/rustfava_ext_test/templates/RustfavaExtTest.html +45 -0
- rustfava/ext/rustfava_ext_test/templates/RustfavaExtTestInclude.html +1 -0
- rustfava/help/__init__.py +15 -0
- rustfava/help/_index.md +29 -0
- rustfava/help/beancount_syntax.md +156 -0
- rustfava/help/budgets.md +31 -0
- rustfava/help/conversion.md +29 -0
- rustfava/help/extensions.md +111 -0
- rustfava/help/features.md +179 -0
- rustfava/help/filters.md +103 -0
- rustfava/help/import.md +27 -0
- rustfava/help/options.md +289 -0
- rustfava/helpers.py +30 -0
- rustfava/internal_api.py +221 -0
- rustfava/json_api.py +952 -0
- rustfava/plugins/__init__.py +3 -0
- rustfava/plugins/link_documents.py +107 -0
- rustfava/plugins/tag_discovered_documents.py +44 -0
- rustfava/py.typed +0 -0
- rustfava/rustledger/__init__.py +31 -0
- rustfava/rustledger/constants.py +76 -0
- rustfava/rustledger/engine.py +485 -0
- rustfava/rustledger/loader.py +273 -0
- rustfava/rustledger/options.py +202 -0
- rustfava/rustledger/query.py +331 -0
- rustfava/rustledger/types.py +830 -0
- rustfava/serialisation.py +220 -0
- rustfava/static/app.css +2988 -0
- rustfava/static/app.css.map +7 -0
- rustfava/static/app.js +12854 -0
- rustfava/static/app.js.map +7 -0
- rustfava/static/beancount-JFV44ZVZ.css +5 -0
- rustfava/static/beancount-JFV44ZVZ.css.map +7 -0
- rustfava/static/beancount-VTTKRGSK.js +4642 -0
- rustfava/static/beancount-VTTKRGSK.js.map +7 -0
- rustfava/static/bql-MGFRUMBP.js +333 -0
- rustfava/static/bql-MGFRUMBP.js.map +7 -0
- rustfava/static/chunk-E7ZF4ASL.js +23061 -0
- rustfava/static/chunk-E7ZF4ASL.js.map +7 -0
- rustfava/static/chunk-V24TLQHT.js +12673 -0
- rustfava/static/chunk-V24TLQHT.js.map +7 -0
- rustfava/static/favicon.ico +0 -0
- rustfava/static/fira-mono-cyrillic-400-normal-BLAGXRCE.woff2 +0 -0
- rustfava/static/fira-mono-cyrillic-500-normal-EN7JUAAW.woff2 +0 -0
- rustfava/static/fira-mono-cyrillic-ext-400-normal-EX7VARTS.woff2 +0 -0
- rustfava/static/fira-mono-cyrillic-ext-500-normal-ZDPTUPRR.woff2 +0 -0
- rustfava/static/fira-mono-greek-400-normal-COGHKMOA.woff2 +0 -0
- rustfava/static/fira-mono-greek-500-normal-4EN2PKZT.woff2 +0 -0
- rustfava/static/fira-mono-greek-ext-400-normal-DYEQIJH7.woff2 +0 -0
- rustfava/static/fira-mono-greek-ext-500-normal-SG73CVKQ.woff2 +0 -0
- rustfava/static/fira-mono-latin-400-normal-NA3VLV7E.woff2 +0 -0
- rustfava/static/fira-mono-latin-500-normal-YC77GFWD.woff2 +0 -0
- rustfava/static/fira-mono-latin-ext-400-normal-DIKTZ5PW.woff2 +0 -0
- rustfava/static/fira-mono-latin-ext-500-normal-ZWY4UO4V.woff2 +0 -0
- rustfava/static/fira-mono-symbols2-400-normal-UITXT77Q.woff2 +0 -0
- rustfava/static/fira-mono-symbols2-500-normal-VWPC2EFN.woff2 +0 -0
- rustfava/static/fira-sans-cyrillic-400-normal-KLQMBCA6.woff2 +0 -0
- rustfava/static/fira-sans-cyrillic-500-normal-NFG7UD6J.woff2 +0 -0
- rustfava/static/fira-sans-cyrillic-ext-400-normal-GWO44OPC.woff2 +0 -0
- rustfava/static/fira-sans-cyrillic-ext-500-normal-SP47E5SC.woff2 +0 -0
- rustfava/static/fira-sans-greek-400-normal-UMQBTLC3.woff2 +0 -0
- rustfava/static/fira-sans-greek-500-normal-4ZKHN4FQ.woff2 +0 -0
- rustfava/static/fira-sans-greek-ext-400-normal-O2DVJAJZ.woff2 +0 -0
- rustfava/static/fira-sans-greek-ext-500-normal-SK6GNWGO.woff2 +0 -0
- rustfava/static/fira-sans-latin-400-normal-OYYTPMAV.woff2 +0 -0
- rustfava/static/fira-sans-latin-500-normal-SMQPZW5A.woff2 +0 -0
- rustfava/static/fira-sans-latin-ext-400-normal-OAUP3WK5.woff2 +0 -0
- rustfava/static/fira-sans-latin-ext-500-normal-LY3YDR5Y.woff2 +0 -0
- rustfava/static/fira-sans-vietnamese-400-normal-OBMQ72MR.woff2 +0 -0
- rustfava/static/fira-sans-vietnamese-500-normal-Y4NZR5EU.woff2 +0 -0
- rustfava/static/source-code-pro-cyrillic-400-normal-TO22V6M3.woff2 +0 -0
- rustfava/static/source-code-pro-cyrillic-500-normal-OGBWWWYW.woff2 +0 -0
- rustfava/static/source-code-pro-cyrillic-ext-400-normal-XH44UCIA.woff2 +0 -0
- rustfava/static/source-code-pro-cyrillic-ext-500-normal-3Z6MMVM6.woff2 +0 -0
- rustfava/static/source-code-pro-greek-400-normal-OUXXUQWK.woff2 +0 -0
- rustfava/static/source-code-pro-greek-500-normal-JA2Z5UXO.woff2 +0 -0
- rustfava/static/source-code-pro-greek-ext-400-normal-WCDKMX7U.woff2 +0 -0
- rustfava/static/source-code-pro-greek-ext-500-normal-ZHVI4VKW.woff2 +0 -0
- rustfava/static/source-code-pro-latin-400-normal-QOGTXED5.woff2 +0 -0
- rustfava/static/source-code-pro-latin-500-normal-X57QEOLQ.woff2 +0 -0
- rustfava/static/source-code-pro-latin-ext-400-normal-QXC74NBF.woff2 +0 -0
- rustfava/static/source-code-pro-latin-ext-500-normal-QGOY7MTT.woff2 +0 -0
- rustfava/static/source-code-pro-vietnamese-400-normal-NPDCDTBA.woff2 +0 -0
- rustfava/static/source-code-pro-vietnamese-500-normal-M6PJKTR5.woff2 +0 -0
- rustfava/static/tree-sitter-beancount-MLXFQBZ5.wasm +0 -0
- rustfava/static/web-tree-sitter-RNOQ6E74.wasm +0 -0
- rustfava/template_filters.py +64 -0
- rustfava/templates/_journal_table.html +156 -0
- rustfava/templates/_layout.html +26 -0
- rustfava/templates/_query_table.html +88 -0
- rustfava/templates/beancount_file +18 -0
- rustfava/templates/help.html +23 -0
- rustfava/templates/macros/_account_macros.html +5 -0
- rustfava/templates/macros/_commodity_macros.html +13 -0
- rustfava/translations/bg/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/bg/LC_MESSAGES/messages.po +618 -0
- rustfava/translations/ca/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/ca/LC_MESSAGES/messages.po +618 -0
- rustfava/translations/de/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/de/LC_MESSAGES/messages.po +618 -0
- rustfava/translations/es/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/es/LC_MESSAGES/messages.po +619 -0
- rustfava/translations/fa/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/fa/LC_MESSAGES/messages.po +618 -0
- rustfava/translations/fr/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/fr/LC_MESSAGES/messages.po +618 -0
- rustfava/translations/ja/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/ja/LC_MESSAGES/messages.po +618 -0
- rustfava/translations/nl/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/nl/LC_MESSAGES/messages.po +617 -0
- rustfava/translations/pt/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/pt/LC_MESSAGES/messages.po +617 -0
- rustfava/translations/pt_BR/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/pt_BR/LC_MESSAGES/messages.po +618 -0
- rustfava/translations/ru/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/ru/LC_MESSAGES/messages.po +617 -0
- rustfava/translations/sk/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/sk/LC_MESSAGES/messages.po +623 -0
- rustfava/translations/sv/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/sv/LC_MESSAGES/messages.po +618 -0
- rustfava/translations/uk/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/uk/LC_MESSAGES/messages.po +618 -0
- rustfava/translations/zh/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/zh/LC_MESSAGES/messages.po +618 -0
- rustfava/translations/zh_Hant_TW/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/zh_Hant_TW/LC_MESSAGES/messages.po +618 -0
- rustfava/util/__init__.py +157 -0
- rustfava/util/date.py +576 -0
- rustfava/util/excel.py +118 -0
- rustfava/util/ranking.py +79 -0
- rustfava/util/sets.py +18 -0
- rustfava/util/unreachable.py +20 -0
- rustfava-0.1.0.dist-info/METADATA +102 -0
- rustfava-0.1.0.dist-info/RECORD +187 -0
- rustfava-0.1.0.dist-info/WHEEL +5 -0
- rustfava-0.1.0.dist-info/entry_points.txt +2 -0
- rustfava-0.1.0.dist-info/licenses/AUTHORS +11 -0
- rustfava-0.1.0.dist-info/licenses/LICENSE +21 -0
- rustfava-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Auto-commit hook for rustfava.
|
|
2
|
+
|
|
3
|
+
This mainly serves as an example how rustfava's extension systems, which only
|
|
4
|
+
really does hooks at the moment, works.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
from typing import override
|
|
13
|
+
|
|
14
|
+
from rustfava.ext import RustfavaExtensionBase
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
17
|
+
from rustfava.beans.abc import Directive
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AutoCommit(RustfavaExtensionBase): # pragma: no cover
|
|
21
|
+
"""Auto-commit hook for rustfava."""
|
|
22
|
+
|
|
23
|
+
def _run(self, args: list[str]) -> None:
|
|
24
|
+
cwd = Path(self.ledger.beancount_file_path).parent
|
|
25
|
+
subprocess.run(args, cwd=cwd, stdout=subprocess.DEVNULL, check=False)
|
|
26
|
+
|
|
27
|
+
@override
|
|
28
|
+
def after_write_source(self, path: str, source: str) -> None:
|
|
29
|
+
"""Add changed file to git and commit."""
|
|
30
|
+
message = "autocommit: file saved"
|
|
31
|
+
self._run(["git", "add", path])
|
|
32
|
+
self._run(["git", "commit", "-m", message])
|
|
33
|
+
|
|
34
|
+
@override
|
|
35
|
+
def after_insert_metadata(
|
|
36
|
+
self,
|
|
37
|
+
entry: Directive,
|
|
38
|
+
key: str,
|
|
39
|
+
value: str,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Commit all changes on `after_insert_metadata`."""
|
|
42
|
+
message = "autocommit: metadata added"
|
|
43
|
+
self._run(["git", "commit", "-am", message])
|
|
44
|
+
|
|
45
|
+
@override
|
|
46
|
+
def after_insert_entry(self, entry: Directive) -> None:
|
|
47
|
+
"""Commit all changes on `after_insert_entry`."""
|
|
48
|
+
message = f"autocommit: entry on {entry.date}"
|
|
49
|
+
self._run(["git", "commit", "-am", message])
|
|
50
|
+
|
|
51
|
+
@override
|
|
52
|
+
def after_delete_entry(self, entry: Directive) -> None:
|
|
53
|
+
"""Commit all changes on `after_delete_entry`."""
|
|
54
|
+
message = f"autocommit: deleted entry on {entry.date}"
|
|
55
|
+
self._run(["git", "commit", "-am", message])
|
|
56
|
+
|
|
57
|
+
@override
|
|
58
|
+
def after_entry_modified(self, entry: Directive, new_lines: str) -> None:
|
|
59
|
+
"""Commit all changes on `after_entry_modified`."""
|
|
60
|
+
message = f"autocommit: modified entry on {entry.date}"
|
|
61
|
+
self._run(["git", "commit", "-am", message])
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @type import("../../../../frontend/src/extensions").ExtensionModule */
|
|
4
|
+
export default {
|
|
5
|
+
init() {
|
|
6
|
+
console.log("initialising extension");
|
|
7
|
+
},
|
|
8
|
+
onPageLoad() {
|
|
9
|
+
console.log("a Fava report page has loaded", window.location.pathname);
|
|
10
|
+
},
|
|
11
|
+
onExtensionPageLoad() {
|
|
12
|
+
console.log(
|
|
13
|
+
"the page for the PortfolioList extension has loaded",
|
|
14
|
+
window.location.pathname,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const updateFilter = document.getElementById("portfolio-update-filter");
|
|
18
|
+
updateFilter?.addEventListener("click", () => {
|
|
19
|
+
const filterInput = document.getElementById("portfolio-list-filter");
|
|
20
|
+
if (filterInput instanceof HTMLInputElement && filterInput.value.length) {
|
|
21
|
+
const search = new URLSearchParams(window.location.search);
|
|
22
|
+
search.set("account_filter", filterInput.value);
|
|
23
|
+
window.location.search = search.toString();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const clearFilter = document.getElementById("portfolio-clear-filter");
|
|
28
|
+
clearFilter?.addEventListener("click", () => {
|
|
29
|
+
const search = new URLSearchParams(window.location.search);
|
|
30
|
+
search.delete("account_filter");
|
|
31
|
+
window.location.search = search.toString();
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Portfolio list extension for rustfava.
|
|
2
|
+
|
|
3
|
+
This is a simple example of rustfava's extension reports system.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from rustfava.ext import RustfavaExtensionBase
|
|
11
|
+
from rustfava.ext.rustfava_ext_test import portfolio_accounts
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
14
|
+
from rustfava.ext.rustfava_ext_test import Portfolio
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PortfolioList(RustfavaExtensionBase): # pragma: no cover
|
|
18
|
+
"""Sample Extension Report that just prints out an Portfolio List."""
|
|
19
|
+
|
|
20
|
+
report_title = "Portfolio List"
|
|
21
|
+
|
|
22
|
+
has_js_module = True
|
|
23
|
+
|
|
24
|
+
def portfolio_accounts(
|
|
25
|
+
self,
|
|
26
|
+
filter_str: str | None = None,
|
|
27
|
+
) -> list[Portfolio]:
|
|
28
|
+
"""Get an account tree based on matching regex patterns."""
|
|
29
|
+
return portfolio_accounts(self.config, filter_str)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{% set arg_filter = request.args.get('account_filter') %}
|
|
2
|
+
|
|
3
|
+
<h2>Portfolio List Sample Report Extension</h2>
|
|
4
|
+
|
|
5
|
+
<p>
|
|
6
|
+
<label for="portfolio-list-filter">Custom Account Filter:</label>
|
|
7
|
+
<input id="portfolio-list-filter" value={{arg_filter or ""}}>
|
|
8
|
+
<button id="portfolio-update-filter">Update Filter</button>
|
|
9
|
+
<button id="portfolio-clear-filter">Clear Filter</button>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
{% for portfolio in extension.portfolio_accounts(arg_filter) %}
|
|
13
|
+
<h3>{{ portfolio.title }}</h3>
|
|
14
|
+
<svelte-component type="query-table"><script type="application/json">{{portfolio.table|tojson}}</script></svelte-component>
|
|
15
|
+
{% endfor %}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @type import("../../../../frontend/src/extensions").ExtensionModule */
|
|
4
|
+
export default {
|
|
5
|
+
init() {
|
|
6
|
+
console.log("initialising extension");
|
|
7
|
+
},
|
|
8
|
+
onPageLoad() {
|
|
9
|
+
console.log("a Fava report page has loaded", window.location.pathname);
|
|
10
|
+
},
|
|
11
|
+
onExtensionPageLoad(ctx) {
|
|
12
|
+
console.log(
|
|
13
|
+
"the page for the PortfolioList extension has loaded",
|
|
14
|
+
window.location.pathname,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const fetchedDataElement = document.getElementById("fetched-data");
|
|
18
|
+
if (fetchedDataElement) {
|
|
19
|
+
ctx.api.get("example_data", {}).then((d) => {
|
|
20
|
+
console.log("fetched data:", d);
|
|
21
|
+
fetchedDataElement.innerText = `fetched data: ${JSON.stringify(d)}`;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const updateFilter = document.getElementById("portfolio-update-filter");
|
|
26
|
+
updateFilter?.addEventListener("click", () => {
|
|
27
|
+
const filterInput = document.getElementById("portfolio-list-filter");
|
|
28
|
+
if (filterInput instanceof HTMLInputElement && filterInput.value.length) {
|
|
29
|
+
const search = new URLSearchParams(window.location.search);
|
|
30
|
+
search.set("account_filter", filterInput.value);
|
|
31
|
+
window.location.search = search.toString();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const clearFilter = document.getElementById("portfolio-clear-filter");
|
|
36
|
+
clearFilter?.addEventListener("click", () => {
|
|
37
|
+
const search = new URLSearchParams(window.location.search);
|
|
38
|
+
search.delete("account_filter");
|
|
39
|
+
window.location.search = search.toString();
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Rustfava extension to test extension functionality.
|
|
2
|
+
|
|
3
|
+
# This can be used mainly for testing of the extension functionality
|
|
4
|
+
and usage of e.g. extension Javascript code or custom elements.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import date
|
|
12
|
+
from decimal import Decimal
|
|
13
|
+
from typing import Any
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from flask import jsonify
|
|
17
|
+
|
|
18
|
+
from rustfava.context import g
|
|
19
|
+
from rustfava.core.charts import DateAndBalance
|
|
20
|
+
from rustfava.core.inventory import SimpleCounterInventory
|
|
21
|
+
from rustfava.core.query import DecimalColumn
|
|
22
|
+
from rustfava.core.query import QueryResultTable
|
|
23
|
+
from rustfava.core.query import StrColumn
|
|
24
|
+
from rustfava.ext import extension_endpoint
|
|
25
|
+
from rustfava.ext import RustfavaExtensionBase
|
|
26
|
+
from rustfava.helpers import RustfavaAPIError
|
|
27
|
+
from rustfava.internal_api import BalancesChart
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
30
|
+
from flask.wrappers import Response
|
|
31
|
+
|
|
32
|
+
from rustfava.core.tree import SerialisedTreeNode
|
|
33
|
+
from rustfava.core.tree import Tree
|
|
34
|
+
from rustfava.core.tree import TreeNode
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class Portfolio:
|
|
39
|
+
"""A portfolio.
|
|
40
|
+
|
|
41
|
+
Consists of a title and the result table to render.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
title: str
|
|
45
|
+
table: QueryResultTable
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _portfolio_data(nodes: list[TreeNode]) -> QueryResultTable:
|
|
49
|
+
"""Turn a portfolio of tree nodes into querytable-style data.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
nodes: Account tree nodes.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
A QueryResultTable for the portfolio.
|
|
56
|
+
"""
|
|
57
|
+
currency = g.ledger.options["operating_currency"][0]
|
|
58
|
+
account_balances: list[tuple[str, Decimal | None]] = []
|
|
59
|
+
total = Decimal()
|
|
60
|
+
for node in nodes:
|
|
61
|
+
balance = g.conv.apply(node.balance, g.ledger.prices)
|
|
62
|
+
if currency in balance:
|
|
63
|
+
balance_dec = balance[currency]
|
|
64
|
+
total += balance_dec
|
|
65
|
+
account_balances.append((node.name, balance_dec))
|
|
66
|
+
else:
|
|
67
|
+
account_balances.append((node.name, None))
|
|
68
|
+
|
|
69
|
+
return QueryResultTable(
|
|
70
|
+
[
|
|
71
|
+
StrColumn("account"),
|
|
72
|
+
DecimalColumn("balance"),
|
|
73
|
+
DecimalColumn("allocation"),
|
|
74
|
+
],
|
|
75
|
+
[
|
|
76
|
+
(
|
|
77
|
+
account,
|
|
78
|
+
balance,
|
|
79
|
+
(round((balance / total) * 100, 2) if balance else None),
|
|
80
|
+
)
|
|
81
|
+
for account, balance in account_balances
|
|
82
|
+
],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def account_name_pattern_portfolio(tree: Tree, pattern: str) -> Portfolio:
|
|
87
|
+
"""Return portfolio info based on matching account name.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
tree: Ledger root tree node.
|
|
91
|
+
pattern: Account name regex pattern.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
A `Portfolio` for the accounts matching the pattern.
|
|
95
|
+
"""
|
|
96
|
+
regexer = re.compile(pattern)
|
|
97
|
+
selected_nodes = [
|
|
98
|
+
node for account, node in tree.items() if regexer.match(account)
|
|
99
|
+
]
|
|
100
|
+
return Portfolio(
|
|
101
|
+
f"Account names matching: '{pattern}'",
|
|
102
|
+
_portfolio_data(selected_nodes),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def account_metadata_pattern_portfolio(
|
|
107
|
+
tree: Tree,
|
|
108
|
+
metadata_key: str,
|
|
109
|
+
pattern: str,
|
|
110
|
+
) -> Portfolio:
|
|
111
|
+
"""Return portfolio info based on matching account open metadata.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
tree: Ledger root tree node.
|
|
115
|
+
metadata_key: Metadata key to match for in account open.
|
|
116
|
+
pattern: Metadata value's regex pattern to match for.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
A `Portfolio` for the accounts with matching open metadata.
|
|
120
|
+
"""
|
|
121
|
+
regexer = re.compile(pattern)
|
|
122
|
+
selected_nodes = [
|
|
123
|
+
tree[entry.account]
|
|
124
|
+
for entry in g.ledger.all_entries_by_type.Open
|
|
125
|
+
if metadata_key in entry.meta
|
|
126
|
+
and regexer.match(str(entry.meta[metadata_key]))
|
|
127
|
+
]
|
|
128
|
+
return Portfolio(
|
|
129
|
+
f"Accounts with '{metadata_key}' metadata matching: '{pattern}'",
|
|
130
|
+
_portfolio_data(selected_nodes),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def portfolio_accounts(
|
|
135
|
+
config: Any,
|
|
136
|
+
filter_str: str | None = None,
|
|
137
|
+
) -> list[Portfolio]:
|
|
138
|
+
"""Get an account tree based on matching regex patterns."""
|
|
139
|
+
tree = g.filtered.root_tree
|
|
140
|
+
|
|
141
|
+
if filter_str: # pragma: no cover
|
|
142
|
+
return [account_name_pattern_portfolio(tree, filter_str)]
|
|
143
|
+
|
|
144
|
+
portfolios = []
|
|
145
|
+
for key, value in config:
|
|
146
|
+
if key == "account_name_pattern":
|
|
147
|
+
portfolios.append(account_name_pattern_portfolio(tree, value))
|
|
148
|
+
elif key == "account_open_metadata_pattern":
|
|
149
|
+
metadata_key, metadata_pattern = value
|
|
150
|
+
portfolios.append(
|
|
151
|
+
account_metadata_pattern_portfolio(
|
|
152
|
+
tree, metadata_key, metadata_pattern
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
else: # pragma: no cover
|
|
156
|
+
msg = "Portfolio List: Invalid option."
|
|
157
|
+
raise RustfavaAPIError(msg)
|
|
158
|
+
|
|
159
|
+
return portfolios
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class RustfavaExtTest(RustfavaExtensionBase):
|
|
163
|
+
"""Rustfava extension to test extension functionality."""
|
|
164
|
+
|
|
165
|
+
report_title = "Rustfava extension test"
|
|
166
|
+
|
|
167
|
+
has_js_module = True
|
|
168
|
+
|
|
169
|
+
def portfolio_accounts(
|
|
170
|
+
self,
|
|
171
|
+
filter_str: str | None = None,
|
|
172
|
+
) -> list[Portfolio]:
|
|
173
|
+
"""Get an account tree based on matching regex patterns."""
|
|
174
|
+
return portfolio_accounts(self.config, filter_str)
|
|
175
|
+
|
|
176
|
+
@extension_endpoint
|
|
177
|
+
def example_tree(self) -> SerialisedTreeNode:
|
|
178
|
+
"""Return a tree to render as a tree-table."""
|
|
179
|
+
assets = g.ledger.options["name_assets"]
|
|
180
|
+
return g.filtered.root_tree.get(assets).serialise_with_context()
|
|
181
|
+
|
|
182
|
+
@extension_endpoint
|
|
183
|
+
def example_data(self) -> Response:
|
|
184
|
+
"""Return some data with a GET endpoint."""
|
|
185
|
+
return jsonify(["some data"])
|
|
186
|
+
|
|
187
|
+
def chart_data(self) -> list[BalancesChart]:
|
|
188
|
+
"""Return some chart data."""
|
|
189
|
+
return [
|
|
190
|
+
BalancesChart(
|
|
191
|
+
"nonsense data",
|
|
192
|
+
[
|
|
193
|
+
DateAndBalance(
|
|
194
|
+
date(2023, 1, 1),
|
|
195
|
+
SimpleCounterInventory(EUR=Decimal(10)),
|
|
196
|
+
),
|
|
197
|
+
DateAndBalance(
|
|
198
|
+
date(2023, 2, 1),
|
|
199
|
+
SimpleCounterInventory(EUR=Decimal(15)),
|
|
200
|
+
),
|
|
201
|
+
DateAndBalance(
|
|
202
|
+
date(2023, 3, 1),
|
|
203
|
+
SimpleCounterInventory(EUR=Decimal(20)),
|
|
204
|
+
),
|
|
205
|
+
],
|
|
206
|
+
)
|
|
207
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{% set arg_filter = request.args.get('account_filter') %}
|
|
2
|
+
|
|
3
|
+
{% include 'RustfavaExtTestInclude.html' %}
|
|
4
|
+
|
|
5
|
+
<p>
|
|
6
|
+
<label for="portfolio-list-filter">Custom Account Filter:</label>
|
|
7
|
+
<input id="portfolio-list-filter" value={{arg_filter or ""}}>
|
|
8
|
+
<button id="portfolio-update-filter">Update Filter</button>
|
|
9
|
+
<button id="portfolio-clear-filter">Clear Filter</button>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<hr>
|
|
13
|
+
|
|
14
|
+
<h3>Test for asynchronously fetched data</h3>
|
|
15
|
+
<p id="fetched-data"></p>
|
|
16
|
+
|
|
17
|
+
<hr>
|
|
18
|
+
|
|
19
|
+
<h3>Rustfava charts custom elements tests</h3>
|
|
20
|
+
<h4>Expected error: missing type</h4>
|
|
21
|
+
<svelte-component></svelte-component>
|
|
22
|
+
<h4>Expected error: unknown type</h4>
|
|
23
|
+
<svelte-component type="unknown"></svelte-component>
|
|
24
|
+
<h4>Expected error: invalid data type</h4>
|
|
25
|
+
<svelte-component type="charts"></svelte-component>
|
|
26
|
+
<h4>This should render a chart</h4>
|
|
27
|
+
<svelte-component type="charts"><script type="application/json">{{extension.chart_data()|tojson}}</script></svelte-component>
|
|
28
|
+
|
|
29
|
+
<hr>
|
|
30
|
+
|
|
31
|
+
<h3>Tree-table test</h3>
|
|
32
|
+
{% set tree = extension.example_tree() %}
|
|
33
|
+
<svelte-component type="tree-table"><script type="application/json">{{tree|tojson}}</script></svelte-component>
|
|
34
|
+
<hr>
|
|
35
|
+
|
|
36
|
+
<h3>Portfolio (renders a query-table)</h3>
|
|
37
|
+
{% for portfolio in extension.portfolio_accounts(arg_filter) %}
|
|
38
|
+
<h4>{{ portfolio.title }}</h4>
|
|
39
|
+
<svelte-component type="query-table"><script type="application/json">{{portfolio.table|tojson}}</script></svelte-component>
|
|
40
|
+
{% endfor %}
|
|
41
|
+
<hr>
|
|
42
|
+
|
|
43
|
+
<h3>Query table (postings by account) from BQL query.</h3>
|
|
44
|
+
{% set postings_per_account = 'SELECT account, count(account) ORDER BY account' %}
|
|
45
|
+
<svelte-component type="query-table"><script type="application/json">{{ledger.query_shell.execute_query_serialised(g.filtered.entries_with_all_prices, postings_per_account)|tojson}}</script></svelte-component>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<h2>Fava Test Extension</h2>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""List of all available help pages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
HELP_PAGES = {
|
|
6
|
+
"_index": "Index",
|
|
7
|
+
"budgets": "Budgets",
|
|
8
|
+
"conversion": "Conversion",
|
|
9
|
+
"import": "Import",
|
|
10
|
+
"options": "Options",
|
|
11
|
+
"beancount_syntax": "Beancount Syntax",
|
|
12
|
+
"features": "Fava's features",
|
|
13
|
+
"filters": "Filtering entries",
|
|
14
|
+
"extensions": "Extensions",
|
|
15
|
+
}
|
rustfava/help/_index.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Welcome to the help pages for rustfava! You are running rustfava
|
|
2
|
+
`{{ rustfava_version }}` powered by rustledger. There are help pages for the
|
|
3
|
+
following topics:
|
|
4
|
+
|
|
5
|
+
- [Beancount Syntax](./beancount_syntax) - short overview of the syntax.
|
|
6
|
+
- [Budgets](./budgets) - how to use rustfava's budgeting feature.
|
|
7
|
+
- [rustfava's Features](./features) - the features in detail.
|
|
8
|
+
- [Filtering entries](./filters) - how to filter the entries.
|
|
9
|
+
- [Extensions](./extensions) - how rustfava can be extended.
|
|
10
|
+
- [Conversion](./conversion) - how to convert between currencies.
|
|
11
|
+
- [Import](./import) - the import system.
|
|
12
|
+
- [Options](./options) - the available options.
|
|
13
|
+
|
|
14
|
+
Rustfava comes with keyboard shortcuts - press <kbd>?</kbd> on any page to see the
|
|
15
|
+
available ones.
|
|
16
|
+
|
|
17
|
+
If you started rustfava from the command line, you can run `rustfava --help` to see all
|
|
18
|
+
the available command line options.
|
|
19
|
+
|
|
20
|
+
If you discover a bug in rustfava, or have some ideas for improvement, please open a
|
|
21
|
+
[bug report](https://github.com/rustledger/rustfava/issues).
|
|
22
|
+
|
|
23
|
+
### Related websites
|
|
24
|
+
|
|
25
|
+
- Rustfava on [GitHub](https://github.com/rustledger/rustfava),
|
|
26
|
+
- rustledger on [GitHub](https://github.com/rustledger/rustledger),
|
|
27
|
+
- Beancount file format [documentation](http://furius.ca/beancount/doc/index),
|
|
28
|
+
- An overview of other implementations of command-line accounting:
|
|
29
|
+
[Plain Text Accounting](http://plaintextaccounting.org).
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Beancount Syntax
|
|
2
|
+
|
|
3
|
+
Below is a short reference of the Beancount language syntax. Also see the full
|
|
4
|
+
[Syntax Documentation](http://furius.ca/beancount/doc/syntax) and the
|
|
5
|
+
[Syntax Cheat Sheet](http://furius.ca/beancount/doc/cheatsheet).
|
|
6
|
+
|
|
7
|
+
Beancount defines a language in which financial transactions are entered into a
|
|
8
|
+
text-file, which then can be processed by Beancount. There are a few building
|
|
9
|
+
blocks that are important to understand Beancount's syntax:
|
|
10
|
+
|
|
11
|
+
- Commodities,
|
|
12
|
+
- Accounts,
|
|
13
|
+
- Directives.
|
|
14
|
+
|
|
15
|
+
## Commodities
|
|
16
|
+
|
|
17
|
+
All in CAPS: `USD`, `EUR`, `CAD`, `GOOG`, `AAPL`, `RBF1005`, `HOME_MAYST`,
|
|
18
|
+
`AIRMILES`, `HOURS`.
|
|
19
|
+
|
|
20
|
+
## Accounts
|
|
21
|
+
|
|
22
|
+
Account are given by a colon-separated list of capitalized words. They must
|
|
23
|
+
begin with one of the five root accounts listed in the table below. The
|
|
24
|
+
separation by colons defines an implicit hierarchy, for example we say that
|
|
25
|
+
`Assets:Cash` is a sub-account of `Assets`.
|
|
26
|
+
|
|
27
|
+
| Name | Type | Contains | Examples |
|
|
28
|
+
| ------------- | ---- | ---------------------------- | ------------------------- |
|
|
29
|
+
| `Assets` | + | Cash, Checking-Account, etc. | `Assets:Checking` |
|
|
30
|
+
| `Liabilities` | - | Credit Card, etc. | `Liabilities:CreditCard` |
|
|
31
|
+
| `Income` | - | Salary, etc. | `Income:EmployerA` |
|
|
32
|
+
| `Expenses` | + | Expense categories | `Expenses:Fun:Cinema` |
|
|
33
|
+
| `Equity` | - | Almost always auto-generated | `Equity:Opening-Balances` |
|
|
34
|
+
|
|
35
|
+
The names of the five root accounts can be changed with the following options:
|
|
36
|
+
|
|
37
|
+
<pre><textarea is="beancount-textarea">
|
|
38
|
+
option "name_assets" "Vermoegen"
|
|
39
|
+
option "name_liabilities" "Verbindlichkeiten"
|
|
40
|
+
option "name_income" "Einkommen"
|
|
41
|
+
option "name_expenses" "Ausgaben"
|
|
42
|
+
option "name_equity" "Eigenkapital"</textarea></pre>
|
|
43
|
+
|
|
44
|
+
## Directives
|
|
45
|
+
|
|
46
|
+
The basic building block are **directives** (also called **entries**). Most
|
|
47
|
+
directives start with a date, then the type of the directive, and then
|
|
48
|
+
directive-specific arguments. The ordering of directives in the input-file does
|
|
49
|
+
not matter, because Beancount orders them based on the date of each directive.
|
|
50
|
+
|
|
51
|
+
General syntax: `YYYY-MM-DD <directive> <arguments...>`
|
|
52
|
+
|
|
53
|
+
### Open and Close accounts
|
|
54
|
+
|
|
55
|
+
To open or close an account use the `open` and `close` directives:
|
|
56
|
+
|
|
57
|
+
<pre><textarea is="beancount-textarea" is="beancount-textarea">
|
|
58
|
+
2015-05-29 open Expenses:Restaurant
|
|
59
|
+
; Account with some currency constraints:
|
|
60
|
+
2015-05-29 open Assets:Checking USD,EUR
|
|
61
|
+
; ...
|
|
62
|
+
2016-02-23 close Assets:Checking</textarea></pre>
|
|
63
|
+
|
|
64
|
+
### Commodities
|
|
65
|
+
|
|
66
|
+
Declaring commodities is optional. Use this if you want to attach metadata by
|
|
67
|
+
currency. If you specify a `name` for a currency like below, this name will be
|
|
68
|
+
displayed as a tooltip on hovering over currency names in rustfava. Likewise, with
|
|
69
|
+
the `precision` metadata, you can specify the number of decimal digits to show
|
|
70
|
+
in rustfava, overriding the precision that is otherwise automatically inferred from
|
|
71
|
+
the input data.
|
|
72
|
+
|
|
73
|
+
<pre><textarea is="beancount-textarea">
|
|
74
|
+
1998-07-22 commodity AAPL
|
|
75
|
+
name: "Apple Computer Inc."
|
|
76
|
+
precision: 3</textarea></pre>
|
|
77
|
+
|
|
78
|
+
### Prices
|
|
79
|
+
|
|
80
|
+
You can use this directive to fill the historical price database:
|
|
81
|
+
|
|
82
|
+
<pre><textarea is="beancount-textarea">
|
|
83
|
+
2015-04-30 price AAPL 125.15 USD
|
|
84
|
+
2015-05-30 price AAPL 130.28 USD</textarea></pre>
|
|
85
|
+
|
|
86
|
+
### Notes
|
|
87
|
+
|
|
88
|
+
<pre><textarea is="beancount-textarea">
|
|
89
|
+
2013-03-20 note Assets:Checking "Called to ask about rebate"</textarea></pre>
|
|
90
|
+
|
|
91
|
+
### Documents
|
|
92
|
+
|
|
93
|
+
<pre><textarea is="beancount-textarea">
|
|
94
|
+
2013-03-20 document Assets:Checking "path/to/statement.pdf"</textarea></pre>
|
|
95
|
+
|
|
96
|
+
### Transactions
|
|
97
|
+
|
|
98
|
+
<pre><textarea is="beancount-textarea">
|
|
99
|
+
2015-05-30 * "Some narration about this transaction"
|
|
100
|
+
Liabilities:CreditCard -101.23 USD
|
|
101
|
+
Expenses:Restaurant 101.23 USD
|
|
102
|
+
|
|
103
|
+
2015-05-30 ! "Cable Co" "Phone Bill" #tag ^link
|
|
104
|
+
id: "TW378743437"
|
|
105
|
+
Expenses:Home:Phone 87.45 USD
|
|
106
|
+
Assets:Checking ; You may leave one amount out</textarea></pre>
|
|
107
|
+
|
|
108
|
+
### Postings
|
|
109
|
+
|
|
110
|
+
<pre><textarea is="beancount-textarea">
|
|
111
|
+
2015-05-30 * "Example transaction with various postings"
|
|
112
|
+
Account:Name 123.45 USD ; simple units
|
|
113
|
+
Account:Name 10 GOOG {502.12 USD} ; with cost
|
|
114
|
+
Account:Name 1000.00 USD @ 1.10 CAD ; with price
|
|
115
|
+
Account:Name 10 GOOG {502.12 USD} @ 1.10 CAD ; with cost & price
|
|
116
|
+
Account:Name 10 GOOG {502.12 USD, 2014-05-12} ; with cost date
|
|
117
|
+
! Account:Name 123.45 USD ; with flag</textarea></pre>
|
|
118
|
+
|
|
119
|
+
### Balance Assertions and Padding
|
|
120
|
+
|
|
121
|
+
Asserts the amount for only the given currency:
|
|
122
|
+
|
|
123
|
+
<pre><textarea is="beancount-textarea">
|
|
124
|
+
2015-06-01 balance Liabilities:CreditCard -634.30 USD</textarea></pre>
|
|
125
|
+
|
|
126
|
+
Automatic insertion of transaction to fulfill the following assertion:
|
|
127
|
+
|
|
128
|
+
<pre><textarea is="beancount-textarea">
|
|
129
|
+
2015-06-01 pad Assets:Checking Equity:Opening-Balances</textarea></pre>
|
|
130
|
+
|
|
131
|
+
### Events
|
|
132
|
+
|
|
133
|
+
<pre><textarea is="beancount-textarea">
|
|
134
|
+
2015-06-01 event "location" "New York, USA"
|
|
135
|
+
2015-06-01 event "address" "123 May Street"</textarea></pre>
|
|
136
|
+
|
|
137
|
+
### Options
|
|
138
|
+
|
|
139
|
+
See the [Beancount Options Reference](http://furius.ca/beancount/doc/options)
|
|
140
|
+
for the full list of supported options.
|
|
141
|
+
|
|
142
|
+
<pre><textarea is="beancount-textarea">
|
|
143
|
+
option "title" "My Personal Ledger"</textarea></pre>
|
|
144
|
+
|
|
145
|
+
### Other
|
|
146
|
+
|
|
147
|
+
<pre><textarea is="beancount-textarea">
|
|
148
|
+
pushtag #trip-to-peru
|
|
149
|
+
; ... the given tag will be added to all entries in between the pushtag and poptag
|
|
150
|
+
poptag #trip-to-peru</textarea></pre>
|
|
151
|
+
|
|
152
|
+
### Comments
|
|
153
|
+
|
|
154
|
+
<pre><textarea is="beancount-textarea">
|
|
155
|
+
; inline comments begin with a semi-colon
|
|
156
|
+
* any line not starting with a valid directive is also ignored silently</textarea></pre>
|
rustfava/help/budgets.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Budgets
|
|
2
|
+
|
|
3
|
+
Budgets on a per-account basis can be added via `custom` directives in the
|
|
4
|
+
Beancount file:
|
|
5
|
+
|
|
6
|
+
<pre><textarea is="beancount-textarea">
|
|
7
|
+
2012-01-01 custom "budget" Expenses:Coffee "daily" 4.00 EUR
|
|
8
|
+
2013-01-01 custom "budget" Expenses:Books "weekly" 20.00 EUR
|
|
9
|
+
2014-02-10 custom "budget" Expenses:Groceries "monthly" 40.00 EUR
|
|
10
|
+
2015-05-01 custom "budget" Expenses:Electricity "quarterly" 85.00 EUR
|
|
11
|
+
2016-06-01 custom "budget" Expenses:Holiday "yearly" 2500.00 EUR</textarea></pre>
|
|
12
|
+
|
|
13
|
+
If budgets are specified, rustfava's reports and charts will display remaining
|
|
14
|
+
budgets and related information.
|
|
15
|
+
|
|
16
|
+
The budget directives can be specified `daily`, `weekly`, `monthly`, `quarterly`
|
|
17
|
+
and `yearly`. The specified budget is valid until another budget directive for
|
|
18
|
+
the account is specified. The budget is broken down to a daily budget, and
|
|
19
|
+
summed up for a range of dates as needed.
|
|
20
|
+
|
|
21
|
+
This makes the budgets very flexible, allowing for a monthly budget, being taken
|
|
22
|
+
over by a weekly budget, and so on.
|
|
23
|
+
|
|
24
|
+
Rustfava displays budgets in both charts and reports. You can find a visualization
|
|
25
|
+
of the global budget in the `Net Profit` and `Expenses` charts for the Income
|
|
26
|
+
Statement report.
|
|
27
|
+
|
|
28
|
+
The Income Statement report is a good starting point for getting access to the
|
|
29
|
+
full budget information in rustfava. The `Changes` charts visualize the data. The
|
|
30
|
+
`Changes (monthly)` and `Balances (monthly)` reports show, respectively, the
|
|
31
|
+
monthly and cumulative (over the selected period) budgets.
|