Ejemplo n.º 1
0
    def _setup_widgets(self):
        self._replace_widget()

        # Add the two buttons
        self.find_button = self._create_button(gtk.STOCK_FIND)
        self.edit_button = self._create_button(gtk.STOCK_NEW)
        can_edit = self._entry.get_editable() and self._entry.get_sensitive()
        self.find_button.set_sensitive(can_edit)

        self.find_button.set_tooltip_text(self.find_tooltip)
        self.edit_button.set_tooltip_text(self.new_tooltip)

        # the entry needs a completion to work in MODE_DATA
        self._completion = gtk.EntryCompletion()
        self._entry.set_completion(self._completion)
        self._entry.set_mode(ENTRY_MODE_DATA)

        initial_value = getattr(self._model, self._model_property)
        self.set_value(initial_value)

        # The filter that will be used. This is not really in the interface. We
        # will just use it to perform the search.
        self._filter = StringSearchFilter('')
        self._executer = QueryExecuter(self.store)
        self._executer.set_search_spec(self._search_class.search_spec)
        self._executer.set_filter_columns(self._filter, self._search_columns)
Ejemplo n.º 2
0
    def __init__(self,
                 entry,
                 store,
                 initial_value=None,
                 parent=None,
                 run_editor=None):
        """
        :param entry: The entry that we should modify
        :param store: The store that will be used for database queries
        :param initial_value: Initial value for the entry
        :param parent: The parent that should be respected when running other
          dialogs
        """
        super(QueryEntryGadget, self).__init__()

        self._current_obj = None
        self._parent = parent
        self._on_run_editor = run_editor
        self.entry = entry
        self.entry.set_mode(ENTRY_MODE_DATA)
        self.store = store

        # The filter that will be used. This is not really in the interface.
        # We will just use it to perform the search.
        self._filter = StringSearchFilter('')
        self._executer = QueryExecuter(self.store)
        self._executer.set_search_spec(self.SEARCH_SPEC)
        self._executer.set_filter_columns(self._filter, self.SEARCH_COLUMNS)

        self._last_operation = None
        self._source_id = None
        self._is_person = issubclass(self.ITEM_EDITOR, BasePersonRoleEditor)

        self._setup()
        self.set_value(initial_value, force=True)
Ejemplo n.º 3
0
    def __init__(self, entry, store, initial_value=None,
                 parent=None, run_editor=None,
                 edit_button=None, info_button=None,
                 search_clause=None):
        """
        :param entry: The entry that we should modify
        :param store: The store that will be used for database queries
        :param initial_value: Initial value for the entry
        :param parent: The parent that should be respected when running other
          dialogs
        """
        super(QueryEntryGadget, self).__init__()

        self._parent = parent
        self._on_run_editor = run_editor
        self._can_edit = False
        self._search_clause = search_clause
        self.entry = entry
        self.entry.set_mode(ENTRY_MODE_DATA)
        self.edit_button = edit_button
        self.info_button = info_button
        self.store = store

        # The filter that will be used. This is not really in the interface.
        # We will just use it to perform the search.
        self._filter = StringSearchFilter('')
        self._executer = QueryExecuter(self.store)
        self._executer.set_search_spec(self.search_spec)
        self._executer.set_filter_columns(self._filter, self.search_columns)

        self._last_operation = None
        self._source_id = None

        self._setup()
        self.set_value(initial_value, force=True)
Ejemplo n.º 4
0
 def _create_search(self):
     self.search = SearchSlave(self._get_columns(self.model.kind))
     self.search.connect('result-item-activated',
                         self._on_search__item_activated)
     self.search.enable_advanced_search()
     self.query = QueryExecuter(self.app.store)
     self.search.set_query_executer(self.query)
     self.search.set_result_view(FinancialSearchResults)
     self.result_view = self.search.result_view
     self.result_view.page = self
     tree_view = self.search.result_view.get_treeview()
     tree_view.set_rules_hint(True)
     tree_view.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_BOTH)
Ejemplo n.º 5
0
 def setup_slaves(self):
     self.search = SearchSlave(self._get_columns(),
                               restore_name=self.__class__.__name__)
     self.search.enable_advanced_search()
     self.attach_slave('place_holder', self.search)
     self.executer = QueryExecuter(self.store)
     self.search.set_query_executer(self.executer)
     self.executer.set_table(LoanView)
     self.executer.add_query_callback(self.get_extra_query)
     self._create_filters()
     self.search.results.connect('selection-changed',
                                 self._on_results_selection_changed)
     self.search.results.set_selection_mode(gtk.SELECTION_MULTIPLE)
     self.search.focus_search_entry()
Ejemplo n.º 6
0
    def get_query_executer(self):
        """
        Fetchs the QueryExecuter for the SearchContainer

        :returns: a querty executer
        :rtype: a :class:`QueryExecuter` subclass
        """
        if self._query_executer is None:
            executer = QueryExecuter(self.store)
            if not self._lazy_search:
                executer.set_limit(sysparam.get_int('MAX_SEARCH_RESULTS'))
            if self._search_spec is not None:
                executer.set_search_spec(self._search_spec)
            self._query_executer = executer
        return self._query_executer
Ejemplo n.º 7
0
 def _create_search(self):
     self.search = SearchSlave(self._get_columns(),
                               restore_name=self.__class__.__name__)
     self.search.enable_advanced_search()
     self.attach_slave('searchbar_holder', self.search)
     self.executer = QueryExecuter(self.store)
     self.search.set_query_executer(self.executer)
     self.executer.set_table(PurchaseOrderView)
     self.executer.add_query_callback(self.get_extra_query)
     self._create_filters()
     self.search.results.connect('selection-changed',
                                 self._on_results__selection_changed)
     self.search.results.connect('row-activated',
                                 self._on_results__row_activated)
     self.search.focus_search_entry()
Ejemplo n.º 8
0
    def __init__(self, entry, store, initial_value=None,
                 parent=None, run_editor=None):
        """
        :param entry: The entry that we should modify
        :param store: The store that will be used for database queries
        :param initial_value: Initial value for the entry
        :param parent: The parent that should be respected when running other
          dialogs
        """
        super(QueryEntryGadget, self).__init__()

        self._current_obj = None
        self._parent = parent
        self._on_run_editor = run_editor
        self.entry = entry
        self.entry.set_mode(ENTRY_MODE_DATA)
        self.store = store

        # The filter that will be used. This is not really in the interface.
        # We will just use it to perform the search.
        self._filter = StringSearchFilter('')
        self._executer = QueryExecuter(self.store)
        self._executer.set_search_spec(self.SEARCH_SPEC)
        self._executer.set_filter_columns(self._filter, self.SEARCH_COLUMNS)

        self._last_operation = None
        self._source_id = None
        self._is_person = issubclass(self.ITEM_EDITOR, BasePersonRoleEditor)

        self._setup()
        self.set_value(initial_value, force=True)
Ejemplo n.º 9
0
    def _setup_widgets(self):
        self._replace_widget()

        # Add the two buttons
        self.find_button = self._create_button(gtk.STOCK_FIND)
        self.edit_button = self._create_button(gtk.STOCK_NEW)
        can_edit = self._entry.get_editable() and self._entry.get_sensitive()
        self.find_button.set_sensitive(can_edit)

        self.find_button.set_tooltip_text(self.find_tooltip)
        self.edit_button.set_tooltip_text(self.new_tooltip)

        # the entry needs a completion to work in MODE_DATA
        self._completion = gtk.EntryCompletion()
        self._entry.set_completion(self._completion)
        self._entry.set_mode(ENTRY_MODE_DATA)

        initial_value = getattr(self._model, self._model_property)
        self.set_value(initial_value)

        # The filter that will be used. This is not really in the interface. We
        # will just use it to perform the search.
        self._filter = StringSearchFilter('')
        self._executer = QueryExecuter(self.store)
        self._executer.set_search_spec(self._search_class.search_spec)
        self._executer.set_filter_columns(self._filter, self._search_columns)
Ejemplo n.º 10
0
    def __init__(self, store, search_table=None, hide_footer=True,
                 title='', selection_mode=None, double_click_confirm=False):
        """
        A base class for search dialog inheritance

        :param store: a store
        :param table:
        :param search_table:
        :param hide_footer:
        :param title:
        :param selection_mode:
        :param double_click_confirm: If double click a item in the list should
          automatically confirm
        """

        self.store = store
        self.search_table = search_table or self.search_table
        if not self.search_table:
            raise ValueError("%r needs a search table" % self)
        self.selection_mode = self._setup_selection_mode(selection_mode)
        self.summary_label = None
        self.double_click_confirm = double_click_confirm

        BasicDialog.__init__(self, hide_footer=hide_footer,
                             main_label_text=self.main_label_text,
                             title=title or self.title,
                             size=self.size)

        self.executer = QueryExecuter(store)
        # FIXME: Remove this limit, but we need to migrate all existing
        #        searches to use lazy lists first. That in turn require
        #        us to rewrite the queries in such a way that count(*)
        #        will work properly.
        self.executer.set_limit(sysparam(self.store).MAX_SEARCH_RESULTS)
        self.set_table(self.search_table)

        self.enable_window_controls()
        self.disable_ok()
        self.set_ok_label(_('Se_lect Items'))
        self._setup_search()
        self._setup_details_slave()

        self.create_filters()
        self.setup_widgets()
        if self.search_label:
            self.set_searchbar_label(self.search_label)
Ejemplo n.º 11
0
 def _date_query(self, search_spec, column):
     sfilter = object()
     executer = QueryExecuter(self.store)
     executer.set_filter_columns(sfilter, [column])
     executer.set_search_spec(search_spec)
     state = DateIntervalQueryState(filter=sfilter,
                                    start=self.start,
                                    end=self.end)
     return executer.search([state])
Ejemplo n.º 12
0
 def _date_query(self, search_spec, column):
     sfilter = object()
     executer = QueryExecuter(self.store)
     executer.set_filter_columns(sfilter, [column])
     executer.set_search_spec(search_spec)
     state = DateIntervalQueryState(filter=sfilter, start=self.start, end=self.end)
     return executer.search([state])
Ejemplo n.º 13
0
 def _create_search(self):
     self.search = SearchSlave(self._get_columns(self.model.kind))
     self.search.connect('result-item-activated',
                         self._on_search__item_activated)
     self.search.enable_advanced_search()
     self.query = QueryExecuter(self.app.store)
     self.search.set_query_executer(self.query)
     self.search.set_result_view(FinancialSearchResults)
     self.result_view = self.search.result_view
     self.result_view.page = self
     tree_view = self.search.result_view.get_treeview()
     tree_view.set_rules_hint(True)
     tree_view.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_BOTH)
Ejemplo n.º 14
0
    def _setup_slaves(self):
        self.search = SearchSlave(self._get_columns(),
                                  restore_name=self.__class__.__name__)
        self.attach_slave('search_group_holder', self.search)

        executer = QueryExecuter(self.store)
        executer.set_table(QuotationView)
        self.search.set_query_executer(executer)

        self.search.set_text_field_columns(['supplier_name'])
        filter = self.search.get_primary_filter()
        filter.set_label(_(u'Supplier:'))
        self.search.focus_search_entry()
        self.search.results.connect('selection-changed',
                                    self._on_searchlist__selection_changed)
        self.search.results.connect('row-activated',
                                    self._on_searchlist__row_activated)

        date_filter = DateSearchFilter(_('Date:'))
        self.search.add_filter(date_filter, columns=['open_date', 'deadline'])

        self.edit_button.set_sensitive(False)
        self.remove_button.set_sensitive(False)
Ejemplo n.º 15
0
    def _create_search(self):
        # This does the first part of the search creation,
        # this need to be done here so that self.results is set when we
        # call GladeDelegate.__init__()

        self.executer = QueryExecuter(self.store)

        # FIXME: Remove this limit, but we need to migrate all existing
        #        searches to use lazy lists first. That in turn require
        #        us to rewrite the queries in such a way that count(*)
        #        will work properly.
        self.executer.set_limit(sysparam(self.store).MAX_SEARCH_RESULTS)
        self.executer.set_table(self.search_table)

        self.search = SearchSlave(self.get_columns(),
                                  restore_name=self.__class__.__name__)
        self.search.enable_advanced_search()
        self.search.set_query_executer(self.executer)
        self.search.connect("search-completed",
                            self._on_search__search_completed)
        self.results = self.search.result_view
        search_filter = self.search.get_primary_filter()
        search_filter.set_label(self.search_label)
