def first(iterable):
    """Returns the first item of ``iterable``.
    """
    try:
        return next(iter(iterable))
    except StopIteration:
        return None def minmax(value, min_value, max_value):
    """Returns `value` or one of the min/max bounds if `value` is not between them.
    """
    return min(max(value, min_value), max_value) def nonone(value, replace_value):
    """Returns ``value`` if ``value`` is not ``None``. Returns ``replace_value`` otherwise.
    """
    if value is None:
        return replace_value
    else:
        return value class PaneType:
    NetWorth = 0
    Profit = 1
    Transaction = 2
    Account = 3
    Schedule = 4
    GeneralLedger = 7
    DocProps = 8
    PluginList = 9
    Empty = 100
    Plugin = 1000
    ReadOnlyTablePlugin = 1001 class FilterType:
    """Available types for filter at :attr:`Document.filter_type`.

    * ``Unassigned``
    * ``Income``
    * ``Expense``
    * ``Transfer``
    * ``Reconciled``
    * ``NotReconciled``.
    """
    Unassigned = object()
    Income = object() # in etable, the filter is for increase
    Expense = object() # in etable, the filter is for decrease
    Transfer = object()
    Reconciled = object()
    NotReconciled = object() class RepeatType:
    # Mirror of RepeatType enum is ccore
    Daily = 1
    Weekly = 2
    Monthly = 3
    Yearly = 4
    Weekday = 5
    WeekdayLast = 6
    ALL = [Daily, Weekly, Monthly, Yearly, Weekday, WeekdayLast]
    # used for serialization
    AS_STR = ['daily', 'weekly', 'monthly', 'yearly', 'weekday', 'weekday_last']

    @classmethod
    def to_str(cls, i):
        try:
            return cls.AS_STR[i-1]
        except IndexError:
            return 'monthly'

    @classmethod
    def from_str(cls, s):
        try:
            return cls.AS_STR.index(s) + 1
        except ValueError:
            return cls.Monthly class DateFormat:
    """Bridge "system" date formats (``%d-%m-%Y``) and "ISO" date formats (``dd-MM-yyyy``).

    We only support simple short and numerical date formats, but we can freely convert each of them
    from/to iso/sys, which is rather convenient.

    This class also supports date format inputs with a moderate amount of garbage in it. It looks
    for a separator, a day, a month and a year element, checks their order and precision, and
    ignores the rest. In case of an unreadable format, it defaults to ``dd/MM/yyyy``.

    The default initialization takes an ISO format. If you want to create a date format from a sys
    format, use :meth:`from_sysformat`.
    """
    ISO2SYS = {'yyyy': '%Y', 'yy': '%y', 'MMM': '%b', 'MM': '%m', 'M': '%m', 'dd': '%d', 'd': '%d'}
    SYS2ISO = {'%Y': 'yyyy', '%y': 'yy', '%m': 'MM', '%b': 'MMM', '%d': 'dd'}

    def __init__(self, format):
        if format is None:
            format = ''
        # Default values in case we can't parse
        self.separator = '/'
        self.elements = ['dd', 'MM', 'yyyy']
        m_separators = re_separators.search(format)
        if m_separators:
            self.separator = m_separators.group()
            elements = format.split(self.separator)
            # macOS (and possibly others) use 'y' to denote the year with a non-zero filled century.
            # Since there is no % equivalent, we'll just use 'yyyy' here instead.
            elements = ['yyyy' if x == 'y' else x for x in elements]
            if all(elem in self.ISO2SYS for elem in elements):
                self.elements = elements

    @staticmethod
    def from_sysformat(format):
        """Creates a new instance from a "sys" format (``%d-%m-%Y``)."""
        if format is None:
            format = ''
        for key, value in DateFormat.SYS2ISO.items():
            format = format.replace(key, value)
        return DateFormat(format)

    def copy(self):
        """Returns a copy of self."""
        return DateFormat(self.iso_format)

    def parse_date(self, string):
        """Parses ``string`` to a ``datetime.date``."""
        # From ticket #381, in some cases the user may input a date field which is in an
        # intermediate editing state such as '1 /12/2012'. They may either accept that or continue
        # to edit to another valid date such as '12/12/2012'. Instead of trying to make the system
        # communicate the end of editing back to the UI level, we simply remove the spurious space
        # characters in the model.
        if self.separator == ' ':
            # In instances where the separator is a space, we can not remove all space characters.
            # Instead, we replace double spaces with a single space.
            string = string.replace('  ', ' ')
        else:
            string = string.replace(' ', '')
        return datetime.strptime(string, self.sys_format).date()

    def make_numerical(self):
        """If the date format contains a non-numerical month, change it to a numerical one."""
        if 'MMM' in self.elements:
            self.elements[self.elements.index('MMM')] = 'MM'

    @property
    def iso_format(self):
        """Returns the format as ISO (``dd-MM-yyyy``)."""
        return self.separator.join(self.elements)

    @property
    def sys_format(self):
        """Returns the format as sys (``%d-%m-%Y``)."""
        repl_elems = [self.ISO2SYS[elem] for elem in self.elements]
        return self.separator.join(repl_elems) def parse_amount(string, currency):
    def __init__(self, *args, **kwargs):
    def _parse(self, infile):
    def _load(self):
        def str2date(s, default=None):
        def handle_newlines(s):
        def read_transaction_element(element):
