home · contact · privacy
Improve ledger.py
[misc] / ledger.py
index 27675da2778e2d7cd94c0354fa7c93848b6eef05..ee6ed807ef72b4a9601acf32d25fad6ec1e0757a 100755 (executable)
--- a/ledger.py
+++ b/ledger.py
@@ -3,6 +3,7 @@ import sys
 import os
 import html
 import jinja2 
+import decimal
 from urllib.parse import parse_qs, urlparse
 hostName = "localhost"
 serverPort = 8082
@@ -27,7 +28,6 @@ def apply_booking_to_account_balances(account_sums, account, currency, amount):
 
 
 def add_taxes(lines):
-    import decimal
     bookings, _ = parse_lines(lines)
     _, account_sums = bookings_to_account_tree(bookings)
     expenses_so_far = -1 * account_sums['Assets']['€']
@@ -36,10 +36,10 @@ def add_taxes(lines):
     left_over = needed_income_before_krankenkasse - ESt_this_month
     too_low = 0
     too_high = 2 * needed_income_before_krankenkasse 
-    E0 = 10908
-    E1 = 15999 
-    E2 = 62809 
-    E3 = 277825 
+    E0 = decimal.Decimal(10908)
+    E1 = decimal.Decimal(15999)
+    E2 = decimal.Decimal(62809) 
+    E3 = decimal.Decimal(277825) 
     while True:
         zvE = 12 * needed_income_before_krankenkasse
         if zvE < E0:
@@ -56,27 +56,29 @@ def add_taxes(lines):
             ESt = decimal.Decimal(0.45) * (zvE - decimal.Decimal(277825)) + decimal.Decimal(106713.52) 
         ESt_this_month = ESt / 12
         left_over = needed_income_before_krankenkasse - ESt_this_month
-        if abs(left_over - expenses_so_far) < 0.1:
+        if abs(left_over - expenses_so_far) < 0.001:
             break
         elif left_over < expenses_so_far:
             too_low = needed_income_before_krankenkasse
         elif left_over > expenses_so_far:
             too_high = needed_income_before_krankenkasse
         needed_income_before_krankenkasse = too_low + (too_high - too_low)/2
-    line_income_tax = f'  Reserves:Einkommenssteuer  {ESt_this_month:.2f}€ ; expenses so far: {expenses_so_far:.2f}€; zvE: {zvE:.2f}€; ESt total: {ESt:.2f}€; needed before Krankenkasse: {needed_income_before_krankenkasse:.2f}€'
-    kk_minimum_income = 1096.67 
+    ESt_this_month = ESt_this_month.quantize(decimal.Decimal('0.00'))
+    line_income_tax = f'  Reserves:Einkommenssteuer  {ESt_this_month}€ ; expenses so far: {expenses_so_far:.2f}€; zvE: {zvE:.2f}€; ESt total: {ESt:.2f}€; needed before Krankenkasse: {needed_income_before_krankenkasse:.2f}€'
+    kk_minimum_income = decimal.Decimal(1096.67) 
     kk_factor = decimal.Decimal(0.189) 
-    kk_minimum_tax = decimal.Decimal(207.27)
+    kk_minimum_tax = decimal.Decimal(207.27).quantize(decimal.Decimal('0.00'))
     # kk_minimum_income = 1131.67 
     # kk_factor = decimal.Decimal(0.191) 
     # kk_minimum_tax = decimal.Decimal(216.15)
     # kk_factor = decimal.Decimal(0.197) 
     # kk_minimum_tax = decimal.Decimal(222.94)
     kk_add = max(0, kk_factor * needed_income_before_krankenkasse - kk_minimum_tax)
-    line_kk_minimum = f'  Reserves:Month:Krankenkassendefaultbeitrag  {kk_minimum_tax:.2f}€  ; assed minimum income {kk_minimum_income:.2f}€ * {kk_factor:.3f}'
-    line_kk_add = f'  Reserves:Month:Krankenkassenbeitragswachstum {kk_add:.2f}€  ; max(0, {kk_factor:.3f} * {needed_income_before_krankenkasse:.2f}€ - {kk_minimum_tax:.2f}€)'
+    kk_add = decimal.Decimal(kk_add).quantize(decimal.Decimal('0.00'))
+    line_kk_minimum = f'  Reserves:Month:Krankenkassendefaultbeitrag  {kk_minimum_tax}€  ; assumed minimum income {kk_minimum_income:.2f}€ * {kk_factor:.3f}'
+    line_kk_add = f'  Reserves:Month:Krankenkassenbeitragswachstum {kk_add}€  ; max(0, {kk_factor:.3f} * {needed_income_before_krankenkasse:.2f}€ - {kk_minimum_tax}€)'
     final_minus = expenses_so_far + ESt_this_month + kk_minimum_tax + kk_add 