Ejemplo n.º 16
0
    def _setup_slaves(self):
        self.search = SearchSlave(self._get_columns(),
                                  restore_name=self.__class__.__name__)
        self.attach_slave('search_group_holder', self.search)

        executer = QueryExecuter(self.store)
        executer.set_table(QuotationView)
        self.search.set_query_executer(executer)

        self.search.set_text_field_columns(['supplier_name'])
        filter = self.search.get_primary_filter()
        filter.set_label(_(u'Supplier:'))
        self.search.focus_search_entry()
        self.search.results.connect('selection-changed',
                                    self._on_searchlist__selection_changed)
        self.search.results.connect('row-activated',
                                    self._on_searchlist__row_activated)

        date_filter = DateSearchFilter(_('Date:'))
        self.search.add_filter(date_filter, columns=['open_date', 'deadline'])

        self.edit_button.set_sensitive(False)
        self.remove_button.set_sensitive(False)
Ejemplo n.º 17
0
class QueryExecuterTest(DomainTest):
    def setUp(self):
        DomainTest.setUp(self)
        self.qe = QueryExecuter(self.store)
        self.qe.set_search_spec(ClientCategory)
        self.sfilter = mock.Mock()
        self.qe.set_filter_columns(self.sfilter, ['name'])

    def _search_string_all(self, text):
        return self.qe.search([
            StringQueryState(filter=self.sfilter,
                             mode=StringQueryState.CONTAINS_ALL,
                             text=text)])

    def _search_string_exactly(self, text):
        return self.qe.search([
            StringQueryState(filter=self.sfilter,
                             mode=StringQueryState.CONTAINS_EXACTLY,
                             text=text)])

    def _search_string_not(self, text):
        return self.qe.search([
            StringQueryState(filter=self.sfilter,
                             mode=StringQueryState.NOT_CONTAINS,
                             text=text)])

    def test_string_query(self):
        self.assertEquals(self.store.find(ClientCategory).count(), 0)
        self.create_client_category(u'EYE MOON FLARE 110 0.5')
        self.create_client_category(u'EYE MOON FLARE 120 1.0')
        self.create_client_category(u'EYE SUN FLARE 120 1.0')
        self.create_client_category(u'EYE SUN FLARE 110 1.0')
        self.create_client_category(u'EYE SUN STONE 120 0.5')

        self.assertEquals(self._search_string_all(u'eye flare 110').count(), 2)
        self.assertEquals(self._search_string_all(u'eye 0.5').count(), 2)
        self.assertEquals(self._search_string_all(u'eye 120').count(), 3)

        self.assertEquals(self._search_string_exactly(u'eye flare 110').count(), 0)
        self.assertEquals(self._search_string_exactly(u'eye 0.5').count(), 0)
        self.assertEquals(self._search_string_exactly(u'eye 120').count(), 0)

        self.assertEquals(self._search_string_not(u'stone 110').count(), 2)
        self.assertEquals(self._search_string_not(u'eye').count(), 0)
        self.assertEquals(self._search_string_not(u'moon 120').count(), 1)
Ejemplo n.º 18
0
    def get_query_executer(self):
        """
        Fetchs the QueryExecuter for the SearchContainer

        :returns: a querty executer
        :rtype: a :class:`QueryExecuter` subclass
        """
        if self._query_executer is None:
            executer = QueryExecuter(self.store)
            if not self._lazy_search:
                executer.set_limit(sysparam.get_int('MAX_SEARCH_RESULTS'))
            if self._search_spec is not None:
                executer.set_search_spec(self._search_spec)
            self._query_executer = executer
        return self._query_executer
Ejemplo n.º 19
0
    def __init__(self, entry, store, initial_value=None,
                 parent=None, run_editor=None,
                 edit_button=None, info_button=None,
                 search_clause=None):
        """
        :param entry: The entry that we should modify
        :param store: The store that will be used for database queries
        :param initial_value: Initial value for the entry
        :param parent: The parent that should be respected when running other
          dialogs
        """
        super(QueryEntryGadget, self).__init__()

        self._parent = parent
        self._on_run_editor = run_editor
        self._can_edit = False
        self._search_clause = search_clause
        self.entry = entry
        self.entry.set_mode(ENTRY_MODE_DATA)
        self.edit_button = edit_button
        self.info_button = info_button
        self.store = store

        # The filter that will be used. This is not really in the interface.
        # We will just use it to perform the search.
        self._filter = StringSearchFilter('')
        self._executer = QueryExecuter(self.store)
        self._executer.set_search_spec(self.search_spec)
        self._executer.set_filter_columns(self._filter, self.search_columns)
        self._executer.set_order_by(self.order_by)

        self._last_operation = None
        self._source_id = None

        self._setup()
        self.set_value(initial_value, force=True)
Ejemplo n.º 20
0
class PurchaseSelectionStep(BaseWizardStep):
    gladefile = 'PurchaseSelectionStep'

    def __init__(self, wizard, store):
        self._next_step = None
        BaseWizardStep.__init__(self, store, wizard)
        self.setup_slaves()

    def _refresh_next(self, validation_value):
        has_selection = self.search.results.get_selected() is not None
        self.wizard.refresh_next(has_selection)

    def _create_search(self):
        self.search = SearchSlave(self._get_columns(),
                                  restore_name=self.__class__.__name__)
        self.search.enable_advanced_search()
        self.attach_slave('searchbar_holder', self.search)
        self.executer = QueryExecuter(self.store)
        self.search.set_query_executer(self.executer)
        self.executer.set_table(PurchaseOrderView)
        self.executer.add_query_callback(self.get_extra_query)
        self._create_filters()
        self.search.results.connect('selection-changed',
                                    self._on_results__selection_changed)
        self.search.results.connect('row-activated',
                                    self._on_results__row_activated)
        self.search.focus_search_entry()

    def _create_filters(self):
        self.search.set_text_field_columns(['supplier_name'])

    def get_extra_query(self, states):
        return PurchaseOrderView.status == PurchaseOrder.ORDER_CONFIRMED

    def _get_columns(self):
        return [
            IdentifierColumn('identifier', sorted=True),
            SearchColumn('open_date',
                         title=_('Date Started'),
                         data_type=datetime.date,
                         width=100),
            SearchColumn('expected_receival_date',
                         data_type=datetime.date,
                         title=_('Expected Receival'),
                         visible=False),
            SearchColumn('supplier_name',
                         title=_('Supplier'),
                         data_type=str,
                         searchable=True,
                         width=130,
                         expand=True),
            SearchColumn('ordered_quantity',
                         title=_('Qty Ordered'),
                         data_type=Decimal,
                         width=110,
                         format_func=format_quantity),
            SearchColumn('received_quantity',
                         title=_('Qty Received'),
                         data_type=Decimal,
                         width=145,
                         format_func=format_quantity),
            SearchColumn('total',
                         title=_('Order Total'),
                         data_type=currency,
                         width=120)
        ]

    def _update_view(self):
        has_selection = self.search.results.get_selected() is not None
        self.details_button.set_sensitive(has_selection)

    #
    # WizardStep hooks
    #

    def post_init(self):
        self._update_view()
        self.register_validate_function(self._refresh_next)
        self.force_validation()

    def next_step(self):
        self.search.save_columns()
        selected = self.search.results.get_selected()
        purchase = selected.purchase

        # We cannot create the model in the wizard since we haven't
        # selected a PurchaseOrder yet which ReceivingOrder depends on
        # Create the order here since this is the first place where we
        # actually have a purchase selected
        if not self.wizard.model:
            self.wizard.model = self.model = ReceivingOrder(
                responsible=api.get_current_user(self.store),
                supplier=purchase.supplier,
                invoice_number=None,
                branch=purchase.branch,
                purchase=purchase,
                store=self.store)

        # Remove all the items added previously, used if we hit back
        # at any point in the wizard.
        if self.model.purchase != purchase:
            self.model.remove_items()
            # This forces ReceivingOrderProductStep to create a new model
            self._next_step = None

        if selected:
            self.model.purchase = purchase
            self.model.branch = purchase.branch
            self.model.supplier = purchase.supplier
            self.model.transporter = purchase.transporter
        else:
            self.model.purchase = None

        # FIXME: Improve the infrastructure to avoid this local caching of
        #        Wizard steps.
        if not self._next_step:
            # Remove all the items added previously, used if we hit back
            # at any point in the wizard.
            self._next_step = ReceivingOrderItemStep(self.store, self.wizard,
                                                     self.model, self)
        return self._next_step

    def has_previous_step(self):
        return False

    def setup_slaves(self):
        self._create_search()

    #
    # Kiwi callbacks
    #


#     def on_searchbar_activate(self, slave, objs):
#         """Use this callback with SearchBar search-activate signal"""
#         self.results.add_list(objs, clear=True)
#         has_selection = self.results.get_selected() is not None
#         self.wizard.refresh_next(has_selection)

    def _on_results__selection_changed(self, results, purchase_order_view):
        self.force_validation()
        self._update_view()

    def _on_results__row_activated(self, results, purchase_order_view):
        run_dialog(PurchaseDetailsDialog,
                   self.wizard,
                   self.store,
                   model=purchase_order_view.purchase)

    def on_details_button__clicked(self, *args):
        selected = self.search.results.get_selected()
        if not selected:
            raise ValueError('You should have one order selected '
                             'at this point, got nothing')
        run_dialog(PurchaseDetailsDialog,
                   self.wizard,
                   self.store,
                   model=selected.purchase)
Ejemplo n.º 21
0
 def _date_filter_query(self, search_table, column):
     executer = QueryExecuter(self.store)
     executer.set_filter_columns(self.date_filter, [column])
     executer.set_table(search_table)
     return executer.search([self.date_filter.get_state()])
Ejemplo n.º 22
0
 def setUp(self):
     DomainTest.setUp(self)
     self.qe = QueryExecuter(self.store)
     self.qe.set_search_spec(ClientCategory)
     self.sfilter = mock.Mock()
     self.qe.set_filter_columns(self.sfilter, ['name'])
