Late night fixing

This commit is contained in:
fleaz 2020-04-25 11:07:38 +02:00
parent f9967bad71
commit d9951b69ca
10 changed files with 730 additions and 29 deletions

View file

@ -6,7 +6,8 @@ import re
from xdg import XDG_CONFIG_HOME, XDG_DATA_HOME from xdg import XDG_CONFIG_HOME, XDG_DATA_HOME
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from models import Category import models
import locale
try: try:
from yaml import CLoader as Loader from yaml import CLoader as Loader
@ -18,8 +19,13 @@ CONFIG_DIR = os.path.join(XDG_CONFIG_HOME, "schmeckels")
DATA_DIR = os.path.join(XDG_DATA_HOME, "schmeckels") DATA_DIR = os.path.join(XDG_DATA_HOME, "schmeckels")
def format_amount(cents):
amount = cents/100
return f"{amount:,.2f}"
def get_list_of_bookable_categories(session): def get_list_of_bookable_categories(session):
categories = session.query(Category).all() categories = session.query(models.Category).all()
return [c for c in categories if c.is_child()] return [c for c in categories if c.is_child()]
@ -85,9 +91,9 @@ def get_rules(profile_name):
def create_category(name, parent, profile, session): def create_category(name, parent, profile, session):
c = session.query(Category).filter(Category.name == name).first() c = session.query(models.Category).filter(models.Category.name == name).first()
if not c: if not c:
c = Category(name=name, parent_id=parent) c = models.Category(name=name, parent_id=parent)
session.add(c) session.add(c)
session.commit() session.commit()
return c.id return c.id

View file

@ -2,6 +2,7 @@
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.orm import relationship, backref from sqlalchemy.orm import relationship, backref
from helper import format_amount
Base = declarative_base() Base = declarative_base()
@ -20,7 +21,7 @@ class Transaction(Base):
return self.amount > 0 return self.amount > 0
def pretty_amount(self): def pretty_amount(self):
return "{0:.2f}".format(self.amount / 100) return format_amount(self.amount)
def get_date(self, format): def get_date(self, format):
if format == "iso": if format == "iso":

View file

@ -1,10 +1,10 @@
#! /usr/bin/env python3 #! /usr/bin/env python3
from bottle import route, run, template, redirect, request, post from bottle import route, run, template, redirect, request, post
from models import Category, Transaction from helper import get_session, format_amount
from helper import get_session
from datetime import datetime from datetime import datetime
from sqlalchemy import func from sqlalchemy import func
import click import click
from models import Transaction, Category
session = None session = None
@ -14,7 +14,7 @@ def index():
fom = datetime.today().replace(day=1) fom = datetime.today().replace(day=1)
sum_of_categories = ( sum_of_categories = (
session.query(Transaction.category_id, func.sum(Transaction.amount).label("total")) session.query(Transaction.category_id, func.sum(Transaction.amount).label("total"))
.filter(Transaction.date < fom) .filter(Transaction.date > fom)
.group_by(Transaction.category_id) .group_by(Transaction.category_id)
.all() .all()
) )
@ -22,10 +22,10 @@ def index():
for s in sum_of_categories: for s in sum_of_categories:
if s.category_id: if s.category_id:
categories.append( categories.append(
{"name": session.query(Category).get(s.category_id).full_name(), "amount": s.total / 100,} {"name": session.query(Category).get(s.category_id).full_name(), "amount": format_amount(s.total)}
) )
else: else:
categories.append({"name": "Unsorted", "amount": s.total / 100}) categories.append({"name": "Unsorted", "amount": format_amount(s.total)})
categories = sorted(categories, key=lambda i: i["name"]) categories = sorted(categories, key=lambda i: i["name"])
return template("index", categories=categories) return template("index", categories=categories)
@ -39,6 +39,12 @@ def categories():
return template("categories", categories=categories) return template("categories", categories=categories)
@route("/graph")
def graph():
data = []
return template("graph", data=data)
@route("/category/<name>") @route("/category/<name>")
def category(name): def category(name):
c = session.query(Category).filter(Category.name == name).first() c = session.query(Category).filter(Category.name == name).first()

View file