-    line_finish = f'  Assets  -{final_minus:.2f}€'
+    line_finish = f'  Assets  -{ESt_this_month + kk_minimum_tax + kk_add} € ; -{final_minus}€'
     return [line_income_tax, line_kk_minimum, line_kk_add, line_finish]
 
 
@@ -126,7 +128,6 @@ def bookings_to_account_tree(bookings):
 
 def parse_lines(lines):
     import datetime
-    import decimal
     inside_booking = False
     date_string, description = None, None
     booking_lines = []
@@ -254,6 +255,7 @@ class Booking:
         self.lines = booking_lines
         self.start_line = start_line
         self.validate_booking_lines()
+        self.sink = {}
         self.account_changes = self.parse_booking_lines_to_account_changes()
 
     def validate_booking_lines(self):
@@ -275,7 +277,7 @@ class Booking:
         if empty_values == 0:
             for k, v in sums.items():
                 if v != 0:
-                    raise HandledException(f"{prefix} does not sum up to zero")
+                    raise HandledException(f"{prefix} does not add up to zero")
         else:
             sinkable = False
             for k, v in sums.items():
@@ -303,6 +305,7 @@ class Booking:
         if sink_account:
             for currency, amount in debt.items():
                 apply_booking_to_account_balances(account_changes, sink_account, currency, -amount)
+                self.sink[currency] = -amount
         return account_changes
 
 
@@ -419,10 +422,11 @@ input[type=number] { text-align: right; font-family: monospace; }
             if 'save' in postvars.keys():
                 if start == end == 0:
                     db.append(lines)
+                    redir_url = f'/#last'
                 else:
                     db.replace(start, end, lines)
+                    redir_url = f'/#{start}'
                 self.send_response(301)
-                redir_url = '/'
                 self.send_header('Location', redir_url)
                 self.end_headers()
             else:
@@ -458,6 +462,8 @@ input[type=number] { text-align: right; font-family: monospace; }
             page += self.add_free(db, start, end, copy=True)
         elif parsed_url.path == '/copy_structured':
             page += self.add_structured(db, start, end, copy=True)
+        elif parsed_url.path == '/ledger2':
+            page += self.ledger2_as_html(db)
         else:
             page += self.ledger_as_html(db)
         page += self.footer
@@ -487,10 +493,73 @@ input[type=number] { text-align: right; font-family: monospace; }
         content = "\n".join(lines)
         return f"<pre>{content}</pre>"
 
+    def ledger2_as_html(self, db):
+        single_c_tmpl = jinja2.Template('<span class="comment">{{c|e}}</span><br />')
+        booking_tmpl = jinja2.Template("""
+<p id="{{start}}"><a href="#{{start}}">{{date}}</a> {{desc}} <span class="comment">{{head_comment|e}}</span>
+[edit: <a href="/add_structured?start={{start}}&end={{end}}">structured</a> 
+/ <a href="/add_free?start={{start}}&end={{end}}">free</a> 
+| copy:<a href="/copy_structured?start={{start}}&end={{end}}">structured</a>
+/ <a href="/copy_free?start={{start}}&end={{end}}">free</a>]
+<table>
+{% for l in booking_lines %}
+<tr><td>{{l.acc|e}}</td><td class="money">{{l.money|e}}</td><td class="money">{{l.balance|e}}</td></tr>
+{% endfor %}
+</table></p>
+""")
+        elements_to_write = []
+        account_sums = {}
+        for booking in db.bookings:
+            i = booking.start_line
+            booking_end = booking.start_line + len(booking.lines)
+            booking_lines = []
+            for booking_line in booking.lines[1:]:
+                if booking_line == '':
+                    continue
+                account = booking_line[0] 
+                account_toks = account.split(':') 
+                path = ''
+                for tok in account_toks:
+                    path += tok
+                    if not path in account_sums.keys():
+                        account_sums[path] = {}
+                    path += ':' 
+                moneys = []
+                money = ''
+                if booking_line[1] is not None:
+                    moneys += [(booking_line[1], booking_line[2])]
+                    money = f'{moneys[0][0]} {moneys[0][1]}'
+                else:
+                    for currency, amount in booking.sink.items():
+                        moneys += {(amount, currency)} 
+                    money = '['
+                    for m in moneys:
+                        money += f'{m[0]} {m[1]} '
+                    money += ']'
+                balance = ''
+                for amount, currency in moneys:
+                    path = ''
+                    for tok in account_toks:
+                        path += tok
+                        if not currency in account_sums[path].keys():
+                            account_sums[path][currency] = 0
+                        account_sums[path][currency] += amount 
+                        path += ':' 
+                    balance += f'{account_sums[account][currency]} {currency}' 
+                booking_lines += [{'acc': booking_line[0], 'money':money, 'balance':balance}] 
+            elements_to_write += [booking_tmpl.render(
+                start=booking.start_line,
+                end=booking_end,
+                date=booking.date_string,
+                desc=booking.description,
+                head_comment=db.comments[booking.start_line],
+                booking_lines = booking_lines)]
+        return '\n'.join(elements_to_write) 
+
     def ledger_as_html(self, db):
         single_c_tmpl = jinja2.Template('<span class="comment">{{c|e}}</span><br />')
         booking_tmpl = jinja2.Template("""
-<p>{{date}} {{desc}} <span class="comment">{{head_comment|e}}</span>
+<p id="{{start}}"><a {% if last %}id="last"{% endif %} href="#{{start}}">{{date}}</a> {{desc}} <span class="comment">{{head_comment|e}}</span>
 [edit: <a href="/add_structured?start={{start}}&end={{end}}">structured</a> 
 / <a href="/add_free?start={{start}}&end={{end}}">free</a> 
 | copy:<a href="/copy_structured?start={{start}}&end={{end}}">structured</a>
@@ -507,6 +576,7 @@ input[type=number] { text-align: right; font-family: monospace; }
 """)
         elements_to_write = []
         last_i = i = 0