Ejemplo n.º 23
0
class TransactionPage(object):
    # shows either a list of:
    #   - transactions
    #   - payments
    def __init__(self, model, app, parent):
        self.model = model
        self.app = app
        self.parent_window = parent
        self._block = False

        self._create_search()
        self._add_date_filter()

        self._setup_search()
        self.refresh()

    def get_toplevel(self):
        return self.parent_window

    def _create_search(self):
        self.search = SearchSlave(self._get_columns(self.model.kind))
        self.search.connect('result-item-activated',
                            self._on_search__item_activated)
        self.search.enable_advanced_search()
        self.query = QueryExecuter(self.app.store)
        self.search.set_query_executer(self.query)
        self.search.set_result_view(FinancialSearchResults)
        self.result_view = self.search.result_view
        self.result_view.page = self
        tree_view = self.search.result_view.get_treeview()
        tree_view.set_rules_hint(True)
        tree_view.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_BOTH)

    def _add_date_filter(self):
        self.date_filter = DateSearchFilter(_('Date:'))
        self.date_filter.clear_options()
        self.date_filter.add_option(Any, 0)
        year = datetime.datetime.today().year
        month_names = get_month_names()
        for i, month in enumerate(month_names):
            name = month_names[i]
            option = type(name + 'Option', (MonthOption, ), {
                'name': _(name),
                'month': i + 1,
                'year': year
            })
            self.date_filter.add_option(option, i + 1)
        self.date_filter.add_custom_options()
        self.date_filter.mode.select_item_by_position(0)
        self.search.add_filter(self.date_filter)

    def _append_date_query(self, field):
        date = self.date_filter.get_state()
        queries = []
        if isinstance(date, DateQueryState) and date.date is not None:
            queries.append(Date(field) == date.date)
        elif isinstance(date, DateIntervalQueryState):
            queries.append(Date(field) >= date.start)
            queries.append(Date(field) <= date.end)
        return queries

    def _payment_query(self, store):
        queries = self._append_date_query(self.query.table.due_date)
        if queries:
            return store.find(self.query.table, And(*queries))

        return store.find(self.query.table)

    def _transaction_query(self, store):
        queries = [
            Or(self.model.id == AccountTransaction.account_id,
               self.model.id == AccountTransaction.source_account_id)
        ]

        queries.extend(self._append_date_query(AccountTransaction.date))
        return store.find(AccountTransactionView, And(*queries))

    def show(self):
        self.search.show()

    def _setup_search(self):
        if self.model.kind == 'account':
            self.query.set_table(AccountTransactionView)
            self.search.set_text_field_columns(['description'])
            self.query.set_query(self._transaction_query)
        elif self.model.kind == 'payable':
            self.search.set_text_field_columns(
                ['description', 'supplier_name'])
            self.query.set_table(OutPaymentView)
            self.query.set_query(self._payment_query)
        elif self.model.kind == 'receivable':
            self.search.set_text_field_columns(['description', 'drawee'])
            self.query.set_table(InPaymentView)
            self.query.set_query(self._payment_query)
        else:
            raise TypeError("unknown model kind: %r" % (self.model.kind, ))

    def refresh(self):
        self.search.result_view.clear()
        if self.model.kind == 'account':
            transactions = AccountTransactionView.get_for_account(
                self.model, self.app.store)
            self.append_transactions(transactions)
        elif self.model.kind == 'payable':
            self._populate_payable_payments(OutPaymentView)
        elif self.model.kind == 'receivable':
            self._populate_payable_payments(InPaymentView)
        else:
            raise TypeError("unknown model kind: %r" % (self.model.kind, ))

    def _get_columns(self, kind):
        if kind in ['payable', 'receivable']:
            return self._get_payment_columns()
        else:
            return self._get_account_columns()

    def _get_account_columns(self):
        def format_withdrawal(value):
            if value < 0:
                return currency(abs(value)).format(symbol=True, precision=2)

        def format_deposit(value):
            if value > 0:
                return currency(value).format(symbol=True, precision=2)

        if self.model.account_type == Account.TYPE_INCOME:
            color_func = lambda x: False
        else:
            color_func = lambda x: x < 0
        return [
            Column('date',
                   title=_("Date"),
                   data_type=datetime.date,
                   sorted=True),
            Column('code', title=_("Code"), data_type=unicode),
            Column('description',
                   title=_("Description"),
                   data_type=unicode,
                   expand=True),
            Column('account', title=_("Account"), data_type=unicode),
            Column('value',
                   title=self.model.account.get_type_label(out=False),
                   data_type=currency,
                   format_func=format_deposit),
            Column('value',
                   title=self.model.account.get_type_label(out=True),
                   data_type=currency,
                   format_func=format_withdrawal),
            ColoredColumn('total',
                          title=_("Total"),
                          data_type=currency,
                          color='red',
                          data_func=color_func)
        ]

    def _get_payment_columns(self):
        return [
            SearchColumn('due_date',
                         title=_("Due date"),
                         data_type=datetime.date,
                         sorted=True),
            IdentifierColumn('identifier', title=_("Code")),
            SearchColumn('description',
                         title=_("Description"),
                         data_type=unicode,
                         expand=True),
            SearchColumn('value', title=_("Value"), data_type=currency)
        ]

    def append_transactions(self, transactions):
        for transaction in transactions:
            description = transaction.get_account_description(self.model)
            value = transaction.get_value(self.model)
            self._add_transaction(transaction, description, value)
        self.update_totals()

    def _populate_payable_payments(self, view_class):
        for view in self.app.store.find(view_class):
            self.search.result_view.append(view)

    def _add_transaction(self, transaction, description, value):
        item = Settable(transaction=transaction)
        self._update_transaction(item, transaction, description, value)
        self.search.result_view.append(item)
        return item

    def _update_transaction(self, item, transaction, description, value):
        item.account = description
        item.date = transaction.date
        item.description = transaction.description
        item.value = value
        item.code = transaction.code

    def update_totals(self):
        total = decimal.Decimal('0')
        for item in self.search.result_view:
            total += item.value
            item.total = total

    def _edit_transaction_dialog(self, item):
        store = api.new_store()
        if isinstance(item.transaction, AccountTransactionView):
            account_transaction = store.fetch(item.transaction.transaction)
        else:
            account_transaction = store.fetch(item.transaction)
        model = getattr(self.model, 'account', self.model)

        transaction = run_dialog(AccountTransactionEditor, self.app, store,
                                 account_transaction, model)

        if transaction:
            store.flush()
            self._update_transaction(item, transaction,
                                     transaction.edited_account.description,
                                     transaction.value)
            self.update_totals()
            self.search.result_view.update(item)
            self.app.accounts.refresh_accounts(self.app.store)
        store.confirm(transaction)
        store.close()

    def on_dialog__opened(self, dialog):
        dialog.connect('account-added', self.on_dialog__account_added)

    def on_dialog__account_added(self, dialog):
        self.app.accounts.refresh_accounts(self.app.store)

    def add_transaction_dialog(self):
        store = api.new_store()
        model = getattr(self.model, 'account', self.model)
        model = store.fetch(model)

        transaction = run_dialog(AccountTransactionEditor, self.app, store,
                                 None, model)
        if transaction:
            transaction.sync()
            value = transaction.value
            other = transaction.get_other_account(model)
            if other == model:
                value = -value
            item = self._add_transaction(transaction, other.description, value)
            self.update_totals()
            self.search.result_view.update(item)
            self.app.accounts.refresh_accounts(self.app.store)
        store.confirm(transaction)
        store.close()

    def _on_search__item_activated(self, objectlist, item):
        if self.model.kind == 'account':
            self._edit_transaction_dialog(item)
Ejemplo n.º 24
0
class QueryEntryGadget(object):
    """This gadget modifies a ProxyEntry to behave like a ProxyComboEntry.

    When instanciated, the gadget will remove the entry from the editor, add
    a Gtk.HBox on its place, and re-attach the entry to the newly created
    hbox. This hbox will also have a button to add/edit a new object.

    There are a few advantages in using this instead of a combo:

    - There is no need to prefill the combo with all the options, which can
      be very slow depending on the number of objects.

    - This allows the user to use a better search mechanism, allowing him to
      filter using multiple keywords and even candidade keys (like a client
      document)
    """

    MIN_KEY_LENGTH = 1
    LOADING_ITEMS_TEXT = _("Loading items...")
    NEW_ITEM_TEXT = _("Create a new item with that name")
    NEW_ITEM_TOOLTIP = _("Create a new item")
    EDIT_ITEM_TOOLTIP = _("Edit the selected item")
    INFO_ITEM_TOOLTIP = _("See info about the selected item")
    NO_ITEMS_FOUND_TEXT = _("No items found")
    advanced_search = True
    selection_only = False
    item_editor = None
    item_info_dialog = ClientEditor
    search_class = None
    search_spec = None
    search_columns = None

    def __init__(self, entry, store, initial_value=None,
                 parent=None, run_editor=None,
                 edit_button=None, info_button=None,
                 search_clause=None):
        """
        :param entry: The entry that we should modify
        :param store: The store that will be used for database queries
        :param initial_value: Initial value for the entry
        :param parent: The parent that should be respected when running other
          dialogs
        """
        super(QueryEntryGadget, self).__init__()

        self._parent = parent
        self._on_run_editor = run_editor
        self._can_edit = False
        self._search_clause = search_clause
        self.entry = entry
        self.entry.set_mode(ENTRY_MODE_DATA)
        self.edit_button = edit_button
        self.info_button = info_button
        self.store = store

        # The filter that will be used. This is not really in the interface.
        # We will just use it to perform the search.
        self._filter = StringSearchFilter('')
        self._executer = QueryExecuter(self.store)
        self._executer.set_search_spec(self.search_spec)
        self._executer.set_filter_columns(self._filter, self.search_columns)

        self._last_operation = None
        self._source_id = None

        self._setup()
        self.set_value(initial_value, force=True)

    #
    #  Public API
    #

    def set_value(self, obj, force=False):
        if not force and obj == self._current_obj:
            return

        obj = self.store.fetch(obj)
        if obj is not None:
            if hasattr(obj, 'description'):
                value = obj.description
            else:
                value = obj.get_description()
            self.entry.prefill([(value, obj)])
            self.update_edit_button(Gtk.STOCK_EDIT, self.EDIT_ITEM_TOOLTIP)
        else:
            value = ''
            self.entry.prefill([])
            self.update_edit_button(Gtk.STOCK_NEW, self.NEW_ITEM_TOOLTIP)

        self._current_obj = obj
        self.entry.update(obj)
        self.entry.set_text(value)

        self._update_widgets()

    def set_editable(self, can_edit):
        self.entry.set_property('editable', can_edit)
        self._update_widgets()

    def update_edit_button(self, stock, tooltip=None):
        image = Gtk.Image.new_from_stock(stock, Gtk.IconSize.MENU)
        self.edit_button.set_image(image)
        if tooltip is not None:
            self.edit_button.set_tooltip_text(tooltip)

    def get_object_from_item(self, item):
        return item

    def describe_item(self, item):
        raise NotImplementedError

    #
    #  Private
    #

    def _setup(self):
        if not self.selection_only:
            if self.edit_button is None or self.info_button is None:
                self._replace_widget()

        if self.edit_button is None:
            self.edit_button = self._add_button(Gtk.STOCK_NEW)
        self.edit_button.connect('clicked', self._on_edit_button__clicked)

        if self.info_button is None:
            self.info_button = self._add_button(Gtk.STOCK_INFO)
        self.info_button.connect('clicked', self._on_info_button__clicked)

        self.entry.connect('activate', self._on_entry__activate)
        self.entry.connect('changed', self._on_entry__changed)
        self.entry.connect('notify::sensitive', self._on_entry_sensitive)
        self.entry.connect('key-press-event', self._on_entry__key_press_event)

        self._popup = _QueryEntryPopup(
            self, has_new_item=not self.selection_only)
        self._popup.connect('item-selected', self._on_popup__item_selected)
        self._popup.connect('create-item', self._on_popup__create_item)

    def _update_widgets(self):
        self._can_edit = self.entry.get_editable() and self.entry.get_sensitive()
        if self.edit_button is not None:
            self.edit_button.set_sensitive(bool(self._can_edit or self._current_obj))
        if self.info_button is not None:
            self.info_button.set_sensitive(bool(self._current_obj))

    def _add_button(self, stock):
        image = Gtk.Image.new_from_stock(stock, Gtk.IconSize.MENU)
        button = Gtk.Button()
        button.set_relief(Gtk.ReliefStyle.NONE)
        button.set_image(image)
        button.show()
        self.box.pack_start(button, False, False, 0)
        return button

    def _replace_widget(self):
        # This will remove the entry, add a hbox in the entry old position, and
        # reattach the entry to this box. The box will then be used to add two
        # new buttons (one for searching, other for editing/adding new objects
        container = self.entry.props.parent

        # stolen from gazpacho code (widgets/base/base.py):
        props = {}
        for pspec in container.__class__.list_child_properties():
            props[pspec.name] = container.child_get_property(self.entry, pspec.name)

        self.box = Gtk.HBox()
        self.box.show()
        self.entry.reparent(self.box)
        container.add(self.box)

        for name, value in props.items():
            container.child_set_property(self.box, name, value)

    def _find_items(self, text):
        self._filter.set_state(text)
        state = self._filter.get_state()
        resultset = self._executer.search([state])
        if self._search_clause:
            resultset = resultset.find(self._search_clause)
        return self._executer.search_async(resultset=resultset, limit=10)

    def _dispatch(self, value):
        self._source_id = None
        if self._last_operation is not None:
            self._last_operation.cancel()
        self._last_operation = self._find_items(value)
        self._last_operation.connect(
            'finish', lambda o: self._popup.add_items(o.get_result()))

    def _run_search(self):
        if not self.advanced_search:
            return

        text = self.entry.get_text()
        item = run_dialog(self.search_class, self._parent, self.store,
                          double_click_confirm=True, initial_string=text)
        if item:
            self.set_value(self.get_object_from_item(item))

    def _run_editor(self, model=None, description=None):
        with api.new_store() as store:
            model = store.fetch(model)
            if self._on_run_editor is not None:
                retval = self._on_run_editor(store, model,
                                             description=description,
                                             visual_mode=not self._can_edit)
            else:
                if issubclass(self.item_editor, BasePersonRoleEditor):
                    rd = run_person_role_dialog
                else:
                    rd = run_dialog
                retval = rd(self.item_editor, self._parent, store, model,
                            description=description,
                            visual_mode=not self._can_edit)

        if store.committed:
            return self.store.fetch(retval)

    #
    #  Callbacks
    #

    def _on_entry__key_press_event(self, window, event):
        keyval = event.keyval
        if keyval == Gdk.KEY_Up or keyval == Gdk.KEY_KP_Up:
            self._popup.scroll(relative=-1)
            return True
        elif keyval == Gdk.KEY_Down or keyval == Gdk.KEY_KP_Down:
            self._popup.scroll(relative=+1)
            return True
        elif keyval == Gdk.KEY_Page_Up:
            self._popup.scroll(relative=-14)
            return True
        elif keyval == Gdk.KEY_Page_Down:
            self._popup.scroll(relative=+14)
            return True
        elif keyval == Gdk.KEY_Escape:
            self._popup.popdown()
            return True

        return False

    def _on_entry__changed(self, entry):
        value = unicode(entry.get_text())
        self.set_value(None)
        if len(value) >= self.MIN_KEY_LENGTH:
            if self._source_id is not None:
                GLib.source_remove(self._source_id)
            self._source_id = GLib.timeout_add(150, self._dispatch, value)
            if not self._popup.visible:
                self._popup.popup()
            self._popup.set_loading(True)
        elif self._popup.visible:
            # In this case, the user has deleted text to less than the
            # min key length, so pop it down
            if self._source_id is not None:
                GLib.source_remove(self._source_id)
                self._source_id = None
            self._popup.popdown()

    def _on_entry__activate(self, entry):
        if self._popup.visible:
            self._popup.popdown()
            self._popup.confirm()
        else:
            self._run_search()

    def _on_entry_sensitive(self, entry, pspec):
        self._update_widgets()

    def _on_popup__item_selected(self, popup, item, fallback_to_search):
        self.set_value(self.get_object_from_item(item))
        popup.popdown()
        self.entry.grab_focus()
        GLib.idle_add(self.entry.select_region, len(self.entry.get_text()), -1)

        if item is None and fallback_to_search:
            self._run_search()

    def _on_popup__create_item(self, popup):
        obj = self._run_editor(description=unicode(self.entry.get_text()))
        self.set_value(obj)

    def _on_edit_button__clicked(self, entry):
        current_obj = self.entry.read()
        obj = self._run_editor(current_obj)
        if obj:
            self.set_value(obj, force=True)

    def _on_info_button__clicked(self, entry):
        obj = self.entry.read()
        with api.new_store() as store:
            run_dialog(self.item_info_dialog, self._parent,
                       store, store.fetch(obj))
