Source code for stoqlib.gui.search.productsearch

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

##
## Copyright (C) 2005-2007 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 Lesser 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 Lesser General Public License for more details.
##
## You should have received a copy of the GNU Lesser 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>
##
""" Search dialogs for product objects """

import datetime
from decimal import Decimal

import gtk
from kiwi.currency import currency
from kiwi.ui.objectlist import Column, ColoredColumn, COL_MODEL
from storm.expr import Eq

from stoqlib.api import api
from stoqlib.domain.person import Branch
from stoqlib.domain.product import (Product, ProductHistory,
                                    ProductStockItem)
from stoqlib.domain.sale import Sale
from stoqlib.domain.sellable import SellableCategory, Sellable
from stoqlib.domain.views import (ProductQuantityView,
                                  ProductFullStockItemView, SoldItemView,
                                  ProductFullWithClosedStockView,
                                  ProductClosedStockView,
                                  ProductBranchStockView,
                                  ProductBatchView, ProductBrandStockView,
                                  ProductBrandByBranchView)
from stoqlib.enums import SearchFilterPosition
from stoqlib.gui.base.gtkadds import change_button_appearance, set_bold
from stoqlib.gui.editors.producteditor import (ProductEditor,
                                               ProductStockEditor)
from stoqlib.gui.search.searchcolumns import SearchColumn, QuantityColumn
from stoqlib.gui.search.searchdialog import SearchDialog, SearchDialogPrintSlave
from stoqlib.gui.search.searcheditor import SearchEditor
from stoqlib.gui.search.searchfilters import (DateSearchFilter, ComboSearchFilter,
                                              Today)
from stoqlib.gui.search.searchresultview import SearchResultTreeView
from stoqlib.gui.search.sellablesearch import SellableSearch
from stoqlib.gui.utils.printing import print_report
from stoqlib.gui.wizards.productwizard import ProductCreateWizard
from stoqlib.lib.defaults import sort_sellable_code
from stoqlib.lib.formatters import format_quantity, get_formatted_cost
from stoqlib.lib.translation import stoqlib_gettext
from stoqlib.reporting.product import (ProductReport, ProductQuantityReport,
                                       ProductClosedStockReport,
                                       ProductPriceReport, ProductStockReport,
                                       ProductsSoldReport, ProductBrandReport,
                                       ProductBrandByBranchReport)

_ = stoqlib_gettext


class _ProductBrandResultTreeView(SearchResultTreeView):

    def __iter__(self):
        for item in self.iter_items():
            yield item

    # Overriding the method _add_results to avoid multiple consults to sql
    def add_result(self, result):
        if not isinstance(result, ProductBrandStockView):
            parent = self._cache.get(result.brand)
        else:
            parent = None

        if parent:
            self.add_result(parent)
        if not result in self:
            self.append(parent, result)
            if parent:
                self.expand(parent)

    def search_completed(self, results):
        if not results.is_empty():
            self._cache = {}
            for item in results[0].store.find(ProductBrandStockView):
                self._cache[item.brand] = item

        super(_ProductBrandResultTreeView, self).search_completed(results)

    def iter_items(self, include_parents=False):
        """ Set include_parents=True if the parents are needed """
        for obj in self.get_model():
            if include_parents:
                yield obj[COL_MODEL]
            for child in obj.iterchildren():
                yield child[COL_MODEL]


class _ProductSearchResultTreeView(SearchResultTreeView):

    # Overriding the method _add_results to avoid multiple consults to sql
    def add_result(self, result):
        if result.parent_id:
            parent = self._cache.get(result.parent_id)
            assert parent is not None
        else:
            parent = None

        if parent:
            self.add_result(parent)
        if not result in self:
            self.append(parent, result)
            if parent:
                self.expand(parent)

    def search_completed(self, results):
        results = list(results)
        if results:
            obj = results[0]
            # Build a cache of parent produts to make the results load faster.
            # Also, the parents will show up in the results even if they would
            # normally not, but if one of its children shows up.
            parents = obj.store.find(type(obj), Eq(Product.is_grid, True))
            self._cache = {}
            for item in parents:
                self._cache[item.id] = item

        super(_ProductSearchResultTreeView, self).search_completed(results)


