Example #1
0
    def cook(self, from_date=None, until_date=None):
        """Cooks raw data into :attr:`transactions`.

        :param from_date: when set, saves calculation time by re-using existing cooked transactions.
        :type from_date: ``datetime.date``
        :param until_date: because of recurrence, we must always have a date at which we stop
                           cooking. If we don't, we might end up in an infinite loop. If not set,
                           will be the date of the transaction with the highest date.
        :type until_date: ``datetime.date``
        """
        # Determine from/until dates
        if from_date is None:
            from_date = date.min
        else:
            # it's possible that we have to reduce from_date a bit. If a split from before as a
            # reconciled date >= from_date, we have to set from_date to that split's normal date
            # We reverse the transactions to correctly detect chained overlappings in date/recdate
            splits = flatten(t.splits for t in reversed(self.transactions)) # splits from *cooked* txns
            for split in splits:
                rdate = split.reconciliation_date
                if rdate is not None and rdate >= from_date:
                    from_date = min(from_date, split.transaction.date)
        self._transactions.sort(key=attrgetter('date', 'position')) # needed in case until_date is None
        if until_date is None:
            until_date = self._transactions[-1].date if self._transactions else from_date
        # Clear old cooked data
        for account in self._accounts:
            account.entries.clear(from_date)
        if from_date == date.min:
            self.transactions = []
        else:
            self.transactions = [t for t in self.transactions if t.date < from_date]
        # Cook
        spawns = flatten(recurrence.get_spawns(until_date) for recurrence in self._scheduled)
        spawns += self._budget_spawns(until_date, spawns)
        # To ensure that our sort order stay correct and consistent, we assign position values
        # to our spawns. To ensure that there's no overlap, we start our position counter at
        # len(transactions)
        for counter, spawn in enumerate(spawns, start=len(self._transactions)):
            spawn.position = counter
        txns = self._transactions + spawns
        # we don't filter out txns > until_date because they might be budgets affecting current data
        # XXX now that budget's base date is the start date, isn't this untrue?
        tocook = [t for t in txns if from_date <= t.date]
        tocook.sort(key=attrgetter('date'))
        splits = flatten(t.splits for t in tocook)
        account2splits = defaultdict(list)
        for split in splits:
            account = split.account
            if account is not None:
                account2splits[account].append(split)
        for account, splits in account2splits.items():
            self._cook_splits(account, splits)
        self.transactions += tocook
        self._cooked_until = until_date
Example #2
0
    def _perform_action(self, import_action, apply=ActionSelectionOptions.ApplyToPane):
        if self.selected_pane is None:
            return

        action_params = self._collect_action_params(import_action, apply)

        if not action_params:
            return

        panes = dedupe(flatten((grp[2] for grp in action_params)))

        for pane in panes:
            pane.match_flag = False
            pane.import_document.cook_flag = False

        for action_param in action_params:
            import_action.perform_action(*action_param)

        for pane in panes:
            if not pane.import_document.cook_flag:
                pane.import_document.cook()

        for pane in panes:
            if not pane.match_flag:
                pane.match_entries()

        self.import_table.refresh()
Example #3
0
 def _cash_flow(self, date_range, currency):
     cache = self._date2entries
     entries = flatten(cache[date] for date in date_range if date in cache)
     entries = (e for e in entries
                if not getattr(e.transaction, 'is_budget', False))
     amounts = (convert_amount(e.amount, currency, e.date) for e in entries)
     return sum(amounts)
Example #4
0
 def delete_accounts(self, accounts):
     accounts = set(accounts)
     self.deleted_accounts |= accounts
     all_entries = flatten(a.entries for a in accounts)
     transactions = set(e.transaction for e in all_entries if not isinstance(e.transaction, Spawn))
     transactions = set(t for t in transactions if not t.affected_accounts() - accounts)
     self.deleted_transactions |= transactions
     self.change_splits(e.split for e in all_entries)
Example #5
0
 def __get_dupe_list(self):
     if self.__dupes is None:
         self.__dupes = flatten(group.dupes for group in self.groups)
         if None in self.__dupes:
             # This is debug logging to try to figure out #44
             logging.warning("There is a None value in the Results' dupe list. dupes: %r groups: %r", self.__dupes, self.groups)
         if self.__filtered_dupes:
             self.__dupes = [dupe for dupe in self.__dupes if dupe in self.__filtered_dupes]
         sd = self.__dupes_sort_descriptor
         if sd:
             self.sort_dupes(sd[0], sd[1], sd[2])
     return self.__dupes