Ejemplo n.º 25
0
class SearchDialog(BasicDialog):
    """  Base class for *all* the search dialogs, responsible for the list
    construction and "Filter" and "Clear" buttons management.

    This class must be subclassed and its subclass *must* implement the methods
    'get_columns' and 'get_query_and_args' (if desired, 'get_query_and_args'
    can be implemented in the user's slave class, so SearchDialog will get its
    slave instance and call the method directly). Its subclass also must
    implement a setup_slaves method and call its equivalent base class method
    as in:

    >>> def setup_slave(self):
    ...    SearchDialog.setup_slaves(self)

    or then, call it in its constructor, like:

    >>> def __init__(self, *args):
    ...     SearchDialog.__init__(self)
    """
    main_label_text = ''

    #: Title that will appear in the window, for instance 'Product Search'
    title = ''

    # The table type which we will query on to get the objects.
    search_table = None

    #: The label that will be used for the main filter in this dialog
    search_label = None

    #: Selection mode to use (if its possible to select more than one row)
    selection_mode = gtk.SELECTION_BROWSE

    #: Default size for this dialog
    size = ()

    #: If the advanced search is enabled or disabled. When ``True`` we will
    #: instrospect the columns returned by :meth:`get_columns`,and use those
    #: that are subclasses of :class:`stoqlib.gui.columns.SearchColumn` to add
    #: as options for the user to filter the results.
    advanced_search = True

    tree = False

    def __init__(self, store, search_table=None, hide_footer=True,
                 title='', selection_mode=None, double_click_confirm=False):
        """
        A base class for search dialog inheritance

        :param store: a store
        :param table:
        :param search_table:
        :param hide_footer:
        :param title:
        :param selection_mode:
        :param double_click_confirm: If double click a item in the list should
          automatically confirm
        """

        self.store = store
        self.search_table = search_table or self.search_table
        if not self.search_table:
            raise ValueError("%r needs a search table" % self)
        self.selection_mode = self._setup_selection_mode(selection_mode)
        self.summary_label = None
        self.double_click_confirm = double_click_confirm

        BasicDialog.__init__(self, hide_footer=hide_footer,
                             main_label_text=self.main_label_text,
                             title=title or self.title,
                             size=self.size)

        self.executer = QueryExecuter(store)
        # FIXME: Remove this limit, but we need to migrate all existing
        #        searches to use lazy lists first. That in turn require
        #        us to rewrite the queries in such a way that count(*)
        #        will work properly.
        self.executer.set_limit(sysparam(self.store).MAX_SEARCH_RESULTS)
        self.set_table(self.search_table)

        self.enable_window_controls()
        self.disable_ok()
        self.set_ok_label(_('Se_lect Items'))
        self._setup_search()
        self._setup_details_slave()

        self.create_filters()
        self.setup_widgets()
        if self.search_label:
            self.set_searchbar_label(self.search_label)

    def _setup_selection_mode(self, selection_mode):
        # For consistency do not allow none or single, in other words,
        # only allowed values are browse and multiple so we always will
        # be able to use both the keyboard and the mouse to select items
        # in the search list.
        selection_mode = selection_mode or self.selection_mode
        if (selection_mode != gtk.SELECTION_BROWSE and
            selection_mode != gtk.SELECTION_MULTIPLE):
            raise ValueError('Invalid selection mode %r' % selection_mode)
        return selection_mode

    def _setup_search(self):
        self.search = SearchSlave(
            self.get_columns(),
            tree=self.tree,
            restore_name=self.__class__.__name__,
        )
        self.search.set_query_executer(self.executer)
        if self.advanced_search:
            self.search.enable_advanced_search()
        self.attach_slave('main', self.search)
        self.header.hide()

        self.results = self.search.result_view
        self.results.set_selection_mode(self.selection_mode)
        self.results.connect('cell-edited', self._on_results__cell_edited)
        self.results.connect('selection-changed',
                             self._on_results__selection_changed)
        self.results.connect('row-activated', self._on_results__row_activated)

    def _setup_details_slave(self):
        # FIXME: Gross hack
        has_details_btn = hasattr(self, 'on_details_button_clicked')
        has_print_btn = hasattr(self, 'on_print_button_clicked')
        if not (has_details_btn or has_print_btn):
            self._details_slave = None
            return
        self._details_slave = _SearchDialogDetailsSlave()
        self.attach_slave('details_holder', self._details_slave)
        if has_details_btn:
            self._details_slave.connect("details",
                                        self.on_details_button_clicked)
        else:
            self._details_slave.details_button.hide()
        if has_print_btn:
            self._details_slave.connect("print", self.on_print_button_clicked)
            self.set_print_button_sensitive(False)
            self.results.connect('has-rows', self._has_rows)
        else:
            self._details_slave.print_button.hide()

    #
    # Public API
    #

    def add_button(self, label, stock=None, image=None):
        """Adds a button in the bottom of the dialog.

        :param label: the text that will be displayed by the button.
        :param stock: the gtk stock id to be used in the button.
        :param image: the image filename.
        """
        button = gtk.Button(label=label)
        if image:
            image_widget = gtk.Image()
            image_widget.set_from_file(
                environ.find_resource('pixmaps', image))
            image_widget.show()
            button.set_image(image_widget)
        elif stock:
            button_set_image_with_label(button, stock, label)
        self.action_area.set_layout(gtk.BUTTONBOX_START)
        self.action_area.pack_start(button, False, False, 6)
        return button

    def set_details_button_sensitive(self, value):
        self._details_slave.details_button.set_sensitive(value)

    def set_print_button_sensitive(self, value):
        self._details_slave.print_button.set_sensitive(value)

    def get_selection(self):
        mode = self.results.get_selection_mode()
        if mode == gtk.SELECTION_BROWSE:
            return self.results.get_selected()
        return self.results.get_selected_rows()

    def confirm(self, retval=None):
        """Confirms the dialog
        :param retval: optional parameter which will be selected when the
          dialog is closed
        """
        if retval is None:
            retval = self.get_selection()
        self.retval = retval
        self.search.save_columns()
        # FIXME: This should chain up so the "confirm" signal gets emitted
        self.close()

    def cancel(self, *args):
        self.retval = []
        self.search.save_columns()
        # FIXME: This should chain up so the "cancel" signal gets emitted
        self.close()

    def set_table(self, table):
        self.executer.set_table(table)
        self.search_table = table

    # FIXME: -> remove/use

    # TODO: Check if we can remove
    def set_searchbar_label(self, label):
        search_filter = self.search.get_primary_filter()
        search_filter.set_label(label)

    def set_searchbar_search_string(self, string):
        if string == self.get_searchbar_search_string():
            return
        search_filter = self.search.get_primary_filter()
        search_filter.entry.set_text(string)

    def get_searchbar_search_string(self):
        search_filter = self.search.get_primary_filter()
        return search_filter.get_state().text

    def set_text_field_columns(self, columns):
        """See :class:`SearchSlave.set_text_field_columns`
        """
        self.search.set_text_field_columns(columns)

    def disable_search_entry(self):
        """See :class:`SearchSlave.disable_search_entry`
        """
        self.search.disable_search_entry()

    def add_filter(self, search_filter, position=SearchFilterPosition.BOTTOM,
                   columns=None, callback=None):
        """See :class:`SearchSlave.add_filter`
        """
        self.search.add_filter(search_filter, position, columns, callback)

    def row_activate(self, obj):
        """This is called when an item in the results list is double clicked.

        :param obj: the item that was double clicked.
        """
        if self.double_click_confirm:
            # But only if its also confirmable with ok_button
            if self.ok_button.props.sensitive:
                self.confirm()

    #
    # Filters
    #

    def create_branch_filter(self, label=None):
        from stoqlib.domain.person import Branch
        branches = Branch.get_active_branches(self.store)
        items = [(b.person.name, b.id) for b in branches]
        # if not items:
        #    raise ValueError('You should have at least one branch at '
        #                      'this point')
        items.insert(0, (_("Any"), None))

        if not label:
            label = _('Branch:')
        branch_filter = ComboSearchFilter(label, items)
        current = api.get_current_branch(self.store)
        if current:
            branch_filter.select(current.id)

        return branch_filter

    def create_payment_filter(self, label=None):
        from stoqlib.domain.payment.method import PaymentMethod
        methods = PaymentMethod.get_active_methods(self.store)
        items = [(_('Any'), None)]
        for method in methods:
            if method.method_name == 'multiple':
                continue
            items.append((method.description, method))

        if not label:
            label = _('Method:')
        payment_filter = ComboSearchFilter(label, items)
        payment_filter.select(None)

        return payment_filter

    def create_provider_filter(self, label=None):
        from stoqlib.domain.payment.card import CreditProvider
        providers = CreditProvider.get_card_providers(self.store)
        items = [(p.short_name, p) for p in providers]
        items.insert(0, (_("Any"), None))

        if not label:
            label = _('Provider:')
        provider_filter = ComboSearchFilter(label, items)

        return provider_filter

    #
    # Callbacks
    #

    def on_search__search_completed(self, search, results, states):
        self.search_completed(results, states)

    def _on_results__cell_edited(self, results, obj, attr):
        """Override this method on child when it's needed to perform some
        tasks when editing a row.
        """

    def _on_results__selection_changed(self, results, selected):
        self.update_widgets()
        if selected:
            self.enable_ok()
        else:
            self.disable_ok()

    def _on_results__row_activated(self, results, obj):
        self.row_activate(obj)

    def _has_rows(self, results, obj):
        self.set_print_button_sensitive(obj)

    #
    # Hooks
    #

    def create_filters(self):
        raise NotImplementedError(
            "create_filters() must be implemented in %r" % self)

    def setup_widgets(self):
        pass

    def get_columns(self):
        raise NotImplementedError(
            "get_columns() must be implemented in %r" % self)

    def update_widgets(self):
        """Subclass can have an 'update_widgets', and this method will be
        called when a signal is emitted by 'Filter' or 'Clear' buttons and
        also when a list item is selected. """

    def search_completed(self, results, states):
        pass
