Source code for stoqlib.gui.wizards.inventorywizard

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

##
## Copyright (C) 2013 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>
##

import decimal
import logging

import gtk
from kiwi.datatypes import ValidationError
from kiwi.ui.objectlist import Column

from stoqlib.api import api
from stoqlib.domain.inventory import Inventory
from stoqlib.domain.product import StorableBatch
from stoqlib.domain.sellable import Sellable
from stoqlib.gui.base.dialogs import run_dialog
from stoqlib.gui.base.wizards import BaseWizard, BaseWizardStep
from stoqlib.gui.dialogs.batchselectiondialog import BatchSelectionDialog
from stoqlib.gui.wizards.abstractwizard import SellableItemStep
from stoqlib.lib.defaults import MAX_INT
from stoqlib.lib.message import warning
from stoqlib.lib.formatters import format_quantity
from stoqlib.lib.translation import stoqlib_gettext as _

log = logging.getLogger(__name__)


class _TemporaryInventoryItem(object):
    def __init__(self, sellable, storable, quantity, batch_number=None):
        self.sellable = sellable
        self.code = sellable.code
        self.description = sellable.description
        self.category_description = sellable.get_category_description()
        self.storable = storable
        self.is_batch = self.storable.is_batch
        self.batches = {}
        self.changed = False
        if batch_number is not None:
            self.add_or_update_batch(batch_number, quantity)
        elif not self.is_batch:
            self.quantity = quantity

    @property
    def quantity(self):
        if self.is_batch:
            return sum(quantity for quantity in self.batches.values())
        return self._quantity

    @quantity.setter
    def quantity(self, quantity):
        assert not self.is_batch
        self._quantity = quantity

    #
    #  Public API
    #

    def add_or_update_batch(self, batch_number, quantity):
        assert self.is_batch

        self.batches.setdefault(batch_number, 0)
        self.batches[batch_number] += quantity


class _InventoryBatchSelectionDialog(BatchSelectionDialog):
    show_existing_batches_list = False
    confirm_dialog_on_entry_activate = True
    allow_no_quantity = True

    #
    #  BatchSelectionDialog
    #

    def validate_entry(self, entry):
        # FIXME: For now we are not allowing not registered batches to be
        # counted. That makes sense though, but we should find a way to
        # validate the batch properly (since storable/batch_number should
        # be unique)
        batch_number = unicode(entry.get_text())
        if not batch_number:
            return

        if self.store.find(StorableBatch, batch_number=batch_number).is_empty():
            return ValidationError(
                _("The batch '%s' does not exist") % (batch_number,))


