Example #1
0
 def _setup_printer(self):
     self._printer = FiscalPrinterHelper(self.store, parent=self)
     self._printer.connect('till-status-changed',
                           self._on_PrinterHelper__till_status_changed)
     self._printer.connect('ecf-changed',
                           self._on_PrinterHelper__ecf_changed)
     self._printer.setup_midnight_check()
Example #2
0
File: pos.py Project: pkaislan/stoq
 def _setup_printer(self):
     self._printer = FiscalPrinterHelper(self.store, parent=self)
     self._printer.connect('till-status-changed',
                           self._on_PrinterHelper__till_status_changed)
     self._printer.connect('ecf-changed',
                           self._on_PrinterHelper__ecf_changed)
     self._printer.setup_midnight_check()
Example #3
0
class TillApp(ShellApp):

    app_title = _(u'Till')
    gladefile = 'till'
    search_spec = SaleView
    search_labels = _(u'matching:')
    report_table = SalesReport

    #
    # Application
    #

    def create_actions(self):
        group = get_accels('app.till')
        actions = [
            ('SaleMenu', None, _('Sale')),
            ('TillOpen', None, _('Open till...'),
             group.get('open_till')),
            ('TillClose', None, _('Close till...'),
             group.get('close_till')),
            ('TillVerify', None, _('Verify till...'),
             group.get('verify_till')),
            ("TillDailyMovement", None, _("Till daily movement..."),
             group.get('daily_movement')),
            ('TillAddCash', None, _('Cash addition...'), ''),
            ('TillRemoveCash', None, _('Cash removal...'), ''),
            ("PaymentReceive", None, _("Payment receival..."),
             group.get('payment_receive'),
             _("Receive payments")),
            ("SearchClient", None, _("Clients..."),
             group.get('search_clients'),
             _("Search for clients")),
            ("SearchSale", None, _("Sales..."),
             group.get('search_sale'),
             _("Search for sales")),
            ("SearchCardPayment", None, _("Card payments..."),
             None, _("Search for card payments")),
            ("SearchSoldItemsByBranch", None, _("Sold items by branch..."),
             group.get('search_sold_items_by_branch'),
             _("Search for items sold by branch")),
            ("SearchTillHistory", None, _("Till entry history..."),
             group.get('search_till_history'),
             _("Search for till history")),
            ("SearchFiscalTillOperations", None, _("Fiscal till operations..."),
             group.get('search_fiscal_till_operations'),
             _("Search for fiscal till operations")),
            ("SearchClosedTill", None, _("Closed till search..."),
             group.get('search_closed_till'),
             _("Search for all closed tills")),
            ("Confirm", gtk.STOCK_APPLY, _("Confirm..."),
             group.get('confirm_sale'),
             _("Confirm the selected sale, decreasing stock and making it "
               "possible to receive it's payments")),
            # FIXME: This button should change the label to "Cancel" when the
            # selected sale can be cancelled and not returned, since that's
            # what is going to happen when the user click in it
            ("Return", gtk.STOCK_CANCEL, _("Return..."),
             group.get('return_sale'),
             _("Return the selected sale, returning stock and the client's "
               "payments")),
            ("Details", gtk.STOCK_INFO, _("Details..."),
             group.get('sale_details'),
             _("Show details of the selected sale")),
        ]

        self.till_ui = self.add_ui_actions('', actions,
                                           filename="till.xml")
        self.set_help_section(_("Till help"), 'app-till')

        self.Confirm.set_short_label(_('Confirm'))
        self.Return.set_short_label(_('Return'))
        self.Details.set_short_label(_('Details'))
        self.Confirm.props.is_important = True
        self.Return.props.is_important = True
        self.Details.props.is_important = True

    def create_ui(self):
        self.popup = self.uimanager.get_widget('/TillSelection')

        self.current_branch = api.get_current_branch(self.store)
        # Groups
        self.main_vbox.set_focus_chain([self.app_vbox])
        self.app_vbox.set_focus_chain([self.search_holder, self.list_vbox])

        # Setting up the toolbar
        self.list_vbox.set_focus_chain([self.footer_hbox])
        self._setup_printer()
        self._setup_widgets()
        self.status_link.set_use_markup(True)
        self.status_link.set_justify(gtk.JUSTIFY_CENTER)

    def get_title(self):
        return _('[%s] - Till') % (
            api.get_current_branch(self.store).get_description(), )

    def activate(self, refresh=True):
        self.window.add_new_items([self.TillAddCash,
                                   self.TillRemoveCash])
        self.window.add_search_items([self.SearchFiscalTillOperations,
                                      self.SearchClient,
                                      self.SearchSale])
        self.window.Print.set_tooltip(_("Print a report of these sales"))
        if refresh:
            self.refresh()
        self._printer.run_initial_checks()
        self.check_open_inventory()

        self.search.focus_search_entry()

    def deactivate(self):
        self.uimanager.remove_ui(self.till_ui)

    def new_activate(self):
        if not self.TillAddCash.get_sensitive():
            return
        self._run_add_cash_dialog()

    def search_activate(self):
        self._run_search_dialog(TillFiscalOperationsSearch)

    #
    # ShellApp
    #

    def set_open_inventory(self):
        self.set_sensitive(self._inventory_widgets, False)

    def create_filters(self):
        self.search.set_query(self._query_executer)
        self.set_text_field_columns(['client_name', 'salesperson_name',
                                     'identifier_str'])
        self.status_filter = ComboSearchFilter(_(u"Show orders"),
                                               self._get_status_values())
        self.add_filter(self.status_filter, position=SearchFilterPosition.TOP,
                        columns=['status'])

    def get_columns(self):
        return [IdentifierColumn('identifier', title=_('Sale #'),
                                 sorted=True),
                Column('status_name', title=_(u'Status'), data_type=str,
                       visible=True),
                SearchColumn('open_date', title=_('Date Started'), width=110,
                             data_type=date, justify=gtk.JUSTIFY_RIGHT),
                SearchColumn('client_name', title=_('Client'),
                             data_type=str, expand=True,
                             ellipsize=pango.ELLIPSIZE_END),
                SearchColumn('salesperson_name', title=_('Salesperson'),
                             data_type=str, width=180,
                             ellipsize=pango.ELLIPSIZE_END),
                SearchColumn('total_quantity', title=_('Quantity'),
                             data_type=decimal.Decimal, width=100,
                             format_func=format_quantity),
                SearchColumn('total', title=_('Total'), data_type=currency,
                             width=100)]

    #
    # Private
    #

    def _query_executer(self, store):
        # We should only show Sales that
        # 1) In the current branch (FIXME: Should be on the same station.
                                    # See bug 4266)
        # 2) Are in the status QUOTE or ORDERED.
        # 3) For the order statuses, the date should be the same as today

        query = And(Sale.branch == self.current_branch,
                    Or(Sale.status == Sale.STATUS_QUOTE,
                       Sale.status == Sale.STATUS_ORDERED,
                       Date(Sale.open_date) == date.today()))

        return store.find(self.search_spec, query)

    def _setup_printer(self):
        self._printer = FiscalPrinterHelper(self.store,
                                            parent=self)
        self._printer.connect('till-status-changed',
                              self._on_PrinterHelper__till_status_changed)
        self._printer.connect('ecf-changed',
                              self._on_PrinterHelper__ecf_changed)
        self._printer.setup_midnight_check()

    def _get_status_values(self):
        statuses = [(v, k) for k, v in Sale.statuses.items()]
        statuses.insert(0, (_('Any'), None))
        return statuses

    def _create_sale_payments(self, order_view):
        store = api.new_store()
        sale = store.fetch(order_view.sale)
        retval = run_dialog(SalePaymentsEditor, self, store, sale)

        # Change the sale status to ORDERED
        if retval and sale.can_order():
            sale.order()

        if store.confirm(retval):
            self.refresh()
        store.close()

    def _confirm_order(self, order_view):
        if self.check_open_inventory():
            return

        store = api.new_store()
        sale = store.fetch(order_view.sale)
        expire_date = sale.expire_date

        if (sale.status == Sale.STATUS_QUOTE and
            expire_date and expire_date.date() < date.today() and
            not yesno(_("This quote has expired. Confirm it anyway?"),
                      gtk.RESPONSE_YES,
                      _("Confirm quote"), _("Don't confirm"))):
            store.close()
            return

        missing = get_missing_items(sale, store)

        if missing:
            retval = run_dialog(MissingItemsDialog, self, sale, missing)
            if retval:
                self.refresh()
            store.close()
            return

        coupon = self._open_coupon()
        if not coupon:
            store.close()
            return
        subtotal = self._add_sale_items(sale, coupon)
        try:
            if coupon.confirm(sale, store, subtotal=subtotal):
                workorders = WorkOrder.find_by_sale(store, sale)
                for order in workorders:
                    order.close()
                store.commit()
                self.refresh()
            else:
                coupon.cancel()
        except SellError as err:
            warning(str(err))
        except ModelDataError as err:
            warning(str(err))

        store.close()

    def _open_coupon(self):
        coupon = self._printer.create_coupon()

        if coupon:
            while not coupon.open():
                if not yesno(_("Failed to open the fiscal coupon.\n"
                               "Until it is opened, it's not possible to "
                               "confirm the sale. Do you want to try again?"),
                             gtk.RESPONSE_YES, _("Try again"), _("Cancel coupon")):
                    return None

        return coupon

    def _add_sale_items(self, sale, coupon):
        subtotal = 0
        for sale_item in sale.get_items():
            coupon.add_item(sale_item)
            subtotal += sale_item.price * sale_item.quantity
        return subtotal

    def _update_total(self):
        balance = currency(self._get_till_balance())
        text = _(u"Total: %s") % converter.as_string(currency, balance)
        self.total_label.set_text(text)

    def _update_payment_total(self):
        balance = currency(self._get_total_paid_payment())
        text = _(u"Total payments: %s") % converter.as_string(currency, balance)
        self.total_payment_label.set_text(text)

    def _get_total_paid_payment(self):
        """Returns the total of payments of the day"""
        payments = self.store.find(Payment,
                                   Date(Payment.paid_date) == localtoday())
        return payments.sum(Payment.paid_value) or 0

    def _get_till_balance(self):
        """Returns the balance of till operations"""
        try:
            till = Till.get_current(self.store)
        except TillError:
            till = None

        if till is None:
            return currency(0)

        return till.get_balance()

    def _setup_widgets(self):
        # SearchSale is here because it's possible to return a sale inside it
        self._inventory_widgets = [self.Confirm, self.SearchSale,
                                   self.Return]
        self.register_sensitive_group(self._inventory_widgets,
                                      lambda: not self.has_open_inventory())

        self.total_label.set_size('xx-large')
        self.total_label.set_bold(True)
        if not sysparam.get_bool('SHOW_TOTAL_PAYMENTS_ON_TILL'):
            self.total_payment_label.hide()
        else:
            self.total_payment_label.set_size('large')
            self.total_payment_label.set_bold(True)
            self.total_label.set_size('large')

        self.small_status.set_size('xx-large')
        self.small_status.set_bold(True)

    def _update_toolbar_buttons(self):
        sale_view = self.results.get_selected()
        if sale_view:
            can_confirm = sale_view.can_confirm()
            # when confirming sales in till, we also might want to cancel
            # sales
            can_return = (sale_view.can_return() or
                          sale_view.can_cancel())
        else:
            can_confirm = can_return = False

        self.set_sensitive([self.Details], bool(sale_view))
        self.set_sensitive([self.Confirm], can_confirm)
        self.set_sensitive([self.Return], can_return)

    def _check_selected(self):
        sale_view = self.results.get_selected()
        if not sale_view:
            raise StoqlibError("You should have a selected item at "
                               "this point")
        return sale_view

    def _run_search_dialog(self, dialog_type, **kwargs):
        store = api.new_store()
        self.run_dialog(dialog_type, store, **kwargs)
        store.close()

    def _run_details_dialog(self):
        sale_view = self._check_selected()
        run_dialog(SaleDetailsDialog, self, self.store, sale_view)

    def _run_add_cash_dialog(self):
        with api.new_store() as store:
            try:
                run_dialog(CashInEditor, self, store)
            except TillError as err:
                # Inform the error to the user instead of crashing
                warning(str(err))
                return

        if store.committed:
            self._update_total()

    def _return_sale(self):
        if self.check_open_inventory():
            return

        sale_view = self._check_selected()

        with api.new_store() as store:
            return_sale(self.get_toplevel(), store.fetch(sale_view.sale), store)

        if store.committed:
            self._update_total()
            self.refresh()

    def _update_ecf(self, has_ecf):
        # If we have an ecf, let the other events decide what to disable.
        if has_ecf:
            return

        # We dont have an ecf. Disable till related operations
        widgets = [self.TillOpen, self.TillClose, self.TillVerify, self.TillAddCash,
                   self.TillRemoveCash, self.SearchTillHistory, self.app_vbox,
                   self.Confirm, self.Return, self.Details]
        self.set_sensitive(widgets, has_ecf)
        text = _(u"Till operations requires a connected fiscal printer")
        self.small_status.set_text(text)

    def _update_till_status(self, closed, blocked):
        # Three different situations:
        #
        # - Till is closed
        # - Till is opened
        # - Till was not closed the previous fiscal day (blocked)

        self.set_sensitive([self.TillOpen], closed)
        self.set_sensitive([self.TillClose], not closed or blocked)
        widgets = [self.TillVerify, self.TillAddCash, self.TillRemoveCash,
                   self.SearchTillHistory, self.search_holder, self.PaymentReceive]
        self.set_sensitive(widgets, not closed and not blocked)

        def large(s):
            return '<span weight="bold" size="xx-large">%s</span>' % (
                api.escape(s), )

        if closed:
            text = large(_(u"Till closed"))
            self.search_holder.hide()
            self.footer_hbox.hide()
            self.large_status.show()
            self.clear()
            self.setup_focus()
            # Adding the label on footer without the link
            self.small_status.set_text(text)

            if not blocked:
                text += '\n\n<span size="large"><a href="open-till">%s</a></span>' % (
                    api.escape(_('Open till')))
            self.status_link.set_markup(text)
        elif blocked:
            self.search_holder.hide()
            self.footer_hbox.hide()
            text = large(_(u"Till blocked"))
            self.status_link.set_markup(text)
            self.small_status.set_text(text)
        else:
            self.search_holder.show()
            self.footer_hbox.show()
            self.large_status.hide()
            till = Till.get_current(self.store)
            text = _(u"Till opened on %s") % till.opening_date.strftime('%x')
            self.small_status.set_text(text)
        self._update_toolbar_buttons()
        self._update_total()
        if sysparam.get_bool('SHOW_TOTAL_PAYMENTS_ON_TILL'):
            self._update_payment_total()

    #
    # Callbacks
    #

    def on_Confirm__activate(self, action):
        selected = self.results.get_selected()

        # If there are unfinished workorders associated with the sale, we
        # cannot print the coupon yet. Instead lets just create the payments.
        workorders = WorkOrder.find_by_sale(self.store, selected.sale)
        if not all(wo.can_close() for wo in workorders):
            self._create_sale_payments(selected)
        else:
            self._confirm_order(selected)
        self._update_total()

    def on_results__double_click(self, results, sale):
        self._run_details_dialog()

    def on_results__selection_changed(self, results, sale):
        self._update_toolbar_buttons()

    def on_results__has_rows(self, results, has_rows):
        self._update_total()

    def on_results__right_click(self, results, result, event):
        self.popup.popup(None, None, None, event.button, event.time)

    def on_Details__activate(self, action):
        self._run_details_dialog()

    def on_Return__activate(self, action):
        self._return_sale()

    def on_status_link__activate_link(self, button, link):
        if link == 'open-till':
            self._printer.open_till()
        return True

    def _on_PrinterHelper__till_status_changed(self, printer, closed, blocked):
        self._update_till_status(closed, blocked)

    def _on_PrinterHelper__ecf_changed(self, printer, ecf):
        self._update_ecf(ecf)

    def on_PaymentReceive__activate(self, action):
        self.run_dialog(PaymentReceivingSearch, self.store)

    # Till

    def on_TillVerify__activate(self, button):
        self._printer.verify_till()

    def on_TillClose__activate(self, button):
        self._printer.close_till()

    def on_TillOpen__activate(self, button):
        self._printer.open_till()

    def on_TillAddCash__activate(self, action):
        self._run_add_cash_dialog()

    def on_TillRemoveCash__activate(self, action):
        with api.new_store() as store:
            run_dialog(CashOutEditor, self, store)
        if store.committed:
            self._update_total()

    def on_TillDailyMovement__activate(self, button):
        self.run_dialog(TillDailyMovementDialog, self.store)

    # Search

    def on_SearchClient__activate(self, action):
        self._run_search_dialog(ClientSearch, hide_footer=True)

    def on_SearchSale__activate(self, action):
        if self.check_open_inventory():
            return

        self._run_search_dialog(SaleWithToolbarSearch)
        self.refresh()

    def on_SearchCardPayment__activate(self, action):
        self.run_dialog(CardPaymentSearch, self.store)

    def on_SearchSoldItemsByBranch__activate(self, button):
        self._run_search_dialog(SoldItemsByBranchSearch)

    def on_SearchTillHistory__activate(self, button):
        self.run_dialog(TillHistoryDialog, self.store)

    def on_SearchFiscalTillOperations__activate(self, button):
        self._run_search_dialog(TillFiscalOperationsSearch)

    def on_SearchClosedTill__activate(self, button):
        self._run_search_dialog(TillClosedSearch)
