Exemple #1
0
 def _apply_query(self):
     # On top, we want exact matches (the name starts with the query). Then, we want matches
     # that contain all the letters, sorted in order of names that have query letters as close
     # to each other as possible.
     q = sort_string(self._search_query)
     matches1, rest = extract(lambda n: n.startswith(q), self._original_names)
     matches2, rest = extract(lambda n: q in n, rest)
     matches3, rest = extract(lambda n: has_letters(n, q), rest)
     matches3.sort(key=lambda n: letters_distance(n, q))
     self._filtered_names = matches1 + matches2 + matches3
     self.selected_index = max(self.selected_index, 0)
     self.selected_index = min(self.selected_index, len(self._filtered_names)-1)
Exemple #2
0
 def _apply_query(self):
     # On top, we want exact matches (the name starts with the query). Then, we want matches
     # that contain all the letters, sorted in order of names that have query letters as close
     # to each other as possible.
     q = sort_string(self._search_query)
     matches1, rest = extract(lambda n: n.startswith(q),
                              self._original_names)
     matches2, rest = extract(lambda n: q in n, rest)
     matches3, rest = extract(lambda n: has_letters(n, q), rest)
     matches3.sort(key=lambda n: letters_distance(n, q))
     self._filtered_names = matches1 + matches2 + matches3
     self.selected_index = max(self.selected_index, 0)
     self.selected_index = min(self.selected_index,
                               len(self._filtered_names) - 1)
Exemple #3
0
    def toggle_entries_reconciled(self, entries):
        """Toggle the reconcile flag of `entries`.

        Sets the ``reconciliation_date`` to entries' date, or unset it when turning the flag off.

        :param entries: list of :class:`.Entry`
        """
        if not entries:
            return
        all_reconciled = not entries or all(entry.reconciled for entry in entries)
        newvalue = not all_reconciled
        action = Action(tr('Change reconciliation'))
        action.change_entries(entries, self.schedules)
        min_date = min(entry.date for entry in entries)
        spawns, entries = extract(lambda e: e.transaction.is_spawn, entries)
        action.change_transactions({e.transaction for e in spawns}, self.schedules)
        # spawns have to be processed before the action's recording, but
        # record() has to be called before we change the entries. This is why
        # we have this rather convulted code.
        if newvalue:
            for spawn in spawns:
                # XXX update transaction selection
                newtxn, newsplit = self._reconcile_spawn_split(
                    spawn, spawn.transaction.date)
                action.added_transactions.add(newtxn)
        self._undoer.record(action)
        if newvalue:
            for entry in entries:
                entry.split.reconciliation_date = entry.transaction.date
        else:
            for entry in entries:
                entry.split.reconciliation_date = None
        self._cook(from_date=min_date)
Exemple #4
0
    def delete_transactions(self, transactions, from_account=None):
        """Removes every transaction in ``transactions`` from the document.

        Adds undo recording, global scope querying, date range adjustments and UI triggers.

        :param transactions: a collection of :class:`.Transaction`.
        :param from_account: the :class:`.Account` from which the operation takes place, if any.
        """
        action = Action(tr('Remove transaction'))
        spawns, txns = extract(lambda x: x.is_spawn, transactions)
        global_scope = self._query_for_scope_if_needed(spawns)
        action.change_transactions(spawns, self.schedules)
        action.deleted_transactions |= set(txns)
        self._undoer.record(action)
        for txn in transactions:
            if txn.is_spawn:
                schedule = find_schedule_of_ref(txn.ref, self.schedules)
                assert schedule is not None
                if global_scope:
                    schedule.stop_before(txn)
                else:
                    schedule.delete(txn)
            else:
                self.transactions.remove(txn)
        min_date = min(t.date for t in transactions)
        self._cook(from_date=min_date)
        self.accounts.clean_empty_categories(from_account)
