Example #1
0
    def _open_image_viewer(self):
        assert self._image_viewer is None

        self._image_viewer = SellableImageViewer(size=(325, 325))
        self._update_image_viewer()
        self._image_viewer.toplevel.connect(
            'delete-event', self._on_image_viewer__delete_event)
        self._image_viewer.show_all()
Example #2
0
    def _open_image_viewer(self):
        assert self.image_viewer is None

        self.image_viewer = SellableImageViewer(size=(325, 325))
        self.image_viewer.toplevel.connect(
            'delete-event', self.on_image_viewer_closed)
        self.image_viewer.show_all()

        self._update_widgets()
Example #3
0
 def on_StockPictureViewer__activate(self, button):
     if self.image_viewer:
         self.StockPictureViewer.props.active = False
         self.image_viewer.destroy()
         self.image_viewer = None
     else:
         self.StockPictureViewer.props.active = True
         self.image_viewer = SellableImageViewer()
         selected = self.results.get_selected()
         if selected:
             self.image_viewer.set_sellable(selected.product.sellable)
         self.image_viewer.toplevel.connect("delete-event",
                                            self.on_image_viewer_closed)
         self.image_viewer.toplevel.set_property("visible", True)
Example #4
0
    def _open_image_viewer(self):
        assert self._image_viewer is None

        self._image_viewer = SellableImageViewer(size=(325, 325))
        self._update_image_viewer()
        self._image_viewer.toplevel.connect("delete-event", self._on_image_viewer__delete_event)
        self._image_viewer.show_all()
Example #5
0
    def _open_image_viewer(self):
        assert self.image_viewer is None

        self.image_viewer = SellableImageViewer(size=(325, 325))
        self.image_viewer.toplevel.connect("delete-event", self.on_image_viewer_closed)
        self.image_viewer.show_all()

        self._update_widgets()
Example #6
0
 def on_StockPictureViewer__activate(self, button):
     if self.image_viewer:
         self.StockPictureViewer.props.active = False
         self.image_viewer.destroy()
         self.image_viewer = None
     else:
         self.StockPictureViewer.props.active = True
         self.image_viewer = SellableImageViewer()
         selected = self.results.get_selected()
         if selected:
             self.image_viewer.set_sellable(selected.product.sellable)
         self.image_viewer.toplevel.connect(
             "delete-event", self.on_image_viewer_closed)
         self.image_viewer.toplevel.set_property("visible", True)
Example #7
0
class StockApp(ShellApp):
    app_title = _('Stock')
    gladefile = "stock"
    search_spec = ProductFullStockView
    search_labels = _('Matching:')
    report_table = SimpleProductReport
    pixbuf_converter = converter.get_converter(gtk.gdk.Pixbuf)

    #
    # Application
    #

    def create_actions(self):
        group = get_accels('app.stock')
        actions = [
            ("NewReceiving", STOQ_RECEIVING, _("Order _receival..."),
             group.get('new_receiving')),
            ('NewTransfer', gtk.STOCK_CONVERT, _('Transfer...'),
             group.get('transfer_product')),
            ('NewStockDecrease', None, _('Stock decrease...'),
             group.get('stock_decrease')),
            ('StockInitial', gtk.STOCK_GO_UP, _('Register initial stock...')),
            ("LoanNew", None, _("Loan...")),
            ("LoanClose", None, _("Close loan...")),
            ("SearchPurchaseReceiving", None, _("Received purchases..."),
             group.get('search_receiving'),
             _("Search for received purchase orders")),
            ("SearchProductHistory", None, _("Product history..."),
             group.get('search_product_history'),
             _("Search for product history")),
            ("SearchStockDecrease", None, _("Stock decreases..."), '',
             _("Search for manual stock decreases")),
            ("SearchPurchasedStockItems", None, _("Purchased items..."),
             group.get('search_purchased_stock_items'),
             _("Search for purchased items")),
            ("SearchBrandItems", None, _("Brand items..."),
             group.get('search_brand_items'),
             _("Search for Brand items on stock")),
            ("SearchStockItems", None, _("Stock items..."),
             group.get('search_stock_items'), _("Search for items on stock")),
            ("SearchTransfer", None, _("Transfers..."),
             group.get('search_transfers'), _("Search for stock transfers")),
            ("SearchClosedStockItems", None, _("Closed stock Items..."),
             group.get('search_closed_stock_items'),
             _("Search for closed stock items")),
            ("LoanSearch", None, _("Loans...")),
            ("LoanSearchItems", None, _("Loan items...")),
            ("ProductMenu", None, _("Product")),
            ("ProductStockHistory", gtk.STOCK_INFO, _("History..."),
             group.get('history'),
             _('Show the stock history of the selected product')),
            ("EditProduct", gtk.STOCK_EDIT, _("Edit..."),
             group.get('edit_product'),
             _("Edit the selected product, allowing you to change it's "
               "details")),
        ]
        self.stock_ui = self.add_ui_actions('', actions, filename='stock.xml')

        toggle_actions = [
            ('StockPictureViewer', None, _('Picture viewer'),
             group.get('toggle_picture_viewer')),
        ]
        self.add_ui_actions('', toggle_actions, 'ToggleActions', 'toggle')
        self.set_help_section(_("Stock help"), 'app-stock')

        self.NewReceiving.set_short_label(_("Receive"))
        self.NewTransfer.set_short_label(_("Transfer"))
        self.EditProduct.set_short_label(_("Edit"))
        self.ProductStockHistory.set_short_label(_("History"))
        self.EditProduct.props.is_important = True
        self.ProductStockHistory.props.is_important = True

    def create_ui(self):
        if api.sysparam(self.store).SMART_LIST_LOADING:
            self.search.enable_lazy_search()

        self.popup = self.uimanager.get_widget('/StockSelection')
        self.window.add_new_items([
            self.NewReceiving, self.NewTransfer, self.NewStockDecrease,
            self.LoanNew
        ])
        self.window.add_search_items([
            self.SearchStockItems,
            self.SearchBrandItems,
            self.SearchStockDecrease,
            self.SearchClosedStockItems,
            self.SearchProductHistory,
            self.SearchPurchasedStockItems,
            self.SearchTransfer,
        ])
        self.window.Print.set_tooltip(_("Print a report of these products"))
        self._inventory_widgets = [
            self.NewTransfer, self.NewReceiving, self.StockInitial,
            self.NewStockDecrease, self.LoanNew, self.LoanClose
        ]
        self.register_sensitive_group(self._inventory_widgets,
                                      lambda: not self.has_open_inventory())

        self.image_viewer = None

        self.image = gtk.Image()
        self.edit_button = self.uimanager.get_widget(
            '/toolbar/AppToolbarPH/EditProduct')
        self.edit_button.set_icon_widget(self.image)
        self.image.show()

        self.search.set_summary_label(column='stock',
                                      label=_('<b>Stock Total:</b>'),
                                      format='<b>%s</b>',
                                      parent=self.get_statusbar_message_area())

    def activate(self, refresh=True):
        self.window.NewToolItem.set_tooltip(_("Create a new receiving order"))
        self.window.SearchToolItem.set_tooltip(_("Search for stock items"))

        self.check_open_inventory()
        self._update_widgets()

    def setup_focus(self):
        self.refresh()

    def deactivate(self):
        self.uimanager.remove_ui(self.stock_ui)

    def new_activate(self):
        if not self.NewReceiving.get_sensitive():
            warning(_("You cannot receive a purchase with an open inventory."))
            return
        self._receive_purchase()

    def search_activate(self):
        self.run_dialog(ProductStockSearch, self.store)

    def set_open_inventory(self):
        self.set_sensitive(self._inventory_widgets, False)

    def create_filters(self):
        self.search.set_query(self._query)
        self.set_text_field_columns(['description'])
        self.branch_filter = ComboSearchFilter(_('Show by:'),
                                               self._get_branches())
        self.branch_filter.select(api.get_current_branch(self.store))
        self.add_filter(self.branch_filter, position=SearchFilterPosition.TOP)

    def get_columns(self):
        return [
            SearchColumn('code',
                         title=_('Code'),
                         sorted=True,
                         sort_func=sort_sellable_code,
                         data_type=str,
                         width=130),
            SearchColumn('barcode',
                         title=_("Barcode"),
                         data_type=str,
                         width=130),
            SearchColumn('category_description',
                         title=_("Category"),
                         data_type=str,
                         width=100,
                         visible=False),
            SearchColumn('description',
                         title=_("Description"),
                         data_type=str,
                         expand=True,
                         ellipsize=pango.ELLIPSIZE_END),
            SearchColumn('manufacturer',
                         title=_("Manufacturer"),
                         data_type=str,
                         visible=False),
            SearchColumn('model',
                         title=_("Model"),
                         data_type=str,
                         visible=False),
            SearchColumn('location',
                         title=_("Location"),
                         data_type=str,
                         width=100,
                         visible=False),
            SearchColumn('stock',
                         title=_('Quantity'),
                         data_type=decimal.Decimal,
                         width=100),
            SearchColumn('unit',
                         title=_("Unit"),
                         data_type=str,
                         width=40,
                         visible=False),
            Column('has_image', title=_('Picture'), data_type=bool, width=80),
        ]

    #
    # Private API
    #

    def _query(self, store):
        branch = self.branch_filter.get_state().value
        return self.search_spec.find_by_branch(store, branch)

    def _get_branches(self):
        items = [(b.person.name, b) for b in self.store.find(Branch)]
        if not items:
            raise DatabaseInconsistency('You should have at least one '
                                        'branch on your database.'
                                        'Found zero')
        items.insert(0, [_('All branches'), None])
        return items

    def _update_widgets(self):
        branch = api.get_current_branch(self.store)

        is_main_branch = self.branch_filter.get_state().value is branch
        item = self.results.get_selected()

        sellable = item and item.product.sellable
        if sellable:
            if sellable.has_image:
                thumbnail = sellable.image.thumbnail
                pixbuf = self.pixbuf_converter.from_string(thumbnail)
            else:
                pixbuf = None

            self._update_edit_image(pixbuf)
            if self.image_viewer:
                self.image_viewer.set_sellable(sellable)
        else:
            self._update_edit_image()

        self.set_sensitive([self.EditProduct], bool(item))
        self.set_sensitive([self.ProductStockHistory],
                           bool(item) and is_main_branch)
        # We need more than one branch to be able to do transfers
        # Note that 'all branches' is not a real branch
        has_branches = len(self.branch_filter.combo) > 2

        transfer_active = self.NewTransfer.get_sensitive()
        self.set_sensitive([self.NewTransfer], transfer_active
                           and has_branches)
        self.set_sensitive([self.SearchTransfer], has_branches)

    def _update_edit_image(self, pixbuf=None):
        if not pixbuf:
            self.image.set_from_stock(gtk.STOCK_EDIT,
                                      gtk.ICON_SIZE_LARGE_TOOLBAR)
            return

        # FIXME: get this icon size from settings
        icon_size = 24
        pixbuf = pixbuf.scale_simple(icon_size, icon_size,
                                     gtk.gdk.INTERP_BILINEAR)
        self.image.set_from_pixbuf(pixbuf)

    def _update_filter_slave(self, slave):
        self.refresh()

    def _transfer_stock(self):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(StockTransferWizard, store)
        store.confirm(model)
        store.close()
        self.refresh()

    def _receive_purchase(self):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(ReceivingOrderWizard, store)
        store.confirm(model)
        store.close()
        self.refresh()

    #
    # Callbacks
    #

    def on_image_viewer_closed(self, window, event):
        self.StockPictureViewer.props.active = False
        self.image_viewer = None

    def on_results__has_rows(self, results, product):
        self._update_widgets()

    def on_results__selection_changed(self, results, product):
        self._update_widgets()

    def on_results__right_click(self, results, result, event):
        self.popup.popup(None, None, None, event.button, event.time)

    def on_ProductStockHistory__activate(self, button):
        selected = self.results.get_selected()
        sellable = selected.sellable
        self.run_dialog(ProductStockHistoryDialog,
                        self.store,
                        sellable,
                        branch=self.branch_filter.combo.get_selected())

    def on_EditProduct__activate(self, button):
        selected = self.results.get_selected()
        assert selected

        store = api.new_store()
        product = store.fetch(selected.product)

        model = self.run_dialog(ProductStockEditor, store, product)
        store.confirm(model)
        store.close()

    # Stock

    def on_NewReceiving__activate(self, button):
        self._receive_purchase()

    def on_NewTransfer__activate(self, button):
        self._transfer_stock()

    def on_NewStockDecrease__activate(self, action):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(StockDecreaseWizard, store)
        store.confirm(model)
        store.close()
        self.refresh()

    def on_StockInitial__activate(self, action):
        if self.check_open_inventory():
            return
        branch = self.branch_filter.get_state().value
        store = api.new_store()
        retval = self.run_dialog(InitialStockDialog, store, branch)
        store.confirm(retval)
        store.close()
        self.refresh()

    def on_StockPictureViewer__activate(self, button):
        if self.image_viewer:
            self.StockPictureViewer.props.active = False
            self.image_viewer.destroy()
            self.image_viewer = None
        else:
            self.StockPictureViewer.props.active = True
            self.image_viewer = SellableImageViewer()
            selected = self.results.get_selected()
            if selected:
                self.image_viewer.set_sellable(selected.product.sellable)
            self.image_viewer.toplevel.connect("delete-event",
                                               self.on_image_viewer_closed)
            self.image_viewer.toplevel.set_property("visible", True)

    # Loan

    def on_LoanNew__activate(self, action):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(NewLoanWizard, store)
        store.confirm(model)
        store.close()
        self.refresh()

    def on_LoanClose__activate(self, action):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(CloseLoanWizard, store)
        store.confirm(model)
        store.close()
        self.refresh()

    def on_LoanSearch__activate(self, action):
        self.run_dialog(LoanSearch, self.store)

    def on_LoanSearchItems__activate(self, action):
        self.run_dialog(LoanItemSearch, self.store)

    # Search

    def on_SearchPurchaseReceiving__activate(self, button):
        self.run_dialog(PurchaseReceivingSearch, self.store)

    def on_SearchTransfer__activate(self, action):
        self.run_dialog(TransferOrderSearch, self.store)
        self.refresh()

    def on_SearchPurchasedStockItems__activate(self, action):
        self.run_dialog(PurchasedItemsSearch, self.store)

    def on_SearchStockItems__activate(self, action):
        self.run_dialog(ProductStockSearch, self.store)

    def on_SearchBrandItems__activate(self, action):
        self.run_dialog(ProductBrandSearch, self.store)

    def on_SearchClosedStockItems__activate(self, action):
        self.run_dialog(ProductClosedStockSearch, self.store)

    def on_SearchProductHistory__activate(self, action):
        self.run_dialog(ProductSearchQuantity, self.store)

    def on_SearchStockDecrease__activate(self, action):
        self.run_dialog(StockDecreaseSearch, self.store)
