Source code for stoq.gui.stock

# -*- Mode: Python; coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4

##
## Copyright (C) 2005-2015 Async Open Source <http://www.async.com.br>
## All rights reserved
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., or visit: http://www.gnu.org/.
##
## Author(s): Stoq Team <stoq-devel@async.com.br>
##
""" Main gui definition for stock application.  """

import logging

import pango
import gtk
from kiwi.datatypes import converter

from stoqlib.api import api
from stoqlib.enums import SearchFilterPosition
from stoqlib.domain.image import Image
from stoqlib.domain.person import Branch
from stoqlib.domain.views import ProductFullStockView
from stoqlib.domain.transfer import TransferOrder
from stoqlib.domain.returnedsale import ReturnedSale
from stoqlib.lib.defaults import sort_sellable_code
from stoqlib.lib.imageutils import get_thumbnail
from stoqlib.lib.message import warning
from stoqlib.lib.translation import stoqlib_ngettext, stoqlib_gettext as _
from stoqlib.gui.dialogs.initialstockdialog import InitialStockDialog
from stoqlib.gui.dialogs.labeldialog import PrintLabelEditor
from stoqlib.gui.dialogs.productstockdetails import ProductStockHistoryDialog
from stoqlib.gui.dialogs.sellableimage import SellableImageViewer
from stoqlib.gui.editors.producteditor import (ProductStockEditor,
                                               ProductStockQuantityEditor)
from stoqlib.gui.search.loansearch import LoanItemSearch, LoanSearch
from stoqlib.gui.search.receivingsearch import PurchaseReceivingSearch
from stoqlib.gui.search.returnedsalesearch import (PendingReturnedSaleSearch,
                                                   ReturnedItemSearch)
from stoqlib.gui.search.productsearch import (ProductSearchQuantity,
                                              ProductStockSearch,
                                              ProductBrandSearch,
                                              ProductBrandByBranchSearch,
                                              ProductBatchSearch,
                                              ProductClosedStockSearch)
from stoqlib.gui.search.purchasesearch import PurchasedItemsSearch
from stoqlib.gui.search.transfersearch import (TransferOrderSearch,
                                               TransferItemSearch)
from stoqlib.gui.search.searchcolumns import SearchColumn, QuantityColumn
from stoqlib.gui.search.searchfilters import ComboSearchFilter
from stoqlib.gui.search.stockdecreasesearch import StockDecreaseSearch
from stoqlib.gui.utils.keybindings import get_accels
from stoqlib.gui.utils.printing import print_labels
from stoqlib.gui.wizards.loanwizard import NewLoanWizard, CloseLoanWizard
from stoqlib.gui.wizards.receivingwizard import ReceivingOrderWizard
from stoqlib.gui.wizards.stockdecreasewizard import StockDecreaseWizard
from stoqlib.gui.wizards.stocktransferwizard import StockTransferWizard
from stoqlib.reporting.product import SimpleProductReport
from stoqlib.gui.stockicons import STOQ_RECEIVING

from stoq.gui.shell.shellapp import ShellApp

log = logging.getLogger(__name__)


[docs]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 #
[docs] 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
[docs] 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())
[docs] 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()
[docs] 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()
[docs] def new_activate(self): if not self.NewReceiving.get_sensitive(): warning(_("You cannot receive a purchase with an open inventory.")) return self._receive_purchase()
[docs] def search_activate(self): self.run_dialog(ProductStockSearch, self.store)
[docs] def set_open_inventory(self): self.set_sensitive(self._inventory_widgets, False)
[docs] 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)
[docs] 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: # 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.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 #
[docs] def on_image_viewer_closed(self, window, event): self.image_viewer = None self.StockPictureViewer.set_active(False)
[docs] def on_results__has_rows(self, results, product): self._update_widgets()
[docs] def on_results__selection_changed(self, results, product): self._update_widgets()
[docs] def on_results__right_click(self, results, result, event): self.popup.popup(None, None, None, event.button, event.time)
[docs] 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())
[docs] 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.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')) if (api.sysparam.get_bool('SYNCHRONIZED_MODE') and branch != api.get_current_branch(self.store)): return warning(_('You can only change the stock of your current branch')) with api.new_store() as store: self.run_dialog(ProductStockQuantityEditor, store, store.fetch(product), branch=branch) if store.committed: self.refresh()
[docs] 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)
[docs] 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
[docs] def on_NewReceiving__activate(self, button): self._receive_purchase()
[docs] def on_NewTransfer__activate(self, button): self._transfer_stock()
[docs] 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()
[docs] 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()
[docs] def on_StockPictureViewer__toggled(self, button): if button.get_active(): self._open_image_viewer() else: self._close_image_viewer()
# Loan
[docs] 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()
[docs] 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()
[docs] def on_LoanSearch__activate(self, action): self.run_dialog(LoanSearch, self.store)
[docs] def on_LoanSearchItems__activate(self, action): self.run_dialog(LoanItemSearch, self.store)
# Search
[docs] def on_SearchPurchaseReceiving__activate(self, button): self.run_dialog(PurchaseReceivingSearch, self.store)
[docs] def on_SearchTransfer__activate(self, action): self._search_transfers()
[docs] def on_SearchTransferItems__activate(self, action): self.run_dialog(TransferItemSearch, self.store)
[docs] def on_SearchPendingReturnedSales__activate(self, action): self._search_pending_returned_sales()
[docs] def on_SearchReturnedItems__activate(self, action): self.run_dialog(ReturnedItemSearch, self.store)
[docs] def on_SearchPurchasedStockItems__activate(self, action): self.run_dialog(PurchasedItemsSearch, self.store)
[docs] def on_SearchStockItems__activate(self, action): self.run_dialog(ProductStockSearch, self.store)
[docs] def on_SearchBrandItems__activate(self, action): self.run_dialog(ProductBrandSearch, self.store)
[docs] def on_SearchBrandItemsByBranch__activate(self, action): self.run_dialog(ProductBrandByBranchSearch, self.store)
[docs] def on_SearchBatchItems__activate(self, action): self.run_dialog(ProductBatchSearch, self.store)
[docs] def on_SearchClosedStockItems__activate(self, action): self.run_dialog(ProductClosedStockSearch, self.store)
[docs] def on_SearchProductHistory__activate(self, action): self.run_dialog(ProductSearchQuantity, self.store)
[docs] def on_SearchStockDecrease__activate(self, action): self.run_dialog(StockDecreaseSearch, self.store)