def load(self): schedule = self.schedule txn = schedule.ref self._start_date = txn.date self._start_date_fmt = self.table.document.app.format_date(self._start_date) self._stop_date = schedule.stop_date if self._stop_date is not None: self._stop_date_fmt = self.table.document.app.format_date(self._stop_date) else: self._stop_date_fmt = '' self._repeat_type = get_repeat_type_desc( schedule.repeat_type, schedule.start_date) self._interval = str(schedule.repeat_every) self._description = txn.description self._payee = txn.payee self._checkno = txn.checkno froms, tos = splitted_splits(txn.splits) self._from_count = len(froms) self._to_count = len(tos) UNASSIGNED = tr('Unassigned') if len(froms) > 1 else '' self._from = ', '.join(s.account.name if s.account is not None else UNASSIGNED for s in froms) UNASSIGNED = tr('Unassigned') if len(tos) > 1 else '' self._to = ', '.join(s.account.name if s.account is not None else UNASSIGNED for s in tos) try: self._amount = sum(s.amount for s in tos) except ValueError: # currency coercing problem currency = self.document.default_currency self._amount = sum(amount_convert(s.amount, currency, s.transaction.date) for s in tos) self.is_amount_native = self.document.is_amount_native(self._amount) self._amount_fmt = self.document.format_amount(self._amount)
def change_schedule(self, schedule, new_ref, repeat_type, repeat_every, stop_date): """Change attributes of ``schedule``. ``new_ref`` is a reference transaction that the schedule is going to repeat. :param schedule: :class:`.Schedule` :param new_ref: :class:`.Transaction` :param repeat_type: :class:`.RepeatType` :param stop_date: ``datetime.date`` """ for split in new_ref.splits: if split.account is not None: # same as in change_transaction() split.account = self.accounts.find(split.account.name, split.account.type) if schedule in self.schedules: action = Action(tr('Change Schedule')) action.change_schedule(schedule) else: action = Action(tr('Add Schedule')) action.added_schedules.add(schedule) self._undoer.record(action) original = schedule.ref min_date = min(original.date, new_ref.date) original.change( description=new_ref.description, payee=new_ref.payee, checkno=new_ref.checkno, notes=new_ref.notes, splits=new_ref.splits ) schedule.change( start_date=new_ref.date, stop_date=stop_date, repeat_type=repeat_type, repeat_every=repeat_every) if schedule not in self.schedules: self.schedules.append(schedule) self._cook(from_date=min_date)
def _refresh(self): self.clear() self.has_multiple_currencies = self.document.accounts.has_multiple_currencies() self.assets = self.make_type_node(tr('ASSETS'), AccountType.Asset) self.liabilities = self.make_type_node(tr('LIABILITIES'), AccountType.Liability) self.net_worth = self._make_node(tr('NET WORTH')) net_worth_start = self.assets.start_amount - self.liabilities.start_amount net_worth_end = self.assets.end_amount - self.liabilities.end_amount budget_date_range = DateRange(date.today(), self.document.date_range.end) # The net worth's budget is not a simple subtraction, it must count the target-less budgets net_worth_budgeted = self.document.budgeted_amount(budget_date_range) net_worth_delta = net_worth_end - net_worth_start force_explicit_currency = self.has_multiple_currencies self.net_worth.start = self.document.format_amount( net_worth_start, force_explicit_currency=force_explicit_currency ) self.net_worth.end = self.document.format_amount( net_worth_end, force_explicit_currency=force_explicit_currency ) # TODO: move this value to budget view. except for tests, this isn't # used any more. self.net_worth.budgeted = self.document.format_amount( net_worth_budgeted, force_explicit_currency=force_explicit_currency ) self.net_worth.delta = self.document.format_amount( net_worth_delta, force_explicit_currency=force_explicit_currency ) self.net_worth.delta_perc = get_delta_perc(net_worth_delta, net_worth_start) self.net_worth.is_total = True self.append(self.assets) self.append(self.liabilities) self.append(self.net_worth)
def load(self): transaction = self.transaction self._load_from_fields(transaction, self.FIELDS) self._date_fmt = None self._position = transaction.position splits = transaction.splits froms, tos = splitted_splits(splits) self._from_count = len(froms) self._to_count = len(tos) UNASSIGNED = tr('Unassigned') if len(froms) > 1 else '' get_display = lambda s: s.account.combined_display if s.account is not None else UNASSIGNED self._from = ', '.join(map(get_display, froms)) UNASSIGNED = tr('Unassigned') if len(tos) > 1 else '' get_display = lambda s: s.account.combined_display if s.account is not None else UNASSIGNED self._to = ', '.join(map(get_display, tos)) self._amount = transaction.amount self._amount_fmt = None self._mtime = datetime.datetime.fromtimestamp(transaction.mtime) if transaction.mtime > 0: self._mtime_fmt = self._mtime.strftime('%Y/%m/%d %H:%M') else: self._mtime_fmt = '' self._recurrent = transaction.is_spawn self._reconciled = any(split.reconciled for split in splits) self._can_set_amount = transaction.can_set_amount
def load(self): transaction = self.transaction self._load_from_fields(transaction, self.FIELDS) self._date_fmt = None self._position = transaction.position splits = transaction.splits froms, tos = splitted_splits(splits) self._from_count = len(froms) self._to_count = len(tos) UNASSIGNED = tr('Unassigned') if len(froms) > 1 else '' get_display = lambda s: s.account.combined_display if s.account is not None else UNASSIGNED self._from = ', '.join(map(get_display, froms)) UNASSIGNED = tr('Unassigned') if len(tos) > 1 else '' get_display = lambda s: s.account.combined_display if s.account is not None else UNASSIGNED self._to = ', '.join(map(get_display, tos)) self._amount = transaction.amount self._amount_fmt = None self._mtime = datetime.datetime.fromtimestamp(transaction.mtime) if transaction.mtime > 0: self._mtime_fmt = self._mtime.strftime('%Y/%m/%d %H:%M') else: self._mtime_fmt = '' self._recurrent = transaction.is_spawn self._reconciled = any(split.reconciled for split in splits) self._is_budget = transaction.is_budget self._can_set_amount = transaction.can_set_amount
def _refresh(self): self.clear() self.has_multiple_currencies = self.document.accounts.has_multiple_currencies( ) self.assets = self.make_type_node(tr('ASSETS'), AccountType.Asset) self.liabilities = self.make_type_node(tr('LIABILITIES'), AccountType.Liability) self.net_worth = self._make_node(tr('NET WORTH')) net_worth_start = self.assets.start_amount - self.liabilities.start_amount net_worth_end = self.assets.end_amount - self.liabilities.end_amount budget_date_range = DateRange(date.today(), self.document.date_range.end) # The net worth's budget is not a simple subtraction, it must count the target-less budgets net_worth_budgeted = self.document.budgeted_amount(budget_date_range) net_worth_delta = net_worth_end - net_worth_start force_explicit_currency = self.has_multiple_currencies self.net_worth.start = self.document.format_amount( net_worth_start, force_explicit_currency=force_explicit_currency) self.net_worth.end = self.document.format_amount( net_worth_end, force_explicit_currency=force_explicit_currency) # TODO: move this value to budget view. except for tests, this isn't # used any more. self.net_worth.budgeted = self.document.format_amount( net_worth_budgeted, force_explicit_currency=force_explicit_currency) self.net_worth.delta = self.document.format_amount( net_worth_delta, force_explicit_currency=force_explicit_currency) self.net_worth.delta_perc = get_delta_perc(net_worth_delta, net_worth_start) self.net_worth.is_total = True self.append(self.assets) self.append(self.liabilities) self.append(self.net_worth)
def _refresh(self): self.clear() self.has_multiple_currencies = self.document.accounts.has_multiple_currencies() self.income = self.make_type_node(tr('INCOME'), AccountType.Income) self.expenses = self.make_type_node(tr('EXPENSES'), AccountType.Expense) self.net_income = self._make_node(tr('NET INCOME')) net_income = self.income.cash_flow_amount - self.expenses.cash_flow_amount last_net_income = self.income.last_cash_flow_amount - self.expenses.last_cash_flow_amount net_budgeted = self.income.budgeted_amount - self.expenses.budgeted_amount delta = net_income - last_net_income force_explicit_currency = self.has_multiple_currencies self.net_income.cash_flow = self.document.format_amount( net_income, force_explicit_currency=force_explicit_currency ) self.net_income.last_cash_flow = self.document.format_amount( last_net_income, force_explicit_currency=force_explicit_currency ) self.net_income.budgeted = self.document.format_amount( net_budgeted, force_explicit_currency=force_explicit_currency ) self.net_income.delta = self.document.format_amount( delta, force_explicit_currency=force_explicit_currency ) self.net_income.delta_perc = get_delta_perc(delta, last_net_income) self.net_income.is_total = True self.append(self.income) self.append(self.expenses) self.append(self.net_income)
def _refresh(self): self.clear() self.has_multiple_currencies = self.document.accounts.has_multiple_currencies( ) self.income = self.make_type_node(tr('INCOME'), AccountType.Income) self.expenses = self.make_type_node(tr('EXPENSES'), AccountType.Expense) self.net_income = self._make_node(tr('NET INCOME')) net_income = self.income.cash_flow_amount - self.expenses.cash_flow_amount last_net_income = self.income.last_cash_flow_amount - self.expenses.last_cash_flow_amount net_budgeted = self.income.budgeted_amount - self.expenses.budgeted_amount delta = net_income - last_net_income force_explicit_currency = self.has_multiple_currencies self.net_income.cash_flow = self.document.format_amount( net_income, force_explicit_currency=force_explicit_currency) self.net_income.last_cash_flow = self.document.format_amount( last_net_income, force_explicit_currency=force_explicit_currency) self.net_income.budgeted = self.document.format_amount( net_budgeted, force_explicit_currency=force_explicit_currency) self.net_income.delta = self.document.format_amount( delta, force_explicit_currency=force_explicit_currency) self.net_income.delta_perc = get_delta_perc(delta, last_net_income) self.net_income.is_total = True self.append(self.income) self.append(self.expenses) self.append(self.net_income)
def _get_action_from_changed_transactions(self, transactions, global_scope=False): if len(transactions) == 1 and not transactions[0].is_spawn \ and transactions[0] not in self.transactions: action = Action(tr('Add transaction')) action.added_transactions.add(transactions[0]) else: action = Action(tr('Change transaction')) action.change_transactions(transactions, self.schedules) if global_scope: spawns, txns = extract(lambda x: x.is_spawn, transactions) action.change_transactions(spawns, self.schedules) return action
def __init__(self, mainwindow, target_account=None): super().__init__() if not hasattr(mainwindow, 'loader'): raise ValueError("Nothing to import!") self.mainwindow = mainwindow self.document = mainwindow.document self.app = self.document.app self._selected_pane_index = 0 self._selected_target_index = 0 def setfunc(index): self.view.set_swap_button_enabled(self.can_perform_swap()) self.swap_type_list = LinkedSelectableList(items=[ "<placeholder> Day <--> Month", "<placeholder> Month <--> Year", "<placeholder> Day <--> Year", tr("Description <--> Payee"), tr("Invert Amounts"), ], setfunc=setfunc) self.swap_type_list.selected_index = SwapType.DayMonth self.panes = [] self.import_table = ImportTable(self) self.loader = self.mainwindow.loader self.target_accounts = [ a for a in self.document.accounts if a.is_balance_sheet_account() ] self.target_accounts.sort(key=lambda a: a.name.lower()) accounts = [] for account in self.loader.accounts: if account.is_balance_sheet_account(): entries = self.loader.accounts.entries_for_account(account) if len(entries): new_name = self.document.accounts.new_name(account.name) if new_name != account.name: self.loader.accounts.rename_account(account, new_name) accounts.append(account) parsing_date_format = DateFormat.from_sysformat( self.loader.parsing_date_format) for account in accounts: target = target_account if target is None and account.reference: target = getfirst(t for t in self.target_accounts if t.reference == account.reference) self.panes.append( AccountPane(self, account, target, parsing_date_format))
def parse_file_for_import(self, filename): """Parses ``filename`` in preparation for importing. Opens and parses ``filename`` and try to determine its format by successively trying to read is as a moneyGuru file, an OFX, a QIF and finally a CSV. Once parsed, take the appropriate action for the file which is either to show the CSV options window or to call :meth:`load_parsed_file_for_import`. """ default_date_format = DateFormat(self.app.date_format).sys_format for loaderclass in (native.Loader, ofx.Loader, qif.Loader, csv.Loader): try: loader = loaderclass(self.document.default_currency, default_date_format=default_date_format) loader.parse(filename) break except FileFormatError: pass else: # No file fitted raise FileFormatError(tr('%s is of an unknown format.') % filename) self.loader = loader if isinstance(self.loader, csv.Loader): panel = CSVOptions(self) panel.view = weakref.proxy(self.view.get_panel_view(panel)) panel.show() return panel else: return self.load_parsed_file_for_import()
def toggle_entries_reconciled(self, entries): """Toggle the reconcile flag of `entries`. Sets the ``reconciliation_date`` to entries' date, or unset it when turning the flag off. :param entries: list of :class:`.Entry` """ if not entries: return all_reconciled = not entries or all(entry.reconciled for entry in entries) newvalue = not all_reconciled action = Action(tr('Change reconciliation')) action.change_entries(entries, self.schedules) min_date = min(entry.date for entry in entries) spawns, entries = extract(lambda e: e.transaction.is_spawn, entries) action.change_transactions({e.transaction for e in spawns}, self.schedules) # spawns have to be processed before the action's recording, but # record() has to be called before we change the entries. This is why # we have this rather convulted code. if newvalue: for spawn in spawns: # XXX update transaction selection newtxn, newsplit = self._reconcile_spawn_split( spawn, spawn.transaction.date) action.added_transactions.add(newtxn) self._undoer.record(action) if newvalue: for entry in entries: entry.split.reconciliation_date = entry.transaction.date else: for entry in entries: entry.split.reconciliation_date = None self._cook(from_date=min_date)
def delete_transactions(self, transactions, from_account=None): """Removes every transaction in ``transactions`` from the document. Adds undo recording, global scope querying, date range adjustments and UI triggers. :param transactions: a collection of :class:`.Transaction`. :param from_account: the :class:`.Account` from which the operation takes place, if any. """ action = Action(tr('Remove transaction')) spawns, txns = extract(lambda x: x.is_spawn, transactions) global_scope = self._query_for_scope_if_needed(spawns) action.change_transactions(spawns, self.schedules) action.deleted_transactions |= set(txns) self._undoer.record(action) for txn in transactions: if txn.is_spawn: schedule = find_schedule_of_ref(txn.ref, self.schedules) assert schedule is not None if global_scope: schedule.stop_before(txn) else: schedule.delete(txn) else: self.transactions.remove(txn) min_date = min(t.date for t in transactions) self._cook(from_date=min_date) self.accounts.clean_empty_categories(from_account)
def save_edits(self): node = self.edited if node is None: return self.edited = None assert node.is_account or node.is_group if node.is_account: success = self.document.change_accounts([node.account], name=node.name) else: other = node.parent.find( lambda n: n.is_group and n.name.lower() == node.name.lower()) success = other is None or other is node if success: accounts = self.document.accounts.filter( groupname=node.oldname, type=node.parent.type) if accounts: self.document.change_accounts(accounts, groupname=node.name) else: self.document.touch() self.document.newgroups.discard((node.oldname, node.parent.type)) self.document.newgroups.add((node.name, node.parent.type)) self.mainwindow.revalidate() if not success: msg = tr("The account '{0}' already exists.").format(node.name) # we use _name because we don't want to change self.edited node._name = node.account.name if node.is_account else node.oldname self.mainwindow.show_message(msg)
def delete_transactions(self, transactions, from_account=None): """Removes every transaction in ``transactions`` from the document. Adds undo recording, global scope querying, date range adjustments and UI triggers. :param transactions: a collection of :class:`.Transaction`. :param from_account: the :class:`.Account` from which the operation takes place, if any. """ action = Action(tr('Remove transaction')) spawns, txns = extract(lambda x: x.is_spawn, transactions) global_scope = self._query_for_scope_if_needed(spawns) action.change_transactions(spawns, self.schedules) action.deleted_transactions |= set(txns) self._undoer.record(action) # to avoid sweeping twn refs from under the rug of other spawns, we # perform schedule deletions at the end of the loop. schedule_deletions = [] for txn in transactions: if txn.is_spawn: schedule = find_schedule_of_spawn(txn, self.schedules) assert schedule is not None if global_scope: schedule.change(stop_date=txn.recurrence_date - datetime.timedelta(1)) else: schedule_deletions.append((schedule, txn.recurrence_date)) else: self.transactions.remove(txn) for schedule, recurrence_date in schedule_deletions: schedule.delete_at(recurrence_date) min_date = min(t.date for t in transactions) self._cook(from_date=min_date) self.accounts.clean_empty_categories(from_account)
class GeneralLedgerView(TransactionViewBase): VIEW_TYPE = PaneType.GeneralLedger PRINT_TITLE_FORMAT = tr('General Ledger from {start_date} to {end_date}') PRINT_VIEW_CLASS = EntryPrint def __init__(self, mainwindow): TransactionViewBase.__init__(self, mainwindow) self.gltable = self.table = GeneralLedgerTable(parent_view=self) self.maintable = self.gltable self.columns = self.maintable.columns self.restore_subviews_size() # --- Overrides def _refresh_totals(self): selected, total, total_debit, total_credit = self.gltable.get_totals() total_debit_fmt = self.document.format_amount(total_debit) total_credit_fmt = self.document.format_amount(total_credit) msg = tr("{0} out of {1} selected. Debit: {2} Credit: {3}") self.status_line = msg.format(selected, total, total_debit_fmt, total_credit_fmt) def _revalidate(self): self.gltable._revalidate() def save_preferences(self): self.gltable.columns.save_columns() def show(self): TransactionViewBase.show(self) self.gltable.show() self._refresh_totals() def update_transaction_selection(self, transactions): self._refresh_totals()
def __init__(self, table, account, date, total_debit, total_credit): super(TotalRow, self).__init__(table, account) self._date = date self._description = tr('TOTAL') # don't touch _increase and _decrease, they trigger editing. self._debit_fmt = table.document.format_amount(total_debit, blank_zero=True) self._credit_fmt = table.document.format_amount(total_credit, blank_zero=True) delta = total_debit - total_credit if delta: if account.is_credit_account(): delta *= -1 positive = delta > 0 # format_amount doesn't explicitly put positive signs, so we have to put it ourselves. # However, if the delta is of foreign currency, we want the sign to be in front of the # amount, not in front of the currency code. delta_fmt = table.document.format_amount(abs(delta)) sign = '+' if positive else '-' if delta_fmt[0].isdigit(): delta_fmt = sign + delta_fmt else: # we have a currency code in front of our amount, a little trick is to replace the # only space character we have by space + sign delta_fmt = delta_fmt.replace(' ', ' ' + sign) self._balance_fmt = delta_fmt else: self._balance_fmt = '' self.is_bold = True
def parse_file_for_import(self, filename): """Parses ``filename`` in preparation for importing. Opens and parses ``filename`` and try to determine its format by successively trying to read is as a moneyGuru file, an OFX, a QIF and finally a CSV. Once parsed, take the appropriate action for the file which is either to show the CSV options window or to call :meth:`load_parsed_file_for_import`. """ default_date_format = DateFormat(self.app.date_format).sys_format for loaderclass in (native.Loader, ofx.Loader, qif.Loader, csv.Loader): try: loader = loaderclass( self.document.default_currency, default_date_format=default_date_format ) loader.parse(filename) break except FileFormatError: pass else: # No file fitted raise FileFormatError(tr('%s is of an unknown format.') % filename) self.loader = loader if isinstance(self.loader, csv.Loader): panel = CSVOptions(self) panel.view = weakref.proxy(self.view.get_panel_view(panel)) panel.show() return panel else: return self.load_parsed_file_for_import()
def load_from_xml(self, filename): """Clears the document and loads data from ``filename``. ``filename`` must be a path to a moneyGuru XML document. :param filename: ``str`` """ loader = native.Loader(self.default_currency) try: loader.parse(filename) except FileFormatError: raise FileFormatError(tr('"%s" is not a moneyGuru file') % filename) loader.load() self.clear() self._document_id = loader.document_id for propname in self._properties: if propname in loader.properties: self._properties[propname] = loader.properties[propname] self.accounts = loader.accounts self.oven._accounts = self.accounts self._undoer._accounts = self.accounts for transaction in loader.transactions: self.transactions.add(transaction, True) for recurrence in loader.schedules: self.schedules.append(recurrence) self.accounts.default_currency = self.default_currency self._cook() self._undoer.set_save_point() self._restore_preferences_after_load()
class ScheduleView(BaseView): VIEW_TYPE = PaneType.Schedule PRINT_TITLE_FORMAT = tr('Schedules from {start_date} to {end_date}') def __init__(self, mainwindow): super().__init__(mainwindow) self.table = ScheduleTable(self) self.columns = self.table.columns self.restore_subviews_size() def _revalidate(self): self.table.refresh_and_show_selection() # --- Override def save_preferences(self): self.table.columns.save_columns() # --- Public def new_item(self): schedule_panel = SchedulePanel(self.mainwindow) schedule_panel.view = weakref.proxy( self.view.get_panel_view(schedule_panel)) schedule_panel.new() return schedule_panel def edit_item(self): schedule_panel = SchedulePanel(self.mainwindow) schedule_panel.view = weakref.proxy( self.view.get_panel_view(schedule_panel)) schedule_panel.load() return schedule_panel def delete_item(self): self.table.delete()
def change_transaction(self, original, new): """Changes the attributes of ``original`` so that they match those of ``new``. Adds undo recording, global scope querying, date range adjustments and UI triggers. :param original: :class:`.Transaction` :param new: :class:`.Transaction`, a modified copy of ``original``. """ global_scope = self._query_for_scope_if_needed([original]) action = Action(tr('Change transaction')) action.change_transactions([original], self.schedules) self._undoer.record(action) # don't forget that account up here is an external instance. Even if an account of # the same name exists in self.accounts, it's not gonna be the same instance. for split in new.splits: if split.account is not None: split.account = self.accounts.find(split.account.name, split.account.type) original.change(splits=new.splits) min_date = min(original.date, new.date) self._change_transaction(original, date=new.date, description=new.description, payee=new.payee, checkno=new.checkno, notes=new.notes, global_scope=global_scope) self._cook(from_date=min_date) self.accounts.clean_empty_categories() self.date_range = self.date_range.around(original.date)
def delete_accounts(self, accounts, reassign_to=None): """Removes ``accounts`` from the document. If the account has entries assigned to it, these entries will be reassigned to the ``reassign_to`` account. :param accounts: List of :class:`.Account` to be removed. :param accounts: :class:`.Account` to use for reassignment. """ # Recording the "Remove account" action into the Undoer is quite something... action = Action(tr('Remove account')) accounts = set(accounts) action.deleted_accounts |= accounts all_entries = flatten(self.accounts.entries_for_account(a) for a in accounts) if reassign_to is None: transactions = {e.transaction for e in all_entries if not e.transaction.is_spawn} transactions = {t for t in transactions if not t.affected_accounts() - accounts} action.deleted_transactions |= transactions action.change_entries(all_entries, self.schedules) affected_schedules = [s for s in self.schedules if accounts & s.affected_accounts()] for schedule in affected_schedules: action.change_schedule(schedule) self._undoer.record(action) for account in accounts: self.transactions.reassign_account(account, reassign_to) for schedule in affected_schedules: schedule.reassign_account(account, reassign_to) self.accounts.remove(account) self._cook()
def change_transaction(self, original, new): """Changes the attributes of ``original`` so that they match those of ``new``. Adds undo recording, global scope querying, date range adjustments and UI triggers. :param original: :class:`.Transaction` :param new: :class:`.Transaction`, a modified copy of ``original``. """ global_scope = self._query_for_scope_if_needed([original]) action = Action(tr('Change transaction')) action.change_transactions([original], self.schedules) self._undoer.record(action) # don't forget that account up here is an external instance. Even if an account of # the same name exists in self.accounts, it's not gonna be the same instance. for split in new.splits: if split.account is not None: split.account = self.accounts.find(split.account.name, split.account.type) original.change(splits=new.splits) min_date = min(original.date, new.date) self._change_transaction( original, date=new.date, description=new.description, payee=new.payee, checkno=new.checkno, notes=new.notes, global_scope=global_scope ) self._cook(from_date=min_date) self.accounts.clean_empty_categories() self.date_range = self.date_range.around(original.date)
def load_from_xml(self, filename): """Clears the document and loads data from ``filename``. ``filename`` must be a path to a moneyGuru XML document. :param filename: ``str`` """ loader = native.Loader(self.default_currency) try: loader.parse(filename) except FileFormatError: raise FileFormatError( tr('"%s" is not a moneyGuru file') % filename) loader.load() self.clear() self._document_id = loader.document_id for propname in self._properties: if propname in loader.properties: self._properties[propname] = loader.properties[propname] self.accounts = loader.accounts self.oven._accounts = self.accounts self._undoer._accounts = self.accounts for transaction in loader.transactions: self.transactions.add(transaction, True) for recurrence in loader.schedules: self.schedules.append(recurrence) self.budgets.start_date = loader.budgets.start_date self.budgets.repeat_type = loader.budgets.repeat_type self.budgets.repeat_every = loader.budgets.repeat_every for budget in loader.budgets: self.budgets.append(budget) self.accounts.default_currency = self.default_currency self._cook() self._undoer.set_save_point() self._restore_preferences_after_load()
def __init__(self, table, date, balance, reconciled_balance, account): super(PreviousBalanceRow, self).__init__(table, account) self._date = date self._balance = balance self._reconciled_balance = reconciled_balance self._description = tr('Previous Balance') self._reconciled = False self.is_bold = True
def parse_amount(string, currency, **kwargs): try: return amount_parse(string, currency, with_expression=False, **kwargs) except UnsupportedCurrencyError: msg = tr( "Unsupported currency: {}. Aborting load. Did you disable a currency plugin?" ).format(currency) raise FileFormatError(msg)
def show(self): self._default_layout = Layout(tr('Default')) self.layout = self._default_layout self._refresh_columns() self._refresh_lines() self._refresh_targets() self.view.refresh_layout_menu() self.view.show()
def _refresh_totals(self): selected = len(self.mainwindow.selected_transactions) total = len(self.visible_transactions) currency = self.document.default_currency total_amount = sum(amount_convert(t.amount, currency, t.date) for t in self.mainwindow.selected_transactions) total_amount_fmt = self.document.format_amount(total_amount) msg = tr("{0} out of {1} selected. Amount: {2}") self.status_line = msg.format(selected, total, total_amount_fmt)
def __init__(self, mainwindow, target_account=None): super().__init__() if not hasattr(mainwindow, 'loader'): raise ValueError("Nothing to import!") self.mainwindow = mainwindow self.document = mainwindow.document self.app = self.document.app self._selected_pane_index = 0 self._selected_target_index = 0 def setfunc(index): self.view.set_swap_button_enabled(self.can_perform_swap()) self.swap_type_list = LinkedSelectableList(items=[ "<placeholder> Day <--> Month", "<placeholder> Month <--> Year", "<placeholder> Day <--> Year", tr("Description <--> Payee"), tr("Invert Amounts"), ], setfunc=setfunc) self.swap_type_list.selected_index = SwapType.DayMonth self.panes = [] self.import_table = ImportTable(self) self.loader = self.mainwindow.loader self.target_accounts = [ a for a in self.document.accounts if a.is_balance_sheet_account()] self.target_accounts.sort(key=lambda a: a.name.lower()) accounts = [] for account in self.loader.accounts: if account.is_balance_sheet_account(): entries = self.loader.accounts.entries_for_account(account) if len(entries): new_name = self.document.accounts.new_name(account.name) if new_name != account.name: self.loader.accounts.rename_account(account, new_name) accounts.append(account) parsing_date_format = DateFormat.from_sysformat(self.loader.parsing_date_format) for account in accounts: target = target_account if target is None and account.reference: target = getfirst( t for t in self.target_accounts if t.reference == account.reference ) self.panes.append( AccountPane(self, account, target, parsing_date_format))
def change_accounts(self, accounts, name=NOEDIT, type=NOEDIT, currency=NOEDIT, groupname=NOEDIT, account_number=NOEDIT, inactive=NOEDIT, notes=NOEDIT): """Properly sets properties for ``accounts``. Sets ``accounts``' properties in a proper manner. Attributes corresponding to arguments set to ``NOEDIT`` will not be touched. :param accounts: List of :class:`.Account` to be changed. :param name: ``str`` :param type: :class:`.AccountType` :param currency: :class:`.Currency` :param groupname: ``str`` :param account_number: ``str`` :param inactive: ``bool`` :param notes: ``str`` """ assert all(a is not None for a in accounts) # Check for name clash for account in accounts: if name is not NOEDIT and name: other = self.accounts.find(name) if (other is not None) and (other != account): return False action = Action(tr('Change account')) action.change_accounts(accounts) self._undoer.record(action) for account in accounts: kwargs = {} if name is not NOEDIT and name: self.accounts.rename_account(account, name.strip()) if (type is not NOEDIT) and (type != account.type): kwargs.update({'type': type, 'groupname': None}) if currency is not NOEDIT: entries = self.accounts.entries_for_account(account) assert not any(e.reconciled for e in entries) kwargs['currency'] = currency if groupname is not NOEDIT: kwargs['groupname'] = groupname if account_number is not NOEDIT: kwargs['account_number'] = account_number if inactive is not NOEDIT: kwargs['inactive'] = inactive if notes is not NOEDIT: kwargs['notes'] = notes if kwargs: account.change(**kwargs) self._cook() self.transactions.clear_cache() return True
def delete_accounts(self, accounts, reassign_to=None): """Removes ``accounts`` from the document. If the account has entries assigned to it, these entries will be reassigned to the ``reassign_to`` account. :param accounts: List of :class:`.Account` to be removed. :param accounts: :class:`.Account` to use for reassignment. """ # Recording the "Remove account" action into the Undoer is quite something... action = Action(tr('Remove account')) accounts = set(accounts) action.deleted_accounts |= accounts all_entries = flatten( self.accounts.entries_for_account(a) for a in accounts) if reassign_to is None: transactions = { e.transaction for e in all_entries if not e.transaction.is_spawn } transactions = { t for t in transactions if not t.affected_accounts() - accounts } action.deleted_transactions |= transactions action.change_entries(all_entries, self.schedules) affected_schedules = [ s for s in self.schedules if accounts & s.affected_accounts() ] for schedule in affected_schedules: action.change_schedule(schedule) for account in accounts: affected_budgets = [ b for b in self.budgets if b.account == account or b.target == account ] if account.is_income_statement_account() and reassign_to is None: action.deleted_budgets |= set(affected_budgets) else: for budget in affected_budgets: action.change_budget(budget) self._undoer.record(action) for account in accounts: self.transactions.reassign_account(account, reassign_to) for schedule in affected_schedules: schedule.reassign_account(account, reassign_to) for budget in affected_budgets: if budget.account == account: if reassign_to is None: self.budgets.remove(budget) else: budget.account = reassign_to elif budget.target == account: budget.target = reassign_to self.accounts.remove(account) self._cook()
def _check_amount_values(self, lines, ci): for line in lines: for attr in [CsvField.Amount, CsvField.Increase, CsvField.Decrease]: if attr not in ci: continue index = ci[attr] value = line[index] try: base.parse_amount(value, self.default_currency) except ValueError: raise FileLoadError(tr("The Amount column has been set on a column that doesn't contain amounts."))
def _load_budget(self, budget): if budget is None: raise OperationAborted self.original = budget self.budget = budget.replicate() self._accounts = [a for a in self.document.accounts if a.is_income_statement_account()] if not self._accounts: msg = tr("Income/Expense accounts must be created before budgets can be set.") raise OperationAborted(msg) self._accounts.sort(key=ACCOUNT_SORT_KEY) self.account_list.refresh() self.account_list.select(self._accounts.index(budget.account) if budget.account is not None else 0)
class ProfitView(AccountSheetView): SAVENAME = 'ProfitView' VIEW_TYPE = PaneType.Profit PRINT_TITLE_FORMAT = tr('Profit and Loss from {start_date} to {end_date}') def __init__(self, mainwindow): AccountSheetView.__init__(self, mainwindow) self.sheet = self.istatement = IncomeStatement(self) self.columns = self.istatement.columns self.graph = self.pgraph = ProfitGraph(self) self.pie = CashFlowPieChart(self) self.restore_subviews_size()
def new_account(self, type, groupname): """Create a new account in the document. Creates a new account of type ``type``, within the ``group`` (which can be ``None`` to indicate no group). The new account will have a unique name based on the string "New Account" (if it exists already, a unique number will be appended to it). Once created, the account is added to the account list. :param type: :class:`.AccountType` :param group: :class:`.Group` :rtype: :class:`.Account` """ name = self.accounts.new_name(tr('New account')) account = self.accounts.create(name, self.default_currency, type) if groupname: account.change(groupname=groupname) action = Action(tr('Add account')) action.added_accounts.add(account) self._undoer.record(action) self.touch() return account
def _load(self, accounts): self.deleted_accounts = set(accounts) all_accounts = list(self.document.accounts) target_accounts = sorted( (a for a in all_accounts if a not in self.deleted_accounts), key=ACCOUNT_SORT_KEY) target_account_names = [a.name for a in target_accounts] target_account_names.insert(0, tr('No Account')) self._target_accounts = target_accounts self._target_accounts.insert(0, None) self.account_list[:] = target_account_names self.account_list.select(0)
def materialize_spawn(self, spawn): assert spawn.is_spawn schedule = find_schedule_of_ref(spawn.ref, self.schedules) assert schedule is not None action = Action(tr('Materialize transaction')) action.change_schedule(schedule) schedule.delete(spawn) materialized = spawn.materialize() action.added_transactions |= {materialized} self._undoer.record(action) self.transactions.add(materialized) self._cook(from_date=materialized.date)
def materialize_spawn(self, spawn): assert spawn.is_spawn schedule = find_schedule_of_spawn(spawn, self.schedules) assert schedule is not None action = Action(tr('Materialize transaction')) action.change_schedule(schedule) materialized = spawn.materialize() action.added_transactions |= {materialized} self._undoer.record(action) schedule.delete_at(spawn.recurrence_date) self.transactions.add(materialized) self._cook(from_date=materialized.date)
def make_group_node(self, groupname, grouptype): node = self._make_node(groupname) node.is_group = True node.oldname = groupname # in case we rename accounts = self.document.accounts.filter( groupname=groupname, type=grouptype) for account in sorted(accounts, key=ACCOUNT_SORT_KEY): node.append(self.make_account_node(account)) node.is_excluded = bool(accounts) and set(accounts) <= self.document.excluded_accounts # all accounts excluded if not node.is_excluded: node.append(self.make_total_node(node, tr('Total ') + groupname)) node.append(self.make_blank_node()) return node
class NetWorthView(AccountSheetView): SAVENAME = 'NetWorthView' VIEW_TYPE = PaneType.NetWorth PRINT_TITLE_FORMAT = tr( "Net Worth at {end_date}, starting from {start_date}") def __init__(self, mainwindow): AccountSheetView.__init__(self, mainwindow) self.sheet = self.bsheet = BalanceSheet(self) self.columns = self.bsheet.columns self.graph = self.nwgraph = NetWorthGraph(self) self.pie = BalancePieChart(self) self.restore_subviews_size()
def delete_budgets(self, budgets): """Removes ``budgets`` from the document. :param budgets: list of :class:`.Budget` """ if not budgets: return action = Action(tr('Remove Budget')) action.deleted_budgets |= set(budgets) self._undoer.record(action) for budget in budgets: self.budgets.remove(budget) self._cook(from_date=self.budgets.start_date)
def change_accounts( self, accounts, name=NOEDIT, type=NOEDIT, currency=NOEDIT, groupname=NOEDIT, account_number=NOEDIT, inactive=NOEDIT, notes=NOEDIT): """Properly sets properties for ``accounts``. Sets ``accounts``' properties in a proper manner. Attributes corresponding to arguments set to ``NOEDIT`` will not be touched. :param accounts: List of :class:`.Account` to be changed. :param name: ``str`` :param type: :class:`.AccountType` :param currency: :class:`.Currency` :param groupname: ``str`` :param account_number: ``str`` :param inactive: ``bool`` :param notes: ``str`` """ assert all(a is not None for a in accounts) # Check for name clash for account in accounts: if name is not NOEDIT and name: other = self.accounts.find(name) if (other is not None) and (other != account): return False action = Action(tr('Change account')) action.change_accounts(accounts) self._undoer.record(action) for account in accounts: kwargs = {} if name is not NOEDIT and name: self.accounts.rename_account(account, name.strip()) if (type is not NOEDIT) and (type != account.type): kwargs.update({'type': type, 'groupname': None}) if currency is not NOEDIT: entries = self.accounts.entries_for_account(account) assert not any(e.reconciled for e in entries) kwargs['currency'] = currency if groupname is not NOEDIT: kwargs['groupname'] = groupname if account_number is not NOEDIT: kwargs['account_number'] = account_number if inactive is not NOEDIT: kwargs['inactive'] = inactive if notes is not NOEDIT: kwargs['notes'] = notes if kwargs: account.change(**kwargs) self._cook() self.transactions.clear_cache() return True
def change_schedule(self, schedule, new_ref, repeat_type, repeat_every, stop_date): """Change attributes of ``schedule``. ``new_ref`` is a reference transaction that the schedule is going to repeat. :param schedule: :class:`.Schedule` :param new_ref: :class:`.Transaction` :param repeat_type: :class:`.RepeatType` :param stop_date: ``datetime.date`` """ for split in new_ref.splits: if split.account is not None: # same as in change_transaction() split.account = self.accounts.find(split.account.name, split.account.type) if schedule in self.schedules: action = Action(tr('Change Schedule')) action.change_schedule(schedule) else: action = Action(tr('Add Schedule')) action.added_schedules.add(schedule) self._undoer.record(action) original = schedule.ref min_date = min(original.date, new_ref.date) original.change(description=new_ref.description, payee=new_ref.payee, checkno=new_ref.checkno, notes=new_ref.notes, splits=new_ref.splits) schedule.start_date = new_ref.date schedule.repeat_type = repeat_type schedule.repeat_every = repeat_every schedule.stop_date = stop_date schedule.reset_spawn_cache() if schedule not in self.schedules: self.schedules.append(schedule) self._cook(from_date=min_date)
def compute_pie_data(self, data): data = [(name, float(amount)) for name, amount in data.items() if amount > 0] data.sort(key=lambda t: t[1], reverse=True) data = [(name, amount, i % (COLOR_COUNT-1)) for i, (name, amount) in enumerate(data)] if len(data) > self.slice_count: others = data[self.slice_count-1:] others_total = sum(amount for _, amount, _ in others) del data[self.slice_count-1:] data.append((tr('Others'), others_total, COLOR_COUNT-1)) total = sum(amount for _, amount, _ in data) if not total: return None fmt = lambda name, amount: '%s %1.1f%%' % (name, amount / total * 100) return [(fmt(name, amount), amount, color) for name, amount, color in data]
def duplicate_transactions(self, transactions): """Create copies of ``transactions`` in the document. Adds undo recording and UI triggers. :param transactions: a collection of :class:`.Transaction` to duplicate. """ if not transactions: return action = Action(tr('Duplicate transactions')) duplicated = [txn.replicate() for txn in transactions] action.added_transactions |= set(duplicated) self._undoer.record(action) self._add_transactions(duplicated)
def delete_schedules(self, schedules): """Removes ``schedules`` from the document. :param schedules: list of :class:`.Schedule` """ if not schedules: return action = Action(tr('Remove Schedule')) action.deleted_schedules |= set(schedules) self._undoer.record(action) for schedule in schedules: self.schedules.remove(schedule) min_date = min(s.ref.date for s in schedules) self._cook(from_date=min_date)