# -*- 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>
##
##
""" Implementation of sellable search """
from decimal import Decimal
import gtk
from kiwi.currency import currency
from storm.expr import Ne
from stoqlib.api import api
from stoqlib.database.orm import ORMObject
from stoqlib.domain.product import ProductSupplierInfo, Product
from stoqlib.domain.sellable import Sellable
from stoqlib.domain.views import SellableFullStockView
from stoqlib.gui.dialogs.sellableimage import SellableImageViewer
from stoqlib.gui.editors.producteditor import ProductEditor
from stoqlib.gui.search.searchcolumns import (AccessorColumn, SearchColumn,
QuantityColumn)
from stoqlib.gui.search.searcheditor import SearchEditor
from stoqlib.gui.wizards.productwizard import ProductCreateWizard
from stoqlib.lib.defaults import sort_sellable_code
from stoqlib.lib.formatters import format_quantity
from stoqlib.lib.parameters import sysparam
from stoqlib.lib.translation import stoqlib_gettext
_ = stoqlib_gettext
[docs]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.SELECTION_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
#
[docs] def key_shift_Return(self):
self.confirm()
[docs] def key_control_Return(self):
self.confirm()
[docs] def key_shift_KP_Enter(self):
self.confirm()
[docs] def key_control_KP_Enter(self):
self.confirm()
[docs] def close(self):
# Make sure image viewer gets closed when this search closes
self._close_image_viewer()
super(SellableSearch, self).close()
[docs] 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)
[docs] def create_filters(self):
self.search.set_query(self.executer_query)
[docs] 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
[docs] 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)
[docs] def on_image_viewer_toggler__toggled(self, item):
if item.get_active():
self._open_image_viewer()
else:
self._close_image_viewer()
[docs] def on_results__right_click(self, klist, row, event):
self.popup.popup(None, None, None, event.button, event.time)
[docs] def on_results__selection_changed(self, klist, row):
self._update_image_viewer()
[docs]class SaleSellableSearch(SellableSearch):
footer_ok_label = _('_Add sale items')
has_new_button = False
def __init__(self, store, hide_footer=False, hide_toolbar=True,
search_str=None, search_query=None, info_message=None,
sale_items=None, quantity=None):
"""
:param sale_items: optionally, a list of sellables which will be
used to deduct stock values
:param quantity: the quantity of stock to add to the order,
is necessary to supply if you supply an order.
"""
self._quantity = quantity
self._first_search = True
self._first_search_string = search_str
self._current_sale_stock = {}
if sale_items:
if self._quantity is None:
raise TypeError("You need to specify a quantity "
"when supplying an order")
for item in sale_items:
if item.sellable.product_storable:
quantity = self._current_sale_stock.get(item.sellable.id, 0)
quantity += item.quantity
self._current_sale_stock[item.sellable.id] = quantity
SellableSearch.__init__(self, store, hide_footer=hide_footer,
hide_toolbar=hide_toolbar,
search_str=search_str,
search_query=search_query,
info_message=info_message)
#
# SellableSearch
#
[docs] def search_completed(self, results, states):
if not self._first_search:
if self._first_search_string != self.get_searchbar_search_string():
self.set_message(None)
if len(results) >= 1:
results.select(results[0])
self.search.focus_search_entry()
self._first_search = False
[docs] def executer_query(self, store):
results = super(SaleSellableSearch, self).executer_query(store)
# if we select a quantity which is not an integer, filter out
# sellables without a unit set
if self._quantity is not None and (self._quantity % 1) != 0:
results = results.find(Ne(Sellable.unit_id, None))
return results
[docs] def get_columns(self):
return [SearchColumn('code', title=_('Code'), data_type=str,
sort_func=sort_sellable_code,
sorted=True),
SearchColumn('barcode', title=_('Barcode'), data_type=str,
visible=False),
SearchColumn('description', title=_('Description'),
data_type=str, expand=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),
SearchColumn('price', title=_('Price'), data_type=currency,
justify=gtk.JUSTIFY_RIGHT, width=120),
SearchColumn('category_description', title=_('Category'),
data_type=str, visible=False),
AccessorColumn('stock', title=_(u'Stock'),
accessor=self._get_available_stock,
format_func=format_quantity, width=90,
data_type=Decimal)]
#
# Private
#
def _get_available_stock(self, sellable_view):
return sellable_view.stock - self._current_sale_stock.get(
sellable_view.id, 0)
[docs]class PurchaseSellableSearch(SellableSearch):
editor_class = ProductEditor
def __init__(self, store, hide_footer=False, hide_toolbar=False,
search_str=None, search_spec=None, search_query=None,
info_message=None, supplier=None):
self._supplier = supplier
SellableSearch.__init__(self, store, hide_footer=hide_footer,
hide_toolbar=hide_toolbar,
search_str=search_str, search_spec=search_spec,
search_query=search_query,
info_message=info_message)
[docs] def get_editor_model(self, model):
return model.product
[docs] def run_editor(self, obj=None):
store = api.new_store()
if not obj:
self.editor_class = ProductCreateWizard
product = self.editor_class.run_wizard(self)
product = product and store.fetch(product)
else:
self.editor_class = ProductEditor
product = self.run_dialog(self.editor_class, self, store,
store.fetch(obj), visual_mode=self._read_only)
# This means we are creating a new product. After that, add the
# current supplier as the supplier for this product
if (obj is None and product and
not product.is_supplied_by(self._supplier)):
ProductSupplierInfo(store=store,
supplier=store.fetch(self._supplier),
product=product,
base_cost=product.sellable.cost,
is_main_supplier=True)
if store.confirm(product):
# If the return value is an ORMObject, fetch it from
# the right connection
if isinstance(product, ORMObject):
product = self.store.get(type(product), product.id)
# If we created a new object, confirm the dialog automatically
if obj is None:
self.confirm(product)
store.close()
return
store.close()
return product