Example #8
0
class StockApp(ShellApp):
    app_title = _('Stock')
    gladefile = "stock"
    search_spec = ProductFullStockView
    search_labels = _('Matching:')
    report_table = SimpleProductReport
    pixbuf_converter = converter.get_converter(GdkPixbuf.Pixbuf)

    #
    # Application
    #

    def create_actions(self):
        group = get_accels('app.stock')
        actions = [
            ("NewReceiving", STOQ_RECEIVING, _("Order _receival..."),
             group.get('new_receiving')),
            ('NewTransfer', Gtk.STOCK_CONVERT, _('Transfer...'),
             group.get('transfer_product')),
            ('NewStockDecrease', None, _('Stock decrease...'),
             group.get('stock_decrease')),
            ('StockInitial', Gtk.STOCK_GO_UP, _('Register initial stock...')),
            ("LoanNew", None, _("Loan...")),
            ("LoanClose", None, _("Close loan...")),
            ("SearchPurchaseReceiving", None, _("Received purchases..."),
             group.get('search_receiving'),
             _("Search for received purchase orders")),
            ("SearchProductHistory", None, _("Product history..."),
             group.get('search_product_history'),
             _("Search for product history")),
            ("SearchStockDecrease", None, _("Stock decreases..."), '',
             _("Search for manual stock decreases")),
            ("SearchPurchasedStockItems", None, _("Purchased items..."),
             group.get('search_purchased_stock_items'),
             _("Search for purchased items")),
            ("SearchBrandItems", None, _("Brand items..."),
             group.get('search_brand_items'),
             _("Search for brand items on stock")),
            ("SearchBrandItemsByBranch", None, _("Brand item by branch..."),
             group.get('search_brand_by_branch'),
             _("Search for brand items by branch on stock")),
            ("SearchBatchItems", None, _("Batch items..."),
             group.get('search_batch_items'),
             _("Search for batch items on stock")),
            ("SearchStockItems", None, _("Stock items..."),
             group.get('search_stock_items'),
             _("Search for items on stock")),
            ("SearchTransfer", None, _("Transfers..."),
             group.get('search_transfers'),
             _("Search for stock transfers")),
            ("SearchClosedStockItems", None, _("Closed stock Items..."),
             group.get('search_closed_stock_items'),
             _("Search for closed stock items")),
            ("LoanSearch", None, _("Loans...")),
            ("LoanSearchItems", None, _("Loan items...")),
            ("SearchTransferItems", None, _("Transfer items...")),
            ("SearchReturnedItems", None, _("Returned items...")),
            ("SearchPendingReturnedSales", None, _("Pending returned sales...")),
            ("ProductMenu", None, _("Product")),
            ("PrintLabels", None, _("Print labels...")),
            ("ManageStock", None, _("Manage stock...")),
            ("ProductStockHistory", Gtk.STOCK_INFO, _("History..."),
             group.get('history'),
             _('Show the stock history of the selected product')),
            ("EditProduct", Gtk.STOCK_EDIT, _("Edit..."),
             group.get('edit_product'),
             _("Edit the selected product, allowing you to change it's "
               "details")),
        ]
        self.stock_ui = self.add_ui_actions('', actions,
                                            filename='stock.xml')

        toggle_actions = [
            ('StockPictureViewer', None, _('Picture viewer'),
             group.get('toggle_picture_viewer')),
        ]
        self.add_ui_actions('', toggle_actions, 'ToggleActions',
                            'toggle')
        self.set_help_section(_("Stock help"), 'app-stock')

        self.NewReceiving.set_short_label(_("Receive"))
        self.NewTransfer.set_short_label(_("Transfer"))
        self.EditProduct.set_short_label(_("Edit"))
        self.ProductStockHistory.set_short_label(_("History"))
        self.EditProduct.props.is_important = True
        self.ProductStockHistory.props.is_important = True

    def create_ui(self):
        if api.sysparam.get_bool('SMART_LIST_LOADING'):
            self.search.enable_lazy_search()

        self.popup = self.uimanager.get_widget('/StockSelection')
        self.window.add_new_items([self.NewReceiving, self.NewTransfer,
                                   self.NewStockDecrease, self.LoanNew])
        self.window.add_search_items([
            self.SearchStockItems,
            self.SearchBrandItems,
            self.SearchStockDecrease,
            self.SearchClosedStockItems,
            self.SearchProductHistory,
            self.SearchPurchasedStockItems,
            self.SearchTransfer,
        ])
        self.window.Print.set_tooltip(
            _("Print a report of these products"))
        self._inventory_widgets = [self.NewTransfer, self.NewReceiving,
                                   self.StockInitial, self.NewStockDecrease,
                                   self.LoanNew, self.LoanClose]
        self.register_sensitive_group(self._inventory_widgets,
                                      lambda: not self.has_open_inventory())

        self.image_viewer = None

        self.image = Gtk.Image()
        self.edit_button = self.uimanager.get_widget('/toolbar/AppToolbarPH/EditProduct')
        self.edit_button.set_icon_widget(self.image)
        self.image.show()

        self.search.set_summary_label(column='stock',
                                      label=_('<b>Stock Total:</b>'),
                                      format='<b>%s</b>',
                                      parent=self.get_statusbar_message_area())

    def activate(self, refresh=True):
        self.window.NewToolItem.set_tooltip(
            _("Create a new receiving order"))
        self.window.SearchToolItem.set_tooltip(
            _("Search for stock items"))

        if refresh:
            self.refresh()

        open_inventory = self.check_open_inventory()

        if not open_inventory:
            self.transfers_bar = self._create_pending_info_message()
            self.returned_bar = self._create_pending_returned_sale_message()
        else:
            self.transfers_bar = None
            self.returned_bar = None

        self._update_widgets()

        self.search.focus_search_entry()

    def deactivate(self):
        if self.transfers_bar:
            self.transfers_bar.hide()
        if self.returned_bar:
            self.returned_bar.hide()

        self.uimanager.remove_ui(self.stock_ui)
        self._close_image_viewer()

    def new_activate(self):
        if not self.NewReceiving.get_sensitive():
            warning(_("You cannot receive a purchase with an open inventory."))
            return
        self._receive_purchase()

    def search_activate(self):
        self.run_dialog(ProductStockSearch, self.store)

    def set_open_inventory(self):
        self.set_sensitive(self._inventory_widgets, False)

    def create_filters(self):
        self.search.set_query(self._query)
        self.set_text_field_columns(['description', 'code', 'barcode',
                                     'category_description', 'manufacturer'])
        branches = Branch.get_active_branches(self.store)
        self.branch_filter = ComboSearchFilter(
            _('Show by:'), api.for_combo(branches, empty=_("All branches")))
        self.branch_filter.select(api.get_current_branch(self.store))
        self.add_filter(self.branch_filter, position=SearchFilterPosition.TOP)

    def get_columns(self):
        return [SearchColumn('code', title=_('Code'), sorted=True,
                             sort_func=sort_sellable_code,
                             data_type=str, width=130),
                SearchColumn('barcode', title=_("Barcode"), data_type=str,
                             width=130),
                SearchColumn('category_description', title=_("Category"),
                             data_type=str, width=100, visible=False),
                SearchColumn('description', title=_("Description"),
                             data_type=str, expand=True,
                             ellipsize=Pango.EllipsizeMode.END),
                SearchColumn('manufacturer', title=_("Manufacturer"),
                             data_type=str, visible=False),
                SearchColumn('brand', title=_("Brand"),
                             data_type=str, visible=False),
                SearchColumn('model', title=_("Model"),
                             data_type=str, visible=False),
                SearchColumn('location', title=_("Location"), data_type=str,
                             width=100, visible=False),
                QuantityColumn('stock', title=_('Quantity'), width=100,
                               use_having=True),
                SearchColumn('has_image', title=_('Picture'),
                             data_type=bool, width=80),
                ]

    #
    # Private API
    #

    def _open_image_viewer(self):
        assert self.image_viewer is None

        self.image_viewer = SellableImageViewer(size=(325, 325))
        self.image_viewer.toplevel.connect(
            'delete-event', self.on_image_viewer_closed)
        self.image_viewer.show_all()

        self._update_widgets()

    def _close_image_viewer(self):
        if self.image_viewer is None:
            return

        self.image_viewer.destroy()
        self.image_viewer = None

    def _query(self, store):
        branch = self.branch_filter.get_state().value
        return self.search_spec.find_by_branch(store, branch)

    def _update_widgets(self):
        branch = api.get_current_branch(self.store)

        is_main_branch = self.branch_filter.get_state().value is branch
        item = self.results.get_selected()

        sellable = item and item.product.sellable
        if sellable:
            if item.has_image:
                # XXX:Workaround for a bug caused by the image domain refactoring
                # which left some existent thumbnail as None
                thumbnail = sellable.image.thumbnail
                if thumbnail is None:
                    # Create new store to create the thumbnail
                    with api.new_store() as new_store:
                        image = sellable.image
                        image = new_store.fetch(image)
                        size = (Image.THUMBNAIL_SIZE_WIDTH, Image.THUMBNAIL_SIZE_HEIGHT)
                        image.thumbnail = get_thumbnail(image.image, size)
                        thumbnail = image.thumbnail
                pixbuf = self.pixbuf_converter.from_string(thumbnail)
            else:
                pixbuf = None

            self._update_edit_image(pixbuf)
            if self.image_viewer:
                self.image_viewer.set_sellable(sellable)
        else:
            self._update_edit_image()

        # Always let the user choose the manage stock option and do a proper
        # check there (showing a warning if he can't)
        self.set_sensitive([self.ManageStock], bool(item))
        self.set_sensitive([self.EditProduct, self.PrintLabels], bool(item))
        self.set_sensitive([self.ProductStockHistory],
                           bool(item) and is_main_branch)
        # We need more than one branch to be able to do transfers
        # Note that 'all branches' is not a real branch
        has_branches = len(self.branch_filter.combo) > 2

        transfer_active = self.NewTransfer.get_sensitive()
        self.set_sensitive([self.NewTransfer],
                           transfer_active and has_branches)
        # Building a list of searches that we must disable if there is no
        # branches other than the main company
        searches = [self.SearchTransfer, self.SearchTransferItems,
                    self.SearchPendingReturnedSales]
        self.set_sensitive(searches, has_branches)

    def _update_edit_image(self, pixbuf=None):
        if not pixbuf:
            self.image.set_from_stock(Gtk.STOCK_EDIT,
                                      Gtk.IconSize.LARGE_TOOLBAR)
            return

        # FIXME: get this icon size from settings
        icon_size = 24
        pixbuf = pixbuf.scale_simple(icon_size, icon_size,
                                     GdkPixbuf.InterpType.BILINEAR)
        self.image.set_from_pixbuf(pixbuf)

    def _update_filter_slave(self, slave):
        self.refresh()

    def _transfer_stock(self):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(StockTransferWizard, store)
        store.confirm(model)
        store.close()
        self.refresh()

    def _receive_purchase(self):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(ReceivingOrderWizard, store)
        store.confirm(model)
        store.close()
        self.refresh()

    def _create_pending_info_message(self):
        branch = api.get_current_branch(self.store)
        n_transfers = TransferOrder.get_pending_transfers(self.store, branch).count()

        if not n_transfers:
            return None

        msg = stoqlib_ngettext(_(u"You have %s incoming transfer"),
                               _(u"You have %s incoming transfers"),
                               n_transfers) % n_transfers
        info_bar = self.window.add_info_bar(Gtk.MessageType.QUESTION, msg)
        button = info_bar.add_button(_(u"Receive"), Gtk.ResponseType.OK)
        button.connect('clicked', self._on_info_transfers__clicked)

        return info_bar

    def _create_pending_returned_sale_message(self):
        branch = api.get_current_branch(self.store)
        n_returned = ReturnedSale.get_pending_returned_sales(self.store, branch).count()

        if not n_returned:
            return None

        msg = stoqlib_ngettext(_(u"You have %s returned sale to receive"),
                               _(u"You have %s returned sales to receive"),
                               n_returned) % n_returned
        info_returned_bar = self.window.add_info_bar(Gtk.MessageType.QUESTION, msg)
        button = info_returned_bar.add_button(_(u"Returned sale"), Gtk.ResponseType.OK)
        button.connect('clicked', self._on_info_returned_sales__clicked)

        return info_returned_bar

    def _search_transfers(self):
        branch = api.get_current_branch(self.store)
        self.run_dialog(TransferOrderSearch, self.store)

        # After the search is closed we may want to update , or even hide the
        # message, if there is no pending transfer to receive
        if self.transfers_bar:
            n_transfers = TransferOrder.get_pending_transfers(self.store, branch).count()

            if n_transfers > 0:
                msg = stoqlib_ngettext(_(u"You have %s incoming transfer"),
                                       _(u"You have %s incoming transfers"),
                                       n_transfers) % n_transfers
                self.transfers_bar.set_message(msg)
            else:
                self.transfers_bar.hide()
        self.refresh()

    def _search_pending_returned_sales(self):
        with api.new_store() as store:
            self.run_dialog(PendingReturnedSaleSearch, store)

        branch = api.get_current_branch(self.store)
        # After the search is closed we may want to update , or even hide the
        # message, if there is no pending returned sale to receive
        if self.returned_bar:
            n_returned = ReturnedSale.get_pending_returned_sales(self.store, branch).count()

            if n_returned > 0:
                msg = stoqlib_ngettext(_(u"You have %s returned sale to receive"),
                                       _(u"You have %s returned sales to receive"),
                                       n_returned) % n_returned
                self.returned_bar.set_message(msg)
            else:
                self.returned_bar.hide()
        self.refresh()

    #
    # Callbacks
    #

    def on_image_viewer_closed(self, window, event):
        self.image_viewer = None
        self.StockPictureViewer.set_active(False)

    def on_results__has_rows(self, results, product):
        self._update_widgets()

    def on_results__selection_changed(self, results, product):
        self._update_widgets()

    def on_results__right_click(self, results, result, event):
        self.popup.popup(None, None, None, None,
                         event.button.button, event.time)

    def on_ProductStockHistory__activate(self, button):
        selected = self.results.get_selected()
        sellable = selected.sellable
        self.run_dialog(ProductStockHistoryDialog, self.store, sellable,
                        branch=self.branch_filter.combo.get_selected())

    def on_ManageStock__activate(self, action):
        user = api.get_current_user(self.store)
        if not user.profile.check_app_permission(u'inventory'):
            return warning(_('Only users with access to the inventory app can'
                             ' change the stock quantity'))

        product = self.results.get_selected().product
        if product.is_grid:
            return warning(_("Can't change stock quantity of a grid parent"))

        if product.storable and product.storable.is_batch:
            return warning(_("It's not possible to change the stock quantity of"
                             " a batch product"))

        branch = self.branch_filter.combo.get_selected()
        if not branch:
            return warning(_('You must select a branch first'))

        with api.new_store() as store:
            self.run_dialog(ProductStockQuantityEditor, store,
                            store.fetch(product), branch=branch)

        if store.committed:
            self.refresh()

    def on_PrintLabels__activate(self, button):
        selected = self.results.get_selected()
        sellable = selected.sellable
        label_data = self.run_dialog(PrintLabelEditor, None, self.store,
                                     sellable)
        if label_data:
            print_labels(label_data, self.store)

    def on_EditProduct__activate(self, button):
        selected = self.results.get_selected()
        assert selected

        store = api.new_store()
        product = store.fetch(selected.product)

        model = self.run_dialog(ProductStockEditor, store, product)
        store.confirm(model)
        store.close()
        if model:
            self.refresh()

    def _on_info_transfers__clicked(self, button):
        self._search_transfers()

    def _on_info_returned_sales__clicked(self, button):
        self._search_pending_returned_sales()

    # Stock

    def on_NewReceiving__activate(self, button):
        self._receive_purchase()

    def on_NewTransfer__activate(self, button):
        self._transfer_stock()

    def on_NewStockDecrease__activate(self, action):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(StockDecreaseWizard, store)
        store.confirm(model)
        store.close()
        self.refresh()

    def on_StockInitial__activate(self, action):
        if self.check_open_inventory():
            return

        with api.new_store() as store:
            self.run_dialog(InitialStockDialog, store)

        if store.committed:
            self.refresh()

    def on_StockPictureViewer__toggled(self, button):
        if button.get_active():
            self._open_image_viewer()
        else:
            self._close_image_viewer()

    # Loan

    def on_LoanNew__activate(self, action):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(NewLoanWizard, store)
        store.confirm(model)
        store.close()
        self.refresh()

    def on_LoanClose__activate(self, action):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(CloseLoanWizard, store)
        store.confirm(model)
        store.close()
        self.refresh()

    def on_LoanSearch__activate(self, action):
        self.run_dialog(LoanSearch, self.store)

    def on_LoanSearchItems__activate(self, action):
        self.run_dialog(LoanItemSearch, self.store)

    # Search

    def on_SearchPurchaseReceiving__activate(self, button):
        self.run_dialog(PurchaseReceivingSearch, self.store)

    def on_SearchTransfer__activate(self, action):
        self._search_transfers()

    def on_SearchTransferItems__activate(self, action):
        self.run_dialog(TransferItemSearch, self.store)

    def on_SearchPendingReturnedSales__activate(self, action):
        self._search_pending_returned_sales()

    def on_SearchReturnedItems__activate(self, action):
        self.run_dialog(ReturnedItemSearch, self.store)

    def on_SearchPurchasedStockItems__activate(self, action):
        self.run_dialog(PurchasedItemsSearch, self.store)

    def on_SearchStockItems__activate(self, action):
        self.run_dialog(ProductStockSearch, self.store)

    def on_SearchBrandItems__activate(self, action):
        self.run_dialog(ProductBrandSearch, self.store)

    def on_SearchBrandItemsByBranch__activate(self, action):
        self.run_dialog(ProductBrandByBranchSearch, self.store)

    def on_SearchBatchItems__activate(self, action):
        self.run_dialog(ProductBatchSearch, self.store)

    def on_SearchClosedStockItems__activate(self, action):
        self.run_dialog(ProductClosedStockSearch, self.store)

    def on_SearchProductHistory__activate(self, action):
        self.run_dialog(ProductSearchQuantity, self.store)

    def on_SearchStockDecrease__activate(self, action):
        self.run_dialog(StockDecreaseSearch, self.store)
