home · contact · privacy
Improve ledger.py.
authorChristian Heller <c.heller@plomlompom.de>
Sun, 29 Oct 2023 04:47:58 +0000 (05:47 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Sun, 29 Oct 2023 04:47:58 +0000 (05:47 +0100)
ledger.py

index 27675da2778e2d7cd94c0354fa7c93848b6eef05..0785b1a7559869c074080b0775d16f1a81c2cb3a 100755 (executable)
--- a/ledger.py
+++ b/ledger.py
@@ -3,6 +3,7 @@ import sys
 import os
 import html
 import jinja2 
 import os
 import html
 import jinja2 
+import decimal
 from urllib.parse import parse_qs, urlparse
 hostName = "localhost"
 serverPort = 8082
 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):
 
 
 def add_taxes(lines):
-    import decimal
     bookings, _ = parse_lines(lines)
     _, account_sums = bookings_to_account_tree(bookings)
     expenses_so_far = -1 * account_sums['Assets']['€']
     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 
     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:
     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
             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
             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_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)
     # 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 
     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]
 
 
     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
 
 def parse_lines(lines):
     import datetime
-    import decimal
     inside_booking = False
     date_string, description = None, None
     booking_lines = []
     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.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):
         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:
         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():
         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)
         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
 
 
         return account_changes
 
 
@@ -458,6 +461,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)
             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
         else:
             page += self.ledger_as_html(db)
         page += self.footer
@@ -487,6 +492,69 @@ input[type=number] { text-align: right; font-family: monospace; }
         content = "\n".join(lines)
         return f"<pre>{content}</pre>"
 
         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>{{date}} {{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("""
     def ledger_as_html(self, db):
         single_c_tmpl = jinja2.Template('<span class="comment">{{c|e}}</span><br />')
         booking_tmpl = jinja2.Template("""
@@ -518,10 +586,10 @@ input[type=number] { text-align: right; font-family: monospace; }
                 if booking_line == '':
                     booking_lines += [{'acc': None, 'money': None, 'comment': comment}]
                     continue
                 if booking_line == '':
                     booking_lines += [{'acc': None, 'money': None, 'comment': comment}]
                     continue
+                account = booking_line[0] 
                 money = ''
                 money = ''
-                if booking_line[1]:
+                if booking_line[1] is not None:
                     money = f'{booking_line[1]} {booking_line[2]}'
                     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(
                 start=booking.start_line,
                 booking_lines += [{'acc': booking_line[0], 'money':money, 'comment':comment}] 
             elements_to_write += [booking_tmpl.render(
                 start=booking.start_line,
@@ -530,6 +598,7 @@ input[type=number] { text-align: right; font-family: monospace; }
                 desc=booking.description,
                 head_comment=db.comments[booking.start_line],
                 booking_lines = booking_lines)]
                 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):
         return '\n'.join(elements_to_write) 
 
     def add_free(self, db, start=0, end=0, copy=False):
@@ -563,7 +632,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" />
 <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]" />
 <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]" />