Exemple #5
0
 def get_spawns(self, start_date, repeat_type, repeat_every, end, transactions, consumedtxns):
     date_counter = DateCounter(start_date, repeat_type, repeat_every, end)
     spawns = []
     current_ref = Transaction(start_date)
     for current_date in date_counter:
         # `recurrence_date` is the date at which the budget *starts*.
         # We need a date counter to see which date is next (so we can know when our period ends
         end_date = inc_date(current_date, repeat_type, repeat_every) - ONE_DAY
         if end_date <= date.today():
             # No spawn in the past
             continue
         spawn = Spawn(
             self, current_ref, recurrence_date=current_date, date=end_date,
             txntype=3)
         spawns.append(spawn)
     account = self.account
     budget_amount = self.amount if account.is_debit_account() else -self.amount
     relevant_transactions = set(t for t in transactions if account in t.affected_accounts())
     relevant_transactions -= consumedtxns
     for spawn in spawns:
         affects_spawn = lambda t: spawn.recurrence_date <= t.date <= spawn.date
         wheat, shaft = extract(affects_spawn, relevant_transactions)
         relevant_transactions = shaft
         txns_amount = sum(t.amount_for_account(account, budget_amount.currency_code) for t in wheat)
         if abs(txns_amount) < abs(budget_amount):
             spawn_amount = budget_amount - txns_amount
             if spawn.amount_for_account(account, budget_amount.currency_code) != spawn_amount:
                 spawn.change(amount=spawn_amount, from_=account, to=None)
         else:
             spawn.change(amount=0, from_=account, to=None)
         consumedtxns |= set(wheat)
     self._previous_spawns = spawns
     return spawns
Exemple #6
0
    def delete_transactions(self, transactions, from_account=None):
        """Removes every transaction in ``transactions`` from the document.

        Adds undo recording, global scope querying, date range adjustments and UI triggers.

        :param transactions: a collection of :class:`.Transaction`.
        :param from_account: the :class:`.Account` from which the operation takes place, if any.
        """
        action = Action(tr('Remove transaction'))
        spawns, txns = extract(lambda x: x.is_spawn, transactions)
        global_scope = self._query_for_scope_if_needed(spawns)
        action.change_transactions(spawns, self.schedules)
        action.deleted_transactions |= set(txns)
        self._undoer.record(action)
        # to avoid sweeping twn refs from under the rug of other spawns, we
        # perform schedule deletions at the end of the loop.
        schedule_deletions = []
        for txn in transactions:
            if txn.is_spawn:
                schedule = find_schedule_of_spawn(txn, self.schedules)
                assert schedule is not None
                if global_scope:
                    schedule.change(stop_date=txn.recurrence_date -
                                    datetime.timedelta(1))
                else:
                    schedule_deletions.append((schedule, txn.recurrence_date))
            else:
                self.transactions.remove(txn)
        for schedule, recurrence_date in schedule_deletions:
            schedule.delete_at(recurrence_date)
        min_date = min(t.date for t in transactions)
        self._cook(from_date=min_date)
        self.accounts.clean_empty_categories(from_account)
Exemple #7
0
    def toggle_entries_reconciled(self, entries):
        """Toggle the reconcile flag of `entries`.

        Sets the ``reconciliation_date`` to entries' date, or unset it when turning the flag off.

        :param entries: list of :class:`.Entry`
        """
        if not entries:
            return
        all_reconciled = not entries or all(entry.reconciled
                                            for entry in entries)
        newvalue = not all_reconciled
        action = Action(tr('Change reconciliation'))
        action.change_entries(entries, self.schedules)
        min_date = min(entry.date for entry in entries)
        spawns, entries = extract(lambda e: e.transaction.is_spawn, entries)
        action.change_transactions({e.transaction
                                    for e in spawns}, self.schedules)
        # spawns have to be processed before the action's recording, but
        # record() has to be called before we change the entries. This is why
        # we have this rather convulted code.
        if newvalue:
            for spawn in spawns:
                # XXX update transaction selection
                newtxn, newsplit = self._reconcile_spawn_split(
                    spawn, spawn.transaction.date)
                action.added_transactions.add(newtxn)
        self._undoer.record(action)
        if newvalue:
            for entry in entries:
                entry.split.reconciliation_date = entry.transaction.date
        else:
            for entry in entries:
                entry.split.reconciliation_date = None
        self._cook(from_date=min_date)
Exemple #8
0
    def delete_transactions(self, transactions, from_account=None):
        """Removes every transaction in ``transactions`` from the document.

        Adds undo recording, global scope querying, date range adjustments and UI triggers.

        :param transactions: a collection of :class:`.Transaction`.
        :param from_account: the :class:`.Account` from which the operation takes place, if any.
        """
        action = Action(tr('Remove transaction'))
        spawns, txns = extract(lambda x: x.is_spawn, transactions)
        global_scope = self._query_for_scope_if_needed(spawns)
        action.change_transactions(spawns, self.schedules)
        action.deleted_transactions |= set(txns)
        self._undoer.record(action)
        # to avoid sweeping twn refs from under the rug of other spawns, we
        # perform schedule deletions at the end of the loop.
        schedule_deletions = []
        for txn in transactions:
            if txn.is_spawn:
                schedule = find_schedule_of_spawn(txn, self.schedules)
                assert schedule is not None
                if global_scope:
                    schedule.change(stop_date=txn.recurrence_date - datetime.timedelta(1))
                else:
                    schedule_deletions.append((schedule, txn.recurrence_date))
            else:
                self.transactions.remove(txn)
        for schedule, recurrence_date in schedule_deletions:
            schedule.delete_at(recurrence_date)
        min_date = min(t.date for t in transactions)
        self._cook(from_date=min_date)
        self.accounts.clean_empty_categories(from_account)