Example #6
0
def get_groups(matches):
    """Returns a list of :class:`Group` from ``matches``.

    Create groups out of match pairs in the smartest way possible.
    """
    matches.sort(key=lambda match: -match.percentage)
    dupe2group = {}
    groups = []
    try:
        for match in matches:
            first, second, _ = match
            first_group = dupe2group.get(first)
            second_group = dupe2group.get(second)
            if first_group:
                if second_group:
                    if first_group is second_group:
                        target_group = first_group
                    else:
                        continue
                else:
                    target_group = first_group
                    dupe2group[second] = target_group
            else:
                if second_group:
                    target_group = second_group
                    dupe2group[first] = target_group
                else:
                    target_group = Group()
                    groups.append(target_group)
                    dupe2group[first] = target_group
                    dupe2group[second] = target_group
            target_group.add_match(match)
    except MemoryError:
        del dupe2group
        del matches
        # should free enough memory to continue
        logging.warning("Memory Overflow. Groups: {0}".format(len(groups)))
    # Now that we have a group, we have to discard groups' matches and see if there're any "orphan"
    # matches, that is, matches that were candidate in a group but that none of their 2 files were
    # accepted in the group. With these orphan groups, it's safe to build additional groups
    matched_files = set(flatten(groups))
    orphan_matches = []
    for group in groups:
        orphan_matches += {
            m
            for m in group.discard_matches()
            if not any(obj in matched_files for obj in [m.first, m.second])
        }
    if groups and orphan_matches:
        groups += get_groups(
            orphan_matches)  # no job, as it isn't supposed to take a long time
    return groups
Example #7
0
 def fill_table(self):
     native_currency = self.document.default_currency
     # Create a list of all splits so that we can access all our amounts.
     allsplits = flatten(t.splits for t in self.document.transactions)
     # Create a set of all used currencies
     currencies = {s.amount.currency for s in allsplits if s.amount}
     for currency in currencies:
         row = self.add_row()
         row.set_field('name', currency.name)
         # currencies have a value_in(other_currency, at_date) returning the exchange rathe
         # at the given date as a float value.
         exchange_rate = currency.value_in(native_currency, date.today())
         row.set_field('rate', '%0.4f' % exchange_rate, sort_value=exchange_rate)
Example #8
0
    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()
Example #9
0
def get_groups(matches, j=job.nulljob):
    """Returns a list of :class:`Group` from ``matches``.

    Create groups out of match pairs in the smartest way possible.
    """
    matches.sort(key=lambda match: -match.percentage)
    dupe2group = {}
    groups = []
    try:
        for match in j.iter_with_progress(matches, tr("Grouped %d/%d matches"), JOB_REFRESH_RATE):
            first, second, _ = match
            first_group = dupe2group.get(first)
            second_group = dupe2group.get(second)
            if first_group:
                if second_group:
                    if first_group is second_group:
                        target_group = first_group
                    else:
                        continue
                else:
                    target_group = first_group
                    dupe2group[second] = target_group
            else:
                if second_group:
                    target_group = second_group
                    dupe2group[first] = target_group
                else:
                    target_group = Group()
                    groups.append(target_group)
                    dupe2group[first] = target_group
                    dupe2group[second] = target_group
            target_group.add_match(match)
    except MemoryError:
        del dupe2group
        del matches
        # should free enough memory to continue
        logging.warning('Memory Overflow. Groups: {0}'.format(len(groups)))
    # Now that we have a group, we have to discard groups' matches and see if there're any "orphan"
    # matches, that is, matches that were candidate in a group but that none of their 2 files were
    # accepted in the group. With these orphan groups, it's safe to build additional groups
    matched_files = set(flatten(groups))
    orphan_matches = []
    for group in groups:
        orphan_matches += {
            m for m in group.discard_matches()
            if not any(obj in matched_files for obj in [m.first, m.second])
        }
    if groups and orphan_matches:
        groups += get_groups(orphan_matches) # no job, as it isn't supposed to take a long time
    return groups