Example #9
0
class SellableSearch(SearchEditor):
    title = _('Item search')
    size = (800, 500)
    model_list_lookup_attr = 'product_id'
    footer_ok_label = _('_Select item')
    search_spec = SellableFullStockView
    editor_class = None
    exclude_delivery_service = True
    text_field_columns = [
        SellableFullStockView.description,
        SellableFullStockView.category_description,
        SellableFullStockView.barcode, SellableFullStockView.code
    ]

    def __init__(self,
                 store,
                 hide_footer=False,
                 hide_toolbar=True,
                 selection_mode=None,
                 search_str=None,
                 search_spec=None,
                 search_query=None,
                 double_click_confirm=True,
                 info_message=None,
                 show_closed_items=False):
        """
        :param store: a store
        :param hide_footer: do I have to hide the dialog footer?
        :param hide_toolbar: do I have to hide the dialog toolbar?
        :param selection_mode: the kiwi list selection mode
        :param search_str: If this search should already filter for some string
        :param double_click_confirm: If double click a item in the list should
            automatically confirm
        :param show_closed_items: if this parameter is True, shows sellable with
            status closed
        """
        if selection_mode is None:
            selection_mode = Gtk.SelectionMode.BROWSE

        self._image_viewer = None
        self._first_search = True
        self._first_search_string = search_str
        self._search_query = search_query
        self._show_closed_items = show_closed_items
        self._delivery_sellable = sysparam.get_object(
            store, 'DELIVERY_SERVICE').sellable

        SearchEditor.__init__(self,
                              store,
                              search_spec=search_spec,
                              editor_class=self.editor_class,
                              hide_footer=hide_footer,
                              hide_toolbar=hide_toolbar,
                              selection_mode=selection_mode,
                              double_click_confirm=double_click_confirm)

        if info_message:
            self.set_message(info_message)

        if search_str:
            self.set_searchbar_search_string(search_str)
            self.search.refresh()

    #
    #  SearchEditor
    #

    def key_shift_Return(self):
        self.confirm()

    def key_control_Return(self):
        self.confirm()

    def key_shift_KP_Enter(self):
        self.confirm()

    def key_control_KP_Enter(self):
        self.confirm()

    def close(self):
        # Make sure image viewer gets closed when this search closes
        self._close_image_viewer()
        super(SellableSearch, self).close()

    def confirm(self, retval=None):
        # FIXME: This is a hack, we need to do proper validation in the parent
        if retval is None and not self.ok_button.get_sensitive():
            return
        super(SellableSearch, self).confirm(retval=retval)

    def setup_widgets(self):
        self.image_viewer_toggler = Gtk.CheckMenuItem(
            label=_("Show image viewer"))
        self.popup = Gtk.Menu()
        self.popup.add(self.image_viewer_toggler)
        self.popup.show_all()

        if hasattr(self.search_spec, 'product'):
            self.branch_stock_button = self.add_button(
                label=_('Stock details'))
            self.branch_stock_button.show()
            self.branch_stock_button.set_sensitive(False)
        else:
            self.branch_stock_button = None

    def create_filters(self):
        self.search.set_query(self.executer_query)

    def get_columns(self):
        columns = [
            SearchColumn('code', title=_(u'Code'), data_type=str),
            SearchColumn('barcode',
                         title=_('Barcode'),
                         data_type=str,
                         sort_func=sort_sellable_code,
                         width=80),
            SearchColumn('category_description',
                         title=_('Category'),
                         data_type=str,
                         width=120),
            SearchColumn('description',
                         title=_('Description'),
                         data_type=str,
                         expand=True,
                         sorted=True),
            SearchColumn('location',
                         title=_('Location'),
                         data_type=str,
                         visible=False),
            SearchColumn('manufacturer',
                         title=_('Manufacturer'),
                         data_type=str,
                         visible=False),
            SearchColumn('model',
                         title=_('Model'),
                         data_type=str,
                         visible=False)
        ]

        if hasattr(self.search_spec, 'price'):
            columns.append(
                SearchColumn('price',
                             title=_(u'Price'),
                             data_type=currency,
                             visible=True))

        if hasattr(self.search_spec, 'minimum_quantity'):
            columns.append(
                SearchColumn('minimum_quantity',
                             title=_(u'Minimum Qty'),
                             data_type=Decimal,
                             visible=False))

        if hasattr(self.search_spec, 'stock'):
            columns.append(QuantityColumn('stock', title=_(u'In Stock')))

        return columns

    def update_widgets(self):
        sellable_view = self.results.get_selected()
        self.set_edit_button_sensitive(bool(sellable_view))
        self.ok_button.set_sensitive(bool(sellable_view))

        if self.branch_stock_button is not None:
            self.branch_stock_button.set_sensitive(
                bool(self._get_selected_storable()))

    def executer_query(self, store):
        # If the viewable has a find_by_branch method, then lets use it instead
        # of the generic find, to show only the stock for the current branch.
        if hasattr(self.search_spec, 'find_by_branch'):
            branch = api.get_current_branch(store)
            results = self.search_spec.find_by_branch(store, branch)
        else:
            results = store.find(self.search_spec)

        if self._search_query:
            results = results.find(self._search_query)

        if not self._show_closed_items:
            results = results.find(
                self.search_spec.status == Sellable.STATUS_AVAILABLE)

        if self.exclude_delivery_service:
            results = results.find(
                self.search_spec.id != self._delivery_sellable.id)

        return results

    #
    #  Private
    #

    def _get_selected_storable(self):
        product = getattr(self.results.get_selected(), 'product', None)
        if product and self.fast_iter:
            product = self.store.get(Product, product.id)

        return product and product.storable

    def _open_image_viewer(self):
        assert self._image_viewer is None

        self._image_viewer = SellableImageViewer(size=(325, 325))
        self._update_image_viewer()
        self._image_viewer.toplevel.connect(
            'delete-event', self._on_image_viewer__delete_event)
        self._image_viewer.show_all()

    def _close_image_viewer(self):
        if self._image_viewer is None:
            return

        self._image_viewer.destroy()
        self._image_viewer = None

    def _update_image_viewer(self):
        if self._image_viewer is None:
            return

        row = self.results.get_selected()
        self._image_viewer.set_sellable(row and row.sellable)

    #
    # Callbacks
    #

    def _on_image_viewer__delete_event(self, window, event):
        self._image_viewer = None
        self.image_viewer_toggler.set_active(False)

    def on_image_viewer_toggler__toggled(self, item):
        if item.get_active():
            self._open_image_viewer()
        else:
            self._close_image_viewer()

    def on_results__right_click(self, klist, row, event):
        self.popup.popup(None, None, None, None, event.button.button,
                         event.time)

    def on_results__selection_changed(self, klist, row):
        self._update_image_viewer()

    def on_branch_stock_button__clicked(self, widget):
        from stoqlib.gui.search.productsearch import ProductBranchSearch
        storable = self._get_selected_storable()
        if not storable:
            return

        self.run_dialog(ProductBranchSearch, self, self.store, storable)
