예제 #1
0
 def __init__(self, app):
     GUITable.__init__(self)
     DupeGuruGUIObject.__init__(self, app)
     self.columns = Columns(self, prefaccess=app, savename='ResultTable')
     self._power_marker = False
     self._delta_values = False
     self._sort_descriptors = ('name', True)
예제 #2
0
 def __init__(self, parent_view):
     ViewChild.__init__(self, parent_view)
     tree.Tree.__init__(self)
     self._was_restored = False
     self.columns = Columns(self,
                            prefaccess=parent_view.document,
                            savename=self.SAVENAME)
     self.edited = None
     self._expanded_paths = {(0, ), (1, )}
예제 #3
0
 def toggle_menu_item(self, index):
     if index == len(self._optional_columns()):
         debit_visible = self.column_is_visible('debit')
         self.set_column_visible('debit', not debit_visible)
         self.set_column_visible('credit', not debit_visible)
         self.set_column_visible('increase', debit_visible)
         self.set_column_visible('decrease', debit_visible)
     else:
         Columns.toggle_menu_item(self, index)
예제 #4
0
 def __init__(self, parent_view):
     ViewChild.__init__(self, parent_view)
     tree.Tree.__init__(self)
     self._was_restored = False
     self.columns = Columns(self, prefaccess=parent_view.document, savename=self.SAVENAME)
     self.edited = None
     self._expanded_paths = {(0, ), (1, )}
예제 #5
0
 def __init__(self, app):
     GUITable.__init__(self)
     DupeGuruGUIObject.__init__(self, app)
     self.columns = Columns(self, prefaccess=app, savename='ResultTable')
     self._power_marker = False
     self._delta_values = False
     self._sort_descriptors = ('name', True)
예제 #6
0
 def menu_items(self):
     items = Columns.menu_items(self)
     marked = self.column_is_visible('debit')
     items.append((tr("Debit/Credit"), marked))
     return items
예제 #7
0
 def save_columns(self):
     Columns.save_columns(self)
     pref_name = '{}.Columns.debit_credit_mode'.format(self.savename)
     value = self.column_is_visible('debit')
     self.prefaccess.set_default(pref_name, value)
예제 #8
0
class ResultTable(GUITable, DupeGuruGUIObject):
    def __init__(self, app):
        GUITable.__init__(self)
        DupeGuruGUIObject.__init__(self, app)
        self.columns = Columns(self, prefaccess=app, savename='ResultTable')
        self._power_marker = False
        self._delta_values = False
        self._sort_descriptors = ('name', True)
    
    #--- Override
    def _view_updated(self):
        self._refresh_with_view()
    
    def _restore_selection(self, previous_selection):
        if self.app.selected_dupes:
            to_find = set(self.app.selected_dupes)
            indexes = [i for i, r in enumerate(self) if r._dupe in to_find]
            self.selected_indexes = indexes
    
    def _update_selection(self):
        rows = self.selected_rows
        self.app._select_dupes(list(map(attrgetter('_dupe'), rows)))
    
    def _fill(self):
        if not self.power_marker:
            for group in self.app.results.groups:
                self.append(DupeRow(self, group, group.ref))
                for dupe in group.dupes:
                    self.append(DupeRow(self, group, dupe))
        else:
            for dupe in self.app.results.dupes:
                group = self.app.results.get_group_of_duplicate(dupe)
                self.append(DupeRow(self, group, dupe))
    
    def _refresh_with_view(self):
        self.refresh()
        self.view.show_selected_row()
    
    #--- Public
    def get_row_value(self, index, column):
        try:
            row = self[index]
        except IndexError:
            return '---'
        if self.delta_values:
            return row.data_delta[column]
        else:
            return row.data[column]
    
    def rename_selected(self, newname):
        row = self.selected_row
        if row is None:
            # There's all kinds of way the current row can be swept off during rename. When it
            # happens, selected_row will be None.
            return False
        row._data = None
        row._data_delta = None
        return self.app.rename_selected(newname)
    
    def sort(self, key, asc):
        if self.power_marker:
            self.app.results.sort_dupes(key, asc, self.delta_values)
        else:
            self.app.results.sort_groups(key, asc)
        self._sort_descriptors = (key, asc)
        self._refresh_with_view()
    
    #--- Properties
    @property
    def power_marker(self):
        return self._power_marker
    
    @power_marker.setter
    def power_marker(self, value):
        if value == self._power_marker:
            return
        self._power_marker = value
        key, asc = self._sort_descriptors
        self.sort(key, asc)
        # no need to refresh, it has happened in sort()
    
    @property
    def delta_values(self):
        return self._delta_values
    
    @delta_values.setter
    def delta_values(self, value):
        if value == self._delta_values:
            return
        self._delta_values = value
        self.refresh()
    
    @property
    def selected_dupe_count(self):
        return sum(1 for row in self.selected_rows if not row.isref)
    
    #--- Event Handlers
    def marking_changed(self):
        self.view.invalidate_markings()
    
    def results_changed(self):
        self._refresh_with_view()
    
    def results_changed_but_keep_selection(self):
        # What we want to to here is that instead of restoring selected *dupes* after refresh, we
        # restore selected *paths*.
        indexes = self.selected_indexes
        self.refresh(refresh_view=False)
        self.select(indexes)
        self.view.refresh()
    
    def save_session(self):
        self.columns.save_columns()
