PATH_TEMPLATES = Path('templates')
_PREFIX_EDIT = 'edit_'
_PREFIX_FILE = 'file_'
-_PREFIX_LEDGER = 'ledger_'
+PREFIX_LEDGER = 'ledger_'
_SERVER_HOST = '127.0.0.1'
_SERVER_PORT = 8084
-_TOK_RAW = 'raw'
-_TOK_STRUCTURED = 'structured'
+_SUFFIX_RAW = 'raw'
+_SUFFIX_STRUCTURED = 'structured'
PAGENAME_BALANCE = 'balance'
-_PAGENAME_EDIT_RAW = f'{_PREFIX_EDIT}{_TOK_RAW}'
-_PAGENAME_EDIT_STRUCTURED = f'{_PREFIX_EDIT}{_TOK_STRUCTURED}'
-PAGENAME_LEDGER_RAW = f'{_PREFIX_LEDGER}{_TOK_RAW}'
-PAGENAME_LEDGER_STRUCTURED = f'{_PREFIX_LEDGER}{_TOK_STRUCTURED}'
+PAGENAME_EDIT_RAW = f'{_PREFIX_EDIT}{_SUFFIX_RAW}'
+PAGENAME_EDIT_STRUCTURED = f'{_PREFIX_EDIT}{_SUFFIX_STRUCTURED}'
+PAGENAME_LEDGER_RAW = f'{PREFIX_LEDGER}{_SUFFIX_RAW}'
+PAGENAME_LEDGER_STRUCTURED = f'{PREFIX_LEDGER}{_SUFFIX_STRUCTURED}'
class Server(PlomHttpServer):
elif self.pagename.startswith(_PREFIX_EDIT)\
and self.postvars.first('apply'):
redir_target = self.post_edit()
- elif self.pagename.startswith(_PREFIX_LEDGER):
+ elif self.pagename.startswith(PREFIX_LEDGER):
redir_target = self.post_ledger_action()
self.redirect(redir_target)
'Based on postvars, edit targeted Booking.'
old_id = int(self.path_toks[2])
new_lines = []
- if self.pagename == _PAGENAME_EDIT_STRUCTURED:
+ if self.pagename == PAGENAME_EDIT_STRUCTURED:
line_keys = self.postvars.keys_prefixed('line_')
lineno_to_inputs: dict[int, list[str]] = {}
for key in line_keys:
return Path(self.path)
if 'add_booking' in self.postvars.as_dict:
id_ = self.server.ledger.add_empty_block()
- return Path('/', _PAGENAME_EDIT_STRUCTURED, f'{id_}')
- keys_prefixed = self.postvars.keys_prefixed(_PREFIX_LEDGER)
+ return Path('/', PAGENAME_EDIT_STRUCTURED, f'{id_}')
+ keys_prefixed = self.postvars.keys_prefixed(PREFIX_LEDGER)
action, id_str = keys_prefixed[0].split('_', maxsplit=2)[1:]
id_ = int(id_str)
if action.startswith('move'):
'Route GET requests to respective handlers.'
if self.pagename == 'blocks':
self.redirect(
- Path('/', _PAGENAME_EDIT_STRUCTURED, self.path_toks[2]))
+ Path('/', PAGENAME_EDIT_STRUCTURED, self.path_toks[2]))
return
ctx = {'unsaved_changes': self.server.ledger.tainted,
'path': self.path}
if self.pagename == PAGENAME_BALANCE:
self.get_balance(ctx)
elif self.pagename.startswith(_PREFIX_EDIT):
- self.get_edit(ctx, self.pagename == _PAGENAME_EDIT_RAW)
- elif self.pagename.startswith(_PREFIX_LEDGER):
+ self.get_edit(ctx, self.pagename == PAGENAME_EDIT_RAW)
+ elif self.pagename.startswith(PREFIX_LEDGER):
self.get_ledger(ctx, self.pagename == PAGENAME_LEDGER_RAW)
else:
self.get_ledger(ctx, False)
def get_edit(self, ctx, raw: bool) -> None:
'Display edit form for individual Booking.'
self._send_rendered(
- _PAGENAME_EDIT_RAW if raw else _PAGENAME_EDIT_STRUCTURED,
+ PAGENAME_EDIT_RAW if raw else PAGENAME_EDIT_STRUCTURED,
ctx | self.server.ledger.view_ctx_edit(int(self.path_toks[2]),
raw))
if not ac.parent],
key=lambda root: root.basename)}
- def view_ctx_edit(self, id_: int, raw: bool) -> dict[str, Any]:
+ def view_ctx_edit(self, id_: int, raw=True) -> dict[str, Any]:
'All context data relevant for rendering an edit view.'
accounts = self._calc_accounts()
block = self._blocks[id_]
# built-ins
from sys import exit as sys_exit
from pathlib import Path
-from typing import Any, Optional
+from typing import Optional
# requirements.txt
from jinja2 import (Environment as JinjaEnv,
FileSystemLoader as JinjaFSLoader)
# ourselves
from ledgplom.http import (
- PAGENAME_BALANCE, PAGENAME_LEDGER_RAW, PAGENAME_LEDGER_STRUCTURED,
- PATH_TEMPLATES)
+ PAGENAME_BALANCE, PAGENAME_EDIT_RAW, PAGENAME_EDIT_STRUCTURED,
+ PAGENAME_LEDGER_RAW, PAGENAME_LEDGER_STRUCTURED,
+ PATH_TEMPLATES, PREFIX_LEDGER)
from ledgplom.ledger import Ledger
jinja = JinjaEnv(loader=JinjaFSLoader(PATH_TEMPLATES),
autoescape=True,
trim_blocks=True)
- templates = {item: jinja.get_template(f'{item}.tmpl') for item in (
- PAGENAME_BALANCE, PAGENAME_LEDGER_RAW, PAGENAME_LEDGER_STRUCTURED)}
- for path in [p for p in _PATH_TESTS.iterdir()
- if p.parts[-1].endswith(_EXT_DAT)]:
- ledger = Ledger(path)
- for key, template in templates.items():
- test_path = Path(str(path)[:-len(_EXT_DAT)] + f'.{key}')
- if not test_path.exists():
+ templates = {item: jinja.get_template(f'{item}.tmpl')
+ for item in (PAGENAME_BALANCE,
+ PAGENAME_EDIT_RAW,
+ PAGENAME_EDIT_STRUCTURED,
+ PAGENAME_LEDGER_RAW,
+ PAGENAME_LEDGER_STRUCTURED)}
+ paths = tuple(_PATH_TESTS.iterdir())
+ for dat_path in [p for p in paths if p.parts[-1].endswith(_EXT_DAT)]:
+ ledger = Ledger(dat_path)
+ for test_path in [p for p in paths
+ if p != dat_path
+ and p.parts[-1].startswith(
+ str(dat_path.parts[-1])[:-len(_EXT_DAT)] + '.')]:
+ tmpl_name, id_str = (str(test_path.parts[-1]).split('.')
+ + ['-1'])[1:3]
+ if tmpl_name not in templates.keys():
continue
+ tmpl_name_start = tmpl_name.split("_")[0]
+ lines_rendered = templates[tmpl_name].render(
+ **getattr(ledger, f'view_ctx_{tmpl_name_start}')(
+ **({'raw': False} if tmpl_name == PAGENAME_EDIT_STRUCTURED
+ else {}) | ({} if tmpl_name_start == PREFIX_LEDGER[:-1]
+ else {'id_': int(id_str)}))
+ ).split('\n')
with test_path.open('r', encoding='utf8') as f:
- lines_expected = [line.rstrip('\n')
- for line in f.readlines()]
- ctx: dict[str, Any] = {}
- if key == PAGENAME_BALANCE:
- ctx |= ledger.view_ctx_balance(-1)
- else:
- ctx |= ledger.view_ctx_ledger()
- lines_rendered = template.render(**ctx).split('\n')
+ lines_expected = tuple(line.rstrip('\n')
+ for line in f.readlines())
msg_prefix = f'test for {test_path}:'
for idx0, line in enumerate(lines_rendered):
idx1 = idx0 + 1
{% macro edit_bar(block, here, there) %}
-<form action="/edit_{{here}}/{{block.id_}}" method="POST">
+<form action="/edit_{{ here }}/{{ block.id_ }}" method="POST">
<span class="disable_on_change">
-{{conditional_block_nav('/blocks/','prev',block)}}
-{{conditional_block_nav('/blocks/','next',block)}}
+{{ conditional_block_nav('/blocks/', 'prev', block) -}}
+{{ conditional_block_nav('/blocks/', 'next', block) -}}
</span>
-<input class="enable_on_change" type="submit" name="apply" value="apply" disabled />
-<input class="enable_on_change" type="submit" name="revert" value="revert" disabled />
+<input class="enable_on_change"
+ type="submit"
+ name="apply"
+ value="apply"
+ disabled
+ />
+<input class="enable_on_change"
+ type="submit"
+ name="revert"
+ value="revert"
+ disabled
+ />
<span class="disable_on_change">
-<a href="/edit_{{there}}/{{block.id_}}">switch to {{there}}</a>
+<a href="/edit_{{ there }}/{{ block.id_ }}">switch to {{ there }}</a>
·
-<a href="/balance?up_incl={{block.id_}}">balance after</a>
+<a href="/balance?up_incl={{ block.id_ }}">balance after</a>
·
-<a href="/ledger_{{here}}/#block_{{block.id_}}">in ledger</a>
+<a href="/ledger_{{ here }}/#block_{{ block.id_ }}">in ledger</a>
</span>
<hr />
{% if block.date_error or (block.booking and block.booking.sink_error) %}
- <div class="critical">block-wide errors:
- {{block.date_error}}
- {% if block.booking %}
- {% if block.date_error %}– and:{% endif %}
- {{block.booking.sink_error}}
- {% endif %}
- </div>
- <hr />
+<div class="critical">
+ block-wide errors:
+ {{ block.date_error }}
+{##}{% if block.booking %}
+ {{ "– and:" if block.date_error -}}
+ {{ block.booking.sink_error }}
+{##}{% endif %}
+</div>
+<hr />
{% endif %}
{% endmacro %}
-{% macro booking_balance(roots, valid) %}
-{% macro booking_balance_account_with_children(node) %}
- {% macro td_wealth(wealth_dict) %}
+{% macro booking_balance(roots, valid) -%}
+
+{##}{% macro booking_balance_account_with_children(node) %}
+{######}{% macro td_wealth(wealth_dict) %}
<td>
- <table>
- {% for curr, amt in wealth_dict.items() %}
- <tr>
- <td class="balance amount">{{amt}}</td>
- <td class="balance currency">{{ currency_short(curr) }}</td>
- </tr>
- {% endfor %}
- </table>
+ <table>
+{##########}{% for curr, amt in wealth_dict.items() %}
+ <tr>
+ <td class="balance amount">
+ {{- amt -}}
+ </td>
+ <td class="balance currency">
+ {{- currency_short(curr) -}}
+ </td>
+ </tr>
+{##########}{% endfor %}
+ </table>
+ </td>{##}
+{######}{% endmacro %}
+ <tr>
+ <td{{ ' class="direct_target"'|safe if node.direct_target }}>
+ {{- node.name }}{{ ':' if node.children -}}
</td>
- {% endmacro %}
+{# #}{{ td_wealth(node.wealth_before) }}
+{# #}{{ td_wealth(node.wealth_diff) }}
+{# #}{{ td_wealth(node.wealth_after) }}
+ </tr>
+{######}{% for child in node.children %}
+ {{- booking_balance_account_with_children(child) -}}
+{######}{% endfor %}
+{##}{% endmacro -%}
+
+<table class="alternating{{ ' critical' if not valid }}">
<tr>
- <td {% if node.direct_target %}class="direct_target"{% endif %}>
- {{node.name}}{% if node.children %}:{% endif %}
- </td>
- {{ td_wealth(node.wealth_before) }}
- {{ td_wealth(node.wealth_diff) }}
- {{ td_wealth(node.wealth_after) }}
+ <th>account</th>
+ <th>before</th>
+ <th>diff</th>
+ <th>after</th>
</tr>
- {% for child in node.children %}
- {{ booking_balance_account_with_children(child) }}
- {% endfor %}
-{% endmacro %}
-<table class="alternating {% if not valid %}critical{% endif %}">
-<tr>
-<th>account</th>
-<th>before</th>
-<th>diff</th>
-<th>after</th>
-</tr>
{% for root in roots %}
- {{ booking_balance_account_with_children(root) }}
+ {{- booking_balance_account_with_children(root) -}}
{% endfor %}
</table>
-{% endmacro %}
+
+{%- endmacro %}
{% block css %}
{{ macros.css_tabular_money() }}
{{ macros.css_booking_balance() }}
-{% endblock %}
+{%- endblock %}
{% block script %}
-{{macros.js_taint()}}
+{{ macros.js_taint() }}
{% endblock %}
{% block content %}
-{{macros.edit_bar(block,'raw','structured')}}
-<textarea name="raw_lines" cols=100 rows={{block.lines|length + 1}} oninput="taint()">
-{% for dat_line in block.lines %}{{ dat_line.raw }}
-{% endfor %}</textarea>
+{{ macros.edit_bar(block, 'raw', 'structured') }}
+<textarea name="raw_lines"
+ cols=100
+ rows={{ block.lines|length + 1 }}
+ oninput="taint()"
+ >
+{% for dat_line in block.lines %}
+{{ dat_line.raw }}
+{% endfor %}
+</textarea>
</form>
{{ macros.booking_balance(roots, valid) }}
{% endblock %}
{% block script %}
-{{macros.js_taint()}}
-var raw_gap_lines = {{raw_gap_lines|tojson|safe}};
-var booking_lines = {{booking_lines|tojson|safe}};
+{{ macros.js_taint() }}
+var raw_gap_lines = {{ raw_gap_lines|tojson|safe }};
+var booking_lines = {{ booking_lines|tojson|safe }};
function new_booking_line(account='', amount='None', currency='') {
return {
{% block content %}
-{{macros.edit_bar(block,'structured','raw')}}
+{{ macros.edit_bar(block, 'structured', 'raw') }}
<span class="disable_on_change">
<input type="button" onclick="mirror()" value="mirror">
<input type="button" onclick="fill_sink()" value="fill sink">
<table id="booking_lines" class="alternating">
</table>
<div>Gap:</div>
-<textarea id="gap_lines" name="raw_lines" cols=100 rows={{raw_gap_lines|length + 1}} oninput="taint()">
+<textarea id="gap_lines"
+ name="raw_lines"
+ cols=100
+ rows={{ raw_gap_lines|length + 1 }}
+ oninput="taint()"
+ >
</textarea>
</form>
<datalist id="all_accounts">
{% for acc in all_accounts %}
-<option value="{{acc}}">{{acc}}</a>
+<option value="{{ acc }}">{{ acc }}</a>
{% endfor %}
</datalist>
<hr />
--- /dev/null
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+var suppress_beforeunload = false;
+
+function taint() {
+ // activate buttons "apply", "revert"
+ Array.from(document.getElementsByClassName('enable_on_change')).forEach((el) => {
+ el.disabled = false;
+ el.addEventListener('click', function(e) {
+ suppress_beforeunload = true;
+ });
+ });
+ // deactivate "disable_on_change" span contents
+ function recursive_span_disable(el) {
+ old_nodes = Array.from(el.childNodes);
+ el.innerHTML = '';
+ old_nodes.forEach((node) => {
+ if (node.tagName == 'SPAN') {
+ recursive_span_disable(node);
+ el.appendChild(node);
+ } else if (node.tagName == 'INPUT') {
+ node.disabled = true;
+ el.appendChild(node);
+ } else if (node.tagName == 'A') {
+ const del = document.createElement('del');
+ del.textContent = node.textContent;
+ el.appendChild(del);
+ } else {
+ el.appendChild(node);
+ };
+ });
+ }
+ Array.from(document.getElementsByClassName('disable_on_change')).forEach((el) => {
+ recursive_span_disable(el);
+ });
+ // try to catch user closing or reloading window
+ window.addEventListener('beforeunload', function(e) {
+ if (!suppress_beforeunload) {
+ e.preventDefault();
+ e.returnValue = true;
+ }
+ });
+ // remove oninput handlers no longer needed (since we only ever go one way)
+ Array.from(document.querySelectorAll('*')
+ ).filter(el => (el.oninput !== null)
+ ).forEach(el => el.oninput = null);
+}
+
+</script>
+<style>
+html {
+ scroll-padding-top: 2em;
+}
+body {
+ background: #ffffff;
+ font-family: sans-serif;
+ text-align: left;
+ margin: 0;
+ padding: 0;
+}
+#header {
+ background: #ffffff;
+ position: sticky;
+ top: 0;
+ padding-left: 0.5em;
+ padding-bottom: 0.25em;
+ border-bottom: 1px solid black;
+}
+table.alternating > tbody > tr:nth-child(odd) {
+ background-color: #dcdcdc;
+}
+table.alternating > tbody > tr:nth-child(even) {
+ background: #ffffff;
+}
+td {
+ vertical-align: top;
+}
+.critical {
+ background: #ff6666 !important;
+}
+td.amount {
+ text-align: right;
+}
+td.amount,
+td.currency {
+ font-family: monospace;
+ font-size: 1.25em;
+}
+table.alternating.critical > tbody > tr:nth-child(even) {
+ background-color: #ff8a8a;
+}
+td.balance.amount {
+ width: 10em;
+}
+td.balance.currency {
+ width: 3em;
+}
+td.direct_target {
+ font-weight: bold;
+}
+</style>
+</head>
+<body>
+<div id="header">
+ <form action="" method="POST">
+ <span class="disable_on_change">
+ ledger <a href="/ledger_structured">structured</a>
+ / <a href="/ledger_raw">raw</a>
+ · <a href="/balance">balance</a>
+ · <input type="submit"name="file_load" value="reload" />
+ </span>
+ </form>
+</div>
+<form action="/edit_raw/1" method="POST">
+<span class="disable_on_change">
+<a href="/blocks/0">prev</a>
+<a href="/blocks/2">next</a>
+</span>
+<input class="enable_on_change"
+ type="submit"
+ name="apply"
+ value="apply"
+ disabled
+ />
+<input class="enable_on_change"
+ type="submit"
+ name="revert"
+ value="revert"
+ disabled
+ />
+<span class="disable_on_change">
+<a href="/edit_structured/1">switch to structured</a>
+·
+<a href="/balance?up_incl=1">balance after</a>
+·
+<a href="/ledger_raw/#block_1">in ledger</a>
+</span>
+<hr />
+
+<textarea name="raw_lines"
+ cols=100
+ rows=5
+ oninput="taint()"
+ >
+2001-01-01 test ; foo
+ foo 10 €
+ bar -10 €
+
+</textarea>
+</form>
+<table class="alternating">
+ <tr>
+ <th>account</th>
+ <th>before</th>
+ <th>diff</th>
+ <th>after</th>
+ </tr>
+ <tr>
+ <td class="direct_target">bar</td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">0</td>
+ <td class="balance currency">€</td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">-10</td>
+ <td class="balance currency">€</td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">-10</td>
+ <td class="balance currency">€</td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td class="direct_target">foo</td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">0</td>
+ <td class="balance currency">€</td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">10</td>
+ <td class="balance currency">€</td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">10</td>
+ <td class="balance currency">€</td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+</table>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+var suppress_beforeunload = false;
+
+function taint() {
+ // activate buttons "apply", "revert"
+ Array.from(document.getElementsByClassName('enable_on_change')).forEach((el) => {
+ el.disabled = false;
+ el.addEventListener('click', function(e) {
+ suppress_beforeunload = true;
+ });
+ });
+ // deactivate "disable_on_change" span contents
+ function recursive_span_disable(el) {
+ old_nodes = Array.from(el.childNodes);
+ el.innerHTML = '';
+ old_nodes.forEach((node) => {
+ if (node.tagName == 'SPAN') {
+ recursive_span_disable(node);
+ el.appendChild(node);
+ } else if (node.tagName == 'INPUT') {
+ node.disabled = true;
+ el.appendChild(node);
+ } else if (node.tagName == 'A') {
+ const del = document.createElement('del');
+ del.textContent = node.textContent;
+ el.appendChild(del);
+ } else {
+ el.appendChild(node);
+ };
+ });
+ }
+ Array.from(document.getElementsByClassName('disable_on_change')).forEach((el) => {
+ recursive_span_disable(el);
+ });
+ // try to catch user closing or reloading window
+ window.addEventListener('beforeunload', function(e) {
+ if (!suppress_beforeunload) {
+ e.preventDefault();
+ e.returnValue = true;
+ }
+ });
+ // remove oninput handlers no longer needed (since we only ever go one way)
+ Array.from(document.querySelectorAll('*')
+ ).filter(el => (el.oninput !== null)
+ ).forEach(el => el.oninput = null);
+}
+
+</script>
+<style>
+html {
+ scroll-padding-top: 2em;
+}
+body {
+ background: #ffffff;
+ font-family: sans-serif;
+ text-align: left;
+ margin: 0;
+ padding: 0;
+}
+#header {
+ background: #ffffff;
+ position: sticky;
+ top: 0;
+ padding-left: 0.5em;
+ padding-bottom: 0.25em;
+ border-bottom: 1px solid black;
+}
+table.alternating > tbody > tr:nth-child(odd) {
+ background-color: #dcdcdc;
+}
+table.alternating > tbody > tr:nth-child(even) {
+ background: #ffffff;
+}
+td {
+ vertical-align: top;
+}
+.critical {
+ background: #ff6666 !important;
+}
+td.amount {
+ text-align: right;
+}
+td.amount,
+td.currency {
+ font-family: monospace;
+ font-size: 1.25em;
+}
+table.alternating.critical > tbody > tr:nth-child(even) {
+ background-color: #ff8a8a;
+}
+td.balance.amount {
+ width: 10em;
+}
+td.balance.currency {
+ width: 3em;
+}
+td.direct_target {
+ font-weight: bold;
+}
+</style>
+</head>
+<body>
+<div id="header">
+ <form action="" method="POST">
+ <span class="disable_on_change">
+ ledger <a href="/ledger_structured">structured</a>
+ / <a href="/ledger_raw">raw</a>
+ · <a href="/balance">balance</a>
+ · <input type="submit"name="file_load" value="reload" />
+ </span>
+ </form>
+</div>
+<form action="/edit_raw/4" method="POST">
+<span class="disable_on_change">
+<a href="/blocks/3">prev</a>
+<del>next</del>
+</span>
+<input class="enable_on_change"
+ type="submit"
+ name="apply"
+ value="apply"
+ disabled
+ />
+<input class="enable_on_change"
+ type="submit"
+ name="revert"
+ value="revert"
+ disabled
+ />
+<span class="disable_on_change">
+<a href="/edit_structured/4">switch to structured</a>
+·
+<a href="/balance?up_incl=4">balance after</a>
+·
+<a href="/ledger_raw/#block_4">in ledger</a>
+</span>
+<hr />
+<div class="critical">
+ block-wide errors:
+ date < previous date
+ – and:
+</div>
+<hr />
+
+<textarea name="raw_lines"
+ cols=100
+ rows=7
+ oninput="taint()"
+ >
+2001-01-01 test
+ foo:x 10 €
+ foo:x 1 USD
+ bar:x:y -10 €
+ bar:z -1 USD
+
+</textarea>
+</form>
+<table class="alternating critical">
+ <tr>
+ <th>account</th>
+ <th>before</th>
+ <th>diff</th>
+ <th>after</th>
+ </tr>
+ <tr>
+ <td>bar:</td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">1</td>
+ <td class="balance currency">€</td>
+ </tr>
+ <tr>
+ <td class="balance amount">0</td>
+ <td class="balance currency">USD</td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">-10</td>
+ <td class="balance currency">€</td>
+ </tr>
+ <tr>
+ <td class="balance amount">-1</td>
+ <td class="balance currency">USD</td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">-9</td>
+ <td class="balance currency">€</td>
+ </tr>
+ <tr>
+ <td class="balance amount">-1</td>
+ <td class="balance currency">USD</td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td>bar:x:</td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">0</td>
+ <td class="balance currency">€</td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">-10</td>
+ <td class="balance currency">€</td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">-10</td>
+ <td class="balance currency">€</td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td class="direct_target">bar:x:y</td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">0</td>
+ <td class="balance currency">€</td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">-10</td>
+ <td class="balance currency">€</td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">-10</td>
+ <td class="balance currency">€</td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td class="direct_target">bar:z</td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">0</td>
+ <td class="balance currency">USD</td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">-1</td>
+ <td class="balance currency">USD</td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">-1</td>
+ <td class="balance currency">USD</td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td>foo:</td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">10</td>
+ <td class="balance currency">€</td>
+ </tr>
+ <tr>
+ <td class="balance amount">0</td>
+ <td class="balance currency">USD</td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">10</td>
+ <td class="balance currency">€</td>
+ </tr>
+ <tr>
+ <td class="balance amount">1</td>
+ <td class="balance currency">USD</td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">20</td>
+ <td class="balance currency">€</td>
+ </tr>
+ <tr>
+ <td class="balance amount">1</td>
+ <td class="balance currency">USD</td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td class="direct_target">foo:x</td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">0</td>
+ <td class="balance currency">€</td>
+ </tr>
+ <tr>
+ <td class="balance amount">0</td>
+ <td class="balance currency">USD</td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">10</td>
+ <td class="balance currency">€</td>
+ </tr>
+ <tr>
+ <td class="balance amount">1</td>
+ <td class="balance currency">USD</td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td class="balance amount">10</td>
+ <td class="balance currency">€</td>
+ </tr>
+ <tr>
+ <td class="balance amount">1</td>
+ <td class="balance currency">USD</td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+</table>
+</body>
+</html>