Example #4
0
File: pos.py Project: pkaislan/stoq
class PosApp(ShellApp):

    app_title = _('Point of Sales')
    gladefile = "pos"

    def __init__(self, window, store=None):
        self._suggested_client = None
        self._current_store = None
        self._trade = None
        self._trade_infobar = None

        ShellApp.__init__(self, window, store=store)

        self._delivery = None
        self._coupon = None
        # Cant use self._coupon to verify if there is a sale, since
        # CONFIRM_SALES_ON_TILL doesnt create a coupon
        self._sale_started = False
        self._scale_settings = DeviceSettings.get_scale_settings(self.store)

    #
    # Application
    #

    def create_actions(self):
        group = get_accels('app.pos')
        actions = [
            # File
            ('NewTrade', None, _('Trade...'), group.get('new_trade')),
            ('PaymentReceive', None, _('Payment Receival...'),
             group.get('payment_receive')),
            ("TillOpen", None, _("Open Till..."), group.get('till_open')),
            ("TillClose", None, _("Close Till..."), group.get('till_close')),
            ("TillVerify", None, _("Verify Till..."),
             group.get('till_verify')),
            ("LoanClose", None, _("Close loan...")),
            ("WorkOrderClose", None, _("Close work order...")),

            # Order
            ("OrderMenu", None, _("Order")),
            ('ConfirmOrder', None, _('Confirm...'),
             group.get('order_confirm')),
            ('CancelOrder', None, _('Cancel...'), group.get('order_cancel')),
            ('NewDelivery', None, _('Create delivery...'),
             group.get('order_create_delivery')),

            # Search
            ("Sales", None, _("Sales..."), group.get('search_sales')),
            ("SoldItemsByBranchSearch", None, _("Sold Items by Branch..."),
             group.get('search_sold_items')),
            ("Clients", None, _("Clients..."), group.get('search_clients')),
            ("ProductSearch", None, _("Products..."),
             group.get('search_products')),
            ("ServiceSearch", None, _("Services..."),
             group.get('search_services')),
            ("DeliverySearch", None, _("Deliveries..."),
             group.get('search_deliveries')),
        ]
        self.pos_ui = self.add_ui_actions('', actions, filename='pos.xml')
        self.set_help_section(_("POS help"), 'app-pos')

    def create_ui(self):
        self.sale_items.set_columns(self.get_columns())
        self.sale_items.set_selection_mode(gtk.SELECTION_BROWSE)
        # Setting up the widget groups
        self.main_vbox.set_focus_chain([self.pos_vbox])

        self.pos_vbox.set_focus_chain([self.list_header_hbox, self.list_vbox])
        self.list_vbox.set_focus_chain([self.footer_hbox])
        self.footer_hbox.set_focus_chain([self.toolbar_vbox])

        # Setting up the toolbar area
        self.toolbar_vbox.set_focus_chain([self.toolbar_button_box])
        self.toolbar_button_box.set_focus_chain([
            self.checkout_button, self.delivery_button, self.edit_item_button,
            self.remove_item_button
        ])

        # Setting up the barcode area
        self.item_hbox.set_focus_chain(
            [self.barcode, self.quantity, self.item_button_box])
        self.item_button_box.set_focus_chain(
            [self.add_button, self.advanced_search])
        self._setup_printer()
        self._setup_widgets()
        self._setup_proxies()
        self._clear_order()

    def activate(self, refresh=True):
        # Admin app doesn't have anything to print/export
        for widget in (self.window.Print, self.window.ExportSpreadSheet):
            widget.set_visible(False)

        # Hide toolbar specially for pos
        self.uimanager.get_widget('/toolbar').hide()
        self.uimanager.get_widget('/menubar/ViewMenu/ToggleToolbar').hide()

        self.check_open_inventory()
        self._update_parameter_widgets()
        self._update_widgets()
        # This is important to do after the other calls, since
        # it emits signals that disable UI which might otherwise
        # be enabled.
        self._printer.run_initial_checks()

        CloseLoanWizardFinishEvent.connect(self._on_CloseLoanWizardFinishEvent)

    def deactivate(self):
        self.uimanager.remove_ui(self.pos_ui)

        # Re enable toolbar
        self.uimanager.get_widget('/toolbar').show()
        self.uimanager.get_widget('/menubar/ViewMenu/ToggleToolbar').show()

        # one PosApp is created everytime the pos is opened. If we dont
        # disconnect, the callback from this instance would still be called, but
        # its no longer valid.
        CloseLoanWizardFinishEvent.disconnect(
            self._on_CloseLoanWizardFinishEvent)

    def setup_focus(self):
        self.barcode.grab_focus()

    def can_change_application(self):
        # Block POS application if we are in the middle of a sale.
        can_change_application = not self._sale_started
        if not can_change_application:
            if yesno(
                    _('You must finish the current sale before you change to '
                      'another application.'), gtk.RESPONSE_NO,
                    _("Cancel sale"), _("Finish sale")):
                self._cancel_order(show_confirmation=False)
                return True

        return can_change_application

    def can_close_application(self):
        can_close_application = not self._sale_started
        if not can_close_application:
            if yesno(
                    _('You must finish or cancel the current sale before you '
                      'can close the POS application.'), gtk.RESPONSE_NO,
                    _("Cancel sale"), _("Finish sale")):
                self._cancel_order(show_confirmation=False)
                return True
        return can_close_application

    def get_columns(self):
        return [
            Column(
                'code',
                title=_('Reference'),
                data_type=str,
                width=130,
                justify=gtk.JUSTIFY_RIGHT),
            Column(
                'full_description',
                title=_('Description'),
                data_type=str,
                expand=True,
                searchable=True,
                ellipsize=pango.ELLIPSIZE_END),
            Column(
                'price',
                title=_('Price'),
                data_type=currency,
                width=110,
                justify=gtk.JUSTIFY_RIGHT),
            Column(
                'quantity_unit',
                title=_('Quantity'),
                data_type=unicode,
                width=110,
                justify=gtk.JUSTIFY_RIGHT),
            Column(
                'total',
                title=_('Total'),
                data_type=currency,
                justify=gtk.JUSTIFY_RIGHT,
                width=100)
        ]

    def set_open_inventory(self):
        self.set_sensitive(self._inventory_widgets, False)

    @public(since="1.5.0")
    def add_sale_item(self, item):
        """Add a TemporarySaleItem item to the sale.

        :param item: a `temporary item <TemporarySaleItem>` to add to the sale.
          If the caller wants to store extra information about the sold items,
          it can create a subclass of TemporarySaleItem and pass that class
          here. This information will propagate when <POSConfirmSaleEvent> is
          emitted.
        """
        assert isinstance(item, TemporarySaleItem)
        self._update_added_item(item)

    #
    # Private
    #

    def _setup_printer(self):
        self._printer = FiscalPrinterHelper(self.store, parent=self)
        self._printer.connect('till-status-changed',
                              self._on_PrinterHelper__till_status_changed)
        self._printer.connect('ecf-changed',
                              self._on_PrinterHelper__ecf_changed)
        self._printer.setup_midnight_check()

    def _setup_proxies(self):
        self.sellableitem_proxy = self.add_proxy(
            Settable(quantity=Decimal(1)), ['quantity'])

    def _update_parameter_widgets(self):
        self.delivery_button.props.visible = sysparam.get_bool(
            'HAS_DELIVERY_MODE')

        window = self.get_toplevel()
        if sysparam.get_bool('POS_FULL_SCREEN'):
            window.fullscreen()
            push_fullscreen(window)
        else:
            pop_fullscreen(window)
            window.unfullscreen()

        for widget in [self.TillOpen, self.TillClose, self.TillVerify]:
            widget.set_visible(not sysparam.get_bool('POS_SEPARATE_CASHIER'))

        if sysparam.get_bool('CONFIRM_SALES_ON_TILL'):
            confirm_label = _("_Close")
        else:
            confirm_label = _("_Checkout")
        button_set_image_with_label(self.checkout_button, gtk.STOCK_APPLY,
                                    confirm_label)

    def _setup_widgets(self):
        self._inventory_widgets = [
            self.Sales, self.barcode, self.quantity, self.sale_items,
            self.advanced_search, self.checkout_button, self.NewTrade,
            self.LoanClose, self.WorkOrderClose
        ]
        self.register_sensitive_group(self._inventory_widgets,
                                      lambda: not self.has_open_inventory())

        self.stoq_logo.set_from_pixbuf(render_logo_pixbuf('pos'))

        self.order_total_label.set_size('xx-large')
        self.order_total_label.set_bold(True)
        self._create_context_menu()

        self.quantity.set_digits(3)

    def _create_context_menu(self):
        menu = ContextMenu()

        item = ContextMenuItem(gtk.STOCK_ADD)
        item.connect('activate', self._on_context_add__activate)
        menu.append(item)

        item = ContextMenuItem(gtk.STOCK_REMOVE)
        item.connect('activate', self._on_context_remove__activate)
        item.connect('can-disable', self._on_context_remove__can_disable)
        menu.append(item)

        self.sale_items.set_context_menu(menu)
        menu.show_all()

    def _update_totals(self):
        subtotal = self._get_subtotal()
        text = _(u"Total: %s") % converter.as_string(currency, subtotal)
        self.order_total_label.set_text(text)

    def _update_added_item(self, sale_item, new_item=True):
        """Insert or update a klist item according with the new_item
        argument
        """
        if new_item:
            if self._coupon_add_item(sale_item) == -1:
                return
            self.sale_items.append(sale_item)
        else:
            self.sale_items.update(sale_item)
        self.sale_items.select(sale_item)
        self.barcode.set_text('')
        self.barcode.grab_focus()
        self._reset_quantity_proxy()
        self._update_totals()

    def _update_list(self, sellable, batch=None):
        assert isinstance(sellable, Sellable)
        try:
            sellable.check_taxes_validity()
        except TaxError as strerr:
            # If the sellable icms taxes are not valid, we cannot sell it.
            warning(strerr)
            return

        quantity = self.sellableitem_proxy.model.quantity
        if sellable.product:
            self._add_product_sellable(sellable, quantity, batch=batch)
        elif sellable.service:
            self._add_service_sellable(sellable, quantity)

    def _add_service_sellable(self, sellable, quantity):
        sale_item = TemporarySaleItem(sellable=sellable, quantity=quantity)
        with api.new_store() as store:
            rv = self.run_dialog(ServiceItemEditor, store, sale_item)

        if not rv:
            return

        self._update_added_item(sale_item)

    def _add_product_sellable(self, sellable, quantity, batch=None):
        product = sellable.product
        if product.storable and not batch and product.storable.is_batch:
            available_batches = list(
                product.storable.get_available_batches(
                    api.get_current_branch(self.store)))
            # The trivial case, where there's just one batch, use it directly
            if len(available_batches) == 1:
                batch = available_batches[0]
                sale_item = TemporarySaleItem(
                    sellable=sellable, quantity=quantity, batch=batch)
                self._update_added_item(sale_item)
                return

            rv = self.run_dialog(
                BatchDecreaseSelectionDialog,
                self.store,
                model=sellable.product_storable,
                quantity=quantity)
            if not rv:
                return

            for batch, b_quantity in rv.items():
                sale_item = TemporarySaleItem(
                    sellable=sellable, quantity=b_quantity, batch=batch)
                self._update_added_item(sale_item)
        else:
            sale_item = TemporarySaleItem(
                sellable=sellable, quantity=quantity, batch=batch)
            self._update_added_item(sale_item)

    def _get_subtotal(self):
        return currency(sum([item.total for item in self.sale_items]))

    def _get_sellable_and_batch(self):
        text = self.barcode.get_text()
        if not text:
            raise StoqlibError("_get_sellable_and_batch needs a barcode")
        text = unicode(text)

        fmt = api.sysparam.get_int('SCALE_BARCODE_FORMAT')

        # Check if this barcode is from a scale
        barinfo = parse_barcode(text, fmt)
        if barinfo:
            text = barinfo.code
            weight = barinfo.weight

        batch = None
        query = Sellable.status == Sellable.STATUS_AVAILABLE

        # FIXME: Put this logic for getting the sellable based on
        # barcode/code/batch_number on domain. Note that something very
        # simular is done on abstractwizard.py

        sellable = self.store.find(
            Sellable, And(query,
                          Lower(Sellable.barcode) == text.lower())).one()

        # If the barcode didnt match, maybe the user typed the product code
        if not sellable:
            sellable = self.store.find(
                Sellable, And(query,
                              Lower(Sellable.code) == text.lower())).one()

        # If none of the above found, try to get the batch number
        if not sellable:
            query = Lower(StorableBatch.batch_number) == text.lower()
            batch = self.store.find(StorableBatch, query).one()
            if batch:
                sellable = batch.storable.product.sellable
                if not sellable.is_available:
                    # If the sellable is not available, reset both
                    sellable = None
                    batch = None

        # If the barcode has the price information, we need to calculate the
        # corresponding weight.
        if barinfo and sellable and barinfo.mode == BarcodeInfo.MODE_PRICE:
            weight = info.price / sellable.price

        if barinfo and sellable:
            self.quantity.set_value(weight)

        return sellable, batch

    def _select_first_item(self):
        if len(self.sale_items):
            # XXX Probably kiwi should handle this for us. Waiting for
            # support
            self.sale_items.select(self.sale_items[0])

    def _set_sale_sensitive(self, value):
        # Enable/disable the part of the ui that is used for sales,
        # usually manipulated when printer information changes.
        widgets = [
            self.barcode, self.quantity, self.sale_items, self.advanced_search,
            self.PaymentReceive
        ]
        self.set_sensitive(widgets, value)

        if value:
            self.barcode.grab_focus()

    def _disable_printer_ui(self):
        self._set_sale_sensitive(False)

        # It's possible to do a Sangria from the Sale search,
        # disable it for now
        widgets = [self.TillOpen, self.TillClose, self.TillVerify, self.Sales]
        self.set_sensitive(widgets, False)

        text = _(u"POS operations requires a connected fiscal printer.")
        self.till_status_label.set_text(text)

    def _till_status_changed(self, closed, blocked):
        def large(s):
            return '<span weight="bold" size="xx-large">%s</span>' % (
                api.escape(s), )

        if closed:
            text = large(_("Till closed"))
            if not blocked:
                text += '\n\n<span size="large"><a href="open-till">%s</a></span>' % (
                    api.escape(_('Open till')))
        elif blocked:
            text = large(_("Till blocked"))
        else:
            text = large(_("Till open"))
        self.till_status_label.set_use_markup(True)
        self.till_status_label.set_justify(gtk.JUSTIFY_CENTER)
        self.till_status_label.set_markup(text)

        self.set_sensitive([self.TillOpen], closed)
        self.set_sensitive([self.TillVerify], not closed and not blocked)
        self.set_sensitive([
            self.TillClose, self.NewTrade, self.LoanClose, self.WorkOrderClose
        ], not closed or blocked)

        self._set_sale_sensitive(not closed and not blocked)

    def _update_widgets(self):
        has_sale_items = len(self.sale_items) >= 1
        self.set_sensitive((self.checkout_button, self.remove_item_button,
                            self.NewDelivery, self.ConfirmOrder),
                           has_sale_items)
        # We can cancel an order whenever we have a coupon opened.
        self.set_sensitive([self.CancelOrder], self._sale_started)
        has_products = False
        has_services = False
        for sale_item in self.sale_items:
            if sale_item and sale_item.sellable.product:
                has_products = True
            if sale_item and sale_item.service:
                has_services = True
            if has_products and has_services:
                break
        self.set_sensitive([self.delivery_button], has_products)
        self.set_sensitive([self.NewDelivery], has_sale_items)
        sale_item = self.sale_items.get_selected()

        if sale_item is not None and sale_item.service:
            # We are fetching DELIVERY_SERVICE into the sale_items' store
            # instead of the default store to avoid accidental commits.
            can_edit = not sysparam.compare_object('DELIVERY_SERVICE',
                                                   sale_item.service)
        else:
            can_edit = False
        self.set_sensitive([self.edit_item_button], can_edit)
        self.set_sensitive([self.remove_item_button], sale_item is not None
                           and sale_item.can_remove)

        self.set_sensitive((self.checkout_button, self.ConfirmOrder),
                           has_products or has_services)
        self.till_status_box.props.visible = not self._sale_started
        self.sale_items.props.visible = self._sale_started

        self._update_totals()
        self._update_buttons()

    def _has_barcode_str(self):
        return self.barcode.get_text().strip() != ''

    def _update_buttons(self):
        has_barcode = self._has_barcode_str()
        has_quantity = self._read_quantity() > 0
        self.set_sensitive([self.add_button], has_barcode and has_quantity)
        self.set_sensitive([self.advanced_search], has_quantity)

    def _read_quantity(self):
        try:
            quantity = self.quantity.read()
        except ValidationError:
            quantity = 0

        return quantity

    def _read_scale(self, sellable):
        data = read_scale_info(self.store)
        self.quantity.set_value(data.weight)

    def _run_advanced_search(self, search_str=None, message=None):
        sellable_view_item = self.run_dialog(
            SellableSearch,
            self.store,
            selection_mode=gtk.SELECTION_BROWSE,
            search_str=search_str,
            sale_items=self.sale_items,
            quantity=self.sellableitem_proxy.model.quantity,
            double_click_confirm=True,
            info_message=message)
        if not sellable_view_item:
            self.barcode.grab_focus()
            return

        sellable = sellable_view_item.sellable
        self._add_sellable(sellable)

    def _reset_quantity_proxy(self):
        self.sellableitem_proxy.model.quantity = Decimal(1)
        self.sellableitem_proxy.update('quantity')
        self.sellableitem_proxy.model.price = None

    def _get_deliverable_items(self):
        """Returns a list of sale items which can be delivered"""
        return [
            item for item in self.sale_items
            if item.sellable.product is not None
        ]

    def _check_delivery_removed(self, sale_item):
        # If a delivery was removed, we need to remove all
        # the references to it eg self._delivery
        if (sale_item.sellable == sysparam.get_object(
                self.store, 'DELIVERY_SERVICE').sellable):
            self._delivery = None

    #
    # Sale Order operations
    #

    def _add_sale_item(self, search_str=None):
        sellable, batch = self._get_sellable_and_batch()
        if not sellable:
            message = (_("The barcode '%s' does not exist. "
                         "Searching for a product instead...") %
                       self.barcode.get_text())
            self._run_advanced_search(search_str, message)
            return

        self._add_sellable(sellable, batch=batch)

    def _add_sellable(self, sellable, batch=None):
        quantity = self._read_quantity()
        if quantity == 0:
            return

        if not sellable.is_valid_quantity(quantity):
            warning(
                _(u"You cannot sell fractions of this product. "
                  u"The '%s' unit does not allow that") %
                sellable.unit_description)
            return

        if sellable.product:
            # If the sellable has a weight unit specified and we have a scale
            # configured for this station, go and check what the scale says.
            if (sellable and sellable.unit
                    and sellable.unit.unit_index == UnitType.WEIGHT
                    and self._scale_settings):
                self._read_scale(sellable)

        storable = sellable.product_storable
        if storable is not None:
            if not self._check_available_stock(storable, sellable):
                info(
                    _("You cannot sell more items of product %s. "
                      "The available quantity is not enough.") %
                    sellable.get_description())
                self.barcode.set_text('')
                self.barcode.grab_focus()
                return

        self._update_list(sellable, batch=batch)
        self.barcode.grab_focus()

    def _check_available_stock(self, storable, sellable):
        branch = api.get_current_branch(self.store)
        available = storable.get_balance_for_branch(branch)
        added = sum([
            sale_item.quantity for sale_item in self.sale_items
            if sale_item.sellable == sellable
        ])
        added += self.sellableitem_proxy.model.quantity
        return available - added >= 0

    def _clear_order(self):
        log.info("Clearing order")
        self._sale_started = False
        self.sale_items.clear()

        widgets = [
            self.search_box, self.list_vbox, self.CancelOrder,
            self.PaymentReceive
        ]
        self.set_sensitive(widgets, True)

        self._suggested_client = None
        self._delivery = None
        self._clear_trade()
        self._reset_quantity_proxy()
        self.barcode.set_text('')
        self._update_widgets()

        # store may already been closed on checkout
        if self._current_store and not self._current_store.obsolete:
            self._current_store.rollback(close=True)
        self._current_store = None

    def _clear_trade(self, remove=False):
        if self._trade and remove:
            self._trade.remove()
        self._trade = None
        self._remove_trade_infobar()

    def _edit_sale_item(self, sale_item):
        if sale_item.service:
            if sysparam.compare_object('DELIVERY_SERVICE', sale_item.service):
                self._edit_delivery()
                return
            with api.new_store() as store:
                model = self.run_dialog(ServiceItemEditor, store, sale_item)
            if model:
                self.sale_items.update(sale_item)
        else:
            # Do not raise any exception here, since this method can be called
            # when the user activate a row with product in the sellables list.
            return

    def _cancel_order(self, show_confirmation=True):
        """
        Cancels the currently opened order.
        @returns: True if the order was canceled, otherwise false
        """
        if len(self.sale_items) and show_confirmation:
            if yesno(
                    _("This will cancel the current order. Are you sure?"),
                    gtk.RESPONSE_NO, _("Don't cancel"), _(u"Cancel order")):
                return False

        log.info("Cancelling coupon")
        if not sysparam.get_bool('CONFIRM_SALES_ON_TILL'):
            if self._coupon:
                self._coupon.cancel()
        self._coupon = None
        self._clear_order()

        return True

    def _create_delivery(self):
        delivery_param = sysparam.get_object(self.store, 'DELIVERY_SERVICE')
        if delivery_param.sellable in self.sale_items:
            self._delivery = delivery_param.sellable

        delivery = self._edit_delivery()
        if delivery:
            self._add_delivery_item(delivery, delivery_param.sellable)
            self._delivery = delivery

    def _edit_delivery(self):
        """Edits a delivery, but do not allow the price to be changed.
        If there's no delivery, create one.
        @returns: The delivery
        """
        # FIXME: Canceling the editor still saves the changes.
        return self.run_dialog(
            CreateDeliveryEditor,
            self.store,
            self._delivery,
            sale_items=self._get_deliverable_items())

    def _add_delivery_item(self, delivery, delivery_sellable):
        for sale_item in self.sale_items:
            if sale_item.sellable == delivery_sellable:
                sale_item.price = delivery.price
                sale_item.notes = delivery.notes
                sale_item.estimated_fix_date = delivery.estimated_fix_date
                self._delivery_item = sale_item
                new_item = False
                break
        else:
            self._delivery_item = TemporarySaleItem(
                sellable=delivery_sellable,
                quantity=1,
                notes=delivery.notes,
                price=delivery.price)
            self._delivery_item.estimated_fix_date = delivery.estimated_fix_date
            new_item = True

        self._update_added_item(self._delivery_item, new_item=new_item)

    def _create_sale(self, store):
        user = api.get_current_user(store)
        branch = api.get_current_branch(store)
        salesperson = user.person.salesperson
        cfop_id = api.sysparam.get_object_id('DEFAULT_SALES_CFOP')
        nature = api.sysparam.get_string('DEFAULT_OPERATION_NATURE')
        group = PaymentGroup(store=store)
        sale = Sale(
            store=store,
            branch=branch,
            salesperson=salesperson,
            group=group,
            cfop_id=cfop_id,
            coupon_id=None,
            operation_nature=nature)

        if self._delivery:
            sale.client = store.fetch(self._delivery.client)
            sale.storeporter = store.fetch(self._delivery.transporter)
            delivery = Delivery(
                store=store,
                address=store.fetch(self._delivery.address),
                transporter=store.fetch(self._delivery.transporter),
            )
        else:
            delivery = None
            sale.client = self._suggested_client

        for fake_sale_item in self.sale_items:
            sale_item = sale.add_sellable(
                store.fetch(fake_sale_item.sellable),
                price=fake_sale_item.price,
                quantity=fake_sale_item.quantity,
                quantity_decreased=fake_sale_item.quantity_decreased,
                batch=store.fetch(fake_sale_item.batch))
            sale_item.notes = fake_sale_item.notes
            sale_item.estimated_fix_date = fake_sale_item.estimated_fix_date

            if delivery and fake_sale_item.deliver:
                delivery.add_item(sale_item)
            elif delivery and fake_sale_item == self._delivery_item:
                delivery.service_item = sale_item

        return sale

    @public(since="1.5.0")
    def checkout(self, cancel_clear=False):
        """Initiates the sale wizard to confirm sale.

        :param cancel_clear: If cancel_clear is true, the sale will be cancelled
          if the checkout is cancelled.
        """
        assert len(self.sale_items) >= 1

        if self._current_store:
            store = self._current_store
            savepoint = 'before_run_fiscalprinter_confirm'
            store.savepoint(savepoint)
        else:
            store = api.new_store()
            savepoint = None

        if self._trade:
            if self._get_subtotal() < self._trade.returned_total:
                info(
                    _("Traded value is greater than the new sale's value. "
                      "Please add more items or return it in Sales app, "
                      "then make a new sale"))
                return

            sale = self._create_sale(store)
            self._trade.new_sale = sale
            self._trade.trade()
        else:
            sale = self._create_sale(store)

        if sysparam.get_bool('CONFIRM_SALES_ON_TILL'):
            sale.order()
            store.commit()
        else:
            assert self._coupon

            ordered = self._coupon.confirm(
                sale, store, savepoint, subtotal=self._get_subtotal())
            # Dont call store.confirm() here, since coupon.confirm()
            # above already did it
            if not ordered:
                # FIXME: Move to TEF plugin
                manager = get_plugin_manager()
                if manager.is_active('tef') or cancel_clear:
                    self._cancel_order(show_confirmation=False)
                elif not self._current_store:
                    # Just do that if a store was created above and
                    # if _cancel_order wasn't called (it closes the connection)
                    store.rollback(close=True)
                return

            log.info("Checking out")

        self._coupon = None
        POSConfirmSaleEvent.emit(sale, self.sale_items[:])

        # We must close the connection only after the event is emmited, since it
        # may use value from the sale that will become invalid after it is
        # closed
        store.close()
        self._clear_order()

    def _remove_selected_item(self):
        sale_item = self.sale_items.get_selected()
        assert sale_item.can_remove
        self._coupon_remove_item(sale_item)
        self.sale_items.remove(sale_item)
        self._check_delivery_removed(sale_item)
        self._select_first_item()
        self._update_widgets()
        self.barcode.grab_focus()

    def _checkout_or_add_item(self):
        search_str = self.barcode.get_text()
        if search_str == '':
            if len(self.sale_items) >= 1:
                self.checkout()
        else:
            self._add_sale_item(search_str)

    def _remove_trade_infobar(self):
        if not self._trade_infobar:
            return
        self._trade_infobar.destroy()
        self._trade_infobar = None

    def _show_trade_infobar(self, trade):
        self._remove_trade_infobar()
        if not trade:
            return

        button = gtk.Button(_("Cancel trade"))
        button.connect('clicked', self._on_remove_trade_button__clicked)
        value = converter.as_string(currency, self._trade.returned_total)
        msg = _("There is a trade with value %s in progress...\n"
                "When checking out, it will be used as part of "
                "the payment.") % (value, )
        self._trade_infobar = self.window.add_info_bar(
            gtk.MESSAGE_INFO, msg, action_widget=button)

    #
    # Coupon related
    #

    def _open_coupon(self):
        coupon = self._printer.create_coupon()

        if coupon:
            while not coupon.open():
                if not yesno(
                        _("It is not possible to start a new sale if the "
                          "fiscal coupon cannot be opened."), gtk.RESPONSE_YES,
                        _("Try again"), _("Cancel sale")):
                    return None

        self.set_sensitive([self.PaymentReceive], False)
        return coupon

    def _coupon_add_item(self, sale_item):
        """Adds an item to the coupon.

        Should return -1 if the coupon was not added, but will return None if
        CONFIRM_SALES_ON_TILL is true

        See :class:`stoqlib.gui.fiscalprinter.FiscalCoupon` for more information
        """
        self._sale_started = True
        if sysparam.get_bool('CONFIRM_SALES_ON_TILL'):
            return

        if self._coupon is None:
            coupon = self._open_coupon()
            if not coupon:
                return -1
            self._coupon = coupon

        return self._coupon.add_item(sale_item)

    def _coupon_remove_item(self, sale_item):
        if sysparam.get_bool('CONFIRM_SALES_ON_TILL'):
            return

        assert self._coupon
        self._coupon.remove_item(sale_item)

    def _close_till(self):
        if self._sale_started:
            if not yesno(
                    _('You must finish or cancel the current sale before '
                      'you can close the till.'), gtk.RESPONSE_NO,
                    _("Cancel sale"), _("Finish sale")):
                return
            self._cancel_order(show_confirmation=False)
        self._printer.close_till()

    #
    # Actions
    #

    def on_CancelOrder__activate(self, action):
        self._cancel_order()

    def on_Clients__activate(self, action):
        self.run_dialog(ClientSearch, self.store, hide_footer=True)

    def on_Sales__activate(self, action):
        with api.new_store() as store:
            self.run_dialog(SaleWithToolbarSearch, store)

    def on_SoldItemsByBranchSearch__activate(self, action):
        self.run_dialog(SoldItemsByBranchSearch, self.store)

    def on_ProductSearch__activate(self, action):
        self.run_dialog(
            ProductSearch,
            self.store,
            hide_footer=True,
            hide_toolbar=True,
            hide_cost_column=True)

    def on_ServiceSearch__activate(self, action):
        self.run_dialog(
            ServiceSearch,
            self.store,
            hide_toolbar=True,
            hide_cost_column=True)

    def on_DeliverySearch__activate(self, action):
        self.run_dialog(DeliverySearch, self.store)

    def on_ConfirmOrder__activate(self, action):
        self.checkout()

    def on_NewDelivery__activate(self, action):
        self._create_delivery()

    def on_PaymentReceive__activate(self, action):
        self.run_dialog(PaymentReceivingSearch, self.store)

    def on_TillClose__activate(self, action):
        self._close_till()

    def on_TillOpen__activate(self, action):
        self._printer.open_till()

    def on_TillVerify__activate(self, action):
        self._printer.verify_till()

    def on_LoanClose__activate(self, action):
        if self.check_open_inventory():
            return

        if self._current_store:
            store = self._current_store
            store.savepoint('before_run_wizard_closeloan')
        else:
            store = api.new_store()

        rv = self.run_dialog(
            CloseLoanWizard, store, create_sale=False, require_sale_items=True)
        if rv:
            if self._suggested_client is None:
                # The loan close wizard lets to close more than one loan at
                # the same time (but only from the same client and branch)
                self._suggested_client = rv[0].client
            self._current_store = store
        elif self._current_store:
            store.rollback_to_savepoint('before_run_wizard_closeloan')
        else:
            store.rollback(close=True)

    def on_WorkOrderClose__activate(self, action):
        if self.check_open_inventory():
            return

        if self._current_store:
            store = self._current_store
            store.savepoint('before_run_search_workorder')
        else:
            store = api.new_store()

        rv = self.run_dialog(
            WorkOrderFinishedSearch, store, double_click_confirm=True)
        if rv:
            work_order = rv.work_order
            for item in work_order.order_items:
                self.add_sale_item(
                    TemporarySaleItem(
                        sellable=item.sellable,
                        quantity=item.quantity,
                        quantity_decreased=item.quantity_decreased,
                        price=item.price,
                        can_remove=False))
            work_order.close()
            if self._suggested_client is None:
                self._suggested_client = work_order.client
            self._current_store = store
        elif self._current_store:
            store.rollback_to_savepoint('before_run_search_workorder')
        else:
            store.rollback(close=True)

    def on_NewTrade__activate(self, action):
        if self._trade:
            if yesno(
                    _("There is already a trade in progress... Do you "
                      "want to cancel it and start a new one?"),
                    gtk.RESPONSE_NO, _("Cancel trade"), _("Finish trade")):
                self._clear_trade(remove=True)
            else:
                return

        if self._current_store:
            store = self._current_store
            store.savepoint('before_run_wizard_saletrade')
        else:
            store = api.new_store()

        trade = self.run_dialog(SaleTradeWizard, store)
        if trade:
            self._trade = trade
            self._current_store = store
        elif self._current_store:
            store.rollback_to_savepoint('before_run_wizard_saletrade')
        else:
            store.rollback(close=True)

        self._show_trade_infobar(trade)

    #
    # Other callbacks
    #

    def _on_context_add__activate(self, menu_item):
        self._run_advanced_search()

    def _on_context_remove__activate(self, menu_item):
        self._remove_selected_item()

    def _on_context_remove__can_disable(self, menu_item):
        selected = self.sale_items.get_selected()
        if selected and selected.can_remove:
            return False

        return True

    def _on_remove_trade_button__clicked(self, button):
        if yesno(
                _("Do you really want to cancel the trade in progress?"),
                gtk.RESPONSE_NO, _("Cancel trade"), _("Don't cancel")):
            self._clear_trade(remove=True)

    def on_till_status_label__activate_link(self, button, link):
        if link == 'open-till':
            self._printer.open_till()
        return True

    def on_advanced_search__clicked(self, button):
        self._run_advanced_search()

    def on_add_button__clicked(self, button):
        self._add_sale_item()

    def on_barcode__activate(self, entry):
        marker("enter pressed")
        self._checkout_or_add_item()

    def after_barcode__changed(self, editable):
        self._update_buttons()

    def on_quantity__activate(self, entry):
        self._checkout_or_add_item()

    def on_quantity__validate(self, entry, value):
        self._update_buttons()
        if value == 0:
            return ValidationError(_("Quantity must be a positive number"))

    def on_sale_items__selection_changed(self, sale_items, sale_item):
        self._update_widgets()

    def on_remove_item_button__clicked(self, button):
        self._remove_selected_item()

    def on_delivery_button__clicked(self, button):
        self._create_delivery()

    def on_checkout_button__clicked(self, button):
        self.checkout()

    def on_edit_item_button__clicked(self, button):
        item = self.sale_items.get_selected()
        if item is None:
            raise StoqlibError("You should have a item selected "
                               "at this point")
        self._edit_sale_item(item)

    def on_sale_items__row_activated(self, sale_items, sale_item):
        self._edit_sale_item(sale_item)

    def _on_PrinterHelper__till_status_changed(self, printer, closed, blocked):
        self._till_status_changed(closed, blocked)

    def _on_PrinterHelper__ecf_changed(self, printer, has_ecf):
        # If we have an ecf, let the other events decide what to disable.
        if has_ecf:
            return

        # We dont have an ecf. Disable till related operations
        self._disable_printer_ui()

    def _on_CloseLoanWizardFinishEvent(self, loans, sale, wizard):
        for item in wizard.get_sold_items():
            sellable, quantity, price = item
            self.add_sale_item(
                TemporarySaleItem(
                    sellable=sellable,
                    quantity=quantity,
                    # Quantity was already decreased on loan
                    quantity_decreased=quantity,
                    price=price,
                    can_remove=False))