예제 #9
0
 def toggle_menu_item(self, index):
     if index == len(self._optional_columns()):
         debit_visible = self.column_is_visible('debit')
         self._set_debit_credit_mode(not debit_visible)
     else:
         Columns.toggle_menu_item(self, index)
예제 #10
0
class ResultTable(GUITable, DupeGuruGUIObject):
    def __init__(self, app):
        GUITable.__init__(self)
        DupeGuruGUIObject.__init__(self, app)
        self.columns = Columns(self, prefaccess=app, savename='ResultTable')
        self._power_marker = False
        self._delta_values = False
        self._sort_descriptors = ('name', True)

    #--- Override
    def _view_updated(self):
        self._refresh_with_view()

    def _restore_selection(self, previous_selection):
        if self.app.selected_dupes:
            to_find = set(self.app.selected_dupes)
            indexes = [i for i, r in enumerate(self) if r._dupe in to_find]
            self.selected_indexes = indexes

    def _update_selection(self):
        rows = self.selected_rows
        self.app._select_dupes(list(map(attrgetter('_dupe'), rows)))

    def _fill(self):
        if not self.power_marker:
            for group in self.app.results.groups:
                self.append(DupeRow(self, group, group.ref))
                for dupe in group.dupes:
                    self.append(DupeRow(self, group, dupe))
        else:
            for dupe in self.app.results.dupes:
                group = self.app.results.get_group_of_duplicate(dupe)
                self.append(DupeRow(self, group, dupe))

    def _refresh_with_view(self):
        self.refresh()
        self.view.show_selected_row()

    #--- Public
    def get_row_value(self, index, column):
        try:
            row = self[index]
        except IndexError:
            return '---'
        if self.delta_values:
            return row.data_delta[column]
        else:
            return row.data[column]

    def rename_selected(self, newname):
        row = self.selected_row
        if row is None:
            # There's all kinds of way the current row can be swept off during rename. When it
            # happens, selected_row will be None.
            return False
        row._data = None
        row._data_delta = None
        return self.app.rename_selected(newname)

    def sort(self, key, asc):
        if self.power_marker:
            self.app.results.sort_dupes(key, asc, self.delta_values)
        else:
            self.app.results.sort_groups(key, asc)
        self._sort_descriptors = (key, asc)
        self._refresh_with_view()

    #--- Properties
    @property
    def power_marker(self):
        return self._power_marker

    @power_marker.setter
    def power_marker(self, value):
        if value == self._power_marker:
            return
        self._power_marker = value
        key, asc = self._sort_descriptors
        self.sort(key, asc)
        # no need to refresh, it has happened in sort()

    @property
    def delta_values(self):
        return self._delta_values

    @delta_values.setter
    def delta_values(self, value):
        if value == self._delta_values:
            return
        self._delta_values = value
        self.refresh()

    @property
    def selected_dupe_count(self):
        return sum(1 for row in self.selected_rows if not row.isref)

    #--- Event Handlers
    def marking_changed(self):
        self.view.invalidate_markings()

    def results_changed(self):
        self._refresh_with_view()

    def results_changed_but_keep_selection(self):
        # What we want to to here is that instead of restoring selected *dupes* after refresh, we
        # restore selected *paths*.
        indexes = self.selected_indexes
        self.refresh(refresh_view=False)
        self.select(indexes)
        self.view.refresh()

    def save_session(self):
        self.columns.save_columns()
