Exemplo n.º 1
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),
                                  store=self.app.store)
        self.search.connect('result-item-activated',
                            self._on_search__item_activated)
        self.search.enable_advanced_search()
        self.search.set_result_view(FinancialSearchResults)
        self.result_view = self.search.result_view
        self.result_view.page = self
        self.result_view.set_cell_data_func(
            self._on_result_view__cell_data_func)
        tree_view = self.search.result_view.get_treeview()
        tree_view.set_rules_hint(True)
        tree_view.set_grid_lines(Gtk.TreeViewGridLines.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.select(Any)
        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):
        executer = self.search.get_query_executer()
        search_spec = executer.search_spec
        queries = self._append_date_query(search_spec.due_date)
        if queries:
            return store.find(search_spec, And(*queries))

        return store.find(search_spec)

    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.search.set_search_spec(AccountTransactionView)
            self.search.set_text_field_columns(['description'])
            self.search.set_query(self._transaction_query)
        elif self.model.kind == 'payable':
            self.search.set_text_field_columns(
                ['description', 'supplier_name'])
            self.search.set_search_spec(OutPaymentView)
            self.search.set_query(self._payment_query)
        elif self.model.kind == 'receivable':
            self.search.set_text_field_columns(['description', 'drawee'])
            self.search.set_search_spec(InPaymentView)
            self.search.set_query(self._payment_query)
        else:
            raise TypeError("unknown model kind: %r" % (self.model.kind, ))

    def refresh(self):
        self.search.refresh()

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

    def _on_result_view__cell_data_func(self, column, renderer, account_view,
                                        text):
        if not isinstance(renderer, Gtk.CellRendererText):
            return text

        if self.model.kind != 'account':
            return text

        trans = account_view.transaction
        is_imbalance = self.app._imbalance_account_id in [
            trans.dest_account_id, trans.source_account_id
        ]

        renderer.set_property('weight-set', is_imbalance)
        if is_imbalance:
            renderer.set_property('weight', Pango.Weight.BOLD)

        return text

    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=str),
            Column('description',
                   title=_("Description"),
                   data_type=str,
                   expand=True),
            Column('account', title=_("Account"), data_type=str),
            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=_("Payment #")),
            SearchColumn('description',
                         title=_("Description"),
                         data_type=str,
                         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)
            # If a transaction has the source equals to the destination account.
            # Show the same transaction, but with reversed value.
            if transaction.source_account_id == transaction.dest_account_id:
                self._add_transaction(transaction, description, -value)
            self._add_transaction(transaction, description, value)
        self.update_totals()

    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)

        store.confirm(transaction)
        if transaction:
            self.app.refresh_pages()
            self.update_totals()
            self.app.accounts.refresh_accounts(self.app.store)
        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)
        store.confirm(transaction)
        if transaction:
            self.app.refresh_pages()
            self.update_totals()
            self.app.accounts.refresh_accounts(self.app.store)
        store.close()

    def _on_search__item_activated(self, objectlist, item):
        if self.model.kind == 'account':
            self._edit_transaction_dialog(item)