class Loader(base.Loader):
    FILE_OPEN_MODE = 'rb'
    NATIVE_DATE_FORMAT = '%Y-%m-%d'
        TODAY = datetime.date.today() def txn_matches(txn, query):
    """Return whether ``txn`` is matching ``query``.

    ``query`` is a ``dict`` of all criteria to look for (example: ``{'payee': 'Barber shop'}``.
    List of possible dict keys:

    * description
    * payee
    * checkno
    * memo
    * amount
    * account
    * group

    All of these queries are string-based, except ``amount``, which requires an
    :class:`.Amount`.

    Returns true if any criteria matches, false otherwise.
    """
    query_description = query.get('description')
    if query_description is not None:
        if query_description in txn.description.lower():
            return True
    query_payee = query.get('payee')
    if query_payee is not None:
        if query_payee in txn.payee.lower():
            return True
    query_checkno = query.get('checkno')
    if query_checkno is not None:
        if query_checkno == txn.checkno.lower():
            return True
    query_memo = query.get('memo')
    if query_memo is not None:
        for split in txn.splits:
            if query_memo in split.memo.lower():
                return True
    query_amount = query.get('amount')
    if query_amount is not None:
        query_value = float(query_amount) if query_amount else 0
        for split in txn.splits:
            split_value = float(split.amount) if split.amount else 0
            if query_value == abs(split_value):
                return True
    query_account = query.get('account')
    if query_account is not None:
        for split in txn.splits:
            if split.account and split.account.name.lower() in query_account:
                return True
    query_group = query.get('group')
    if query_group is not None:
        for split in txn.splits:
            if split.account and split.account.groupname and \
                    split.account.groupname.lower() in query_group:
                return True
    return False class ImportWindow(GUIObject):
    # --- View interface
    # close()
    # close_selected_tab()
    # set_swap_button_enabled(enabled: bool)
    # update_selected_pane()
    # show()
    #

    def __init__(self, mainwindow, target_account=None):
        super().__init__()
        if not hasattr(mainwindow, 'loader'):
            raise ValueError("Nothing to import!")
        self.mainwindow = mainwindow
        self.document = mainwindow.document
        self.app = self.document.app
        self._selected_pane_index = 0
        self._selected_target_index = 0

        def setfunc(index):
            self.view.set_swap_button_enabled(self.can_perform_swap())
        self.swap_type_list = LinkedSelectableList(items=[
            "<placeholder> Day <--> Month",
            "<placeholder> Month <--> Year",
            "<placeholder> Day <--> Year",
            tr("Description <--> Payee"),
            tr("Invert Amounts"),
        ], setfunc=setfunc)
        self.swap_type_list.selected_index = SwapType.DayMonth
        self.panes = []
        self.import_table = ImportTable(self)

        self.loader = self.mainwindow.loader
        self.target_accounts = [
            a for a in self.document.accounts if a.is_balance_sheet_account()]
        self.target_accounts.sort(key=lambda a: a.name.lower())
        accounts = []
        for account in self.loader.accounts:
            if account.is_balance_sheet_account():
                entries = self.loader.accounts.entries_for_account(account)
                if len(entries):
                    new_name = self.document.accounts.new_name(account.name)
                    if new_name != account.name:
                        self.loader.accounts.rename_account(account, new_name)
                    accounts.append(account)
        parsing_date_format = DateFormat.from_sysformat(self.loader.parsing_date_format)
        for account in accounts:
            target = target_account
            if target is None and account.reference:
                target = getfirst(
                    t for t in self.target_accounts if t.reference == account.reference
                )
            self.panes.append(
                AccountPane(self, account, target, parsing_date_format))

    # --- Private
    def _can_swap_date_fields(self, first, second): # 'day', 'month', 'year'
        pane = self.selected_pane
        if pane is None:
            return False
        return pane.can_swap_date_fields(first, second)

    def _invert_amounts(self, apply_to_all):
        if apply_to_all:
            panes = self.panes
        else:
            panes = [self.selected_pane]
        for pane in panes:
            entries = self.loader.accounts.entries_for_account(pane.account)
            txns = dedupe(e.transaction for e in entries)
            for txn in txns:
                for split in txn.splits:
                    split.amount = -split.amount
        self.import_table.refresh()

    def _refresh_target_selection(self):
        if not self.panes:
            return
        target = self.selected_pane.selected_target
        self._selected_target_index = 0
        if target is not None:
            try:
                self._selected_target_index = self.target_accounts.index(target) + 1
            except ValueError:
                pass

    def _refresh_swap_list_items(self):
        if not self.panes:
            return
        items = []
        basefmt = self.selected_pane.parsing_date_format
        for first, second in [(DAY, MONTH), (MONTH, YEAR), (DAY, YEAR)]:
            swapped = swap_format_elements(basefmt, first, second)
            items.append("{} --> {}".format(basefmt.iso_format, swapped.iso_format))
        self.swap_type_list[:3] = items

    def _swap_date_fields(self, first, second, apply_to_all): # 'day', 'month', 'year'
        assert self._can_swap_date_fields(first, second)
        if apply_to_all:
            panes = [p for p in self.panes if p.can_swap_date_fields(first, second)]
        else:
            panes = [self.selected_pane]

        def switch_func(txn):
            txn.date = swapped_date(txn.date, first, second)

        self._swap_fields(panes, switch_func)
        # Now, lets' change the date format on these panes
        for pane in panes:
            basefmt = self.selected_pane.parsing_date_format
            swapped = swap_format_elements(basefmt, first, second)
            pane.parsing_date_format = swapped
            pane._sort_matches()
        self.import_table.refresh()
        self._refresh_swap_list_items()

    def _swap_description_payee(self, apply_to_all):
        if apply_to_all:
            panes = self.panes
        else:
            panes = [self.selected_pane]

        def switch_func(txn):
            txn.description, txn.payee = txn.payee, txn.description

        self._swap_fields(panes, switch_func)

    def _swap_fields(self, panes, switch_func):
        seen = set()
        for pane in panes:
            entries = self.loader.accounts.entries_for_account(pane.account)
            txns = dedupe(e.transaction for e in entries)
            for txn in txns:
                if txn.affected_accounts() & seen:
                    # We've already swapped this txn in a previous pane.
                    continue
                switch_func(txn)
            seen.add(pane.account)
        self.import_table.refresh()

    def _update_selected_pane(self):
        self.import_table.refresh()
        self._refresh_swap_list_items()
        self.view.update_selected_pane()
        self.view.set_swap_button_enabled(self.can_perform_swap())

    # --- Override
    def _view_updated(self):
        if self.document.can_restore_from_prefs():
            self.restore_view()
        # XXX Logically, we should call _update_selected_pane() but doing so
        # make tests fail. to investigate.
        self._refresh_target_selection()
        self.view.update_selected_pane()
        self._refresh_swap_list_items()
        self.import_table.refresh()

    # --- Public
    def can_perform_swap(self):
        index = self.swap_type_list.selected_index
        if index == SwapType.DayMonth:
            return self._can_swap_date_fields(DAY, MONTH)
        elif index == SwapType.MonthYear:
            return self._can_swap_date_fields(MONTH, YEAR)
        elif index == SwapType.DayYear:
            return self._can_swap_date_fields(DAY, YEAR)
        else:
            return True

    def close_pane(self, index):
        was_selected = index == self.selected_pane_index
        del self.panes[index]
        if not self.panes:
            self.view.close()
            return
        self._selected_pane_index = min(self._selected_pane_index, len(self.panes) - 1)
        if was_selected:
            self._update_selected_pane()


    def import_selected_pane(self):
        pane = self.selected_pane
        matches = pane.matches
        matches = [
            (e, ref) for ref, e in matches
            if e is not None and e not in self.import_table.dont_import]
        if pane.selected_target is not None:
            # We import in an existing account, adjust all the transactions accordingly
            target_account = pane.selected_target
        else:
            target_account = None
        self.document.import_entries(target_account, pane.account, matches)
        self.mainwindow.revalidate()
        self.close_pane(self.selected_pane_index)
        self.view.close_selected_tab()

    def match_entries_by_date_and_amount(self, threshold):
        self.selected_pane.match_entries_by_date_and_amount(threshold)
        self.import_table.refresh()

    def perform_swap(self, apply_to_all=False):
        index = self.swap_type_list.selected_index
        if index == SwapType.DayMonth:
            self._swap_date_fields(DAY, MONTH, apply_to_all=apply_to_all)
        elif index == SwapType.MonthYear:
            self._swap_date_fields(MONTH, YEAR, apply_to_all=apply_to_all)
        elif index == SwapType.DayYear:
            self._swap_date_fields(DAY, YEAR, apply_to_all=apply_to_all)
        elif index == SwapType.DescriptionPayee:
            self._swap_description_payee(apply_to_all=apply_to_all)
        elif index == SwapType.InvertAmount:
            self._invert_amounts(apply_to_all=apply_to_all)

    def restore_view(self):
        self.import_table.columns.restore_columns()

    # --- Properties
    @property
    def selected_pane(self):
        return self.panes[self.selected_pane_index] if self.panes else None

    @property
    def selected_pane_index(self):
        return self._selected_pane_index

    @selected_pane_index.setter
    def selected_pane_index(self, value):
        if value >= len(self.panes):
            return
        self._selected_pane_index = value
        self._refresh_target_selection()
        self._update_selected_pane()

    @property
    def selected_target_account(self):
        return self.selected_pane.selected_target

    @property
    def selected_target_account_index(self):
        return self._selected_target_index

    @selected_target_account_index.setter
    def selected_target_account_index(self, value):
        target = self.target_accounts[value - 1] if value > 0 else None
        self.selected_pane.selected_target = target
        self._selected_target_index = value
        self.import_table.refresh()

    @property
    def target_account_names(self):
        return [tr('< New Account >')] + [a.name for a in self.target_accounts] class TransactionView(TransactionViewBase):
    VIEW_TYPE = PaneType.Transaction
    PRINT_TITLE_FORMAT = tr('Transactions from {start_date} to {end_date}')
    PRINT_VIEW_CLASS = TransactionPrint

    def __init__(self, mainwindow):
        super().__init__(mainwindow)
        self._visible_transactions = None
        self.filter_bar = FilterBar(self)
        self.ttable = self.table = TransactionTable(self)
        self.maintable = self.ttable
        self.columns = self.maintable.columns
        self.restore_subviews_size()

    def _revalidate(self):
        TransactionViewBase._revalidate(self)
        self.filter_bar.refresh()

    # --- Private
    def _refresh_totals(self):
        selected = len(self.mainwindow.selected_transactions)
        total = len(self.visible_transactions)
        currency = self.document.default_currency
        total_amount = sum(amount_convert(t.amount, currency, t.date) for t in self.mainwindow.selected_transactions)
        total_amount_fmt = self.document.format_amount(total_amount)
        msg = tr("{0} out of {1} selected. Amount: {2}")
        self.status_line = msg.format(selected, total, total_amount_fmt)

    def _set_visible_transactions(self):
        date_range = self.document.date_range
        txns = [t for t in self.document.oven.transactions if t.date in date_range]
        query_string = self.mainwindow.filter_string
        filter_type = self.mainwindow.filter_type
        if not query_string and filter_type is None:
            self._visible_transactions = txns
            return
        if query_string:
            query = self.mainwindow.parse_search_query(query_string)
            txns = [t for t in txns if txn_matches(t, query)]
        if filter_type is FilterType.Unassigned:
            txns = [t for t in txns if t.has_unassigned_split]
        elif filter_type is FilterType.Income:
            txns = [t for t in txns if any(getattr(s.account, 'type', '') == AccountType.Income for s in t.splits)]
        elif filter_type is FilterType.Expense:
            txns = [t for t in txns if any(getattr(s.account, 'type', '') == AccountType.Expense for s in t.splits)]
        elif filter_type is FilterType.Transfer:
            def is_transfer(t):
                return len([s for s in t.splits if s.account is not None and s.account.is_balance_sheet_account()]) >= 2
            txns = list(filter(is_transfer, txns))
        elif filter_type is FilterType.Reconciled:
            txns = [t for t in txns if any(s.reconciled for s in t.splits)]
        elif filter_type is FilterType.NotReconciled:
            txns = [t for t in txns if all(not s.reconciled for s in t.splits)]
        self._visible_transactions = txns

    # --- Override
    def _invalidate_cache(self):
        self._visible_transactions = None

    def save_preferences(self):
        self.ttable.columns.save_columns()

    def show(self):
        super().show()
        self.ttable.show()

    def update_transaction_selection(self, transactions):
        self._refresh_totals()

    # --- Public
    def move_down(self):
        self.ttable.move_down()

    def move_up(self):
        self.ttable.move_up()

    def show_account(self):
        self.ttable.show_from_account()

    # --- Properties
    @property
    def visible_transactions(self):
        if self._visible_transactions is None:
            self._set_visible_transactions()
        return self._visible_transactions
