def test_save_and_load(self): # previously, when reloading matches, they wouldn't be reloaded as namedtuples f = io.BytesIO() self.results.save_to_xml(f) f.seek(0) self.results.load_from_xml(f, self.get_file) first(self.results.groups[0].matches).percentage
def test_simple(self): l = [NamedObject("foo bar"), NamedObject("bar bleh"), NamedObject("a b c foo")] r = getmatches(l) eq_(2, len(r)) m = first(m for m in r if m.percentage == 50) #"foo bar" and "bar bleh" assert_match(m, 'foo bar', 'bar bleh') m = first(m for m in r if m.percentage == 33) #"foo bar" and "a b c foo" assert_match(m, 'foo bar', 'a b c foo')
def test_simple(self): l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")] r = getmatches(l) eq_(2,len(r)) m = first(m for m in r if m.percentage == 50) #"foo bar" and "bar bleh" assert_match(m, 'foo bar', 'bar bleh') m = first(m for m in r if m.percentage == 33) #"foo bar" and "a b c foo" assert_match(m, 'foo bar', 'a b c foo')
def amount(self, value): assert self.can_set_amount if value == self.amount: return debit = first(s for s in self.splits if s.debit) credit = first(s for s in self.splits if s.credit) debit_account = debit.account if debit is not None else None credit_account = credit.account if credit is not None else None self.splits = [Split(self, debit_account, value), Split(self, credit_account, -value)]
def amount(self, value): assert self.can_set_amount if value == self.amount: return debit = first(s for s in self.splits if s.debit) credit = first(s for s in self.splits if s.credit) debit_account = debit.account if debit is not None else None credit_account = credit.account if credit is not None else None self.splits = [ Split(self, debit_account, value), Split(self, credit_account, -value) ]
def test_simple(self): itemList = [ NamedObject("foo bar"), NamedObject("bar bleh"), NamedObject("a b c foo"), ] r = getmatches(itemList) eq_(2, len(r)) m = first(m for m in r if m.percentage == 50) # "foo bar" and "bar bleh" assert_match(m, "foo bar", "bar bleh") m = first(m for m in r if m.percentage == 33) # "foo bar" and "a b c foo" assert_match(m, "foo bar", "a b c foo")
def balance(self, strong_split=None, keep_two_splits=False): # strong_split is the split that was last edited. # keep_two_splits is a flag that, if enabled and that a strong_split is defined, causes the # balance process to set the weak split to the invert value of the strong one so that we # don't end up with a third unassigned split. # When the flag is false and for the special case where there is 2 splits on the same # "side" and a strong split, we reverse the weak split. if len(self.splits) == 2 and strong_split is not None: weak_split = self.splits[0] if self.splits[0] is not strong_split else self.splits[1] if keep_two_splits: weak_split.amount = -strong_split.amount elif (weak_split.amount > 0) == (strong_split.amount > 0): # on the same side weak_split.amount *= -1 splits_with_amount = [s for s in self.splits if s.amount] if splits_with_amount and not allsame(s.amount.currency for s in splits_with_amount): self.balance_currencies(strong_split) return imbalance = sum(s.amount for s in self.splits) if not imbalance: return is_unassigned = lambda s: s.account is None and s is not strong_split imbalance = sum(s.amount for s in self.splits) if imbalance: unassigned = first(s for s in self.splits if is_unassigned(s)) if unassigned is not None: unassigned.amount -= imbalance else: self.splits.append(Split(self, None, -imbalance)) for split in self.splits[:]: if is_unassigned(split) and split.amount == 0: self.splits.remove(split)
def selected_target_index(self): target_name = self.layout.target_account_name if not target_name: return 0 index = first(i for i, t in enumerate(self._target_accounts) if t.name == target_name) return index + 1 if index is not None else 0
def save_edits(self): elements = self.app.selected_elements len(elements) == 1 first(elements).text = self.edit_text # XXX Ok, this is a horrible hack, but I'll straighten this out later # we need to refresh the elements table self.app.notify('elements_changed')
def select_pane_of_type(self, pane_type, clear_filter=True): if clear_filter: self.document.filter_string = '' index = first(i for i, p in enumerate(self.panes) if p.view.VIEW_TYPE == pane_type) if index is None: self._add_pane(self._create_pane(pane_type)) else: self.current_pane_index = index
def open_selected_plugin(self): index = self.plugin_list.selected_index if index is None: return plugin_name = self.plugin_list[index] plugin = first(p for p in self.mainwindow.app.plugins if p.NAME == plugin_name) if plugin is not None: self.mainwindow.set_current_pane_with_plugin(plugin)
def account_changed(self): self._undo_stack_changed() tochange = first( p for p in self.panes if p.account is not None and p.account.name != p.label) if tochange is not None: tochange.label = tochange.account.name self.view.refresh_panes()
def assign_imbalance(self): """Assigns remaining imbalance to the selected split. If the selected split is not an assigned split, does nothing. """ split = first(self._selected_splits) if split is not None: self.transaction.assign_imbalance(split) self.split_table.refresh_splits()
def open_selected_plugin(self): index = self.plugin_list.selected_index if index is None: return plugin_name = self.plugin_list[index] plugin = first(p for p in self.mainwindow.app.get_enabled_plugins() if p.NAME == plugin_name) if plugin is not None: self.mainwindow.set_current_pane_with_plugin(plugin)
def balance(self, strong_split=None, keep_two_splits=False): """Balance out :attr:`splits` if needed. A balanced transaction has all its splits making a zero sum. Balancing a transaction is rather easy: We sum all our splits and create an unassigned split of the opposite of that amount. To avoid polluting our splits, we look if we already have an unassigned split and, if we do, we adjust its amount instead of creating a new split. There's a special case to that rule, and that is when we have two splits. When those two splits are on the same "side" (both positive or both negative), we assume that the user has just reversed ``strong_split``'s side and that the logical next step is to also reverse the other split (the "weak" split), which we'll do. If ``keep_two_splits`` is true, we'll go one step further and adjust the weak split's amount to fit what was just entered in the strong split. If it's false, we'll create an unassigned split if needed. Easy, right? Things get a bit more complicated when a have a :ref:`multi-currency transaction <multi-currency-txn>`. When that happens, we do a more complicated balancing, which happens in :meth:`balance_currencies`. :param strong_split: The split that was last edited. The reason why we're balancing the transaction now. If set, it will not be adjusted by the balancing because we don't want to pull the rug from under our user's feet and undo an edit he's just made. :type strong_split: :class:`Split` :param bool keep_two_splits: If set and if we have a two-split transaction, we'll keep it that way, adjusting the "weak" split amount as needed. """ if len(self.splits) == 2 and strong_split is not None: weak_split = self.splits[0] if self.splits[ 0] is not strong_split else self.splits[1] if keep_two_splits: weak_split.amount = -strong_split.amount elif (weak_split.amount > 0) == (strong_split.amount > 0): # on the same side weak_split.amount *= -1 splits_with_amount = [s for s in self.splits if s.amount] if splits_with_amount and not allsame(s.amount.currency for s in splits_with_amount): self.balance_currencies(strong_split) return imbalance = sum(s.amount for s in self.splits) if not imbalance: return is_unassigned = lambda s: s.account is None and s is not strong_split imbalance = sum(s.amount for s in self.splits) if imbalance: unassigned = first(s for s in self.splits if is_unassigned(s)) if unassigned is not None: unassigned.amount -= imbalance else: self.splits.append(Split(self, None, -imbalance)) for split in self.splits[:]: if is_unassigned(split) and split.amount == 0: self.splits.remove(split)
def mct_balance(self, new_split_currency): # when calling this, the transaction is supposed to be balanced with balance() already converted_amounts = (convert_amount(split.amount, new_split_currency, self.date) for split in self.splits) converted_total = sum(converted_amounts) if converted_total != 0: target = first(s for s in self.splits if (s.account is None) and of_currency(s.amount, new_split_currency)) if target is not None: target.amount -= converted_total else: self.splits.append(Split(self, None, -converted_total))
def open_account(self, account): if account is not None: # Try to find a suitable pane, or add a new one index = first(i for i, p in enumerate(self.panes) if p.account is account) if index is None: self._add_pane(self._create_pane(PaneType.Account, account)) else: self.current_pane_index = index elif self._current_pane.view.VIEW_TYPE == PaneType.Account: self.select_pane_of_type(PaneType.NetWorth)
def elements_selected(self): elements = self.app.selected_elements self.edit_enabled = False if not elements: self.edit_text = '' elif len(elements) == 1: self.edit_text = first(elements).text self.edit_enabled = True else: self.edit_text = "(Multiple selection)" self.view.refresh_edit_text()
def mct_balance(self): """Balances the mct by using xchange rates. The currency of the new split is the currency of the currently selected split. """ self.split_table.edition_must_stop() split = first(self._selected_splits) new_split_currency = self.document.default_currency if split is not None and split.amount != 0: new_split_currency = split.amount.currency self.transaction.mct_balance(new_split_currency) self.split_table.refresh_splits()
def balance(self, strong_split=None, keep_two_splits=False): """Balance out :attr:`splits` if needed. A balanced transaction has all its splits making a zero sum. Balancing a transaction is rather easy: We sum all our splits and create an unassigned split of the opposite of that amount. To avoid polluting our splits, we look if we already have an unassigned split and, if we do, we adjust its amount instead of creating a new split. There's a special case to that rule, and that is when we have two splits. When those two splits are on the same "side" (both positive or both negative), we assume that the user has just reversed ``strong_split``'s side and that the logical next step is to also reverse the other split (the "weak" split), which we'll do. If ``keep_two_splits`` is true, we'll go one step further and adjust the weak split's amount to fit what was just entered in the strong split. If it's false, we'll create an unassigned split if needed. Easy, right? Things get a bit more complicated when a have a :ref:`multi-currency transaction <multi-currency-txn>`. When that happens, we do a more complicated balancing, which happens in :meth:`balance_currencies`. :param strong_split: The split that was last edited. The reason why we're balancing the transaction now. If set, it will not be adjusted by the balancing because we don't want to pull the rug from under our user's feet and undo an edit he's just made. :type strong_split: :class:`Split` :param bool keep_two_splits: If set and if we have a two-split transaction, we'll keep it that way, adjusting the "weak" split amount as needed. """ if len(self.splits) == 2 and strong_split is not None: weak_split = self.splits[0] if self.splits[0] is not strong_split else self.splits[1] if keep_two_splits: weak_split.amount = -strong_split.amount elif (weak_split.amount > 0) == (strong_split.amount > 0): # on the same side weak_split.amount *= -1 splits_with_amount = [s for s in self.splits if s.amount] if splits_with_amount and not allsame(s.amount.currency for s in splits_with_amount): self.balance_currencies(strong_split) return imbalance = sum(s.amount for s in self.splits) if not imbalance: return is_unassigned = lambda s: s.account is None and s is not strong_split imbalance = sum(s.amount for s in self.splits) if imbalance: unassigned = first(s for s in self.splits if is_unassigned(s)) if unassigned is not None: unassigned.amount -= imbalance else: self.splits.append(Split(self, None, -imbalance)) for split in self.splits[:]: if is_unassigned(split) and split.amount == 0: self.splits.remove(split)
def select_layout(self, name): if not name: new_layout = self._default_layout else: new_layout = first(layout for layout in self._layouts if layout.name == name) if new_layout is self.layout: return self.layout = new_layout self.layout.adjust_columns(self._colcount) self.view.refresh_columns_name() self.view.refresh_lines() self.view.refresh_targets()
def setAccelKeys(menu): actions = menu.actions() titles = [a.text() for a in actions] available_characters = {c.lower() for s in titles for c in s if c.isalpha()} for action in actions: text = action.text() c = first(c for c in text if c.lower() in available_characters) if c is None: continue i = text.index(c) newtext = text[:i] + '&' + text[i:] available_characters.remove(c.lower()) action.setText(newtext)
def continue_import(self): loader = self.document.loader loader.columns = self.layout.columns lines = [line for index, line in enumerate(self.lines) if not self.line_is_excluded(index)] loader.lines = lines target_name = self.layout.target_account_name loader.target_account = first(t for t in self._target_accounts if t.name == target_name) try: self.document.load_parsed_file_for_import() except FileLoadError as e: self.view.show_message(str(e)) else: self.view.hide()
def fit(self, element, expandH=False, expandV=False): # Go through all available rects and take the first one that fits. fits = lambda rect: rect.width() >= element.rect.width() and rect.height() >= element.rect.height() fittingRect = first(r for r in self.availableRects if fits(r)) if fittingRect is None: return False element.rect.moveTopLeft(fittingRect.topLeft()) if expandH: element.rect.setWidth(fittingRect.width()) if expandV: element.rect.setHeight(fittingRect.height()) self.elements.append(element) element.placed() self._computeAvailableRects() return True
def _firstEditableIndex(self, originalIndex, columnIndexes=None): """Returns the first editable index in `originalIndex`'s row or None. If `columnIndexes` is not None, the scan for an editable index will be limited to these columns. """ model = self.model() h = self._headerView() editedRow = originalIndex.row() if columnIndexes is None: columnIndexes = (h.logicalIndex(i) for i in range(h.count())) create = lambda col: model.createIndex(editedRow, col, originalIndex.internalPointer()) scannedIndexes = (create(i) for i in columnIndexes if not h.isSectionHidden(i)) editableIndex = first(index for index in scannedIndexes if model.flags(index) & Qt.ItemIsEditable) return editableIndex
def _do_delete_dupe(self, dupe, *args): if isinstance(dupe, IPhoto): try: a = app('iPhoto') album = a.photo_library_album() if album is None: msg = "There are communication problems with iPhoto. Try opening iPhoto first, it might solve it." raise EnvironmentError(msg) [photo] = album.photos[its.image_path == str(dupe.path)]() a.remove(photo, timeout=0) except ValueError: msg = "Could not find photo '{}' in iPhoto Library".format( str(dupe.path)) raise EnvironmentError(msg) except (CommandError, RuntimeError) as e: raise EnvironmentError(str(e)) if isinstance(dupe, AperturePhoto): try: a = app('Aperture') # I'm flying blind here. In my own test library, all photos are in an album with the # id "LibraryFolder", so I'm going to guess that it's the case at least most of the # time. As a safeguard, if we don't find any library with that id, we'll use the # first album. # Now, about deleting: All attempts I've made at sending photos to trash failed, # even with normal applescript. So, what we're going to do here is to create a # "dupeGuru Trash" project and tell the user to manually send those photos to trash. libraries = a.libraries() library = first(l for l in libraries if l.id == 'LibraryFolder') if library is None: library = libraries[0] trash_project = a.projects["dupeGuru Trash"] if trash_project.exists(): trash_project = trash_project() else: trash_project = library.make( new=k.project, with_properties={k.name: "dupeGuru Trash"}) [photo] = library.image_versions[its.id == dupe.db_id]() photo.move(to=trash_project) except (IndexError, ValueError): msg = "Could not find photo '{}' in Aperture Library".format( str(dupe.path)) raise EnvironmentError(msg) except (CommandError, RuntimeError) as e: raise EnvironmentError(str(e)) else: DupeGuruBase._do_delete_dupe(self, dupe, *args)
def fit(self, element, expandH=False, expandV=False): # Go through all available rects and take the first one that fits. fits = lambda rect: rect.width() >= element.rect.width( ) and rect.height() >= element.rect.height() fittingRect = first(r for r in self.availableRects if fits(r)) if fittingRect is None: return False element.rect.moveTopLeft(fittingRect.topLeft()) if expandH: element.rect.setWidth(fittingRect.width()) if expandV: element.rect.setHeight(fittingRect.height()) self.elements.append(element) element.placed() self._computeAvailableRects() return True
def setAccelKeys(menu): actions = menu.actions() titles = [a.text() for a in actions] available_characters = { c.lower() for s in titles for c in s if c.isalpha() } for action in actions: text = action.text() c = first(c for c in text if c.lower() in available_characters) if c is None: continue i = text.index(c) newtext = text[:i] + '&' + text[i:] available_characters.remove(c.lower()) action.setText(newtext)
def continue_import(self): loader = self.mainwindow.loader loader.columns = self.layout.columns lines = [ line for index, line in enumerate(self.lines) if not self.line_is_excluded(index) ] loader.lines = lines target_name = self.layout.target_account_name loader.target_account = first(t for t in self._target_accounts if t.name == target_name) try: self.mainwindow.load_parsed_file_for_import() except FileLoadError as e: self.view.show_message(str(e)) else: self.view.hide()
def refresh_panes(self): if not hasattr(self.document, 'loader'): return self.refresh_targets() accounts = [a for a in self.document.loader.accounts if a.is_balance_sheet_account() and a.entries] parsing_date_format = DateFormat.from_sysformat(self.document.loader.parsing_date_format) for account in accounts: target_account = None if self.document.loader.target_account is not None: target_account = self.document.loader.target_account elif account.reference: target_account = first(t for t in self.target_accounts if t.reference == account.reference) self.panes.append(AccountPane(account, target_account, parsing_date_format)) # XXX Should replace by _update_selected_pane()? self._refresh_target_selection() self._refresh_swap_list_items() self.import_table.refresh()
def balance_currencies(self, strong_split=None): """Balances a :ref:`multi-currency transaction <multi-currency-txn>`. Balancing out multi-currencies transasctions can be real easy because we consider that currencies can never mix (and we would never make the gross mistake of using market exchange rates to do our balancing), so, if we have at least one split on each side of different currencies, we consider ourselves balanced and do nothing. However, we might be in a situation of "logical imbalance", which means that the transaction doesn't logically makes sense. For example, if all our splits are on the same side, we can't possibly balance out. If we have EUR and CAD splits, that CAD splits themselves balance out but that EUR splits are all on the same side, we have a logical imbalance. This method finds those imbalance and fix them by creating unsassigned splits balancing out every currency being in that situation. :param strong_split: The split that was last edited. See :meth:`balance`. :type strong_split: :class:`Split` """ splits_with_amount = [s for s in self.splits if s.amount != 0] if not splits_with_amount: return currency2balance = defaultdict(int) for split in splits_with_amount: currency2balance[split.amount.currency] += split.amount imbalanced = stripfalse(currency2balance.values() ) # filters out zeros (balances currencies) # For a logical imbalance to be possible, all imbalanced amounts must be on the same side if imbalanced and allsame(amount > 0 for amount in imbalanced): unassigned = [ s for s in self.splits if s.account is None and s is not strong_split ] for amount in imbalanced: split = first( s for s in unassigned if s.amount == 0 or s.amount.currency == amount.currency) if split is not None: if split.amount == amount: # we end up with a null split, remove it self.splits.remove(split) else: split.amount -= amount # adjust else: self.splits.append(Split(self, None, -amount))
def _do_delete_dupe(self, dupe, *args): if isinstance(dupe, IPhoto): try: a = app("iPhoto") album = a.photo_library_album() if album is None: msg = "There are communication problems with iPhoto. Try opening iPhoto first, it might solve it." raise EnvironmentError(msg) [photo] = album.photos[its.image_path == str(dupe.path)]() a.remove(photo, timeout=0) except ValueError: msg = "Could not find photo '{}' in iPhoto Library".format(str(dupe.path)) raise EnvironmentError(msg) except (CommandError, RuntimeError) as e: raise EnvironmentError(str(e)) if isinstance(dupe, AperturePhoto): try: a = app("Aperture") # I'm flying blind here. In my own test library, all photos are in an album with the # id "LibraryFolder", so I'm going to guess that it's the case at least most of the # time. As a safeguard, if we don't find any library with that id, we'll use the # first album. # Now, about deleting: All attempts I've made at sending photos to trash failed, # even with normal applescript. So, what we're going to do here is to create a # "dupeGuru Trash" project and tell the user to manually send those photos to trash. libraries = a.libraries() library = first(l for l in libraries if l.id == "LibraryFolder") if library is None: library = libraries[0] trash_project = a.projects["dupeGuru Trash"] if trash_project.exists(): trash_project = trash_project() else: trash_project = library.make(new=k.project, with_properties={k.name: "dupeGuru Trash"}) [photo] = library.image_versions[its.id == dupe.db_id]() photo.move(to=trash_project) except (IndexError, ValueError): msg = "Could not find photo '{}' in Aperture Library".format(str(dupe.path)) raise EnvironmentError(msg) except (CommandError, RuntimeError) as e: raise EnvironmentError(str(e)) else: DupeGuruBase._do_delete_dupe(self, dupe, *args)
def balance_currencies(self, strong_split=None): splits_with_amount = [s for s in self.splits if s.amount != 0] if not splits_with_amount: return currency2balance = defaultdict(int) for split in splits_with_amount: currency2balance[split.amount.currency] += split.amount imbalanced = stripfalse(currency2balance.values()) # filters out zeros (balances currencies) # For a logical imbalance to be possible, all imbalanced amounts must be on the same side if imbalanced and allsame(amount > 0 for amount in imbalanced): unassigned = [s for s in self.splits if s.account is None and s is not strong_split] for amount in imbalanced: split = first(s for s in unassigned if s.amount == 0 or s.amount.currency == amount.currency) if split is not None: if split.amount == amount: # we end up with a null split, remove it self.splits.remove(split) else: split.amount -= amount # adjust else: self.splits.append(Split(self, None, -amount))
def mct_balance(self, new_split_currency): """Balances a :ref:`multi-currency transaction <multi-currency-txn>` using exchange rates. *This balancing doesn't occur automatically, it is a user-initiated action.* Sums up the value of all splits in ``new_split_currency``, using exchange rates for :attr:`date`. If not zero, create a new unassigned split with the opposite of that amount. Of course, we need to have called :meth:`balance` before we can call this. :param new_split_currency: :class:`.Currency` """ converted_amounts = (convert_amount(split.amount, new_split_currency, self.date) for split in self.splits) converted_total = sum(converted_amounts) if converted_total != 0: target = first(s for s in self.splits if (s.account is None) and of_currency(s.amount, new_split_currency)) if target is not None: target.amount -= converted_total else: self.splits.append(Split(self, None, -converted_total))
def _set_panes(self, pane_data): # Replace opened panes with new panes from `pane_data`, which is a [(pane_type, arg)] self._current_pane = None self._current_pane_index = -1 for pane in self.panes: pane.view.disconnect() self.panes = [] for pane_type, arg in pane_data: if pane_type >= PaneType.Plugin: plugin = first(p for p in self.app.plugins if p.NAME == arg) if plugin is not None: self.panes.append(self._create_pane_from_plugin(plugin)) else: self.panes.append(self._create_pane(PaneType.NetWorth)) else: try: self.panes.append(self._create_pane(pane_type, account=arg)) except ValueError: self.panes.append(self._create_pane(PaneType.NetWorth)) self.view.refresh_panes() self.current_pane_index = 0
def copy_or_move(self, dupe, copy: bool, destination: str, dest_type: DestType): source_path = dupe.path location_path = first(p for p in self.directories if dupe.path in p) dest_path = Path(destination) if dest_type in {DestType.Relative, DestType.Absolute}: # no filename, no windows drive letter source_base = source_path.remove_drive_letter().parent() if dest_type == DestType.Relative: source_base = source_base[location_path:] dest_path = dest_path[source_base] if not dest_path.exists(): dest_path.makedirs() # Add filename to dest_path. For file move/copy, it's not required, but for folders, yes. dest_path = dest_path[source_path.name] logging.debug("Copy/Move operation from '%s' to '%s'", source_path, dest_path) # Raises an EnvironmentError if there's a problem if copy: smart_copy(source_path, dest_path) else: smart_move(source_path, dest_path) self.clean_empty_dirs(source_path.parent())
def copy_or_move(self, dupe, copy: bool, destination: str, dest_type: DestType): source_path = dupe.path location_path = first(p for p in self.directories if dupe.path in p) dest_path = Path(destination) if dest_type in {DestType.Relative, DestType.Absolute}: # no filename, no windows drive letter source_base = source_path.remove_drive_letter()[:-1] if dest_type == DestType.Relative: source_base = source_base[location_path:] dest_path = dest_path + source_base if not dest_path.exists(): dest_path.makedirs() # Add filename to dest_path. For file move/copy, it's not required, but for folders, yes. dest_path = dest_path + source_path[-1] logging.debug("Copy/Move operation from '%s' to '%s'", source_path, dest_path) # Raises an EnvironmentError if there's a problem if copy: smart_copy(source_path, dest_path) else: smart_move(source_path, dest_path) self.clean_empty_dirs(source_path[:-1])
def balance_currencies(self, strong_split=None): """Balances a :ref:`multi-currency transaction <multi-currency-txn>`. Balancing out multi-currencies transasctions can be real easy because we consider that currencies can never mix (and we would never make the gross mistake of using market exchange rates to do our balancing), so, if we have at least one split on each side of different currencies, we consider ourselves balanced and do nothing. However, we might be in a situation of "logical imbalance", which means that the transaction doesn't logically makes sense. For example, if all our splits are on the same side, we can't possibly balance out. If we have EUR and CAD splits, that CAD splits themselves balance out but that EUR splits are all on the same side, we have a logical imbalance. This method finds those imbalance and fix them by creating unsassigned splits balancing out every currency being in that situation. :param strong_split: The split that was last edited. See :meth:`balance`. :type strong_split: :class:`Split` """ splits_with_amount = [s for s in self.splits if s.amount != 0] if not splits_with_amount: return currency2balance = defaultdict(int) for split in splits_with_amount: currency2balance[split.amount.currency] += split.amount imbalanced = stripfalse(currency2balance.values()) # filters out zeros (balances currencies) # For a logical imbalance to be possible, all imbalanced amounts must be on the same side if imbalanced and allsame(amount > 0 for amount in imbalanced): unassigned = [s for s in self.splits if s.account is None and s is not strong_split] for amount in imbalanced: split = first(s for s in unassigned if s.amount == 0 or s.amount.currency == amount.currency) if split is not None: if split.amount == amount: # we end up with a null split, remove it self.splits.remove(split) else: split.amount -= amount # adjust else: self.splits.append(Split(self, None, -amount))
def _set_panes(self, pane_data): # Replace opened panes with new panes from `pane_data`, which is a [(pane_type, arg)] self._current_pane = None self._current_pane_index = -1 for pane in self.panes: pane.view.disconnect() self.panes = [] for pane_type, arg in pane_data: if pane_type >= PaneType.Plugin: plugin = first(p for p in self.app.get_enabled_plugins() if p.plugin_id() == arg) if plugin is not None: self.panes.append(self._create_pane_from_plugin(plugin)) else: self.panes.append(self._create_pane(PaneType.NetWorth)) else: try: self.panes.append(self._create_pane(pane_type, account=arg)) except ValueError: self.panes.append(self._create_pane(PaneType.NetWorth)) self.view.refresh_panes() self.current_pane_index = 0
def _load(self): budget = first(self.mainwindow.selected_budgets) self._load_budget(budget)
def _load(self): schedule = first(self.mainwindow.selected_schedules) self._load_schedule(schedule)
def account_changed(self): self._undo_stack_changed() tochange = first(p for p in self.panes if p.account is not None and p.account.name != p.label) if tochange is not None: tochange.label = tochange.account.name self.view.refresh_panes()
def get_line(self, line_header): return first(line for line in self.lines if line.header == line_header)
def delete_entries(self, entries): from_account = first(entries).account transactions = dedupe(e.transaction for e in entries) self.delete_transactions(transactions, from_account=from_account)