# -*- 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>
##
"""Wizard for work order pre-sales"""
import decimal
import pango
import gtk
from kiwi.currency import currency
from kiwi.ui.objectlist import Column
from kiwi.ui.widgets.combo import ProxyComboBox
from stoqlib.api import api
from stoqlib.domain.sale import Sale
from stoqlib.domain.workorder import (WorkOrder, WorkOrderCategory,
WorkOrderItem)
from stoqlib.gui.base.wizards import BaseWizardStep
from stoqlib.gui.slaves.workorderslave import WorkOrderQuoteSlave
from stoqlib.gui.widgets.notebookbutton import NotebookCloseButton
from stoqlib.gui.wizards.salequotewizard import (SaleQuoteWizard,
StartSaleQuoteStep,
SaleQuoteItemStep)
from stoqlib.lib.formatters import (format_sellable_description,
format_quantity, get_formatted_percentage)
from stoqlib.lib.message import warning
from stoqlib.lib.translation import stoqlib_gettext as _
# The of radio buttons that will fit confortably in the wizard. If the sale
# has more than this number of work orders, then it will be displayed as a
# combo box instead of radio buttons
_MAX_WORK_ORDERS_FOR_RADIO = 3
class _WorkOrderQuoteSlave(WorkOrderQuoteSlave):
# The description entry is needed here to describe each O.S.
show_description_entry = True
[docs]class WorkOrderQuoteStartStep(StartSaleQuoteStep):
"""First step for work order pre-sales
Just like
:class:`stoqlib.gui.wizards.salequotewizard.StartSaleQuoteStep`,
but the work order category can be selected on it and the next
step is :class:`.WorkOrderStep`
"""
gladefile = 'WorkOrderQuoteStartStep'
model_type = Sale
#
# StartSaleQuoteStep
#
[docs] def post_init(self):
self.client.mandatory = True
super(WorkOrderQuoteStartStep, self).post_init()
[docs] def next_step(self):
#self.wizard.wo_category = self.wo_categories.get_selected()
self.wizard.workorders = []
return WorkOrderQuoteWorkOrderStep(
self.store, self.wizard, self, self.model)
[docs] def setup_proxies(self):
self._fill_wo_categories_combo()
super(WorkOrderQuoteStartStep, self).setup_proxies()
#
# Private
#
def _fill_wo_categories_combo(self):
wo_categories = list(self.store.find(WorkOrderCategory))
self.wo_categories.color_attribute = 'color'
self.wo_categories.prefill(
api.for_combo(wo_categories, empty=_("No category")))
self.wo_categories.set_sensitive(len(wo_categories))
# We can use any work order, since all workorders in the same sale are
# sharing the same category.
workorder = WorkOrder.find_by_sale(self.store, self.model).any()
if workorder and workorder.category:
self.wo_categories.select(workorder.category)
#
# Callbacks
#
[docs] def on_wo_categories__content_changed(self, combo):
self.wizard.wo_category = combo.get_selected()
[docs]class WorkOrderQuoteWorkOrderStep(BaseWizardStep):
"""Second step for work order pre-sales
In this step, the sales person can/will create the workorder(s)
required for this sale (one for each spectacles)
"""
gladefile = 'WorkOrderQuoteWorkOrderStep'
def __init__(self, store, wizard, previous, model):
self.model = model
BaseWizardStep.__init__(self, store, wizard, previous)
self._work_order_ids = {0}
self._create_ui()
#
# Public API
#
[docs] def get_work_order_slave(self, work_order):
"""Get a slave for the |workorder|
This is the slave that will be added for each created work order.
Subclasses can override this to change it.
"""
# WorkOrderQuoteSlave needs this
self.edit_mode = self.wizard.edit_mode
return _WorkOrderQuoteSlave(self.store, work_order)
#
# BaseWizardStep
#
[docs] def post_init(self):
self.register_validate_function(self.wizard.refresh_next)
self.force_validation()
[docs] def next_step(self):
return WorkOrderQuoteItemStep(
self.wizard, self, self.store, self.model)
#
# Private
#
def _create_ui(self):
new_button = gtk.Button(gtk.STOCK_NEW)
new_button.set_use_stock(True)
new_button.set_relief(gtk.RELIEF_NONE)
new_button.show()
new_button.connect('clicked', self._on_new_work_order__clicked)
self.work_orders_nb.set_action_widget(new_button, gtk.PACK_END)
self.new_tab_button = new_button
saved_orders = list(WorkOrder.find_by_sale(self.store, self.model))
# This sale does not have any work order yet. Create the first for it
if not saved_orders:
self._add_work_order(self._create_work_order())
return
# This sale already have some orders, restore them so the user can edit
for order in saved_orders:
self._add_work_order(order)
def _create_work_order(self):
return WorkOrder(
store=self.store,
sale=self.model,
sellable=None,
description=u'',
branch=api.get_current_branch(self.store),
client=self.model.client)
def _add_work_order(self, work_order):
self.wizard.workorders.append(work_order)
work_order_id = max(self._work_order_ids) + 1
self._work_order_ids.add(work_order_id)
# TRANSLATORS: WO is short for Work Order
label = _('WO %d') % (work_order_id)
button = NotebookCloseButton()
hbox = gtk.HBox(spacing=6)
hbox.pack_start(gtk.Label(label))
hbox.pack_start(button)
hbox.show_all()
holder = gtk.EventBox()
holder.show()
slave = self.get_work_order_slave(work_order)
slave.close_button = button
self.work_orders_nb.append_page(holder, hbox)
self.attach_slave(label, slave, holder)
button.connect('clicked', self._on_remove_work_order__clicked, holder,
label, work_order, work_order_id)
def _remove_work_order(self, holder, name, work_order, work_order_id):
if work_order.is_finished():
warning(_("You cannot remove workorder with the status '%s'")
% work_order.status_str)
return
if not work_order.get_items().find().is_empty():
warning(_("This workorder already has items and cannot be removed"))
return
# We cannot remove the WO from the database (since it already has some
# history), but we can disassociate it from the sale, cancel and leave
# a reason for it.
reason = (_(u'Removed from sale %s') % work_order.sale.identifier)
work_order.sale = None
work_order.cancel(reason=reason)
self._work_order_ids.remove(work_order_id)
# Remove the tab
self.detach_slave(name)
pagenum = self.work_orders_nb.page_num(holder)
self.work_orders_nb.remove_page(pagenum)
# And remove the WO
self.wizard.workorders.remove(work_order)
self.force_validation()
#
# Kiwi callbacks
#
def _on_new_work_order__clicked(self, button):
self._add_work_order(self._create_work_order())
def _on_remove_work_order__clicked(self, button, slave_holder, slave_name,
work_order, work_order_id):
# FIXME: Hide the button from the
# Dont let the user remove the last WO
total_pages = self.work_orders_nb.get_n_pages()
if total_pages == 1:
return
self._remove_work_order(slave_holder, slave_name,
work_order, work_order_id)
[docs]class WorkOrderQuoteItemStep(SaleQuoteItemStep):
"""Third step for work order pre-sales
Just like :class:`stoqlib.gui.wizards.salequotewizard.SaleQuoteItemStep`,
but each item added here will be added to a workorder too (selected
on a combo).
"""
#
# Public API
#
[docs] def get_extra_columns(self):
"""Get some extra columns for the items list
Subclasses can override this and add some extra columns. Those
columns will be added just after the 'description' and before
the 'quantity' columns.
"""
return [Column('_equipment', title=_(u'Equipment'), data_type=str,
ellipsize=pango.ELLIPSIZE_END)]
[docs] def setup_work_order(self, work_order):
"""Do some extra setup for the work order
This is called at the initialization of this step. Subclasses can
override this to do any extra setup they need on the work order.
:param work_order: the |workorder| we are describing
"""
#
# SaleQuoteItemStep
#
[docs] def setup_proxies(self):
self._radio_group = None
self._setup_work_orders_widgets()
super(WorkOrderQuoteItemStep, self).setup_proxies()
[docs] def get_order_item(self, sellable, price, quantity, batch=None, parent=None):
item = super(WorkOrderQuoteItemStep, self).get_order_item(
sellable, price, quantity, batch=batch, parent=parent)
work_order = self._selected_workorder
wo_item = work_order.add_sellable(
sellable, price=price, batch=batch, quantity=quantity)
wo_item.sale_item = item
item._equipment = work_order.description
return item
[docs] def get_saved_items(self):
for item in super(WorkOrderQuoteItemStep, self).get_saved_items():
wo_item = WorkOrderItem.get_from_sale_item(self.store, item)
item._equipment = wo_item.order.description
yield item
[docs] def remove_items(self, items):
# Remove the workorder items first to avoid reference problems
for item in items:
wo_item = WorkOrderItem.get_from_sale_item(self.store, item)
# If the item's quantity_decreased changed in this step, the
# synchronization between the 2 that happens on self.validate_step
# would not have happened yet, meaning that order.remove_item
# would try to return a wrong quantity to the stock. Force the
# synchronization to avoid any problems like that
wo_item.quantity_decreased = item.quantity_decreased
wo_item.order.remove_item(wo_item)
super(WorkOrderQuoteItemStep, self).remove_items(items)
[docs] def get_columns(self):
columns = [
Column('sellable.code', title=_(u'Code'),
data_type=str, visible=False),
Column('sellable.barcode', title=_(u'Barcode'),
data_type=str, visible=False),
Column('sellable.description', title=_('Description'),
data_type=str, expand=True,
format_func=self._format_description, format_func_data=True),
]
columns.extend(self.get_extra_columns())
columns.extend([
Column('quantity', title=_(u'Quantity'),
data_type=decimal.Decimal, format_func=format_quantity),
Column('base_price', title=_('Original Price'), data_type=currency),
Column('price', title=_('Sale Price'), data_type=currency),
Column('sale_discount', title=_('Discount'),
data_type=decimal.Decimal,
format_func=get_formatted_percentage),
Column('total', title=_(u'Total'),
data_type=currency),
])
return columns
[docs] def validate_step(self):
# When finishing the wizard, make sure that all modifications on
# sale items on this step are propagated to their work order items
for sale_item in self.model.get_items():
wo_item = WorkOrderItem.get_from_sale_item(self.store, sale_item)
wo_item.quantity = sale_item.quantity
wo_item.quantity_decreased = sale_item.quantity_decreased
wo_item.price = sale_item.price
return super(WorkOrderQuoteItemStep, self).validate_step()
#
# Private
#
def _format_description(self, item, data):
return format_sellable_description(item.sellable, item.batch)
def _setup_work_orders_widgets(self):
self._work_orders_hbox = gtk.HBox(spacing=6)
self.item_vbox.pack_start(self._work_orders_hbox, False, True, 6)
self.item_vbox.reorder_child(self._work_orders_hbox, 0)
self._work_orders_hbox.show()
label = gtk.Label(_("Work order:"))
self._work_orders_hbox.pack_start(label, False, True)
data = []
for wo in self.wizard.workorders:
# The work order might be already approved if we are editing a sale
if wo.can_approve():
wo.approve()
self.setup_work_order(wo)
data.append([wo.description, wo])
if len(data) <= _MAX_WORK_ORDERS_FOR_RADIO:
self.work_orders_combo = None
for desc, wo in data:
self._add_work_order_radio(desc, wo)
else:
self.work_orders_combo = ProxyComboBox()
self.work_orders_combo.prefill(data)
self._selected_workorder = self.work_orders_combo.get_selected()
self._work_orders_hbox.pack_start(self.work_orders_combo,
False, False)
self._work_orders_hbox.show_all()
def _add_work_order_radio(self, desc, workorder):
radio = gtk.RadioButton(group=self._radio_group, label=desc)
radio.set_data('workorder', workorder)
radio.connect('toggled', self._on_work_order_radio__toggled)
if self._radio_group is None:
self._radio_group = radio
self._selected_workorder = workorder
self._work_orders_hbox.pack_start(radio, False, False, 6)
radio.show_all()
#
# Callbacks
#
[docs] def on_work_orders_combo__content_changed(self, combo):
self._selected_workorder = combo.get_selected()
def _on_work_order_radio__toggled(self, radio):
if not radio.get_active():
return
self._selected_workorder = radio.get_data('workorder')
[docs]class WorkOrderQuoteWizard(SaleQuoteWizard):
"""Wizard for work order pre-sales
This is similar to the regular pre-sale, but has an additional step to
create some workorders, and the item step is changed a little bit, to allow
the sales person to select in what work order the item should be added to.
"""
def __init__(self, store, model=None):
# Mimic BaseEditorSlave api
self.edit_mode = model is not None
self.wo_category = None
self.workorders = []
SaleQuoteWizard.__init__(self, store, model=model)
[docs] def get_title(self, model=None):
return _("Sale with work order")
[docs] def get_first_step(self, store, model):
return WorkOrderQuoteStartStep(store, self, model)
[docs] def finish(self):
for wo in self.workorders:
wo.client = self.model.client
wo.category = self.wo_category
super(WorkOrderQuoteWizard, self).finish()