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)
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)
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)
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)
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
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)
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)
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)
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)
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)
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
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)
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)
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
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)
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
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)
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()
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)
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()