Example #10
0
class StockApp(ShellApp):
    app_title = _('Stock')
    gladefile = "stock"
    search_table = ProductFullStockView
    search_labels = _('Matching:')
    report_table = SimpleProductReport
    pixbuf_converter = converter.get_converter(gtk.gdk.Pixbuf)

    #
    # Application
    #

    def create_actions(self):
        group = get_accels('app.stock')
        actions = [
            ("NewReceiving", STOQ_RECEIVING, _("Order _receival..."),
             group.get('new_receiving')),
            ('NewTransfer', gtk.STOCK_CONVERT, _('Transfer...'),
             group.get('transfer_product')),
            ('NewStockDecrease', None, _('Stock decrease...'),
             group.get('stock_decrease')),
            ('StockInitial', gtk.STOCK_GO_UP, _('Register initial stock...')),
            ("LoanNew", None, _("Loan...")),
            ("LoanClose", None, _("Close loan...")),
            ("SearchPurchaseReceiving", None, _("Received purchases..."),
             group.get('search_receiving'),
             _("Search for received purchase orders")),
            ("SearchProductHistory", None, _("Product history..."),
             group.get('search_product_history'),
             _("Search for product history")),
            ("SearchStockDecrease", None, _("Stock decreases..."), '',
             _("Search for manual stock decreases")),
            ("SearchPurchasedStockItems", None, _("Purchased items..."),
             group.get('search_purchased_stock_items'),
             _("Search for purchased items")),
            ("SearchStockItems", None, _("Stock items..."),
             group.get('search_stock_items'),
             _("Search for items on stock")),
            ("SearchTransfer", None, _("Transfers..."),
             group.get('search_transfers'),
             _("Search for stock transfers")),
            ("SearchClosedStockItems", None, _("Closed stock Items..."),
             group.get('search_closed_stock_items'),
             _("Search for closed stock items")),
            ("LoanSearch", None, _("Loans...")),
            ("LoanSearchItems", None, _("Loan items...")),
            ("ProductMenu", None, _("Product")),
            ("ProductStockHistory", gtk.STOCK_INFO, _("History..."),
             group.get('history'),
             _('Show the stock history of the selected product')),
            ("EditProduct", gtk.STOCK_EDIT, _("Edit..."),
             group.get('edit_product'),
             _("Edit the selected product, allowing you to change it's "
               "details")),
        ]
        self.stock_ui = self.add_ui_actions('', actions,
                                            filename='stock.xml')

        toggle_actions = [
            ('StockPictureViewer', None, _('Picture viewer'),
             group.get('toggle_picture_viewer')),
        ]
        self.add_ui_actions('', toggle_actions, 'ToggleActions',
                            'toggle')
        self.set_help_section(_("Stock help"), 'app-stock')

        self.NewReceiving.set_short_label(_("Receive"))
        self.NewTransfer.set_short_label(_("Transfer"))
        self.EditProduct.set_short_label(_("Edit"))
        self.ProductStockHistory.set_short_label(_("History"))
        self.EditProduct.props.is_important = True
        self.ProductStockHistory.props.is_important = True

    def create_ui(self):
        if api.sysparam(self.store).SMART_LIST_LOADING:
            self.search.enable_lazy_search()

        self.popup = self.uimanager.get_widget('/StockSelection')
        self.window.add_new_items([self.NewReceiving, self.NewTransfer,
                                   self.NewStockDecrease, self.LoanNew])
        self.window.add_search_items([
            self.SearchStockItems,
            self.SearchStockDecrease,
            self.SearchClosedStockItems,
            self.SearchProductHistory,
            self.SearchPurchasedStockItems,
            self.SearchTransfer,
        ])
        self.window.Print.set_tooltip(
            _("Print a report of these products"))
        self._inventory_widgets = [self.NewTransfer, self.NewReceiving,
                                   self.StockInitial, self.NewStockDecrease,
                                   self.LoanNew, self.LoanClose]
        self.register_sensitive_group(self._inventory_widgets,
                                      lambda: not self.has_open_inventory())

        self.image_viewer = None

        self.image = gtk.Image()
        self.edit_button = self.uimanager.get_widget('/toolbar/AppToolbarPH/EditProduct')
        self.edit_button.set_icon_widget(self.image)
        self.image.show()

        self.search.set_summary_label(column='stock',
                                      label=_('<b>Stock Total:</b>'),
                                      format='<b>%s</b>',
                                      parent=self.get_statusbar_message_area())

    def activate(self, params):
        self.window.NewToolItem.set_tooltip(
            _("Create a new receiving order"))
        self.window.SearchToolItem.set_tooltip(
            _("Search for stock items"))

        self.check_open_inventory()
        self._update_widgets()

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

    def deactivate(self):
        self.uimanager.remove_ui(self.stock_ui)

    def new_activate(self):
        if not self.NewReceiving.get_sensitive():
            warning(_("You cannot receive a purchase with an open inventory."))
            return
        self._receive_purchase()

    def search_activate(self):
        self.run_dialog(ProductStockSearch, self.store)

    def set_open_inventory(self):
        self.set_sensitive(self._inventory_widgets, False)

    def create_filters(self):
        self.executer.set_query(self._query)
        self.set_text_field_columns(['description'])
        self.branch_filter = ComboSearchFilter(
            _('Show by:'), self._get_branches())
        self.branch_filter.select(api.get_current_branch(self.store))
        self.add_filter(self.branch_filter, position=SearchFilterPosition.TOP)

    def get_columns(self):
        return [SearchColumn('code', title=_('Code'), sorted=True,
                             sort_func=sort_sellable_code,
                             data_type=str, width=130),
                SearchColumn('barcode', title=_("Barcode"), data_type=str,
                             width=130),
                SearchColumn('category_description', title=_("Category"),
                             data_type=str, width=100, visible=False),
                SearchColumn('description', title=_("Description"),
                             data_type=str, expand=True,
                             ellipsize=pango.ELLIPSIZE_END),
                SearchColumn('manufacturer', title=_("Manufacturer"),
                             data_type=str, visible=False),
                SearchColumn('model', title=_("Model"),
                             data_type=str, visible=False),
                SearchColumn('location', title=_("Location"), data_type=str,
                             width=100, visible=False),
                SearchColumn('stock', title=_('Quantity'),
                             data_type=decimal.Decimal, width=100),
                SearchColumn('unit', title=_("Unit"), data_type=str,
                             width=40, visible=False),
                Column('has_image', title=_('Picture'),
                       data_type=bool, width=80),
                ]

    #
    # Private API
    #

    def _query(self, store):
        branch = self.branch_filter.get_state().value
        return self.search_table.find_by_branch(store, branch)

    def _get_branches(self):
        items = [(b.person.name, b)
                 for b in self.store.find(Branch)]
        if not items:
            raise DatabaseInconsistency('You should have at least one '
                                        'branch on your database.'
                                        'Found zero')
        items.insert(0, [_('All branches'), None])
        return items

    def _update_widgets(self):
        branch = api.get_current_branch(self.store)

        is_main_branch = self.branch_filter.get_state().value is branch
        item = self.results.get_selected()

        sellable = item and item.product.sellable
        if sellable:
            if sellable.has_image:
                thumbnail = sellable.image.thumbnail
                pixbuf = self.pixbuf_converter.from_string(thumbnail)
            else:
                pixbuf = None

            self._update_edit_image(pixbuf)
            if self.image_viewer:
                self.image_viewer.set_sellable(sellable)
        else:
            self._update_edit_image()

        self.set_sensitive([self.EditProduct], bool(item))
        self.set_sensitive([self.ProductStockHistory],
                           bool(item) and is_main_branch)
        # We need more than one branch to be able to do transfers
        # Note that 'all branches' is not a real branch
        has_branches = len(self.branch_filter.combo) > 2

        transfer_active = self.NewTransfer.get_sensitive()
        self.set_sensitive([self.NewTransfer],
                           transfer_active and has_branches)
        self.set_sensitive([self.SearchTransfer], has_branches)

    def _update_edit_image(self, pixbuf=None):
        if not pixbuf:
            self.image.set_from_stock(gtk.STOCK_EDIT,
                                      gtk.ICON_SIZE_LARGE_TOOLBAR)
            return

        # FIXME: get this icon size from settings
        icon_size = 24
        pixbuf = pixbuf.scale_simple(icon_size, icon_size,
                                     gtk.gdk.INTERP_BILINEAR)
        self.image.set_from_pixbuf(pixbuf)

    def _update_filter_slave(self, slave):
        self.refresh()

    def _transfer_stock(self):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(StockTransferWizard, store)
        store.confirm(model)
        store.close()
        self.search.refresh()

    def _receive_purchase(self):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(ReceivingOrderWizard, store)
        store.confirm(model)
        store.close()
        self.search.refresh()

    #
    # Callbacks
    #

    def on_image_viewer_closed(self, window, event):
        self.StockPictureViewer.props.active = False
        self.image_viewer = None

    def on_results__has_rows(self, results, product):
        self._update_widgets()

    def on_results__selection_changed(self, results, product):
        self._update_widgets()

    def on_results__right_click(self, results, result, event):
        self.popup.popup(None, None, None, event.button, event.time)

    def on_ProductStockHistory__activate(self, button):
        selected = self.results.get_selected()
        sellable = selected.sellable
        self.run_dialog(ProductStockHistoryDialog, self.store, sellable,
                        branch=self.branch_filter.combo.get_selected())

    def on_EditProduct__activate(self, button):
        selected = self.results.get_selected()
        assert selected

        store = api.new_store()
        product = store.fetch(selected.product)

        model = self.run_dialog(ProductStockEditor, store, product)
        store.confirm(model)
        store.close()

    # Stock

    def on_NewReceiving__activate(self, button):
        self._receive_purchase()

    def on_NewTransfer__activate(self, button):
        self._transfer_stock()

    def on_NewStockDecrease__activate(self, action):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(StockDecreaseWizard, store)
        store.confirm(model)
        store.close()
        self.search.refresh()

    def on_StockInitial__activate(self, action):
        if self.check_open_inventory():
            return
        branch = self.branch_filter.get_state().value
        store = api.new_store()
        retval = self.run_dialog(InitialStockDialog, store, branch)
        store.confirm(retval)
        store.close()
        self.search.refresh()

    def on_StockPictureViewer__activate(self, button):
        if self.image_viewer:
            self.StockPictureViewer.props.active = False
            self.image_viewer.destroy()
            self.image_viewer = None
        else:
            self.StockPictureViewer.props.active = True
            self.image_viewer = SellableImageViewer()
            selected = self.results.get_selected()
            if selected:
                self.image_viewer.set_sellable(selected.product.sellable)
            self.image_viewer.toplevel.connect(
                "delete-event", self.on_image_viewer_closed)
            self.image_viewer.toplevel.set_property("visible", True)

    # Loan

    def on_LoanNew__activate(self, action):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(NewLoanWizard, store)
        store.confirm(model)
        store.close()
        self.search.refresh()

    def on_LoanClose__activate(self, action):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(CloseLoanWizard, store)
        store.confirm(model)
        store.close()
        self.search.refresh()

    def on_LoanSearch__activate(self, action):
        self.run_dialog(LoanSearch, self.store)

    def on_LoanSearchItems__activate(self, action):
        self.run_dialog(LoanItemSearch, self.store)

    # Search

    def on_SearchPurchaseReceiving__activate(self, button):
        self.run_dialog(PurchaseReceivingSearch, self.store)

    def on_SearchTransfer__activate(self, action):
        self.run_dialog(TransferOrderSearch, self.store)

    def on_SearchPurchasedStockItems__activate(self, action):
        self.run_dialog(PurchasedItemsSearch, self.store)

    def on_SearchStockItems__activate(self, action):
        self.run_dialog(ProductStockSearch, self.store)

    def on_SearchClosedStockItems__activate(self, action):
        self.run_dialog(ProductClosedStockSearch, self.store)

    def on_SearchProductHistory__activate(self, action):
        self.run_dialog(ProductSearchQuantity, self.store)

    def on_SearchStockDecrease__activate(self, action):
        self.run_dialog(StockDecreaseSearch, self.store)