예제 #11
0
 def restore_columns(self):
     Columns.restore_columns(self)
     pref_name = '{}.Columns.debit_credit_mode'.format(self.savename)
     debit_credit_mode = bool(self.prefaccess.get_default(pref_name))
     self._set_debit_credit_mode(debit_credit_mode)
예제 #12
0
 def __init__(self, document):
     GUITableBase.__init__(self)
     self.document = document
     self.columns = Columns(self, prefaccess=document, savename=self.SAVENAME)
예제 #13
0
 def toggle_menu_item(self, index):
     if index == len(self._optional_columns()):
         debit_visible = self.column_is_visible('debit')
         self._set_debit_credit_mode(not debit_visible)
     else:
         Columns.toggle_menu_item(self, index)
예제 #14
0
 def restore_columns(self):
     Columns.restore_columns(self)
     pref_name = '{}.Columns.debit_credit_mode'.format(self.savename)
     debit_credit_mode = bool(self.prefaccess.get_default(pref_name))
     self._set_debit_credit_mode(debit_credit_mode)
예제 #15
0
class Report(ViewChild, tree.Tree, SheetViewNotificationsMixin):
    SAVENAME = ''
    COLUMNS = []
    INVALIDATING_MESSAGES = MESSAGES_DOCUMENT_CHANGED | {
        'accounts_excluded', 'date_range_changed'
    }

    def __init__(self, parent_view):
        ViewChild.__init__(self, parent_view)
        tree.Tree.__init__(self)
        self._was_restored = False
        self.columns = Columns(self,
                               prefaccess=parent_view.document,
                               savename=self.SAVENAME)
        self.edited = None
        self._expanded_paths = {(0, ), (1, )}

    # --- Override
    def _do_restore_view(self):
        if not self.document.can_restore_from_prefs():
            return False
        prefname = '{}.ExpandedPaths'.format(self.SAVENAME)
        expanded = self.document.get_default(prefname, list())
        if expanded:
            self._expanded_paths = {tuple(p) for p in expanded}
            self.view.refresh_expanded_paths()
        self.columns.restore_columns()
        return True

    def _revalidate(self):
        self.refresh(refresh_view=False)
        self._update_selection()
        self.view.refresh()
        self.restore_view()

    # --- Virtual
    def _compute_account_node(self, node):
        pass

    def _make_node(self, name):
        node = Node(name)
        node.account_number = ''
        return node

    def _refresh(self):
        pass

    # --- Protected
    def _node_of_account(self, account):
        return self.find(lambda n: getattr(n, 'account', None) is account)

    def _prune_invalid_expanded_paths(self):
        newpaths = set()
        for path in self._expanded_paths:
            try:
                node = self.get_node(path)
                if node.is_group or node.is_type:
                    newpaths.add(path)
            except IndexError:
                pass
        self._expanded_paths = newpaths

    def _select_first(self):
        for type_node in self:
            if len(type_node) > 2:  # total + blank
                self.selected = type_node[0]
                break

    def _update_selection(self):
        if not (isinstance(self.selected, Node) and self.selected.is_account):
            self._select_first()

    # --- Public
    def add_account(self):
        self.view.stop_editing()
        self.save_edits()
        node = self.selected
        if isinstance(node, Node) and node.is_group:
            account_type = node.group.type
            account_group = node.group
        elif isinstance(node, Node) and node.is_account:
            account_type = node.account.type
            account_group = node.account.group
        else:
            # there are only 2 types per report
            path = self.selected_path
            account_type = self[1].type if path and path[0] == 1 else self[
                0].type
            account_group = None
        if account_group is not None:
            account_group.expanded = True
        account = self.document.new_account(
            account_type, account_group)  # refresh happens on account_added
        self.selected = self._node_of_account(account)
        self.view.update_selection()
        self.view.start_editing()

    def add_account_group(self):
        self.view.stop_editing()
        self.save_edits()
        node = self.selected
        if isinstance(node, Node) and node.is_group:
            account_type = node.group.type
        elif isinstance(node, Node) and node.is_account:
            account_type = node.account.type
        else:
            path = self.selected_path
            account_type = self[1].type if path and path[0] == 1 else self[
                0].type
        group = self.document.new_group(account_type)
        self.selected = self.find(lambda n: getattr(n, 'group', None) is group)
        self.view.update_selection()
        self.view.start_editing()

    def can_delete(self):
        node = self.selected
        return isinstance(node, Node) and (node.is_account or node.is_group)

    def can_move(self, source_paths, dest_path):
        """Returns whether it's possible to move the nodes at 'source_paths' under the node at
        'dest_path'.
        """
        if not dest_path:  # Don't move under the root
            return False
        dest_node = self.get_node(dest_path)
        if not (dest_node.is_group or dest_node.is_type):
            # Move only under a group node or a type node
            return False
        for source_path in source_paths:
            source_node = self.get_node(source_path)
            if not source_node.is_account:
                return False
            if source_node.parent is dest_node:  # Don't move under the same node
                return False
        return True

    def cancel_edits(self):
        node = self.edited
        if node is None:
            return
        assert node.is_account or node.is_group
        node.name = node.account.name if node.is_account else node.group.name
        self.edited = None

    def collapse_node(self, node):
        self._expanded_paths.discard(tuple(node.path))
        if node.is_group:
            self.parent_view.collapse_group(node.group)

    def delete(self):
        if not self.can_delete():
            return
        self.view.stop_editing()
        selected_nodes = self.selected_nodes
        gnodes = [n for n in selected_nodes if n.is_group]
        if gnodes:
            groups = [n.group for n in gnodes]
            self.document.delete_groups(groups)
        anodes = [n for n in selected_nodes if n.is_account]
        if anodes:
            accounts = [n.account for n in anodes]
            if any(a.entries for a in accounts):
                panel = self.parent_view.get_account_reassign_panel()
                panel.load(accounts)
            else:
                self.document.delete_accounts(accounts)

    def expand_node(self, node):
        self._expanded_paths.add(tuple(node.path))
        if node.is_group:
            self.parent_view.expand_group(node.group)

    def make_account_node(self, account):
        node = self._make_node(account.name)
        node.account = account
        node.is_account = True
        node.account_number = account.account_number
        node.is_excluded = account in self.document.excluded_accounts
        if not node.is_excluded:
            self._compute_account_node(node)
        return node

    def make_blank_node(self):
        node = self._make_node(None)
        node.is_blank = True
        return node

    def make_group_node(self, group):
        node = self._make_node(group.name)
        node.group = group
        node.is_group = True
        accounts = self.document.accounts.filter(group=group)
        for account in sorted(accounts):
            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 ') + group.name))
        node.append(self.make_blank_node())
        return node

    def make_total_node(self, name):
        node = self._make_node(name)
        node.is_total = True
        return node

    def make_type_node(self, name, type):
        node = self._make_node(name)
        node.type = type
        node.is_type = True
        for group in sorted(self.document.groups.filter(type=type)):
            node.append(self.make_group_node(group))
        for account in sorted(
                self.document.accounts.filter(type=type, group=None)):
            node.append(self.make_account_node(account))
        accounts = self.document.accounts.filter(type=type)
        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 ') + name))
        node.append(self.make_blank_node())
        return node

    def move(self, source_paths, dest_path):
        """Moves the nodes at 'source_paths' under the node at 'dest_path'."""
        assert self.can_move(source_paths, dest_path)
        accounts = [self.get_node(p).account for p in source_paths]
        dest_node = self.get_node(dest_path)
        if dest_node.is_type:
            self.document.change_accounts(accounts,
                                          group=None,
                                          type=dest_node.type)
        elif dest_node.is_group:
            self.document.change_accounts(accounts,
                                          group=dest_node.group,
                                          type=dest_node.group.type)

    def refresh(self, refresh_view=True):
        selected_accounts = self.selected_accounts
        selected_paths = self.selected_paths
        self._refresh()
        selected_nodes = []
        for account in selected_accounts:
            node_of_account = self._node_of_account(account)
            if node_of_account is not None:
                selected_nodes.append(node_of_account)
        if selected_nodes:
            self.selected_nodes = selected_nodes
        else:
            self.selected_paths = selected_paths
        self._prune_invalid_expanded_paths()
        if refresh_view:
            self.view.refresh()

    def restore_view(self):
        if not self._was_restored:
            if self._do_restore_view():
                self._was_restored = True

    def save_edits(self):
        node = self.edited
        if node is None:
            return
        self.edited = None
        assert node.is_account or node.is_group
        try:
            if node.is_account:
                self.document.change_accounts([node.account], name=node.name)
            else:
                self.document.change_group(node.group, name=node.name)
        except DuplicateAccountNameError:
            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.group.name
            self.mainwindow.show_message(msg)

    def save_preferences(self):
        # Save node expansion state
        prefname = '{}.ExpandedPaths'.format(self.SAVENAME)
        self.document.set_default(prefname, self.expanded_paths)
        self.columns.save_columns()

    def selection_as_csv(self):
        csvrows = []
        columns = (self.columns.coldata[colname]
                   for colname in self.columns.colnames)
        columns = [col for col in columns if col.visible]
        for node in self.selected_nodes:
            csvrow = []
            for col in columns:
                try:
                    csvrow.append(getattr(node, col.name))
                except AttributeError:
                    pass
            csvrows.append(csvrow)
        fp = StringIO()
        csv.writer(fp, delimiter='\t', quotechar='"').writerows(csvrows)
        fp.seek(0)
        return fp.read()

    def show_selected_account(self):
        self.mainwindow.open_account(self.selected_account)

    def show_account(self, path):
        node = self.get_node(path)
        self.mainwindow.open_account(node.account)

    def toggle_excluded(self):
        nodes = self.selected_nodes
        affected_accounts = set()
        for node in nodes:
            if node.is_type:
                affected_accounts |= set(
                    self.document.accounts.filter(type=node.type))
            elif node.is_group:
                affected_accounts |= set(
                    self.document.accounts.filter(group=node.group))
            elif node.is_account:
                affected_accounts.add(node.account)
        if affected_accounts:
            self.document.toggle_accounts_exclusion(affected_accounts)

    # --- Event handlers
    def account_added(self):
        self.refresh()

    def account_changed(self):
        self.refresh()

    def account_deleted(self):
        selected_path = self.selected_path
        self.refresh(refresh_view=False)
        next_node = self.get_node(selected_path)
        if not (next_node.is_account or next_node.is_group):
            selected_path[-1] -= 1
            if selected_path[-1] < 0:
                selected_path = selected_path[:-1]
        self.selected_path = selected_path
        self.view.refresh()

    def accounts_excluded(self):
        self.refresh()

    def date_range_changed(self):
        self.refresh()

    document_restoring_preferences = restore_view

    def edition_must_stop(self):
        self.view.stop_editing()
        self.save_edits()

    # account might have been auto-created during import
    def transactions_imported(self):
        self.refresh(refresh_view=False)
        self._select_first()
        self.view.refresh()

    def document_changed(self):
        self.refresh(refresh_view=False)
        self._select_first()
        self.view.refresh()

    def performed_undo_or_redo(self):
        self.refresh()

    # --- Properties
    @property
    def can_show_selected_account(self):
        return self.selected_account is not None

    @property
    def expanded_paths(self):
        paths = list(self._expanded_paths)
        # We want the paths in orthe of length so that the paths are correctly expanded in the gui.
        paths.sort(key=lambda p: (len(p), ) + p)
        return paths

    selected = tree.Tree.selected_node

    @property
    def selected_account(self):
        accounts = self.selected_accounts
        if accounts:
            return accounts[0]
        else:
            return None

    @property
    def selected_accounts(self):
        nodes = self.selected_nodes
        return [node.account for node in nodes if node.is_account]