[docs]class ProductSearch(SellableSearch): title = _('Product Search') search_spec = ProductFullWithClosedStockView editor_class = ProductEditor report_class = ProductReport has_branch_filter = True has_status_filter = True has_print_price_button = True csv_data = (_("Product"), _("product")) footer_ok_label = _('Add products') def __init__(self, store, hide_footer=True, hide_toolbar=False, hide_cost_column=False, hide_price_column=False, double_click_confirm=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 hide_cost_column: if it's True, no need to show the column 'cost' :param hide_price_column: if it's True no need to show the column 'price' """ # We only want to display data as a tree for ProductSearch, but # not setting 'tree = True' on the class definition as it has too many # subclasses for us to manually set to False on each one if self.__class__ is ProductSearch: self.result_view_class = _ProductSearchResultTreeView self.tree = True self.hide_cost_column = hide_cost_column self.hide_price_column = hide_price_column SellableSearch.__init__(self, store, hide_footer=hide_footer, hide_toolbar=hide_toolbar, double_click_confirm=double_click_confirm) if self.has_print_price_button: self._setup_print_slave() else: self._print_slave = None def _setup_print_slave(self): self._print_slave = SearchDialogPrintSlave() change_button_appearance(self._print_slave.print_price_button, gtk.STOCK_PRINT, _("_Price table")) self.attach_slave('print_holder', self._print_slave) self._print_slave.connect('print', self.on_print_price_button_clicked) self._print_slave.print_price_button.set_sensitive(False)
[docs] def on_print_price_button_clicked(self, button): print_report(ProductPriceReport, list(self.results), filters=self.search.get_search_filters(), branch_name=self.branch_filter.combo.get_active_text())
# # Private # def _get_status_values(self): return ([(_('Any'), None)] + [(v, k) for k, v in Sellable.statuses.items()]) # # ProductSearch #
[docs] def setup_widgets(self): super(ProductSearch, self).setup_widgets() if self.csv_data is not None: self.add_csv_button(*self.csv_data)
[docs] def create_filters(self): super(ProductSearch, self).create_filters() if self.has_branch_filter: branch_filter = self.create_branch_filter(_('In branch:')) self.add_filter(branch_filter, columns=[]) self.branch_filter = branch_filter else: self.branch_filter = None if self.has_status_filter: status_filter = self.create_sellable_filter() self.add_filter(status_filter, columns=['status'], position=SearchFilterPosition.TOP) self.status_filter = status_filter else: self.status_filter = None
[docs] def get_editor_class_for_object(self, obj): if obj is None: return ProductCreateWizard return self.editor_class
[docs] def get_editor_model(self, product_full_stock_view): return product_full_stock_view.product
[docs] def run_editor(self, obj): editor_class = self.get_editor_class_for_object(obj) if obj is None and issubclass(editor_class, ProductCreateWizard): return editor_class.run_wizard(self) return super(ProductSearch, self).run_editor(obj)
[docs] def get_columns(self): cols = [SearchColumn('code', title=_('Code'), data_type=str, sort_func=sort_sellable_code, sorted=True), SearchColumn('barcode', title=_('Barcode'), data_type=str), SearchColumn('category_description', title=_(u'Category'), data_type=str, width=120), SearchColumn('description', title=_(u'Description'), expand=True, data_type=str), SearchColumn('manufacturer', title=_('Manufacturer'), data_type=str, visible=False), SearchColumn('model', title=_('Model'), data_type=str, visible=False), SearchColumn('ncm', title=_('NCM'), data_type=str, visible=False), Column('unit', title=_('Unit'), data_type=str, visible=False), SearchColumn('location', title=_('Location'), data_type=str, visible=False), SearchColumn('status', title=_('Status'), data_type=str, valid_values=self._get_status_values(), visible=False), SearchColumn('brand', title=_('Brand'), data_type=str, visible=False), SearchColumn('family', title=_('Family'), data_type=str, visible=False), SearchColumn('internal_use', title=_('Is internal'), data_type=bool, visible=False)] # The price/cost columns must be controlled by hide_cost_column and # hide_price_column. Since the product search will be available across # the applications, it's important to restrict such columns depending # of the context. if not self.hide_cost_column: cols.append(SearchColumn('cost', _('Cost'), data_type=currency, format_func=get_formatted_cost, width=90)) if not self.hide_price_column: cols.append(SearchColumn('price', title=_('Price'), data_type=currency, width=90)) cols.append(QuantityColumn('stock', title=_('Stock'), use_having=True)) return cols
[docs] def executer_query(self, store): branch_id = self.branch_filter.get_state().value if branch_id is None: branch = None else: branch = store.get(Branch, branch_id) results = self.search_spec.find_by_branch(store, branch) return results.find(Eq(Product.is_composed, False))
# # Callbacks #
[docs] def on_results__has_rows(self, results, obj): if self._print_slave is not None: self._print_slave.print_price_button.set_sensitive(obj)
[docs]class ProductSearchQuantity(ProductSearch): title = _('Product History Search') search_spec = ProductQuantityView report_class = ProductQuantityReport csv_data = None advanced_search = False has_print_price_button = False show_production_columns = False text_field_columns = [ProductQuantityView.description] branch_filter_column = ProductQuantityView.branch def __init__(self, store, hide_footer=True, hide_toolbar=True): ProductSearch.__init__(self, store, hide_footer=hide_footer, hide_toolbar=hide_toolbar) # # ProductSearch #
[docs] def create_filters(self): # Date date_filter = DateSearchFilter(_('Date:')) date_filter.select(Today) columns = [ProductHistory.sold_date, ProductHistory.received_date, ProductHistory.production_date, ProductHistory.decreased_date] self.add_filter(date_filter, columns=columns) self.date_filter = date_filter
[docs] def get_columns(self): return [Column('code', title=_('Code'), data_type=str, sort_func=sort_sellable_code, sorted=True, width=130), Column('description', title=_('Description'), data_type=str, expand=True), QuantityColumn('quantity_sold', title=_('Sold'), visible=not self.show_production_columns), QuantityColumn('quantity_transfered', title=_('Transfered'), visible=not self.show_production_columns), QuantityColumn('quantity_received', title=_('Received'), visible=not self.show_production_columns), QuantityColumn('quantity_produced', title=_('Produced'), visible=self.show_production_columns), QuantityColumn('quantity_consumed', title=_('Consumed'), visible=self.show_production_columns), QuantityColumn('quantity_decreased', title=_('Manualy Decreased'), visible=self.show_production_columns), QuantityColumn('quantity_lost', title=_('Lost'), visible=self.show_production_columns, )]
[docs]class ProductsSoldSearch(ProductSearch): title = _('Products Sold Search') search_spec = SoldItemView report_class = ProductsSoldReport csv_data = None has_print_price_button = False advanced_search = False text_field_columns = [SoldItemView.description] branch_filter_column = Sale.branch_id def __init__(self, store, hide_footer=True, hide_toolbar=True): ProductSearch.__init__(self, store, hide_footer=hide_footer, hide_toolbar=hide_toolbar) # # Private API # def _update_summary(self, klist): quantity = total = 0 for obj in klist: quantity += obj.quantity total += obj.total_sold self.quantity_label.set_label(_(u'Total quantity: %s') % format_quantity(quantity)) self.total_sold_label.set_label(_(u'Total sold: %s') % get_formatted_cost(total)) # # ProductSearch #
[docs] def setup_widgets(self): super(ProductSearch, self).setup_widgets() hbox = gtk.HBox() hbox.set_spacing(6) self.vbox.pack_start(hbox, False, True) self.vbox.reorder_child(hbox, 2) self.vbox.set_spacing(6) hbox.pack_start(gtk.Label(), True, True) # Create two labels to show a summary for the search (kiwi's # SummaryLabel supports only one column) self.quantity_label = gtk.Label() hbox.pack_start(self.quantity_label, False, False) self.total_sold_label = gtk.Label() hbox.pack_start(self.total_sold_label, False, False) hbox.show_all() set_bold(self.quantity_label) set_bold(self.total_sold_label)
[docs] def create_filters(self): self.date_filter = DateSearchFilter(_('Date:')) self.date_filter.select(Today) self.add_filter(self.date_filter, columns=[Sale.confirm_date])
[docs] def get_columns(self): return [Column('code', title=_('Code'), data_type=str, sorted=True), Column('description', title=_('Description'), data_type=str, expand=True), QuantityColumn('quantity', title=_('Sold')), Column('average_cost', title=_('Avg. Cost'), data_type=currency), Column('total_sold', title=_('Total sold'), data_type=currency)]
# # Callbacks #
[docs] def on_search__search_completed(self, search, result_view, states): self._update_summary(result_view)
[docs]class ProductStockSearch(ProductSearch): title = _('Product Stock Search') # FIXME: This search needs another viewable, since ProductFullStockView # cannot filter the branch of the purchase, when counting the number of # purchased orders by branch search_spec = ProductFullStockItemView editor_class = ProductStockEditor report_class = ProductStockReport csv_data = None has_print_price_button = False has_new_button = False has_status_filter = False advanced_search = True # # ProductSearch #
[docs] def setup_widgets(self): super(ProductStockSearch, self).setup_widgets() difference_label = gtk.Label() difference_label.set_markup( "<small><b>%s</b></small>" % api.escape(_(u"The DIFFERENCE column is equal to " "IN STOCK minus MINIMUM columns"))) difference_label.show() self.search.vbox.pack_end(difference_label, False, False, 6)
[docs] def get_columns(self): return [SearchColumn('code', title=_('Code'), data_type=str, sort_func=sort_sellable_code), SearchColumn('category_description', title=_('Category'), data_type=str, width=100), 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), SearchColumn('location', title=_('Location'), data_type=str, visible=False), QuantityColumn('maximum_quantity', title=_('Maximum'), visible=False), QuantityColumn('minimum_quantity', title=_('Minimum')), QuantityColumn('stock', title=_('In Stock'), use_having=True), QuantityColumn('to_receive_quantity', title=_('To Receive')), ColoredColumn('difference', title=_('Difference'), color='red', format_func=format_quantity, data_type=Decimal, data_func=lambda x: x <= Decimal(0))]
[docs] def executer_query(self, store): branch_id = self.branch_filter.get_state().value if branch_id is None: branch = None else: branch = store.get(Branch, branch_id) return self.search_spec.find_by_branch(store, branch)
[docs]class ProductClosedStockSearch(ProductSearch): """A SearchEditor for Closed Products""" title = _('Closed Product Stock Search') search_spec = ProductClosedStockView report_class = ProductClosedStockReport has_status_filter = False has_print_price_button = False has_new_button = False def __init__(self, store, hide_footer=True, hide_toolbar=True, hide_cost_column=True, hide_price_column=True): ProductSearch.__init__(self, store, hide_footer, hide_toolbar, hide_cost_column=hide_cost_column, hide_price_column=hide_price_column)
[docs]class ProductBatchSearch(ProductSearch): title = _('Batch Search') search_spec = ProductBatchView has_print_price_button = False csv_data = (_('Batch'), _('batch')) fast_iter = True text_field_columns = [ProductBatchView.description, ProductBatchView.batch_number] branch_filter_column = ProductStockItem.branch_id unlimited_results = True def __init__(self, store, hide_footer=True, hide_toolbar=True): ProductSearch.__init__(self, store, hide_footer=hide_footer, hide_toolbar=hide_toolbar) # # ProductSearch #
[docs] def create_filters(self): # We need to overide to prevent the ProductSearch.setup_filters being # called pass
[docs] def setup_widgets(self): super(ProductBatchSearch, self).setup_widgets() self.search.set_summary_label('quantity', label=(u'Total:'), format='<b>%s</b>')
[docs] def get_columns(self): cols = [SearchColumn('category', title=_('Category'), data_type=str), SearchColumn('description', title=_('Description'), data_type=str, sorted=True, expand=True), SearchColumn('branch_name', title=_('Branch'), data_type=str, visible=False), 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('batch_number', title=_('Batch'), data_type=str), SearchColumn('batch_date', title=_('Date'), data_type=datetime.date), SearchColumn('quantity', title=_('Qty'), data_type=Decimal)] return cols
[docs]class ProductBrandSearch(SearchEditor): title = _('Brand Search') size = (775, 450) search_spec = ProductBrandStockView editor_class = ProductEditor report_class = ProductBrandReport text_field_columns = [ProductBrandStockView.brand] branch_filter_column = ProductStockItem.branch_id def __init__(self, store): SearchEditor.__init__(self, store, hide_footer=True, hide_toolbar=True) # # SearchDialog Hooks #
[docs] def setup_widgets(self): self.add_csv_button(_('Brands'), _('brands')) self.search.set_summary_label('quantity', label=_(u'Total:'), format='<b>%s</b>')
[docs] def create_filters(self): # Category categories = self.store.find(SellableCategory) items = api.for_combo(categories, attr='full_description') items.insert(0, (_('Any'), None)) category_filter = ComboSearchFilter(_('Category'), items) self.add_filter(category_filter, columns=[Sellable.category])
# # SearchEditor Hooks #
[docs] def get_columns(self): cols = [SearchColumn('brand', title=_('Brand'), data_type=str, sorted=True, expand=True), Column('quantity', title=_('Quantity'), data_type=Decimal)] return cols
[docs]class ProductBrandByBranchSearch(SearchDialog): title = _('Brand by Branch Search') size = (775, 450) search_spec = ProductBrandByBranchView report_class = ProductBrandByBranchReport result_view_class = _ProductBrandResultTreeView unlimited_results = True text_field_columns = [ProductBrandByBranchView.brand, ProductBrandByBranchView.company] # # SearchDialog Hooks #
[docs] def setup_widgets(self): self.add_csv_button(_('Brands'), _('brands')) self.search.set_summary_label('quantity', label=_(u'Total:'), format='<b>%s</b>')
[docs] def create_filters(self): self.search.set_query(self.executer_query) # Category categories = self.store.find(SellableCategory) items = api.for_combo(categories, attr='full_description') items.insert(0, (_('Any'), None)) category_filter = ComboSearchFilter(_('Category'), items) self.add_filter(category_filter, position=SearchFilterPosition.TOP) self.category_filter = category_filter
# # SearchEditor Hooks #
[docs] def get_columns(self): return [SearchColumn('brand', title=_('Brand'), data_type=str, sorted=True, expand=True), SearchColumn('company', title=_('Branch'), data_type=str), SearchColumn('manufacturer', title=_('Manufacturer'), data_type=str), SearchColumn('product_category', title=_('Category'), data_type=str), Column('quantity', title=_('Quantity'), data_type=Decimal)]
[docs] def executer_query(self, store): category_description = self.category_filter.get_state().value if category_description: category = category_description else: category = None return self.search_spec.find_by_category(store, category)
[docs] def print_report(self): print_report(self.report_class, self.results, list(self.results.iter_items(include_parents=True)), filters=self.search.get_search_filters())
[docs]class ProductBranchSearch(SearchDialog): """Show products in stock on all branchs """ title = _('Product Branch Search') size = (600, 500) search_spec = ProductBranchStockView advanced_search = False text_field_columns = [ProductBranchStockView.branch_name] def __init__(self, store, storable): self._storable = storable dialog_title = _("Stock of %s") % storable.product.description SearchDialog.__init__(self, store, title=dialog_title) self.search.refresh() # # SearchDialog Hooks #
[docs] def create_filters(self): self.search.set_query(self.executer_query)
# # SearchEditor Hooks #
[docs] def get_columns(self): return [Column('branch_name', title=_('Branch'), data_type=str, expand=True), QuantityColumn('stock', title=_('In Stock'))]
[docs] def executer_query(self, store): return self.search_spec.find_by_storable(store, self._storable)
[docs]def test(): # pragma: no cover from stoqlib.gui.base.dialogs import run_dialog ec = api.prepare_test() run_dialog(ProductSearch, None, ec.store)
if __name__ == '__main__': test()