Example #11
0
class StockApp(ShellApp):
    app_title = _('Stock')
    gladefile = "stock"
    search_spec = ProductFullStockView
    search_labels = _('Matching:')
    report_table = SimpleProductReport
    pixbuf_converter = converter.get_converter(gtk.gdk.Pixbuf)

    #
    # Application
    #

    def create_actions(self):
        group = get_accels('app.stock')
        actions = [
            ("NewReceiving", STOQ_RECEIVING, _("Order _receival..."),
             group.get('new_receiving')),
            ('NewTransfer', gtk.STOCK_CONVERT, _('Transfer...'),
             group.get('transfer_product')),
            ('NewStockDecrease', None, _('Stock decrease...'),
             group.get('stock_decrease')),
            ('StockInitial', gtk.STOCK_GO_UP, _('Register initial stock...')),
            ("LoanNew", None, _("Loan...")),
            ("LoanClose", None, _("Close loan...")),
            ("SearchPurchaseReceiving", None, _("Received purchases..."),
             group.get('search_receiving'),
             _("Search for received purchase orders")),
            ("SearchProductHistory", None, _("Product history..."),
             group.get('search_product_history'),
             _("Search for product history")),
            ("SearchStockDecrease", None, _("Stock decreases..."), '',
             _("Search for manual stock decreases")),
            ("SearchPurchasedStockItems", None, _("Purchased items..."),
             group.get('search_purchased_stock_items'),
             _("Search for purchased items")),
            ("SearchBrandItems", None, _("Brand items..."),
             group.get('search_brand_items'),
             _("Search for brand items on stock")),
            ("SearchBrandItemsByBranch", None, _("Brand item by branch..."),
             group.get('search_brand_by_branch'),
             _("Search for brand items by branch on stock")),
            ("SearchBatchItems", None, _("Batch items..."),
             group.get('search_batch_items'),
             _("Search for batch items on stock")),
            ("SearchStockItems", None, _("Stock items..."),
             group.get('search_stock_items'),
             _("Search for items on stock")),
            ("SearchTransfer", None, _("Transfers..."),
             group.get('search_transfers'),
             _("Search for stock transfers")),
            ("SearchClosedStockItems", None, _("Closed stock Items..."),
             group.get('search_closed_stock_items'),
             _("Search for closed stock items")),
            ("LoanSearch", None, _("Loans...")),
            ("LoanSearchItems", None, _("Loan items...")),
            ("SearchTransferItems", None, _("Transfer items...")),
            ("SearchReturnedItems", None, _("Returned items...")),
            ("SearchPendingReturnedSales", None, _("Pending returned sales...")),
            ("ProductMenu", None, _("Product")),
            ("PrintLabels", None, _("Print labels...")),
            ("ProductStockHistory", gtk.STOCK_INFO, _("History..."),
             group.get('history'),
             _('Show the stock history of the selected product')),
            ("EditProduct", gtk.STOCK_EDIT, _("Edit..."),
             group.get('edit_product'),
             _("Edit the selected product, allowing you to change it's "
               "details")),
        ]
        self.stock_ui = self.add_ui_actions('', actions,
                                            filename='stock.xml')

        toggle_actions = [
            ('StockPictureViewer', None, _('Picture viewer'),
             group.get('toggle_picture_viewer')),
        ]
        self.add_ui_actions('', toggle_actions, 'ToggleActions',
                            'toggle')
        self.set_help_section(_("Stock help"), 'app-stock')

        self.NewReceiving.set_short_label(_("Receive"))
        self.NewTransfer.set_short_label(_("Transfer"))
        self.EditProduct.set_short_label(_("Edit"))
        self.ProductStockHistory.set_short_label(_("History"))
        self.EditProduct.props.is_important = True
        self.ProductStockHistory.props.is_important = True

    def create_ui(self):
        if api.sysparam.get_bool('SMART_LIST_LOADING'):
            self.search.enable_lazy_search()

        self.popup = self.uimanager.get_widget('/StockSelection')
        self.window.add_new_items([self.NewReceiving, self.NewTransfer,
                                   self.NewStockDecrease, self.LoanNew])
        self.window.add_search_items([
            self.SearchStockItems,
            self.SearchBrandItems,
            self.SearchStockDecrease,
            self.SearchClosedStockItems,
            self.SearchProductHistory,
            self.SearchPurchasedStockItems,
            self.SearchTransfer,
        ])
        self.window.Print.set_tooltip(
            _("Print a report of these products"))
        self._inventory_widgets = [self.NewTransfer, self.NewReceiving,
                                   self.StockInitial, self.NewStockDecrease,
                                   self.LoanNew, self.LoanClose]
        self.register_sensitive_group(self._inventory_widgets,
                                      lambda: not self.has_open_inventory())

        self.image_viewer = None

        self.image = gtk.Image()
        self.edit_button = self.uimanager.get_widget('/toolbar/AppToolbarPH/EditProduct')
        self.edit_button.set_icon_widget(self.image)
        self.image.show()

        self.search.set_summary_label(column='stock',
                                      label=_('<b>Stock Total:</b>'),
                                      format='<b>%s</b>',
                                      parent=self.get_statusbar_message_area())

    def activate(self, refresh=True):
        self.window.NewToolItem.set_tooltip(
            _("Create a new receiving order"))
        self.window.SearchToolItem.set_tooltip(
            _("Search for stock items"))

        if refresh:
            self.refresh()

        open_inventory = self.check_open_inventory()

        if not open_inventory:
            self.transfers_bar = self._create_pending_info_message()
            self.returned_bar = self._create_pending_returned_sale_message()
        else:
            self.transfers_bar = None
            self.returned_bar = None

        self._update_widgets()

        self.search.focus_search_entry()

    def deactivate(self):
        if self.transfers_bar:
            self.transfers_bar.hide()
        if self.returned_bar:
            self.returned_bar.hide()

        self.uimanager.remove_ui(self.stock_ui)
        self._close_image_viewer()

    def new_activate(self):
        if not self.NewReceiving.get_sensitive():
            warning(_("You cannot receive a purchase with an open inventory."))
            return
        self._receive_purchase()

    def search_activate(self):
        self.run_dialog(ProductStockSearch, self.store)

    def set_open_inventory(self):
        self.set_sensitive(self._inventory_widgets, False)

    def create_filters(self):
        self.search.set_query(self._query)
        self.set_text_field_columns(['description', 'code', 'barcode',
                                     'category_description', 'manufacturer'])
        branches = Branch.get_active_branches(self.store)
        self.branch_filter = ComboSearchFilter(
            _('Show by:'), api.for_combo(branches, empty=_("All branches")))
        self.branch_filter.select(api.get_current_branch(self.store))
        self.add_filter(self.branch_filter, position=SearchFilterPosition.TOP)

    def get_columns(self):
        return [SearchColumn('code', title=_('Code'), sorted=True,
                             sort_func=sort_sellable_code,
                             data_type=str, width=130),
                SearchColumn('barcode', title=_("Barcode"), data_type=str,
                             width=130),
                SearchColumn('category_description', title=_("Category"),
                             data_type=str, width=100, visible=False),
                SearchColumn('description', title=_("Description"),
                             data_type=str, expand=True,
                             ellipsize=pango.ELLIPSIZE_END),
                SearchColumn('manufacturer', title=_("Manufacturer"),
                             data_type=str, visible=False),
                SearchColumn('brand', title=_("Brand"),
                             data_type=str, visible=False),
                SearchColumn('model', title=_("Model"),
                             data_type=str, visible=False),
                SearchColumn('location', title=_("Location"), data_type=str,
                             width=100, visible=False),
                QuantityColumn('stock', title=_('Quantity'), width=100,
                               use_having=True),
                SearchColumn('has_image', title=_('Picture'),
                             data_type=bool, width=80),
                ]

    #
    # Private API
    #

    def _open_image_viewer(self):
        assert self.image_viewer is None

        self.image_viewer = SellableImageViewer(size=(325, 325))
        self.image_viewer.toplevel.connect(
            'delete-event', self.on_image_viewer_closed)
        self.image_viewer.show_all()

        self._update_widgets()

    def _close_image_viewer(self):
        if self.image_viewer is None:
            return

        self.image_viewer.destroy()
        self.image_viewer = None

    def _query(self, store):
        branch = self.branch_filter.get_state().value
        return self.search_spec.find_by_branch(store, branch)

    def _update_widgets(self):
        branch = api.get_current_branch(self.store)

        is_main_branch = self.branch_filter.get_state().value is branch
        item = self.results.get_selected()

        sellable = item and item.product.sellable
        if sellable:
            if item.has_image:
                thumbnail = sellable.image.thumbnail
                pixbuf = self.pixbuf_converter.from_string(thumbnail)
            else:
                pixbuf = None

            self._update_edit_image(pixbuf)
            if self.image_viewer:
                self.image_viewer.set_sellable(sellable)
        else:
            self._update_edit_image()

        self.set_sensitive([self.EditProduct, self.PrintLabels], bool(item))
        self.set_sensitive([self.ProductStockHistory],
                           bool(item) and is_main_branch)
        # We need more than one branch to be able to do transfers
        # Note that 'all branches' is not a real branch
        has_branches = len(self.branch_filter.combo) > 2

        transfer_active = self.NewTransfer.get_sensitive()
        self.set_sensitive([self.NewTransfer],
                           transfer_active and has_branches)
        # Building a list of searches that we must disable if there is no
        # branches other than the main company
        searches = [self.SearchTransfer, self.SearchTransferItems,
                    self.SearchPendingReturnedSales]
        self.set_sensitive(searches, has_branches)

    def _update_edit_image(self, pixbuf=None):
        if not pixbuf:
            self.image.set_from_stock(gtk.STOCK_EDIT,
                                      gtk.ICON_SIZE_LARGE_TOOLBAR)
            return

        # FIXME: get this icon size from settings
        icon_size = 24
        pixbuf = pixbuf.scale_simple(icon_size, icon_size,
                                     gtk.gdk.INTERP_BILINEAR)
        self.image.set_from_pixbuf(pixbuf)

    def _update_filter_slave(self, slave):
        self.refresh()

    def _transfer_stock(self):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(StockTransferWizard, store)
        store.confirm(model)
        store.close()
        self.refresh()

    def _receive_purchase(self):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(ReceivingOrderWizard, store)
        store.confirm(model)
        store.close()
        self.refresh()

    def _create_pending_info_message(self):
        branch = api.get_current_branch(self.store)
        n_transfers = TransferOrder.get_pending_transfers(self.store, branch).count()

        if not n_transfers:
            return None

        msg = stoqlib_ngettext(_(u"You have %s incoming transfer"),
                               _(u"You have %s incoming transfers"),
                               n_transfers) % n_transfers
        info_bar = self.window.add_info_bar(gtk.MESSAGE_QUESTION, msg)
        button = info_bar.add_button(_(u"Receive"), gtk.RESPONSE_OK)
        button.connect('clicked', self._on_info_transfers__clicked)

        return info_bar

    def _create_pending_returned_sale_message(self):
        branch = api.get_current_branch(self.store)
        n_returned = ReturnedSale.get_pending_returned_sales(self.store, branch).count()

        if not n_returned:
            return None

        msg = stoqlib_ngettext(_(u"You have %s returned sale to receive"),
                               _(u"You have %s returned sales to receive"),
                               n_returned) % n_returned
        info_returned_bar = self.window.add_info_bar(gtk.MESSAGE_QUESTION, msg)
        button = info_returned_bar.add_button(_(u"Returned sale"), gtk.RESPONSE_OK)
        button.connect('clicked', self._on_info_returned_sales__clicked)

        return info_returned_bar

    def _search_transfers(self):
        branch = api.get_current_branch(self.store)
        self.run_dialog(TransferOrderSearch, self.store)

        # After the search is closed we may want to update , or even hide the
        # message, if there is no pending transfer to receive
        if self.transfers_bar:
            n_transfers = TransferOrder.get_pending_transfers(self.store, branch).count()

            if n_transfers > 0:
                msg = stoqlib_ngettext(_(u"You have %s incoming transfer"),
                                       _(u"You have %s incoming transfers"),
                                       n_transfers) % n_transfers
                self.transfers_bar.set_message(msg)
            else:
                self.transfers_bar.hide()
        self.refresh()

    def _search_pending_returned_sales(self):
        with api.new_store() as store:
            self.run_dialog(PendingReturnedSaleSearch, store)

        branch = api.get_current_branch(self.store)
        # After the search is closed we may want to update , or even hide the
        # message, if there is no pending returned sale to receive
        if self.returned_bar:
            n_returned = ReturnedSale.get_pending_returned_sales(self.store, branch).count()

            if n_returned > 0:
                msg = stoqlib_ngettext(_(u"You have %s returned sale to receive"),
                                       _(u"You have %s returned sales to receive"),
                                       n_returned) % n_returned
                self.returned_bar.set_message(msg)
            else:
                self.returned_bar.hide()
        self.refresh()

    #
    # Callbacks
    #

    def on_image_viewer_closed(self, window, event):
        self.image_viewer = None
        self.StockPictureViewer.set_active(False)

    def on_results__has_rows(self, results, product):
        self._update_widgets()

    def on_results__selection_changed(self, results, product):
        self._update_widgets()

    def on_results__right_click(self, results, result, event):
        self.popup.popup(None, None, None, event.button, event.time)

    def on_ProductStockHistory__activate(self, button):
        selected = self.results.get_selected()
        sellable = selected.sellable
        self.run_dialog(ProductStockHistoryDialog, self.store, sellable,
                        branch=self.branch_filter.combo.get_selected())

    def on_PrintLabels__activate(self, button):
        selected = self.results.get_selected()
        sellable = selected.sellable
        label_data = self.run_dialog(PrintLabelEditor, None, self.store,
                                     sellable)
        if label_data:
            print_labels(label_data, self.store)

    def on_EditProduct__activate(self, button):
        selected = self.results.get_selected()
        assert selected

        store = api.new_store()
        product = store.fetch(selected.product)

        model = self.run_dialog(ProductStockEditor, store, product)
        store.confirm(model)
        store.close()
        if model:
            self.refresh()

    def _on_info_transfers__clicked(self, button):
        self._search_transfers()

    def _on_info_returned_sales__clicked(self, button):
        self._search_pending_returned_sales()

    # Stock

    def on_NewReceiving__activate(self, button):
        self._receive_purchase()

    def on_NewTransfer__activate(self, button):
        self._transfer_stock()

    def on_NewStockDecrease__activate(self, action):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(StockDecreaseWizard, store)
        store.confirm(model)
        store.close()
        self.refresh()

    def on_StockInitial__activate(self, action):
        if self.check_open_inventory():
            return

        with api.new_store() as store:
            self.run_dialog(InitialStockDialog, store)

        if store.committed:
            self.refresh()

    def on_StockPictureViewer__toggled(self, button):
        if button.get_active():
            self._open_image_viewer()
        else:
            self._close_image_viewer()

    # Loan

    def on_LoanNew__activate(self, action):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(NewLoanWizard, store)
        store.confirm(model)
        store.close()
        self.refresh()

    def on_LoanClose__activate(self, action):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(CloseLoanWizard, store)
        store.confirm(model)
        store.close()
        self.refresh()

    def on_LoanSearch__activate(self, action):
        self.run_dialog(LoanSearch, self.store)

    def on_LoanSearchItems__activate(self, action):
        self.run_dialog(LoanItemSearch, self.store)

    # Search

    def on_SearchPurchaseReceiving__activate(self, button):
        self.run_dialog(PurchaseReceivingSearch, self.store)

    def on_SearchTransfer__activate(self, action):
        self._search_transfers()

    def on_SearchTransferItems__activate(self, action):
        self.run_dialog(TransferItemSearch, self.store)

    def on_SearchPendingReturnedSales__activate(self, action):
        self._search_pending_returned_sales()

    def on_SearchReturnedItems__activate(self, action):
        self.run_dialog(ReturnedItemSearch, self.store)

    def on_SearchPurchasedStockItems__activate(self, action):
        self.run_dialog(PurchasedItemsSearch, self.store)

    def on_SearchStockItems__activate(self, action):
        self.run_dialog(ProductStockSearch, self.store)

    def on_SearchBrandItems__activate(self, action):
        self.run_dialog(ProductBrandSearch, self.store)

    def on_SearchBrandItemsByBranch__activate(self, action):
        self.run_dialog(ProductBrandByBranchSearch, self.store)

    def on_SearchBatchItems__activate(self, action):
        self.run_dialog(ProductBatchSearch, self.store)

    def on_SearchClosedStockItems__activate(self, action):
        self.run_dialog(ProductClosedStockSearch, self.store)

    def on_SearchProductHistory__activate(self, action):
        self.run_dialog(ProductSearchQuantity, self.store)

    def on_SearchStockDecrease__activate(self, action):
        self.run_dialog(StockDecreaseSearch, self.store)
