# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4
##
## Copyright (C) 2009-2016 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>
##
""" Sale editors """
import collections
import gtk
from kiwi.currency import currency
from kiwi.datatypes import ValidationError
from kiwi.python import Settable
from kiwi.ui.forms import TextField
from stoqlib.api import api
from stoqlib.domain.event import Event
from stoqlib.domain.fiscal import CfopData
from stoqlib.domain.person import Branch, Client, SalesPerson
from stoqlib.domain.sale import Sale, SaleItem, SaleToken
from stoqlib.gui.base.dialogs import run_dialog
from stoqlib.gui.dialogs.credentialsdialog import CredentialsDialog
from stoqlib.gui.editors.baseeditor import BaseEditor, BaseEditorSlave
from stoqlib.gui.editors.invoiceitemeditor import InvoiceItemEditor
from stoqlib.gui.fields import PersonField, PersonQueryField, DateTextField
from stoqlib.gui.widgets.calculator import CalculatorPopup
from stoqlib.lib.decorators import cached_property
from stoqlib.lib.defaults import quantize, QUANTITY_PRECISION, MAX_INT
from stoqlib.lib.parameters import sysparam
from stoqlib.lib.pluginmanager import get_plugin_manager
from stoqlib.lib.translation import stoqlib_gettext
_ = stoqlib_gettext
[docs]class SaleQuoteItemEditor(InvoiceItemEditor):
model_type = SaleItem
model_name = _("Sale Quote Item")
[docs] def setup_slaves(self):
self.item_slave = SaleQuoteItemSlave(self.store, self.model, parent=self)
self.attach_slave('item-holder', self.item_slave)
[docs]class SaleQuoteItemSlave(BaseEditorSlave):
gladefile = 'SaleQuoteItemSlave'
model_type = SaleItem
proxy_widgets = ['price', 'cfop']
#: The manager is someone who can allow a bigger discount for a sale item.
manager = None
def __init__(self, store, model, parent):
self.model = model
self.icms_slave = parent.icms_slave
self.ipi_slave = parent.ipi_slave
# Use a temporary object to edit the quantities, so we can delay the
# database constraint checks
self.quantity_model = Settable(quantity=model.quantity,
reserved=model.quantity_decreased)
self.proxy = None
BaseEditorSlave.__init__(self, store, self.model)
sale = self.model.sale
if sale.status == Sale.STATUS_CONFIRMED:
self._set_not_editable()
if model.parent_item:
# We should not allow the user to edit the children
self._set_not_editable()
self.reserved.set_sensitive(False)
sellable = model.sellable
if sellable.product and sellable.product.is_package:
# Do not allow the user to edit the price of the package
self.price.set_sensitive(False)
def _setup_widgets(self):
self._calc = CalculatorPopup(self.price, CalculatorPopup.MODE_SUB)
self.sale.set_text(unicode(self.model.sale.identifier))
self.description.set_text(self.model.sellable.get_description())
self.original_price.update(self.model.base_price)
self.price.set_adjustment(gtk.Adjustment(lower=0, upper=MAX_INT,
step_incr=1, page_incr=10))
unit = self.model.sellable.unit
digits = QUANTITY_PRECISION if unit and unit.allow_fraction else 0
for widget in [self.quantity, self.reserved]:
widget.set_digits(digits)
widget.set_adjustment(gtk.Adjustment(lower=0, upper=MAX_INT,
step_incr=1, page_incr=10))
manager = get_plugin_manager()
self.nfe_is_active = manager.is_any_active(['nfe', 'nfce'])
if not self.nfe_is_active:
self.cfop_label.hide()
self.cfop.hide()
if not self._can_reserve():
self.reserved.hide()
self.reserved_lbl.hide()
# We populate this even if it's hidden because we need a default value
# selected to add to the sale item
cfop_items = CfopData.get_for_sale(self.store)
self.cfop.prefill(api.for_combo(cfop_items))
self._update_total()
self.reserved.get_adjustment().set_upper(self.quantity_model.quantity)
[docs] def setup_proxies(self):
self._setup_widgets()
self.proxy = self.add_proxy(self.model, self.proxy_widgets)
# Quantity is not in the proxy above so that it doen't get updated
# before quantity_reserved in the database
self.reserved_proxy = self.add_proxy(self.quantity_model,
['quantity', 'reserved'])
def _set_not_editable(self):
self.price.set_sensitive(False)
self.quantity.set_sensitive(False)
def _maybe_log_discount(self):
# If not authorized to apply a discount or the CredentialsDialog is
# cancelled, dont generate the log
if not self.manager:
return
price = self.model.sellable.get_price_for_category(self.model.sale.client_category)
new_price = self.price.read()
if new_price >= price:
return
discount = 100 - new_price * 100 / price
Event.log_sale_item_discount(
store=self.store,
sale_number=self.model.sale.identifier,
user_name=self.manager.username,
discount_value=discount,
product=self.model.sellable.description,
original_price=price,
new_price=new_price)
def _maybe_update_children_quantity(self):
qty = self.model.quantity
for child in self.model.children_items:
component = child.get_component(self.model)
child.quantity = qty * component.quantity
def _maybe_reserve_products(self):
if not self._can_reserve():
# Not reserve products. But allow to edit sale item quantity,
# if the batch is not informed or sale item has no
# storable.
self.model.quantity = self.quantity_model.quantity
return
decreased = self.model.quantity_decreased
to_reserve = self.quantity_model.reserved
diff = to_reserve - decreased
for sale_item in self.model.children_items:
component = sale_item.get_component(self.model)
component_qty = component.quantity * diff
if diff > 0:
sale_item.reserve(component_qty)
elif diff < 0:
sale_item.return_to_stock(abs(component_qty))
if diff > 0:
# Update quantity first, so that the db constraint does not break
self.model.quantity = self.quantity_model.quantity
# We need to decrease a few more from the stock
self.model.reserve(diff)
elif diff < 0:
# We need to return some items to the stock
self.model.return_to_stock(abs(diff))
# Update quantity last, so that the db constraint does not break
self.model.quantity = self.quantity_model.quantity
else:
# even if there is no products to reserve, we still need to set the
# quantity value.
self.model.quantity = self.quantity_model.quantity
def _can_reserve(self):
product = self.model.sellable.product
# We cant reserve services
if not product:
return False
storable = product.storable
# Cant reserve products without storable, except if it is a package
if not storable and not product.is_package:
return False
# If the storable is has batches, but the sale item still dont have, its
# not possible to reserve.
if storable and storable.is_batch and not self.model.batch:
return False
# No stock item means we cannot reserve any quantity yet
if storable:
stock_item = storable.get_stock_item(self.model.sale.branch,
self.model.batch)
if stock_item is None:
return False
return True
def _update_total(self):
# We need to update the total manually, since the model quantity
# update is delayed
total = self.model.price * self.quantity_model.quantity
if self.model.ipi_info:
total += self.model.ipi_info.v_ipi
self.total.update(currency(quantize(total)))
def _validate_quantity(self, new_quantity, allow_zero=False):
if not allow_zero and new_quantity <= 0:
return ValidationError(_(u"The quantity should be "
u"greater than zero."))
sellable = self.model.sellable
if not sellable.is_valid_quantity(new_quantity):
return ValidationError(_(u"This product unit (%s) does not "
u"support fractions.") %
sellable.unit_description)
[docs] def on_confirm(self):
self._maybe_log_discount()
self._maybe_reserve_products()
self._maybe_update_children_quantity()
#
# Kiwi callbacks
#
[docs] def after_price__changed(self, widget):
if self.ipi_slave:
self.ipi_slave.update_values()
if self.icms_slave:
self.icms_slave.update_values()
self._update_total()
[docs] def after_quantity__changed(self, widget):
self._update_total()
reserved = min(self.quantity_model.quantity,
self.quantity_model.reserved)
self.reserved.get_adjustment().set_upper(self.quantity_model.quantity)
self.reserved.update(reserved)
[docs] def on_price__validate(self, widget, value):
if value <= 0:
return ValidationError(_(u"The price must be greater than zero."))
if (not sysparam.get_bool('ALLOW_HIGHER_SALE_PRICE') and
value > self.model.base_price):
return ValidationError(_(u'The sell price cannot be greater '
'than %s.') % self.model.base_price)
sellable = self.model.sellable
manager = self.manager or api.get_current_user(self.store)
if api.sysparam.get_bool('REUTILIZE_DISCOUNT'):
extra_discount = self.model.sale.get_available_discount_for_items(
user=manager, exclude_item=self.model)
else:
extra_discount = None
valid_data = sellable.is_valid_price(
value, category=self.model.sale.client_category,
user=manager, extra_discount=extra_discount)
if not valid_data['is_valid']:
return ValidationError(
(_(u'Max discount for this product is %.2f%%.') %
valid_data['max_discount']))
[docs] def on_quantity__validate(self, widget, value):
return self._validate_quantity(value)
[docs] def on_reserved__validate(self, widget, value):
if not self._can_reserve():
return
# Do some pre-validation
valid = self._validate_quantity(value, allow_zero=True)
if valid:
return valid
if not self.model.has_children():
storable = self.model.sellable.product.storable
decreased = self.model.quantity_decreased
to_reserve = value - decreased
# There is no need to reserve more products
if to_reserve <= 0:
return
stock_item = storable.get_stock_item(self.model.sale.branch,
self.model.batch)
if to_reserve > stock_item.quantity:
return ValidationError('Not enough stock to reserve.')
else:
for child in self.model.children_items:
component = child.get_component(self.model)
storable = child.sellable.product_storable
decreased = child.quantity_decreased
to_reserve = (value * component.quantity) - decreased
if to_reserve <= 0:
return
# XXX Should I keep child.batch here? Since batch is not
# available as component yet
stock_item = storable.get_stock_item(child.sale.branch, child.batch)
if stock_item.quantity < to_reserve:
return ValidationError(_("One or more components for this package "
"doesn't have enough of stock to reserve"))
[docs] def on_price__icon_press(self, entry, icon_pos, event):
if icon_pos != gtk.ENTRY_ICON_PRIMARY: # pragma no cover
return
# Ask for the credentials of a different user that can possibly allow a
# bigger discount.
self.manager = run_dialog(CredentialsDialog, self, self.store)
self.price.validate(force=True)
class _BaseSalePersonChangeEditor(BaseEditor):
model_type = Sale
model_name = _('Sale')
@cached_property()
def fields(self):
return collections.OrderedDict(
identifier=TextField(_("Sale #"), proxy=True, editable=False),
open_date=DateTextField(_("Open date"), proxy=True, editable=False),
status_str=TextField(_("Status"), proxy=True, editable=False),
salesperson_id=PersonField(_("Salesperson"), proxy=True,
can_add=False, can_edit=False,
person_type=SalesPerson),
client=PersonQueryField(_("Client"), proxy=True,
person_type=Client),
)
def on_confirm(self):
self.model.group.payer = self.model.client and self.model.client.person
[docs]class SaleClientEditor(_BaseSalePersonChangeEditor):
[docs] def setup_proxies(self):
self.fields['salesperson_id'].set_sensitive(False)
[docs] def on_client__content_changed(self, widget):
client = widget.read()
if client is not None and client.status != Client.STATUS_SOLVENT:
self.fields['client'].gadget.update_edit_button(
gtk.STOCK_DIALOG_WARNING)
[docs]class SalesPersonEditor(_BaseSalePersonChangeEditor):
title = _('Salesperson change')
[docs] def setup_proxies(self):
field = self.fields['client']
# TODO: Maybe kiwi should have an api to hide the whole field, just
# like it has a set_sensitive on the field
for widget in [field.widget, field.label_widget,
field.add_button, field.edit_button, field.delete_button]:
if widget:
widget.hide()
[docs]class SaleTokenEditor(BaseEditor):
model_type = SaleToken
model_name = _('Sale token')
confirm_widgets = ['code']
@cached_property()
def fields(self):
return collections.OrderedDict(
name=TextField(_('Name'), proxy=True, mandatory=True),
code=TextField(_('Code'), proxy=True, mandatory=True),
branch_id=PersonField(_('Branch'), proxy=True, person_type=Branch,
can_add=False, can_edit=False, mandatory=True),
)
def __init__(self, store, model=None, visual_mode=False):
BaseEditor.__init__(self, store, model, visual_mode)
if model:
self.set_description(model.code)
[docs] def setup_proxies(self):
self.proxy = self.add_proxy(self.model, SaleTokenEditor.proxy_widgets)
[docs] def create_model(self, store):
return SaleToken(store=self.store,
code=u'',
name=u'',
branch=api.get_current_branch(self.store))
[docs] def on_code__validate(self, widget, value):
# FIXME: Consider the branch in this check
if self.model.check_unique_value_exists(self.model_type.code, value):
return ValidationError(_("Code already in use"))