Source code for stoqlib.gui.editors.producteditor

# -*- 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>
##
""" Editors definitions for products"""

import collections
from decimal import Decimal

import gtk
from kiwi.currency import currency
from kiwi.datatypes import ValidationError
from kiwi.python import Settable
from kiwi.ui.forms import MultiLineField, NumericField, PriceField, TextField

from stoqdrivers.enum import TaxType

from stoqlib.api import api
from stoqlib.domain.inventory import Inventory
from stoqlib.domain.product import (ProductSupplierInfo, Product,
                                    ProductComponent,
                                    ProductQualityTest, Storable,
                                    ProductManufacturer, ProductAttribute)
from stoqlib.domain.sellable import (Sellable,
                                     SellableTaxConstant)
from stoqlib.gui.base.dialogs import run_dialog
from stoqlib.gui.base.messagebar import MessageBar
from stoqlib.gui.editors.baseeditor import BaseEditor
from stoqlib.gui.editors.sellableeditor import SellableEditor
from stoqlib.gui.slaves.imageslave import ImageGallerySlave
from stoqlib.lib.decorators import cached_property
from stoqlib.lib.defaults import quantize, MAX_INT
from stoqlib.lib.message import info
from stoqlib.lib.parameters import sysparam
from stoqlib.lib.translation import stoqlib_gettext

_ = stoqlib_gettext


#
# Slaves
#

