def load(self, accounts): description = self.description payee = self.payee checkno = self.checkno date = self.date account = amount = None if self.account: account, amount = process_split(accounts, self.account, self.amount, self.currency) transaction = Transaction(1, date, description, payee, checkno, account, amount) for str_account, str_amount, memo in self.splits: account, amount = process_split(accounts, str_account, str_amount, None) # splits are only used for QIF and in QIF, amounts are reversed amount *= -1 memo = nonone(memo, '') split = transaction.new_split() split.account = account split.amount = amount split.memo = memo while len(transaction.splits) < 2: transaction.new_split() transaction.balance() if self.reference is not None: for split in transaction.splits: if split.reference is None: split.reference = self.reference return transaction
def _restore_preferences_after_load(self): # some preference need the file loaded before attempting a restore logging.debug('restore_preferences_after_load() beginning') excluded_account_names = set( nonone(self.get_default(EXCLUDED_ACCOUNTS_PREFERENCE), [])) self.excluded_accounts = { a for a in self.accounts if a.name in excluded_account_names }
def layout2preference(layout): result = {} result['name'] = layout.name # trim trailing None values columns = list(dropwhile(lambda x: x is None, layout.columns[::-1]))[::-1] # None values cannot be put in preferences, change them to an empty string. columns = [nonone(col, '') for col in columns] result['columns'] = columns result['excluded_lines'] = sorted(list(layout.excluded_lines)) if layout.target_account_name: result['target_account'] = layout.target_account_name return result
def paint(self, painter, option, index): ItemDelegate.paint(self, painter, option, index) extraFlags = nonone(self._model.data(index, EXTRA_ROLE), 0) if extraFlags & (EXTRA_UNDERLINED | EXTRA_UNDERLINED_DOUBLE): p1 = option.rect.bottomLeft() p2 = option.rect.bottomRight() p1.setX(p1.x()+1) p2.setX(p2.x()-1) painter.drawLine(p1, p2) if extraFlags & EXTRA_UNDERLINED_DOUBLE: # Yes, yes, we step over the item's bounds, but we have no choice because under # Windows, there's not enough height for double lines. Moreover, the line under # a total line is a blank line, so we have plenty of space. p1.setY(p1.y()+2) p2.setY(p2.y()+2) painter.drawLine(p1, p2)
def paint(self, painter, option, index): ItemDelegate.paint(self, painter, option, index) extraFlags = nonone(self._model.data(index, EXTRA_ROLE), 0) if extraFlags & (EXTRA_UNDERLINED | EXTRA_UNDERLINED_DOUBLE): p1 = option.rect.bottomLeft() p2 = option.rect.bottomRight() p1.setX(p1.x() + 1) p2.setX(p2.x() - 1) painter.drawLine(p1, p2) if extraFlags & EXTRA_UNDERLINED_DOUBLE: # Yes, yes, we step over the item's bounds, but we have no choice because under # Windows, there's not enough height for double lines. Moreover, the line under # a total line is a blank line, so we have plenty of space. p1.setY(p1.y() + 2) p2.setY(p2.y() + 2) painter.drawLine(p1, p2)
def render(self, painter): # We used to re-use itemDelegate() for cell drawing, but it turned out to me more # complex than anything (with margins being too wide and all...) columnWidths = self.stats.columnWidths(self.rect.width()) rowHeight = self.stats.rowHeight headerHeight = self.stats.headerHeight left = self.rect.left() top = self.rect.top() painter.save() painter.setFont(self.ds.headerFont()) for colStats, colWidth in zip(self.stats.columns, columnWidths): col = colStats.col headerRect = QRect(left, top, colWidth, headerHeight) headerRect = applyMargin(headerRect, CELL_MARGIN) painter.drawText(headerRect, col.alignment, col.display) left += colWidth top += headerHeight painter.restore() painter.drawLine( self.rect.left(), self.rect.top()+headerHeight, self.rect.right(), self.rect.top()+headerHeight ) painter.save() painter.setFont(self.ds.rowFont()) for rs in self.rowStats: rowIndex = rs.index left = self.rect.left() extraRole = nonone(self.ds.data(rowIndex, 0, EXTRA_ROLE), 0) rowIsSpanning = extraRole & EXTRA_SPAN_ALL_COLUMNS if rowIsSpanning: itemRect = QRect(left, top, self.rect.width(), rowHeight) self.renderCell(painter, rs, None, itemRect) else: for colStats, colWidth in zip(self.stats.columns, columnWidths): indentation = self.ds.indentation(rowIndex, colStats.index) itemRect = QRect(left+indentation, top, colWidth, rowHeight) itemRect = applyMargin(itemRect, CELL_MARGIN) self.renderCell(painter, rs, colStats, itemRect) left += colWidth if rs.splitCount > 2: splitsRect = QRect( self.rect.left()+SPLIT_XOFFSET, top+rowHeight, self.rect.width(), rs.height-rowHeight ) self.renderSplit(painter, rs, splitsRect) top += rs.height painter.restore()
def get_spawns(self, end): """Returns the list of transactions spawned by our recurrence. We start at :attr:`start_date` and end at ``end``. We have to specify an end to our spawning to avoid getting infinite results. .. rubric:: End date adjustment If a changed date end up being smaller than the "spawn date", it's possible that a spawn that should have been spawned for the date range is not spawned. Therefore, we always spawn at least until the date of the last exception. For global changes, it's even more complicated. If the global date delta is negative enough, we can end up with a spawn that doesn't go far enough, so we must adjust our max date by this delta. :param datetime.date end: When to stop spawning. :rtype: list of :class:`Spawn` """ if self.date2exception: end = max(end, max(self.date2exception.keys())) if self.date2globalchange: min_date_delta = min(ref.date-date for date, ref in self.date2globalchange.items()) if min_date_delta < datetime.timedelta(days=0): end += -min_date_delta end = min(end, nonone(self.stop_date, datetime.date.max)) date_counter = DateCounter(self.start_date, self.repeat_type, self.repeat_every, end) result = [] global_date_delta = datetime.timedelta(days=0) current_ref = self.ref for current_date in date_counter: if current_date in self.date2globalchange: current_ref = self.date2globalchange[current_date] global_date_delta = current_ref.date - current_date if current_date in self.date2exception: exception = self.date2exception[current_date] if exception is not None: result.append(exception) else: if current_date not in self.date2instances: spawn = self._create_spawn(current_ref, current_date) if global_date_delta: # Only muck with spawn.date if we have a delta. otherwise we're breaking # budgets. spawn.date = current_date + global_date_delta self.date2instances[current_date] = spawn result.append(self.date2instances[current_date]) return result
def get_default(self, key, fallback_value=None): """Returns moneyGuru user pref for ``key``. .. seealso:: :meth:`ApplicationView.get_default` :param str key: The key of the prefence to return. :param fallback_value: if the pref doesn't exist or isn't of the same type as the fallback value, return the fallback. Therefore, you can use the fallback value as a way to tell "I expect preferences of this type". """ result = nonone(self.view.get_default(key), fallback_value) if fallback_value is not None and not isinstance(result, type(fallback_value)): # we don't want to end up with garbage values from the prefs try: result = type(fallback_value)(result) except Exception: result = fallback_value return result
def get_default(self, key, fallback_value=None): """Returns moneyGuru user pref for ``key``. .. seealso:: :meth:`ApplicationView.get_default` :param str key: The key of the prefence to return. :param fallback_value: if the pref doesn't exist or isn't of the same type as the fallback value, return the fallback. Therefore, you can use the fallback value as a way to tell "I expect preferences of this type". """ result = nonone(self.view.get_default(key), fallback_value) if fallback_value is not None and not isinstance( result, type(fallback_value)): # we don't want to end up with garbage values from the prefs try: result = type(fallback_value)(result) except Exception: result = fallback_value return result
def __init__(self, ds): rowFM = QFontMetrics(ds.rowFont()) self.splitFM = QFontMetrics(ds.splitFont()) headerFM = QFontMetrics(ds.headerFont()) self.rowHeight = rowFM.height() + CELL_MARGIN * 2 self.splitHeight = self.splitFM.height() + CELL_MARGIN * 2 self.headerHeight = headerFM.height() + CELL_MARGIN * 2 spannedRowIndexes = set() for rowIndex in range(ds.rowCount()): extraRole = nonone(ds.data(rowIndex, 0, EXTRA_ROLE), 0) if extraRole & EXTRA_SPAN_ALL_COLUMNS: spannedRowIndexes.add(rowIndex) self.columns = [] for colIndex in range(ds.columnCount()): col = ds.columnAtIndex(colIndex) sumWidth = 0 maxWidth = 0 # We need to have *at least* the width of the header. minWidth = headerFM.width(col.display) + CELL_MARGIN * 2 for rowIndex in range(ds.rowCount()): if rowIndex in spannedRowIndexes: continue data = ds.data(rowIndex, colIndex, Qt.DisplayRole) if data: font = ds.data(rowIndex, colIndex, Qt.FontRole) fm = QFontMetrics(font) if font is not None else rowFM width = fm.width(data) + CELL_MARGIN * 2 width += ds.indentation(rowIndex, colIndex) sumWidth += width maxWidth = max(maxWidth, width) pixmap = ds.data(rowIndex, colIndex, Qt.DecorationRole) if pixmap is not None: width = pixmap.width() + CELL_MARGIN * 2 maxWidth = max(maxWidth, width) avgWidth = sumWidth // ds.rowCount() maxWidth = max(maxWidth, minWidth) if col.cantTruncate: # if it's a "can't truncate" column, we make no concession minWidth = maxWidth cs = ColumnStats(colIndex, col, avgWidth, maxWidth, minWidth) self.columns.append(cs) self.maxWidth = sum(cs.maxWidth for cs in self.columns) self.minWidth = sum(cs.minWidth for cs in self.columns)
def renderCell(self, painter, rowStats, colStats, itemRect): rowIndex = rowStats.index colIndex = colStats.index if colStats is not None else 0 extraFlags = nonone(self.ds.data(rowIndex, colIndex, EXTRA_ROLE), 0) pixmap = self.ds.data(rowIndex, colIndex, Qt.DecorationRole) if pixmap: painter.drawPixmap(itemRect.topLeft(), pixmap) else: # we don't support drawing pixmap and text in the same cell (don't need it) bgbrush = self.ds.data(rowIndex, colIndex, Qt.BackgroundRole) if bgbrush is not None: painter.fillRect(itemRect, bgbrush) if rowStats.splitCount > 2 and colStats.col.name in {'from', 'to', 'transfer'}: text = '--split--' else: text = self.ds.data(rowIndex, colIndex, Qt.DisplayRole) if text: alignment = self.ds.data(rowIndex, colIndex, Qt.TextAlignmentRole) if not alignment: alignment = Qt.AlignLeft|Qt.AlignVCenter font = self.ds.data(rowIndex, colIndex, Qt.FontRole) if font is None: font = self.ds.rowFont() fm = QFontMetrics(font) # elidedText has a tendency to "over-elide" that's why we have "+1" text = fm.elidedText(text, Qt.ElideRight, itemRect.width()+1) painter.save() painter.setFont(font) painter.drawText(itemRect, alignment, text) painter.restore() if extraFlags & (EXTRA_UNDERLINED | EXTRA_UNDERLINED_DOUBLE): p1 = itemRect.bottomLeft() p2 = itemRect.bottomRight() # Things get crowded with double lines and we have to cheat a little bit on # item rects. p1.setY(p1.y()+2) p2.setY(p2.y()+2) painter.drawLine(p1, p2) if extraFlags & EXTRA_UNDERLINED_DOUBLE: p1.setY(p1.y()-3) p2.setY(p2.y()-3) painter.drawLine(p1, p2)
def _getDataFromIndex(self, index): """Retrieves model amount data and converts it to a DisplayAmount. Retrieves the amount string from the model based on the column name. Uses a regular expression to parse this string, converting it into a DisplayAmount to be returned. Args: index - QModelIndex in the model Returns: None if the data is not in the model or the regular expression failed to parse the data. A DisplayAmount with currency and value information used to perform amount painting. For example: "MXN 432 321,01" -> DisplayAmount("MXN", "432 321,01") """ if not index.isValid(): return None column = self._model.columns.column_by_index(index.column()) if column.name != self._attr_name: return None amount = getattr(self._model[index.row()], column.name) amount = CURR_VALUE_RE.match(amount) if amount is None: logging.warning("Amount for column %s index row %s " "with amount '%s' did not match regular " "expression for amount painting.", column.name, index.row(), amount) return None amount = amount.groups() return DisplayAmount(nonone(amount[0], "").strip(), amount[1])
def text(self, newtext): self.value = self._parse(nonone(newtext, ''))
def filter_string(self, value): value = nonone(value, '').strip() if value == self._filter_string: return self._filter_string = value self._apply_filter()
def _load(self): TODAY = datetime.date.today() def str2date(s, default=None): try: return base.parse_date_str(s, self.parsing_date_format) except (ValueError, TypeError): return default def handle_newlines(s): # etree doesn't correctly save newlines. During save, we escape them. Now's the time to # restore them. # XXX After a while, when most users will have used a moneyGuru version that doesn't # need newline escaping on save, we can remove this one as well. if not s: return s return s.replace('\\n', '\n') def read_transaction_element(element): attrib = element.attrib date = str2date(attrib.get('date'), TODAY) description = attrib.get('description') payee = attrib.get('payee') checkno = attrib.get('checkno') txn = Transaction(1, date, description, payee, checkno, None, None) txn.notes = handle_newlines(attrib.get('notes')) or '' try: txn.mtime = int(attrib.get('mtime', 0)) except ValueError: txn.mtime = 0 reference = attrib.get('reference') for split_element in element.iter('split'): attrib = split_element.attrib accountname = attrib.get('account') str_amount = attrib.get('amount') account, amount = base.process_split( self.accounts, accountname, str_amount, strict_currency=True) split = txn.new_split() split.account = account split.amount = amount split.memo = attrib.get('memo') or '' split.reference = attrib.get('reference') or reference if attrib.get('reconciled') == 'y': split.reconciliation_date = date elif account is None or not (not amount or amount.currency_code == account.currency): # fix #442: off-currency transactions shouldn't be reconciled split.reconciliation_date = None elif 'reconciliation_date' in attrib: split.reconciliation_date = str2date(attrib['reconciliation_date']) txn.balance() while len(txn.splits) < 2: txn.new_split() return txn root = self.root self.document_id = root.attrib.get('document_id') props_element = root.find('properties') if props_element is not None: for name, value in props_element.attrib.items(): # For now, all our prefs except default_currency are ints, so # we can simply assume tryint, but we'll eventually need # something more sophisticated. if name != 'default_currency': value = tryint(value, default=None) if name and value is not None: self.properties[name] = value for account_element in root.iter('account'): attrib = account_element.attrib name = attrib.get('name') if not name: continue currency = self.get_currency(attrib.get('currency')) type = base.get_account_type(attrib.get('type')) account = self.accounts.create(name, currency, type) group = attrib.get('group') reference = attrib.get('reference') account_number = attrib.get('account_number', '') inactive = attrib.get('inactive') == 'y' notes = handle_newlines(attrib.get('notes', '')) account.change( groupname=group, reference=reference, account_number=account_number, inactive=inactive, notes=notes) elements = [e for e in root if e.tag == 'transaction'] # we only want transaction element *at the root* for transaction_element in elements: txn = read_transaction_element(transaction_element) self.transactions.add(txn) for recurrence_element in root.iter('recurrence'): attrib = recurrence_element.attrib ref = read_transaction_element(recurrence_element.find('transaction')) repeat_type = attrib.get('type') repeat_every = int(attrib.get('every', '1')) recurrence = Recurrence(ref, repeat_type, repeat_every) recurrence.stop_date = str2date(attrib.get('stop_date')) for exception_element in recurrence_element.iter('exception'): try: date = str2date(exception_element.attrib['date']) txn_element = exception_element.find('transaction') exception = read_transaction_element(txn_element) if txn_element is not None else None if exception: spawn = Spawn(recurrence, exception, date, exception.date) recurrence.date2exception[date] = spawn else: recurrence.delete_at(date) except KeyError: continue for change_element in recurrence_element.iter('change'): try: date = str2date(change_element.attrib['date']) txn_element = change_element.find('transaction') change = read_transaction_element(txn_element) if txn_element is not None else None spawn = Spawn(recurrence, change, date, change.date) recurrence.date2globalchange[date] = spawn except KeyError: continue self.schedules.append(recurrence) budgets = list(root.iter('budget')) if budgets: attrib = budgets[0].attrib start_date = str2date(attrib.get('start_date')) if start_date: self.budgets.start_date = start_date self.budgets.repeat_type = attrib.get('type') repeat_every = tryint(attrib.get('every'), default=None) if repeat_every: self.budgets.repeat_every = repeat_every for budget_element in budgets: attrib = budget_element.attrib account_name = attrib.get('account') amount = attrib.get('amount') notes = attrib.get('notes') if not (account_name and amount): continue account = self.accounts.find(account_name) if account is None: continue amount = parse_amount(amount, account.currency) budget = Budget(account, amount) budget.notes = nonone(notes, '') self.budgets.append(budget)
def _set_completion(self, completion): completion = nonone(completion, '') self._complete_completion = completion self.completion = completion[len(self._text):] if self.completion: self.view.refresh()
def is_balance_negative(self): return nonone(self._the_balance(), 0) < 0
def _restore_preferences_after_load(self): # some preference need the file loaded before attempting a restore logging.debug('restore_preferences_after_load() beginning') excluded_account_names = set(nonone(self.get_default(EXCLUDED_ACCOUNTS_PREFERENCE), [])) self.excluded_accounts = {a for a in self.accounts if a.name in excluded_account_names}