Exemple #9
0
    def delete_transactions(self, transactions, from_account=None):
        """Removes every transaction in ``transactions`` from the document.

        Adds undo recording, global scope querying, date range adjustments and UI triggers.

        :param transactions: a collection of :class:`.Transaction`.
        :param from_account: the :class:`.Account` from which the operation takes place, if any.
        """
        action = Action(tr('Remove transaction'))
        spawns, txns = extract(lambda x: x.is_spawn, transactions)
        global_scope = self._query_for_scope_if_needed(spawns)
        action.change_transactions(spawns, self.schedules)
        action.deleted_transactions |= set(txns)
        self._undoer.record(action)
        for txn in transactions:
            if txn.is_spawn:
                schedule = find_schedule_of_ref(txn.ref, self.schedules)
                assert schedule is not None
                if global_scope:
                    schedule.stop_before(txn)
                else:
                    schedule.delete(txn)
            else:
                self.transactions.remove(txn)
        min_date = min(t.date for t in transactions)
        self._cook(from_date=min_date)
        self.accounts.clean_empty_categories(from_account)
Exemple #10
0
    def draw_pie(self, data, circle_bounds):
        if not data:
            return
        circle_size = min(circle_bounds.w, circle_bounds.h)
        radius = circle_size / 2
        center = circle_bounds.center()

        # draw pie
        total_amount = sum(amount for _, amount, _ in data)
        start_angle = 0
        legends = []
        for legend_text, amount, color_index in data:
            fraction = amount / total_amount
            angle = fraction * 360
            self.view.draw_pie(center, radius, start_angle, angle, color_index)
            legend_angle = start_angle + (angle / 2)
            legend = Legend(text=legend_text, color=color_index, angle=legend_angle)
            legends.append(legend)
            start_angle += angle

        # compute legend rects
        _, legend_height = self.view.text_size('', FontID.Legend)
        for legend in legends:
            legend.base_point = point_in_circle(center, radius, legend.angle)
            legend_width, _ = self.view.text_size(legend.text, FontID.Legend)
            legend.text_rect = rect_from_center(legend.base_point, (legend_width, legend_height))
            legend.compute_label_rect()

        # make sure they're inside circle_bounds
        for legend in legends:
            pull_rect_in(legend.label_rect, circle_bounds)

        left, right = extract(lambda l: l.base_point.x < center.x, legends)
        # If any legend intersect, we start by sending everyone to their horizontal circle bounds.
        # Then, on each side, if anyone intersect, we go in "Spread mode", spreading all legends
        # vertically in a regular manner.
        if legends_intersect(legends):
            for legend in left:
                legend.label_rect.left = circle_bounds.left
            for legend in right:
                legend.label_rect.right = circle_bounds.right
        for side in (left, right):
            if legends_intersect(side):
                spread_vertically(side, circle_bounds)

        # draw legends
        # draw lines before legends because we don't want them being drawn over other legends
        if len(legends) > 1:
            for legend in legends:
                if not legend.should_draw_line():
                    continue
                self.view.draw_line(legend.label_rect.center(), legend.base_point, legend.color)
        for legend in legends:
            self.view.draw_rect(legend.label_rect, legend.color, BrushID.Legend)
            legend.compute_text_rect()
            self.view.draw_text(legend.text, legend.text_rect, FontID.Legend)
Exemple #11
0
 def _get_action_from_changed_transactions(self, transactions, global_scope=False):
     if len(transactions) == 1 and not transactions[0].is_spawn \
             and transactions[0] not in self.transactions:
         action = Action(tr('Add transaction'))
         action.added_transactions.add(transactions[0])
     else:
         action = Action(tr('Change transaction'))
         action.change_transactions(transactions, self.schedules)
     if global_scope:
         spawns, txns = extract(lambda x: x.is_spawn, transactions)
         action.change_transactions(spawns, self.schedules)
     return action
Exemple #12
0
    def change_transactions(self, transactions, schedules):
        """Record imminent changes to ``transactions``.

        If any of the transactions are a :class:`.Spawn`, also record a change to their related
        schedule.
        """
        spawns, normal = extract(lambda t: t.is_spawn, transactions)
        for t in normal:
            self.changed_transactions.add(t)
        for spawn in spawns:
            for schedule in schedules:
                if schedule.contains_ref(spawn.ref):
                    self.change_schedule(schedule)