예제 #16
0
 def __init__(self, exclude_list_dialog, app):
     GUITable.__init__(self)
     DupeGuruGUIObject.__init__(self, app)
     self.columns = Columns(self)
     self.dialog = exclude_list_dialog
예제 #17
0
 def __init__(self, app):
     GUIObject.__init__(self, app)
     GUITable.__init__(self)
     self.columns = Columns(self)
예제 #18
0
 def save_columns(self):
     Columns.save_columns(self)
     pref_name = '{}.Columns.debit_credit_mode'.format(self.savename)
     value = self.column_is_visible('debit')
     self.prefaccess.set_default(pref_name, value)
예제 #19
0
class Report(ViewChild, tree.Tree, SheetViewNotificationsMixin):
    SAVENAME = ''
    COLUMNS = []
    INVALIDATING_MESSAGES = MESSAGES_DOCUMENT_CHANGED | {'accounts_excluded', 'date_range_changed'}

    def __init__(self, parent_view):
        ViewChild.__init__(self, parent_view)
        tree.Tree.__init__(self)
        self._was_restored = False
        self.columns = Columns(self, prefaccess=parent_view.document, savename=self.SAVENAME)
        self.edited = None
        self._expanded_paths = {(0, ), (1, )}

    #--- Override
    def _do_restore_view(self):
        if not self.document.can_restore_from_prefs():
            return False
        prefname = '{}.ExpandedPaths'.format(self.SAVENAME)
        expanded = self.document.get_default(prefname, list())
        if expanded:
            self._expanded_paths = {tuple(p) for p in expanded}
            self.view.refresh_expanded_paths()
        self.columns.restore_columns()
        return True

    def _revalidate(self):
        self.refresh(refresh_view=False)
        self._update_selection()
        self.view.refresh()
        self.restore_view()

    #--- Virtual
    def _compute_account_node(self, node):
        pass

    def _make_node(self, name):
        node = Node(name)
        node.account_number = ''
        return node

    def _refresh(self):
        pass

    #--- Protected
    def _node_of_account(self, account):
        return self.find(lambda n: getattr(n, 'account', None) is account)

    def _prune_invalid_expanded_paths(self):
        newpaths = set()
        for path in self._expanded_paths:
            try:
                node = self.get_node(path)
                if node.is_group or node.is_type:
                    newpaths.add(path)
            except IndexError:
                pass
        self._expanded_paths = newpaths

    def _select_first(self):
        for type_node in self:
            if len(type_node) > 2: # total + blank
                self.selected = type_node[0]
                break

    def _update_selection(self):
        if not (isinstance(self.selected, Node) and self.selected.is_account):
            self._select_first()

    #--- Public
    def add_account(self):
        self.view.stop_editing()
        self.save_edits()
        node = self.selected
        if isinstance(node, Node) and node.is_group:
            account_type = node.group.type
            account_group = node.group
        elif isinstance(node, Node) and node.is_account:
            account_type = node.account.type
            account_group = node.account.group
        else:
            # there are only 2 types per report
            path = self.selected_path
            account_type = self[1].type if path and path[0] == 1 else self[0].type
            account_group = None
        if account_group is not None:
            account_group.expanded = True
        account = self.document.new_account(account_type, account_group) # refresh happens on account_added
        self.selected = self._node_of_account(account)
        self.view.update_selection()
        self.view.start_editing()

    def add_account_group(self):
        self.view.stop_editing()
        self.save_edits()
        node = self.selected
        if isinstance(node, Node) and node.is_group:
            account_type = node.group.type
        elif isinstance(node, Node) and node.is_account:
            account_type = node.account.type
        else:
            path = self.selected_path
            account_type = self[1].type if path and path[0] == 1 else self[0].type
        group = self.document.new_group(account_type)
        self.selected = self.find(lambda n: getattr(n, 'group', None) is group)
        self.view.update_selection()
        self.view.start_editing()

    def can_delete(self):
        node = self.selected
        return isinstance(node, Node) and (node.is_account or node.is_group)

    def can_move(self, source_paths, dest_path):
        """Returns whether it's possible to move the nodes at 'source_paths' under the node at
        'dest_path'.
        """
        if not dest_path:  # Don't move under the root
            return False
        dest_node = self.get_node(dest_path)
        if not (dest_node.is_group or dest_node.is_type):
            # Move only under a group node or a type node
            return False
        for source_path in source_paths:
            source_node = self.get_node(source_path)
            if not source_node.is_account:
                return False
            if source_node.parent is dest_node:  # Don't move under the same node
                return False
        return True

    def cancel_edits(self):
        node = self.edited
        if node is None:
            return
        assert node.is_account or node.is_group
        node.name = node.account.name if node.is_account else node.group.name
        self.edited = None

    def collapse_node(self, node):
        self._expanded_paths.discard(tuple(node.path))
        if node.is_group:
            self.parent_view.collapse_group(node.group)

    def delete(self):
        if not self.can_delete():
            return
        self.view.stop_editing()
        selected_nodes = self.selected_nodes
        gnodes = [n for n in selected_nodes if n.is_group]
        if gnodes:
            groups = [n.group for n in gnodes]
            self.document.delete_groups(groups)
        anodes = [n for n in selected_nodes if n.is_account]
        if anodes:
            accounts = [n.account for n in anodes]
            if any(a.entries for a in accounts):
                panel = self.parent_view.get_account_reassign_panel()
                panel.load(accounts)
            else:
                self.document.delete_accounts(accounts)

    def expand_node(self, node):
        self._expanded_paths.add(tuple(node.path))
        if node.is_group:
            self.parent_view.expand_group(node.group)

    def make_account_node(self, account):
        node = self._make_node(account.name)
        node.account = account
        node.is_account = True
        node.account_number = account.account_number
        node.is_excluded = account in self.document.excluded_accounts
        if not node.is_excluded:
            self._compute_account_node(node)
        return node

    def make_blank_node(self):
        node = self._make_node(None)
        node.is_blank = True
        return node

    def make_group_node(self, group):
        node = self._make_node(group.name)
        node.group = group
        node.is_group = True
        accounts = self.document.accounts.filter(group=group)
        for account in sorted(accounts):
            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 ') + group.name))
        node.append(self.make_blank_node())
        return node

    def make_total_node(self, name):
        node = self._make_node(name)
        node.is_total = True
        return node

    def make_type_node(self, name, type):
        node = self._make_node(name)
        node.type = type
        node.is_type = True
        for group in sorted(self.document.groups.filter(type=type)):
            node.append(self.make_group_node(group))
        for account in sorted(self.document.accounts.filter(type=type, group=None)):
            node.append(self.make_account_node(account))
        accounts = self.document.accounts.filter(type=type)
        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 ') + name))
        node.append(self.make_blank_node())
        return node

    def move(self, source_paths, dest_path):
        """Moves the nodes at 'source_paths' under the node at 'dest_path'."""
        assert self.can_move(source_paths, dest_path)
        accounts = [self.get_node(p).account for p in source_paths]
        dest_node = self.get_node(dest_path)
        if dest_node.is_type:
            self.document.change_accounts(accounts, group=None, type=dest_node.type)
        elif dest_node.is_group:
            self.document.change_accounts(accounts, group=dest_node.group, type=dest_node.group.type)

    def refresh(self, refresh_view=True):
        selected_accounts = self.selected_accounts
        selected_paths = self.selected_paths
        self._refresh()
        selected_nodes = []
        for account in selected_accounts:
            node_of_account = self._node_of_account(account)
            if node_of_account is not None:
                selected_nodes.append(node_of_account)
        if selected_nodes:
            self.selected_nodes = selected_nodes
        else:
            self.selected_paths = selected_paths
        self._prune_invalid_expanded_paths()
        if refresh_view:
            self.view.refresh()

    def restore_view(self):
        if not self._was_restored:
            if self._do_restore_view():
                self._was_restored = True

    def save_edits(self):
        node = self.edited
        if node is None:
            return
        self.edited = None
        assert node.is_account or node.is_group
        try:
            if node.is_account:
                self.document.change_accounts([node.account], name=node.name)
            else:
                self.document.change_group(node.group, name=node.name)
        except DuplicateAccountNameError:
            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.group.name
            self.mainwindow.show_message(msg)

    def save_preferences(self):
        # Save node expansion state
        prefname = '{}.ExpandedPaths'.format(self.SAVENAME)
        self.document.set_default(prefname, self.expanded_paths)
        self.columns.save_columns()

    def selection_as_csv(self):
        csvrows = []
        columns = (self.columns.coldata[colname] for colname in self.columns.colnames)
        columns = [col for col in columns if col.visible]
        for node in self.selected_nodes:
            csvrow = []
            for col in columns:
                try:
                    csvrow.append(getattr(node, col.name))
                except AttributeError:
                    pass
            csvrows.append(csvrow)
        fp = StringIO()
        csv.writer(fp, delimiter='\t', quotechar='"').writerows(csvrows)
        fp.seek(0)
        return fp.read()

    def show_selected_account(self):
        self.mainwindow.open_account(self.selected_account)

    def show_account(self, path):
        node = self.get_node(path)
        self.mainwindow.open_account(node.account)

    def toggle_excluded(self):
        nodes = self.selected_nodes
        affected_accounts = set()
        for node in nodes:
            if node.is_type:
                affected_accounts |= set(self.document.accounts.filter(type=node.type))
            elif node.is_group:
                affected_accounts |= set(self.document.accounts.filter(group=node.group))
            elif node.is_account:
                affected_accounts.add(node.account)
        if affected_accounts:
            self.document.toggle_accounts_exclusion(affected_accounts)

    #--- Event handlers
    def account_added(self):
        self.refresh()

    def account_changed(self):
        self.refresh()

    def account_deleted(self):
        selected_path = self.selected_path
        self.refresh(refresh_view=False)
        next_node = self.get_node(selected_path)
        if not (next_node.is_account or next_node.is_group):
            selected_path[-1] -= 1
            if selected_path[-1] < 0:
                selected_path = selected_path[:-1]
        self.selected_path = selected_path
        self.view.refresh()

    def accounts_excluded(self):
        self.refresh()

    def date_range_changed(self):
        self.refresh()

    document_restoring_preferences = restore_view

    def edition_must_stop(self):
        self.view.stop_editing()
        self.save_edits()

    # account might have been auto-created during import
    def transactions_imported(self):
        self.refresh(refresh_view=False)
        self._select_first()
        self.view.refresh()

    def document_changed(self):
        self.refresh(refresh_view=False)
        self._select_first()
        self.view.refresh()

    def performed_undo_or_redo(self):
        self.refresh()

    #--- Properties
    @property
    def can_show_selected_account(self):
        return self.selected_account is not None

    @property
    def expanded_paths(self):
        paths = list(self._expanded_paths)
        # We want the paths in orthe of length so that the paths are correctly expanded in the gui.
        paths.sort(key=lambda p: (len(p), ) + p)
        return paths

    selected = tree.Tree.selected_node

    @property
    def selected_account(self):
        accounts = self.selected_accounts
        if accounts:
            return accounts[0]
        else:
            return None

    @property
    def selected_accounts(self):
        nodes = self.selected_nodes
        return [node.account for node in nodes if node.is_account]