Example #10
0
 def _invert_amounts(self, apply_to_all):
     if apply_to_all:
         panes = self.panes
     else:
         panes = [self.selected_pane]
     entries = flatten(p.account.entries for p in panes)
     txns = dedupe(e.transaction for e in entries)
     for txn in txns:
         for split in txn.splits:
             split.amount = -split.amount
     # Entries, I don't remember why, hold a copy of their split's amount. It has to be updated.
     for entry in entries:
         entry.amount = entry.split.amount
     self.import_table.refresh()
Example #11
0
 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()
Example #12
0
    def delete_accounts(self, accounts):
        """Record the imminent deletion of ``accounts``.

        Use this method rather than directly modifying the ``deleted_accounts`` set because we also
        trigger the modification of all transasctions related to that account (their splits are
        going to be reassigned).
        """
        accounts = set(accounts)
        self.deleted_accounts |= accounts
        all_entries = flatten(a.entries for a in accounts)
        transactions = set(e.transaction for e in all_entries if not isinstance(e.transaction, Spawn))
        transactions = set(t for t in transactions if not t.affected_accounts() - accounts)
        self.deleted_transactions |= transactions
        self.change_splits(e.split for e in all_entries)
Example #13
0
 def __get_dupe_list(self):
     if self.__dupes is None:
         self.__dupes = flatten(group.dupes for group in self.groups)
         if None in self.__dupes:
             # This is debug logging to try to figure out #44
             logging.warning(
                 "There is a None value in the Results' dupe list. dupes: %r groups: %r",
                 self.__dupes, self.groups
             )
         if self.__filtered_dupes:
             self.__dupes = [dupe for dupe in self.__dupes if dupe in self.__filtered_dupes]
         sd = self.__dupes_sort_descriptor
         if sd:
             self.sort_dupes(sd[0], sd[1], sd[2])
     return self.__dupes
Example #14
0
    def delete_accounts(self, accounts, reassign=False):
        """Record the imminent deletion of ``accounts``.

        Use this method rather than directly modifying the ``deleted_accounts`` set because we also
        trigger the modification of all transasctions related to that account (their splits are
        going to be reassigned).

        If transactions are going to be reassigned, set the ``reassign`` flag so that we don't
        consider orphaned txns as deleted.
        """
        accounts = set(accounts)
        self.deleted_accounts |= accounts
        all_entries = flatten(a.entries for a in accounts)
        if not reassign:
            transactions = {e.transaction for e in all_entries if not isinstance(e.transaction, Spawn)}
            transactions = {t for t in transactions if not t.affected_accounts() - accounts}
            self.deleted_transactions |= transactions
        self.change_splits(e.split for e in all_entries)
Example #15
0
 def fill_table(self):
     native_currency = self.document.default_currency
     # Create a list of all splits so that we can access all our amounts.
     allsplits = flatten(t.splits for t in self.document.transactions)
     # Create a set of all used currencies
     currencies = {s.amount.currency for s in allsplits if s.amount}
     for currency in currencies:
         row = self.add_row()
         row.set_field('name', currency.name)
         # currencies have a value_in(other_currency, at_date) returning the exchange rathe
         # at the given date as a float value.
         exchange_rate = currency.value_in(native_currency, date.today())
         row.set_field('rate', '%0.4f' % exchange_rate, sort_value=exchange_rate)
         try:
             _, max_date = currency.rates_db.date_range(currency.code)
             value = self.mainwindow.app.format_date(max_date)
         except TypeError: # result is None
             value = "N/A"
         row.set_field('fetchdate', value)
Example #16
0
    def apply_filter(self, filter_str):
        """Applies a filter ``filter_str`` to :attr:`groups`

        When you apply the filter, only  dupes with the filename matching ``filter_str`` will be in
        in the results. To cancel the filter, just call apply_filter with ``filter_str`` to None,
        and the results will go back to normal.

        If call apply_filter on a filtered results, the filter will be applied
        *on the filtered results*.

        :param str filter_str: a string containing a regexp to filter dupes with.
        """
        if not filter_str:
            self.__filtered_dupes = None
            self.__filtered_groups = None
            self.__filters = None
        else:
            if not self.__filters:
                self.__filters = []
            try:
                filter_re = re.compile(filter_str, re.IGNORECASE)
            except re.error:
                return  # don't apply this filter.
            self.__filters.append(filter_str)
            if self.__filtered_dupes is None:
                self.__filtered_dupes = flatten(g[:] for g in self.groups)
            self.__filtered_dupes = set(
                dupe
                for dupe in self.__filtered_dupes
                if filter_re.search(str(dupe.path))
            )
            filtered_groups = set()
            for dupe in self.__filtered_dupes:
                filtered_groups.add(self.get_group_of_duplicate(dupe))
            self.__filtered_groups = list(filtered_groups)
        self.__recalculate_stats()
        sd = self.__groups_sort_descriptor
        if sd:
            self.sort_groups(sd[0], sd[1])
        self.__dupes = None
