beancount / fava

Fava - web interface for Beancount

Home Page:https://beancount.github.io/fava/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Expose TreeTable to extensions

scauligi opened this issue · comments

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?

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)?

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.

(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
{% macro account_name(ledger, account_name) -%}
<a href="{{ url_for('account', name=account_name) }}" class="account">
  {{- account_name.split(':')[-1] -}}
</a>
{%- if ledger.accounts[account_name].uptodate_status %}

{{ indicator(ledger, account_name) }}
{{ last_account_activity(ledger, account_name) }}
{% endif %}
{% endmacro %}

{% macro render_currency(ledger, currency) -%}
<span title="{{ ledger.commodities.name(currency) }}">{{ currency }}</span>
{%- endmacro %}

{% macro render_diff_and_number(balance, cost, currency, invert=False) %}
  {% set num = balance.pop(currency, 0) %}
  {% set num2 = -num if invert else num %}
  <span class="number">{{ num2|format_currency(currency) }}</span>
  {% if currency in cost %}
    {% set cost_num = cost.pop(currency, 0) %}
    {% set diff = num - cost_num %}
    {% if invert %}
      {% set diff = -diff %}
    {% endif %}
    {%- if diff -%}
    <br>
    <span class="diff{{ ' positive' if diff > 0 else ' negative' }}" title="{{ cost_num|format_currency(currency, invert=invert) }} {{ currency }}">({{ diff|format_currency(currency) }})</span>
    {%- endif -%}
  {%- endif -%}
{%- endmacro %}

{% macro tree(account_node, invert=False, ledger=None) %}
{% set ledger = ledger or g.ledger %}
<tree-table>
  <ol class="flex-table tree-table{{ ' two-currencies' if ledger.options.operating_currency|length > 1 else '' }}" title="{{ _('Hold Shift while clicking to expand all children.\nHold Ctrl or Cmd while clicking to expand one level.') }}">
    <li class="head">
      <p>
      <span class="account-cell"><button type="button" class="link expand-all hidden" title="{{ _('Expand all accounts') }}">{{ _('Expand all') }}</button></span>
      {% for currency in ledger.options.operating_currency %}
      <span class="num">{{ currency }}</span>
      {% endfor %}
      <span class="num other">{{ _('Other') }}</span>
      </p>
    </li>
    {% 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 {} %}
    <li{{ ' class=toggled' if extension.collapse_account(account.name) else '' }}>
      <p{{ ' class=has-balance' if not balance.is_empty() else '' }}>
      <span class="account-cell depth-{{ loop.depth0 }} droptarget{{ ' has-children' if account.children else '' }}" data-account-name="{{ account.name }}">
        {{ account_name(ledger, account.name) }}
      </span>
      {% for currency in ledger.options.operating_currency %}
      <span class="num">
        <span class="balance">{{ render_diff_and_number(balance, cost, currency, invert=invert) }}</span>
        <span class="balance-children">{{ render_diff_and_number(balance_children, cost_children, currency, invert=invert) }}</span>
      </span>
      {% endfor %}
      <span class="num other">
        <span class="balance">
          {% for currency in balance.keys()|sort %}
            {{ render_diff_and_number(balance, cost, currency, invert=invert) }} {{ render_currency(ledger, currency) }}<br>
          {% endfor %}
        </span>
        <span class="balance-children">
          {% for currency in balance_children.keys()|sort %}
            {{ render_diff_and_number(balance_children, cost_children, currency, invert=invert) }} {{ render_currency(ledger, currency) }}<br>
          {% endfor %}
        </span>
      </span>
      </p>
      {% if account.children %}
  <ol>{{- loop(account.children|sort(attribute='name')) -}} </ol>
      {% endif %}
    </li>
    {% endfor %}
  </ol>
</tree-table>
{% 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
    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.