Ejemplo n.º 26
0
class SearchEntryGadget(object):
    find_tooltip = _('Search')
    edit_tooltip = _('Edit')
    new_tooltip = _('Create')

    def __init__(self,
                 entry,
                 store,
                 model,
                 model_property,
                 search_columns,
                 search_class,
                 parent,
                 run_editor=None):
        """
        This gadget modifies a ProxyEntry turning it into a replacement for
        ProxyComboEntry.

        When instanciated, the gadget will remove the entry from the editor, add
        a gtk.HBox on its place, and re-attach the entry to the newly created
        hbox. This hbox will also have two buttons: One for showing the related
        search dialog (or search editor), and another one to add/edit a new
        object.

        There are a few advantages in using this instead of a combo:

        - There is no need to prefill the combo with all the options, which can
          be very slow depending on the number of objects.
        - This allows the user to use a better search mechanism, allowing him to
          filter using multiple keywords and even candidade keys (like a client
          document)

        :param entry: The entry that we should modify
        :param store: The store that will be used for database queries
        :param model: The model that we are updating
        :param model_property: Property name of the model that should be updated
        :param search_columns: Columns that will be queried when the user
          activates the entry
        :param search_class: Class of the search editor/dialog that will be
          displayed when more than one object is found
        :param parent: The parent that should be respected when running other
          dialogs
        :param find_tooltip: the tooltip to use for the search button
        :param edit_tooltip: the tooltip to use for the edit button
        :param new_tooltip: the tooltip to use for the new button
        """
        self.store = store
        self._entry = entry
        self._model = model
        # TODO: Maybe this two variables shoulb be a list of properties of the
        # table instead of strings
        self._model_property = model_property
        self._search_columns = search_columns
        self._search_class = search_class
        self._parent = parent
        self._on_run_editor = run_editor

        # TODO: Respect permission manager
        self._editor_class = search_class.editor_class

        # If the search is for a person, the editor is called with a special
        # function
        if issubclass(search_class, BasePersonSearch):
            self._is_person = True
        else:
            self._is_person = False

        self._setup_widgets()
        self._setup_callbacks()

    #
    #   Private API
    #

    def _setup_widgets(self):
        self._replace_widget()

        # Add the two buttons
        self.find_button = self._create_button(gtk.STOCK_FIND)
        self.edit_button = self._create_button(gtk.STOCK_NEW)
        can_edit = self._entry.get_editable() and self._entry.get_sensitive()
        self.find_button.set_sensitive(can_edit)

        self.find_button.set_tooltip_text(self.find_tooltip)
        self.edit_button.set_tooltip_text(self.new_tooltip)

        # the entry needs a completion to work in MODE_DATA
        self._completion = gtk.EntryCompletion()
        self._entry.set_completion(self._completion)
        self._entry.set_mode(ENTRY_MODE_DATA)

        initial_value = getattr(self._model, self._model_property)
        self.set_value(initial_value)

        # The filter that will be used. This is not really in the interface. We
        # will just use it to perform the search.
        self._filter = StringSearchFilter('')
        self._executer = QueryExecuter(self.store)
        self._executer.set_search_spec(self._search_class.search_spec)
        self._executer.set_filter_columns(self._filter, self._search_columns)

    def _create_button(self, stock):
        image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_MENU)
        button = gtk.Button()
        button.set_relief(gtk.RELIEF_NONE)
        button.set_image(image)
        button.show()
        self.box.pack_start(button, False, False)
        return button

    def _replace_widget(self):
        # This will remove the entry, add a hbox in the entry old position, and
        # reattach the entry to this box. The box will then be used to add two
        # new buttons (one for searching, other for editing/adding new objects
        container = self._entry.parent

        # stolen from gazpacho code (widgets/base/base.py):
        props = {}
        for pspec in gtk.container_class_list_child_properties(container):
            props[pspec.name] = container.child_get_property(
                self._entry, pspec.name)

        self.box = gtk.HBox()
        self.box.show()
        self._entry.reparent(self.box)
        container.add(self.box)

        for name, value in props.items():
            container.child_set_property(self.box, name, value)

    def _setup_callbacks(self):
        self._entry.connect('activate', self._on_entry_activate)
        self._entry.connect('changed', self._on_entry_changed)
        self._entry.connect('notify::sensitive', self._on_entry_sensitive)
        self.find_button.connect('clicked', self._on_find_button__clicked)
        self.edit_button.connect('clicked', self._on_edit_button__clicked)

    def _run_search(self):
        text = self._entry.get_text()
        value = run_dialog(self._search_class,
                           self._parent,
                           self.store,
                           double_click_confirm=True,
                           initial_string=text)
        if value:
            self.set_value(self.get_model_obj(value))

    def _run_editor(self):
        with api.new_store() as store:
            model = getattr(self._model, self._model_property)
            model = store.fetch(model)
            if self._on_run_editor:
                value = self._on_run_editor(store, model)
            elif self._is_person:
                value = run_person_role_dialog(self._editor_class,
                                               self._parent, store, model)
            else:
                value = run_dialog(self._editor_class, self._parent, store,
                                   model)

        if value:
            value = self.store.fetch(self.get_model_obj(value))
            self.set_value(value)

    #
    #   Public API
    #

    def set_value(self, obj):
        if obj:
            display_value = obj.get_description()
            self._entry.prefill([(display_value, obj)])
            self.update_edit_button(gtk.STOCK_INFO, self.edit_tooltip)
        else:
            display_value = ''
            self._entry.prefill([])
            self.update_edit_button(gtk.STOCK_NEW, self.new_tooltip)

        self._value = obj
        self._entry.update(obj)
        self._entry.set_text(display_value)

    def get_model_obj(self, obj):
        return obj

    def update_edit_button(self, stock, tooltip):
        image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_MENU)
        self.edit_button.set_image(image)
        self.edit_button.set_tooltip_text(tooltip)

    #
    #   Callbacks
    #

    def _on_entry_activate(self, entry):
        if not self._entry.get_property('editable'):
            return
        text = entry.get_text()
        self._filter.set_state(text)
        state = self._filter.get_state()
        results = list(self._executer.search([state])[:2])
        if len(results) != 1:
            # XXX: If nothing is found in the query above, runing the search
            # will cause the query to be executed a second time. Refactor the
            # search to allow us to send the initial results avoiding this
            # second query.
            return self._run_search()

        # This means the search returned only one result.
        self.set_value(self.get_model_obj(results[0]))

    def _on_entry_changed(self, entry):
        # If the user edits the value in the entry, it invalidates the value.
        if self._value:
            self.set_value(None)

    def _on_entry_sensitive(self, entry, pspec):
        can_edit = self._entry.get_editable() and self._entry.get_sensitive()
        self.find_button.set_sensitive(can_edit)
        self.edit_button.set_sensitive(can_edit)

    def _on_edit_button__clicked(self, entry):
        self._run_editor()

    def _on_find_button__clicked(self, entry):
        self._run_search()
Ejemplo n.º 27
0
class LoanSelectionStep(BaseWizardStep):
    gladefile = 'HolderTemplate'

    def __init__(self, wizard, store):
        BaseWizardStep.__init__(self, store, wizard)
        self.setup_slaves()

    def _create_filters(self):
        self.search.set_text_field_columns(['client_name'])

    def _get_columns(self):
        return [IdentifierColumn('identifier', sorted=True),
                SearchColumn('responsible_name', title=_(u'Responsible'),
                             data_type=str, expand=True),
                SearchColumn('client_name', title=_(u'Client'),
                             data_type=str, expand=True),
                SearchColumn('open_date', title=_(u'Opened'),
                             data_type=datetime.date),
                SearchColumn('expire_date', title=_(u'Expire'),
                             data_type=datetime.date),
                Column('loaned', title=_(u'Loaned'),
                       data_type=Decimal),
                ]

    def _refresh_next(self, value=None):
        can_continue = False
        selected_rows = self.search.results.get_selected_rows()
        if selected_rows:
            client = selected_rows[0].client_id
            branch = selected_rows[0].branch_id
            # Only loans that belong to the same client and are from the same
            # branch can be closed together
            can_continue = all(v.client_id == client and v.branch_id == branch
                               for v in selected_rows)
        self.wizard.refresh_next(can_continue)

    def get_extra_query(self, states):
        return LoanView.status == Loan.STATUS_OPEN

    def setup_slaves(self):
        self.search = SearchSlave(self._get_columns(),
                                  restore_name=self.__class__.__name__)
        self.search.enable_advanced_search()
        self.attach_slave('place_holder', self.search)
        self.executer = QueryExecuter(self.store)
        self.search.set_query_executer(self.executer)
        self.executer.set_table(LoanView)
        self.executer.add_query_callback(self.get_extra_query)
        self._create_filters()
        self.search.results.connect('selection-changed',
                                    self._on_results_selection_changed)
        self.search.results.set_selection_mode(gtk.SELECTION_MULTIPLE)
        self.search.focus_search_entry()

    #
    # WizardStep
    #

    def has_previous_step(self):
        return False

    def post_init(self):
        self.register_validate_function(self._refresh_next)
        self.force_validation()

    def next_step(self):
        # FIXME: For some reason, the loan isn't in self.store
        views = self.search.results.get_selected_rows()
        self.wizard.models = [self.store.fetch(v.loan) for v in views]
        return LoanItemSelectionStep(self.wizard, self, self.store,
                                     self.wizard.models)

    #
    # Callbacks
    #

    def _on_results_selection_changed(self, widget, selection):
        self._refresh_next()
Ejemplo n.º 28
0
class SearchEntryGadget(object):
    find_tooltip = _('Search')
    edit_tooltip = _('Edit')
    new_tooltip = _('Create')

    def __init__(self, entry, store, model, model_property,
                 search_columns, search_class, parent):
        """
        This gadget modifies a ProxyEntry turning it into a replacement for
        ProxyComboEntry.

        When instanciated, the gadget will remove the entry from the editor, add
        a gtk.HBox on its place, and re-attach the entry to the newly created
        hbox. This hbox will also have two buttons: One for showing the related
        search dialog (or search editor), and another one to add/edit a new
        object.

        There are a few advantages in using this instead of a combo:

        - There is no need to prefill the combo with all the options, which can
          be very slow depending on the number of objects.
        - This allows the user to use a better search mechanism, allowing him to
          filter using multiple keywords and even candidade keys (like a client
          document)

        :param entry: The entry that we should modify
        :param store: The store that will be used for database queries
        :param model: The model that we are updating
        :param model_property: Property name of the model that should be updated
        :param search_columns: Columns that will be queried when the user
          activates the entry
        :param search_class: Class of the search editor/dialog that will be
          displayed when more than one object is found
        :param parent: The parent that should be respected when running other
          dialogs
        :param find_tooltip: the tooltip to use for the search button
        :param edit_tooltip: the tooltip to use for the edit button
        :param new_tooltip: the tooltip to use for the new button
        """
        self.store = store
        self._entry = entry
        self._model = model
        # TODO: Maybe this two variables shoulb be a list of properties of the
        # table instead of strings
        self._model_property = model_property
        self._search_columns = search_columns
        self._search_class = search_class
        self._parent = parent

        # TODO: Respect permission manager
        self._editor_class = search_class.editor_class

        # If the search is for a person, the editor is called with a special
        # function
        if issubclass(search_class, BasePersonSearch):
            self._is_person = True
        else:
            self._is_person = False

        self._setup_widgets()
        self._setup_callbacks()

    #
    #   Private API
    #

    def _setup_widgets(self):
        self._replace_widget()

        # Add the two buttons
        self.find_button = self._create_button(gtk.STOCK_FIND)
        self.edit_button = self._create_button(gtk.STOCK_NEW)
        can_edit = self._entry.get_editable() and self._entry.get_sensitive()
        self.find_button.set_sensitive(can_edit)

        self.find_button.set_tooltip_text(self.find_tooltip)
        self.edit_button.set_tooltip_text(self.new_tooltip)

        # the entry needs a completion to work in MODE_DATA
        self._completion = gtk.EntryCompletion()
        self._entry.set_completion(self._completion)
        self._entry.set_mode(ENTRY_MODE_DATA)

        initial_value = getattr(self._model, self._model_property)
        self.set_value(initial_value)

        # The filter that will be used. This is not really in the interface. We
        # will just use it to perform the search.
        self._filter = StringSearchFilter('')
        self._executer = QueryExecuter(self.store)
        self._executer.set_search_spec(self._search_class.search_spec)
        self._executer.set_filter_columns(self._filter, self._search_columns)

    def _create_button(self, stock):
        image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_MENU)
        button = gtk.Button()
        button.set_relief(gtk.RELIEF_NONE)
        button.set_image(image)
        button.show()
        self.box.pack_start(button, False, False)
        return button

    def _replace_widget(self):
        # This will remove the entry, add a hbox in the entry old position, and
        # reattach the entry to this box. The box will then be used to add two
        # new buttons (one for searching, other for editing/adding new objects
        container = self._entry.parent

        # stolen from gazpacho code (widgets/base/base.py):
        props = {}
        for pspec in gtk.container_class_list_child_properties(container):
            props[pspec.name] = container.child_get_property(self._entry, pspec.name)

        self.box = gtk.HBox()
        self.box.show()
        self._entry.reparent(self.box)
        container.add(self.box)

        for name, value in props.items():
            container.child_set_property(self.box, name, value)

    def _setup_callbacks(self):
        self._entry.connect('activate', self._on_entry_activate)
        self._entry.connect('changed', self._on_entry_changed)
        self._entry.connect('notify::sensitive', self._on_entry_sensitive)
        self.find_button.connect('clicked', self._on_find_button__clicked)
        self.edit_button.connect('clicked', self._on_edit_button__clicked)

    def _run_search(self):
        text = self._entry.get_text()
        value = run_dialog(self._search_class, self._parent, self.store,
                           double_click_confirm=True,
                           initial_string=text)
        if value:
            self.set_value(value)

    def _run_editor(self):
        with api.new_store() as store:
            model = getattr(self._model, self._model_property)
            model = store.fetch(model)
            if self._is_person:
                value = run_person_role_dialog(self._editor_class, self._parent, store, model)
            else:
                value = run_dialog(self._editor_class, self._parent, store, model)

        if value:
            value = self.store.fetch(value)
            self.set_value(value)

    #
    #   Public API
    #

    def set_value(self, obj):
        if obj:
            display_value = obj.get_description()
            obj_id = obj.id
            self._entry.prefill([(display_value, obj_id)])
            self.set_edit_button_stock(gtk.STOCK_INFO)
            self.edit_button.set_tooltip_text(self.edit_tooltip)
        else:
            display_value = ''
            obj_id = None
            self._entry.prefill([])
            self.set_edit_button_stock(gtk.STOCK_NEW)
            self.edit_button.set_tooltip_text(self.new_tooltip)

        self._value = obj
        self._entry.update(obj_id)
        self._entry.set_text(display_value)

    def set_edit_button_stock(self, stock):
        image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_MENU)
        self.edit_button.set_image(image)

    #
    #   Callbacks
    #

    def _on_entry_activate(self, entry):
        if not self._entry.get_property('editable'):
            return
        text = entry.get_text()
        self._filter.set_state(text)
        state = self._filter.get_state()
        results = list(self._executer.search([state])[:2])
        if len(results) != 1:
            # XXX: If nothing is found in the query above, runing the search
            # will cause the query to be executed a second time. Refactor the
            # search to allow us to send the initial results avoiding this
            # second query.
            return self._run_search()

        # This means the search returned only one result.
        self.set_value(results[0])

    def _on_entry_changed(self, entry):
        # If the user edits the value in the entry, it invalidates the value.
        if self._value:
            self.set_value(None)

    def _on_entry_sensitive(self, entry, pspec):
        can_edit = self._entry.get_editable() and self._entry.get_sensitive()
        self.find_button.set_sensitive(can_edit)
        self.edit_button.set_sensitive(can_edit)

    def _on_edit_button__clicked(self, entry):
        self._run_editor()

    def _on_find_button__clicked(self, entry):
        self._run_search()
