Source code for stoqlib.gui.wizards.loanwizard

# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4

##
## Copyright (C) 2010 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>
##
##
""" Loan wizard"""

from decimal import Decimal
import datetime

import gtk
from kiwi.currency import currency
from kiwi.datatypes import ValidationError
from kiwi.python import Settable
from kiwi.ui.widgets.entry import ProxyEntry
from kiwi.ui.objectlist import Column
from storm.expr import And, Or, Eq

from stoqlib.api import api
from stoqlib.domain.person import LoginUser, ClientCategory
from stoqlib.domain.loan import Loan, LoanItem
from stoqlib.domain.payment.group import PaymentGroup
from stoqlib.domain.sale import Sale
from stoqlib.domain.sellable import Sellable
from stoqlib.domain.views import LoanView, ProductWithStockBranchView
from stoqlib.lib.dateutils import localtoday
from stoqlib.lib.defaults import MAX_INT
from stoqlib.lib.formatters import format_quantity
from stoqlib.lib.message import info, yesno
from stoqlib.lib.translation import stoqlib_gettext
from stoqlib.gui.base.dialogs import run_dialog
from stoqlib.gui.base.wizards import (WizardEditorStep, BaseWizard,
                                      BaseWizardStep)
from stoqlib.gui.dialogs.batchselectiondialog import BatchDecreaseSelectionDialog
from stoqlib.gui.dialogs.missingitemsdialog import (get_missing_items,
                                                    MissingItemsDialog)
from stoqlib.gui.events import (NewLoanWizardFinishEvent,
                                CloseLoanWizardFinishEvent,
                                LoanItemSelectionStepEvent,
                                InvoiceSetupEvent,
                                WizardAddSellableEvent,
                                StockOperationPersonValidationEvent)
from stoqlib.gui.editors.loanitemeditor import LoanItemEditor
from stoqlib.gui.editors.noteeditor import NoteEditor
from stoqlib.gui.search.searchcolumns import IdentifierColumn, SearchColumn
from stoqlib.gui.search.searchslave import SearchSlave
from stoqlib.gui.utils.printing import print_report
from stoqlib.gui.widgets.queryentry import ClientEntryGadget
from stoqlib.gui.wizards.abstractwizard import SellableItemStep
from stoqlib.gui.wizards.salequotewizard import SaleQuoteItemStep
from stoqlib.reporting.loanreceipt import LoanReceipt

_ = stoqlib_gettext


#
# Wizard Steps
#


