Ejemplo n.º 1
0
 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
Ejemplo n.º 2
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
     }
Ejemplo n.º 3
0
 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
Ejemplo n.º 4
0
 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
Ejemplo n.º 5
0
 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)
Ejemplo n.º 6
0
 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)
Ejemplo n.º 7
0
 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()
Ejemplo n.º 8
0
    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
Ejemplo n.º 9
0
    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
Ejemplo n.º 10
0
    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
Ejemplo n.º 11
0
 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)
Ejemplo n.º 12
0
 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)
Ejemplo n.º 13
0
    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])
Ejemplo n.º 14
0
 def text(self, newtext):
     self.value = self._parse(nonone(newtext, ''))
Ejemplo n.º 15
0
 def filter_string(self, value):
     value = nonone(value, '').strip()
     if value == self._filter_string:
         return
     self._filter_string = value
     self._apply_filter()
Ejemplo n.º 16
0
    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)
Ejemplo n.º 17
0
 def _set_completion(self, completion):
     completion = nonone(completion, '')
     self._complete_completion = completion
     self.completion = completion[len(self._text):]
     if self.completion:
         self.view.refresh()
Ejemplo n.º 18
0
 def is_balance_negative(self):
     return nonone(self._the_balance(), 0) < 0
Ejemplo n.º 19
0
 def filter_string(self, value):
     value = nonone(value, '').strip()
     if value == self._filter_string:
         return
     self._filter_string = value
     self._apply_filter()
Ejemplo n.º 20
0
 def is_balance_negative(self):
     return nonone(self._the_balance(), 0) < 0
Ejemplo n.º 21
0
 def _set_completion(self, completion):
     completion = nonone(completion, '')
     self._complete_completion = completion
     self.completion = completion[len(self._text):]
     if self.completion:
         self.view.refresh()
Ejemplo n.º 22
0
 def text(self, newtext):
     self.value = self._parse(nonone(newtext, ''))
Ejemplo n.º 23
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}