Ejemplo n.º 29
0
class ShellApp(GladeDelegate):
    """Base class for shell applications.

    The main use is to interact with a shell window and reduce
    duplication between other applications.
    """

    #: This attribute is used when generating titles for applications.
    #: It's also useful if we get a list of available applications with
    #: the application names translated. This list is going to be used when
    #: creating new user profiles.
    app_title = None

    #: name of the application, 'pos', 'payable', etc
    app_name = None

    #: If this application has a search like interface
    search = None

    #: This dictionary holds information about required permissions to access
    #: certain actions. Keys should be the name of the action (for instance
    #: SearchEmployess), and the value should be a tuple with the permission key
    #: (domain object or action identifier) and the required permission. In this
    #: case: ('Employee', perm.PERM_SEARCH). See <stoqlib.lib.permissions>
    action_permissions = {}

    #: The table we will query on to perform the search
    search_table = None

    #: Label left of the search entry
    search_label = _('Search:')

    #: the report class for printing the object list embedded on app.
    report_table = None

    def __init__(self, window, store=None):
        if store is None:
            store = api.get_default_store()
        self.store = store
        self.window = window

        self._loading_filters = False
        self._sensitive_group = dict()
        self.help_ui = None
        self.uimanager = self.window.uimanager

        self._pre_init()
        GladeDelegate.__init__(self,
                               gladefile=self.gladefile,
                               toplevel_name=self.toplevel_name)
        self._post_init()

    def _pre_init(self):
        # FIXME: Perhaps we should add a proper API to add a search to
        #        an application, however it's a bit complicated since the
        #        search creation is done in two steps due to how kiwi auto
        #        signal connection works
        if self.search_table is not None:
            self._create_search()
        self._app_settings = api.user_settings.get('app-ui', {})
        # Create actions, this must be done before the constructor
        # is called, eg when signals are autoconnected
        self.create_actions()

    def _post_init(self):
        self.create_ui()

        if self.search_table is not None:
            self.attach_slave('search_holder', self.search)
            self.create_filters()
            self._restore_filter_settings()
            self.search.focus_search_entry()

    def _create_search(self):
        # This does the first part of the search creation,
        # this need to be done here so that self.results is set when we
        # call GladeDelegate.__init__()

        self.executer = QueryExecuter(self.store)

        # FIXME: Remove this limit, but we need to migrate all existing
        #        searches to use lazy lists first. That in turn require
        #        us to rewrite the queries in such a way that count(*)
        #        will work properly.
        self.executer.set_limit(sysparam(self.store).MAX_SEARCH_RESULTS)
        self.executer.set_table(self.search_table)

        self.search = SearchSlave(self.get_columns(),
                                  restore_name=self.__class__.__name__)
        self.search.enable_advanced_search()
        self.search.set_query_executer(self.executer)
        self.search.connect("search-completed",
                            self._on_search__search_completed)
        self.results = self.search.result_view
        search_filter = self.search.get_primary_filter()
        search_filter.set_label(self.search_label)

    def _display_open_inventory_message(self):
        msg = _(u'There is an inventory process open at the moment.\n'
                'While that inventory is open, you will be unable to do '
                'operations that modify your stock.')
        self.inventory_bar = self.window.add_info_bar(gtk.MESSAGE_WARNING, msg)

    def _save_filter_settings(self):
        if self._loading_filters:
            return
        filter_states = self.search.get_filter_states()
        settings = self._app_settings.setdefault(self.app_name, {})
        settings['filter-states'] = filter_states

    def _restore_filter_settings(self):
        self._loading_filters = True
        settings = self._app_settings.setdefault(self.app_name, {})
        filter_states = settings.get('filter-states')
        if filter_states is not None:
            # Disable auto search to avoid an extra query when restoring the
            # state
            self.search.set_auto_search(False)
            self.search.set_filter_states(filter_states)
            self.search.set_auto_search(True)
        self._loading_filters = False

    #
    # Overridables
    #

    def create_actions(self):
        """This is called before the BaseWindow constructor, so we
        can create actions that can be autoconnected.
        The widgets and actions loaded from builder files are not set
        yet"""

    def create_ui(self):
        """This is called when the UI such as GtkWidgets should be
        created. Glade widgets are now created and can be accessed
        in the instance.
        """

    def activate(self, params):
        """This is when you switch to an application.
        You should setup widget sensitivity here and refresh lists etc
        :params params: an dictionary with optional parameters.
        """

    def setup_focus(self):
        """Define this method on child when it's needed.
        This is for calling grab_focus(), it's called after the window
        is shown. focus chains should be created in create_ui()"""

    def get_title(self):
        # This method must be redefined in child when it's needed
        branch = api.get_current_branch(self.store)
        return _('[%s] - %s') % (branch.get_description(), self.app_title)

    def can_change_application(self):
        """Define if we can change the current application or not.

        :returns: True if we can change the application, False otherwise.
        """
        return True

    def can_close_application(self):
        """Define if we can close the current application or not.

        :returns: True if we can close the application, False otherwise.
        """
        return True

    def set_open_inventory(self):
        """ Subclasses should overide this if they call
        :obj:`.check_open_inventory`.

        This method will be called it there is an open inventory, so the
        application can disable some funcionalities
        """
        raise NotImplementedError

    def new_activate(self):
        """Called when the New toolbar item is activated"""
        raise NotImplementedError

    def search_activate(self):
        """Called when the Search toolbar item is activated"""
        raise NotImplementedError

    def print_activate(self):
        """Called when the Print toolbar item is activated"""
        if self.search_table is None:
            raise NotImplementedError

        if self.results.get_selection_mode() == gtk.SELECTION_MULTIPLE:
            results = self.results.get_selected_rows()
        else:
            result = self.results.get_selected()
            results = [result] if result else None

        # There are no itens selected. We should print the entire list
        if not results:
            results = list(self.search.get_last_results())

        self.print_report(self.report_table, self.results, results)

    def export_spreadsheet_activate(self):
        """Called when the Export menu item is activated"""
        if self.search_table is None:
            raise NotImplementedError

        sse = SpreadSheetExporter()
        sse.export(object_list=self.results,
                   name=self.app_name,
                   filename_prefix=self.app.name)

    def create_filters(self):
        """Implement this to provide filters for the search container"""

    def search_completed(self, results, states):
        """Implement this if you want to know when a search has
        been completed.

        :param results: the search results
        :param states: search states used to construct the search query search
        """

    #
    # Public API
    #

    def add_ui_actions(self,
                       ui_string,
                       actions,
                       name='Actions',
                       action_type='normal',
                       filename=None):
        return self.window.add_ui_actions(ui_string=ui_string,
                                          actions=actions,
                                          name=name,
                                          action_type=action_type,
                                          filename=filename,
                                          instance=self)

    def add_tool_menu_actions(self, actions):
        return self.window.add_tool_menu_actions(actions=actions)

    def set_help_section(self, label, section):
        self.window.set_help_section(label=label, section=section)

    def get_statusbar_message_area(self):
        return self.window.statusbar.message_area

    def print_report(self, report_class, *args, **kwargs):
        filters = self.search.get_search_filters()
        if filters:
            kwargs['filters'] = filters

        print_report(report_class, *args, **kwargs)

    def set_sensitive(self, widgets, value):
        """Set the *widgets* sensitivity based on *value*

        If a sensitive group was registered for any widget,
        it's validation function will be tested and, if ``False``
        is returned, it will be set insensitive, ignoring *value*

        :param widgets: a list of widgets
        :param value: either `True` or `False`
        """
        # FIXME: Maybe this should ne done on kiwi?
        for widget in widgets:
            sensitive = value

            for validator in self._sensitive_group.get(widget, []):
                if not validator[0](*validator[1]):
                    sensitive = False
                    break

            widget.set_sensitive(sensitive)

    def register_sensitive_group(self, widgets, validation_func, *args):
        """Register widgets on a sensitive group.

        Everytime :obj:`.set_sensitive()` is called, if there is any
        validation function for a given widget on sensitive group,
        then that will be used to decide if it gets sensitive or
        insensitive.

        :param widgets: a list of widgets
        :param validation_func: a function for validation. It should
            return either ``True`` or ``False``.
        :param args: args that will be passed to *validation_func*
        """
        assert callable(validation_func)

        for widget in widgets:
            validators = self._sensitive_group.setdefault(widget, set())
            validators.add((validation_func, args))

    def run_dialog(self, dialog_class, *args, **kwargs):
        """ Encapsuled method for running dialogs. """
        return run_dialog(dialog_class, self, *args, **kwargs)

    @cached_function()
    def has_open_inventory(self):
        return Inventory.has_open(self.store,
                                  api.get_current_branch(self.store))

    def check_open_inventory(self):
        """Checks if there is an open inventory.

        In the case there is one, will call set_open_inventory (subclasses
        should implement it).

        Returns True if there is an open inventory. False otherwise
        """
        inventory_bar = getattr(self, 'inventory_bar', None)

        if self.has_open_inventory():
            if inventory_bar:
                inventory_bar.show()
            else:
                self._display_open_inventory_message()
            self.set_open_inventory()
            return True
        elif inventory_bar:
            inventory_bar.hide()
            return False

    # FIXME: Most of these should be removed and access the search API
    #        directly, eg, self.search.clear() etc

    def add_filter(self,
                   search_filter,
                   position=SearchFilterPosition.BOTTOM,
                   columns=None,
                   callback=None):
        """
        See :class:`SearchSlave.add_filter`
        """
        self.search.add_filter(search_filter, position, columns, callback)

    def set_text_field_columns(self, columns):
        """
        See :class:`SearchSlave.set_text_field_columns`
        """
        self.search.set_text_field_columns(columns)

    def refresh(self):
        """
        See :class:`stoqlib.gui.search.searchslave.SearchSlave.refresh`
        """
        self.search.refresh()

    def clear(self):
        """
        See :class:`stoqlib.gui.search.searchslave.SearchSlave.clear`
        """
        self.search.clear()

    def select_result(self, result):
        """Select the object in the result list

        If the object is not in the list (filtered out, for instance), no error
        is thrown and nothing is selected
        """
        try:
            self.results.select(result)
        except ValueError:
            pass

    #
    # Callbacks
    #

    def _on_search__search_completed(self, search, results, states):
        self.search_completed(results, states)

        has_results = len(results)
        for widget in [self.window.Print, self.window.ExportSpreadSheet]:
            widget.set_sensitive(has_results)
        self._save_filter_settings()