+        last_start = db.bookings[-1].start_line
         for booking in db.bookings:
             i = booking.start_line
             elements_to_write += [single_c_tmpl.render(c=c) for c in db.comments[last_i:i] if c != '']
@@ -518,23 +588,25 @@ input[type=number] { text-align: right; font-family: monospace; }
                 if booking_line == '':
                     booking_lines += [{'acc': None, 'money': None, 'comment': comment}]
                     continue
+                account = booking_line[0] 
                 money = ''
-                if booking_line[1]:
+                if booking_line[1] is not None:
                     money = f'{booking_line[1]} {booking_line[2]}'
-                account = booking_line[0] 
                 booking_lines += [{'acc': booking_line[0], 'money':money, 'comment':comment}] 
             elements_to_write += [booking_tmpl.render(
+                last=booking.start_line == last_start,
                 start=booking.start_line,
                 end=booking_end,
                 date=booking.date_string,
                 desc=booking.description,
                 head_comment=db.comments[booking.start_line],
                 booking_lines = booking_lines)]
+        elements_to_write += [single_c_tmpl.render(c=c) for c in db.comments[last_i:] if c != '']
         return '\n'.join(elements_to_write) 
 
     def add_free(self, db, start=0, end=0, copy=False):
         tmpl = jinja2.Template("""
-<form method="POST" action="{{action}}">
+<form method="POST" action="{{action|e}}">
 <textarea name="booking" rows=10 cols=80>
 {% for line in lines %}{{ line }}
 {% endfor %}
@@ -547,7 +619,7 @@ input[type=number] { text-align: right; font-family: monospace; }
         lines = db.get_lines(start, end)
         if copy:
             start = end = 0
-        return tmpl.render(start=start, end=end, lines=lines) 
+        return tmpl.render(action='add_free', start=start, end=end, lines=lines) 
 
     def add_structured(self, db, start=0, end=0, copy=False, temp_lines=[], add_empty_line=None):
         tmpl = jinja2.Template("""
@@ -563,7 +635,7 @@ input[type=number] { text-align: right; font-family: monospace; }
 <br />
 {% for line in booking_lines %}
 <input name="line_{{line.i}}_account" value="{{line.acc|e}}" size=40 list="accounts" />
-<input type="number" name="line_{{line.i}}_amount" value="{{line.amt}}" size=10 />
+<input type="number" name="line_{{line.i}}_amount" step=0.01 value="{{line.amt}}" size=10 />
 <input name="line_{{line.i}}_currency" value="{{line.curr|e}}" size=3 list="currencies" />
 <textarea name="line_{{line.i}}_comment" rows=1 cols={% if line.comm_cols %}{{line.comm_cols}}{% else %}20{% endif %}>{{line.comment|e}}</textarea>
 <input type="submit" name="line_{{line.i}}_delete" value="[x]" />