beancount / fava

Fava - web interface for Beancount
https://beancount.github.io/fava/
MIT License
1.93k stars 286 forks source link

Expose TreeTable to extensions #1695

Open scauligi opened 11 months ago

scauligi commented 11 months ago

Version 1.26 moved much of the UI generation from templates into svelte/javascript. Before, I could include some of the template helpers (eg for treetables) in my own extension templates, but now that it's in svelte there doesn't seem to be a good way of making my own TreeTables aside from copying-and-pasting code and CSS styles.

Is it possible to expose TreeTables in a way that extensions can more easily use them?

yagebu commented 9 months ago

Hi, thanks for opening this issue. As also seen in #1721, this seems to be a useful component for extensions :) As a "quick fix", I've re-added the old css and js so you could still use the old ones for a while (however, I haven't re-added the template code so you'd need to copy paste that).

I think there's basically two ways that the new component(s) could be exposed to the frontend: 1) providing them to Javascript extensions (https://svelte.dev/docs/client-side-component-api) or 2) wrapping them in a custom HTML element (just like it is already done for the charts) where the data for the component would be provided as JSON in this custom element. (doing both also be an option)

Could you describe your use case a bit? Are you using them to render a Fava TreeNode or do you want to render some other tree-like data (like fava_investor seems to do)?

upsuper commented 8 months ago

I would like to also be able to access the tree table.

In my use case, I render the balance sheet with only accounts under certain criteria.

upsuper commented 8 months ago

(however, I haven't re-added the template code so you'd need to copy paste that)

It's definitely not that simple as just copy and paste, as a lot of code was deleted as part of that change. I had to go through a lot of hoops before it started working again.

To save people some time, this is my _tree_table.html:

_tree_table.html ```html {% macro account_name(ledger, account_name) -%} {%- if ledger.accounts[account_name].uptodate_status %} {{ indicator(ledger, account_name) }} {{ last_account_activity(ledger, account_name) }} {% endif %} {% endmacro %} {% macro render_currency(ledger, currency) -%} {{ currency }} {%- endmacro %} {% macro render_diff_and_number(balance, cost, currency, invert=False) %} {% set num = balance.pop(currency, 0) %} {% set num2 = -num if invert else num %} {{ num2|format_currency(currency) }} {% if currency in cost %} {% set cost_num = cost.pop(currency, 0) %} {% set diff = num - cost_num %} {% if invert %} {% set diff = -diff %} {% endif %} {%- if diff -%}
({{ diff|format_currency(currency) }}) {%- endif -%} {%- endif -%} {%- endmacro %} {% macro tree(account_node, invert=False, ledger=None) %} {% set ledger = ledger or g.ledger %}
  1. {% for currency in ledger.options.operating_currency %} {{ currency }} {% endfor %} {{ _('Other') }}

  2. {% set end_date = g.filtered.end_date %} {% for account in ([account_node] if account_node.name else account_node.children) if extension.should_show(account) recursive %} {% set balance = extension.cost_or_value(account.balance, end_date) %} {% set balance_children = extension.cost_or_value(account.balance_children, end_date) %} {% set cost = extension.cost(account.balance) if g.conversion == 'at_value' else {} %} {% set cost_children = extension.cost(account.balance_children) if g.conversion == 'at_value' else {} %} {% for currency in ledger.options.operating_currency %} {{ render_diff_and_number(balance, cost, currency, invert=invert) }} {{ render_diff_and_number(balance_children, cost_children, currency, invert=invert) }} {% endfor %} {% for currency in balance.keys()|sort %} {{ render_diff_and_number(balance, cost, currency, invert=invert) }} {{ render_currency(ledger, currency) }}
    {% endfor %}
    {% for currency in balance_children.keys()|sort %} {{ render_diff_and_number(balance_children, cost_children, currency, invert=invert) }} {{ render_currency(ledger, currency) }}
    {% endfor %}

    {% if account.children %}
      {{- loop(account.children|sort(attribute='name')) -}}
    {% endif %} {% endfor %}
{% endmacro %} ```

And because many of the filters referenced in the macro has been removed, I have also had to add the following methods into my extension class to be used by the template:

__init__.py ```python def should_show(self, account: TreeNode) -> bool: """Determine whether the account should be shown.""" from fava.context import g if not account.balance_children.is_empty() or any( self.should_show(a) for a in account.children ): return True ledger = g.ledger filtered = g.filtered if account.name not in ledger.accounts: return False fava_options = ledger.fava_options if not fava_options.show_closed_accounts and filtered.account_is_closed( account.name, ): return False if ( not fava_options.show_accounts_with_zero_balance and account.balance.is_empty() ): return False if ( not fava_options.show_accounts_with_zero_transactions and not account.has_txns ): return False return True def collapse_account(self, account_name: str) -> bool: """Return true if account should be collapsed.""" from fava.context import g collapse_patterns = g.ledger.fava_options.collapse_pattern return any(pattern.match(account_name) for pattern in collapse_patterns) def cost(self, inventory: CounterInventory) -> SimpleCounterInventory: """Get the cost of an inventory.""" return inventory.reduce(get_cost) def cost_or_value( self, inventory: CounterInventory, date: date | None = None, ) -> SimpleCounterInventory: """Get the cost or value of an inventory.""" from fava.context import g return cost_or_value(inventory, g.conversion, g.ledger.prices, date) ```

Hope this would be useful.

But it is definitely better if we can just use the same tree table used in the builtin report.