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`. """ 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_splits([e.split for e in entries]) min_date = min(entry.date for entry in entries) splits = [entry.split for entry in entries] spawns, splits = extract(lambda s: isinstance(s.transaction, Spawn), splits) for spawn in spawns: action.change_schedule(spawn.transaction.recurrence) self._undoer.record(action) if newvalue: for split in splits: split.reconciliation_date = split.transaction.date for spawn in spawns: #XXX update transaction selection materialized_split = self._reconcile_spawn_split(spawn, spawn.transaction.date) action.added_transactions.add(materialized_split.transaction) else: for split in splits: split.reconciliation_date = None self._cook(from_date=min_date) self.notify('transaction_changed')
def get_spawns(self, end, transactions, consumedtxns): # transactions will affect the amounts of the budget spawns # consumedtxns is a set of txns already "consumed" by a budget. It is the budget's # responsability to add txns to this set as it "consumes" them spawns = Recurrence.get_spawns(self, end) # No spawn in the past spawns = [spawn for spawn in spawns if spawn.date > date.today()] 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) 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) != spawn_amount: spawn.amount = abs(spawn_amount) spawn.set_splits([Split(spawn, account, spawn_amount), Split(spawn, self.target, -spawn_amount)]) else: spawn.set_splits([]) consumedtxns |= set(wheat) self._previous_spawns = spawns return spawns
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 change_transactions(self, transactions): """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: isinstance(t, Spawn), transactions) self.changed_transactions |= set((t, t.replicate()) for t in normal) for schedule in set(spawn.recurrence for spawn in spawns): self.change_schedule(schedule)
def collect_results(collect_all=False): # collect results and wait until the queue is small enough to accomodate a new results. nonlocal async_results, matches, comparison_count limit = 0 if collect_all else RESULTS_QUEUE_LIMIT while len(async_results) > limit: ready, working = extract(lambda r: r.ready(), async_results) for result in ready: matches += result.get() async_results.remove(result) comparison_count += 1 progress_msg = tr("Performed %d/%d chunk matches") % (comparison_count, len(comparisons_to_do)) j.set_progress(comparison_count, progress_msg)
def _get_action_from_changed_transactions(self, transactions, global_scope=False): if len(transactions) == 1 and not isinstance(transactions[0], 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) if global_scope: spawns, txns = extract(lambda x: isinstance(x, Spawn), transactions) for schedule in {spawn.recurrence for spawn in spawns}: action.change_schedule(schedule) return action
def collect_results(collect_all=False): # collect results and wait until the queue is small enough to accomodate a new results. nonlocal async_results, matches, comparison_count limit = 0 if collect_all else RESULTS_QUEUE_LIMIT while len(async_results) > limit: ready, working = extract(lambda r: r.ready(), async_results) for result in ready: matches += result.get() async_results.remove(result) comparison_count += 1 progress_msg = tr("Performed %d/%d chunk matches") % ( comparison_count, len(comparisons_to_do)) j.set_progress(comparison_count, progress_msg)
def collect_results(collect_all=False): # collect results and wait until the queue is small enough to accomodate a new results. nonlocal async_results, matches, comparison_count, comparisons_to_do limit = 0 if collect_all else RESULTS_QUEUE_LIMIT while len(async_results) > limit: ready, working = extract(lambda r: r.ready(), async_results) for result in ready: matches += result.get() async_results.remove(result) comparison_count += 1 # About the NOQA below: I think there's a bug in pyflakes. To investigate... progress_msg = tr("Performed %d/%d chunk matches") % ( comparison_count, len(comparisons_to_do), ) # NOQA j.set_progress(comparison_count, progress_msg)
def get_spawns(self, end, transactions, consumedtxns): """Returns the list of transactions spawned by our budget. Works pretty much like :meth:`core.model.recurrence.Recurrence.get_spawns`, except for the extra arguments. :param transactions: Transactions that can affect our budget spawns' final amount. :type transactions: list of :class:`.Transaction` :param consumedtxns: Transactions that have already been "consumed" by a budget spawn in this current round of spawning (one a budget "ate" a transaction, we don't have it affect another). This set is going to be mutated (augmented) by this method. All you have to do is start with an empty set and pass it around for each call. :type consumedtxns: set of :class:`.Transaction` """ spawns = Recurrence.get_spawns(self, end) # No spawn in the past spawns = [spawn for spawn in spawns if spawn.date > date.today()] 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) 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) != spawn_amount: spawn.amount = abs(spawn_amount) spawn.set_splits([ Split(spawn, account, spawn_amount), Split(spawn, self.target, -spawn_amount) ]) else: spawn.set_splits([]) consumedtxns |= set(wheat) self._previous_spawns = spawns return spawns
def merge_oneletter_elems(elements): # we go through all one-lettered boxes, we check if it intersects with any other rect. In # addition, we also check that the bottom-right corner of the letter is in the top-left part # of the paragraph. # HOWEVER, We must not forget that Y-positions in pdfminer layout is upside down. The bottom of # the page is 0 and the top is the max y-pos. oneletter, others = extract(lambda e: len(e.text.strip()) == 1, elements) for elem1 in oneletter: rect = elem1.rect for elem2 in others: otherrect = elem2.rect if rect.intersects(otherrect): corner = rect.corners()[1] line = Line(otherrect.center(), corner) if line.dx() < 0 and line.dy() > 0: elem2.text = elem1.text.strip() + elem2.text elements.remove(elem1) break
def get_spawns(self, end, transactions, consumedtxns): """Returns the list of transactions spawned by our budget. Works pretty much like :meth:`core.model.recurrence.Recurrence.get_spawns`, except for the extra arguments. :param transactions: Transactions that can affect our budget spawns' final amount. :type transactions: list of :class:`.Transaction` :param consumedtxns: Transactions that have already been "consumed" by a budget spawn in this current round of spawning (one a budget "ate" a transaction, we don't have it affect another). This set is going to be mutated (augmented) by this method. All you have to do is start with an empty set and pass it around for each call. :type consumedtxns: set of :class:`.Transaction` """ spawns = Recurrence.get_spawns(self, end) # No spawn in the past spawns = [spawn for spawn in spawns if spawn.date > date.today()] 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) 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) != spawn_amount: spawn.amount = abs(spawn_amount) spawn.set_splits([Split(spawn, account, spawn_amount), Split(spawn, self.target, -spawn_amount)]) else: spawn.set_splits([]) consumedtxns |= set(wheat) self._previous_spawns = spawns return spawns
def delete_transactions(self, transactions, from_account=None): action = Action(tr('Remove transaction')) spawns, txns = extract(lambda x: isinstance(x, Spawn), transactions) global_scope = self._query_for_scope_if_needed(spawns) schedules = set(spawn.recurrence for spawn in spawns) action.deleted_transactions |= set(txns) for schedule in schedules: action.change_schedule(schedule) self._undoer.record(action) for txn in transactions: if isinstance(txn, Spawn): if global_scope: txn.recurrence.stop_before(txn) else: txn.recurrence.delete(txn) else: self.transactions.remove(txn) min_date = min(t.date for t in transactions) self._cook(from_date=min_date) self._clean_empty_categories(from_account=from_account) self.notify('transaction_deleted') if action.changed_schedules: self.notify('schedule_changed')
def change_transactions(self, transactions): spawns, normal = extract(lambda t: isinstance(t, Spawn), transactions) self.changed_transactions |= set((t, t.replicate()) for t in normal) for schedule in set(spawn.recurrence for spawn in spawns): self.change_schedule(schedule)