+ amount = None
+ if len(postvars['amount'][i]) > 0:
+ amount = decimal.Decimal(postvars['amount'][i])
+ transfer_lines += [TransferLine(None, account, amount, postvars['currency'][i], postvars['comment'][i])]
+ return cls(None, starts_at, date, description, top_comment, transfer_lines, validate=validate)
+
+ def clean(self):
+ self.transfer_lines = []
+ self.account_changes = {}
+
+ def parse_lines(self):
+ 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')
+ 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}')
+ elif 0 == len(toks):
+ toks = 2*['']
+ else:
+ toks += ['']
+ self.date = toks[0]
+ self.description = toks[1]
+ self.validate_head()
+ for i, line in enumerate(self.lines[1:]):
+ try:
+ self.transfer_lines += [TransferLine(line, validate=self.validate)]
+ except PlomException as e:
+ raise PlomException(f'{self.intro}, transfer line {i}: {e}')
+ self.calculate_account_changes()
+
+ def calculate_account_changes(self):
+ sink_account = None
+ money_changes = Wealth()
+ for i, transfer_line in enumerate(self.transfer_lines):
+ intro = f'{self.intro}, transfer line {i}'
+ if transfer_line.amount is None:
+ if sink_account is not None and self.validate:
+ raise PlomException(f'{intro}: second sink found (only one allowed)')
+ sink_account = transfer_line.account
+ else:
+ 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] += 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] += 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:
+ return
+ if len(self.date) == 0:
+ raise PlomException(f'{self.intro}: missing date')
+ if len(self.description) == 0:
+ raise PlomException(f'{self.intro}: missing description')
+ try:
+ datetime.strptime(self.date, '%Y-%m-%d')
+ except ValueError:
+ raise PlomException(f'{self.intro}: bad headline date format: {self.date}')
+
+ def fill_sink(self):
+ replacement_lines = []
+ 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]
+ break
+ lines = self.lines[:i+1] + [LedgerTextLine(l) for l in replacement_lines] + self.lines[i+2:]
+ self.clean()
+ self.lines = lines
+ self.parse_lines()
+
+ def mirror(self):
+ new_transfer_lines = []
+ 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.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()
+
+ def add_taxes(self, db):
+ acc_kk_add = 'Reserves:KrankenkassenBeitragsWachstum'
+ acc_kk_minimum = 'Reserves:Monthly:KrankenkassenDefaultBeitrag'
+ acc_kk = 'Expenses:KrankenKasse'
+ acc_ESt = 'Reserves:EinkommensSteuer'
+ acc_assets = 'Assets'
+ acc_neuanfangspuffer_expenses = 'Reserves:NeuAnfangsPuffer:Ausgaben'
+ months_passed = datetime.strptime(self.date, '%Y-%m-%d').month - 1
+ past_kk_expenses = 0
+ past_kk_add = 0
+ past_neuanfangspuffer_expenses = 0
+ for b in db.bookings:
+ if b.date == self.date:
+ break
+ if acc_neuanfangspuffer_expenses in b.account_changes.keys():
+ past_neuanfangspuffer_expenses -= b.account_changes[acc_neuanfangspuffer_expenses].money_dict['€']
+ if acc_kk_add in b.account_changes.keys():
+ past_kk_add += b.account_changes[acc_kk_add].money_dict['€']
+ if acc_kk_minimum in b.account_changes.keys():
+ past_kk_expenses += b.account_changes[acc_kk_minimum].money_dict['€']
+
+ needed_netto = -self.account_changes['Assets'].money_dict['€']
+ past_taxed_needs_before_kk = past_neuanfangspuffer_expenses - past_kk_expenses
+ ESt_this_month = 0
+ E0 = decimal.Decimal(11604)
+ E1 = decimal.Decimal(17006)
+ E2 = decimal.Decimal(66761)
+ E3 = decimal.Decimal(277826)
+ taxed_income_before_kk = needed_netto
+ too_low = 0
+ too_high = 2 * needed_netto
+ while True:
+ estimate_for_remaining_year = (12 - months_passed) * taxed_income_before_kk
+ zvE = past_taxed_needs_before_kk + estimate_for_remaining_year
+ if zvE < E0:
+ ESt_year = decimal.Decimal(0)
+ elif zvE < E1:
+ y = (zvE - E0)/10000
+ ESt_year = (decimal.Decimal(922.98) * y + 1400) * y
+ elif zvE < E2:
+ y = (zvE - E1)/10000
+ ESt_year = (decimal.Decimal(181.19) * y + 2397) * y + decimal.Decimal(1025.38)
+ elif zvE < E3:
+ ESt_year = decimal.Decimal(0.42) * zvE - 10602.13