class PurchaseSelectionStep(BaseWizardStep): gladefile = 'PurchaseSelectionStep' def __init__(self, wizard, store): self._next_step = None BaseWizardStep.__init__(self, store, wizard) self.setup_slaves() def _create_search(self): self.search = SearchSlave(self._get_columns(), restore_name=self.__class__.__name__, store=self.store, search_spec=PurchaseOrderView) self.search.enable_advanced_search() self.attach_slave('searchbar_holder', self.search) executer = self.search.get_query_executer() executer.add_query_callback(self.get_extra_query) self._create_filters() self.search.result_view.set_selection_mode(Gtk.SelectionMode.MULTIPLE) self.search.result_view.connect('selection-changed', self._on_results__selection_changed) self.search.result_view.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', 'identifier_str']) def get_extra_query(self, states): query = PurchaseOrderView.status == PurchaseOrder.ORDER_CONFIRMED # Dont let the user receive purchases from other branches when working # in synchronized mode if (api.sysparam.get_bool('SYNCHRONIZED_MODE') and not api.can_see_all_branches()): branch = api.get_current_branch(self.store) query = And(query, PurchaseOrderView.branch_id == branch.id) return query def _get_columns(self): return [ IdentifierColumn('identifier', title=_('Purchase #'), 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): selected_rows = self.search.result_view.get_selected_rows() can_continue = len( set((v.supplier_id, v.branch_id) for v in selected_rows)) == 1 self.wizard.refresh_next(can_continue) self.details_button.set_sensitive(len(selected_rows) == 1) # # WizardStep hooks # def post_init(self): self._update_view() self.force_validation() def next_step(self): self.search.save_columns() selected_rows = self.search.result_view.get_selected_rows() return ReceivingOrderItemStep(self.store, self.wizard, self, selected_rows) def has_previous_step(self): return False def setup_slaves(self): self._create_search() # # Kiwi callbacks # 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_rows()[0] 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)
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', 'identifier_str']) 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__, store=self.store, search_spec=LoanView) self.search.enable_advanced_search() self.attach_slave('place_holder', self.search) executer = self.search.get_query_executer() 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()
class PurchaseSelectionStep(BaseWizardStep): gladefile = 'PurchaseSelectionStep' def __init__(self, wizard, store): self._next_step = None BaseWizardStep.__init__(self, store, wizard) self.setup_slaves() def _create_search(self): self.search = SearchSlave(self._get_columns(), restore_name=self.__class__.__name__, store=self.store, search_spec=PurchaseOrderView) self.search.enable_advanced_search() self.attach_slave('searchbar_holder', self.search) executer = self.search.get_query_executer() executer.add_query_callback(self.get_extra_query) self._create_filters() self.search.result_view.set_selection_mode(gtk.SELECTION_MULTIPLE) self.search.result_view.connect('selection-changed', self._on_results__selection_changed) self.search.result_view.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', 'identifier_str']) def get_extra_query(self, states): query = PurchaseOrderView.status == PurchaseOrder.ORDER_CONFIRMED # Dont let the user receive purchases from other branches when working # in synchronized mode if api.sysparam.get_bool('SYNCHRONIZED_MODE'): branch = api.get_current_branch(self.store) query = And(query, PurchaseOrderView.branch_id == branch.id) return query 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): selected_rows = self.search.result_view.get_selected_rows() can_continue = len(set((v.supplier_id, v.branch_id) for v in selected_rows)) == 1 self.wizard.refresh_next(can_continue) self.details_button.set_sensitive(len(selected_rows) == 1) # # WizardStep hooks # def post_init(self): self._update_view() self.force_validation() def next_step(self): self.search.save_columns() selected_rows = self.search.result_view.get_selected_rows() return ReceivingOrderItemStep(self.store, self.wizard, self, selected_rows) def has_previous_step(self): return False def setup_slaves(self): self._create_search() # # Kiwi callbacks # 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_rows()[0] 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)
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=None): if store is None: store = api.get_default_store() self.store = store self.window = window self._sensitive_group = dict() self.help_ui = None self.uimanager = self.window.uimanager # 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() 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 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 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_spec is None: raise NotImplementedError if self.results.get_selection_mode() == Gtk.SelectionMode.MULTIPLE: results = self.results.get_selected_rows() or list( self.search.get_last_results()) else: # There are not multiple selection. # We should print the entire list. 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: 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 """ # # 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 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 `<stoqlib.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 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:`stoqlib.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:`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.search.save_filter_settings('app-ui', self.app_name)
class ReceivingSelectionStep(BaseWizardStep): gladefile = 'PurchaseSelectionStep' def __init__(self, wizard, store): self._next_step = None BaseWizardStep.__init__(self, store, wizard) self.setup_slaves() def _create_search(self): self.search = SearchSlave(self._get_columns(), restore_name=self.__class__.__name__, store=self.store, search_spec=PurchaseReceivingView) self.search.enable_advanced_search() self.attach_slave('searchbar_holder', self.search) executer = self.search.get_query_executer() executer.add_query_callback(self.get_extra_query) self._create_filters() self.search.result_view.set_selection_mode(Gtk.SelectionMode.MULTIPLE) self.search.result_view.connect('selection-changed', self._on_results__selection_changed) self.search.result_view.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', 'purchase_identifier']) def get_extra_query(self, states): query = And(Eq(PurchaseReceivingView.purchase_group, None), Eq(PurchaseReceivingView.receiving_invoice, None)) # Dont let the user receive purchases from other branches when working # in synchronized mode if (api.sysparam.get_bool('SYNCHRONIZED_MODE') and not api.can_see_all_branches()): branch = api.get_current_branch(self.store) query = And(query, PurchaseReceivingView.branch_id == branch.id) return query def _get_columns(self): return [IdentifierColumn('identifier', _('Receiving #'), width=140), IdentifierColumn('purchase_identifier', _('Purchase #'), width=110), SearchColumn('packing_number', title=_('Packing Number'), data_type=str, visible=False), SearchColumn('receival_date', _('Receival date'), expand=True, data_type=datetime.date, sorted=True, width=110), SearchColumn('supplier_name', _('Supplier'), data_type=str, expand=True), SearchColumn('responsible_name', _('Responsible'), data_type=str, visible=False, expand=True), SearchColumn('purchase_responsible_name', _('Purchaser'), data_type=str, visible=False, expand=True), SearchColumn('invoice_number', _('Invoice #'), data_type=int, width=80), Column('subtotal', title=_('Products total'), data_type=currency, width=150)] def _update_view(self): selected_rows = self.search.result_view.get_selected_rows() can_continue = len(set((v.supplier_id, v.branch_id) for v in selected_rows)) == 1 self.wizard.refresh_next(can_continue) self.details_button.set_sensitive(len(selected_rows) == 1) # # WizardStep hooks # def post_init(self): self._update_view() self.force_validation() def next_step(self): self.search.save_columns() selected_rows = self.search.result_view.get_selected_rows() return ProductsCostCheckStep(self.wizard, self, self.store, selected_rows) def has_previous_step(self): return False def setup_slaves(self): self._create_search() # # Kiwi callbacks # def _on_results__selection_changed(self, results, purchase_order_view): self.force_validation() self._update_view() def _on_results__row_activated(self, results, receiving_order_view): run_dialog(ReceivingOrderDetailsDialog, self.wizard, self.store, model=receiving_order_view.order) def on_details_button__clicked(self, *args): selected = self.search.results.get_selected_rows()[0] if not selected: raise ValueError('You should have one order selected ' 'at this point, got nothing') run_dialog(ReceivingOrderDetailsDialog, self.wizard, self.store, model=selected.order)
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()
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.RESPONSE_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()
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)
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', 'identifier_str']) def _get_columns(self): return [ IdentifierColumn('identifier', title=_('Loan #'), 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__, store=self.store, search_spec=LoanView) self.search.enable_advanced_search() self.attach_slave('place_holder', self.search) executer = self.search.get_query_executer() 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()
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 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=None): if store is None: store = api.get_default_store() self.store = store self.window = window self._sensitive_group = dict() self.help_ui = None self.uimanager = self.window.uimanager # 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() 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.MESSAGE_WARNING, msg) # # 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, 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 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_spec 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_spec is None: 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 """ # # 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 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 `<stoqlib.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 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, rollback=True): """ See :class:`stoqlib.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:`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.search.save_filter_settings('app-ui', self.app_name)
class ReceivingSelectionStep(BaseWizardStep): gladefile = 'PurchaseSelectionStep' def __init__(self, wizard, store): self._next_step = None BaseWizardStep.__init__(self, store, wizard) self.setup_slaves() def _create_search(self): self.search = SearchSlave(self._get_columns(), restore_name=self.__class__.__name__, store=self.store, search_spec=PurchaseReceivingView) self.search.enable_advanced_search() self.attach_slave('searchbar_holder', self.search) executer = self.search.get_query_executer() executer.add_query_callback(self.get_extra_query) self._create_filters() self.search.result_view.set_selection_mode(Gtk.SelectionMode.MULTIPLE) self.search.result_view.connect('selection-changed', self._on_results__selection_changed) self.search.result_view.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', 'purchase_identifier']) def get_extra_query(self, states): query = And(Eq(PurchaseReceivingView.purchase_group, None), Eq(PurchaseReceivingView.receiving_invoice, None)) # Dont let the user receive purchases from other branches when working # in synchronized mode if (api.sysparam.get_bool('SYNCHRONIZED_MODE') and not api.can_see_all_branches()): branch = api.get_current_branch(self.store) query = And(query, PurchaseReceivingView.branch_id == branch.id) return query def _get_columns(self): return [IdentifierColumn('identifier', _('Receiving #'), width=140), IdentifierColumn('purchase_identifier', _('Purchase #'), width=110), SearchColumn('packing_number', title=_('Packing Number'), data_type=str, visible=False), SearchColumn('receival_date', _('Receival date'), expand=True, data_type=datetime.date, sorted=True, width=110), SearchColumn('supplier_name', _('Supplier'), data_type=str, expand=True), SearchColumn('responsible_name', _('Responsible'), data_type=str, visible=False, expand=True), SearchColumn('purchase_responsible_name', _('Purchaser'), data_type=str, visible=False, expand=True), SearchColumn('invoice_number', _('Invoice #'), data_type=int, width=80), Column('subtotal', title=_('Products total'), data_type=currency, width=150)] def _update_view(self): selected_rows = self.search.result_view.get_selected_rows() can_continue = len(set((v.supplier_id, v.branch_id) for v in selected_rows)) == 1 self.wizard.refresh_next(can_continue) self.details_button.set_sensitive(len(selected_rows) == 1) # # WizardStep hooks # def post_init(self): self._update_view() self.force_validation() def next_step(self): self.search.save_columns() selected_rows = self.search.result_view.get_selected_rows() return ProductsCostCheckStep(self.wizard, self, self.store, selected_rows) def has_previous_step(self): return False def setup_slaves(self): self._create_search() # # Kiwi callbacks # def _on_results__selection_changed(self, results, purchase_order_view): self.force_validation() self._update_view() def _on_results__row_activated(self, results, receiving_order_view): run_dialog(ReceivingOrderDetailsDialog, self.wizard, self.store, model=receiving_order_view.order) def on_details_button__clicked(self, *args): selected = self.search.results.get_selected_rows()[0] if not selected: raise ValueError('You should have one order selected ' 'at this point, got nothing') run_dialog(ReceivingOrderDetailsDialog, self.wizard, self.store, model=selected.order)
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__, store=self.store, search_spec=PurchaseOrderView) self.search.enable_advanced_search() self.attach_slave('searchbar_holder', self.search) executer = self.search.get_query_executer() 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', 'identifier_str']) def get_extra_query(self, states): query = PurchaseOrderView.status == PurchaseOrder.ORDER_CONFIRMED # Dont let the user receive purchases from other branches when working # in synchronized mode if api.sysparam.get_bool('SYNCHRONIZED_MODE'): branch = api.get_current_branch(self.store) query = And(query, PurchaseOrderView.branch_id == branch.id) return query 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)