Example #12
0
class SellableSearch(SearchEditor):
    title = _('Item search')
    size = (800, 500)
    model_list_lookup_attr = 'product_id'
    footer_ok_label = _('_Select item')
    search_spec = SellableFullStockView
    editor_class = None
    exclude_delivery_service = True
    text_field_columns = [SellableFullStockView.description,
                          SellableFullStockView.category_description,
                          SellableFullStockView.barcode,
                          SellableFullStockView.code]

    def __init__(self, store, hide_footer=False, hide_toolbar=True,
                 selection_mode=None, search_str=None, search_spec=None,
                 search_query=None, double_click_confirm=True, info_message=None):
        """
        :param store: a store
        :param hide_footer: do I have to hide the dialog footer?
        :param hide_toolbar: do I have to hide the dialog toolbar?
        :param selection_mode: the kiwi list selection mode
        :param search_str: If this search should already filter for some string
        :param double_click_confirm: If double click a item in the list should
            automatically confirm
        """
        if selection_mode is None:
            selection_mode = gtk.SELECTION_BROWSE

        self._image_viewer = None
        self._first_search = True
        self._first_search_string = search_str
        self._search_query = search_query
        self._delivery_sellable = sysparam.get_object(
            store, 'DELIVERY_SERVICE').sellable

        SearchEditor.__init__(self, store, search_spec=search_spec,
                              editor_class=self.editor_class,
                              hide_footer=hide_footer,
                              hide_toolbar=hide_toolbar,
                              selection_mode=selection_mode,
                              double_click_confirm=double_click_confirm)

        if info_message:
            self.set_message(info_message)

        if search_str:
            self.set_searchbar_search_string(search_str)
            self.search.refresh()

    #
    #  SearchEditor
    #

    def key_shift_Return(self):
        self.confirm()

    def key_control_Return(self):
        self.confirm()

    def key_shift_KP_Enter(self):
        self.confirm()

    def key_control_KP_Enter(self):
        self.confirm()

    def close(self):
        # Make sure image viewer gets closed when this search closes
        self._close_image_viewer()
        super(SellableSearch, self).close()

    def confirm(self, retval=None):
        # FIXME: This is a hack, we need to do proper validation in the parent
        if retval is None and not self.ok_button.get_sensitive():
            return
        super(SellableSearch, self).confirm(retval=retval)

    def setup_widgets(self):
        self.image_viewer_toggler = gtk.CheckMenuItem(_("Show image viewer"))
        self.popup = gtk.Menu()
        self.popup.add(self.image_viewer_toggler)
        self.popup.show_all()

        if hasattr(self.search_spec, 'product'):
            self.branch_stock_button = self.add_button(label=_('Stock details'))
            self.branch_stock_button.show()
            self.branch_stock_button.set_sensitive(False)
        else:
            self.branch_stock_button = None

    def create_filters(self):
        self.search.set_query(self.executer_query)

    def get_columns(self):
        columns = [SearchColumn('code', title=_(u'Code'), data_type=str),
                   SearchColumn('barcode', title=_('Barcode'), data_type=str,
                                sort_func=sort_sellable_code, width=80),
                   SearchColumn('category_description', title=_('Category'),
                                data_type=str, width=120),
                   SearchColumn('description', title=_('Description'),
                                data_type=str, expand=True, sorted=True),
                   SearchColumn('manufacturer', title=_('Manufacturer'),
                                data_type=str, visible=False),
                   SearchColumn('model', title=_('Model'),
                                data_type=str, visible=False)]

        if hasattr(self.search_spec, 'price'):
            columns.append(SearchColumn('price',
                                        title=_(u'Price'),
                                        data_type=currency, visible=True))

        if hasattr(self.search_spec, 'minimum_quantity'):
            columns.append(SearchColumn('minimum_quantity',
                                        title=_(u'Minimum Qty'),
                                        data_type=Decimal, visible=False))

        if hasattr(self.search_spec, 'stock'):
            columns.append(SearchColumn('stock', title=_(u'In Stock'),
                                        data_type=Decimal))

        return columns

    def update_widgets(self):
        sellable_view = self.results.get_selected()
        self.set_edit_button_sensitive(bool(sellable_view))
        self.ok_button.set_sensitive(bool(sellable_view))

        # Some viewables may not have the product (for viewables with only
        # services). Also use hasattr for product since it may be None
        if self.branch_stock_button is not None:
            storable = (getattr(sellable_view, 'product', None) and
                        getattr(sellable_view.product, 'storable'))
            self.branch_stock_button.set_sensitive(bool(storable))

    def executer_query(self, store):
        # If the viewable has a find_by_branch method, then lets use it instead
        # of the generic find, to show only the stock for the current branch.
        if hasattr(self.search_spec, 'find_by_branch'):
            branch = api.get_current_branch(store)
            results = self.search_spec.find_by_branch(store, branch)
        else:
            results = store.find(self.search_spec)

        if self._search_query:
            results = results.find(self._search_query)

        if self.exclude_delivery_service:
            results = results.find(And(
                self.search_spec.status == Sellable.STATUS_AVAILABLE,
                self.search_spec.id != self._delivery_sellable.id))

        return results

    #
    #  Private
    #

    def _open_image_viewer(self):
        assert self._image_viewer is None

        self._image_viewer = SellableImageViewer(size=(325, 325))
        self._update_image_viewer()
        self._image_viewer.toplevel.connect(
            'delete-event', self._on_image_viewer__delete_event)
        self._image_viewer.show_all()

    def _close_image_viewer(self):
        if self._image_viewer is None:
            return

        self._image_viewer.destroy()
        self._image_viewer = None

    def _update_image_viewer(self):
        if self._image_viewer is None:
            return

        row = self.results.get_selected()
        self._image_viewer.set_sellable(row.sellable)

    #
    # Callbacks
    #

    def _on_image_viewer__delete_event(self, window, event):
        self._image_viewer = None
        self.image_viewer_toggler.set_active(False)

    def on_image_viewer_toggler__toggled(self, item):
        if item.get_active():
            self._open_image_viewer()
        else:
            self._close_image_viewer()

    def on_results__right_click(self, klist, row, event):
        self.popup.popup(None, None, None, event.button, event.time)

    def on_results__selection_changed(self, klist, row):
        self._update_image_viewer()

    def on_branch_stock_button__clicked(self, widget):
        from stoqlib.gui.search.productsearch import ProductBranchSearch
        viewable = self.results.get_selected()
        product = viewable.product
        if product and product.storable:
            self.run_dialog(ProductBranchSearch, self, self.store,
                            product.storable)
