super().__init__(*args, **kwargs)
+class LedgerTextLine:
+
+ def __init__(self, text_line):
+ self.text_line = text_line
+ self.comment = ''
+ split_by_comment = text_line.rstrip().split(sep=';', maxsplit=1)
+ self.non_comment = split_by_comment[0].rstrip()
+ self.empty = len(split_by_comment) == 1 and len(self.non_comment) == 0
+ if self.empty:
+ return
+ if len(split_by_comment) == 2:
+ self.comment = split_by_comment[1].lstrip()
+
+
+
# html_head = """
# <style>
# body { color: #000000; }
def __init__(self):
self.money_dict = {}
- def add_money_dict(self, moneys):
+ def __iadd__(self, moneys):
+ money_dict = moneys
+ if type(moneys) == Wealth:
+ moneys = moneys.money_dict
for currency, amount in moneys.items():
if not currency in self.money_dict.keys():
self.money_dict[currency] = 0
self.money_dict[currency] += amount
+ return self
- def add_wealth(self, moneys):
- self.add_money_dict(moneys.money_dict)
+ @property
+ def sink_empty(self):
+ return len(self.as_sink) == 0
@property
- def as_sink_dict(self):
+ def as_sink(self):
sink = {}
for currency, amount in self.money_dict.items():
if 0 == amount:
self.children = []
def add_wealth(self, moneys):
- self.own_moneys.add_wealth(moneys)
+ self.own_moneys += moneys
@property
def full_name(self):
@property
def full_moneys(self):
full_moneys = Wealth()
- full_moneys.add_wealth(self.own_moneys)
+ full_moneys += self.own_moneys
for child in self.children:
- full_moneys.add_wealth(child.full_moneys)
+ full_moneys += child.full_moneys
return full_moneys
# return account_tree, account_sums
-def parse_lines2(lines, ignore_editable_exceptions=False):
- lines = lines.copy() + [''] # to simulate ending of last booking
+def parse_lines_to_bookings(lines, ignore_editable_exceptions=False):
+ lines = [LedgerTextLine(line) for line in lines]
+ lines += [LedgerTextLine('')] # to simulate ending of last booking
bookings = []
inside_booking = False
booking_lines = []
last_date = ''
for i, line in enumerate(lines):
intro = f'file line {i}'
- stripped_line = line.rstrip()
- if stripped_line == '':
+ if line.empty:
if inside_booking:
booking = Booking(lines=booking_lines, starts_at=booking_start_i)
if last_date > booking.date and not ignore_editable_exceptions:
self.currency = currency
self.comment = comment
if line:
- split_by_comment = line.rstrip().split(sep=';', maxsplit=1)
- if len(split_by_comment) == 0:
+ if line.empty:
raise PlomException('line empty')
- elif len(split_by_comment) == 2:
- self.comment = split_by_comment[1].lstrip()
- non_comment = split_by_comment[0].rstrip()
- toks = non_comment.split()
+ self.comment = line.comment
+ toks = line.non_comment.split()
if (len(toks) not in {1, 3}):
if validate:
raise PlomException(f'number of non-comment tokens not 1 or 3')
if validate:
raise PlomException(f'invalid token for Decimal: {toks[1]}')
else:
- self.amount = toks[1]
+ self.comment = f'unparsed: {toks[1]}; {self.comment}'
self.currency = toks[2]
@property
else:
return f'{self.amount:.2f}'
+ @property
def for_writing(self):
line = f' {self.account}'
if self.amount is not None:
self.intro = f'booking starting at line {self.starts_at}'
self.clean()
if lines:
- self.real_lines = lines
+ self.lines = lines
self.parse_lines()
else:
self.date = date
if len(self.transfer_lines) < 2 and self.validate:
raise PlomException(f'{self.intro}: too few transfer lines')
self.calculate_account_changes()
- self.real_lines = self.for_writing()
+ self.lines = [LedgerTextLine(l) for l in self.for_writing]
@classmethod
def from_postvars(cls, postvars, starts_at='?', validate=True):
self.account_changes = {}
def parse_lines(self):
- self.top_comment = ''
- if len(self.real_lines) < 3 and self.validate:
- raise PlomException(f'{self.intro}: ends with less than 3 lines:' + str(self.real_lines))
- split_by_comment = self.real_lines[0].rstrip().split(sep=";", maxsplit=1)
- if len(split_by_comment) == 0 and self.validate:
+ if len(self.lines) < 3 and self.validate:
+ raise PlomException(f'{self.intro}: ends with less than 3 lines:' + str(self.lines))
+ top_line = self.lines[0]
+ if top_line.empty and self.validate:
raise PlomException('{self.intro}: headline empty')
- elif len(split_by_comment) == 2 and self.validate:
- self.top_comment = split_by_comment[1].lstrip()
- non_comment = split_by_comment[0].rstrip()
- toks = non_comment.split()
+ self.top_comment = top_line.comment
+ toks = top_line.non_comment.split(maxsplit=1)
if len(toks) < 2:
if self.validate:
raise PlomException(f'{self.intro}: headline missing elements: {non_comment}')
self.date = toks[0]
self.description = toks[1]
self.validate_head()
- for i, line in enumerate(self.real_lines[1:]):
+ for i, line in enumerate(self.lines[1:]):
try:
self.transfer_lines += [TransferLine(line, validate=self.validate)]
except PlomException as e:
if not transfer_line.account in self.account_changes.keys():
self.account_changes[transfer_line.account] = Wealth()
money = {transfer_line.currency: transfer_line.amount}
- self.account_changes[transfer_line.account].add_money_dict(money)
- money_changes.add_money_dict(money)
- if sink_account is None and len(money_changes.as_sink_dict) > 0 and self.validate:
- raise PlomException(f'{intro}: does not balance (unused non-empty sink)')
+ self.account_changes[transfer_line.account] += money
+ money_changes += money
+ if sink_account is None and (not money_changes.sink_empty) and self.validate:
+ raise PlomException(f'{intro}: does not balance (undeclared non-empty sink)')
if sink_account is not None:
if not sink_account in self.account_changes.keys():
self.account_changes[sink_account] = Wealth()
- self.account_changes[sink_account].add_money_dict(money_changes.as_sink_dict)
+ self.account_changes[sink_account] += money_changes.as_sink
+
+ @property
+ def for_writing(self):
+ lines = [f'{self.date} {self.description}']
+ if self.top_comment is not None and self.top_comment.rstrip() != '':
+ lines[0] += f' ; {self.top_comment}'
+ for line in self.transfer_lines:
+ lines += [line.for_writing]
+ return lines
+
+ @property
+ def comment_cols(self):
+ return max(20, len(self.top_comment))
def validate_head(self):
if not self.validate:
for i, line in enumerate(self.transfer_lines):
if line.amount is None:
for currency, amount in self.account_changes[line.account].money_dict.items():
- replacement_lines += [TransferLine(None, f'{line.account}', amount, currency).for_writing()]
+ replacement_lines += [TransferLine(None, f'{line.account}', amount, currency).for_writing]
break
- lines = self.real_lines[:i+1] + replacement_lines + self.real_lines[i+2:]
+ lines = self.lines[:i+1] + [LedgerTextLine(l) for l in replacement_lines] + self.lines[i+2:]
self.clean()
- self.real_lines = lines
+ self.lines = lines
self.parse_lines()
- def add_mirror(self):
+ def mirror(self):
new_transfer_lines = []
- for line in self.transfer_lines:
- new_transfer_lines += [TransferLine(None, f'({line.account})', line.amount, line.currency, line.comment)]
+ for transfer_line in self.transfer_lines:
+ uncommented_source = LedgerTextLine(transfer_line.for_writing).non_comment
+ comment = f're: {uncommented_source.lstrip()}'
+ new_account = '?'
+ new_transfer_lines += [TransferLine(None, new_account, -transfer_line.amount, transfer_line.currency, comment)]
for transfer_line in new_transfer_lines:
- self.real_lines += [transfer_line.for_writing()]
+ self.lines += [LedgerTextLine(transfer_line.for_writing)]
+ self.clean()
+ self.parse_lines()
+
+ def replace(self, replace_from, replace_to):
+ lines = []
+ for l in self.for_writing:
+ lines += [l.replace(replace_from, replace_to)]
+ self.lines = [LedgerTextLine(l) for l in lines]
self.clean()
self.parse_lines()
self.transfer_lines += [TransferLine(None, acc_assets, final_minus, '€')]
self.transfer_lines += [TransferLine(None, acc_buffer, -final_minus, '€')]
- def for_writing(self):
- lines = [f'{self.date} {self.description}']
- if self.top_comment is not None and self.top_comment.rstrip() != '':
- lines[0] += f' ; {self.top_comment}'
- for line in self.transfer_lines:
- lines += [line.for_writing()]
- return lines
-
- @property
- def comment_cols(self):
- return max(20, len(self.top_comment))
-
# class Booking:
#
self.prefix = prefix
self.bookings = []
self.comments = []
- self.real_lines = []
+ self.text_lines = []
super().__init__(db_path)
- self.bookings = parse_lines2(self.real_lines, ignore_editable_exceptions)
+ self.bookings = parse_lines_to_bookings(self.text_lines, ignore_editable_exceptions)
def read_db_file(self, f):
- self.real_lines += [l.rstrip() for l in f.readlines()] # TODO is this necessary? (parser already removes lines?)
+ self.text_lines += f.readlines()
+ # self.text_lines += [l.rstrip() for l in f.readlines()] # TODO is this necessary? (parser already removes lines?)
# def get_lines(self, start, end):
- # return self.real_lines[start:end]
+ # return self.text_lines[start:end]
# def write_db(self, text, mode='w'):
# if text[-1] != '\n':
# start_at = 0
# if len(self.bookings) > 0:
# if date >= self.bookings[-1].date_string:
- # start_at = len(self.real_lines)
+ # start_at = len(self.text_lines)
# lines = [''] + lines
# else:
# for b in self.bookings:
# start_at = b.start_line
# break
# lines += [''] # DEBUG is new
- # return self.write_lines_in_total_lines_at(self.real_lines, start_at, lines)
+ # return self.write_lines_in_total_lines_at(self.text_lines, start_at, lines)
# def update(self, start, end, lines, date):
- # remaining_lines = self.real_lines[:start] + self.real_lines[end:]
+ # remaining_lines = self.text_lines[:start] + self.text_lines[end:]
# n_original_lines = end - start
# start_at = len(remaining_lines)
# for b in self.bookings:
# start_at -= n_original_lines
# elif b.date_string > date:
# break
- # # print("DEBUG update start_at", start_at, "len(remaining_lines)", len(remaining_lines), "len(self.real_lines)", len(self.real_lines), "end", end)
- # if start_at != 0 and end != len(self.real_lines) and start_at == len(remaining_lines):
+ # # print("DEBUG update start_at", start_at, "len(remaining_lines)", len(remaining_lines), "len(self.text_lines)", len(self.text_lines), "end", end)
+ # if start_at != 0 and end != len(self.text_lines) and start_at == len(remaining_lines):
# # Add empty predecessor line if appending.
# lines = [''] + lines
# return self.write_lines_in_total_lines_at(remaining_lines, start_at, lines)
break
else:
place_at = i + 1
- self.bookings = self.bookings[:place_at] + [booking] + self.bookings[place_at:]
+ self.bookings.insert(place_at, booking)
def add_taxes(self, lines, finish=False):
ret = []
# return ret
def ledger_as_html(self):
- for nth, booking in enumerate(self.bookings):
- booking.can_up = nth > 0 and self.bookings[nth - 1].date == booking.date
- booking.can_down = nth < len(self.bookings) - 1 and self.bookings[nth + 1].date == booking.date
+ for index, booking in enumerate(self.bookings):
+ booking.can_up = index > 0 and self.bookings[index - 1].date == booking.date
+ booking.can_down = index < len(self.bookings) - 1 and self.bookings[index + 1].date == booking.date
return j2env.get_template('ledger.html').render(bookings=self.bookings)
# def ledger_as_html(self):
# elements_to_write += [single_c_tmpl.render(c=c) for c in self.comments[last_i:] if c != ''] #
# return '\n'.join(elements_to_write)
- def balance_as_html(self, until=None):
- bookings = self.bookings[:(until if until is None else int(until)+1)]
+ def balance_as_html(self, until_after=None):
+ bookings = self.bookings[:(until_after if until_after is None else int(until_after)+1)]
account_trunk = Account('', None)
accounts = {account_trunk.full_name: account_trunk}
for booking in bookings:
# content = "\n".join(lines)
# return f"<pre>{content}</pre>"
- def edit(self, index, sent=None, error_msg=None, edit_mode='table'):
+ def edit(self, index, sent=None, error_msg=None, edit_mode='table', copy=False):
accounts = set()
if sent or -1 == index:
content = sent if sent else ([] if 'textarea'==edit_mode else None)
else:
content = self.bookings[index]
+ if copy:
+ content.date = str(datetime.now())[:10]
if 'textarea' == edit_mode and content:
- content = content.for_writing()
+ content = content.for_writing
else:
for booking in self.bookings:
for transfer_line in booking.transfer_lines:
accounts.add(transfer_line.account)
- return j2env.get_template('edit.html').render(content=content, index=index, error_msg=error_msg, edit_mode=edit_mode, accounts=accounts)
+ return j2env.get_template('edit.html').render(content=content, index=index, error_msg=error_msg, edit_mode=edit_mode, accounts=accounts, adding=(copy or -1 == index))
# def add_free(self, start=0, end=0, copy=False):
# tmpl = jinja2.Template(add_form_header + add_free_html + add_form_footer)
# end=end)
# return content
- def move_up(self, nth):
- return self.move(nth, -1)
+ def move_up(self, index):
+ return self.move(index, -1)
- def move_down(self, nth):
- return self.move(nth, +1)
+ def move_down(self, index):
+ return self.move(index, +1)
- def move(self, nth, direction):
- to_move = self.bookings[nth]
- swap_nth = nth+1*(direction)
- to_swap = self.bookings[swap_nth]
- self.bookings[nth] = to_swap
- self.bookings[nth+1*(direction)] = to_move
- return swap_nth
+ def move(self, index, direction):
+ to_move = self.bookings[index]
+ swap_index = index + 1*(direction)
+ to_swap = self.bookings[swap_index]
+ self.bookings[index] = to_swap
+ self.bookings[index + 1*(direction)] = to_move
+ return swap_index
def write_db(self):
lines = []
for i, booking in enumerate(self.bookings):
if i > 0:
lines += ['']
- lines += booking.for_writing()
+ lines += booking.for_writing
self.write_text_to_db('\n'.join(lines) + '\n')
# def move_up(self, start, end):
# # FIXME currently broken due to changed self.write_lines_in_total_lines_at, easy fix would be lines += [""] maybe?
# lines = self.get_lines(start, end)
# if start == 0:
- # total_lines = self.real_lines[end+1:]
+ # total_lines = self.text_lines[end+1:]
# lines = [''] + lines
# start_at += 1
# else:
- # total_lines = self.real_lines[:start-1] + self.real_lines[end:] # -1 because we reduce the original position's two empty limit lines to one in-between line
+ # total_lines = self.text_lines[:start-1] + self.text_lines[end:] # -1 because we reduce the original position's two empty limit lines to one in-between line
# lines += ['']
# self.write_lines_in_total_lines_at(total_lines, start_at, lines)
db = LedgerDB(prefix, ignore_editable_exceptions=True)
index = 0
parsed_url = urlparse(self.path)
- for string in {'save', 'copy', 'check', 'mirror', 'fill sink', 'as textarea', 'as table', 'move up', 'move down', 'add taxes'}:
+ for string in {'update', 'add', 'check', 'mirror', 'fill_sink', 'textarea', 'table', 'move_up', 'move_down', 'add_taxes', 'replace'}:
if string in postvars.keys():
submit_button = string
break
- if prefix + '/ledger' == parsed_url.path:
- if submit_button == 'move up':
- index = db.move_up(int(postvars['move up'][0]))
- elif submit_button == 'move down':
- index = db.move_down(int(postvars['move down'][0]))
+ if f'{prefix}/ledger' == parsed_url.path and submit_button in {'move_up', 'move_down'}:
+ mover = getattr(db, submit_button)
+ index = mover(int(postvars[submit_button][0]))
elif prefix + '/edit' == parsed_url.path:
index = int(postvars['index'][0])
- starts_at = '?' if index == -1 else db.bookings[index].starts_at
edit_mode = postvars['edit_mode'][0]
validate = submit_button in {'save', 'copy', 'check'}
+ starts_at = '?' if index == -1 else db.bookings[index].starts_at
if 'textarea' == edit_mode:
- lines = postvars['booking'][0].rstrip().split('\n')
+ lines = [LedgerTextLine(line) for line in postvars['booking'][0].rstrip().split('\n')]
booking = Booking(lines, starts_at, validate=validate)
else:
booking = Booking.from_postvars(postvars, starts_at, validate)
- if submit_button in {'save', 'copy'}:
- if index != -1 and submit_button != 'copy':
- if booking.date == db.bookings[index].date:
- db.bookings[index] = booking
- booking_is_placed = True
- else:
- db.bookings = db.bookings[:index] + db.bookings[index+1:]
- db.insert_booking_at_date(booking)
+ if submit_button in {'update', 'add'}:
+ if submit_button == 'update':
+ if 'textarea' == edit_mode and 'delete' == ''.join([l.text_line for l in lines]).strip():
+ del db.bookings[index]
+ # if not creating new Booking, and date unchanged, keep it in place
+ elif booking.date == db.bookings[index].date:
+ db.bookings[index] = booking
+ else:
+ del db.bookings[index]
+ db.insert_booking_at_date(booking)
else:
db.insert_booking_at_date(booking)
- else:
+ else: # non-DB-writing calls
error_msg = None
if 'check' == submit_button:
error_msg = 'All looks fine!'
- elif 'mirror' == submit_button:
- booking.add_mirror()
- elif 'fill sink' == submit_button:
- booking.fill_sink()
- elif 'add taxes' == submit_button:
- booking.add_taxes()
- elif submit_button in {'as textarea', 'as table'}:
- edit_mode = submit_button[len('as '):]
+ elif submit_button in {'mirror', 'fill_sink', 'add_taxes'}:
+ getattr(booking, submit_button)()
+ elif 'replace' == submit_button:
+ booking.replace(postvars['replace_from'][0], postvars['replace_to'][0])
+ elif submit_button in {'textarea', 'table'}:
+ edit_mode = submit_button
page = db.edit(index, booking, error_msg=error_msg, edit_mode=edit_mode)
self.send_HTML(page)
return
try:
db = LedgerDB(prefix=prefix)
except EditableException as e:
+ # We catch the EditableException for further editing, and then
+ # re-run the DB initiation without it blocking DB creation.
db = LedgerDB(prefix=prefix, ignore_editable_exceptions=True)
page = db.edit(index=e.booking_index, error_msg=f'ERROR: {e}')
self.send_HTML(page)
return
parsed_url = urlparse(self.path)
params = parse_qs(parsed_url.query)
- if parsed_url.path == prefix + '/balance':
- stop = params.get('stop', [None])[0]
+ if parsed_url.path == f'{prefix}/balance':
+ stop = params.get('until_after', [None])[0]
page = db.balance_as_html(stop)
- elif parsed_url.path == prefix + '/edit':
+ elif parsed_url.path == f'{prefix}/edit':
index = params.get('i', [-1])[0]
- page = db.edit(int(index))
+ copy = params.get('copy', [0])[0]
+ page = db.edit(int(index), copy=bool(copy))
else:
page = db.ledger_as_html()
self.send_HTML(page)