Ejemplo n.º 30
0
 def _date_filter_query(self, search_spec, column):
     executer = QueryExecuter(self.store)
     executer.set_filter_columns(self.date_filter, [column])
     executer.set_table(search_spec)
     return executer.search([self.date_filter.get_state()])
Ejemplo n.º 31
0
class QueryExecuterTest(DomainTest):
    def setUp(self):
        DomainTest.setUp(self)
        self.qe = QueryExecuter(self.store)
        self.qe.set_search_spec(ClientCategory)
        self.sfilter = mock.Mock()
        self.qe.set_filter_columns(self.sfilter, ['name'])

    def _search_async(self, states):
        op = self.qe.search_async(states)
        self.qe._operation_executer._queue.join()
        return list(op.get_result())

    def _search_string_all(self, text):
        return self.qe.search([
            StringQueryState(filter=self.sfilter,
                             mode=StringQueryState.CONTAINS_ALL,
                             text=text)
        ])

    def _search_string_all_async(self, text):
        return self._search_async([
            StringQueryState(filter=self.sfilter,
                             mode=StringQueryState.CONTAINS_ALL,
                             text=text)
        ])

    def _search_string_exactly(self, text):
        return self.qe.search([
            StringQueryState(filter=self.sfilter,
                             mode=StringQueryState.CONTAINS_EXACTLY,
                             text=text)
        ])

    def _search_string_exactly_async(self, text):
        return self._search_async([
            StringQueryState(filter=self.sfilter,
                             mode=StringQueryState.CONTAINS_EXACTLY,
                             text=text)
        ])

    def _search_string_not(self, text):
        return self.qe.search([
            StringQueryState(filter=self.sfilter,
                             mode=StringQueryState.NOT_CONTAINS,
                             text=text)
        ])

    def _search_string_not_async(self, text):
        return self._search_async([
            StringQueryState(filter=self.sfilter,
                             mode=StringQueryState.NOT_CONTAINS,
                             text=text)
        ])

    def test_string_query(self):
        self.assertEqual(self.store.find(ClientCategory).count(), 0)
        self.create_client_category(u'EYE MOON FLARE 110 0.5')
        self.create_client_category(u'EYE MOON FLARE 120 1.0')
        self.create_client_category(u'EYE SUN FLARE 120 1.0')
        self.create_client_category(u'EYE SUN FLARE 110 1.0')
        self.create_client_category(u'EYE SUN STONE 120 0.5')

        self.assertEqual(self._search_string_all(u'eye flare 110').count(), 2)
        self.assertEqual(self._search_string_all(u'eye 0.5').count(), 2)
        self.assertEqual(self._search_string_all(u'eye 120').count(), 3)

        self.assertEqual(
            self._search_string_exactly(u'eye flare 110').count(), 0)
        self.assertEqual(self._search_string_exactly(u'eye 0.5').count(), 0)
        self.assertEqual(self._search_string_exactly(u'eye 120').count(), 0)

        self.assertEqual(self._search_string_not(u'stone 110').count(), 2)
        self.assertEqual(self._search_string_not(u'eye').count(), 0)
        self.assertEqual(self._search_string_not(u'moon 120').count(), 1)

    def test_search_async(self):
        self.assertEqual(self.store.find(ClientCategory).count(), 0)
        try:
            self.create_client_category(u'EYE MOON FLARE 110 0.5')
            self.create_client_category(u'EYE MOON FLARE 120 1.0')
            self.create_client_category(u'EYE SUN FLARE 120 1.0')
            self.create_client_category(u'EYE SUN FLARE 110 1.0')
            self.create_client_category(u'EYE SUN STONE 120 0.5')
            # search_async uses another connection. Because of that, we need to
            # commit the store or else it will not be able to find the objects
            self.store.commit()

            self.assertEqual(
                len(self._search_string_all_async(u'eye flare 110')), 2)
            self.assertEqual(len(self._search_string_all_async(u'eye 0.5')), 2)
            self.assertEqual(len(self._search_string_all_async(u'eye 120')), 3)

            self.assertEqual(
                len(self._search_string_exactly_async(u'eye flare 110')), 0)
            self.assertEqual(
                len(self._search_string_exactly_async(u'eye 0.5')), 0)
            self.assertEqual(
                len(self._search_string_exactly_async(u'eye 120')), 0)

            self.assertEqual(len(self._search_string_not_async(u'stone 110')),
                             2)
            self.assertEqual(len(self._search_string_not_async(u'eye')), 0)
            self.assertEqual(len(self._search_string_not_async(u'moon 120')),
                             1)
        finally:
            self.clean_domain([ClientCategory])
            self.store.commit()
Ejemplo n.º 32
0
class TransactionPage(object):
    # shows either a list of:
    #   - transactions
    #   - payments
    def __init__(self, model, app, parent):
        self.model = model
        self.app = app
        self.parent_window = parent
        self._block = False

        self._create_search()
        self._add_date_filter()

        self._setup_search()
        self.refresh()

    def get_toplevel(self):
        return self.parent_window

    def _create_search(self):
        self.search = SearchSlave(self._get_columns(self.model.kind))
        self.search.connect('result-item-activated',
                            self._on_search__item_activated)
        self.search.enable_advanced_search()
        self.query = QueryExecuter(self.app.store)
        self.search.set_query_executer(self.query)
        self.search.set_result_view(FinancialSearchResults)
        self.result_view = self.search.result_view
        self.result_view.page = self
        tree_view = self.search.result_view.get_treeview()
        tree_view.set_rules_hint(True)
        tree_view.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_BOTH)

    def _add_date_filter(self):
        self.date_filter = DateSearchFilter(_('Date:'))
        self.date_filter.clear_options()
        self.date_filter.add_option(Any, 0)
        year = datetime.datetime.today().year
        month_names = get_month_names()
        for i, month in enumerate(month_names):
            name = month_names[i]
            option = type(name + 'Option', (MonthOption, ),
                          {'name': _(name),
                           'month': i + 1,
                           'year': year})
            self.date_filter.add_option(option, i + 1)
        self.date_filter.add_custom_options()
        self.date_filter.mode.select_item_by_position(0)
        self.search.add_filter(self.date_filter)

    def _append_date_query(self, field):
        date = self.date_filter.get_state()
        queries = []
        if isinstance(date, DateQueryState) and date.date is not None:
            queries.append(Date(field) == date.date)
        elif isinstance(date, DateIntervalQueryState):
            queries.append(Date(field) >= date.start)
            queries.append(Date(field) <= date.end)
        return queries

    def _payment_query(self, store):
        queries = self._append_date_query(self.query.table.due_date)
        if queries:
            return store.find(self.query.table, And(*queries))

        return store.find(self.query.table)

    def _transaction_query(self, store):
        queries = [Or(self.model.id == AccountTransaction.account_id,
                      self.model.id == AccountTransaction.source_account_id)]

        queries.extend(self._append_date_query(AccountTransaction.date))
        return store.find(AccountTransactionView, And(*queries))

    def show(self):
        self.search.show()

    def _setup_search(self):
        if self.model.kind == 'account':
            self.query.set_table(AccountTransactionView)
            self.search.set_text_field_columns(['description'])
            self.query.set_query(self._transaction_query)
        elif self.model.kind == 'payable':
            self.search.set_text_field_columns(['description', 'supplier_name'])
            self.query.set_table(OutPaymentView)
            self.query.set_query(self._payment_query)
        elif self.model.kind == 'receivable':
            self.search.set_text_field_columns(['description', 'drawee'])
            self.query.set_table(InPaymentView)
            self.query.set_query(self._payment_query)
        else:
            raise TypeError("unknown model kind: %r" % (self.model.kind, ))

    def refresh(self):
        self.search.result_view.clear()
        if self.model.kind == 'account':
            transactions = AccountTransactionView.get_for_account(self.model, self.app.store)
            self.append_transactions(transactions)
        elif self.model.kind == 'payable':
            self._populate_payable_payments(OutPaymentView)
        elif self.model.kind == 'receivable':
            self._populate_payable_payments(InPaymentView)
        else:
            raise TypeError("unknown model kind: %r" % (self.model.kind, ))

    def _get_columns(self, kind):
        if kind in ['payable', 'receivable']:
            return self._get_payment_columns()
        else:
            return self._get_account_columns()

    def _get_account_columns(self):
        def format_withdrawal(value):
            if value < 0:
                return currency(abs(value)).format(symbol=True, precision=2)

        def format_deposit(value):
            if value > 0:
                return currency(value).format(symbol=True, precision=2)

        if self.model.account_type == Account.TYPE_INCOME:
            color_func = lambda x: False
        else:
            color_func = lambda x: x < 0
        return [Column('date', title=_("Date"), data_type=datetime.date, sorted=True),
                Column('code', title=_("Code"), data_type=unicode),
                Column('description', title=_("Description"),
                       data_type=unicode, expand=True),
                Column('account', title=_("Account"), data_type=unicode),
                Column('value',
                       title=self.model.account.get_type_label(out=False),
                       data_type=currency,
                       format_func=format_deposit),
                Column('value',
                       title=self.model.account.get_type_label(out=True),
                       data_type=currency,
                       format_func=format_withdrawal),
                ColoredColumn('total', title=_("Total"), data_type=currency,
                              color='red',
                              data_func=color_func)]

    def _get_payment_columns(self):
        return [SearchColumn('due_date', title=_("Due date"), data_type=datetime.date, sorted=True),
                IdentifierColumn('identifier', title=_("Code")),
                SearchColumn('description', title=_("Description"), data_type=unicode, expand=True),
                SearchColumn('value', title=_("Value"),
                             data_type=currency)]

    def append_transactions(self, transactions):
        for transaction in transactions:
            description = transaction.get_account_description(self.model)
            value = transaction.get_value(self.model)
            self._add_transaction(transaction, description, value)
        self.update_totals()

    def _populate_payable_payments(self, view_class):
        for view in self.app.store.find(view_class):
            self.search.result_view.append(view)

    def _add_transaction(self, transaction, description, value):
        item = Settable(transaction=transaction)
        self._update_transaction(item, transaction, description, value)
        self.search.result_view.append(item)
        return item

    def _update_transaction(self, item, transaction, description, value):
        item.account = description
        item.date = transaction.date
        item.description = transaction.description
        item.value = value
        item.code = transaction.code

    def update_totals(self):
        total = decimal.Decimal('0')
        for item in self.search.result_view:
            total += item.value
            item.total = total

    def _edit_transaction_dialog(self, item):
        store = api.new_store()
        if isinstance(item.transaction, AccountTransactionView):
            account_transaction = store.fetch(item.transaction.transaction)
        else:
            account_transaction = store.fetch(item.transaction)
        model = getattr(self.model, 'account', self.model)

        transaction = run_dialog(AccountTransactionEditor, self.app,
                                 store, account_transaction, model)

        if transaction:
            store.flush()
            self._update_transaction(item, transaction,
                                     transaction.edited_account.description,
                                     transaction.value)
            self.update_totals()
            self.search.result_view.update(item)
            self.app.accounts.refresh_accounts(self.app.store)
        store.confirm(transaction)
        store.close()

    def on_dialog__opened(self, dialog):
        dialog.connect('account-added', self.on_dialog__account_added)

    def on_dialog__account_added(self, dialog):
        self.app.accounts.refresh_accounts(self.app.store)

    def add_transaction_dialog(self):
        store = api.new_store()
        model = getattr(self.model, 'account', self.model)
        model = store.fetch(model)

        transaction = run_dialog(AccountTransactionEditor, self.app,
                                 store, None, model)
        if transaction:
            transaction.sync()
            value = transaction.value
            other = transaction.get_other_account(model)
            if other == model:
                value = -value
            item = self._add_transaction(transaction, other.description, value)
            self.update_totals()
            self.search.result_view.update(item)
            self.app.accounts.refresh_accounts(self.app.store)
        store.confirm(transaction)
        store.close()

    def _on_search__item_activated(self, objectlist, item):
        if self.model.kind == 'account':
            self._edit_transaction_dialog(item)