Exemplo n.º 2
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.
    """

    domain = 'stoq'

    #: 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 spec for store.find() to perform the search on
    search_spec = 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):
        self.store = store
        self.window = window

        self._sensitive_group = dict()
        self.help_ui = None

        # Each app will have its own ActionGroup prefixed with the app name, so
        # that menus (and other activatables) can activate actions by name (for
        # instance sale.new_sale)
        self.action_group = Gio.SimpleActionGroup()
        window.toplevel.insert_action_group(self.app_name, self.action_group)
        window._action_specs = {}

        # FIXME: These two should probably post-__init__, but
        #        that breaks date_label in the calender app
        self._create_search()
        self.create_actions()
        GladeDelegate.__init__(self,
                               gladefile=self.gladefile,
                               toplevel_name=self.toplevel_name)

        self._attach_search()
        self.create_ui()

        domain_options = self.get_domain_options()
        self.window.add_domain_header(domain_options)
        self.create_popover(domain_options)

    def _create_search(self):
        if self.search_spec is None:
            return
        self.columns = self.get_columns()
        ApplicationSetupSearchEvent.emit(self)
        self.search = SearchSlave(self.columns,
                                  store=self.store,
                                  restore_name=self.__class__.__name__,
                                  search_spec=self.search_spec)

    def _attach_search(self):
        if self.search_spec is None:
            return
        self.search.enable_advanced_search()
        self.attach_slave('search_holder', self.search)
        search_filter = self.search.get_primary_filter()
        search_filter.set_label(self.search_label)
        self.create_filters()
        self.search.restore_filter_settings('app-ui', self.app_name)
        self.search.focus_search_entry()

        # FIXME: Remove and use search directly instead of the result view
        self.results = self.search.result_view

    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.MessageType.WARNING, msg)

    #
    # Overridables
    #

    def get_domain_options(self):
        return []

    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, refresh=True):
        """This is when you switch to an application.

        You should setup widget sensitivity here and refresh lists etc

        :param refresh: if we should refresh the search
        """

    def deactivate(self):
        pass

    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 branch.get_description()

    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):  # pragma no cover
        """ 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 print_activate(self):
        """Called when the Print toolbar item is activated"""
        if self.search_spec is None:  # pragma no cover
            raise NotImplementedError

        # Print all the 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_spec is None:  # pragma no cover
            raise NotImplementedError

        model = self.results.get_model()
        if isinstance(model, LazyObjectModel):
            model.load_items_from_results(0, model._count)

        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
        """

    def create_popover(self, options):
        """Create a popover for the main search

        This will create a popover that should be displayed by callsing
        self.show_popover() when necessary
        """
        self.domain_menu = Gio.Menu()
        for (icon, label, action, in_header) in options:
            self.domain_menu.append(label, action)

        self._popover = Gtk.Popover.new_from_model(self.results, self.domain_menu)
        self._popover.set_position(Gtk.PositionType.BOTTOM)

    #
    # Public API
    #

    def add_ui_actions(self, actions, name='Actions'):
        self.window.add_ui_actions(self, actions, name)

    def show_popover(self, event):
        """Disaply the popover under the mouse cursor

        :param event: The event that was passed to the right-click event
        """
        x, y = event.button.window.coords_to_parent(event.button.x,
                                                    event.button.y)
        rect = Gdk.Rectangle()
        rect.x = x
        rect.y = y
        rect.width = rect.height = 1
        self._popover.set_pointing_to(rect)
        self._popover.show()

    def add_columns(self, columns):
        """Add some columns to the default ones.

        Note that this method must be called during the setup of this search,
        which right now is only possible for those who capture the
        `<stoq.lib.gui.events.ApplicationSetupSearchEvent>`
        """
        self.columns.extend(columns)

    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

            if isinstance(widget, Gio.SimpleAction):
                widget.set_enabled(sensitive)
            else:
                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 create_branch_filter(self, label=None, column=None):
        branch_filter = self.search.create_branch_filter(label, column)
        # If there is only one item in the combo, lets hide it.
        if len(branch_filter.combo) == 1:
            branch_filter.hide()
        return branch_filter

    def refresh(self, rollback=True):
        """
        See :class:`stoq.lib.gui.search.searchslave.SearchSlave.refresh`
        """
        # Since the store here is actually a transaction and the items
        # on it can be changed from another station, do a rollback so
        # the items get reloaded, avoiding cache problems
        # Note that this gets mocked on tests to not do the rollback
        if rollback:
            self.store.rollback(close=False)
        self.search.refresh()

    def clear(self):
        """
        See :class:`stoq.lib.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_results__right_click(self, results, result, event):
        if self._popover:
            self.show_popover(event)

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

        self.set_sensitive([self.window.print, self.window.export], len(results))
        self.search.save_filter_settings('app-ui', self.app_name)
Exemplo n.º 3
0
class QuoteGroupSelectionStep(BaseWizardStep):
    gladefile = 'QuoteGroupSelectionStep'

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

    def _setup_slaves(self):
        self.search = SearchSlave(self._get_columns(),
                                  restore_name=self.__class__.__name__,
                                  search_spec=QuotationView,
                                  store=self.store)
        self.attach_slave('search_group_holder', self.search)

        self.search.set_text_field_columns(['supplier_name', 'identifier_str'])
        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)

    def _get_columns(self):
        return [IdentifierColumn('identifier', title=_("Quote #"), sorted=True),
                IdentifierColumn('group_identifier', title=_('Group #')),
                Column('supplier_name', title=_('Supplier'), data_type=str,
                       width=300),
                Column('open_date', title=_('Open date'),
                       data_type=datetime.date),
                Column('deadline', title=_('Deadline'),
                       data_type=datetime.date)]

    def _can_purchase(self, item):
        return item.cost > currency(0) and item.quantity > Decimal(0)

    def _can_order(self, quotation):
        if quotation is None:
            return False

        for item in quotation.purchase.get_items():
            if not self._can_purchase(item):
                return False
        return True

    def _update_view(self):
        selected = self.search.results.get_selected()
        has_selected = selected is not None
        self.edit_button.set_sensitive(has_selected)
        self.remove_button.set_sensitive(has_selected)
        self.wizard.refresh_next(self._can_order(selected))

    def _run_quote_editor(self):
        store = api.new_store()
        selected = store.fetch(self.search.results.get_selected().purchase)
        retval = run_dialog(QuoteFillingDialog, self.wizard, selected, store)
        store.confirm(retval)
        store.close()
        self._update_view()

    def _remove_quote(self):
        q = self.search.results.get_selected().quotation
        msg = _('Are you sure you want to remove "%s" ?') % q.get_description()
        if not yesno(msg, Gtk.ResponseType.NO,
                     _("Remove quote"), _("Don't remove")):
            return

        store = api.new_store()
        group = store.fetch(q.group)
        quote = store.fetch(q)
        group.remove_item(quote)
        # there is no reason to keep the group if there's no more quotes
        if group.get_items().count() == 0:
            store.remove(group)
        store.confirm(True)
        store.close()
        self.search.refresh()

    #
    # WizardStep hooks
    #

    def next_step(self):
        self.search.save_columns()
        selected = self.search.results.get_selected()
        if selected is None:
            return

        return QuoteGroupItemsSelectionStep(self.wizard, self.store,
                                            selected.group, self)

    #
    # Callbacks
    #

    def _on_searchlist__selection_changed(self, widget, item):
        self._update_view()

    def _on_searchlist__row_activated(self, widget, item):
        self._run_quote_editor()

    def on_edit_button__clicked(self, widget):
        self._run_quote_editor()

    def on_remove_button__clicked(self, widget):
        self._remove_quote()