Example #13
0
class StockApp(ShellApp):
    app_title = _('Stock')
    gladefile = "stock"
    search_spec = ProductFullStockView
    search_labels = _('Matching:')
    report_table = SimpleProductReport
    pixbuf_converter = converter.get_converter(GdkPixbuf.Pixbuf)

    #
    # Application
    #

    def create_actions(self):
        group = get_accels('app.stock')
        actions = [
            ("NewReceiving", None, _("Order _receival..."),
             group.get('new_receiving')),
            ('NewTransfer', Gtk.STOCK_CONVERT, _('Transfer...'),
             group.get('transfer_product')),
            ('NewStockDecrease', None, _('Stock decrease...'),
             group.get('stock_decrease')),
            ('StockInitial', Gtk.STOCK_GO_UP, _('Register initial stock...')),
            ("LoanNew", None, _("Loan...")),
            ("LoanClose", None, _("Close loan...")),
            ("SearchPurchaseReceiving", None, _("Received purchases..."),
             group.get('search_receiving'),
             _("Search for received purchase orders")),
            ("SearchProductHistory", None, _("Product history..."),
             group.get('search_product_history'),
             _("Search for product history")),
            ("SearchStockDecrease", None, _("Stock decreases..."), '',
             _("Search for manual stock decreases")),
            ("SearchPurchasedStockItems", None, _("Purchased items..."),
             group.get('search_purchased_stock_items'),
             _("Search for purchased items")),
            ("SearchBrandItems", None, _("Brand items..."),
             group.get('search_brand_items'),
             _("Search for brand items on stock")),
            ("SearchBrandItemsByBranch", None, _("Brand item by branch..."),
             group.get('search_brand_by_branch'),
             _("Search for brand items by branch on stock")),
            ("SearchBatchItems", None, _("Batch items..."),
             group.get('search_batch_items'),
             _("Search for batch items on stock")),
            ("SearchStockItems", None, _("Stock items..."),
             group.get('search_stock_items'),
             _("Search for items on stock")),
            ("SearchTransfer", None, _("Transfers..."),
             group.get('search_transfers'),
             _("Search for stock transfers")),
            ("SearchClosedStockItems", None, _("Closed stock Items..."),
             group.get('search_closed_stock_items'),
             _("Search for closed stock items")),
            ("LoanSearch", None, _("Loans...")),
            ("LoanSearchItems", None, _("Loan items...")),
            ("SearchTransferItems", None, _("Transfer items...")),
            ("SearchReturnedItems", None, _("Returned items...")),
            ("SearchPendingReturnedSales", None, _("Pending returned sales...")),
            ("ProductMenu", None, _("Product")),
            ("PrintLabels", None, _("Print labels...")),
            ("ManageStock", None, _("Manage stock...")),

            ("ProductStockHistory", Gtk.STOCK_INFO, _("History..."),
             group.get('history'),
             _('Show the stock history of the selected product')),
            ("EditProduct", Gtk.STOCK_EDIT, _("Edit..."),
             group.get('edit_product'),
             _("Edit the selected product, allowing you to change it's "
               "details")),
        ]
        self.stock_ui = self.add_ui_actions(actions)

        toggle_actions = [
            ('StockPictureViewer', None, _('Picture viewer'),
             group.get('toggle_picture_viewer')),
        ]
        self.add_ui_actions(toggle_actions, 'ToggleActions')
        self.set_help_section(_("Stock help"), 'app-stock')

    def create_ui(self):
        if api.sysparam.get_bool('SMART_LIST_LOADING'):
            self.search.enable_lazy_search()

        self.window.add_print_items([self.PrintLabels])
        self.window.add_export_items()
        self.window.add_extra_items([self.StockInitial, self.LoanClose,
                                     self.StockPictureViewer])
        self.window.add_new_items([self.NewReceiving, self.NewTransfer,
                                   self.NewStockDecrease, self.LoanNew])
        self.window.add_search_items([
            self.SearchPurchaseReceiving,
            self.SearchProductHistory,
            self.SearchTransfer,
            self.SearchTransferItems,
            self.SearchStockDecrease,
            self.SearchReturnedItems,
            self.SearchPurchasedStockItems,
            self.SearchStockItems,
            self.SearchBrandItems,
            self.SearchBrandItemsByBranch,
            self.SearchBatchItems,
            self.SearchClosedStockItems,
            self.SearchPendingReturnedSales,
            # Separator
            self.LoanSearch,
            self.LoanSearchItems,

        ])
        self._inventory_widgets = [self.NewTransfer, self.NewReceiving,
                                   self.StockInitial, self.NewStockDecrease,
                                   self.LoanNew, self.LoanClose]
        self.register_sensitive_group(self._inventory_widgets,
                                      lambda: not self.has_open_inventory())

        self.image_viewer = None

        self.image = Gtk.Image()
        # FIXME: How are we goint to display an image preview?
        #self.edit_button = self.uimanager.get_widget('/toolbar/AppToolbarPH/EditProduct')
        #self.edit_button.set_icon_widget(self.image)
        self.image.show()

        self.search.set_summary_label(column='stock',
                                      label=_('<b>Stock Total:</b>'),
                                      format='<b>%s</b>',
                                      parent=self.get_statusbar_message_area())

    def get_domain_options(self):
        options = [
            ('fa-info-circle-symbolic', _('History'), 'stock.ProductStockHistory', True),
            ('fa-edit-symbolic', _('Edit'), 'stock.EditProduct', True),
            ('', _('Print labels'), 'stock.PrintLabels', False),
            ('', _('Manage stock'), 'stock.ManageStock', False),
        ]

        return options

    def activate(self, refresh=True):
        if refresh:
            self.refresh()

        open_inventory = self.check_open_inventory()

        if not open_inventory:
            self.transfers_bar = self._create_pending_info_message()
            self.returned_bar = self._create_pending_returned_sale_message()
        else:
            self.transfers_bar = None
            self.returned_bar = None

        self._update_widgets()

        self.search.focus_search_entry()

    def deactivate(self):
        if self.transfers_bar:
            self.transfers_bar.hide()
        if self.returned_bar:
            self.returned_bar.hide()

        self._close_image_viewer()

    def set_open_inventory(self):
        self.set_sensitive(self._inventory_widgets, False)

    def create_filters(self):
        self.search.set_query(self._query)
        self.set_text_field_columns(['description', 'code', 'barcode',
                                     'category_description', 'manufacturer'])
        branches = Branch.get_active_branches(self.store)
        self.branch_filter = ComboSearchFilter(
            _('Show by:'), api.for_combo(branches, empty=_("All branches")))
        self.branch_filter.select(api.get_current_branch(self.store))
        self.add_filter(self.branch_filter, position=SearchFilterPosition.TOP)

    def get_columns(self):
        return [SearchColumn('code', title=_('Code'), sorted=True,
                             sort_func=sort_sellable_code,
                             data_type=str, width=130),
                SearchColumn('barcode', title=_("Barcode"), data_type=str,
                             width=130),
                SearchColumn('category_description', title=_("Category"),
                             data_type=str, width=100, visible=False),
                SearchColumn('description', title=_("Description"),
                             data_type=str, expand=True,
                             ellipsize=Pango.EllipsizeMode.END),
                SearchColumn('manufacturer', title=_("Manufacturer"),
                             data_type=str, visible=False),
                SearchColumn('brand', title=_("Brand"),
                             data_type=str, visible=False),
                SearchColumn('model', title=_("Model"),
                             data_type=str, visible=False),
                SearchColumn('location', title=_("Location"), data_type=str,
                             width=100, visible=False),
                QuantityColumn('stock', title=_('Quantity'), width=100,
                               use_having=True),
                SearchColumn('has_image', title=_('Picture'),
                             data_type=bool, width=80),
                ]

    #
    # Private API
    #

    def _open_image_viewer(self):
        assert self.image_viewer is None

        self.image_viewer = SellableImageViewer(size=(325, 325))
        self.image_viewer.toplevel.connect(
            'delete-event', self.on_image_viewer_closed)
        self.image_viewer.show_all()

        self._update_widgets()

    def _close_image_viewer(self):
        if self.image_viewer is None:
            return

        self.image_viewer.destroy()
        self.image_viewer = None

    def _query(self, store):
        branch = self.branch_filter.get_state().value
        return self.search_spec.find_by_branch(store, branch)

    def _update_widgets(self):
        branch = api.get_current_branch(self.store)

        is_main_branch = self.branch_filter.get_state().value is branch
        item = self.results.get_selected()

        sellable = item and item.product.sellable
        if sellable:
            if item.has_image:
                # XXX:Workaround for a bug caused by the image domain refactoring
                # which left some existent thumbnail as None
                thumbnail = sellable.image.thumbnail
                if thumbnail is None:
                    # Create new store to create the thumbnail
                    with api.new_store() as new_store:
                        image = sellable.image
                        image = new_store.fetch(image)
                        size = (Image.THUMBNAIL_SIZE_WIDTH, Image.THUMBNAIL_SIZE_HEIGHT)
                        image.thumbnail = get_thumbnail(image.image, size)
                        thumbnail = image.thumbnail
                pixbuf = self.pixbuf_converter.from_string(thumbnail)
            else:
                pixbuf = None

            self._update_edit_image(pixbuf)
            if self.image_viewer:
                self.image_viewer.set_sellable(sellable)
        else:
            self._update_edit_image()

        # Always let the user choose the manage stock option and do a proper
        # check there (showing a warning if he can't)
        self.set_sensitive([self.ManageStock], bool(item))
        self.set_sensitive([self.EditProduct, self.PrintLabels], bool(item))
        self.set_sensitive([self.ProductStockHistory],
                           bool(item) and is_main_branch)
        # We need more than one branch to be able to do transfers
        # Note that 'all branches' is not a real branch
        has_branches = len(self.branch_filter.combo) > 2

        transfer_active = self.NewTransfer.get_enabled()
        self.set_sensitive([self.NewTransfer],
                           transfer_active and has_branches)
        # Building a list of searches that we must disable if there is no
        # branches other than the main company
        searches = [self.SearchTransfer, self.SearchTransferItems,
                    self.SearchPendingReturnedSales]
        self.set_sensitive(searches, has_branches)

    def _update_edit_image(self, pixbuf=None):
        if not pixbuf:
            self.image.set_from_stock(Gtk.STOCK_EDIT,
                                      Gtk.IconSize.LARGE_TOOLBAR)
            return

        # FIXME: get this icon size from settings
        icon_size = 24
        pixbuf = pixbuf.scale_simple(icon_size, icon_size,
                                     GdkPixbuf.InterpType.BILINEAR)
        self.image.set_from_pixbuf(pixbuf)

    def _update_filter_slave(self, slave):
        self.refresh()

    def _transfer_stock(self):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(StockTransferWizard, store)
        store.confirm(model)
        store.close()
        self.refresh()

    def _receive_purchase(self):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(ReceivingOrderWizard, store)
        store.confirm(model)
        store.close()
        self.refresh()

    def _create_pending_info_message(self):
        branch = api.get_current_branch(self.store)
        n_transfers = TransferOrder.get_pending_transfers(self.store, branch).count()

        if not n_transfers:
            return None

        msg = stoqlib_ngettext(_(u"You have %s incoming transfer"),
                               _(u"You have %s incoming transfers"),
                               n_transfers) % n_transfers
        info_bar = self.window.add_info_bar(Gtk.MessageType.QUESTION, msg)
        button = info_bar.add_button(_(u"Receive"), Gtk.ResponseType.OK)
        button.connect('clicked', self._on_info_transfers__clicked)

        return info_bar

    def _create_pending_returned_sale_message(self):
        branch = api.get_current_branch(self.store)
        n_returned = ReturnedSale.get_pending_returned_sales(self.store, branch).count()

        if not n_returned:
            return None

        msg = stoqlib_ngettext(_(u"You have %s returned sale to receive"),
                               _(u"You have %s returned sales to receive"),
                               n_returned) % n_returned
        info_returned_bar = self.window.add_info_bar(Gtk.MessageType.QUESTION, msg)
        button = info_returned_bar.add_button(_(u"Returned sale"), Gtk.ResponseType.OK)
        button.connect('clicked', self._on_info_returned_sales__clicked)

        return info_returned_bar

    def _search_transfers(self):
        branch = api.get_current_branch(self.store)
        self.run_dialog(TransferOrderSearch, self.store)

        # After the search is closed we may want to update , or even hide the
        # message, if there is no pending transfer to receive
        if self.transfers_bar:
            n_transfers = TransferOrder.get_pending_transfers(self.store, branch).count()

            if n_transfers > 0:
                msg = stoqlib_ngettext(_(u"You have %s incoming transfer"),
                                       _(u"You have %s incoming transfers"),
                                       n_transfers) % n_transfers
                self.transfers_bar.set_message(msg)
            else:
                self.transfers_bar.hide()
        self.refresh()

    def _search_pending_returned_sales(self):
        with api.new_store() as store:
            self.run_dialog(PendingReturnedSaleSearch, store)

        branch = api.get_current_branch(self.store)
        # After the search is closed we may want to update , or even hide the
        # message, if there is no pending returned sale to receive
        if self.returned_bar:
            n_returned = ReturnedSale.get_pending_returned_sales(self.store, branch).count()

            if n_returned > 0:
                msg = stoqlib_ngettext(_(u"You have %s returned sale to receive"),
                                       _(u"You have %s returned sales to receive"),
                                       n_returned) % n_returned
                self.returned_bar.set_message(msg)
            else:
                self.returned_bar.hide()
        self.refresh()

    #
    # Callbacks
    #

    def on_image_viewer_closed(self, window, event):
        self.image_viewer = None
        self.StockPictureViewer.set_state(GLib.Variant.new_boolean(False))

    def on_results__has_rows(self, results, product):
        self._update_widgets()

    def on_results__selection_changed(self, results, product):
        self._update_widgets()

    def on_ProductStockHistory__activate(self, button):
        selected = self.results.get_selected()
        sellable = selected.sellable
        self.run_dialog(ProductStockHistoryDialog, self.store, sellable,
                        branch=self.branch_filter.combo.get_selected())

    def on_ManageStock__activate(self, action):
        user = api.get_current_user(self.store)
        if not user.profile.check_app_permission(u'inventory'):
            return warning(_('Only users with access to the inventory app can'
                             ' change the stock quantity'))

        product = self.results.get_selected().product
        if product.is_grid:
            return warning(_("Can't change stock quantity of a grid parent"))

        if product.storable and product.storable.is_batch:
            return warning(_("It's not possible to change the stock quantity of"
                             " a batch product"))

        branch = self.branch_filter.combo.get_selected()
        if not branch:
            return warning(_('You must select a branch first'))

        with api.new_store() as store:
            self.run_dialog(ProductStockQuantityEditor, store,
                            store.fetch(product), branch=branch)

        if store.committed:
            self.refresh()

    def on_PrintLabels__activate(self, button):
        selected = self.results.get_selected()
        sellable = selected.sellable
        label_data = self.run_dialog(PrintLabelEditor, None, self.store,
                                     sellable)
        if label_data:
            print_labels(label_data, self.store)

    def on_EditProduct__activate(self, button):
        selected = self.results.get_selected()
        assert selected

        store = api.new_store()
        product = store.fetch(selected.product)

        model = self.run_dialog(ProductStockEditor, store, product)
        store.confirm(model)
        store.close()
        if model:
            self.refresh()

    def _on_info_transfers__clicked(self, button):
        self._search_transfers()

    def _on_info_returned_sales__clicked(self, button):
        self._search_pending_returned_sales()

    # Stock

    def on_NewReceiving__activate(self, button):
        self._receive_purchase()

    def on_NewTransfer__activate(self, button):
        self._transfer_stock()

    def on_NewStockDecrease__activate(self, action):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(StockDecreaseWizard, store)
        store.confirm(model)
        store.close()
        self.refresh()

    def on_StockInitial__activate(self, action):
        if self.check_open_inventory():
            return

        with api.new_store() as store:
            self.run_dialog(InitialStockDialog, store)

        if store.committed:
            self.refresh()

    def on_StockPictureViewer__change_state(self, action, value):
        action.set_state(value)
        if value.get_boolean():
            self._open_image_viewer()
        else:
            self._close_image_viewer()

    # Loan

    def on_LoanNew__activate(self, action):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(NewLoanWizard, store)
        store.confirm(model)
        store.close()
        self.refresh()

    def on_LoanClose__activate(self, action):
        if self.check_open_inventory():
            return
        store = api.new_store()
        model = self.run_dialog(CloseLoanWizard, store)
        store.confirm(model)
        store.close()
        self.refresh()

    def on_LoanSearch__activate(self, action):
        self.run_dialog(LoanSearch, self.store)

    def on_LoanSearchItems__activate(self, action):
        self.run_dialog(LoanItemSearch, self.store)

    # Search

    def on_SearchPurchaseReceiving__activate(self, button):
        self.run_dialog(PurchaseReceivingSearch, self.store)

    def on_SearchTransfer__activate(self, action):
        self._search_transfers()

    def on_SearchTransferItems__activate(self, action):
        self.run_dialog(TransferItemSearch, self.store)

    def on_SearchPendingReturnedSales__activate(self, action):
        self._search_pending_returned_sales()

    def on_SearchReturnedItems__activate(self, action):
        self.run_dialog(ReturnedItemSearch, self.store)

    def on_SearchPurchasedStockItems__activate(self, action):
        self.run_dialog(PurchasedItemsSearch, self.store)

    def on_SearchStockItems__activate(self, action):
        self.run_dialog(ProductStockSearch, self.store)

    def on_SearchBrandItems__activate(self, action):
        self.run_dialog(ProductBrandSearch, self.store)

    def on_SearchBrandItemsByBranch__activate(self, action):
        self.run_dialog(ProductBrandByBranchSearch, self.store)

    def on_SearchBatchItems__activate(self, action):
        self.run_dialog(ProductBatchSearch, self.store)

    def on_SearchClosedStockItems__activate(self, action):
        self.run_dialog(ProductClosedStockSearch, self.store)

    def on_SearchProductHistory__activate(self, action):
        self.run_dialog(ProductSearchQuantity, self.store)

    def on_SearchStockDecrease__activate(self, action):
        self.run_dialog(StockDecreaseSearch, self.store)