@ -16,7 +16,7 @@ def command(profile):
category_lookup = {c.full_name(): c.id for c in categories} category_lookup = {c.full_name(): c.id for c in categories}
category_names = FuzzyWordCompleter([c.full_name() for c in categories]) category_names = FuzzyWordCompleter([c.full_name() for c in categories])
unsorted = session.query(Transaction).filter(Transaction.category_id is None).all() unsorted = session.query(Transaction).filter(Transaction.category_id == None).all()
print("Found {} unsorted transcations".format(len(unsorted))) print("Found {} unsorted transcations".format(len(unsorted)))
for t in unsorted: for t in unsorted:
@ -29,10 +29,14 @@ def command(profile):
print("Goodbye") print("Goodbye")
sys.exit(0) sys.exit(0)
if select == "":
print("Skipping")
continue
cat_id = category_lookup.get(select, None) cat_id = category_lookup.get(select, None)
if not cat_id: if not cat_id:
print(f"Creating new category '{select}'") print(f"Creating new category '{select}'")
cat_id = add_category(select, profile) cat_id = add_category(select, profile, session)
# Update the list of categories # Update the list of categories
categories = get_list_of_bookable_categories(session) categories = get_list_of_bookable_categories(session)
category_lookup = {c.full_name(): c.id for c in categories} category_lookup = {c.full_name(): c.id for c in categories}

653
static/apexcharts.css Normal file
View file