Exemple #13
0
    def change_transactions(self, transactions, schedules):
        """Record imminent changes to ``transactions``.

        If any of the transactions are a :class:`.Spawn`, also record a change to their related
        schedule.
        """
        spawns, normal = extract(lambda t: t.is_spawn, transactions)
        for t in normal:
            self.changed_transactions.add(t)
        for spawn in spawns:
            for schedule in schedules:
                if schedule.contains_spawn(spawn):
                    self.change_schedule(schedule)
Exemple #14
0
 def _get_action_from_changed_transactions(self,
                                           transactions,
                                           global_scope=False):
     if len(transactions) == 1 and not transactions[0].is_spawn \
             and transactions[0] not in self.transactions:
         action = Action(tr('Add transaction'))
         action.added_transactions.add(transactions[0])
     else:
         action = Action(tr('Change transaction'))
         action.change_transactions(transactions, self.schedules)
     if global_scope:
         spawns, txns = extract(lambda x: x.is_spawn, transactions)
         action.change_transactions(spawns, self.schedules)
     return action
Exemple #15
0
    def toggle_entries_reconciled(self, entries):
        """Toggle the reconcile flag of `entries`.

        Sets the ``reconciliation_date`` to entries' date, or unset it when turning the flag off.

        :param entries: list of :class:`.Entry`
        """
        if not entries:
            return
        all_reconciled = not entries or all(entry.reconciled
                                            for entry in entries)
        newvalue = not all_reconciled
        action = Action(tr('Change reconciliation'))
        action.change_entries(entries, self.schedules)
        min_date = min(entry.date for entry in entries)
        spawns, entries = extract(lambda e: e.transaction.is_spawn, entries)
        action.change_transactions({e.transaction
                                    for e in spawns}, self.schedules)
        # spawns have to be processed before the action's recording, but
        # record() has to be called before we change the entries. This is why
        # we have this rather convulted code.
        schedules_to_delete = []
        if newvalue:
            for spawn in spawns:
                schedule = find_schedule_of_spawn(spawn.transaction,
                                                  self.schedules)
                assert schedule is not None
                schedules_to_delete.append((schedule, spawn))
                materialized = spawn.transaction.materialize()
                self.transactions.add(materialized)
                action.added_transactions.add(materialized)
                split_index = spawn.transaction.splits.index(spawn.split)
                newsplit = materialized.splits[split_index]
                newsplit.reconciliation_date = materialized.date
        self._undoer.record(action)
        for schedule, spawn in schedules_to_delete:
            schedule.delete_at(spawn.transaction.recurrence_date)
        if newvalue:
            for entry in entries:
                # XXX update transaction selection
                entry.split.reconciliation_date = entry.transaction.date
        else:
            for entry in entries:
                entry.split.reconciliation_date = None
        self._cook(from_date=min_date)
Exemple #16
0
 def get_spawns(self, start_date, repeat_type, repeat_every, end,
                transactions, consumedtxns):
     date_counter = DateCounter(start_date, repeat_type, repeat_every, end)
     spawns = []
     current_ref = Transaction(start_date)
     for current_date in date_counter:
         # `recurrence_date` is the date at which the budget *starts*.
         # We need a date counter to see which date is next (so we can know when our period ends
         end_date = inc_date(current_date, repeat_type,
                             repeat_every) - ONE_DAY
         if end_date <= date.today():
             # No spawn in the past
             continue
         spawn = Spawn(self,
                       current_ref,
                       recurrence_date=current_date,
                       date=end_date,
                       txntype=3)
         spawns.append(spawn)
     account = self.account
     budget_amount = self.amount if account.is_debit_account(
     ) else -self.amount
     relevant_transactions = set(t for t in transactions
                                 if account in t.affected_accounts())
     relevant_transactions -= consumedtxns
     for spawn in spawns:
         affects_spawn = lambda t: spawn.recurrence_date <= t.date <= spawn.date
         wheat, shaft = extract(affects_spawn, relevant_transactions)
         relevant_transactions = shaft
         txns_amount = sum(
             t.amount_for_account(account, budget_amount.currency_code)
             for t in wheat)
         if abs(txns_amount) < abs(budget_amount):
             spawn_amount = budget_amount - txns_amount
             if spawn.amount_for_account(
                     account, budget_amount.currency_code) != spawn_amount:
                 spawn.change(amount=spawn_amount, from_=account, to=None)
         else:
             spawn.change(amount=0, from_=account, to=None)
         consumedtxns |= set(wheat)
     self._previous_spawns = spawns
     return spawns
