# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4
##
## Copyright (C) 2006-2009 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>
##
##
""" Receiving wizard definition """
import datetime
from decimal import Decimal
import gtk
from kiwi.currency import currency
from kiwi.ui.objectlist import Column
from storm.expr import And
from stoqlib.api import api
from stoqlib.domain.purchase import PurchaseOrder, PurchaseOrderView
from stoqlib.domain.receiving import ReceivingOrder
from stoqlib.gui.base.wizards import (WizardEditorStep, BaseWizard,
BaseWizardStep)
from stoqlib.gui.base.dialogs import run_dialog
from stoqlib.gui.slaves.receivingslave import ReceivingInvoiceSlave
from stoqlib.gui.dialogs.batchselectiondialog import BatchIncreaseSelectionDialog
from stoqlib.gui.dialogs.purchasedetails import PurchaseDetailsDialog
from stoqlib.gui.dialogs.labeldialog import SkipLabelsEditor
from stoqlib.gui.events import ReceivingOrderWizardFinishEvent
from stoqlib.gui.search.searchcolumns import IdentifierColumn, SearchColumn
from stoqlib.gui.search.searchslave import SearchSlave
from stoqlib.gui.utils.printing import print_labels
from stoqlib.lib.defaults import MAX_INT
from stoqlib.lib.formatters import format_quantity, get_formatted_cost
from stoqlib.lib.message import yesno, warning
from stoqlib.lib.translation import stoqlib_gettext
_ = stoqlib_gettext
class _TemporaryReceivingItem(object):
def __init__(self, item, parent_item=None):
self.purchase_item = item
self.code = item.sellable.code
self.barcode = item.sellable.barcode
self.description = item.sellable.description
self.category_description = item.sellable.get_category_description()
self.unit_description = item.sellable.unit_description
self.cost = item.cost
self.remaining_quantity = item.get_pending_quantity()
self.storable = item.sellable.product_storable
self.is_batch = self.storable and self.storable.is_batch
self.need_adjust_batch = self.is_batch
self.batches = {}
if not self.is_batch:
self.quantity = self.remaining_quantity
self.parent_item = parent_item
self.children_items = []
for child in item.children_items:
self.children_items.append(_TemporaryReceivingItem(child,
parent_item=self))
@property
def total(self):
return currency(self.cost * self.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
#
# Wizard Steps
#
[docs]class PurchaseSelectionStep(BaseWizardStep):
gladefile = 'PurchaseSelectionStep'
def __init__(self, wizard, store):
self._next_step = None
BaseWizardStep.__init__(self, store, wizard)
self.setup_slaves()
def _create_search(self):
self.search = SearchSlave(self._get_columns(),
restore_name=self.__class__.__name__,
store=self.store,
search_spec=PurchaseOrderView)
self.search.enable_advanced_search()
self.attach_slave('searchbar_holder', self.search)
executer = self.search.get_query_executer()
executer.add_query_callback(self.get_extra_query)
self._create_filters()
self.search.result_view.set_selection_mode(gtk.SELECTION_MULTIPLE)
self.search.result_view.connect('selection-changed',
self._on_results__selection_changed)
self.search.result_view.connect('row-activated',
self._on_results__row_activated)
self.search.focus_search_entry()
def _create_filters(self):
self.search.set_text_field_columns(['supplier_name', 'identifier_str'])
def _get_columns(self):
return [IdentifierColumn('identifier', title=_('Purchase #'), sorted=True),
SearchColumn('open_date', title=_('Date Started'),
data_type=datetime.date, width=100),
SearchColumn('expected_receival_date', data_type=datetime.date,
title=_('Expected Receival'), visible=False),
SearchColumn('supplier_name', title=_('Supplier'),
data_type=str, searchable=True, width=130,
expand=True),
SearchColumn('ordered_quantity', title=_('Qty Ordered'),
data_type=Decimal, width=110,
format_func=format_quantity),
SearchColumn('received_quantity', title=_('Qty Received'),
data_type=Decimal, width=145,
format_func=format_quantity),
SearchColumn('total', title=_('Order Total'),
data_type=currency, width=120)]
def _update_view(self):
selected_rows = self.search.result_view.get_selected_rows()
can_continue = len(set((v.supplier_id, v.branch_id) for v in selected_rows)) == 1
self.wizard.refresh_next(can_continue)
self.details_button.set_sensitive(len(selected_rows) == 1)
#
# WizardStep hooks
#
[docs] def post_init(self):
self._update_view()
self.force_validation()
[docs] def next_step(self):
self.search.save_columns()
selected_rows = self.search.result_view.get_selected_rows()
return ReceivingOrderItemStep(self.store, self.wizard, self,
selected_rows)
[docs] def has_previous_step(self):
return False
[docs] def setup_slaves(self):
self._create_search()
#
# Kiwi callbacks
#
def _on_results__selection_changed(self, results, purchase_order_view):
self.force_validation()
self._update_view()
def _on_results__row_activated(self, results, purchase_order_view):
run_dialog(PurchaseDetailsDialog, self.wizard, self.store,
model=purchase_order_view.purchase)
[docs]class ReceivingOrderItemStep(BaseWizardStep):
gladefile = 'ReceivingOrderItemStep'
model_type = ReceivingOrder
def __init__(self, store, wizard, previous_step, purchases):
self.purchases = purchases
BaseWizardStep.__init__(self, store, wizard, previous_step)
#
# WizardEditorStep
#
[docs] def post_init(self):
# If the user is comming back from the next, make sure things don't get
# messed
if self.store.savepoint_exists('before_receivinginvoice_step'):
self.store.rollback_to_savepoint('before_receivinginvoice_step')
self.edit_btn.set_sensitive(bool(self.purchase_items.get_selected()))
self.register_validate_function(self._validation_func)
self.force_validation()
self._setup_widgets()
self._update_view()
[docs] def next_step(self):
self.store.savepoint('before_receivinginvoice_step')
self._create_receiving_order()
self._create_receiving_items()
return ReceivingInvoiceStep(self.store, self.wizard, self.model, self)
[docs] def validate_step(self):
if any(i.need_adjust_batch for i in self.purchase_items):
warning(_("Before proceeding you need to adjust quantities for "
"the batch products (highlighted in red)"))
return False
return True
#
# Private
#
def _update_view(self):
self.total_received.update(self._get_total_received())
self.force_validation()
def _setup_widgets(self):
adjustment = gtk.Adjustment(lower=0, upper=MAX_INT, step_incr=1)
self.purchase_items.set_columns([
Column('code', title=_('Code'),
data_type=str, searchable=True, visible=False),
Column('barcode', title=_('Barcode'),
data_type=str, searchable=True, visible=False),
Column('description', title=_('Description'),
data_type=str, expand=True, searchable=True, sorted=True),
Column('category_description', title=_('Category'),
data_type=str, width=120),
Column('remaining_quantity', title=_('Qty'), data_type=int,
format_func=format_quantity, expand=True),
Column('quantity', title=_('Qty to receive'), data_type=int,
editable=True, spin_adjustment=adjustment,
format_func=format_quantity),
Column('unit_description', title=_('Unit'), data_type=str,
width=50),
Column('cost', title=_('Cost'), data_type=currency,
format_func=get_formatted_cost, width=90),
Column('total', title=_('Total'), data_type=currency, width=100)])
# We must clear the ObjectTree before
self.purchase_items.clear()
for item in self._get_pending_items(with_children=False):
self.purchase_items.append(None, item)
for child in item.children_items:
self.purchase_items.append(item, child)
self.purchase_items.set_cell_data_func(
self._on_purchase_items__cell_data_func)
def _get_pending_items(self, with_children=True):
for purchase_view in self.purchases:
for item in purchase_view.purchase.get_pending_items(with_children=with_children):
yield _TemporaryReceivingItem(item)
def _get_total_received(self):
return sum([item.total for item in self.purchase_items])
def _create_receiving_order(self):
# We only let the user get this far if the purchases select are for the
# same branch and supplier
supplier_id = self.purchases[0].supplier_id
branch_id = self.purchases[0].branch_id
# If the receiving is for another branch, we need a temporary identifier
temporary_identifier = None
if (api.sysparam.get_bool('SYNCHRONIZED_MODE') and
api.get_current_branch(self.store).id != branch_id):
temporary_identifier = ReceivingOrder.get_temporary_identifier(self.store)
# We cannot create the model in the wizard since we haven't
# selected a PurchaseOrder yet which ReceivingOrder depends on
# Create the order here since this is the first place where we
# actually have a purchase selected
self.wizard.model = self.model = ReceivingOrder(
identifier=temporary_identifier,
responsible=api.get_current_user(self.store),
supplier=supplier_id, invoice_number=None,
branch=branch_id, store=self.store)
for row in self.purchases:
self.model.add_purchase(row.purchase)
def _create_receiving_items(self):
for item in self.purchase_items:
if item.parent_item:
# Make sure we are adding parent_item first
continue
if item.is_batch:
for batch, quantity in item.batches.items():
self.model.add_purchase_item(
item.purchase_item,
quantity=quantity,
batch_number=batch)
elif item.quantity > 0:
parent_item = self.model.add_purchase_item(item.purchase_item,
item.quantity)
for child in item.children_items:
self.model.add_purchase_item(child.purchase_item,
quantity=child.quantity,
parent_item=parent_item)
def _edit_item(self, item):
retval = run_dialog(BatchIncreaseSelectionDialog, self.wizard,
store=self.store, model=item.storable,
quantity=item.remaining_quantity,
original_batches=item.batches)
item.batches = retval or item.batches
# Once we edited the batch item once, it's not obrigatory
# to edit it again.
item.need_adjust_batch = False
self.purchase_items.update(item)
self._update_view()
def _validation_func(self, value):
has_receivings = self._get_total_received() > 0
self.wizard.refresh_next(value and has_receivings)
#
# Callbacks
#
def _on_purchase_items__cell_data_func(self, column, renderer, obj, text):
renderer.set_property('sensitive', not obj.purchase_item.parent_item)
if not isinstance(renderer, gtk.CellRendererText):
return text
if column.attribute == 'quantity':
editable = not (obj.is_batch or obj.purchase_item.parent_item)
renderer.set_property('editable-set', editable)
renderer.set_property('editable', editable)
renderer.set_property('foreground', 'red')
renderer.set_property('foreground-set', obj.need_adjust_batch)
return text
[docs] def on_purchase_items__cell_edited(self, purchase_items, obj, attr):
for child in obj.children_items:
child.quantity = obj.quantity * child.remaining_quantity
self._update_view()
[docs] def on_purchase_items__cell_editing_started(self, purchase_items, obj,
attr, renderer, editable):
if attr == 'quantity':
adjustment = editable.get_adjustment()
# Don't let the user return more than was bought
adjustment.set_upper(obj.remaining_quantity)
[docs] def on_purchase_items__selection_changed(self, purchase_items, item):
self.edit_btn.set_sensitive(bool(item and item.is_batch))
[docs] def on_purchase_items__row_activated(self, purchase_items, item):
if not bool(item and item.is_batch):
return
self._edit_item(item)
[docs] def on_edit_btn__clicked(self, button):
item = self.purchase_items.get_selected()
self._edit_item(item)
[docs]class ReceivingInvoiceStep(WizardEditorStep):
gladefile = 'HolderTemplate'
model_type = ReceivingOrder
#
# WizardStep hooks
#
[docs] def has_next_step(self):
return False
[docs] def post_init(self):
self._is_valid = False
self.invoice_slave = ReceivingInvoiceSlave(self.store, self.model)
self.invoice_slave.connect('activate', self._on_invoice_slave__activate)
self.attach_slave("place_holder", self.invoice_slave)
# Slaves must be focused after being attached
self.invoice_slave.invoice_number.grab_focus()
self.register_validate_function(self._validate_func)
self.force_validation()
if not self.has_next_step():
self.wizard.enable_finish()
[docs] def validate_step(self):
create_freight_payment = self.invoice_slave.create_freight_payment()
self.model.update_payments(create_freight_payment)
return self.model
# Callbacks
def _validate_func(self, is_valid):
self._is_valid = is_valid
self.wizard.refresh_next(is_valid)
def _on_invoice_slave__activate(self, slave):
if self._is_valid:
self.wizard.finish()
#
# Main wizard
#
[docs]class ReceivingOrderWizard(BaseWizard):
title = _("Receive Purchase Order")
size = (750, 350)
need_cancel_confirmation = True
# help_section = 'purchase-new-receival'
def __init__(self, store):
self.model = None
first_step = PurchaseSelectionStep(self, store)
BaseWizard.__init__(self, store, first_step, self.model)
self.next_button.set_sensitive(False)
def _maybe_print_labels(self):
param = api.sysparam.get_string('LABEL_TEMPLATE_PATH')
if not param:
return
if not yesno(_(u'Do you want to print the labels for the received products?'),
gtk.RESPONSE_YES, _(u'Print labels'), _(u"Don't print")):
return
label_data = run_dialog(SkipLabelsEditor, self, self.store)
if label_data:
print_labels(label_data, self.store, receiving=self.model)
#
# WizardStep hooks
#
[docs] def finish(self):
assert self.model
assert self.model.branch
# Remove the items that will not be received now.
for item in self.model.get_items():
if item.quantity > 0:
continue
self.store.remove(item)
self.model.confirm()
self.retval = self.model
# Confirm before printing to avoid losing data if something breaks
self.store.confirm(self.retval)
self._maybe_print_labels()
ReceivingOrderWizardFinishEvent.emit(self.model)
self.close()