예제 #20
0
class GUITable(GUITableBase):
    SAVENAME = ''
    COLUMNS = []

    def __init__(self, document):
        GUITableBase.__init__(self)
        self.document = document
        self.columns = Columns(self, prefaccess=document, savename=self.SAVENAME)

    def can_move(self, row_indexes, position):
        if not 0 <= position <= len(self):
            return False
        row_indexes.sort()
        first_index = row_indexes[0]
        last_index = row_indexes[-1]
        has_gap = row_indexes != list(range(first_index, first_index + len(row_indexes)))
        # When there is a gap, position can be in the middle of row_indexes
        if not has_gap and position in (row_indexes + [last_index + 1]):
            return False
        return True

    def refresh_and_show_selection(self):
        self.refresh()
        self.view.show_selected_row()

    def selection_as_csv(self):
        csvrows = []
        columns = (self.columns.coldata[colname] for colname in self.columns.colnames)
        columns = [col for col in columns if col.visible]
        for row in self.selected_rows:
            csvrow = []
            for col in columns:
                try:
                    csvrow.append(row.get_cell_value(col.name))
                except AttributeError:
                    pass
            csvrows.append(csvrow)
        fp = StringIO()
        csv.writer(fp, delimiter='\t', quotechar='"').writerows(csvrows)
        fp.seek(0)
        return fp.read()

    # --- Event handlers
    def edition_must_stop(self):
        self.view.stop_editing()
        self.save_edits()

    def document_changed(self):
        self.refresh()

    def document_restoring_preferences(self):
        self.columns.restore_columns()

    def performed_undo_or_redo(self):
        self.refresh()

    # Plug these below to the appropriate event in subclasses
    def _filter_applied(self):
        self.refresh(refresh_view=False)
        self._update_selection()
        self.view.refresh()

    def _item_changed(self):
        self.refresh_and_show_selection()

    def _item_deleted(self):
        self.refresh(refresh_view=False)
        self._update_selection()
        self.view.refresh()
예제 #21
0
 def __init__(self, ignore_list_dialog):
     GUITable.__init__(self)
     self.columns = Columns(self)
     self.view = None
     self.dialog = ignore_list_dialog
예제 #22
0
 def __init__(self, problem_dialog):
     GUITable.__init__(self)
     self.columns = Columns(self)
     self.dialog = problem_dialog
예제 #23
0
 def menu_items(self):
     items = Columns.menu_items(self)
     marked = self.column_is_visible('debit')
     items.append((tr("Debit/Credit"), marked))
     return items