import logging
import re
import weakref
from core.util import first, minmax, nonone
from core.trans import tr
from ..const import PaneType, FilterType
from ..exception import OperationAborted, FileFormatError
from ..model._ccore import inc_date, Recurrence
from ..model.date import RepeatType, DateFormat
from ..loader import csv, qif, ofx, native
from .util import txn_matches
from .base import DocumentGUIObject
from .search_field import SearchField
from .date_range_selector import DateRangeSelector
from .account_lookup import AccountLookup
from .completion_lookup import CompletionLookup
from .export_panel import ExportPanel
from .import_window import ImportWindow
from .csv_options import CSVOptions
from .networth_view import NetWorthView
from .profit_view import ProfitView
from .transaction_view import TransactionView
from .account_view import AccountView
from .schedule_view import ScheduleView
from .general_ledger_view import GeneralLedgerView
from .docprops_view import DocPropsView
from .empty_view import EmptyView
        elif pane_type == PaneType.Schedule:
            result = ScheduleView(self)
        elif pane_type == PaneType.GeneralLedger:
            result = GeneralLedgerView(self)
        elif pane_type == PaneType.DocProps:
            result = DocPropsView(self)
        elif pane_type == PaneType.Empty:
            result = EmptyView(self)
        else:
            raise ValueError("Cannot create view of type {}".format(pane_type))
        return result

    def _invalidate_hidden_panes(self):
        for pane in self.panes:
            if pane is not self._current_pane:
                pane.view.invalidate()

    def _invalidate_visible_entries(self):
        self._account2visibleentries = {}

    def _perform_if_possible(self, action_name):
        current_view = self._current_pane.view
        if current_view.can_perform(action_name):
            return getattr(current_view, action_name)()

    def _restore_default_panes(self):
        pane_types = [
            PaneType.NetWorth, PaneType.Profit, PaneType.Transaction,
            PaneType.Schedule
        ]
        pane_data = list(zip(pane_types, [None] * len(pane_types)))
        self._set_panes(pane_data)

    def _restore_opened_panes(self):
        stored_panes = self.document.get_default(Preference.OpenedPanes)
        logging.debug('Restoring panes from data %r', stored_panes)
        if not stored_panes:
            return
        pane_data = []
        for data in stored_panes:
            pane_type = data['pane_type']
            if pane_type == PaneType.Account:
                account_name = str(data.get('account_name', '')) # str() because the value might be an int
                account = self.document.accounts.find(account_name)
                if account is None:
                    continue
                arg = account
            elif pane_type >= PaneType.Plugin:
                arg = str(data.get('plugin_name', ''))
            else:
                arg = None
            pane_data.append((pane_type, arg))
        if pane_data:
            self._set_panes(pane_data)
            selected_pane_index = self.document.get_default(Preference.SelectedPane)
            if selected_pane_index is not None:
                self.current_pane_index = selected_pane_index

    def _save_preferences(self):
        opened_panes = []
        for pane in self.panes:
            data = {}
            data['pane_type'] = pane.view.VIEW_TYPE
            if pane.account is not None:
                data['account_name'] = pane.account.name
            if pane.view.VIEW_TYPE >= PaneType.Plugin:
                data['plugin_name'] = pane.view.plugin.plugin_id()
            opened_panes.append(data)
        logging.debug('Saving panes with data %r', opened_panes)
        self.document.set_default(Preference.OpenedPanes, opened_panes)
        self.document.set_default(Preference.SelectedPane, self._current_pane_index)
        self.document.set_default(Preference.HiddenAreas, list(self.hidden_areas))
        window_frame = self.view.save_window_frame()
        if window_frame:
            self.document.set_default(Preference.WindowFrame, list(window_frame))

    def _set_panes(self, pane_data):
        # Replace opened panes with new panes from `pane_data`, which is a [(pane_type, arg)]
        self._current_pane = None
        self._current_pane_index = -1
        self.panes = []
        for pane_type, arg in pane_data:
            try:
                self.panes.append(self._create_pane(pane_type, account=arg))
            except ValueError:
                self.panes.append(self._create_pane(PaneType.NetWorth))
        self.view.refresh_panes()
        self.current_pane_index = 0

    def _set_current_pane(self, newpane):
        index = self.current_pane_index
        self.panes[index] = newpane
        self.view.refresh_panes()
        self._change_current_pane(newpane)

    def _update_area_visibility(self):
        if self._current_pane is not None:
            self._current_pane.view.update_visibility()
        self.view.update_area_visibility()

    def _visible_entries_for_account(self, account):
        date_range = self.document.date_range
        entries = self.document.accounts.entries_for_account(account)
        entries = [e for e in entries if e.date in date_range]
        query_string = self.filter_string
        filter_type = self.filter_type
        if query_string:
            query = self.parse_search_query(query_string)
            entries = [e for e in entries if txn_matches(e.transaction, query)]
        if filter_type is FilterType.Unassigned:
            entries = [e for e in entries if not e.transfer]
        elif (filter_type is FilterType.Income) or (filter_type is FilterType.Expense):
            if account.is_credit_account():
                want_positive = filter_type is FilterType.Expense
            else:
                want_positive = filter_type is FilterType.Income
            if want_positive:
                entries = [e for e in entries if e.amount > 0]
            else:
                entries = [e for e in entries if e.amount < 0]

        elif filter_type is FilterType.Transfer: