def is_mct(self): """*readonly*. ``bool``. Whether our splits contain more than one currency.""" splits_with_amount = (s for s in self.splits if s.amount != 0) try: return not allsame(s.amount.currency for s in splits_with_amount) except ValueError: # no split with amount return False
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 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 can_move_transactions(self, transactions, before, after): assert transactions if any(isinstance(txn, Spawn) for txn in transactions): return False if not allsame(txn.date for txn in transactions): return False from_date = transactions[0].date before_date = before.date if before else None after_date = after.date if after else None return from_date in (before_date, after_date)
def toggle_selected_mark_state(self): selected = self.without_ref(self.selected_dupes) if not selected: return if allsame(self.results.is_marked(d) for d in selected): markfunc = self.results.mark_toggle else: markfunc = self.results.mark for dupe in selected: markfunc(dupe) self.notify('marking_changed')
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 _load(self): transactions = self.mainwindow.selected_transactions if len(transactions) < 2: raise OperationAborted() self.can_change_accounts = all(len(t.splits) == 2 for t in transactions) self.can_change_amount = all(t.can_set_amount for t in transactions) self.date_field.value = date.today() self.description_field.text = '' self.payee_field.text = '' self.checkno_field.text = '' self.from_field.text = '' self.to_field.text = '' self.amount_field.value = 0 self.currency = None first = transactions[0] if allsame(t.date for t in transactions): self.date_field.value = first.date if allsame(t.description for t in transactions): self.description_field.text = first.description if allsame(t.payee for t in transactions): self.payee_field.text = first.payee if allsame(t.checkno for t in transactions): self.checkno_field.text = first.checkno splits = flatten(t.splits for t in transactions) splits = [s for s in splits if s.amount] if splits and allsame(s.amount.currency for s in splits): self.currency = splits[0].amount.currency else: self.currency = self.document.default_currency self.currency_list.select(Currency.all.index(self.currency)) if self.can_change_accounts: def get_from(t): s1, s2 = t.splits return s1 if s1.amount <=0 else s2 def get_to(t): s1, s2 = t.splits return s2 if s1.amount <=0 else s1 def get_name(split): return split.account.name if split.account is not None else '' if allsame(get_name(get_from(t)) for t in transactions): self.from_field.text = get_name(get_from(first)) if allsame(get_name(get_to(t)) for t in transactions): self.to_field.text = get_name(get_to(first)) if self.can_change_amount: if allsame(t.amount for t in transactions): self.amount_field.value = first.amount self._init_checkboxes()
def _load(self, transactions): assert len(transactions) >= 2 self.can_change_accounts = all( len(t.splits) == 2 for t in transactions) self.can_change_amount = all(t.can_set_amount for t in transactions) self.date_field.value = date.today() self.description_field.text = '' self.payee_field.text = '' self.checkno_field.text = '' self.from_field.text = '' self.to_field.text = '' self.amount_field.value = 0 self.currency = None first = transactions[0] if allsame(t.date for t in transactions): self.date_field.value = first.date if allsame(t.description for t in transactions): self.description_field.text = first.description if allsame(t.payee for t in transactions): self.payee_field.text = first.payee if allsame(t.checkno for t in transactions): self.checkno_field.text = first.checkno splits = flatten(t.splits for t in transactions) splits = [s for s in splits if s.amount] if splits and allsame(s.amount.currency for s in splits): self.currency = splits[0].amount.currency else: self.currency = self.document.default_currency self.currency_list.select(Currency.all.index(self.currency)) if self.can_change_accounts: def get_from(t): s1, s2 = t.splits return s1 if s1.amount <= 0 else s2 def get_to(t): s1, s2 = t.splits return s2 if s1.amount <= 0 else s1 def get_name(split): return split.account.name if split.account is not None else '' if allsame(get_name(get_from(t)) for t in transactions): self.from_field.text = get_name(get_from(first)) if allsame(get_name(get_to(t)) for t in transactions): self.to_field.text = get_name(get_to(first)) if self.can_change_amount: if allsame(t.amount for t in transactions): self.amount_field.value = first.amount self._init_checkboxes()
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 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 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 is_mct(self): splits_with_amount = (s for s in self.splits if s.amount != 0) try: return not allsame(s.amount.currency for s in splits_with_amount) except ValueError: # no split with amount return False