Example #17
0
    def apply_filter(self, filter_str):
        """Applies a filter ``filter_str`` to :attr:`groups`

        When you apply the filter, only  dupes with the filename matching ``filter_str`` will be in
        in the results. To cancel the filter, just call apply_filter with ``filter_str`` to None,
        and the results will go back to normal.

        If call apply_filter on a filtered results, the filter will be applied
        *on the filtered results*.

        :param str filter_str: a string containing a regexp to filter dupes with.
        """
        if not filter_str:
            self.__filtered_dupes = None
            self.__filtered_groups = None
            self.__filters = None
        else:
            if not self.__filters:
                self.__filters = []
            try:
                filter_re = re.compile(filter_str, re.IGNORECASE)
            except re.error:
                return # don't apply this filter.
            self.__filters.append(filter_str)
            if self.__filtered_dupes is None:
                self.__filtered_dupes = flatten(g[:] for g in self.groups)
            self.__filtered_dupes = set(dupe for dupe in self.__filtered_dupes if filter_re.search(str(dupe.path)))
            filtered_groups = set()
            for dupe in self.__filtered_dupes:
                filtered_groups.add(self.get_group_of_duplicate(dupe))
            self.__filtered_groups = list(filtered_groups)
        self.__recalculate_stats()
        sd = self.__groups_sort_descriptor
        if sd:
            self.sort_groups(sd[0], sd[1])
        self.__dupes = None
