# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4
##
## Copyright (C) 2006-2015 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>
##
""" Slaves for products """
import collections
from decimal import Decimal
import gtk
from kiwi.currency import currency
from kiwi.datatypes import ValidationError
from kiwi.enums import ListType
from kiwi.ui.objectlist import Column, SummaryLabel
from kiwi.ui.widgets.combo import ProxyComboBox
from kiwi.utils import gsignal
from storm.expr import Eq, And
from stoqlib.api import api
from stoqlib.domain.person import Supplier
from stoqlib.domain.product import (ProductSupplierInfo, ProductComponent,
ProductQualityTest, Product,
ProductManufacturer, Storable, GridGroup)
from stoqlib.domain.production import ProductionOrderProducingView
from stoqlib.domain.taxes import ProductTaxTemplate
from stoqlib.domain.views import ProductFullStockView
from stoqlib.gui.base.dialogs import run_dialog
from stoqlib.gui.base.lists import ModelListSlave
from stoqlib.gui.editors.baseeditor import (BaseEditorSlave,
BaseRelationshipEditorSlave)
from stoqlib.gui.editors.grideditor import GridAttributeEditor
from stoqlib.gui.editors.producteditor import (TemporaryProductComponent,
ProductComponentEditor,
QualityTestEditor,
ProductSupplierEditor,
ProductPackageComponentEditor)
from stoqlib.gui.fields import GridGroupField
from stoqlib.lib.decorators import cached_property
from stoqlib.lib.defaults import quantize, MAX_INT
from stoqlib.lib.formatters import get_formatted_cost
from stoqlib.lib.message import info, yesno, warning
from stoqlib.lib.parameters import sysparam
from stoqlib.lib.translation import stoqlib_gettext
_ = stoqlib_gettext
[docs]class ProductAttributeSlave(BaseEditorSlave):
gladefile = 'ProductAttributeSlave'
model_type = object
@cached_property()
def fields(self):
return collections.OrderedDict(
attribute_group=GridGroupField(_('Attribute group'), mandatory=True),
)
def __init__(self, store, model=None, visual_mode=False, edit_mode=None):
self._widgets = {}
self._create_attribute_box = None
super(ProductAttributeSlave, self).__init__(
store, model=model, visual_mode=visual_mode, edit_mode=edit_mode)
def _setup_widgets(self):
group = GridGroup.get_active_groups(self.store)
self.attribute_group.prefill(
api.for_combo(group, attr='description', empty=_("Select a group")))
def _add_attribute(self, attr):
if not attr.is_active:
# Do not attach the widget if the attribute is inactive
return
widget = gtk.CheckButton(label=attr.description)
widget.set_sensitive(attr.has_active_options())
self.main_box.pack_start(widget, expand=False)
widget.show()
self._widgets[widget] = attr
[docs] def setup_proxies(self):
self._setup_widgets()
[docs] def get_selected_attributes(self):
active_check_box = []
for widget, value in self._widgets.iteritems():
if widget.get_active():
active_check_box.append(value)
return active_check_box
def _refresh_attributes(self):
if self._create_attribute_box is not None:
self.main_box.remove(self._create_attribute_box)
self._create_attribute_box.destroy()
for check_box in self._widgets.keys():
self.main_box.remove(check_box)
check_box.destroy()
# After we destroy the check buttons we should reset the list of widgets
self._widgets = {}
group = self.attribute_group.get_selected()
if not group:
return
for attr in group.attributes:
self._add_attribute(attr)
self._create_attribute_box = gtk.HBox()
btn = gtk.Button(_("Add a new attribute"))
btn.connect('clicked', self._on_add_new_attribute_btn__clicked)
self._create_attribute_box.pack_start(btn, expand=False)
self._create_attribute_box.pack_start(gtk.Label(), expand=True)
self._create_attribute_box.show_all()
self.main_box.pack_start(self._create_attribute_box, expand=False)
#
# Kiwi Callbacks
#
[docs] def on_attribute_group__content_changed(self, widget):
self._refresh_attributes()
def _on_add_new_attribute_btn__clicked(self, btn):
group = self.attribute_group.get_selected()
with api.new_store() as store:
retval = run_dialog(GridAttributeEditor,
parent=None, store=store,
group=store.fetch(group))
if retval:
self._refresh_attributes()
[docs]class ProductGridSlave(BaseEditorSlave):
gladefile = 'ProductGridSlave'
model_type = Product
def __init__(self, store, model, visual_mode=False):
self._attr_list = list(model.attributes)
self._option_list = {}
self._widgets = {}
BaseEditorSlave.__init__(self, store, model, visual_mode)
def _setup_widgets(self):
self.attr_table.resize((len(self._attr_list) / 3) + 1, 6)
for pos, attribute in enumerate(self._attr_list):
self._add_options(attribute, pos)
self.add_product_button.set_sensitive(False)
self.product_list.set_columns(self._get_columns())
self.product_list.add_list(self.model.children)
def _add_options(self, attr, pos):
combo = ProxyComboBox()
label = gtk.Label(attr.attribute.description + u':')
label.set_alignment(xalign=1, yalign=0.5)
# This dictionary is populated with the purpose of tests
self._widgets[attr.attribute.description] = combo
# Use 3 labels per row
row_pos = pos / 3
col_pos = 2 * (pos % 3)
self.attr_table.attach(label, col_pos, col_pos + 1, row_pos, row_pos + 1,
gtk.EXPAND | gtk.FILL, 0, 0, 0)
self.attr_table.attach(combo, col_pos + 1, col_pos + 2, row_pos, row_pos + 1,
gtk.EXPAND | gtk.FILL, 0, 0, 0)
self.attr_table.show_all()
self._fill_options(combo, attr)
combo.connect('changed', self._on_combo_selection__changed)
def _fill_options(self, widget, attr):
options = attr.options.find(is_active=True)
widget.prefill(api.for_combo(options, empty=_("Select an option"),
sorted=False))
def _get_columns(self):
return [Column('description', title=_('Description'), data_type=str,
expand=True, sorted=True),
Column('sellable.code', title=_('Code'), data_type=str)]
[docs] def can_add(self):
selected_option = self._option_list.values()
# In order to add a new product...
# ...The user should select all options...
if len(selected_option) != len(self._attr_list):
return False
# ...and he should have selected valid options
if not all(selected_option):
return False
# Also, make sure a product with those options doesn't exists.
child_exists = self.model.child_exists(selected_option)
if child_exists:
return False
return True
[docs] def setup_proxies(self):
self._setup_widgets()
#
# Kiwi Callbacks
#
def _on_combo_selection__changed(self, widget):
self._option_list[widget] = widget.get_selected()
self.add_product_button.set_sensitive(self.can_add())
[docs]class ProductTaxSlave(BaseEditorSlave):
gladefile = 'ProductTaxSlave'
model_type = Product
proxy_widgets = ['icms_template', 'ipi_template', 'pis_template',
'cofins_template']
[docs] def update_visual_mode(self):
self.icms_template.set_sensitive(False)
self.ipi_template.set_sensitive(False)
self.pis_template.set_sensitive(False)
self.cofins_template.set_sensitive(False)
def _fill_combo(self, combo, type):
types = [(None, None)]
types.extend([(t.name, t.get_tax_model()) for t in
self.store.find(ProductTaxTemplate, tax_type=type)])
combo.prefill(types)
def _setup_widgets(self):
self._fill_combo(self.icms_template, ProductTaxTemplate.TYPE_ICMS)
self._fill_combo(self.ipi_template, ProductTaxTemplate.TYPE_IPI)
self._fill_combo(self.pis_template, ProductTaxTemplate.TYPE_PIS)
self._fill_combo(self.cofins_template, ProductTaxTemplate.TYPE_COFINS)
[docs] def setup_proxies(self):
self._setup_widgets()
self.proxy = self.add_proxy(self.model, self.proxy_widgets)
if self.model.parent is not None:
self._disable_child_widgets()
def _disable_child_widgets(self):
self.icms_template.set_property('sensitive', False)
self.ipi_template.set_property('sensitive', False)
self.pis_template.set_property('sensitive', False)
self.cofins_template.set_property('sensitive', False)
[docs]class ProductComponentSlave(BaseEditorSlave):
gladefile = 'ProductComponentSlave'
model_type = TemporaryProductComponent
proxy_widgets = ['production_time', 'yield_quantity']
gsignal('cost-updated', object)
def __init__(self, store, product=None, visual_mode=False):
self._product = product
self._remove_component_list = []
# Temporary component value to prevent emmiting the signal when not
# necessary
self._previous_value = product.sellable.cost if product else 0
BaseEditorSlave.__init__(self, store, model=None, visual_mode=visual_mode)
def _get_columns(self):
return [Column('code', title=_(u'Code'), data_type=int,
expander=True, sorted=True),
Column('quantity', title=_(u'Quantity'),
data_type=Decimal),
Column('unit', title=_(u'Unit'), data_type=str),
Column('description', title=_(u'Description'),
data_type=str, expand=True),
Column('category', title=_(u'Category'), data_type=str),
# Translators: Ref. is for Reference (as in design reference)
Column('design_reference', title=_(u'Ref.'), data_type=str),
Column('production_cost', title=_(u'Production Cost'),
format_func=get_formatted_cost, data_type=currency),
Column('total_production_cost', title=_(u'Total'),
format_func=get_formatted_cost, data_type=currency),
]
def _setup_widgets(self):
is_package = self.model.product.is_package
component_list = list(self._get_products(additional_query=is_package))
if component_list:
self.component_combo.prefill(component_list)
else:
self.sort_components_check.set_sensitive(False)
self.yield_quantity.set_adjustment(
gtk.Adjustment(lower=0, upper=MAX_INT, step_incr=1))
self.component_tree.set_columns(self._get_columns())
self._populate_component_tree()
self.component_label = SummaryLabel(
klist=self.component_tree,
column='total_production_cost',
label='<b>%s</b>' % api.escape(_(u'Total:')),
value_format='<b>%s</b>')
self.component_label.show()
self.component_tree_vbox.pack_start(self.component_label, False)
self.info_label.set_bold(True)
self._update_widgets()
if self.visual_mode:
self.component_combo.set_sensitive(False)
self.add_button.set_sensitive(False)
self.sort_components_check.set_sensitive(False)
def _get_products(self, sort_by_name=True, additional_query=False):
# FIXME: This is a kind of workaround until we have the
# SQLCompletion funcionality, then we will not need to sort the
# data.
if sort_by_name:
attr = ProductFullStockView.description
else:
attr = ProductFullStockView.category_description
products = []
query = Eq(Product.is_grid, False)
if additional_query:
# XXX For now, we are not allowing package_product to have another
# package_product or batch_product as component
query = And(query, Eq(Storable.is_batch, False))
for product_view in self.store.find(ProductFullStockView, query).order_by(attr):
if product_view.product is self._product:
continue
description = product_view.get_product_and_category_description()
products.append((description, product_view.product))
return products
def _update_widgets(self):
has_selected = self.component_combo.read() is not None
self.add_button.set_sensitive(has_selected)
has_selected = self.component_tree.get_selected() is not None
self.edit_button.set_sensitive(has_selected)
self.remove_button.set_sensitive(has_selected)
value = self.get_component_cost()
if self._previous_value != value:
self._previous_value = value
self.emit('cost-updated', value)
self.component_label.set_value(get_formatted_cost(value))
if not self._validate_components():
self.component_combo.set_sensitive(False)
self.add_button.set_sensitive(False)
self.edit_button.set_sensitive(False)
self.remove_button.set_sensitive(False)
self.info_label.set_text(_(u"This product is being produced. "
"Can't change components."))
def _populate_component_tree(self):
self._add_to_component_tree()
def _get_components(self, product):
for component in self.store.find(ProductComponent, product=product):
yield TemporaryProductComponent(
self.store,
product=component.product,
component=component.component,
quantity=component.quantity,
design_reference=component.design_reference,
price=component.price)
def _add_to_component_tree(self, component=None):
parent = None
if component is None:
# load all components that already exists
subcomponents = self._get_components(self._product)
else:
if component not in self.component_tree:
self.component_tree.append(None, component)
subcomponents = self._get_components(component.component)
parent = component
for subcomponent in subcomponents:
self.component_tree.append(parent, subcomponent)
# recursively add the children
self._add_to_component_tree(subcomponent)
def _can_add_component(self, component):
if component.component.is_composed_by(self._product):
return False
return True
def _run_product_component_dialog(self, product_component=None):
update = True
if product_component is None:
update = False
component = self.component_combo.get_selected_data()
product_component = TemporaryProductComponent(
self.store,
product=self._product, component=component)
# If we try to add a component which is already in tree,
# just edit it
for component in self.component_tree:
if component.component == product_component.component:
update = True
product_component = component
break
if not self._can_add_component(product_component):
product_desc = self._product.sellable.get_description()
component_desc = product_component.description
info(_(u'You can not add this product as component, since '
'%s is composed by %s' % (component_desc, product_desc)))
return
toplevel = self.get_toplevel().get_toplevel()
if self.model.product.is_package:
model = run_dialog(ProductPackageComponentEditor, toplevel,
self.store, product_component)
else:
# We cant use savepoint here, since product_component
# is not an ORM object.
model = run_dialog(ProductComponentEditor, toplevel, self.store,
product_component)
if not model:
return
if update:
self.component_tree.update(model)
else:
self._add_to_component_tree(model)
self._update_widgets()
def _edit_component(self):
# Only allow edit the root components, since its the component
# that really belongs to the current product
selected = self.component_tree.get_selected()
root = self.component_tree.get_root(selected)
self._run_product_component_dialog(root)
def _totally_remove_component(self, component):
descendants = self.component_tree.get_descendants(component)
for descendant in descendants:
# we can not remove an item twice
if descendant not in self.component_tree:
continue
else:
self._totally_remove_component(descendant)
self.component_tree.remove(component)
def _remove_component(self, component):
# Only allow remove the root components, since its the component
# that really belongs to the current product
root_component = self.component_tree.get_root(component)
msg = _("This will remove the component \"%s\". Are you sure?") % (
root_component.description)
if not yesno(msg, gtk.RESPONSE_NO,
_("Remove component"),
_("Keep component")):
return
self._remove_component_list.append(root_component)
self._totally_remove_component(root_component)
self._update_widgets()
def _validate_components(self):
return not ProductionOrderProducingView.is_product_being_produced(
self.model.product)
#
# BaseEditorSlave
#
[docs] def setup_proxies(self):
self._setup_widgets()
self.proxy = self.add_proxy(self._product, self.proxy_widgets)
[docs] def create_model(self, store):
return TemporaryProductComponent(self.store, product=self._product)
[docs] def on_confirm(self):
for component in self._remove_component_list:
component.delete_product_component(self.store)
for component in self.component_tree:
component.add_or_update_product_component(self.store)
self._product.update_sellable_price()
[docs] def validate_confirm(self):
if not len(self.component_tree) > 0:
info(_(u'There is no component in this product.'))
return False
return True
[docs] def get_component_cost(self):
value = Decimal('0')
for component in self.component_tree:
if self.component_tree.get_parent(component):
continue
value += component.get_total_production_cost()
value = value / self._product.yield_quantity
return quantize(value, precision=sysparam.get_int('COST_PRECISION_DIGITS'))
#
# Kiwi Callbacks
#
[docs] def on_yield_quantity__validate(self, widget, value):
if value <= 0:
return ValidationError(_(u'The value must be positive.'))
[docs] def after_yield_quantity__changed(self, widget):
self._update_widgets()
[docs] def on_component_combo__content_changed(self, widget):
self._update_widgets()
[docs] def on_component_tree__selection_changed(self, widget, value):
if self.visual_mode:
return
self._update_widgets()
[docs] def on_component_tree__row_activated(self, widget, selected):
if self.visual_mode:
return
if not self._validate_components():
return
self._edit_component()
[docs] def on_component_tree__row_expanded(self, widget, value):
if self.visual_mode:
return
self._update_widgets()
[docs] def on_sort_components_check__toggled(self, widget):
sort_by_name = not widget.get_active()
is_package = self.model.product.is_package
self.component_combo.prefill(
self._get_products(sort_by_name=sort_by_name,
additional_query=is_package))
self.component_combo.select_item_by_position(0)
[docs]class ProductPackageSlave(ProductComponentSlave):
def _setup_widgets(self):
super(ProductPackageSlave, self)._setup_widgets()
self.production_time_box.hide()
def _get_columns(self):
return [Column('code', title=_(u'Code'), data_type=int,
expander=True, sorted=True),
Column('quantity', title=_(u'Quantity'),
data_type=Decimal),
Column('unit', title=_(u'Unit'), data_type=str),
Column('description', title=_(u'Description'),
data_type=str, expand=True),
Column('category', title=_(u'Category'), data_type=str),
Column('total_production_cost', title=_(u'Total'),
format_func=get_formatted_cost, data_type=currency),
Column('price', title=_(u'Price'),
format_func=get_formatted_cost, data_type=currency)]
[docs]class ProductQualityTestSlave(ModelListSlave):
model_type = ProductQualityTest
editor_class = QualityTestEditor
columns = [
Column('description', title=_(u'Description'),
data_type=str, expand=True),
Column('type_str', title=_(u'Type'), data_type=str),
Column('success_value_str', title=_(u'Success Value'), data_type=str),
]
def __init__(self, parent, store, product,
visual_mode=False, reuse_store=True):
self._product = product
ModelListSlave.__init__(self, parent, store=store,
reuse_store=reuse_store)
if visual_mode:
self.set_list_type(ListType.READONLY)
self.refresh()
#
# ListSlave Implementation
#
[docs] def populate(self):
return self._product.quality_tests
[docs] def run_editor(self, store, model):
return self.run_dialog(self.editor_class, store=store, model=model,
product=self._product)
[docs] def remove_item(self, item):
# If the test was used before in a production, it cannot be
# removed
if not item.can_remove():
warning(_(u'You can not remove this test, since it\'s already '
'been used.'))
return False
return ModelListSlave.remove_item(self, item)
[docs]class ProductSupplierSlave(BaseRelationshipEditorSlave):
"""A slave for changing the suppliers for a product.
"""
target_name = _(u'Supplier')
editor = ProductSupplierEditor
model_type = ProductSupplierInfo
def __init__(self, store, product, visual_mode=False):
self._product = product
BaseRelationshipEditorSlave.__init__(self, store, visual_mode=visual_mode)
suggested = sysparam.get_object(store, 'SUGGESTED_SUPPLIER')
if suggested is not None:
self.target_combo.select(suggested)
if self._product.parent is not None:
self._disable_child_widgets()
[docs] def get_targets(self):
suppliers = Supplier.get_active_suppliers(self.store)
return api.for_person_combo(suppliers)
[docs] def get_relations(self):
return self._product.get_suppliers_info()
[docs] def get_columns(self):
return [Column('name', title=_(u'Supplier'),
data_type=str, expand=True, sorted=True),
Column('supplier_code', title=_(u'Product Code'),
data_type=str),
Column('lead_time_str', title=_(u'Lead time'), data_type=str),
Column('minimum_purchase', title=_(u'Minimum Purchase'),
data_type=Decimal),
Column('base_cost', title=_(u'Cost'), data_type=currency,
format_func=get_formatted_cost)]
[docs] def create_model(self):
product = self._product
supplier = self.target_combo.get_selected_data()
if product.is_supplied_by(supplier):
product_desc = self._product.sellable.get_description()
info(_(u'%s is already supplied by %s') % (product_desc,
supplier.person.name))
return
model = ProductSupplierInfo(product=product,
supplier=supplier,
store=self.store)
model.base_cost = product.sellable.cost
return model
def _disable_child_widgets(self):
self.add_button.set_property('sensitive', False)
self.target_combo.set_property('sensitive', False)
self.relations_list.set_list_type(ListType.READONLY)