[docs]class InventoryCountTypeStep(BaseWizardStep): """Step responsible for defining the type of the count A simple step that will present 2 radiobutton options for the user to choose between an assisted count and a manual count. """ gladefile = 'InventoryCountTypeStep' def _read_import_file(self): data = {} with open(self.import_file.get_filename()) as fh: for line in fh: try: barcode, quantity = line[:-1].split(',') data[barcode] = int(quantity) except ValueError: warning(_('It was not possible to import inventory count.' ' Check file format')) return self.wizard.imported_count = data # # WizardEditorStep #
[docs] def next_step(self): self.wizard.temporary_items.clear() if self.import_count.get_active(): self._read_import_file() return InventoryCountItemStep(self.wizard, self, self.store, self.wizard.model)
# # Callbacks #
[docs] def on_manual_count__toggled(self, radio): self.wizard.manual_count = radio.get_active()
[docs] def on_import_count__toggled(self, radio): import_active = radio.get_active() self.import_file.set_sensitive(import_active) has_file = self.import_file.get_filename() self.wizard.refresh_next((import_active and has_file) or not import_active)
[docs] def on_import_file__file_set(self, chooser): self.wizard.refresh_next(chooser.get_filename())
[docs]class InventoryCountItemStep(SellableItemStep): """Step responsible for the real products counting This step will behave different, depending on the :class:`InventoryCountTypeStep`'s choice. For example: * If we choose to do a manual counting, all items will be populated and the user will be able to inform the quantity of each one * If we choose to do an assisted counting, no items will be populated (with the exception of the ones already counted before) and the user will be able to add items as it scans the barcode. Note that on the assisted count, pressing enter on the barcode will add the item on the list and not focus the quantity entry. That's done to make it easier for counting items using a barcode scanner. """ model_type = Inventory item_table = _TemporaryInventoryItem summary_label_text = "<b>%s</b>" % api.escape(_('Total quantity:')) summary_label_column = 'quantity' sellable_editable = False stock_labels_visible = False batch_selection_dialog = _InventoryBatchSelectionDialog add_sellable_on_barcode_activate = True # # SellableItemStep #
[docs] def post_init(self): super(InventoryCountItemStep, self).post_init() # We use this to check if the sellable the user is trying to add # really is on the inventory self._inventory_sellables = set(i[3] for i in self.model.get_inventory_data()) self.proxy.remove_widget('cost') self.cost.hide() self.cost_label.hide() self.hide_add_button() self.hide_edit_button() self.hide_del_button() if self.wizard.manual_count: self.hide_item_addition_toolbar() self.slave.klist.set_selection_mode(gtk.SELECTION_SINGLE) self.slave.klist.connect('cell-edited', self._on_klist__cell_edited) self.slave.klist.connect('row-activated', self._on_klist__row_activated) self.slave.klist.set_cell_data_func(self._on_klist__cell_data_func) self.force_validation()
[docs] def get_order_item(self, sellable, cost, quantity, batch=None, parent=None): if sellable not in self._inventory_sellables: return item = self.wizard.temporary_items.get(sellable, None) # We populated all items on get_saved_items, so this should not be None assert item is not None if batch is not None: assert isinstance(batch, basestring) item.add_or_update_batch(batch, quantity) else: item.quantity += quantity item.changed = True return item
[docs] def get_saved_items(self): data = self.model.get_inventory_data() for item, storable, product, sellable, batch in data: if (sellable in self.wizard.temporary_items and batch and item.counted_quantity is not None): tmp_item = self.wizard.temporary_items[sellable] tmp_item.add_or_update_batch(batch.batch_number, item.counted_quantity or 0) tmp_item.changed = (tmp_item.changed and item.counted_quantity is not None) elif sellable in self.wizard.temporary_items: continue else: quantity = (item.counted_quantity or self.wizard.imported_count.pop(sellable.barcode, 0)) tmp_item = _TemporaryInventoryItem(sellable, storable, quantity) tmp_item.changed = item.counted_quantity is not None or quantity self.wizard.temporary_items[sellable] = tmp_item yield tmp_item # There are counted itens in the imported file that were not in the # original inventory items. This means that this item was never stored # in this branch. for barcode, quantity in self.wizard.imported_count.items(): if not quantity: continue sellable = self.store.find(Sellable, barcode=unicode(barcode)).one() storable = sellable.product.storable item = self.model.add_storable(storable, 0) tmp_item = _TemporaryInventoryItem(sellable, storable, quantity) self.wizard.temporary_items[sellable] = tmp_item yield tmp_item
[docs] def get_batch_items(self): return []
[docs] def get_batch_order_items(self, sellable, value, quantity): if sellable not in self._inventory_sellables: return [] storable = sellable.product.storable available_batches = list( storable.get_available_batches(self.model.branch)) # The trivial case, where there's just one batch, we count it directly if len(available_batches) == 1: batch = available_batches[0] # Pass the batch number since it's what what this step is expecting return [self.get_order_item(sellable, value, quantity=quantity, batch=batch.batch_number)] return super(InventoryCountItemStep, self).get_batch_order_items( sellable, value, quantity)
[docs] def get_columns(self): adjustment = gtk.Adjustment(lower=0, upper=MAX_INT, step_incr=1, page_incr=10) return [ Column('code', title=_('Code'), data_type=str, sorted=True), Column('description', title=_('Description'), data_type=str, expand=True), Column('category_description', title=_('Category'), data_type=str), Column('quantity', title=_('Quantity'), data_type=decimal.Decimal, editable=True, spin_adjustment=adjustment, format_func=self._format_quantity, format_func_data=True), ]
[docs] def has_next_step(self): return False
[docs] def validate(self, value): super(InventoryCountItemStep, self).validate(value) # FIXME: Maybe we should not require all to be changed if # we are doing an assisted count self.wizard.refresh_next(value and any(i.changed for i in self.slave.klist))
# # Private # def _update_view(self): self.summary.update_total() self.force_validation() def _format_quantity(self, item, data): # FIXME: Why is this item sometimes None? It shouldn't ever be! if item is None: return '' if not item.changed: return '' return format_quantity(item.quantity) # # Callbacks # def _on_klist__cell_data_func(self, column, renderer, item, text): if column.attribute == 'quantity': renderer.set_property('editable-set', not item.is_batch) renderer.set_property('editable', not item.is_batch) return text def _on_klist__row_activated(self, storables, item): if item.is_batch: retval = run_dialog(_InventoryBatchSelectionDialog, self.wizard, store=self.store, model=item.storable, quantity=0, original_batches=item.batches) item.batches = retval or item.batches item.changed = item.changed or bool(retval) self.slave.klist.update(item) self._update_view() def _on_klist__cell_edited(self, klist, item, column): if column.attribute == 'quantity': item.changed = True self._update_view() # FIXME: This event is being emitted twice making it to jump # 2 rows below the current one. Remove this workaround when solving it if item != klist.get_selected(): return treeview = klist.get_treeview() rows, column = treeview.get_cursor() next_row = rows[0] + 1 if next_row < len(klist): treeview.set_cursor(next_row, column) else: self.wizard.next_button.grab_focus()
[docs] def on_barcode__activate(self, widget): barcode = widget.get_text() log.info('Inventory barcode activate: %s', barcode) self._try_get_sellable()
[docs]class InventoryCountWizard(BaseWizard): """A wizard for counting items on an |inventory|""" size = (800, 450) title = _('Inventory product counting') help_section = 'inventory-count' def __init__(self, store, model): self.temporary_items = {} self.imported_count = {} self.manual_count = True first_step = InventoryCountTypeStep(store, self, previous=None) BaseWizard.__init__(self, store, first_step, model) # # BaseWizard #
[docs] def finish(self): self._update_items() self.retval = self.model self.close()
# # Private # def _update_items(self): model_items = {} data = self.model.get_inventory_data() for item, storable, product, sellable, batch in data: batch_number = batch.batch_number if batch else None model_items[(sellable, batch_number)] = item for sellable, tmp_item in self.temporary_items.items(): if tmp_item.is_batch: for batch_number, quantity in tmp_item.batches.items(): try: # We will try to get the InventoryItem and update it item = model_items.pop((sellable, batch_number)) except KeyError: # If a KeyError happens, it means that we counted some # quantity for a batch that wasn't registered on stoq # yet, so add a new InventoryItem for it log.info('storable batch not in inventory: %r, %r' % (sellable.product.storable, batch_number)) # We add the new inventory item with the # recored_quantity=0 (ie, there was no stock item for # this batch) item = self.model.add_storable( sellable.product.storable, quantity=0, batch_number=batch_number) item.counted_quantity = quantity else: item = model_items.pop((sellable, None)) item.counted_quantity = tmp_item.quantity # Since we popped the items in here, those items not on the list are # considered to have 0 stock for sellable, item in model_items.items(): # None means it wasn't counted item.counted_quantity = 0