Example #18
0
    def load(self):
        """Loads the parsed info into self.accounts and self.transactions.

        You must have called parse() before calling this.
        """
        def load_transaction_info(info):
            description = info.description
            payee = info.payee
            checkno = info.checkno
            date = info.date
            transaction = Transaction(date, description, payee, checkno)
            transaction.notes = nonone(info.notes, '')
            for split_info in info.splits:
                account = split_info.account
                amount = split_info.amount
                if split_info.amount_reversed:
                    amount = -amount
                memo = nonone(split_info.memo, '')
                split = Split(transaction, account, amount)
                split.memo = memo
                if split_info.reconciliation_date is not None:
                    split.reconciliation_date = split_info.reconciliation_date
                elif split_info.reconciled: # legacy
                    split.reconciliation_date = transaction.date
                split.reference = split_info.reference
                transaction.splits.append(split)
            while len(transaction.splits) < 2:
                transaction.splits.append(Split(transaction, None, 0))
            transaction.balance()
            transaction.mtime = info.mtime
            if info.reference is not None:
                for split in transaction.splits:
                    if split.reference is None:
                        split.reference = info.reference
            return transaction

        self._load()
        self.flush_account() # Implicit
        # Now, we take the info we have and transform it into model instances
        currencies = set()
        start_date = datetime.date.max
        for info in self.group_infos:
            group = Group(info.name, info.type)
            self.groups.append(group)
        for info in self.account_infos:
            account_type = info.type
            if account_type not in AccountType.All:
                account_type = AccountType.Asset
            account_currency = self.default_currency
            try:
                if info.currency:
                    account_currency = Currency(info.currency)
            except ValueError:
                pass # keep account_currency as self.default_currency
            account = Account(info.name, account_currency, account_type)
            if info.group:
                account.group = self.groups.find(info.group, account_type)
            if info.budget:
                self.budget_infos.append(BudgetInfo(info.name, info.budget_target, info.budget))
            account.reference = info.reference
            account.account_number = info.account_number
            account.notes = info.notes
            currencies.add(account.currency)
            self.accounts.add(account)

        # Pre-parse transaction info. We bring all relevant info recorded at the txn level into the split level
        all_txn = self.transaction_infos + [r.transaction_info for r in self.recurrence_infos] +\
            flatten([stripfalse(r.date2exception.values()) for r in self.recurrence_infos]) +\
            flatten([r.date2globalchange.values() for r in self.recurrence_infos])
        for info in all_txn:
            split_accounts = [s.account for s in info.splits]
            if info.account and info.account not in split_accounts:
                info.splits.insert(0, SplitInfo(info.account, info.amount, info.currency, False))
            if info.transfer and info.transfer not in split_accounts:
                info.splits.append(SplitInfo(info.transfer, info.amount, info.currency, True))
            for split_info in info.splits:
                # this amount is just to determine the auto_create_type
                str_amount = split_info.amount
                if split_info.currency:
                    str_amount += split_info.currency
                amount = self.parse_amount(str_amount, self.default_currency)
                auto_create_type = AccountType.Income if amount >= 0 else AccountType.Expense
                split_info.account = (
                    self.accounts.find(split_info.account, auto_create_type)
                    if split_info.account
                    else None
                )
                currency = split_info.account.currency if split_info.account is not None else self.default_currency
                split_info.amount = self.parse_amount(str_amount, currency)
                if split_info.amount:
                    currencies.add(split_info.amount.currency)

        self.transaction_infos.sort(key=attrgetter('date'))
        for date, transaction_infos in groupby(self.transaction_infos, attrgetter('date')):
            start_date = min(start_date, date)
            for position, info in enumerate(transaction_infos, start=1):
                transaction = load_transaction_info(info)
                self.transactions.add(transaction, position=position)

        # Scheduled
        for info in self.recurrence_infos:
            ref = load_transaction_info(info.transaction_info)
            recurrence = Recurrence(ref, info.repeat_type, info.repeat_every)
            recurrence.stop_date = info.stop_date
            for date, transaction_info in info.date2exception.items():
                if transaction_info is not None:
                    exception = load_transaction_info(transaction_info)
                    spawn = Spawn(recurrence, exception, date, exception.date)
                    recurrence.date2exception[date] = spawn
                else:
                    recurrence.delete_at(date)
            for date, transaction_info in info.date2globalchange.items():
                change = load_transaction_info(transaction_info)
                spawn = Spawn(recurrence, change, date, change.date)
                recurrence.date2globalchange[date] = spawn
            self.schedules.append(recurrence)
        # Budgets
        TODAY = datetime.date.today()
        fallback_start_date = datetime.date(TODAY.year, TODAY.month, 1)
        for info in self.budget_infos:
            account = self.accounts.find(info.account)
            if account is None:
                continue
            target = self.accounts.find(info.target) if info.target else None
            amount = self.parse_amount(info.amount, account.currency)
            start_date = nonone(info.start_date, fallback_start_date)
            budget = Budget(account, target, amount, start_date, repeat_type=info.repeat_type)
            budget.notes = nonone(info.notes, '')
            budget.stop_date = info.stop_date
            if info.repeat_every:
                budget.repeat_every = info.repeat_every
            self.budgets.append(budget)
        self._post_load()
        self.oven.cook(datetime.date.min, until_date=None)
        Currency.get_rates_db().ensure_rates(start_date, [x.code for x in currencies])
Example #19
0
 def criteria_list(self):
     dupes = flatten(g[:] for g in self.results.groups)
     values = sorted(dedupe(self.extract_value(d) for d in dupes))
     return [Criterion(self, value) for value in values]
Example #20
0
 def criteria_list(self):
     dupes = flatten(g[:] for g in self.results.groups)
     values = sorted(dedupe(self.extract_value(d) for d in dupes))
     return [Criterion(self, value) for value in values]