@ -0,0 +1,653 @@
.apexcharts-canvas {
position: relative;
user-select: none;
/* cannot give overflow: hidden as it will crop tooltips which overflow outside chart area */
}
/* scrollbar is not visible by default for legend, hence forcing the visibility */
.apexcharts-canvas ::-webkit-scrollbar {
-webkit-appearance: none;
width: 6px;
}
.apexcharts-canvas ::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: rgba(0, 0, 0, .5);
box-shadow: 0 0 1px rgba(255, 255, 255, .5);
-webkit-box-shadow: 0 0 1px rgba(255, 255, 255, .5);
}
.apexcharts-canvas.apexcharts-theme-dark {
background: #343F57;
}
.apexcharts-inner {
position: relative;
}
.apexcharts-text tspan {
font-family: inherit;
}
.legend-mouseover-inactive {
transition: 0.15s ease all;
opacity: 0.20;
}
.apexcharts-series-collapsed {
opacity: 0;
}
.apexcharts-tooltip {
border-radius: 5px;
box-shadow: 2px 2px 6px -4px #999;
cursor: default;
font-size: 14px;
left: 62px;
opacity: 0;
pointer-events: none;
position: absolute;
top: 20px;
overflow: hidden;
white-space: nowrap;
z-index: 12;
transition: 0.15s ease all;
}
.apexcharts-tooltip.apexcharts-theme-light {
border: 1px solid #e3e3e3;
background: rgba(255, 255, 255, 0.96);
}
.apexcharts-tooltip.apexcharts-theme-dark {
color: #fff;
background: rgba(30, 30, 30, 0.8);
}
.apexcharts-tooltip * {
font-family: inherit;
}
.apexcharts-tooltip.apexcharts-active {
opacity: 1;
transition: 0.15s ease all;
}
.apexcharts-tooltip-title {
padding: 6px;
font-size: 15px;
margin-bottom: 4px;
}
.apexcharts-tooltip.apexcharts-theme-light .apexcharts-tooltip-title {
background: #ECEFF1;
border-bottom: 1px solid #ddd;
}
.apexcharts-tooltip.apexcharts-theme-dark .apexcharts-tooltip-title {
background: rgba(0, 0, 0, 0.7);
border-bottom: 1px solid #333;
}
.apexcharts-tooltip-text-value,
.apexcharts-tooltip-text-z-value {
display: inline-block;
font-weight: 600;
margin-left: 5px;
}
.apexcharts-tooltip-text-z-label:empty,
.apexcharts-tooltip-text-z-value:empty {
display: none;
}
.apexcharts-tooltip-text-value,
.apexcharts-tooltip-text-z-value {
font-weight: 600;
}
.apexcharts-tooltip-marker {
width: 12px;
height: 12px;
position: relative;
top: 0px;
margin-right: 10px;
border-radius: 50%;
}
.apexcharts-tooltip-series-group {
padding: 0 10px;
display: none;
text-align: left;
justify-content: left;
align-items: center;
}
.apexcharts-tooltip-series-group.apexcharts-active .apexcharts-tooltip-marker {
opacity: 1;
}
.apexcharts-tooltip-series-group.apexcharts-active,
.apexcharts-tooltip-series-group:last-child {
padding-bottom: 4px;
}
.apexcharts-tooltip-series-group-hidden {
opacity: 0;
height: 0;
line-height: 0;
padding: 0 !important;
}
.apexcharts-tooltip-y-group {
padding: 6px 0 5px;
}
.apexcharts-tooltip-candlestick {
padding: 4px 8px;
}
.apexcharts-tooltip-candlestick>div {
margin: 4px 0;
}
.apexcharts-tooltip-candlestick span.value {
font-weight: bold;
}
.apexcharts-tooltip-rangebar {
padding: 5px 8px;
}
.apexcharts-tooltip-rangebar .category {
font-weight: 600;
color: #777;
}
.apexcharts-tooltip-rangebar .series-name {
font-weight: bold;
display: block;
margin-bottom: 5px;
}
.apexcharts-xaxistooltip {
opacity: 0;
padding: 9px 10px;
pointer-events: none;
color: #373d3f;
font-size: 13px;
text-align: center;
border-radius: 2px;
position: absolute;
z-index: 10;
background: #ECEFF1;
border: 1px solid #90A4AE;
transition: 0.15s ease all;
}
.apexcharts-xaxistooltip.apexcharts-theme-dark {
background: rgba(0, 0, 0, 0.7);
border: 1px solid rgba(0, 0, 0, 0.5);
color: #fff;
}
.apexcharts-xaxistooltip:after,
.apexcharts-xaxistooltip:before {
left: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
.apexcharts-xaxistooltip:after {
border-color: rgba(236, 239, 241, 0);
border-width: 6px;
margin-left: -6px;
}
.apexcharts-xaxistooltip:before {
border-color: rgba(144, 164, 174, 0);
border-width: 7px;
margin-left: -7px;
}
.apexcharts-xaxistooltip-bottom:after,
.apexcharts-xaxistooltip-bottom:before {
bottom: 100%;
}
.apexcharts-xaxistooltip-top:after,
.apexcharts-xaxistooltip-top:before {
top: 100%;
}
.apexcharts-xaxistooltip-bottom:after {
border-bottom-color: #ECEFF1;
}
.apexcharts-xaxistooltip-bottom:before {
border-bottom-color: #90A4AE;
}
.apexcharts-xaxistooltip-bottom.apexcharts-theme-dark:after {
border-bottom-color: rgba(0, 0, 0, 0.5);
}
.apexcharts-xaxistooltip-bottom.apexcharts-theme-dark:before {
border-bottom-color: rgba(0, 0, 0, 0.5);
}
.apexcharts-xaxistooltip-top:after {
border-top-color: #ECEFF1
}
.apexcharts-xaxistooltip-top:before {
border-top-color: #90A4AE;
}
.apexcharts-xaxistooltip-top.apexcharts-theme-dark:after {
border-top-color: rgba(0, 0, 0, 0.5);
}
.apexcharts-xaxistooltip-top.apexcharts-theme-dark:before {
border-top-color: rgba(0, 0, 0, 0.5);
}
.apexcharts-xaxistooltip.apexcharts-active {
opacity: 1;
transition: 0.15s ease all;
}
.apexcharts-yaxistooltip {
opacity: 0;
padding: 4px 10px;
pointer-events: none;
color: #373d3f;
font-size: 13px;
text-align: center;
border-radius: 2px;
position: absolute;
z-index: 10;
background: #ECEFF1;
border: 1px solid #90A4AE;
}
.apexcharts-yaxistooltip.apexcharts-theme-dark {
background: rgba(0, 0, 0, 0.7);
border: 1px solid rgba(0, 0, 0, 0.5);
color: #fff;
}
.apexcharts-yaxistooltip:after,
.apexcharts-yaxistooltip:before {
top: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
.apexcharts-yaxistooltip:after {
border-color: rgba(236, 239, 241, 0);
border-width: 6px;
margin-top: -6px;
}
.apexcharts-yaxistooltip:before {
border-color: rgba(144, 164, 174, 0);
border-width: 7px;
margin-top: -7px;
}
.apexcharts-yaxistooltip-left:after,
.apexcharts-yaxistooltip-left:before {
left: 100%;
}
.apexcharts-yaxistooltip-right:after,
.apexcharts-yaxistooltip-right:before {
right: 100%;
}
.apexcharts-yaxistooltip-left:after {
border-left-color: #ECEFF1;
}
.apexcharts-yaxistooltip-left:before {
border-left-color: #90A4AE;
}
.apexcharts-yaxistooltip-left.apexcharts-theme-dark:after {
border-left-color: rgba(0, 0, 0, 0.5);
}
.apexcharts-yaxistooltip-left.apexcharts-theme-dark:before {
border-left-color: rgba(0, 0, 0, 0.5);
}
.apexcharts-yaxistooltip-right:after {
border-right-color: #ECEFF1;
}
.apexcharts-yaxistooltip-right:before {
border-right-color: #90A4AE;
}
.apexcharts-yaxistooltip-right.apexcharts-theme-dark:after {
border-right-color: rgba(0, 0, 0, 0.5);
}
.apexcharts-yaxistooltip-right.apexcharts-theme-dark:before {
border-right-color: rgba(0, 0, 0, 0.5);
}
.apexcharts-yaxistooltip.apexcharts-active {
opacity: 1;
}
.apexcharts-yaxistooltip-hidden {
display: none;
}
.apexcharts-xcrosshairs,
.apexcharts-ycrosshairs {
pointer-events: none;
opacity: 0;
transition: 0.15s ease all;
}
.apexcharts-xcrosshairs.apexcharts-active,
.apexcharts-ycrosshairs.apexcharts-active {
opacity: 1;
transition: 0.15s ease all;
}
.apexcharts-ycrosshairs-hidden {
opacity: 0;
}
.apexcharts-selection-rect {
cursor: move;
}
.svg_select_points,
.svg_select_points_rot {
opacity: 0;
visibility: hidden;
}
.svg_select_points_l,
.svg_select_points_r {
cursor: ew-resize;
opacity: 1;
visibility: visible;
fill: #888;
}
.apexcharts-canvas.apexcharts-zoomable .hovering-zoom {
cursor: crosshair
}
.apexcharts-canvas.apexcharts-zoomable .hovering-pan {
cursor: move
}
.apexcharts-zoom-icon,
.apexcharts-zoomin-icon,
.apexcharts-zoomout-icon,
.apexcharts-reset-icon,
.apexcharts-pan-icon,
.apexcharts-selection-icon,
.apexcharts-menu-icon,
.apexcharts-toolbar-custom-icon {
cursor: pointer;
width: 20px;
height: 20px;
line-height: 24px;
color: #6E8192;
text-align: center;
}
.apexcharts-zoom-icon svg,
.apexcharts-zoomin-icon svg,
.apexcharts-zoomout-icon svg,
.apexcharts-reset-icon svg,
.apexcharts-menu-icon svg {
fill: #6E8192;
}
.apexcharts-selection-icon svg {
fill: #444;
transform: scale(0.76)
}
.apexcharts-theme-dark .apexcharts-zoom-icon svg,
.apexcharts-theme-dark .apexcharts-zoomin-icon svg,
.apexcharts-theme-dark .apexcharts-zoomout-icon svg,
.apexcharts-theme-dark .apexcharts-reset-icon svg,
.apexcharts-theme-dark .apexcharts-pan-icon svg,
.apexcharts-theme-dark .apexcharts-selection-icon svg,
.apexcharts-theme-dark .apexcharts-menu-icon svg,
.apexcharts-theme-dark .apexcharts-toolbar-custom-icon svg {
fill: #f3f4f5;
}
.apexcharts-canvas .apexcharts-zoom-icon.apexcharts-selected svg,
.apexcharts-canvas .apexcharts-selection-icon.apexcharts-selected svg,
.apexcharts-canvas .apexcharts-reset-zoom-icon.apexcharts-selected svg {
fill: #008FFB;
}
.apexcharts-theme-light .apexcharts-selection-icon:not(.apexcharts-selected):hover svg,
.apexcharts-theme-light .apexcharts-zoom-icon:not(.apexcharts-selected):hover svg,
.apexcharts-theme-light .apexcharts-zoomin-icon:hover svg,
.apexcharts-theme-light .apexcharts-zoomout-icon:hover svg,
.apexcharts-theme-light .apexcharts-reset-icon:hover svg,
.apexcharts-theme-light .apexcharts-menu-icon:hover svg {
fill: #333;
}
.apexcharts-selection-icon,
.apexcharts-menu-icon {
position: relative;
}
.apexcharts-reset-icon {
margin-left: 5px;
}
.apexcharts-zoom-icon,
.apexcharts-reset-icon,
.apexcharts-menu-icon {
transform: scale(0.85);
}
.apexcharts-zoomin-icon,
.apexcharts-zoomout-icon {
transform: scale(0.7)
}
.apexcharts-zoomout-icon {
margin-right: 3px;
}
.apexcharts-pan-icon {
transform: scale(0.62);
position: relative;
left: 1px;
top: 0px;
}
.apexcharts-pan-icon svg {
fill: #fff;
stroke: #6E8192;
stroke-width: 2;
}
.apexcharts-pan-icon.apexcharts-selected svg {
stroke: #008FFB;
}
.apexcharts-pan-icon:not(.apexcharts-selected):hover svg {
stroke: #333;
}
.apexcharts-toolbar {
position: absolute;
z-index: 11;
max-width: 176px;
text-align: right;
border-radius: 3px;
padding: 0px 6px 2px 6px;
display: flex;
justify-content: space-between;
align-items: center;
}
.apexcharts-menu {
background: #fff;
position: absolute;
top: 100%;
border: 1px solid #ddd;
border-radius: 3px;
padding: 3px;
right: 10px;
opacity: 0;
min-width: 110px;
transition: 0.15s ease all;
pointer-events: none;
}
.apexcharts-menu.apexcharts-menu-open {
opacity: 1;
pointer-events: all;
transition: 0.15s ease all;
}
.apexcharts-menu-item {
padding: 6px 7px;
font-size: 12px;
cursor: pointer;
}
.apexcharts-theme-light .apexcharts-menu-item:hover {
background: #eee;
}
.apexcharts-theme-dark .apexcharts-menu {
background: rgba(0, 0, 0, 0.7);
color: #fff;
}
@media screen and (min-width: 768px) {
.apexcharts-canvas:hover .apexcharts-toolbar {
opacity: 1;
}
}
.apexcharts-datalabel.apexcharts-element-hidden {
opacity: 0;
}
.apexcharts-pie-label,
.apexcharts-datalabels,
.apexcharts-datalabel,
.apexcharts-datalabel-label,
.apexcharts-datalabel-value {
cursor: default;
pointer-events: none;
}
.apexcharts-pie-label-delay {
opacity: 0;
animation-name: opaque;
animation-duration: 0.3s;
animation-fill-mode: forwards;
animation-timing-function: ease;
}
.apexcharts-canvas .apexcharts-element-hidden {
opacity: 0;
}
.apexcharts-hide .apexcharts-series-points {
opacity: 0;
}
.apexcharts-gridline,
.apexcharts-annotation-rect,
.apexcharts-tooltip .apexcharts-marker,
.apexcharts-area-series .apexcharts-area,
.apexcharts-line,
.apexcharts-zoom-rect,
.apexcharts-toolbar svg,
.apexcharts-annotations-rects,
.apexcharts-area-series .apexcharts-series-markers .apexcharts-marker.no-pointer-events,
.apexcharts-line-series .apexcharts-series-markers .apexcharts-marker.no-pointer-events,
.apexcharts-radar-series path,
.apexcharts-radar-series polygon {
pointer-events: none;
}
/* markers */
.apexcharts-marker {
transition: 0.15s ease all;
}
@keyframes opaque {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
/* Resize generated styles */
@keyframes resizeanim {
from {
opacity: 0;
}
to {
opacity: 0;
}
}
.resize-triggers {
animation: 1ms resizeanim;
visibility: hidden;
opacity: 0;
}
.resize-triggers,
.resize-triggers>div,
.contract-trigger:before {
content: " ";
display: block;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow: hidden;
}
.resize-triggers>div {
background: #eee;
overflow: auto;
}
.contract-trigger:before {
width: 200%;
height: 200%;
}

14
static/apexcharts.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -19,12 +19,18 @@
<li class="mr-2"> <li class="mr-2">
<a class="text-blue-500 hover:text-blue-800" href="/transactions">Transactions</a> <a class="text-blue-500 hover:text-blue-800" href="/transactions">Transactions</a>
</li> </li>
<li class="mr-2">
<a class="text-blue-500 hover:text-blue-800" href="/transactions?unsorted=1">Unsorted Transactions</a>
</li>
<li class="mr-2"> <li class="mr-2">
<a class="text-blue-500 hover:text-blue-800" href="/categories">Categories</a> <a class="text-blue-500 hover:text-blue-800" href="/categories">Categories</a>
</li> </li>
<li class="mr-2">
<a class="text-blue-500 hover:text-blue-800" href="/graph">Monthly Graph</a>
</li>
</ul> </ul>
</div> </div>
<div class="container mx-auto"> <div class="pt-2 container mx-auto">
{{!base}} {{!base}}
</div> </div>

4
views/graph.tpl Normal file
View file

@ -0,0 +1,4 @@
% rebase("base.tpl")
<h1 class="text-2xl font-bold text-indigo-500">Last 12 months</h1>

View file

@ -2,18 +2,25 @@
<h1 class="text-2xl font-bold text-indigo-500">Monatsübersicht</h1> <h1 class="text-2xl font-bold text-indigo-500">Monatsübersicht</h1>
<table class="table-auto border-none">
<thead> % if len(categories) > 0:
<tr> <table class="table-auto border-none">
<th class="px-4 py-2">Categorie</th> <thead>
<th class="px-4 py-2 text-right">Summe</th> <tr>
</tr> <th class="px-4 py-2">Categorie</th>
</thead> <th class="px-4 py-2 text-right">Summe</th>
<tbody> </tr>kk
% for c in categories: </thead>
<tr> <tbody>
<td class="border px-4 py-2">{{ c["name"] }}</td> % for c in categories:
<td class="border px-4 py-2 text-right {{ 'text-green-500' if c['amount'] > 0 else 'text-red-500' }}">{{ c["amount"] }} €</td> <tr>
</tr> <td class="border px-4 py-2">{{ c["name"] }}</td>
<td class="border px-4 py-2 text-right {{ 'text-red-500' if c['amount'][0] == "-" else 'text-green-500' }}">{{ c["amount"] }} €</td>
</tr>
% end
</tbody>
% else:
No transactions this month
% end % end
</tbody>

View file

@ -14,7 +14,7 @@
<th class="px-4 py-2">Date</th> <th class="px-4 py-2">Date</th>
<th class="px-4 py-2">Sender/Empfänger</th> <th class="px-4 py-2">Sender/Empfänger</th>
<th class="px-4 py-2">Verwendungszweck</th> <th class="px-4 py-2">Verwendungszweck</th>
<th class="px-4 py-2">Betrag</th> <th class="px-4 py-2 text-right">Betrag</th>
<th class="px-4 py-2">Kategorie</th> <th class="px-4 py-2">Kategorie</th>
</tr> </tr>
</thead> </thead>
@ -25,7 +25,7 @@
<td class="border px-4 py-2">{{ t.get_date("de") }}</td> <td class="border px-4 py-2">{{ t.get_date("de") }}</td>
<td class="border px-4 py-2">{{ t.name }}<br/> <span class="text-gray-500">{{t.iban}}</span></td> <td class="border px-4 py-2">{{ t.name }}<br/> <span class="text-gray-500">{{t.iban}}</span></td>
<td class="border px-4 py-2">{{ t.description }}</td> <td class="border px-4 py-2">{{ t.description }}</td>
<td class="border px-4 py-2 {{ 'text-green-500' if t.is_positive() else 'text-red-500' }}">{{ t.pretty_amount() }}</td> <td class="border px-4 py-2 text-right {{ 'text-green-500' if t.is_positive() else 'text-red-500' }}">{{ t.pretty_amount() }}</td>
% if t.category: % if t.category:
<td class="border px-4 py-2"> <a href="/category/{{ t.category.name }}"> {{ t.category.full_name()}}</a></td> <td class="border px-4 py-2"> <a href="/category/{{ t.category.name }}"> {{ t.category.full_name()}}</a></td>
% else: % else: