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.
Files changed (187) hide show
  1. rustfava/__init__.py +30 -0
  2. rustfava/_ctx_globals_class.py +55 -0
  3. rustfava/api_models.py +36 -0
  4. rustfava/application.py +534 -0
  5. rustfava/beans/__init__.py +6 -0
  6. rustfava/beans/abc.py +327 -0
  7. rustfava/beans/account.py +79 -0
  8. rustfava/beans/create.py +377 -0
  9. rustfava/beans/flags.py +20 -0
  10. rustfava/beans/funcs.py +38 -0
  11. rustfava/beans/helpers.py +52 -0
  12. rustfava/beans/ingest.py +75 -0
  13. rustfava/beans/load.py +31 -0
  14. rustfava/beans/prices.py +151 -0
  15. rustfava/beans/protocols.py +82 -0
  16. rustfava/beans/str.py +454 -0
  17. rustfava/beans/types.py +63 -0
  18. rustfava/cli.py +187 -0
  19. rustfava/context.py +13 -0
  20. rustfava/core/__init__.py +729 -0
  21. rustfava/core/accounts.py +161 -0
  22. rustfava/core/attributes.py +145 -0
  23. rustfava/core/budgets.py +207 -0
  24. rustfava/core/charts.py +301 -0
  25. rustfava/core/commodities.py +37 -0
  26. rustfava/core/conversion.py +229 -0
  27. rustfava/core/documents.py +87 -0
  28. rustfava/core/extensions.py +132 -0
  29. rustfava/core/fava_options.py +255 -0
  30. rustfava/core/file.py +542 -0
  31. rustfava/core/filters.py +484 -0
  32. rustfava/core/group_entries.py +97 -0
  33. rustfava/core/ingest.py +509 -0
  34. rustfava/core/inventory.py +167 -0
  35. rustfava/core/misc.py +105 -0
  36. rustfava/core/module_base.py +18 -0
  37. rustfava/core/number.py +106 -0
  38. rustfava/core/query.py +180 -0
  39. rustfava/core/query_shell.py +301 -0
  40. rustfava/core/tree.py +265 -0
  41. rustfava/core/watcher.py +219 -0
  42. rustfava/ext/__init__.py +232 -0
  43. rustfava/ext/auto_commit.py +61 -0
  44. rustfava/ext/portfolio_list/PortfolioList.js +34 -0
  45. rustfava/ext/portfolio_list/__init__.py +29 -0
  46. rustfava/ext/portfolio_list/templates/PortfolioList.html +15 -0
  47. rustfava/ext/rustfava_ext_test/RustfavaExtTest.js +42 -0
  48. rustfava/ext/rustfava_ext_test/__init__.py +207 -0
  49. rustfava/ext/rustfava_ext_test/templates/RustfavaExtTest.html +45 -0
  50. rustfava/ext/rustfava_ext_test/templates/RustfavaExtTestInclude.html +1 -0
  51. rustfava/help/__init__.py +15 -0
  52. rustfava/help/_index.md +29 -0
  53. rustfava/help/beancount_syntax.md +156 -0
  54. rustfava/help/budgets.md +31 -0
  55. rustfava/help/conversion.md +29 -0
  56. rustfava/help/extensions.md +111 -0
  57. rustfava/help/features.md +179 -0
  58. rustfava/help/filters.md +103 -0
  59. rustfava/help/import.md +27 -0
  60. rustfava/help/options.md +289 -0
  61. rustfava/helpers.py +30 -0
  62. rustfava/internal_api.py +221 -0
  63. rustfava/json_api.py +952 -0
  64. rustfava/plugins/__init__.py +3 -0
  65. rustfava/plugins/link_documents.py +107 -0
  66. rustfava/plugins/tag_discovered_documents.py +44 -0
  67. rustfava/py.typed +0 -0
  68. rustfava/rustledger/__init__.py +31 -0
  69. rustfava/rustledger/constants.py +76 -0
  70. rustfava/rustledger/engine.py +485 -0
  71. rustfava/rustledger/loader.py +273 -0
  72. rustfava/rustledger/options.py +202 -0
  73. rustfava/rustledger/query.py +331 -0
  74. rustfava/rustledger/types.py +830 -0
  75. rustfava/serialisation.py +220 -0
  76. rustfava/static/app.css +2988 -0
  77. rustfava/static/app.css.map +7 -0
  78. rustfava/static/app.js +12854 -0
  79. rustfava/static/app.js.map +7 -0
  80. rustfava/static/beancount-JFV44ZVZ.css +5 -0
  81. rustfava/static/beancount-JFV44ZVZ.css.map +7 -0
  82. rustfava/static/beancount-VTTKRGSK.js +4642 -0
  83. rustfava/static/beancount-VTTKRGSK.js.map +7 -0
  84. rustfava/static/bql-MGFRUMBP.js +333 -0
  85. rustfava/static/bql-MGFRUMBP.js.map +7 -0
  86. rustfava/static/chunk-E7ZF4ASL.js +23061 -0
  87. rustfava/static/chunk-E7ZF4ASL.js.map +7 -0
  88. rustfava/static/chunk-V24TLQHT.js +12673 -0
  89. rustfava/static/chunk-V24TLQHT.js.map +7 -0
  90. rustfava/static/favicon.ico +0 -0
  91. rustfava/static/fira-mono-cyrillic-400-normal-BLAGXRCE.woff2 +0 -0
  92. rustfava/static/fira-mono-cyrillic-500-normal-EN7JUAAW.woff2 +0 -0
  93. rustfava/static/fira-mono-cyrillic-ext-400-normal-EX7VARTS.woff2 +0 -0
  94. rustfava/static/fira-mono-cyrillic-ext-500-normal-ZDPTUPRR.woff2 +0 -0
  95. rustfava/static/fira-mono-greek-400-normal-COGHKMOA.woff2 +0 -0
  96. rustfava/static/fira-mono-greek-500-normal-4EN2PKZT.woff2 +0 -0
  97. rustfava/static/fira-mono-greek-ext-400-normal-DYEQIJH7.woff2 +0 -0
  98. rustfava/static/fira-mono-greek-ext-500-normal-SG73CVKQ.woff2 +0 -0
  99. rustfava/static/fira-mono-latin-400-normal-NA3VLV7E.woff2 +0 -0
  100. rustfava/static/fira-mono-latin-500-normal-YC77GFWD.woff2 +0 -0
  101. rustfava/static/fira-mono-latin-ext-400-normal-DIKTZ5PW.woff2 +0 -0
  102. rustfava/static/fira-mono-latin-ext-500-normal-ZWY4UO4V.woff2 +0 -0
  103. rustfava/static/fira-mono-symbols2-400-normal-UITXT77Q.woff2 +0 -0
  104. rustfava/static/fira-mono-symbols2-500-normal-VWPC2EFN.woff2 +0 -0
  105. rustfava/static/fira-sans-cyrillic-400-normal-KLQMBCA6.woff2 +0 -0
  106. rustfava/static/fira-sans-cyrillic-500-normal-NFG7UD6J.woff2 +0 -0
  107. rustfava/static/fira-sans-cyrillic-ext-400-normal-GWO44OPC.woff2 +0 -0
  108. rustfava/static/fira-sans-cyrillic-ext-500-normal-SP47E5SC.woff2 +0 -0
  109. rustfava/static/fira-sans-greek-400-normal-UMQBTLC3.woff2 +0 -0
  110. rustfava/static/fira-sans-greek-500-normal-4ZKHN4FQ.woff2 +0 -0
  111. rustfava/static/fira-sans-greek-ext-400-normal-O2DVJAJZ.woff2 +0 -0
  112. rustfava/static/fira-sans-greek-ext-500-normal-SK6GNWGO.woff2 +0 -0
  113. rustfava/static/fira-sans-latin-400-normal-OYYTPMAV.woff2 +0 -0
  114. rustfava/static/fira-sans-latin-500-normal-SMQPZW5A.woff2 +0 -0
  115. rustfava/static/fira-sans-latin-ext-400-normal-OAUP3WK5.woff2 +0 -0
  116. rustfava/static/fira-sans-latin-ext-500-normal-LY3YDR5Y.woff2 +0 -0
  117. rustfava/static/fira-sans-vietnamese-400-normal-OBMQ72MR.woff2 +0 -0
  118. rustfava/static/fira-sans-vietnamese-500-normal-Y4NZR5EU.woff2 +0 -0
  119. rustfava/static/source-code-pro-cyrillic-400-normal-TO22V6M3.woff2 +0 -0
  120. rustfava/static/source-code-pro-cyrillic-500-normal-OGBWWWYW.woff2 +0 -0
  121. rustfava/static/source-code-pro-cyrillic-ext-400-normal-XH44UCIA.woff2 +0 -0
  122. rustfava/static/source-code-pro-cyrillic-ext-500-normal-3Z6MMVM6.woff2 +0 -0
  123. rustfava/static/source-code-pro-greek-400-normal-OUXXUQWK.woff2 +0 -0
  124. rustfava/static/source-code-pro-greek-500-normal-JA2Z5UXO.woff2 +0 -0
  125. rustfava/static/source-code-pro-greek-ext-400-normal-WCDKMX7U.woff2 +0 -0
  126. rustfava/static/source-code-pro-greek-ext-500-normal-ZHVI4VKW.woff2 +0 -0
  127. rustfava/static/source-code-pro-latin-400-normal-QOGTXED5.woff2 +0 -0
  128. rustfava/static/source-code-pro-latin-500-normal-X57QEOLQ.woff2 +0 -0
  129. rustfava/static/source-code-pro-latin-ext-400-normal-QXC74NBF.woff2 +0 -0
  130. rustfava/static/source-code-pro-latin-ext-500-normal-QGOY7MTT.woff2 +0 -0
  131. rustfava/static/source-code-pro-vietnamese-400-normal-NPDCDTBA.woff2 +0 -0
  132. rustfava/static/source-code-pro-vietnamese-500-normal-M6PJKTR5.woff2 +0 -0
  133. rustfava/static/tree-sitter-beancount-MLXFQBZ5.wasm +0 -0
  134. rustfava/static/web-tree-sitter-RNOQ6E74.wasm +0 -0
  135. rustfava/template_filters.py +64 -0
  136. rustfava/templates/_journal_table.html +156 -0
  137. rustfava/templates/_layout.html +26 -0
  138. rustfava/templates/_query_table.html +88 -0
  139. rustfava/templates/beancount_file +18 -0
  140. rustfava/templates/help.html +23 -0
  141. rustfava/templates/macros/_account_macros.html +5 -0
  142. rustfava/templates/macros/_commodity_macros.html +13 -0
  143. rustfava/translations/bg/LC_MESSAGES/messages.mo +0 -0
  144. rustfava/translations/bg/LC_MESSAGES/messages.po +618 -0
  145. rustfava/translations/ca/LC_MESSAGES/messages.mo +0 -0
  146. rustfava/translations/ca/LC_MESSAGES/messages.po +618 -0
  147. rustfava/translations/de/LC_MESSAGES/messages.mo +0 -0
  148. rustfava/translations/de/LC_MESSAGES/messages.po +618 -0
  149. rustfava/translations/es/LC_MESSAGES/messages.mo +0 -0
  150. rustfava/translations/es/LC_MESSAGES/messages.po +619 -0
  151. rustfava/translations/fa/LC_MESSAGES/messages.mo +0 -0
  152. rustfava/translations/fa/LC_MESSAGES/messages.po +618 -0
  153. rustfava/translations/fr/LC_MESSAGES/messages.mo +0 -0
  154. rustfava/translations/fr/LC_MESSAGES/messages.po +618 -0
  155. rustfava/translations/ja/LC_MESSAGES/messages.mo +0 -0
  156. rustfava/translations/ja/LC_MESSAGES/messages.po +618 -0
  157. rustfava/translations/nl/LC_MESSAGES/messages.mo +0 -0
  158. rustfava/translations/nl/LC_MESSAGES/messages.po +617 -0
  159. rustfava/translations/pt/LC_MESSAGES/messages.mo +0 -0
  160. rustfava/translations/pt/LC_MESSAGES/messages.po +617 -0
  161. rustfava/translations/pt_BR/LC_MESSAGES/messages.mo +0 -0
  162. rustfava/translations/pt_BR/LC_MESSAGES/messages.po +618 -0
  163. rustfava/translations/ru/LC_MESSAGES/messages.mo +0 -0
  164. rustfava/translations/ru/LC_MESSAGES/messages.po +617 -0
  165. rustfava/translations/sk/LC_MESSAGES/messages.mo +0 -0
  166. rustfava/translations/sk/LC_MESSAGES/messages.po +623 -0
  167. rustfava/translations/sv/LC_MESSAGES/messages.mo +0 -0
  168. rustfava/translations/sv/LC_MESSAGES/messages.po +618 -0
  169. rustfava/translations/uk/LC_MESSAGES/messages.mo +0 -0
  170. rustfava/translations/uk/LC_MESSAGES/messages.po +618 -0
  171. rustfava/translations/zh/LC_MESSAGES/messages.mo +0 -0
  172. rustfava/translations/zh/LC_MESSAGES/messages.po +618 -0
  173. rustfava/translations/zh_Hant_TW/LC_MESSAGES/messages.mo +0 -0
  174. rustfava/translations/zh_Hant_TW/LC_MESSAGES/messages.po +618 -0
  175. rustfava/util/__init__.py +157 -0
  176. rustfava/util/date.py +576 -0
  177. rustfava/util/excel.py +118 -0
  178. rustfava/util/ranking.py +79 -0
  179. rustfava/util/sets.py +18 -0
  180. rustfava/util/unreachable.py +20 -0
  181. rustfava-0.1.0.dist-info/METADATA +102 -0
  182. rustfava-0.1.0.dist-info/RECORD +187 -0
  183. rustfava-0.1.0.dist-info/WHEEL +5 -0
  184. rustfava-0.1.0.dist-info/entry_points.txt +2 -0
  185. rustfava-0.1.0.dist-info/licenses/AUTHORS +11 -0
  186. rustfava-0.1.0.dist-info/licenses/LICENSE +21 -0
  187. 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
+ }
@@ -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>
@@ -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.