Example #21
0
    def load(self):
        """Loads the parsed info into self.accounts and self.transactions.

        You must have called parse() before calling this.
        """
        def load_transaction_info(info):
            description = info.description
            payee = info.payee
            checkno = info.checkno
            date = info.date
            transaction = Transaction(date, description, payee, checkno)
            transaction.notes = nonone(info.notes, '')
            for split_info in info.splits:
                account = split_info.account
                amount = split_info.amount
                if split_info.amount_reversed:
                    amount = -amount
                memo = nonone(split_info.memo, '')
                split = Split(transaction, account, amount)
                split.memo = memo
                if account is None or not of_currency(amount,
                                                      account.currency):
                    # fix #442: off-currency transactions shouldn't be reconciled
                    split.reconciliation_date = None
                elif split_info.reconciliation_date is not None:
                    split.reconciliation_date = split_info.reconciliation_date
                elif split_info.reconciled:  # legacy
                    split.reconciliation_date = transaction.date
                split.reference = split_info.reference
                transaction.splits.append(split)
            while len(transaction.splits) < 2:
                transaction.splits.append(Split(transaction, None, 0))
            transaction.balance()
            transaction.mtime = info.mtime
            if info.reference is not None:
                for split in transaction.splits:
                    if split.reference is None:
                        split.reference = info.reference
            return transaction

        self._load()
        self.flush_account()  # Implicit
        # Now, we take the info we have and transform it into model instances
        currencies = set()
        start_date = datetime.date.max
        for info in self.group_infos:
            group = Group(info.name, info.type)
            self.groups.append(group)
        for info in self.account_infos:
            account_type = info.type
            if account_type not in AccountType.All:
                account_type = AccountType.Asset
            account_currency = self.default_currency
            try:
                if info.currency:
                    account_currency = Currency(info.currency)
            except ValueError:
                pass  # keep account_currency as self.default_currency
            account = Account(info.name, account_currency, account_type)
            if info.group:
                account.group = self.groups.find(info.group, account_type)
            if info.budget:
                self.budget_infos.append(
                    BudgetInfo(info.name, info.budget_target, info.budget))
            account.reference = info.reference
            account.account_number = info.account_number
            account.inactive = info.inactive
            account.notes = info.notes
            currencies.add(account.currency)
            self.accounts.add(account)

        # Pre-parse transaction info. We bring all relevant info recorded at the txn level into the split level
        all_txn = self.transaction_infos + [r.transaction_info for r in self.recurrence_infos] +\
            flatten([stripfalse(r.date2exception.values()) for r in self.recurrence_infos]) +\
            flatten([r.date2globalchange.values() for r in self.recurrence_infos])
        for info in all_txn:
            split_accounts = [s.account for s in info.splits]
            if info.account and info.account not in split_accounts:
                info.splits.insert(
                    0,
                    SplitInfo(info.account, info.amount, info.currency, False))
            if info.transfer and info.transfer not in split_accounts:
                info.splits.append(
                    SplitInfo(info.transfer, info.amount, info.currency, True))
            for split_info in info.splits:
                # this amount is just to determine the auto_create_type
                str_amount = split_info.amount
                if split_info.currency:
                    str_amount += split_info.currency
                amount = self.parse_amount(str_amount, self.default_currency)
                auto_create_type = AccountType.Income if amount >= 0 else AccountType.Expense
                split_info.account = (self.accounts.find(
                    split_info.account, auto_create_type)
                                      if split_info.account else None)
                currency = split_info.account.currency if split_info.account is not None else self.default_currency
                split_info.amount = self.parse_amount(str_amount, currency)
                if split_info.amount:
                    currencies.add(split_info.amount.currency)

        self.transaction_infos.sort(key=attrgetter('date'))
        for date, transaction_infos in groupby(self.transaction_infos,
                                               attrgetter('date')):
            start_date = min(start_date, date)
            for position, info in enumerate(transaction_infos, start=1):
                transaction = load_transaction_info(info)
                self.transactions.add(transaction, position=position)

        # Scheduled
        for info in self.recurrence_infos:
            ref = load_transaction_info(info.transaction_info)
            recurrence = Recurrence(ref, info.repeat_type, info.repeat_every)
            recurrence.stop_date = info.stop_date
            for date, transaction_info in info.date2exception.items():
                if transaction_info is not None:
                    exception = load_transaction_info(transaction_info)
                    spawn = Spawn(recurrence, exception, date, exception.date)
                    recurrence.date2exception[date] = spawn
                else:
                    recurrence.delete_at(date)
            for date, transaction_info in info.date2globalchange.items():
                change = load_transaction_info(transaction_info)
                spawn = Spawn(recurrence, change, date, change.date)
                recurrence.date2globalchange[date] = spawn
            self.schedules.append(recurrence)
        # Budgets
        TODAY = datetime.date.today()
        fallback_start_date = datetime.date(TODAY.year, TODAY.month, 1)
        for info in self.budget_infos:
            account = self.accounts.find(info.account)
            if account is None:
                continue
            target = self.accounts.find(info.target) if info.target else None
            amount = self.parse_amount(info.amount, account.currency)
            start_date = nonone(info.start_date, fallback_start_date)
            budget = Budget(account,
                            target,
                            amount,
                            start_date,
                            repeat_type=info.repeat_type)
            budget.notes = nonone(info.notes, '')
            budget.stop_date = info.stop_date
            if info.repeat_every:
                budget.repeat_every = info.repeat_every
            self.budgets.append(budget)
        self._post_load()
        self.oven.cook(datetime.date.min, until_date=None)
        Currency.get_rates_db().ensure_rates(start_date,
                                             [x.code for x in currencies])