Example #5
0
File: pos.py Project: tmaxter/stoq
class PosApp(ShellApp):

    app_title = _('Point of Sales')
    gladefile = "pos"

    def __init__(self, window, store=None):
        self._suggested_client = None
        self._current_store = None
        self._trade = None
        self._trade_infobar = None

        ShellApp.__init__(self, window, store=store)

        self._delivery = None
        self.param = api.sysparam(self.store)
        self._coupon = None
        # Cant use self._coupon to verify if there is a sale, since
        # CONFIRM_SALES_ON_TILL doesnt create a coupon
        self._sale_started = False
        self._scale_settings = DeviceSettings.get_scale_settings(self.store)

    #
    # Application
    #

    def create_actions(self):
        group = get_accels('app.pos')
        actions = [
            # File
            ('NewTrade', None, _('Trade...'), group.get('new_trade')),
            ('PaymentReceive', None, _('Payment Receival...'),
             group.get('payment_receive')),
            ("TillOpen", None, _("Open Till..."), group.get('till_open')),
            ("TillClose", None, _("Close Till..."), group.get('till_close')),
            ("TillVerify", None, _("Verify Till..."),
             group.get('till_verify')),
            ("LoanClose", None, _("Close loan...")),
            ("WorkOrderClose", None, _("Close work order...")),

            # Order
            ("OrderMenu", None, _("Order")),
            ('ConfirmOrder', None, _('Confirm...'),
             group.get('order_confirm')),
            ('CancelOrder', None, _('Cancel...'), group.get('order_cancel')),
            ('NewDelivery', None, _('Create delivery...'),
             group.get('order_create_delivery')),

            # Search
            ("Sales", None, _("Sales..."), group.get('search_sales')),
            ("SoldItemsByBranchSearch", None, _("Sold Items by Branch..."),
             group.get('search_sold_items')),
            ("Clients", None, _("Clients..."), group.get('search_clients')),
            ("ProductSearch", None, _("Products..."),
             group.get('search_products')),
            ("ServiceSearch", None, _("Services..."),
             group.get('search_services')),
            ("DeliverySearch", None, _("Deliveries..."),
             group.get('search_deliveries')),
        ]
        self.pos_ui = self.add_ui_actions('', actions, filename='pos.xml')
        self.set_help_section(_("POS help"), 'app-pos')

    def create_ui(self):
        self.sale_items.set_columns(self.get_columns())
        self.sale_items.set_selection_mode(gtk.SELECTION_BROWSE)
        # Setting up the widget groups
        self.main_vbox.set_focus_chain([self.pos_vbox])

        self.pos_vbox.set_focus_chain([self.list_header_hbox, self.list_vbox])
        self.list_vbox.set_focus_chain([self.footer_hbox])
        self.footer_hbox.set_focus_chain([self.toolbar_vbox])

        # Setting up the toolbar area
        self.toolbar_vbox.set_focus_chain([self.toolbar_button_box])
        self.toolbar_button_box.set_focus_chain([
            self.checkout_button, self.delivery_button, self.edit_item_button,
            self.remove_item_button
        ])

        # Setting up the barcode area
        self.item_hbox.set_focus_chain(
            [self.barcode, self.quantity, self.item_button_box])
        self.item_button_box.set_focus_chain(
            [self.add_button, self.advanced_search])
        self._setup_printer()
        self._setup_widgets()
        self._setup_proxies()
        self._clear_order()

    def activate(self, params):
        # Admin app doesn't have anything to print/export
        for widget in (self.window.Print, self.window.ExportSpreadSheet):
            widget.set_visible(False)

        # Hide toolbar specially for pos
        self.uimanager.get_widget('/toolbar').hide()
        self.uimanager.get_widget('/menubar/ViewMenu/ToggleToolbar').hide()

        self.check_open_inventory()
        self._update_parameter_widgets()
        self._update_widgets()
        # This is important to do after the other calls, since
        # it emits signals that disable UI which might otherwise
        # be enabled.
        self._printer.run_initial_checks()

        CloseLoanWizardFinishEvent.connect(self._on_CloseLoanWizardFinishEvent)

    def deactivate(self):
        self.uimanager.remove_ui(self.pos_ui)

        # Re enable toolbar
        self.uimanager.get_widget('/toolbar').show()
        self.uimanager.get_widget('/menubar/ViewMenu/ToggleToolbar').show()

        # one PosApp is created everytime the pos is opened. If we dont
        # disconnect, the callback from this instance would still be called, but
        # its no longer valid.
        CloseLoanWizardFinishEvent.disconnect(
            self._on_CloseLoanWizardFinishEvent)

    def setup_focus(self):
        self.barcode.grab_focus()

    def can_change_application(self):
        # Block POS application if we are in the middle of a sale.
        can_change_application = not self._sale_started
        if not can_change_application:
            if yesno(
                    _('You must finish the current sale before you change to '
                      'another application.'), gtk.RESPONSE_NO,
                    _("Cancel sale"), _("Finish sale")):
                self._cancel_order(show_confirmation=False)
                return True

        return can_change_application

    def can_close_application(self):
        can_close_application = not self._sale_started
        if not can_close_application:
            if yesno(
                    _('You must finish or cancel the current sale before you '
                      'can close the POS application.'), gtk.RESPONSE_NO,
                    _("Cancel sale"), _("Finish sale")):
                self._cancel_order(show_confirmation=False)
                return True
        return can_close_application

    def get_columns(self):
        return [
            Column('code',
                   title=_('Reference'),
                   data_type=str,
                   width=130,
                   justify=gtk.JUSTIFY_RIGHT),
            Column('description',
                   title=_('Description'),
                   data_type=str,
                   expand=True,
                   searchable=True,
                   ellipsize=pango.ELLIPSIZE_END),
            Column('price',
                   title=_('Price'),
                   data_type=currency,
                   width=110,
                   justify=gtk.JUSTIFY_RIGHT),
            Column('quantity_unit',
                   title=_('Quantity'),
                   data_type=unicode,
                   width=110,
                   justify=gtk.JUSTIFY_RIGHT),
            Column('total',
                   title=_('Total'),
                   data_type=currency,
                   justify=gtk.JUSTIFY_RIGHT,
                   width=100)
        ]

    def set_open_inventory(self):
        self.set_sensitive(self._inventory_widgets, False)

    @public(since="1.5.0")
    def add_sale_item(self, item):
        """Add a TemporarySaleItem item to the sale.

        :param item: a `temporary item <TemporarySaleItem>` to add to the sale.
          If the caller wants to store extra information about the sold items,
          it can create a subclass of TemporarySaleItem and pass that class
          here. This information will propagate when <POSConfirmSaleEvent> is
          emitted.
        """
        assert isinstance(item, TemporarySaleItem)
        self._update_added_item(item)

    #
    # Private
    #

    def _setup_printer(self):
        self._printer = FiscalPrinterHelper(self.store, parent=self)
        self._printer.connect('till-status-changed',
                              self._on_PrinterHelper__till_status_changed)
        self._printer.connect('ecf-changed',
                              self._on_PrinterHelper__ecf_changed)
        self._printer.setup_midnight_check()

    def _set_product_on_sale(self):
        sellable = self._get_sellable()
        # If the sellable has a weight unit specified and we have a scale
        # configured for this station, go and check out what the printer says.
        if (sellable and sellable.unit
                and sellable.unit.unit_index == UnitType.WEIGHT
                and self._scale_settings):
            self._read_scale()

    def _setup_proxies(self):
        self.sellableitem_proxy = self.add_proxy(Settable(quantity=Decimal(1)),
                                                 ['quantity'])

    def _update_parameter_widgets(self):
        self.delivery_button.props.visible = self.param.HAS_DELIVERY_MODE

        window = self.get_toplevel()
        if self.param.POS_FULL_SCREEN:
            window.fullscreen()
            push_fullscreen(window)
        else:
            pop_fullscreen(window)
            window.unfullscreen()

        for widget in [self.TillOpen, self.TillClose, self.TillVerify]:
            widget.set_visible(not self.param.POS_SEPARATE_CASHIER)

        if self.param.CONFIRM_SALES_ON_TILL:
            confirm_label = _("_Close")
        else:
            confirm_label = _("_Checkout")
        button_set_image_with_label(self.checkout_button, gtk.STOCK_APPLY,
                                    confirm_label)

    def _setup_widgets(self):
        self._inventory_widgets = [
            self.Sales, self.barcode, self.quantity, self.sale_items,
            self.advanced_search, self.checkout_button, self.NewTrade,
            self.LoanClose, self.WorkOrderClose
        ]
        self.register_sensitive_group(self._inventory_widgets,
                                      lambda: not self.has_open_inventory())

        self.stoq_logo.set_from_pixbuf(render_logo_pixbuf('pos'))

        self.order_total_label.set_size('xx-large')
        self.order_total_label.set_bold(True)
        self._create_context_menu()

        self.quantity.set_digits(3)

    def _create_context_menu(self):
        menu = ContextMenu()

        item = ContextMenuItem(gtk.STOCK_ADD)
        item.connect('activate', self._on_context_add__activate)
        menu.append(item)

        item = ContextMenuItem(gtk.STOCK_REMOVE)
        item.connect('activate', self._on_context_remove__activate)
        item.connect('can-disable', self._on_context_remove__can_disable)
        menu.append(item)

        self.sale_items.set_context_menu(menu)
        menu.show_all()

    def _update_totals(self):
        subtotal = self._get_subtotal()
        text = _(u"Total: %s") % converter.as_string(currency, subtotal)
        self.order_total_label.set_text(text)

    def _update_added_item(self, sale_item, new_item=True):
        """Insert or update a klist item according with the new_item
        argument
        """
        if new_item:
            if self._coupon_add_item(sale_item) == -1:
                return
            self.sale_items.append(sale_item)
        else:
            self.sale_items.update(sale_item)
        self.sale_items.select(sale_item)
        self.barcode.set_text('')
        self.barcode.grab_focus()
        self._reset_quantity_proxy()
        self._update_totals()

    def _update_list(self, sellable):
        assert isinstance(sellable, Sellable)
        try:
            sellable.check_taxes_validity()
        except TaxError as strerr:
            # If the sellable icms taxes are not valid, we cannot sell it.
            warning(strerr)
            return

        quantity = self.sellableitem_proxy.model.quantity

        is_service = sellable.service
        if is_service and quantity > 1:
            # It's not a common operation to add more than one item at
            # a time, it's also problematic since you'd have to show
            # one dialog per service item. See #3092
            info(
                _("It's not possible to add more than one service "
                  "at a time to an order. So, only one was added."))

        sale_item = TemporarySaleItem(sellable=sellable, quantity=quantity)
        if is_service:
            with api.trans() as store:
                rv = self.run_dialog(ServiceItemEditor, store, sale_item)
            if not rv:
                return
        self._update_added_item(sale_item)

    def _get_subtotal(self):
        return currency(sum([item.total for item in self.sale_items]))

    def _get_sellable(self):
        barcode = self.barcode.get_text()
        if not barcode:
            raise StoqlibError("_get_sellable needs a barcode")
        barcode = unicode(barcode)

        fmt = api.sysparam(self.store).SCALE_BARCODE_FORMAT

        # Check if this barcode is from a scale
        info = parse_barcode(barcode, fmt)
        if info:
            barcode = info.code
            weight = info.weight

        sellable = self.store.find(Sellable,
                                   barcode=barcode,
                                   status=Sellable.STATUS_AVAILABLE).one()

        # If the barcode didnt match, maybe the user typed the product code
        if not sellable:
            sellable = self.store.find(Sellable,
                                       code=barcode,
                                       status=Sellable.STATUS_AVAILABLE).one()

        # If the barcode has the price information, we need to calculate the
        # corresponding weight.
        if info and sellable and info.mode == BarcodeInfo.MODE_PRICE:
            weight = info.price / sellable.price

        if info and sellable:
            self.quantity.set_value(weight)

        return sellable

    def _select_first_item(self):
        if len(self.sale_items):
            # XXX Probably kiwi should handle this for us. Waiting for
            # support
            self.sale_items.select(self.sale_items[0])

    def _set_sale_sensitive(self, value):
        # Enable/disable the part of the ui that is used for sales,
        # usually manipulated when printer information changes.
        widgets = [
            self.barcode, self.quantity, self.sale_items, self.advanced_search,
            self.PaymentReceive
        ]
        self.set_sensitive(widgets, value)

        if value:
            self.barcode.grab_focus()

    def _disable_printer_ui(self):
        self._set_sale_sensitive(False)

        # It's possible to do a Sangria from the Sale search,
        # disable it for now
        widgets = [self.TillOpen, self.TillClose, self.TillVerify, self.Sales]
        self.set_sensitive(widgets, False)

        text = _(u"POS operations requires a connected fiscal printer.")
        self.till_status_label.set_text(text)

    def _till_status_changed(self, closed, blocked):
        def large(s):
            return '<span weight="bold" size="xx-large">%s</span>' % (
                api.escape(s), )

        if closed:
            text = large(_("Till closed"))
            if not blocked:
                text += '\n\n<span size="large"><a href="open-till">%s</a></span>' % (
                    api.escape(_('Open till')))
        elif blocked:
            text = large(_("Till blocked"))
        else:
            text = large(_("Till open"))
        self.till_status_label.set_use_markup(True)
        self.till_status_label.set_justify(gtk.JUSTIFY_CENTER)
        self.till_status_label.set_markup(text)

        self.set_sensitive([self.TillOpen], closed)
        self.set_sensitive([self.TillVerify], not closed and not blocked)
        self.set_sensitive([
            self.TillClose, self.NewTrade, self.LoanClose, self.WorkOrderClose
        ], not closed or blocked)

        self._set_sale_sensitive(not closed and not blocked)

    def _update_widgets(self):
        has_sale_items = len(self.sale_items) >= 1
        self.set_sensitive((self.checkout_button, self.remove_item_button,
                            self.NewDelivery, self.ConfirmOrder),
                           has_sale_items)
        # We can cancel an order whenever we have a coupon opened.
        self.set_sensitive([self.CancelOrder], self._sale_started)
        has_products = False
        has_services = False
        for sale_item in self.sale_items:
            if sale_item and sale_item.sellable.product:
                has_products = True
            if sale_item and sale_item.service:
                has_services = True
            if has_products and has_services:
                break
        self.set_sensitive([self.delivery_button], has_products)
        self.set_sensitive([self.NewDelivery], has_sale_items)
        sale_item = self.sale_items.get_selected()

        if sale_item is not None and sale_item.service:
            store = sale_item.service.store
            # We are fetching DELIVERY_SERVICE into the sale_items' store
            # instead of the default store to avoid accidental commits.
            delivery_service = store.fetch(self.param.DELIVERY_SERVICE)
            can_edit = sale_item.service != delivery_service
        else:
            can_edit = False
        self.set_sensitive([self.edit_item_button], can_edit)
        self.set_sensitive([self.remove_item_button], sale_item is not None
                           and sale_item.can_remove)

        self.set_sensitive((self.checkout_button, self.ConfirmOrder),
                           has_products or has_services)
        self.till_status_box.props.visible = not self._sale_started
        self.sale_items.props.visible = self._sale_started

        self._update_totals()
        self._update_buttons()

    def _has_barcode_str(self):
        return self.barcode.get_text().strip() != ''

    def _update_buttons(self):
        has_barcode = self._has_barcode_str()
        has_quantity = self._read_quantity() > 0
        self.set_sensitive([self.add_button], has_barcode and has_quantity)
        self.set_sensitive([self.advanced_search], has_quantity)

    def _read_quantity(self):
        try:
            quantity = self.quantity.read()
        except ValidationError:
            quantity = 0

        return quantity

    def _read_scale(self, sellable):
        data = read_scale_info(self.store)
        self.quantity.set_value(data.weight)

    def _run_advanced_search(self, search_str=None, message=None):
        sellable_view_item = self.run_dialog(
            SellableSearch,
            self.store,
            selection_mode=gtk.SELECTION_BROWSE,
            search_str=search_str,
            sale_items=self.sale_items,
            quantity=self.sellableitem_proxy.model.quantity,
            double_click_confirm=True,
            info_message=message)
        if not sellable_view_item:
            self.barcode.grab_focus()
            return

        sellable = sellable_view_item.sellable
        self._add_sellable(sellable)

    def _reset_quantity_proxy(self):
        self.sellableitem_proxy.model.quantity = Decimal(1)
        self.sellableitem_proxy.update('quantity')
        self.sellableitem_proxy.model.price = None

    def _get_deliverable_items(self):
        """Returns a list of sale items which can be delivered"""
        return [
            item for item in self.sale_items
            if item.sellable.product is not None
        ]

    def _check_delivery_removed(self, sale_item):
        # If a delivery was removed, we need to remove all
        # the references to it eg self._delivery
        if sale_item.sellable == self.param.DELIVERY_SERVICE.sellable:
            self._delivery = None

    #
    # Sale Order operations
    #

    def _add_sale_item(self, search_str=None):
        sellable = self._get_sellable()
        if not sellable:
            message = (_("The barcode '%s' does not exist. "
                         "Searching for a product instead...") %
                       self.barcode.get_text())
            self._run_advanced_search(search_str, message)
            return

        self._add_sellable(sellable)

    def _add_sellable(self, sellable):
        quantity = self._read_quantity()
        if quantity == 0:
            return

        if not sellable.is_valid_quantity(quantity):
            warning(
                _(u"You cannot sell fractions of this product. "
                  u"The '%s' unit does not allow that") %
                sellable.get_unit_description())
            return

        if sellable.product:
            # If the sellable has a weight unit specified and we have a scale
            # configured for this station, go and check what the scale says.
            if (sellable and sellable.unit
                    and sellable.unit.unit_index == UnitType.WEIGHT
                    and self._scale_settings):
                self._read_scale(sellable)

        storable = sellable.product_storable
        if storable is not None:
            if not self._check_available_stock(storable, sellable):
                info(
                    _("You cannot sell more items of product %s. "
                      "The available quantity is not enough.") %
                    sellable.get_description())
                self.barcode.set_text('')
                self.barcode.grab_focus()
                return

        self._update_list(sellable)
        self.barcode.grab_focus()

    def _check_available_stock(self, storable, sellable):
        branch = api.get_current_branch(self.store)
        available = storable.get_balance_for_branch(branch)
        added = sum([
            sale_item.quantity for sale_item in self.sale_items
            if sale_item.sellable == sellable
        ])
        added += self.sellableitem_proxy.model.quantity
        return available - added >= 0

    def _clear_order(self):
        log.info("Clearing order")
        self._sale_started = False
        self.sale_items.clear()

        widgets = [
            self.search_box, self.list_vbox, self.CancelOrder,
            self.PaymentReceive
        ]
        self.set_sensitive(widgets, True)

        self._suggested_client = None
        self._delivery = None
        self._clear_trade()
        self._reset_quantity_proxy()
        self.barcode.set_text('')
        self._update_widgets()

        # store may already been closed on checkout
        if self._current_store and not self._current_store.obsolete:
            self._current_store.rollback(close=True)
        self._current_store = None

    def _clear_trade(self, remove=False):
        if self._trade and remove:
            self._trade.remove()
        self._trade = None
        self._remove_trade_infobar()

    def _edit_sale_item(self, sale_item):
        if sale_item.service:
            delivery_service = self.param.DELIVERY_SERVICE
            if sale_item.service == delivery_service:
                self._edit_delivery()
                return
            with api.trans() as store:
                model = self.run_dialog(ServiceItemEditor, store, sale_item)
            if model:
                self.sale_items.update(sale_item)
        else:
            # Do not raise any exception here, since this method can be called
            # when the user activate a row with product in the sellables list.
            return

    def _cancel_order(self, show_confirmation=True):
        """
        Cancels the currently opened order.
        @returns: True if the order was canceled, otherwise false
        """
        if len(self.sale_items) and show_confirmation:
            if yesno(_("This will cancel the current order. Are you sure?"),
                     gtk.RESPONSE_NO, _("Don't cancel"), _(u"Cancel order")):
                return False

        log.info("Cancelling coupon")
        if not self.param.CONFIRM_SALES_ON_TILL:
            if self._coupon:
                self._coupon.cancel()
        self._coupon = None
        self._clear_order()

        return True

    def _create_delivery(self):
        delivery_sellable = self.param.DELIVERY_SERVICE.sellable
        if delivery_sellable in self.sale_items:
            self._delivery = delivery_sellable

        delivery = self._edit_delivery()
        if delivery:
            self._add_delivery_item(delivery, delivery_sellable)
            self._delivery = delivery

    def _edit_delivery(self):
        """Edits a delivery, but do not allow the price to be changed.
        If there's no delivery, create one.
        @returns: The delivery
        """
        # FIXME: Canceling the editor still saves the changes.
        return self.run_dialog(CreateDeliveryEditor,
                               self.store,
                               self._delivery,
                               sale_items=self._get_deliverable_items())

    def _add_delivery_item(self, delivery, delivery_sellable):
        for sale_item in self.sale_items:
            if sale_item.sellable == delivery_sellable:
                sale_item.price = delivery.price
                sale_item.notes = delivery.notes
                sale_item.estimated_fix_date = delivery.estimated_fix_date
                self._delivery_item = sale_item
                new_item = False
                break
        else:
            self._delivery_item = TemporarySaleItem(sellable=delivery_sellable,
                                                    quantity=1,
                                                    notes=delivery.notes,
                                                    price=delivery.price)
            self._delivery_item.estimated_fix_date = delivery.estimated_fix_date
            new_item = True

        self._update_added_item(self._delivery_item, new_item=new_item)

    def _create_sale(self, store):
        user = api.get_current_user(store)
        branch = api.get_current_branch(store)
        salesperson = user.person.salesperson
        cfop = api.sysparam(store).DEFAULT_SALES_CFOP
        group = PaymentGroup(store=store)
        sale = Sale(
            store=store,
            branch=branch,
            salesperson=salesperson,
            group=group,
            cfop=cfop,
            coupon_id=None,
            operation_nature=api.sysparam(store).DEFAULT_OPERATION_NATURE)

        if self._delivery:
            sale.client = store.fetch(self._delivery.client)
            sale.storeporter = store.fetch(self._delivery.transporter)
            delivery = Delivery(
                store=store,
                address=store.fetch(self._delivery.address),
                transporter=store.fetch(self._delivery.transporter),
            )
        else:
            delivery = None
            sale.client = self._suggested_client

        for fake_sale_item in self.sale_items:
            sale_item = sale.add_sellable(
                store.fetch(fake_sale_item.sellable),
                price=fake_sale_item.price,
                quantity=fake_sale_item.quantity,
                quantity_decreased=fake_sale_item.quantity_decreased)
            sale_item.notes = fake_sale_item.notes
            sale_item.estimated_fix_date = fake_sale_item.estimated_fix_date

            if delivery and fake_sale_item.deliver:
                delivery.add_item(sale_item)
            elif delivery and fake_sale_item == self._delivery_item:
                delivery.service_item = sale_item

        return sale

    @public(since="1.5.0")
    def checkout(self, cancel_clear=False):
        """Initiates the sale wizard to confirm sale.

        :param cancel_clear: If cancel_clear is true, the sale will be cancelled
          if the checkout is cancelled.
        """
        assert len(self.sale_items) >= 1

        if self._current_store:
            store = self._current_store
            savepoint = 'before_run_fiscalprinter_confirm'
            store.savepoint(savepoint)
        else:
            store = api.new_store()
            savepoint = None

        if self._trade:
            if self._get_subtotal() < self._trade.returned_total:
                info(
                    _("Traded value is greater than the new sale's value. "
                      "Please add more items or return it in Sales app, "
                      "then make a new sale"))
                return

            sale = self._create_sale(store)
            self._trade.new_sale = sale
            self._trade.trade()
        else:
            sale = self._create_sale(store)

        if self.param.CONFIRM_SALES_ON_TILL:
            sale.order()
            store.commit(close=True)
        else:
            assert self._coupon

            ordered = self._coupon.confirm(sale,
                                           store,
                                           savepoint,
                                           subtotal=self._get_subtotal())
            # Dont call store.confirm() here, since coupon.confirm()
            # above already did it
            if not ordered:
                # FIXME: Move to TEF plugin
                manager = get_plugin_manager()
                if manager.is_active('tef') or cancel_clear:
                    self._cancel_order(show_confirmation=False)
                elif not self._current_store:
                    # Just do that if a store was created above and
                    # if _cancel_order wasn't called (it closes the connection)
                    store.rollback(close=True)
                return

            log.info("Checking out")
            store.close()

        self._coupon = None
        POSConfirmSaleEvent.emit(sale, self.sale_items[:])
        self._clear_order()

    def _remove_selected_item(self):
        sale_item = self.sale_items.get_selected()
        assert sale_item.can_remove
        self._coupon_remove_item(sale_item)
        self.sale_items.remove(sale_item)
        self._check_delivery_removed(sale_item)
        self._select_first_item()
        self._update_widgets()
        self.barcode.grab_focus()

    def _checkout_or_add_item(self):
        search_str = self.barcode.get_text()
        if search_str == '':
            if len(self.sale_items) >= 1:
                self.checkout()
        else:
            self._add_sale_item(search_str)

    def _remove_trade_infobar(self):
        if not self._trade_infobar:
            return
        self._trade_infobar.destroy()
        self._trade_infobar = None

    def _show_trade_infobar(self, trade):
        self._remove_trade_infobar()
        if not trade:
            return

        button = gtk.Button(_("Cancel trade"))
        button.connect('clicked', self._on_remove_trade_button__clicked)
        value = converter.as_string(currency, self._trade.returned_total)
        msg = _("There is a trade with value %s in progress...\n"
                "When checking out, it will be used as part of "
                "the payment.") % (value, )
        self._trade_infobar = self.window.add_info_bar(gtk.MESSAGE_INFO,
                                                       msg,
                                                       action_widget=button)

    #
    # Coupon related
    #

    def _open_coupon(self):
        coupon = self._printer.create_coupon()

        if coupon:
            while not coupon.open():
                if not yesno(
                        _("It is not possible to start a new sale if the "
                          "fiscal coupon cannot be opened."), gtk.RESPONSE_YES,
                        _("Try again"), _("Cancel sale")):
                    return None

        self.set_sensitive([self.PaymentReceive], False)
        return coupon

    def _coupon_add_item(self, sale_item):
        """Adds an item to the coupon.

        Should return -1 if the coupon was not added, but will return None if
        CONFIRM_SALES_ON_TILL is true

        See :class:`stoqlib.gui.fiscalprinter.FiscalCoupon` for more information
        """
        self._sale_started = True
        if self.param.CONFIRM_SALES_ON_TILL:
            return

        if self._coupon is None:
            coupon = self._open_coupon()
            if not coupon:
                return -1
            self._coupon = coupon

        return self._coupon.add_item(sale_item)

    def _coupon_remove_item(self, sale_item):
        if self.param.CONFIRM_SALES_ON_TILL:
            return

        assert self._coupon
        self._coupon.remove_item(sale_item)

    def _close_till(self):
        if self._sale_started:
            if not yesno(
                    _('You must finish or cancel the current sale before '
                      'you can close the till.'), gtk.RESPONSE_NO,
                    _("Cancel sale"), _("Finish sale")):
                return
            self._cancel_order(show_confirmation=False)
        self._printer.close_till()

    #
    # Actions
    #

    def on_CancelOrder__activate(self, action):
        self._cancel_order()

    def on_Clients__activate(self, action):
        self.run_dialog(ClientSearch, self.store, hide_footer=True)

    def on_Sales__activate(self, action):
        with api.trans() as store:
            self.run_dialog(SaleWithToolbarSearch, store)

    def on_SoldItemsByBranchSearch__activate(self, action):
        self.run_dialog(SoldItemsByBranchSearch, self.store)

    def on_ProductSearch__activate(self, action):
        self.run_dialog(ProductSearch,
                        self.store,
                        hide_footer=True,
                        hide_toolbar=True,
                        hide_cost_column=True)

    def on_ServiceSearch__activate(self, action):
        self.run_dialog(ServiceSearch,
                        self.store,
                        hide_toolbar=True,
                        hide_cost_column=True)

    def on_DeliverySearch__activate(self, action):
        self.run_dialog(DeliverySearch, self.store)

    def on_ConfirmOrder__activate(self, action):
        self.checkout()

    def on_NewDelivery__activate(self, action):
        self._create_delivery()

    def on_PaymentReceive__activate(self, action):
        self.run_dialog(PaymentReceivingSearch, self.store)

    def on_TillClose__activate(self, action):
        self._close_till()

    def on_TillOpen__activate(self, action):
        self._printer.open_till()

    def on_TillVerify__activate(self, action):
        self._printer.verify_till()

    def on_LoanClose__activate(self, action):
        if self.check_open_inventory():
            return

        if self._current_store:
            store = self._current_store
            store.savepoint('before_run_wizard_closeloan')
        else:
            store = api.new_store()

        rv = self.run_dialog(CloseLoanWizard,
                             store,
                             create_sale=False,
                             require_sale_items=True)
        if rv:
            if self._suggested_client is None:
                # The loan close wizard lets to close more than one loan at
                # the same time (but only from the same client and branch)
                self._suggested_client = rv[0].client
            self._current_store = store
        elif self._current_store:
            store.rollback_to_savepoint('before_run_wizard_closeloan')
        else:
            store.rollback(close=True)

    def on_WorkOrderClose__activate(self, action):
        if self.check_open_inventory():
            return

        if self._current_store:
            store = self._current_store
            store.savepoint('before_run_search_workorder')
        else:
            store = api.new_store()

        rv = self.run_dialog(WorkOrderFinishedSearch, store)
        if rv:
            work_order = rv.work_order
            for item in work_order.order_items:
                self.add_sale_item(
                    TemporarySaleItem(
                        sellable=item.sellable,
                        quantity=item.quantity,
                        # Quantity was already decreased on work order
                        quantity_decreased=item.quantity,
                        price=item.price,
                        can_remove=False))
            work_order.close()
            if self._suggested_client is None:
                self._suggested_client = work_order.client
            self._current_store = store
        elif self._current_store:
            store.rollback_to_savepoint('before_run_search_workorder')
        else:
            store.rollback(close=True)

    def on_NewTrade__activate(self, action):
        if self._trade:
            if yesno(
                    _("There is already a trade in progress... Do you "
                      "want to cancel it and start a new one?"),
                    gtk.RESPONSE_NO, _("Cancel trade"), _("Finish trade")):
                self._clear_trade(remove=True)
            else:
                return

        if self._current_store:
            store = self._current_store
            store.savepoint('before_run_wizard_saletrade')
        else:
            store = api.new_store()

        trade = self.run_dialog(SaleTradeWizard, store)
        if trade:
            self._trade = trade
            self._current_store = store
        elif self._current_store:
            store.rollback_to_savepoint('before_run_wizard_saletrade')
        else:
            store.rollback(close=True)

        self._show_trade_infobar(trade)

    #
    # Other callbacks
    #

    def _on_context_add__activate(self, menu_item):
        self._run_advanced_search()

    def _on_context_remove__activate(self, menu_item):
        self._remove_selected_item()

    def _on_context_remove__can_disable(self, menu_item):
        selected = self.sale_items.get_selected()
        if selected and selected.can_remove:
            return False

        return True

    def _on_remove_trade_button__clicked(self, button):
        if yesno(_("Do you really want to cancel the trade in progress?"),
                 gtk.RESPONSE_NO, _("Cancel trade"), _("Don't cancel")):
            self._clear_trade(remove=True)

    def on_till_status_label__activate_link(self, button, link):
        if link == 'open-till':
            self._printer.open_till()
        return True

    def on_advanced_search__clicked(self, button):
        self._run_advanced_search()

    def on_add_button__clicked(self, button):
        self._add_sale_item()

    def on_barcode__activate(self, entry):
        marker("enter pressed")
        self._checkout_or_add_item()

    def after_barcode__changed(self, editable):
        self._update_buttons()

    def on_quantity__activate(self, entry):
        self._checkout_or_add_item()

    def on_quantity__validate(self, entry, value):
        self._update_buttons()
        if value == 0:
            return ValidationError(_("Quantity must be a positive number"))

    def on_sale_items__selection_changed(self, sale_items, sale_item):
        self._update_widgets()

    def on_remove_item_button__clicked(self, button):
        self._remove_selected_item()

    def on_delivery_button__clicked(self, button):
        self._create_delivery()

    def on_checkout_button__clicked(self, button):
        self.checkout()

    def on_edit_item_button__clicked(self, button):
        item = self.sale_items.get_selected()
        if item is None:
            raise StoqlibError("You should have a item selected "
                               "at this point")
        self._edit_sale_item(item)

    def on_sale_items__row_activated(self, sale_items, sale_item):
        self._edit_sale_item(sale_item)

    def _on_PrinterHelper__till_status_changed(self, printer, closed, blocked):
        self._till_status_changed(closed, blocked)

    def _on_PrinterHelper__ecf_changed(self, printer, has_ecf):
        # If we have an ecf, let the other events decide what to disable.
        if has_ecf:
            return

        # We dont have an ecf. Disable till related operations
        self._disable_printer_ui()

    def _on_CloseLoanWizardFinishEvent(self, loans, sale, wizard):
        for item in wizard.get_sold_items():
            sellable, quantity, price = item
            self.add_sale_item(
                TemporarySaleItem(
                    sellable=sellable,
                    quantity=quantity,
                    # Quantity was already decreased on loan
                    quantity_decreased=quantity,
                    price=price,
                    can_remove=False))