Exemple #17
0
    def toggle_entries_reconciled(self, entries):
        """Toggle the reconcile flag of `entries`.

        Sets the ``reconciliation_date`` to entries' date, or unset it when turning the flag off.

        :param entries: list of :class:`.Entry`
        """
        if not entries:
            return
        all_reconciled = not entries or all(entry.reconciled for entry in entries)
        newvalue = not all_reconciled
        action = Action(tr('Change reconciliation'))
        action.change_entries(entries, self.schedules)
        min_date = min(entry.date for entry in entries)
        spawns, entries = extract(lambda e: e.transaction.is_spawn, entries)
        action.change_transactions({e.transaction for e in spawns}, self.schedules)
        # spawns have to be processed before the action's recording, but
        # record() has to be called before we change the entries. This is why
        # we have this rather convulted code.
        schedules_to_delete = []
        if newvalue:
            for spawn in spawns:
                schedule = find_schedule_of_spawn(spawn.transaction, self.schedules)
                assert schedule is not None
                schedules_to_delete.append((schedule, spawn))
                materialized = spawn.transaction.materialize()
                self.transactions.add(materialized)
                action.added_transactions.add(materialized)
                split_index = spawn.transaction.splits.index(spawn.split)
                newsplit = materialized.splits[split_index]
                newsplit.reconciliation_date = materialized.date
        self._undoer.record(action)
        for schedule, spawn in schedules_to_delete:
            schedule.delete_at(spawn.transaction.recurrence_date)
        if newvalue:
            for entry in entries:
                # XXX update transaction selection
                entry.split.reconciliation_date = entry.transaction.date
        else:
            for entry in entries:
                entry.split.reconciliation_date = None
        self._cook(from_date=min_date)
Exemple #18
0
 def add_owner(self, user):
     _ = self
     e, n = user.email(), user.nickname()
     _.owner = ListOwnerDb(user=user, name=extract(e), nickname=n, email=e)
     return _.owner.put()
Exemple #19
0
    def draw_pie(self, data, circle_bounds):
        if not data:
            return
        circle_size = min(circle_bounds.w, circle_bounds.h)
        radius = circle_size / 2
        center = circle_bounds.center()

        # draw pie
        total_amount = sum(amount for _, amount, _ in data)
        start_angle = 0
        legends = []
        for legend_text, amount, color_index in data:
            fraction = amount / total_amount
            angle = fraction * 360
            self.view.draw_pie(center, radius, start_angle, angle, color_index)
            legend_angle = start_angle + (angle / 2)
            legend = Legend(text=legend_text,
                            color=color_index,
                            angle=legend_angle)
            legends.append(legend)
            start_angle += angle

        # compute legend rects
        _, legend_height = self.view.text_size('', FontID.Legend)
        for legend in legends:
            legend.base_point = point_in_circle(center, radius, legend.angle)
            legend_width, _ = self.view.text_size(legend.text, FontID.Legend)
            legend.text_rect = rect_from_center(legend.base_point,
                                                (legend_width, legend_height))
            legend.compute_label_rect()

        # make sure they're inside circle_bounds
        for legend in legends:
            pull_rect_in(legend.label_rect, circle_bounds)

        left, right = extract(lambda l: l.base_point.x < center.x, legends)
        # If any legend intersect, we start by sending everyone to their horizontal circle bounds.
        # Then, on each side, if anyone intersect, we go in "Spread mode", spreading all legends
        # vertically in a regular manner.
        if legends_intersect(legends):
            for legend in left:
                legend.label_rect.left = circle_bounds.left
            for legend in right:
                legend.label_rect.right = circle_bounds.right
        for side in (left, right):
            if legends_intersect(side):
                spread_vertically(side, circle_bounds)

        # draw legends
        # draw lines before legends because we don't want them being drawn over other legends
        if len(legends) > 1:
            for legend in legends:
                if not legend.should_draw_line():
                    continue
                self.view.draw_line(legend.label_rect.center(),
                                    legend.base_point, legend.color)
        for legend in legends:
            self.view.draw_rect(legend.label_rect, legend.color,
                                BrushID.Legend)
            legend.compute_text_rect()
            self.view.draw_text(legend.text, legend.text_rect, FontID.Legend)
Exemple #20
0
 def add_owner(self, user):
     _ = self
     e, n = user.email(), user.nickname()
     _.owner = ListOwnerDb(user=user, name=extract(e), nickname=n, email=e)
     return _.owner.put()