Example #22
0
 def _cash_flow(self, date_range, currency):
     cache = self._date2entries
     entries = flatten(cache[date] for date in date_range if date in cache)
     entries = (e for e in entries if not getattr(e.transaction, 'is_budget', False))
     amounts = (convert_amount(e.amount, currency, e.date) for e in entries)
     return sum(amounts)
Example #23
0
 def __contains__(self, node):
     if isinstance(node, str) and self.__needs_update():
         return node in flatten(self._fetch_subitems())
     else:
         return super(Directory, self).__contains__(node)
Example #24
0
 def _swap_fields(self, panes, switch_func):
     entries = flatten(p.account.entries for p in panes)
     txns = dedupe(e.transaction for e in entries)
     for txn in txns:
         switch_func(txn)
     self.import_table.refresh()
Example #25
0
    def cook(self, from_date=None, until_date=None):
        """Cooks raw data into :attr:`transactions`.

        :param from_date: when set, saves calculation time by re-using existing cooked transactions.
        :type from_date: ``datetime.date``
        :param until_date: because of recurrence, we must always have a date at which we stop
                           cooking. If we don't, we might end up in an infinite loop. If not set,
                           will be the date of the transaction with the highest date.
        :type until_date: ``datetime.date``
        """
        # Determine from/until dates
        if from_date is None:
            from_date = date.min
        else:
            # it's possible that we have to reduce from_date a bit. If a split from before as a
            # reconciled date >= from_date, we have to set from_date to that split's normal date
            # We reverse the transactions to correctly detect chained overlappings in date/recdate
            splits = flatten(t.splits for t in reversed(
                self.transactions))  # splits from *cooked* txns
            for split in splits:
                rdate = split.reconciliation_date
                if rdate is not None and rdate >= from_date:
                    from_date = min(from_date, split.transaction.date)
        self._transactions.sort(key=attrgetter(
            'date', 'position'))  # needed in case until_date is None
        if until_date is None:
            until_date = self._transactions[
                -1].date if self._transactions else from_date
        # Clear old cooked data
        for account in self._accounts:
            account.entries.clear(from_date)
        if from_date == date.min:
            self.transactions = []
        else:
            self.transactions = [
                t for t in self.transactions if t.date < from_date
            ]
        # Cook
        spawns = flatten(
            recurrence.get_spawns(until_date)
            for recurrence in self._scheduled)
        spawns += self._budget_spawns(until_date, spawns)
        # To ensure that our sort order stay correct and consistent, we assign position values
        # to our spawns. To ensure that there's no overlap, we start our position counter at
        # len(transactions)
        for counter, spawn in enumerate(spawns, start=len(self._transactions)):
            spawn.position = counter
        txns = self._transactions + spawns
        # we don't filter out txns > until_date because they might be budgets affecting current data
        # XXX now that budget's base date is the start date, isn't this untrue?
        tocook = [t for t in txns if from_date <= t.date]
        tocook.sort(key=attrgetter('date'))
        splits = flatten(t.splits for t in tocook)
        account2splits = defaultdict(list)
        for split in splits:
            account = split.account
            if account is not None:
                account2splits[account].append(split)
        for account, splits in account2splits.items():
            self._cook_splits(account, splits)
        self.transactions += tocook
        self._cooked_until = until_date
Example #26
0
 def __contains__(self, node):
     if isinstance(node, str) and self.__needs_update():
         return node in flatten(self._fetch_subitems())
     else:
         return super(Directory, self).__contains__(node)