[docs]class StartNewLoanStep(WizardEditorStep): gladefile = 'SalesPersonStep' model_type = Loan proxy_widgets = ['client', 'salesperson', 'expire_date', 'client_category'] def _setup_widgets(self): # Hide total and subtotal self.summary_table.hide() self.total_box.hide() # Hide cost center combobox self.cost_center_lbl.hide() self.cost_center.hide() # Responsible combo self.salesperson_lbl.set_text(_(u'Responsible:')) self.salesperson.model_attribute = 'responsible' users = self.store.find(LoginUser, is_active=True) self.salesperson.prefill(api.for_person_combo(users)) self.salesperson.set_sensitive(False) self._setup_clients_widget() self._fill_clients_category_combo() self.expire_date.mandatory = True # CFOP combo self.cfop_lbl.hide() self.cfop.hide() self.create_cfop.hide() # Transporter/RemovedBy Combo self.transporter_lbl.set_text(_(u'Removed By:')) self.create_transporter.hide() # removed_by widget self.removed_by = ProxyEntry(unicode) self.removed_by.model_attribute = 'removed_by' if 'removed_by' not in self.proxy_widgets: self.proxy_widgets.append('removed_by') self.removed_by.show() self._replace_widget(self.transporter, self.removed_by) def _setup_clients_widget(self): self.client.mandatory = True self.client_gadget = ClientEntryGadget( entry=self.client, store=self.store, initial_value=self.model.client, parent=self.wizard) def _fill_clients_category_combo(self): categories = self.store.find(ClientCategory) self.client_category.prefill(api.for_combo(categories, empty='')) def _replace_widget(self, old_widget, new_widget): # retrieve the position, since we will replace two widgets later. parent = old_widget.get_parent() top = parent.child_get_property(old_widget, 'top-attach') bottom = parent.child_get_property(old_widget, 'bottom-attach') left = parent.child_get_property(old_widget, 'left-attach') right = parent.child_get_property(old_widget, 'right-attach') parent.remove(old_widget) parent.attach(new_widget, left, right, top, bottom) # # WizardStep hooks #
[docs] def post_init(self): self.register_validate_function(self.wizard.refresh_next) self.force_validation()
[docs] def next_step(self): return LoanItemStep(self.wizard, self, self.store, self.model)
[docs] def has_previous_step(self): return False
[docs] def setup_proxies(self): self._setup_widgets() self.proxy = self.add_proxy(self.model, StartNewLoanStep.proxy_widgets)
# # Callbacks #
[docs] def on_client__changed(self, widget): client = widget.read() if not client: return self.client_category.select(client.category)
[docs] def on_expire_date__validate(self, widget, value): if value < localtoday().date(): msg = _(u"The expire date must be set to today or a future date.") return ValidationError(msg)
[docs] def on_observations_button__clicked(self, *args): run_dialog(NoteEditor, self.wizard, self.store, self.model, 'notes', title=_("Additional Information"))
[docs] def on_client__validate(self, widget, client): return StockOperationPersonValidationEvent.emit(client.person, type(client))
[docs]class LoanItemStep(SaleQuoteItemStep): """ Wizard step for loan items selection """ change_remove_btn_sensitive = False model_type = Loan item_table = LoanItem sellable_view = ProductWithStockBranchView item_editor = LoanItemEditor validate_stock = True batch_selection_dialog = BatchDecreaseSelectionDialog
[docs] def get_sellable_view_query(self): branch = self.model.branch # Also include products that are not storable branch_query = Or(self.sellable_view.branch_id == branch.id, Eq(self.sellable_view.branch_id, None)) # The stock quantity of consigned products can not be # decreased manually. See bug 5212. query = And(branch_query, Sellable.get_available_sellables_query(self.store)) return self.sellable_view, query
[docs] def get_order_item(self, sellable, price, quantity, batch=None, parent=None): item = self.model.add_sellable(sellable, quantity, price, batch=batch) item._stock_quantity = self.proxy.model.stock_quantity WizardAddSellableEvent.emit(self.wizard, item) return item
[docs] def has_next_step(self): return False
[docs]class LoanSelectionStep(BaseWizardStep): gladefile = 'HolderTemplate' def __init__(self, wizard, store): BaseWizardStep.__init__(self, store, wizard) self.setup_slaves() def _create_filters(self): self.search.set_text_field_columns(['client_name', 'identifier_str']) def _get_columns(self): return [IdentifierColumn('identifier', title=_('Loan #'), sorted=True), SearchColumn('responsible_name', title=_(u'Responsible'), data_type=str, expand=True), SearchColumn('client_name', title=_(u'Client'), data_type=str, expand=True), SearchColumn('open_date', title=_(u'Opened'), data_type=datetime.date), SearchColumn('expire_date', title=_(u'Expire'), data_type=datetime.date), Column('loaned', title=_(u'Loaned'), data_type=Decimal), ] def _refresh_next(self, value=None): can_continue = False selected_rows = self.search.results.get_selected_rows() if selected_rows: client = selected_rows[0].client_id branch = selected_rows[0].branch_id # Only loans that belong to the same client and are from the same # branch can be closed together can_continue = all(v.client_id == client and v.branch_id == branch for v in selected_rows) self.wizard.refresh_next(can_continue)
[docs] def get_extra_query(self, states): return LoanView.status == Loan.STATUS_OPEN
[docs] def setup_slaves(self): self.search = SearchSlave(self._get_columns(), restore_name=self.__class__.__name__, store=self.store, search_spec=LoanView) self.search.enable_advanced_search() self.attach_slave('place_holder', self.search) executer = self.search.get_query_executer() executer.add_query_callback(self.get_extra_query) self._create_filters() self.search.results.connect('selection-changed', self._on_results_selection_changed) self.search.results.set_selection_mode(gtk.SELECTION_MULTIPLE) self.search.focus_search_entry()
# # WizardStep #
[docs] def has_previous_step(self): return False
[docs] def post_init(self): self.register_validate_function(self._refresh_next) self.force_validation()
[docs] def next_step(self): # FIXME: For some reason, the loan isn't in self.store views = self.search.results.get_selected_rows() self.wizard.models = [self.store.fetch(v.loan) for v in views] return LoanItemSelectionStep(self.wizard, self, self.store, self.wizard.models)
# # Callbacks # def _on_results_selection_changed(self, widget, selection): self._refresh_next()
[docs]class LoanItemSelectionStep(SellableItemStep): model_type = list item_table = LoanItem cost_editable = False summary_label_column = None def __init__(self, wizard, previous, store, model): super(LoanItemSelectionStep, self).__init__(wizard, previous, store, model) for loan in model: for item in loan.loaned_items: self.wizard.original_items[item] = Settable( quantity=item.quantity, sale_quantity=item.sale_quantity, return_quantity=item.return_quantity, remaining_quantity=item.get_remaining_quantity(), ) LoanItemSelectionStepEvent.emit(self) # # SellableItemStep #
[docs] def has_next_step(self): return False
[docs] def post_init(self): super(LoanItemSelectionStep, self).post_init() self.hide_add_button() self.hide_edit_button() self.hide_del_button() self.hide_item_addition_toolbar() self.slave.klist.connect('cell-edited', self._on_klist__cell_edited) self.slave.klist.connect('cell-editing-started', self._on_klist__cell_editing_started) self.force_validation()
[docs] def get_columns(self): adjustment = gtk.Adjustment(lower=0, upper=MAX_INT, step_incr=1) return [ Column('sellable.code', title=_('Code'), data_type=str, visible=False), Column('sellable.barcode', title=_('Barcode'), data_type=str, visible=False), Column('sellable.description', title=_('Description'), data_type=str, expand=True), Column('quantity', title=_('Loaned'), data_type=Decimal, format_func=format_quantity), Column('sale_quantity', title=_('Sold'), data_type=Decimal, format_func=format_quantity, editable=True, spin_adjustment=adjustment), Column('return_quantity', title=_('Returned'), data_type=Decimal, format_func=format_quantity, editable=True, spin_adjustment=adjustment), Column('remaining_quantity', title=_('Remaining'), data_type=Decimal, format_func=format_quantity), Column('price', title=_('Price'), data_type=currency), ]
[docs] def get_saved_items(self): for loan in self.model: for item in loan.loaned_items: yield item
[docs] def validate_step(self): any_changed = False has_sale_items = False for item in self.get_saved_items(): original = self.wizard.original_items[item] sale_quantity = item.sale_quantity - original.sale_quantity if sale_quantity > 0: has_sale_items = True if item.get_remaining_quantity() < original.remaining_quantity: any_changed = True # Should not happen! assert (item.sale_quantity >= original.sale_quantity or item.return_quantity >= original.return_quantity) assert item.quantity >= item.sale_quantity + item.return_quantity if self.wizard.require_sale_items and not has_sale_items: return False # Don't let user finish if he didn't mark anything to return/sale return any_changed
[docs] def validate(self, value): super(LoanItemSelectionStep, self).validate(value) self.wizard.refresh_next(value and self.validate_step())
# # Callbacks # def _on_klist__cell_edited(self, klist, obj, column): attr = column.attribute # FIXME: Even with the adjustment, the user still can type # values out of range with the keyboard. Maybe it's kiwi's fault if attr in ['sale_quantity', 'return_quantity']: value = getattr(obj, attr) lower_value = getattr(self.wizard.original_items[obj], attr) if value < lower_value: setattr(obj, attr, lower_value) diff = obj.quantity - obj.return_quantity - obj.sale_quantity if diff < 0: setattr(obj, attr, value + diff) self.force_validation() def _on_klist__cell_editing_started(self, klist, obj, attr, renderer, editable): original_item = self.wizard.original_items[obj] if attr == 'sale_quantity': adjustment = editable.get_adjustment() adjustment.set_lower(original_item.sale_quantity) adjustment.set_upper(obj.quantity - obj.return_quantity) if attr == 'return_quantity': adjustment = editable.get_adjustment() adjustment.set_lower(original_item.return_quantity) adjustment.set_upper(obj.quantity - obj.sale_quantity)
# # Main wizard #
[docs]class NewLoanWizard(BaseWizard): size = (775, 400) help_section = 'loan' def __init__(self, store, model=None): title = self._get_title(model) model = model or self._create_model(store) if model.status != Loan.STATUS_OPEN: raise ValueError('Invalid loan status. It should ' 'be STATUS_OPEN') first_step = StartNewLoanStep(store, self, model) BaseWizard.__init__(self, store, first_step, model, title=title, edit_mode=False) def _get_title(self, model=None): if not model: return _('New Loan Wizard') def _create_model(self, store): loan = Loan(responsible=api.get_current_user(store), branch=api.get_current_branch(store), store=store) # Temporarily save the client_category, so it works fine with # SaleQuoteItemStep loan.client_category = None return loan def _print_receipt(self, order): # we can only print the receipt if the loan was confirmed. if yesno(_('Would you like to print the receipt now?'), gtk.RESPONSE_YES, _("Print receipt"), _("Don't print")): print_report(LoanReceipt, order) # # WizardStep hooks #
[docs] def finish(self): missing = get_missing_items(self.model, self.store) if missing: run_dialog(MissingItemsDialog, self, self.model, missing) return False invoice_ok = InvoiceSetupEvent.emit() if invoice_ok is False: # If there is any problem with the invoice, the event will display an error # message and the dialog is kept open so the user can fix whatever is wrong. return self.model.confirm() self.model.sync_stock() self.retval = self.model self.close() NewLoanWizardFinishEvent.emit(self.model) # Confirm before printing to avoid losing data if something breaks self.store.confirm(self.retval) self._print_receipt(self.model)
[docs]class CloseLoanWizard(BaseWizard): size = (775, 400) title = _(u'Close Loan Wizard') help_section = 'loan' def __init__(self, store, create_sale=True, require_sale_items=False): """ :param store: A database store :param create_sale: If a sale should be created for all the items that will be sold from this loan. :param require_sale_items: If there should be at least one item sold in the Loan. If ``False``, a loan with only returned items will be allowed to be confirmed. When ``True``, there should be at least one item in the loan that will be sold before confirming this wizard. """ self._create_sale = create_sale self._sold_items = [] self.original_items = {} self.require_sale_items = require_sale_items first_step = LoanSelectionStep(self, store) BaseWizard.__init__(self, store, first_step, model=None, title=self.title, edit_mode=False) # # Public API #
[docs] def get_sold_items(self): """Get items set to be sold on this wizard Returns a list of sold |sellables|, the quantity sold of those |sellables| and the price it was sold at. :returns: a list of tuples (|sellable|, quantity, price) """ return self._sold_items
# # WizardStep hooks #
[docs] def finish(self): for loan in self.models: for item in loan.loaned_items: original = self.original_items[item] sale_quantity = item.sale_quantity - original.sale_quantity if sale_quantity > 0: self._sold_items.append( (item.sellable, sale_quantity, item.price)) if self._create_sale and self._sold_items: user = api.get_current_user(self.store) sale = Sale( store=self.store, # Even if there is more than one loan, they are always from the # same (client, branch) branch=self.models[0].branch, client=self.models[0].client, salesperson=user.person.sales_person, group=PaymentGroup(store=self.store), coupon_id=None) for sellable, quantity, price in self._sold_items: sale.add_sellable(sellable, quantity, price, # Quantity was already decreased on loan quantity_decreased=quantity) sale.order() info(_("Close loan details..."), _("A sale was created from loan items. You can confirm " "that sale in the Till application later.")) else: sale = None for model in self.models: model.sync_stock() if model.can_close(): model.close() self.retval = self.models self.close() CloseLoanWizardFinishEvent.emit(self.models, sale, self)
[docs]def test(): # pragma nocover creator = api.prepare_test() run_dialog(CloseLoanWizard, None, creator.store, create_sale=True) creator.store.rollback()
#creator.store.confirm(retval) if __name__ == '__main__': # pragma nocover test()