[docs]class TemporaryProductComponent(object): def __init__(self, store, product=None, component=None, quantity=Decimal(1), design_reference=u'', price=Decimal(0)): self.product = product self.component = component self.quantity = quantity self.design_reference = design_reference if self.component is not None: # keep this values in memory in order to speed up the # data access sellable = self.component.sellable self.id = sellable.id self.code = sellable.code self.description = sellable.get_description() self.category = sellable.get_category_description() self.unit = sellable.unit_description self.production_cost = self.component.get_production_cost() product_component = self._get_product_component(store) self.price = product_component.price if product_component else price else: self.price = price def _get_product_component(self, store): return store.find(ProductComponent, product=self.product, component=self.component).one() # # Public API #
[docs] def get_total_production_cost(self): cost = self.price or self.production_cost return quantize(cost * self.quantity)
[docs] def delete_product_component(self, store): component = self._get_product_component(store) if component is not None: # FIXME: bug 5581 Check if we can really remove this object when # working with synced databases store.remove(component)
[docs] def add_or_update_product_component(self, store): component = self._get_product_component(store) if component is not None: # updating component.quantity = self.quantity component.design_reference = self.design_reference component.price = self.price else: # adding ProductComponent(product=self.product, component=self.component, quantity=self.quantity, design_reference=self.design_reference, store=store, price=self.price)
# # Quality Test Editor & Slave #
[docs]class QualityTestEditor(BaseEditor): model_name = _('Quality Test') model_type = ProductQualityTest gladefile = 'QualityTestEditor' proxy_widgets = ['description', 'test_type'] confirm_widgets = ['description'] def __init__(self, store, model=None, product=None): self._product = product BaseEditor.__init__(self, store=store, model=model) def _setup_widgets(self): self.sizegroup1.add_widget(self.decimal_value) self.sizegroup1.add_widget(self.boolean_value) self.test_type.prefill([(value, key) for key, value in ProductQualityTest.types.items()]) self.boolean_value.prefill([(_('True'), True), (_(('False')), False)]) # Editing values if self.model.test_type == ProductQualityTest.TYPE_BOOLEAN: self.boolean_value.select(self.model.get_boolean_value()) else: min_value, max_value = self.model.get_range_value() self.min_value.set_value(min_value) self.max_value.set_value(max_value)
[docs] def create_model(self, store): return ProductQualityTest(product=self._product, store=store)
[docs] def setup_proxies(self): self._setup_widgets() self.proxy = self.add_proxy(self.model, self.proxy_widgets)
[docs] def on_confirm(self): if self.model.test_type == ProductQualityTest.TYPE_BOOLEAN: self.model.set_boolean_value(self.boolean_value.read()) else: self.model.set_range_value(self.min_value.read(), self.max_value.read())
# # Callbacks #
[docs] def on_test_type__changed(self, widget): if self.model.test_type == ProductQualityTest.TYPE_BOOLEAN: self.boolean_value.show() self.decimal_value.hide() else: self.boolean_value.hide() self.decimal_value.show()
# # Product Supplier Editor & Slave #
[docs]class ProductSupplierEditor(BaseEditor): model_name = _('Product Supplier') model_type = ProductSupplierInfo gladefile = 'ProductSupplierEditor' proxy_widgets = ('base_cost', 'icms', 'notes', 'lead_time', 'minimum_purchase', 'supplier_code') confirm_widgets = ['base_cost', 'icms', 'lead_time', 'minimum_purchase', 'supplier_code'] def _setup_widgets(self): unit = self.model.product.sellable.unit if unit is None: description = _(u'Unit(s)') else: description = unit.description self.unit_label.set_text(description) self.base_cost.set_digits(sysparam.get_int('COST_PRECISION_DIGITS')) self.base_cost.set_adjustment( gtk.Adjustment(lower=0, upper=MAX_INT, step_incr=1)) self.minimum_purchase.set_adjustment( gtk.Adjustment(lower=0, upper=MAX_INT, step_incr=1)) # # BaseEditor hooks #
[docs] def setup_proxies(self): self._setup_widgets() self.proxy = self.add_proxy(self.model, self.proxy_widgets)
[docs] def validate_confirm(self): return self.base_cost.read() > 0
# # Kiwi handlers #
[docs] def on_minimum_purchase__validate(self, entry, value): if not value or value <= Decimal(0): return ValidationError(_("Minimum purchase must be greater than " "zero."))
[docs] def on_base_cost__validate(self, entry, value): if not value or value <= currency(0): return ValidationError(_("Value must be greater than zero."))
[docs] def on_lead_time__validate(self, entry, value): if value < 1: return ValidationError(_("Lead time must be greater or equal one " "day"))
[docs] def on_supplier_code__validate(self, entry, value): if not value: return d = {self.model_type.supplier_id: self.model.supplier.id, self.model_type.supplier_code: value} supplier_info = self.model.check_unique_tuple_exists(d) if supplier_info is not None: desc = supplier_info.product.sellable.description return ValidationError( _("This code already exists for this supplier " "on product '%s'") % (desc, ))
# # Editors #
[docs]class ProductComponentEditor(BaseEditor): gladefile = 'ProductComponentEditor' proxy_widgets = ['quantity', 'design_reference', 'price'] title = _(u'Product Component') model_type = TemporaryProductComponent def _setup_widgets(self): self.price.hide() self.price_lbl.hide() self.component_description.set_text(self.model.description) self.quantity.set_adjustment( gtk.Adjustment(lower=0, upper=MAX_INT, step_incr=1, page_incr=10)) self.price.set_adjustment( gtk.Adjustment(lower=0, upper=MAX_INT, step_incr=1, page_incr=10)) # set a default quantity value for new components if not self.model.quantity: self.quantity.set_value(1) # # BaseEditor #
[docs] def setup_proxies(self): self._setup_widgets() self.proxy = self.add_proxy( self.model, ProductComponentEditor.proxy_widgets)
[docs] def validate_confirm(self): return self.quantity.read() > 0
# # Kiwi Callbacks #
[docs] def on_quantity__validate(self, widget, value): if not value > 0: # FIXME: value < upper bound return ValidationError(_(u'The component quantity must be ' 'greater than zero.'))
[docs]class ProductPackageComponentEditor(ProductComponentEditor): confirm_widgets = ['price']
[docs] def setup_proxies(self): super(ProductPackageComponentEditor, self).setup_proxies() self.design_reference.hide() self.design_reference_lbl.hide() self.price.show() self.price_lbl.show()
# # Kiwi Callbacks #
[docs] def on_price__validate(self, widget, value): if value <= 0: return ValidationError(_("The price must be greater than zero."))
[docs]class ProductEditor(SellableEditor): model_name = _('Product') model_type = Product help_section = 'product' ui_form_name = u'product' product_widgets = ['product_type_str'] proxy_widgets = SellableEditor.proxy_widgets + product_widgets def __init__(self, store, model=None, visual_mode=False, product_type=Product.TYPE_COMMON, template=None, wizard=None): """ :param product_type: one of the available :attr:`stoqlib.domain.product.Product.product_types` that will be used when creating a new one :param template: a product to use as a template when creating a new one. Some properties will be copied from it. """ self._template = template self._product_type = product_type self._wizard = wizard SellableEditor.__init__(self, store, model, visual_mode=visual_mode) # This can't be done in setup_slaves() as we need to access # self.main_dialog when setting up the quality test slave self._add_extra_tabs() # # Private # def _add_infobar(self, message, message_type): infobar = MessageBar(message, message_type) infobar.show() self.main_box.pack_start(infobar, False, False, 0) self.main_box.reorder_child(infobar, 0) return infobar def _add_extra_tabs(self): for tabname, tabslave in self.get_extra_tabs(): self.add_extra_tab(tabname, tabslave) def _disable_child_widgets(self): """This method disables edition of attributes gotten from parent. """ widgets = [self.description, self.category_combo, self.cost, self.price, self.default_sale_cfop, self.unit_combo, self.tax_constant, self.add_category] for widget in widgets: widget.set_property('sensitive', False) # # SellableEditor #
[docs] def get_taxes(self): query = (SellableTaxConstant.tax_type != int(TaxType.SERVICE)) constants = self.store.find( SellableTaxConstant, query).order_by(SellableTaxConstant.description) return [(c.description, c) for c in constants]
[docs] def setup_slaves(self): super(ProductEditor, self).setup_slaves() from stoqlib.gui.slaves.productslave import ProductInformationSlave info_slave = ProductInformationSlave(self.store, self.model, self.db_form, visual_mode=self.visual_mode) self.add_extra_tab(_(u'Details'), info_slave)
[docs] def setup_proxies(self): super(ProductEditor, self).setup_proxies() self.add_proxy(self.model, self.product_widgets) if self.model.parent is not None: self._disable_child_widgets() msg = (_("Some properties of this product have been disabled for " "editing as that should be done on the parent product.")) self._add_infobar(msg, gtk.MESSAGE_INFO) elif self.model.product_type == Product.TYPE_GRID: msg = (_("This is just a skeleton product responsible for " "creating grid products. Create those on the 'Grid' tab")) self._add_infobar(msg, gtk.MESSAGE_INFO) if self.model.product_type == Product.TYPE_PACKAGE: # We are building its sale price on Pack content tab, so disable # this the widget and set a simbolic value self.price.set_property('sensitive', False) # FIXME Disable promotional price for now self.sale_price_button.set_property('sensitive', False) msg = (_("The price of this product consists of the sum of " "its components price on 'Pack content' tab")) self._add_infobar(msg, gtk.MESSAGE_INFO)
[docs] def get_extra_tabs(self): from stoqlib.gui.slaves.productslave import (ProductTaxSlave, ProductSupplierSlave, ProductGridSlave, ProductPackageSlave) extra_tabs = [] suppliers_slave = ProductSupplierSlave(self.store, self.model, self.visual_mode) extra_tabs.append((_(u'Suppliers'), suppliers_slave)) tax_slave = ProductTaxSlave(self.store, self.model, self.visual_mode) extra_tabs.append((_(u'Taxes'), tax_slave)) if self.model.product_type == Product.TYPE_GRID: # If there is a wizard, it means we are creating a new product. # Store the selected attributes in the database if self._wizard: for attribute in self._wizard.attr_list: ProductAttribute(store=self.store, product_id=self.model.id, attribute_id=attribute.id) attribute_option_slave = ProductGridSlave(self.store, self.model, self.visual_mode) extra_tabs.append((_(u'Grid'), attribute_option_slave)) attribute_option_slave.grid_tab_alignment.connect('focus', self._on_grid_tab_alignment__focus) elif self.model.product_type == Product.TYPE_PACKAGE: self.package_slave = ProductPackageSlave(self.store, self.model, visual_mode=self.visual_mode) extra_tabs.append((_(u'Pack content'), self.package_slave)) return extra_tabs
[docs] def setup_widgets(self): self.cost.set_digits(sysparam.get_int('COST_PRECISION_DIGITS')) self.description.grab_focus()
[docs] def create_model(self, store): self._model_created = True sellable = Sellable(store=store) model = Product(store=store, sellable=sellable) no_storable = [Product.TYPE_WITHOUT_STOCK, Product.TYPE_PACKAGE] if not self._product_type in no_storable: storable = Storable(product=model, store=store) if self._product_type == Product.TYPE_BATCH: storable.is_batch = True elif self._product_type == Product.TYPE_WITHOUT_STOCK: model.manage_stock = False elif self._product_type == Product.TYPE_CONSIGNED: model.consignment = True elif self._product_type == Product.TYPE_GRID: model.is_grid = True # Configurable products should not manage stock model.manage_stock = False elif self._product_type == Product.TYPE_PACKAGE: model.is_package = True # Package products should not manage stock model.manage_stock = False if self._template is not None: sellable.tax_constant = self._template.sellable.tax_constant sellable.unit = self._template.sellable.unit sellable.category = self._template.sellable.category sellable.base_price = self._template.sellable.base_price sellable.cost = self._template.sellable.cost model.manufacturer = self._template.manufacturer model.brand = self._template.brand model.family = self._template.family model.ncm = self._template.ncm model.icms_template = self._template.icms_template model.ipi_template = self._template.ipi_template for product_attr in self._template.attributes: ProductAttribute(store=self.store, product_id=model.id, attribute_id=product_attr.attribute.id) for supplier_info in self._template.suppliers: ProductSupplierInfo( store=self.store, product=model, supplier=supplier_info.supplier) else: sellable.tax_constant = sysparam.get_object(self.store, 'DEFAULT_PRODUCT_TAX_CONSTANT') sellable.unit_id = sysparam.get_object_id('SUGGESTED_UNIT') return model
[docs] def on_confirm(self): # The user choose not to manage stock for this product, so we must # remove the storable. if not self.model.manage_stock and self.model.storable: self.store.remove(self.model.storable) # When creating a purchase, we use the supplier cost instead of the one # in the sellable. If there is only one supplier for this product, also # update its cost. TODO: What should we do when there is more than one # supplier? infos = list(self.model.get_suppliers_info()) if len(infos) == 1: infos[0].base_cost = self.model.sellable.cost if self.model.is_grid and self.has_changes(): self.model.update_children_info()
# # Callbacks # def _on_grid_tab_alignment__focus(self, widget, value): self.model.update_children_description()
[docs]class ProductionProductEditor(ProductEditor): _cost_msg = _(u'Cost must be greater than the sum of the components.') def _is_valid_cost(self, cost): if hasattr(self, '_component_slave'): component_cost = self.component_slave.get_component_cost() return cost >= component_cost return True
[docs] def create_model(self, store): model = ProductEditor.create_model(self, store) model.is_composed = True return model
[docs] def get_extra_tabs(self): from stoqlib.gui.slaves.productslave import (ProductTaxSlave, ProductComponentSlave, ProductQualityTestSlave) self.component_slave = ProductComponentSlave(self.store, self.model, self.visual_mode) tax_slave = ProductTaxSlave(self.store, self.model, self.visual_mode) quality_slave = ProductQualityTestSlave(self, self.store, self.model, self.visual_mode) return [(_(u'Components'), self.component_slave), (_(u'Taxes'), tax_slave), (_(u'Quality'), quality_slave), ]
[docs] def setup_widgets(self): super(ProductionProductEditor, self).setup_widgets() self.cost.set_sensitive(not sysparam.get_bool('UPDATE_PRODUCT_COST_ON_COMPONENT_UPDATE'))
[docs] def validate_confirm(self): if not self._is_valid_cost(self.cost.read()): info(self._cost_msg) return False return True
[docs] def on_component_slave__cost_updated(self, slave, cost): if sysparam.get_bool('UPDATE_PRODUCT_COST_ON_COMPONENT_UPDATE'): # We have to update the sellable directly since kiwi won't do it for # us, since the widget is insensitive self.model.sellable.cost = cost self.sellable_proxy.update('cost')
[docs] def on_cost__validate(self, widget, value): if value <= 0: return ValidationError(_(u'Cost cannot be zero or negative.')) if not self._is_valid_cost(value): return ValidationError(self._cost_msg)
[docs]class ProductStockEditor(BaseEditor): """This is a product editor limitted to editing physical stock information""" model_name = _('Product') model_type = Product gladefile = 'ProductStockEditor'
[docs] def setup_slaves(self): from stoqlib.gui.slaves.productslave import ProductInformationSlave info_slave = ProductInformationSlave(self.store, self.model, visual_mode=self.visual_mode) info_slave.nfe_frame.hide() self.attach_slave('information_holder', info_slave) from stoqlib.gui.slaves.sellableslave import SellableDetailsSlave details_slave = SellableDetailsSlave(self.store, self.model.sellable, visual_mode=self.visual_mode) self.attach_slave('details_holder', details_slave) # Make everything aligned by pytting notes_lbl on the same size group info_slave.left_labels_group.add_widget(details_slave.notes_lbl) event_box = gtk.EventBox() event_box.show() image_gallery_slave = ImageGallerySlave( self.store, self.model.sellable, self.visual_mode) self.notebook.append_page(event_box, gtk.Label(_("Images"))) self.attach_slave('images', image_gallery_slave, event_box)
[docs]class ProductStockQuantityEditor(BaseEditor): """Editor for adjusting the stock quantity of a product This editor will set the quantity of a product for a given branch. If the product does not manage stock yet, a storable will be created first and the initial stock be registred. If the product is already a storable, an inventory will be created for just this product (so that the original quantity is registred and can be audited), and the quantity will be adjusted. A reason in this case is mandatory """ title = _('Adjust stock quantity') model_name = _('Stock') model_type = Settable @cached_property() def fields(self): # Check if sellable's unit allow fraction to use decimal places unit = self._product.sellable.unit if unit and unit.allow_fraction: quantity_digits = 3 else: quantity_digits = 0 fields = collections.OrderedDict( quantity=NumericField(_('Quantity'), proxy=True, mandatory=True, digits=quantity_digits), ) # When creating an inventory, a reason is necessary if self._stock_item: fields['reason'] = MultiLineField(_('Reason'), proxy=True, mandatory=True) else: # Inventories dont do anything with the cost yet. Maybe we should # fix that fields['cost'] = PriceField(_('Cost'), proxy=True, mandatory=True) return fields def __init__(self, store, model, branch): self._branch = branch assert self._branch self._product = model # model here is the product, but we will create a settable later if model.storable: # We dont support editing batch products yet. assert not model.storable.is_batch self._stock_item = model.storable.get_stock_item(branch, batch=None) else: self._stock_item = None BaseEditor.__init__(self, store=store)
[docs] def create_model(self, store): if self._stock_item: return Settable(quantity=self._stock_item.quantity, cost=self._stock_item.stock_cost, reason=u'') else: return Settable(quantity=Decimal(0), cost=self._product.sellable.cost)
def _register_inventory(self): query = Storable.id == self._product.id inventory = Inventory.create_inventory(self.store, branch=self._branch, responsible=api.get_current_user(self.store), query=query) # At this point, the inventory should have only one item. item = inventory.get_items().one() item.counted_quantity = item.actual_quantity = self.model.quantity # item.product_cost = self.model.cost item.reason = self.model.reason item.adjust(invoice_number=None) inventory.close() def _register_initial_stock(self): if not self._product.storable: self._product.set_as_storable_product(self.model.quantity, self._branch, self.model.cost) else: self._product.storable.register_initial_stock(self.model.quantity, self._branch, self.model.cost)
[docs] def on_confirm(self): if not self._stock_item: # If the item does not manage stock, we will make it a managed # storable and register the initial stock return self._register_initial_stock() else: # If the product already manages stock, this will be an easy way for # the user to fix the actual quantity (like a mini inventory). # XXX: Make sure that the user has access to the inventory app # before calling this. return self._register_inventory()
[docs]class ProductManufacturerEditor(BaseEditor): model_name = _('Manufacturer') model_type = ProductManufacturer confirm_widgets = ['name'] @cached_property() def fields(self): return collections.OrderedDict( name=TextField(_('Name'), proxy=True, mandatory=True), code=TextField(_('Code'), proxy=True), )
[docs] def create_model(self, store): return ProductManufacturer(name=u'', store=store)
[docs] def setup_proxies(self): self.name.grab_focus()
# # Kiwi Callbacks #
[docs] def on_name__validate(self, widget, new_name): if not new_name: return ValidationError( _("The manufacturer should have a name.")) if self.model.check_unique_value_exists(ProductManufacturer.name, new_name): return ValidationError( _("The manufacturer '%s' already exists.") % new_name)
[docs] def on_code__validate(self, widget, new_code): if self.model.check_unique_value_exists(ProductManufacturer.code, new_code): return ValidationError(_("The code '%s' already exists") % new_code)
[docs]def test_product(): # pragma nocover ec = api.prepare_test() product = ec.create_product() run_dialog(ProductEditor, parent=None, store=ec.store, model=product)
if __name__ == '__main__': # pragma nocover test_product()