Ejemplo n.º 33
0
class QueryExecuterTest(DomainTest):
    def setUp(self):
        DomainTest.setUp(self)
        self.qe = QueryExecuter(self.store)
        self.qe.set_search_spec(ClientCategory)
        self.sfilter = mock.Mock()
        self.qe.set_filter_columns(self.sfilter, ['name'])

    def _search_async(self, states):
        op = self.qe.search_async(states)
        self.qe._operation_executer._queue.join()
        return list(op.get_result())

    def _search_string_all(self, text):
        return self.qe.search([
            StringQueryState(filter=self.sfilter,
                             mode=StringQueryState.CONTAINS_ALL,
                             text=text)])

    def _search_string_all_async(self, text):
        return self._search_async([
            StringQueryState(filter=self.sfilter,
                             mode=StringQueryState.CONTAINS_ALL,
                             text=text)])

    def _search_string_exactly(self, text):
        return self.qe.search([
            StringQueryState(filter=self.sfilter,
                             mode=StringQueryState.CONTAINS_EXACTLY,
                             text=text)])

    def _search_string_exactly_async(self, text):
        return self._search_async([
            StringQueryState(filter=self.sfilter,
                             mode=StringQueryState.CONTAINS_EXACTLY,
                             text=text)])

    def _search_string_not(self, text):
        return self.qe.search([
            StringQueryState(filter=self.sfilter,
                             mode=StringQueryState.NOT_CONTAINS,
                             text=text)])

    def _search_string_not_async(self, text):
        return self._search_async([
            StringQueryState(filter=self.sfilter,
                             mode=StringQueryState.NOT_CONTAINS,
                             text=text)])

    def test_string_query(self):
        self.assertEquals(self.store.find(ClientCategory).count(), 0)
        self.create_client_category(u'EYE MOON FLARE 110 0.5')
        self.create_client_category(u'EYE MOON FLARE 120 1.0')
        self.create_client_category(u'EYE SUN FLARE 120 1.0')
        self.create_client_category(u'EYE SUN FLARE 110 1.0')
        self.create_client_category(u'EYE SUN STONE 120 0.5')

        self.assertEquals(self._search_string_all(u'eye flare 110').count(), 2)
        self.assertEquals(self._search_string_all(u'eye 0.5').count(), 2)
        self.assertEquals(self._search_string_all(u'eye 120').count(), 3)

        self.assertEquals(self._search_string_exactly(u'eye flare 110').count(), 0)
        self.assertEquals(self._search_string_exactly(u'eye 0.5').count(), 0)
        self.assertEquals(self._search_string_exactly(u'eye 120').count(), 0)

        self.assertEquals(self._search_string_not(u'stone 110').count(), 2)
        self.assertEquals(self._search_string_not(u'eye').count(), 0)
        self.assertEquals(self._search_string_not(u'moon 120').count(), 1)

    def test_search_async(self):
        self.assertEquals(self.store.find(ClientCategory).count(), 0)
        try:
            self.create_client_category(u'EYE MOON FLARE 110 0.5')
            self.create_client_category(u'EYE MOON FLARE 120 1.0')
            self.create_client_category(u'EYE SUN FLARE 120 1.0')
            self.create_client_category(u'EYE SUN FLARE 110 1.0')
            self.create_client_category(u'EYE SUN STONE 120 0.5')
            # search_async uses another connection. Because of that, we need to
            # commit the store or else it will not be able to find the objects
            self.store.commit()

            self.assertEquals(
                len(self._search_string_all_async(u'eye flare 110')), 2)
            self.assertEquals(
                len(self._search_string_all_async(u'eye 0.5')), 2)
            self.assertEquals(
                len(self._search_string_all_async(u'eye 120')), 3)

            self.assertEquals(
                len(self._search_string_exactly_async(u'eye flare 110')), 0)
            self.assertEquals(
                len(self._search_string_exactly_async(u'eye 0.5')), 0)
            self.assertEquals(
                len(self._search_string_exactly_async(u'eye 120')), 0)

            self.assertEquals(
                len(self._search_string_not_async(u'stone 110')), 2)
            self.assertEquals(
                len(self._search_string_not_async(u'eye')), 0)
            self.assertEquals(
                len(self._search_string_not_async(u'moon 120')), 1)
        finally:
            self.clean_domain([ClientCategory])
            self.store.commit()
Ejemplo n.º 34
0
class QueryEntryGadget(object):
    """This gadget modifies a ProxyEntry to behave like a ProxyComboEntry.

    When instanciated, the gadget will remove the entry from the editor, add
    a gtk.HBox on its place, and re-attach the entry to the newly created
    hbox. This hbox will also have a button to add/edit a new object.

    There are a few advantages in using this instead of a combo:

    - There is no need to prefill the combo with all the options, which can
      be very slow depending on the number of objects.

    - This allows the user to use a better search mechanism, allowing him to
      filter using multiple keywords and even candidade keys (like a client
      document)
    """

    MIN_KEY_LENGTH = 1
    LOADING_ITEMS_TEXT = _("Loading items...")
    NEW_ITEM_TEXT = _("Create a new item with that name")
    NEW_ITEM_TOOLTIP = _("Create a new item")
    EDIT_ITEM_TOOLTIP = _("Edit the selected item")
    ITEM_EDITOR = None
    SEARCH_CLASS = None
    SEARCH_SPEC = None
    SEARCH_COLUMNS = None

    def __init__(self, entry, store, initial_value=None,
                 parent=None, run_editor=None):
        """
        :param entry: The entry that we should modify
        :param store: The store that will be used for database queries
        :param initial_value: Initial value for the entry
        :param parent: The parent that should be respected when running other
          dialogs
        """
        super(QueryEntryGadget, self).__init__()

        self._current_obj = None
        self._parent = parent
        self._on_run_editor = run_editor
        self.entry = entry
        self.entry.set_mode(ENTRY_MODE_DATA)
        self.store = store

        # The filter that will be used. This is not really in the interface.
        # We will just use it to perform the search.
        self._filter = StringSearchFilter('')
        self._executer = QueryExecuter(self.store)
        self._executer.set_search_spec(self.SEARCH_SPEC)
        self._executer.set_filter_columns(self._filter, self.SEARCH_COLUMNS)

        self._last_operation = None
        self._source_id = None
        self._is_person = issubclass(self.ITEM_EDITOR, BasePersonRoleEditor)

        self._setup()
        self.set_value(initial_value, force=True)

    #
    #  Public API
    #

    def set_value(self, obj, force=False):
        if not force and obj == self._current_obj:
            return

        obj = self.store.fetch(obj)
        if obj is not None:
            value = obj.get_description()
            self.entry.prefill([(value, obj)])
            self.update_edit_button(gtk.STOCK_INFO, self.EDIT_ITEM_TOOLTIP)
        else:
            value = ''
            self.entry.prefill([])
            self.update_edit_button(gtk.STOCK_NEW, self.NEW_ITEM_TOOLTIP)

        self._current_obj = obj
        self.entry.update(obj)
        self.entry.set_text(value)

    def set_editable(self, can_edit):
        self.edit_button.set_sensitive(can_edit)
        self.entry.set_property('editable', can_edit)

    def update_edit_button(self, stock, tooltip):
        image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_MENU)
        self.edit_button.set_image(image)
        self.edit_button.set_tooltip_text(tooltip)

    def get_object_from_item(self, item):
        return item

    def describe_item(self, item):
        raise NotImplementedError

    #
    #  Private
    #

    def _setup(self):
        self._replace_widget()

        self.edit_button = self._add_button(gtk.STOCK_NEW)
        self.edit_button.connect('clicked', self._on_edit_button__clicked)

        self.entry.connect('activate', self._on_entry__activate)
        self.entry.connect('changed', self._on_entry__changed)
        self.entry.connect('notify::sensitive', self._on_entry_sensitive)
        self.entry.connect('key-press-event', self._on_entry__key_press_event)

        self._popup = _QueryEntryPopup(self)
        self._popup.connect('item-selected', self._on_popup__item_selected)
        self._popup.connect('create-item', self._on_popup__create_item)

    def _add_button(self, stock):
        image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_MENU)
        button = gtk.Button()
        button.set_relief(gtk.RELIEF_NONE)
        button.set_image(image)
        button.show()
        self.box.pack_start(button, False, False)
        return button

    def _replace_widget(self):
        # This will remove the entry, add a hbox in the entry old position, and
        # reattach the entry to this box. The box will then be used to add two
        # new buttons (one for searching, other for editing/adding new objects
        container = self.entry.parent

        # stolen from gazpacho code (widgets/base/base.py):
        props = {}
        for pspec in gtk.container_class_list_child_properties(container):
            props[pspec.name] = container.child_get_property(self.entry, pspec.name)

        self.box = gtk.HBox()
        self.box.show()
        self.entry.reparent(self.box)
        container.add(self.box)

        for name, value in props.items():
            container.child_set_property(self.box, name, value)

    def _find_items(self, text):
        self._filter.set_state(text)
        state = self._filter.get_state()
        return self._executer.search_async([state], limit=10)

    def _dispatch(self, value):
        self._source_id = None
        if self._last_operation is not None:
            self._last_operation.cancel()
        self._last_operation = self._find_items(value)
        self._last_operation.connect(
            'finish', lambda o: self._popup.add_items(o.get_result()))

    def _run_search(self):
        text = self.entry.get_text()
        if not text:
            return

        item = run_dialog(self.SEARCH_CLASS, self._parent, self.store,
                          double_click_confirm=True, initial_string=text)
        if item:
            self.set_value(self.get_object_from_item(item))

    def _run_editor(self, model=None, description=None):
        with api.new_store() as store:
            model = store.fetch(model)
            if self._on_run_editor is not None:
                retval = self._on_run_editor(store, model,
                                             description=description)
            else:
                rd = run_person_role_dialog if self._is_person else run_dialog
                retval = rd(self.ITEM_EDITOR, self._parent, store, model,
                            description=description)

        if store.committed:
            return self.store.fetch(retval)

    #
    #  Callbacks
    #

    def _on_entry__key_press_event(self, window, event):
        keyval = event.keyval
        if keyval == gtk.keysyms.Up or keyval == gtk.keysyms.KP_Up:
            self._popup.scroll(relative=-1)
            return True
        elif keyval == gtk.keysyms.Down or keyval == gtk.keysyms.KP_Down:
            self._popup.scroll(relative=+1)
            return True
        elif keyval == gtk.keysyms.Page_Up:
            self._popup.scroll(relative=-14)
            return True
        elif keyval == gtk.keysyms.Page_Down:
            self._popup.scroll(relative=+14)
            return True
        elif keyval == gtk.keysyms.Escape:
            self._popup.popdown()
            return True

        return False

    def _on_entry__changed(self, entry):
        value = unicode(entry.get_text())
        self.set_value(None)
        if len(value) >= self.MIN_KEY_LENGTH:
            if self._source_id is not None:
                glib.source_remove(self._source_id)
            self._source_id = glib.timeout_add(150, self._dispatch, value)
            if not self._popup.visible:
                self._popup.popup()
            self._popup.set_loading(True)
        elif self._popup.visible:
            # In this case, the user has deleted text to less than the
            # min key length, so pop it down
            if self._source_id is not None:
                glib.source_remove(self._source_id)
                self._source_id = None
            self._popup.popdown()

    def _on_entry__activate(self, entry):
        self._popup.popdown()
        self._popup.confirm()

    def _on_entry_sensitive(self, entry, pspec):
        can_edit = entry.get_editable() and entry.get_sensitive()
        self.edit_button.set_sensitive(can_edit)

    def _on_popup__item_selected(self, popup, item, fallback_to_search):
        self.set_value(self.get_object_from_item(item))
        popup.popdown()
        self.entry.grab_focus()
        glib.idle_add(self.entry.select_region, len(self.entry.get_text()), -1)

        if item is None and fallback_to_search:
            self._run_search()

    def _on_popup__create_item(self, popup):
        obj = self._run_editor(description=unicode(self.entry.get_text()))
        self.set_value(obj)

    def _on_edit_button__clicked(self, entry):
        current_obj = self.entry.read()
        obj = self._run_editor(current_obj)
        if obj:
            self.set_value(obj, force=True)