Exemplo n.º 1
0
    def _setup_widgets(self):
        is_package = self.model.product.is_package
        component_list = list(self._get_products(additional_query=is_package))
        if component_list:
            self.component_combo.prefill(component_list)
        else:
            self.sort_components_check.set_sensitive(False)

        self.yield_quantity.set_adjustment(
            gtk.Adjustment(lower=0, upper=MAX_INT, step_incr=1))

        self.component_tree.set_columns(self._get_columns())
        self._populate_component_tree()
        self.component_label = SummaryLabel(klist=self.component_tree,
                                            column='total_production_cost',
                                            label='<b>%s</b>' %
                                            api.escape(_(u'Total:')),
                                            value_format='<b>%s</b>')
        self.component_label.show()
        self.component_tree_vbox.pack_start(self.component_label, False)
        self.info_label.set_bold(True)
        self._update_widgets()
        if self.visual_mode:
            self.component_combo.set_sensitive(False)
            self.add_button.set_sensitive(False)
            self.sort_components_check.set_sensitive(False)
Exemplo n.º 2
0
 def _setup_summary_labels(self):
     summary_label = SummaryLabel(klist=self.payments_list,
                                  column='paid_value',
                                  label='<b>%s</b>' % api.escape(_(u"Total:")),
                                  value_format='<b>%s</b>')
     summary_label.show()
     self.payments_vbox.pack_start(summary_label, False)
Exemplo n.º 3
0
 def _setup_summary_labels(self):
     summary_label = SummaryLabel(klist=self.payments_list,
                                  column='paid_value',
                                  label='<b>%s</b>' % api.escape(_(u"Total:")),
                                  value_format='<b>%s</b>')
     summary_label.show()
     self.payments_vbox.pack_start(summary_label, False, True, 0)
Exemplo n.º 4
0
 def _setup_summary_labels(self):
     order_summary_label = SummaryLabel(klist=self.ordered_items,
                                        column='total',
                                        label='<b>%s</b>' %
                                        api.escape(_(u"Total")),
                                        value_format='<b>%s</b>')
     order_summary_label.show()
     self.ordered_vbox.pack_start(order_summary_label, False)
Exemplo n.º 5
0
 def _setup_summary_labels(self):
     order_summary_label = SummaryLabel(
         klist=self.ordered_items,
         column='total',
         label='<b>%s</b>' % api.escape(_(u"Total")),
         value_format='<b>%s</b>')
     order_summary_label.show()
     self.ordered_vbox.pack_start(order_summary_label, False)
Exemplo n.º 6
0
 def _setup_summary_labels(self):
     summary_label = SummaryLabel(
         klist=self.payments_list,
         column="paid_value",
         label="<b>%s</b>" % api.escape(_(u"Total:")),
         value_format="<b>%s</b>",
     )
     summary_label.show()
     self.payments_vbox.pack_start(summary_label, False)
Exemplo n.º 7
0
 def _setup_summary(self):
     # FIXME: Move this into AdditionListSlave
     if not self.summary_label_column:
         self.summary = None
         return
     self.summary = SummaryLabel(klist=self.slave.klist,
                                 column=self.summary_label_column,
                                 label=self.summary_label_text,
                                 value_format='<b>%s</b>')
     self.summary.show()
     self.slave.list_vbox.pack_start(self.summary, False, True, 0)
Exemplo n.º 8
0
    def setup_widgets(self):
        value_format = '<b>%s</b>'
        balance_label = "<b>%s</b>" % api.escape(_("Balance:"))

        account_summary_label = SummaryLabel(klist=self.klist,
                                             column='paid_value',
                                             label=balance_label,
                                             value_format=value_format,
                                             data_func=lambda p: p.is_outpayment())

        account_summary_label.show()
        self.pack_start(account_summary_label, False)
Exemplo n.º 9
0
    def setup_widgets(self):
        value_format = '<b>%s</b>'
        balance_label = "<b>%s</b>" % api.escape(_("Balance:"))

        account_summary_label = SummaryLabel(klist=self.klist,
                                             column='paid_value',
                                             label=balance_label,
                                             value_format=value_format,
                                             data_func=lambda p: p.is_outpayment())

        account_summary_label.show()
        self.pack_start(account_summary_label, False, True, 0)
Exemplo n.º 10
0
    def _setup_widgets(self):
        self._setup_status()

        self.product_list.set_columns(self._get_product_columns())
        products = self.store.find(TransferOrderItem, transfer_order=self.model)
        self.product_list.add_list(list(products))

        value_format = '<b>%s</b>'
        total_label = value_format % api.escape(_("Total:"))
        products_summary_label = SummaryLabel(klist=self.product_list,
                                              column='total',
                                              label=total_label,
                                              value_format=value_format)
        products_summary_label.show()
        self.products_vbox.pack_start(products_summary_label, False, True, 0)
Exemplo n.º 11
0
    def _setup_widgets(self):
        self._setup_status()

        self.product_list.set_columns(self._get_product_columns())
        products = self.store.find(TransferOrderItem, transfer_order=self.model)
        self.product_list.add_list(list(products))

        value_format = '<b>%s</b>'
        total_label = value_format % api.escape(_("Total:"))
        products_summary_label = SummaryLabel(klist=self.product_list,
                                              column='total',
                                              label=total_label,
                                              value_format=value_format)
        products_summary_label.show()
        self.products_vbox.pack_start(products_summary_label, False)
Exemplo n.º 12
0
    def _setup_widgets(self):
        is_package = self.model.product.is_package
        component_list = list(self._get_products(additional_query=is_package))
        if component_list:
            self.component_combo.prefill(component_list)
        else:
            self.sort_components_check.set_sensitive(False)

        self.yield_quantity.set_adjustment(
            gtk.Adjustment(lower=0, upper=MAX_INT, step_incr=1))

        self.component_tree.set_columns(self._get_columns())
        self._populate_component_tree()
        self.component_label = SummaryLabel(
            klist=self.component_tree,
            column='total_production_cost',
            label='<b>%s</b>' % api.escape(_(u'Total:')),
            value_format='<b>%s</b>')
        self.component_label.show()
        self.component_tree_vbox.pack_start(self.component_label, False)
        self.info_label.set_bold(True)
        self._update_widgets()
        if self.visual_mode:
            self.component_combo.set_sensitive(False)
            self.add_button.set_sensitive(False)
            self.sort_components_check.set_sensitive(False)
Exemplo n.º 13
0
class PurchaseFinishProductListStep(WizardEditorStep):
    gladefile = 'PurchaseFinishProductListStep'
    model_type = Settable
    proxy_widgets = ()

    def __init__(self, store, wizard, model):
        WizardEditorStep.__init__(self, store, wizard, model)
        self._setup_widgets()

    def _setup_widgets(self):
        self.product_list.set_columns(self._get_columns())
        items = PurchaseItemView.find_by_purchase(self.store, self.model.purchase)
        self.product_list.add_list(items)

        self._setup_summary()

    def _setup_summary(self):
        self.summary = SummaryLabel(klist=self.product_list,
                                    column='total_received',
                                    value_format='<b>%s</b>')
        self.summary.show()
        self.vbox1.pack_start(self.summary, expand=False)

    def _get_columns(self):
        return [Column('description', title=_('Description'),
                       data_type=str, expand=True, searchable=True),
                Column('quantity', title=_('Ordered'), data_type=int,
                       width=90, format_func=format_quantity, expand=True),
                Column('quantity_received', title=_('Received'), data_type=int,
                       width=110, format_func=format_quantity),
                Column('sellable.unit_description', title=_('Unit'),
                       data_type=str, width=50),
                Column('cost', title=_('Cost'), data_type=currency, width=90),
                Column('total_received', title=_('Total'), data_type=currency,
                       width=100)]

    #
    # WizardStep hooks
    #

    def next_step(self):
        return PurchaseFinishPaymentAdjustStep(self.store, self.wizard,
                                               self.model, self)

    def has_previous_step(self):
        return False
Exemplo n.º 14
0
    def _create_summary_label(self, report, column='value', label=None):
        # Setting tha data
        obj_list = getattr(self, report + '_list')
        box = getattr(self, report + '_vbox')
        if label is None:
            label = _('Total:')
        label = '<b>%s</b>' % api.escape(label)
        value_format = '<b>%s</b>'

        # Creating the label
        label = SummaryLabel(klist=obj_list, column=column, label=label,
                             value_format=value_format)

        # Displaying the label
        box.pack_start(label, False, False, 0)
        label.show()
        return label
Exemplo n.º 15
0
    def _setup_widgets(self):
        # We cant remove cash if closing till from a previous day
        self.value.set_sensitive(not self._previous_day)
        if self._previous_day:
            value = 0
        else:
            value = self.model.get_balance()
        self.value.update(value)

        self.day_history.set_columns(self._get_columns())
        self.day_history.connect("row-activated", lambda olist, row: self.confirm())
        self.day_history.add_list(self._get_day_history())
        summary_day_history = SummaryLabel(
            klist=self.day_history, column="value", label="<b>%s</b>" % api.escape(_(u"Total balance:"))
        )
        summary_day_history.show()
        self.day_history_box.pack_start(summary_day_history, False)
Exemplo n.º 16
0
class PurchaseFinishProductListStep(WizardEditorStep):
    gladefile = 'PurchaseFinishProductListStep'
    model_type = Settable
    proxy_widgets = ()

    def __init__(self, store, wizard, model):
        WizardEditorStep.__init__(self, store, wizard, model)
        self._setup_widgets()

    def _setup_widgets(self):
        self.product_list.set_columns(self._get_columns())
        items = PurchaseItemView.find_by_purchase(self.store, self.model.purchase)
        self.product_list.add_list(items)

        self._setup_summary()

    def _setup_summary(self):
        self.summary = SummaryLabel(klist=self.product_list,
                                    column='total_received',
                                    value_format='<b>%s</b>')
        self.summary.show()
        self.vbox1.pack_start(self.summary, False, True, 0)

    def _get_columns(self):
        return [Column('description', title=_('Description'),
                       data_type=str, expand=True, searchable=True),
                Column('quantity', title=_('Ordered'), data_type=int,
                       width=90, format_func=format_quantity, expand=True),
                Column('quantity_received', title=_('Received'), data_type=int,
                       width=110, format_func=format_quantity),
                Column('sellable.unit_description', title=_('Unit'),
                       data_type=str, width=50),
                Column('cost', title=_('Cost'), data_type=currency, width=90),
                Column('total_received', title=_('Total'), data_type=currency,
                       width=100)]

    #
    # WizardStep hooks
    #

    def next_step(self):
        return PurchaseFinishPaymentAdjustStep(self.store, self.wizard,
                                               self.model, self)

    def has_previous_step(self):
        return False
Exemplo n.º 17
0
    def _setup_widgets(self):
        # We cant remove cash if closing till from a previous day
        self.value.set_sensitive(not self._previous_day)
        if self._previous_day:
            value = 0
        else:
            value = self.model.get_balance()
        self.value.update(value)

        self.day_history.set_columns(self._get_columns())
        self.day_history.connect('row-activated', lambda olist, row: self.confirm())
        self.day_history.add_list(self._get_day_history())
        summary_day_history = SummaryLabel(
            klist=self.day_history,
            column='value',
            label='<b>%s</b>' % api.escape(_(u'Total balance:')))
        summary_day_history.show()
        self.day_history_box.pack_start(summary_day_history, False, True, 0)
Exemplo n.º 18
0
    def _setup_widgets(self):
        self.product_list.set_columns(self._get_product_columns())
        products = self.store.find(ReceivingOrderItem,
                                   receiving_order_id=self.model.id)
        self.product_list.add_list(list(products))

        value_format = '<b>%s</b>'
        total_label = value_format % api.escape(_("Total:"))
        products_summary_label = SummaryLabel(klist=self.product_list,
                                              column='total',
                                              label=total_label,
                                              value_format=value_format)

        products_summary_label.show()
        self.products_vbox.pack_start(products_summary_label, False)

        label = self.print_labels.get_children()[0]
        label = label.get_children()[0].get_children()[1]
        label.set_label(_(u'Print labels'))
Exemplo n.º 19
0
    def _setup_widgets(self):
        self.product_list.set_columns(self._get_product_columns())
        # if the parameter is on we have to build the summary
        if api.sysparam.get_bool("CREATE_PAYMENTS_ON_STOCK_DECREASE"):
            value_format = "<b>%s</b>"
            total_cost_label = value_format % api.escape(_("Total cost:"))
            products_cost_summary_label = SummaryLabel(
                klist=self.product_list, column="total_cost", label=total_cost_label, value_format=value_format
            )
            products_cost_summary_label.show()
            self.products_vbox.pack_start(products_cost_summary_label, False)
        products = self.store.find(StockDecreaseItem, stock_decrease=self.model)
        self.product_list.add_list(list(products))

        if self.model.group:
            self.payment_list.set_columns(self._get_payment_columns())
            self.payment_list.add_list(list(self.model.group.payments))
        else:
            self.notebook.remove_page(2)
Exemplo n.º 20
0
    def setup_widgets(self):
        value_format = '<b>%s</b>'
        total_label = "<b>%s</b>" % api.escape(_("Total:"))
        sales_summary_label = SummaryLabel(klist=self.klist,
                                           column='total',
                                           label=total_label,
                                           value_format=value_format)
        sales_summary_label.show()
        self.pack_start(sales_summary_label, False, True, 0)

        self.has_open_inventory = self._has_open_inventory()

        if len(self.klist):
            return_button = Gtk.Button.new_with_label(_('Return sale'))
            self.button_box.pack_start(return_button, True, True, 0)
            return_button.set_sensitive(bool(self.klist.get_selected()))
            return_button.show()

            self.button_box.return_button = return_button
            return_button.connect('clicked', self._on_return_button__clicked)
Exemplo n.º 21
0
    def _setup_widgets(self):
        self.product_list.set_columns(self._get_product_columns())
        # if the parameter is on we have to build the summary
        if api.sysparam.get_bool("CREATE_PAYMENTS_ON_STOCK_DECREASE"):
            value_format = '<b>%s</b>'
            total_cost_label = value_format % stoq_api.escape(_("Total cost:"))
            products_cost_summary_label = SummaryLabel(klist=self.product_list,
                                                       column='total_cost',
                                                       label=total_cost_label,
                                                       value_format=value_format)
            products_cost_summary_label.show()
            self.products_vbox.pack_start(products_cost_summary_label, False, True, 0)
        products = self.store.find(StockDecreaseItem, stock_decrease=self.model)
        self.product_list.add_list(list(products))

        if self.model.group:
            self.payment_list.set_columns(self._get_payment_columns())
            self.payment_list.add_list(list(self.model.group.payments))
        else:
            self.notebook.remove_page(2)
Exemplo n.º 22
0
 def _setup_summary(self):
     # FIXME: Move this into AdditionListSlave
     if not self.summary_label_column:
         self.summary = None
         return
     self.summary = SummaryLabel(klist=self.slave.klist,
                                 column=self.summary_label_column,
                                 label=self.summary_label_text,
                                 value_format='<b>%s</b>')
     self.summary.show()
     self.slave.list_vbox.pack_start(self.summary, expand=False)
Exemplo n.º 23
0
    def setup_widgets(self):
        value_format = '<b>%s</b>'
        total_label = "<b>%s</b>" % api.escape(_("Total:"))
        sales_summary_label = SummaryLabel(klist=self.klist,
                                           column='total',
                                           label=total_label,
                                           value_format=value_format)
        sales_summary_label.show()
        self.pack_start(sales_summary_label, False)

        self.has_open_inventory = self._has_open_inventory()

        if len(self.klist):
            return_button = gtk.Button(_('Return sale'))
            self.button_box.pack_start(return_button)
            return_button.set_sensitive(bool(self.klist.get_selected()))
            return_button.show()

            self.button_box.return_button = return_button
            return_button.connect('clicked', self._on_return_button__clicked)
Exemplo n.º 24
0
    def _setup_widgets(self):
        self.purchases_list.set_columns(self._get_purchase_columns())
        self.product_list.set_columns(self._get_product_columns())
        self.payments_list.set_columns(self._get_payments_columns())

        purchases = self.model.get_supplier_purchases()
        self.purchases_list.add_list(purchases)

        self._build_data(purchases)
        self.product_list.add_list(self.products)
        self.payments_list.add_list(self.payments)

        value_format = '<b>%s</b>'
        total_label = "<b>%s</b>" % api.escape(_("Total:"))
        purchases_summary_label = SummaryLabel(klist=self.purchases_list,
                                               column='total',
                                               label=total_label,
                                               value_format=value_format)

        purchases_summary_label.show()
        self.purchases_vbox.pack_start(purchases_summary_label, False, True, 0)
Exemplo n.º 25
0
    def _setup_widgets(self):
        # We cant remove cash if closing till from a previous day
        self.value.set_sensitive(not self._previous_day)
        if self._previous_day:
            value = 0
        else:
            value = self.model.get_balance()
        self.value.update(value)

        self.day_history.set_columns(self._get_columns())
        if not self._blind_close:
            self.day_history.add_list(self._get_day_history())
            summary_day_history = SummaryLabel(
                klist=self.day_history,
                column='system_value',
                label='<b>%s</b>' % api.escape(_(u'Total balance:')))
            summary_day_history.show()
            self.day_history_box.pack_start(summary_day_history, False, True,
                                            0)
        else:
            self.totals_grid.hide()
            self.day_history.add_list(self.till.create_day_summary())
Exemplo n.º 26
0
    def _setup_widgets(self):
        component_list = list(self._get_products())
        if component_list:
            self.component_combo.prefill(component_list)
        else:
            self.sort_components_check.set_sensitive(False)

        self.component_tree.set_columns(self._get_columns())
        self._populate_component_tree()
        self.component_label = SummaryLabel(klist=self.component_tree,
                                            column='total_production_cost',
                                            label='<b>%s</b>' %
                                            api.escape(_(u'Total:')),
                                            value_format='<b>%s</b>')
        self.component_label.show()
        self.component_tree_vbox.pack_start(self.component_label, False)
        self.info_label.set_bold(True)
        self._update_widgets()
        if self.visual_mode:
            self.component_combo.set_sensitive(False)
            self.add_button.set_sensitive(False)
            self.sort_components_check.set_sensitive(False)
Exemplo n.º 27
0
    def _setup_widgets(self):
        self.product_list.set_columns(self._get_product_columns())
        products = self.model.get_items(with_children=False)
        # XXX Just a precaution
        self.product_list.clear()
        for product in products:
            self.product_list.append(None, product)
            for child in product.children_items:
                self.product_list.append(product, child)

        value_format = '<b>%s</b>'
        total_label = value_format % api.escape(_("Total:"))
        products_summary_label = SummaryLabel(klist=self.product_list,
                                              column='total',
                                              label=total_label,
                                              value_format=value_format)

        products_summary_label.show()
        self.products_vbox.pack_start(products_summary_label, False, True, 0)

        label = self.print_labels.get_children()[0]
        label = label.get_children()[0].get_children()[1]
        label.set_label(_(u'Print labels'))
Exemplo n.º 28
0
    def _setup_widgets(self):
        self.product_list.set_columns(self._get_product_columns())
        products = self.model.get_items(with_children=False)
        # XXX Just a precaution
        self.product_list.clear()
        for product in products:
            self.product_list.append(None, product)
            for child in product.children_items:
                self.product_list.append(product, child)

        value_format = '<b>%s</b>'
        total_label = value_format % api.escape(_("Total:"))
        products_summary_label = SummaryLabel(klist=self.product_list,
                                              column='total',
                                              label=total_label,
                                              value_format=value_format)

        products_summary_label.show()
        self.products_vbox.pack_start(products_summary_label, False)

        label = self.print_labels.get_children()[0]
        label = label.get_children()[0].get_children()[1]
        label.set_label(_(u'Print labels'))
Exemplo n.º 29
0
    def set_summary_label(self, column, label='Total:', format='%s'):
        """
        Adds a summary label to the result set
        @param column: the column to sum from
        @param label: the label to use, defaults to 'Total:'
        @param format: the format, defaults to '%%s', must include '%%s'
        """
        if not '%s' in format:
            raise ValueError("format must contain %s")

        try:
            self.results.get_column_by_name(column)
        except LookupError:
            raise ValueError("%s is not a valid column" % (column,))

        if self._summary_label:
            self._summary_label.parent.remove(self._summary_label)
        self._summary_label = SummaryLabel(klist=self.results,
                                           column=column,
                                           label=label,
                                           value_format=format)
        self.pack_end(self._summary_label, False, False)
        self.reorder_child(self._summary_label, 1)
        self._summary_label.show()
Exemplo n.º 30
0
    def _setup_widgets(self):
        component_list = list(self._get_products())
        if component_list:
            self.component_combo.prefill(component_list)
        else:
            self.sort_components_check.set_sensitive(False)

        self.component_tree.set_columns(self._get_columns())
        self._populate_component_tree()
        self.component_label = SummaryLabel(
            klist=self.component_tree,
            column='total_production_cost',
            label='<b>%s</b>' % api.escape(_(u'Total:')),
            value_format='<b>%s</b>')
        self.component_label.show()
        self.component_tree_vbox.pack_start(self.component_label, False)
        self.info_label.set_bold(True)
        self._update_widgets()
        if self.visual_mode:
            self.component_combo.set_sensitive(False)
            self.add_button.set_sensitive(False)
            self.sort_components_check.set_sensitive(False)
Exemplo n.º 31
0
class SellableItemSlave(BaseEditorSlave):
    """A slave for selecting sellable items.

    It defines the following:

      - barcode entry
      - quantity spinbutton
      - cost entry
      - add button
      - find product button
      - sellable objectlist

    Optionally buttons to modify the list

      - Add
      - Remove
      - Edit

    Subclasses should define a sellable_view property and a
    get_sellable_view_query, both used to define what sellables can be added
    to the step.

    The view used should have the following properties:

     - barcode
     - description
     - category_description

    and should also provide an acessor that returns the sellable object.

    """
    gsignal('sellable-selected', object)

    gladefile = 'SellableItemSlave'
    proxy_widgets = (
        'quantity',
        'unit_label',
        'cost',
        'minimum_quantity',
        'stock_quantity',
        'sellable_description',
    )
    summary_label_text = None
    summary_label_column = 'total'
    value_column = 'cost'
    sellable_view = ProductFullStockItemView
    sellable_editable = False
    validate_stock = False

    #: If we should also validate the price of the sellable. (checking if it is
    #: respecting the rules of discount
    validate_price = False

    # FIXME: s/cost/value/
    cost_editable = True
    item_editor = None
    batch_selection_dialog = None

    #: the sellable search class used to select a sellable to add on the list
    sellable_search = SellableSearch

    #: if we should allow to add an item without available batches (no stock).
    #: Can happen when selecting a product that control batches for decrease,
    #: in that case, :meth:`.get_order_item` will receive *batch=None*
    allow_no_batch = False

    #: the mode to pass to the
    #: :class:`stoq.lib.gui.widgets.calculator.CalculatorPopup`.
    #: If ``None``, the calculator will not be attached
    calculator_mode = None

    #: If we should make visible a label showing the stock and the minimum
    #: quantity of a sellable when one is selected. Note that sellables
    #: without storables (e.g. services) won't have them shown anyway
    stock_labels_visible = True

    def __init__(self, store, parent, model=None, visual_mode=None):
        self.parent = parent

        # The manager is someone who can allow a bigger discount for a sale item
        self.manager = None

        # This is used by add_sellable to know what item represents
        # a given sellable/batch/value so it can be removed without
        # needing to ask for the children class
        self._items_cache = {}

        super(SellableItemSlave, self).__init__(store,
                                                model=model,
                                                visual_mode=visual_mode)
        self._setup_widgets()

    #
    #  BaseEditorSlave
    #

    def setup_proxies(self):
        if self.calculator_mode is not None:
            self.calculator_popup = CalculatorPopup(self.cost,
                                                    self.calculator_mode)
        self.proxy = self.add_proxy(None, self.proxy_widgets)

    def setup_slaves(self):
        self.slave = AdditionListSlave(self.store,
                                       self.get_columns(),
                                       editor_class=self.item_editor,
                                       restore_name=self.__class__.__name__,
                                       visual_mode=self.visual_mode,
                                       tree=True)

        for item in self.get_saved_items():
            if hasattr(item, 'parent_item'):

                def add_result(result):
                    parent = result.parent_item
                    if parent:
                        add_result(parent)
                    if result not in self.slave.klist:
                        self.slave.klist.append(parent, result)
                    self.slave.klist.expand(result)

                add_result(item)
            else:
                self.slave.klist.append(None, item)

        self.slave.klist.connect('cell-edited', self._on_klist__cell_edited)
        self.slave.connect('before-delete-items',
                           self._on_list_slave__before_delete_items)
        self.slave.connect('after-delete-items',
                           self._on_list_slave__after_delete_items)
        self.slave.connect('on-edit-item', self._on_list_slave__edit_item)
        self.slave.connect('on-add-item', self._on_list_slave__add_item)
        self.attach_slave('list_holder', self.slave)

    def update_visual_mode(self):
        for widget in [self.barcode, self.product_button]:
            widget.set_sensitive(False)

    #
    # Public API
    #

    def add_sellable(self, sellable, parent=None, reset_proxy=True):
        """Add a sellable to the current step

        This will call step.get_order_item to create the correct item for the
        current model, and this created item will be returned.

        :param sellable: the |sellable| we are adding
        :param parent: the |sellable|'s parent we are adding
        :param reset_proxy: indicate if we want to clear the proxy model right away
        """
        quantity = self.get_quantity()
        value = self.cost.read()
        storable = sellable.product_storable
        order_items = []

        batch = self.proxy.model.batch
        # If a batch_number is selected, we will add that item directly. But
        # we need to adjust the batch's type since places using any
        # batch selection different from BatchDecreaseSelectionDialog will
        # be expecting the batch number
        if batch and not issubclass(self.batch_selection_dialog,
                                    BatchDecreaseSelectionDialog):
            batch = batch.batch_number

        if (storable is not None and storable.is_batch and batch is None
                and self.batch_selection_dialog is not None):
            order_items.extend(
                self.get_batch_order_items(sellable, value, quantity))
        else:
            order_item = self.get_order_item(sellable,
                                             value,
                                             quantity,
                                             batch=batch,
                                             parent=parent)
            if order_item is not None:
                order_items.append(order_item)

        for item in order_items:
            if item in self.slave.klist:
                self.slave.klist.update(item)
            else:
                self.slave.klist.append(parent, item)

            product = item.sellable.product
            if product and product.is_package and parent is None:
                for child in self.proxy.model.children:
                    self.add_sellable(child, parent=item)
                    self.slave.klist.expand(item)

        self.update_total()

        if reset_proxy and len(order_items):
            self._reset_sellable()

        # After an item is added, reset manager to None so the discount is only
        # authorized for one item at a time.
        self.manager = None

    def remove_items(self, items):
        """Remove items from the current :class:`IContainer`.

        Subclasses can override this if special logic is necessary.
        """
        for item in items:
            # We need to remove the children before remove the parent_item
            self.remove_items(getattr(item, 'children_items', []))
            if isinstance(item, (SaleItem, WorkOrderItem)):
                # SaleItem and WorkOrderItem may change the stock items. And stock transactions
                # require the logged user
                self.model.remove_item(item, api.get_current_user(item.store))
            else:
                self.model.remove_item(item)

    def hide_item_addition_toolbar(self):
        self.item_table.hide()

    def hide_add_button(self):
        """Hides the add button
        """
        self.slave.hide_add_button()

    def hide_del_button(self):
        """Hides the del button
        """
        self.slave.hide_del_button()

    def hide_edit_button(self):
        """Hides the edit button
        """
        self.slave.hide_edit_button()

    def get_quantity(self):
        """Returns the quantity of the current model or 1 if there is no model
        :returns: the quantity
        """
        return self.proxy.model and self.proxy.model.quantity or Decimal(1)

    def get_model_item_by_sellable(self, sellable):
        """Returns a model instance by the given sellable.
        :returns: a model instance or None if we could not find the model.
        """
        for item in self.slave.klist:
            if item.sellable == sellable:
                return item

    def get_remaining_quantity(self, sellable, batch=None):
        """Returns the remaining quantity in stock for the given *sellable*

        This will check the remaining quantity in stock taking the
        items on the list in consideration. This is very useful since
        these items still haven't decreased stock.

        :param sellable: the |sellable| to be checked for remaining
            quantity
        :param batch: if not ``None``, the remaining quantity will
            be checked taking the |batch| in consideration
        :return: the remaining quantity or ``None`` if the sellable
            doesn't control stock (e.g. a service)
        """
        if sellable.service or sellable.product_storable is None:
            return None

        total_quatity = sum(i.quantity for i in self.slave.klist
                            if (i.sellable, i.batch) == (sellable, batch))

        branch = self.model.branch
        storable = sellable.product_storable
        # FIXME: It would be better to just use storable.get_balance_for_branch
        # and pass batch=batch there. That would avoid this if
        if batch is not None:
            balance = batch.get_balance_for_branch(branch)
        else:
            balance = storable.get_balance_for_branch(branch)

        return balance - total_quatity

    def update_total(self):
        """Update the summary label with the current total"""
        if self.summary:
            self.summary.update_total()
        self.force_validation()

    def get_parent(self):
        return self.get_toplevel().get_toplevel()

    def validate(self, value):
        self.add_sellable_button.set_sensitive(
            value and bool(self.proxy.model)
            and bool(self.proxy.model.sellable))

    #
    # Hooks
    #

    def get_sellable_view_query(self):
        """This method should return a tuple containing the viewable that should
        be used and a query that should filter the sellables that can and cannot
        be added to this step.
        """
        return (self.sellable_view,
                Sellable.get_unblocked_sellables_query(self.store))

    def get_order_item(self,
                       sellable,
                       value,
                       quantity,
                       batch=None,
                       parent=None):
        """Adds the sellable to the current model

        This method is called when the user added the sellable in the wizard
        step. Subclasses should implement this method to add the sellable to the
        current model.

        :param sellable: the selected |sellable|
        :param value: the value selected for the sellable
        :param quantity: the quantity selected for the sellable
        :param batch: the batch that was selected for the sellable.
            Note that this argument will only be passed if
            :attr:`.batch_selection_dialog` is defined.
        """
        raise NotImplementedError('This method must be defined on child')

    def get_saved_items(self):
        raise NotImplementedError('This method must be defined on child')

    def get_columns(self):
        raise NotImplementedError('This method must be defined on child')

    def can_add_sellable(self, sellable):
        """Whether we can add a sellable to the list or not

        This is a hook method that gets called when trying to add a
        sellable to the list. It can be rewritten on child classes for
        extra functionality
        :param sellable: the selected sellable
        :returns: True or False (True by default)
        """
        return True

    def get_sellable_model(self, sellable, batch=None):
        """Create a Settable containing multiple information to be used on the
        slave.

        :param sellable: a |sellable| we are adding to wizard
        :returns: a Settable containing some information of the item
        """
        minimum = Decimal(0)
        stock = Decimal(0)
        cost = currency(0)
        quantity = Decimal(0)
        description = u''
        unit_label = u''

        children = {}
        if sellable:
            description = "<b>%s</b>" % api.escape(sellable.get_description())
            cost = getattr(sellable, self.value_column)
            quantity = Decimal(1)
            storable = sellable.product_storable
            unit_label = sellable.unit_description
            if storable:
                minimum = storable.minimum_quantity
                stock = storable.get_balance_for_branch(self.model.branch)

            product = sellable.product
            if product:
                for component in product.get_components():
                    child_sellable = component.component.sellable
                    children[child_sellable] = self.get_sellable_model(
                        child_sellable)

        return Settable(quantity=quantity,
                        cost=cost,
                        sellable=sellable,
                        minimum_quantity=minimum,
                        stock_quantity=stock,
                        sellable_description=description,
                        unit_label=unit_label,
                        batch=batch,
                        children=children)

    def sellable_selected(self, sellable, batch=None):
        """This will be called when a sellable is selected in the combo.
        It can be overriden in a subclass if they wish to do additional
        logic at that point

        :param sellable: the selected |sellable|
        :param batch: the |batch|, if the |sellable| was selected
            by it's batch_number
        """
        has_storable = False

        self.proxy.set_model(self.get_sellable_model(sellable, batch=batch))

        has_sellable = bool(sellable)
        self.add_sellable_button.set_sensitive(has_sellable)
        self.force_validation()
        self.quantity.set_sensitive(has_sellable)
        self.cost.set_sensitive(has_sellable and self.cost_editable)
        self._update_product_labels_visibility(has_storable)

        unit = sellable and sellable.unit
        self.quantity.set_digits(
            QUANTITY_PRECISION if unit and unit.allow_fraction else 0)

        self.emit('sellable-selected', sellable)

    def get_batch_items(self):
        """Get batch items for sellables inside this slave

        :returns: a dict mapping the batch to it's quantity
        """
        batch_items = collections.OrderedDict()
        for item in self.slave.klist:
            if item.batch is None:
                continue
            batch_items.setdefault(item.batch, 0)
            # Sum all quantities of the same batch
            batch_items[item.batch] += item.quantity

        return batch_items

    def get_batch_order_items(self, sellable, value, quantity):
        """Get order items for sellable considering it's |batches|

        By default, this will run :obj:`.batch_selection_dialog` to get
        the batches and their quantities and then call :meth:`.get_order_item`
        on each one.

        :param sellable: a |sellable|
        :param value: the value (e.g. price, cost) of the sellable
        :param quantity: the quantity of the sellable
        """
        order_items = []
        storable = sellable.product_storable
        original_batch_items = self.get_batch_items()
        if issubclass(self.batch_selection_dialog,
                      BatchDecreaseSelectionDialog):
            extra_kw = dict(decreased_batches=original_batch_items)
            available_batches = list(
                storable.get_available_batches(self.model.branch))
            # If there're no available batches (no stock) and we are allowing
            # no batches, add the item without the batch.
            if len(available_batches) == 0 and self.allow_no_batch:
                return [
                    self.get_order_item(sellable, value, quantity=quantity)
                ]
            # The trivial case, where there's just one batch, and since this
            # is a decrease, we can select it directly
            if len(available_batches) == 1:
                batch = available_batches[0]
                return [
                    self.get_order_item(sellable,
                                        value,
                                        quantity=quantity,
                                        batch=batch)
                ]
        else:
            extra_kw = dict(original_batches=original_batch_items)

        retval = run_dialog(self.batch_selection_dialog,
                            self.get_parent(),
                            store=self.store,
                            model=storable,
                            quantity=quantity,
                            **extra_kw)
        retval = retval or {}

        for batch, b_quantity in retval.items():
            order_item = self.get_order_item(sellable,
                                             value,
                                             quantity=b_quantity,
                                             batch=batch)
            if order_item is None:
                continue
            order_items.append(order_item)

        return order_items

    def get_extra_discount(self, sellable):
        """Called to get an extra discount for the sellable being added

        Subclasses can implement this to allow some extra discount for the
        sellable being added. For example, one can implement this to
        allow some extra discount based on the unused discount on the
        already added items

        Note that, if you need to get the manager to check for max discount,
        you can use :obj:`.manager`

        :param sellable: the sellable being added
        :returns: the extra discount for the sellable being added,
            or ``None`` if not extra discount should be allowed
        """
        return None

    def get_sellable_search_extra_kwargs(self):
        """Called to get extra args for :attr:`.sellable_search`

        A subclass can override this and return a dict with extra keywords
        to pass to the sellable search defined on the class.

        :returns: a ``dict`` of extra keywords
        """
        return {}

    def try_get_sellable(self, grab_focus=True):
        """Try to get the sellable based on the barcode typed
        This will try to get the sellable using the barcode the user entered.

           If one is not found, than an advanced search will be displayed for
        the user, and the string he typed in the barcode entry will be
        used to filter the results.

        :param grab_focus: indicate if the focus should go to the quantity
                           widget
        """
        sellable, batch = self._get_sellable_and_batch()
        if not sellable:
            search_str = self.barcode.get_text()
            self.run_advanced_search(search_str)
            return

        self.sellable_selected(sellable, batch=batch)

        if grab_focus:
            self.quantity.grab_focus()
        self.quantity.set_sensitive(grab_focus)
        self.add_sellable_button.set_sensitive(grab_focus)

    #
    #  Private
    #
    def _setup_widgets(self):
        self._update_product_labels_visibility(False)
        cost_digits = sysparam.get_int('COST_PRECISION_DIGITS')
        self.quantity.set_sensitive(False)
        # 10 for the length of MAX_INT, 3 for the precision and 1 for comma
        self.quantity.set_max_length(14)
        self.cost.set_sensitive(False)
        # 10 for the length of MAX_INT and 1 for comma
        self.cost.set_max_length(10 + cost_digits + 1)
        self.add_sellable_button.set_sensitive(False)
        self.overlay.set_overlay_pass_through(self.box, True)
        self.unit_label.set_bold(True)

        for widget in [self.quantity, self.cost]:
            widget.set_adjustment(
                Gtk.Adjustment(lower=0, upper=MAX_INT, step_increment=1))

        self._reset_sellable()
        self._setup_summary()
        self.cost.set_digits(cost_digits)
        self.quantity.set_digits(3)

        self.barcode.grab_focus()
        self.item_table.set_focus_chain([
            self.barcode, self.quantity, self.cost, self.add_sellable_button,
            self.product_button
        ])
        self.register_validate_function(self.validate)

    def _setup_summary(self):
        # FIXME: Move this into AdditionListSlave
        if not self.summary_label_column:
            self.summary = None
            return
        self.summary = SummaryLabel(klist=self.slave.klist,
                                    column=self.summary_label_column,
                                    label=self.summary_label_text,
                                    value_format='<b>%s</b>')
        self.summary.show()
        self.slave.list_vbox.pack_start(self.summary, False, True, 0)

    def run_advanced_search(self, search_str=None):
        table, query = self.get_sellable_view_query()
        ret = run_dialog(self.sellable_search,
                         self.get_parent(),
                         self.store,
                         search_spec=table,
                         search_query=query,
                         search_str=search_str,
                         hide_toolbar=not self.sellable_editable,
                         **self.get_sellable_search_extra_kwargs())
        if not ret:
            return

        # We receive different items depend on if we
        # - selected an item in the search
        # - created a new item and it closed the dialog for us
        if not isinstance(
                ret,
            (Product, ProductFullStockItemView, ProductComponentView,
             SellableFullStockView, ServiceView, ProductFullStockView)):
            raise AssertionError(ret)

        sellable = ret.sellable
        if not self.can_add_sellable(sellable):
            return
        if sellable.barcode:
            self.barcode.set_text(sellable.barcode)
        self.sellable_selected(sellable)
        self.quantity.grab_focus()

    def _find_sellable_and_batch(self, text):
        """Find a sellable given a code, barcode or batch_number

        When searching using the code attribute of the sellable, the search will
        be case insensitive.

        :param text: the code, barcode or batch_number
        :returns: The sellable that matches the given barcode or code or
          ``None`` if nothing was found.
        """
        viewable, default_query = self.get_sellable_view_query()

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

        # First try barcode, then code since there might be a product
        # with a code equal to another product's barcode
        for attr in [viewable.barcode, viewable.code]:
            query = Lower(attr) == text.lower()
            if default_query:
                query = And(query, default_query)

            result = self.store.find(viewable, query).one()
            if result:
                return result.sellable, None

        # if none of the above worked, try to find by batch number
        query = Lower(StorableBatch.batch_number) == text.lower()
        batch = self.store.find(StorableBatch, query).one()
        if batch:
            sellable = batch.storable.product.sellable
            query = viewable.id == sellable.id
            if default_query:
                query = And(query, default_query)
            # Make sure batch's sellable is in the view
            if not self.store.find(viewable, query).is_empty():
                return sellable, batch

        return None, None

    def _get_sellable_and_batch(self):
        """This method always read the barcode and searches de database.

        If you only need the current selected sellable, use
        self.proxy.model.sellable
        """
        barcode = self.barcode.get_text()
        if not barcode:
            return None, None

        sellable, batch = self._find_sellable_and_batch(barcode)

        if not sellable:
            return None, None
        elif not self.can_add_sellable(sellable):
            return None, None

        return sellable, batch

    def _add_sellable(self, reset_proxy=True):
        sellable = self.proxy.model.sellable
        assert sellable

        sellable = self.store.fetch(sellable)

        self.add_sellable(sellable, reset_proxy=reset_proxy)
        self.barcode.grab_focus()

    def _reset_sellable(self):
        self.proxy.set_model(None)
        self.sellable_selected(None)

    def _update_product_labels_visibility(self, visible):
        for widget in [
                self.minimum_quantity_lbl, self.minimum_quantity,
                self.stock_quantity, self.stock_quantity_lbl
        ]:
            widget.set_visible(self.stock_labels_visible and visible)

    #
    #  Callbacks
    #

    def _on_klist__cell_edited(self, klist, obj, attr):
        self.update_total()

    def _on_list_slave__before_delete_items(self, slave, items):
        self.remove_items(items)
        self.force_validation()

    def _on_list_slave__after_delete_items(self, slave):
        self.update_total()

    def _on_list_slave__add_item(self, slave, item):
        self.update_total()

    def _on_list_slave__edit_item(self, slave, item):
        self.update_total()

    def on_add_sellable_button__clicked(self, button):
        self._add_sellable()

    def on_product_button__clicked(self, button):
        self.try_get_sellable()

    def on_barcode__activate(self, widget):
        self.try_get_sellable()

    def on_quantity__activate(self, entry):
        if self.add_sellable_button.get_sensitive():
            self._add_sellable()

    def on_cost__activate(self, entry):
        if self.add_sellable_button.get_sensitive():
            self._add_sellable()

    def on_quantity__validate(self, entry, value):
        if not self.proxy.model.sellable:
            return

        # Only support positive quantities
        if value <= 0:
            return ValidationError(_(u'The quantity must be positive'))

        # Dont allow numbers bigger than MAX_INT (see stoqlib.lib.defaults)
        if value > MAX_INT:
            return ValidationError(
                _(u'The quantity cannot be bigger than %s') % MAX_INT)

        sellable = self.proxy.model.sellable
        if sellable and not sellable.is_valid_quantity(value):
            return ValidationError(
                _(u"This product unit (%s) does not "
                  u"support fractions.") % sellable.unit_description)

        storable = sellable.product_storable
        if not self.validate_stock or not storable:
            return
        remaining_quantity = self.get_remaining_quantity(sellable)
        if remaining_quantity is None:
            return
        if value > remaining_quantity:
            return ValidationError(
                _("This quantity is not available in stock"))

    def on_cost__validate(self, widget, value):
        sellable = self.proxy.model.sellable
        if not sellable:
            return

        # Dont allow numbers bigger than MAX_INT (see stoqlib.lib.defaults)
        if value > MAX_INT:
            return ValidationError(
                _('Price cannot be bigger than %s') % MAX_INT)

        if value <= 0:
            return ValidationError(_(u'Cost must be greater than zero.'))

        if self.validate_price:
            category = getattr(self.model, 'client_category', None)
            default_price = sellable.get_price_for_category(category)
            if (not sysparam.get_bool('ALLOW_HIGHER_SALE_PRICE')
                    and value > default_price):
                return ValidationError(
                    _(u'The sell price cannot be greater '
                      'than %s.') % default_price)

            manager = self.manager or api.get_current_user(self.store)
            client = getattr(self.model, 'client', None)
            category = client and client.category
            extra_discount = self.get_extra_discount(sellable)
            valid_data = sellable.is_valid_price(value,
                                                 category,
                                                 manager,
                                                 extra_discount=extra_discount)

            if not valid_data['is_valid']:
                return ValidationError(
                    (_(u'Max discount for this product is %.2f%%.') %
                     valid_data['max_discount']))

    def on_cost__icon_press(self, entry, icon_pos, event):
        if icon_pos != Gtk.EntryIconPosition.PRIMARY:
            return

        # No need to check credentials if it is not a price
        if not self.validate_price:
            return

        # Ask for the credentials of a different user that can possibly allow a
        # bigger discount.
        self.manager = run_dialog(CredentialsDialog, self.parent, self.store)
        if self.manager:
            self.cost.validate(force=True)
Exemplo n.º 32
0
    def _setup_summary_labels(self):
        value_format = "<b>%s</b>"
        total_label = "<b>%s</b>" % api.escape(_("Total:"))
        total_summary_label = SummaryLabel(klist=self.payments_list,
                                           column='value',
                                           label=total_label,
                                           value_format=value_format)
        total_summary_label.show()
        self.payments_vbox.pack_start(total_summary_label, False)

        total_label = "<b>%s</b>" % api.escape(_("Total paid:"))
        total_paid_summary_label = SummaryLabel(klist=self.payments_list,
                                                column='paid_value',
                                                label=total_label,
                                                value_format=value_format)
        total_paid_summary_label.show()
        self.payments_vbox.pack_start(total_paid_summary_label, False)

        total_label = "<b>%s</b>" % api.escape(_("Total:"))
        transaction_summary_label = SummaryLabel(
            klist=self.stock_transactions_list,
            column='total',
            label=total_label,
            value_format=value_format)
        transaction_summary_label.show()
        self.stock_transactions_vbox.pack_start(transaction_summary_label, False)

        total_label = "<b>%s</b>" % api.escape(_("Total:"))
        sale_summary_label = SummaryLabel(klist=self.sales_list,
                                          column='total',
                                          label=total_label,
                                          value_format=value_format)
        sale_summary_label.show()
        self.sales_vbox.pack_start(sale_summary_label, False)
Exemplo n.º 33
0
    def _setup_widgets(self):
        if self._is_batch:
            self._add_batches_tab()

        self.receiving_list.set_columns(self._get_receiving_columns())
        self.sales_list.set_columns(self._get_sale_columns())
        self.transfer_list.set_columns(self._get_transfer_columns())
        self.loan_list.set_columns(self._get_loan_columns())
        self.decrease_list.set_columns(self._get_decrease_columns())
        self.inventory_list.set_columns(self._get_inventory_columns())
        self.returned_list.set_columns(self._get_returned_columns())

        current_branch = api.get_current_branch(self.store)
        items = self.store.find(ReceivingItemView, sellable_id=self.model.id)
        if api.sysparam.get_bool('SYNCHRONIZED_MODE'):
            items = items.find(Branch.id == current_branch.id)
        self.receiving_list.add_list(list(items))

        items = SaleItemsView.find_confirmed(self.store, sellable=self.model)
        if api.sysparam.get_bool('SYNCHRONIZED_MODE'):
            items = items.find(Branch.id == current_branch.id)
        self.sales_list.add_list(list(items))

        items = TransferItemView.find_by_branch(self.store, self.model,
                                                current_branch)
        self.transfer_list.add_list(list(items))

        items = self.store.find(LoanItemView, sellable_id=self.model.id)
        if api.sysparam.get_bool('SYNCHRONIZED_MODE'):
            items = items.find(Branch.id == current_branch.id)
        self.loan_list.add_list(list(items))

        items = self.store.find(StockDecreaseItemsView, sellable=self.model.id)
        if api.sysparam.get_bool('SYNCHRONIZED_MODE'):
            items = items.find(Branch.id == current_branch.id)
        self.decrease_list.add_list(list(items))

        items = InventoryItemsView.find_by_product(self.store,
                                                   self.model.product)
        if api.sysparam.get_bool('SYNCHRONIZED_MODE'):
            items = items.find(Branch.id == current_branch.id)
        self.inventory_list.add_list(items)

        items = self.store.find(ReturnedSaleItemsView,
                                sellable_id=self.model.id)
        if api.sysparam.get_bool('SYNCHRONIZED_MODE'):
            items = items.find(Branch.id == current_branch.id)
        self.returned_list.add_list(items)

        value_format = '<b>%s</b>'
        total_label = "<b>%s</b>" % api.escape(_("Total:"))
        receiving_summary_label = SummaryLabel(klist=self.receiving_list,
                                               column='quantity',
                                               label=total_label,
                                               value_format=value_format)
        receiving_summary_label.show()
        self.receiving_vbox.pack_start(receiving_summary_label, False)

        sales_summary_label = SummaryLabel(klist=self.sales_list,
                                           column='quantity',
                                           label=total_label,
                                           value_format=value_format)
        sales_summary_label.show()
        self.sales_vbox.pack_start(sales_summary_label, False)

        transfer_summary_label = SummaryLabel(klist=self.transfer_list,
                                              column='item_quantity',
                                              label=total_label,
                                              value_format=value_format)
        transfer_summary_label.show()
        self.transfer_vbox.pack_start(transfer_summary_label, False)

        loan_summary_label = SummaryLabel(klist=self.loan_list,
                                          column='quantity',
                                          label=total_label,
                                          value_format=value_format)
        self.loan_vbox.pack_start(loan_summary_label, False)

        decrease_summary_label = SummaryLabel(klist=self.decrease_list,
                                              column='quantity',
                                              label=total_label,
                                              value_format=value_format)
        decrease_summary_label.show()
        self.decrease_vbox.pack_start(decrease_summary_label, False)
Exemplo n.º 34
0
    def _setup_widgets(self):
        if self._is_batch:
            self._add_batches_tab()

        self.receiving_list.set_columns(self._get_receiving_columns())
        self.sales_list.set_columns(self._get_sale_columns())
        self.transfer_list.set_columns(self._get_transfer_columns())
        self.loan_list.set_columns(self._get_loan_columns())
        self.decrease_list.set_columns(self._get_decrease_columns())
        self.inventory_list.set_columns(self._get_inventory_columns())
        self.returned_list.set_columns(self._get_returned_columns())

        current_branch = api.get_current_branch(self.store)
        items = self.store.find(ReceivingItemView, sellable_id=self.model.id)
        if api.sysparam.get_bool('SYNCHRONIZED_MODE'):
            items = items.find(Branch.id == current_branch.id)
        self.receiving_list.add_list(list(items))

        items = SaleItemsView.find_confirmed(self.store,
                                             sellable=self.model)
        if api.sysparam.get_bool('SYNCHRONIZED_MODE'):
            items = items.find(Branch.id == current_branch.id)
        self.sales_list.add_list(list(items))

        items = self.store.find(
            TransferOrderItem,
            And(Ne(TransferOrderItem.transfer_order_id, None),
                TransferOrderItem.sellable_id == self.model.id))
        self.transfer_list.add_list(list(items))

        items = self.store.find(LoanItemView, sellable_id=self.model.id)
        if api.sysparam.get_bool('SYNCHRONIZED_MODE'):
            items = items.find(Branch.id == current_branch.id)
        self.loan_list.add_list(list(items))

        items = self.store.find(StockDecreaseItemsView, sellable=self.model.id)
        if api.sysparam.get_bool('SYNCHRONIZED_MODE'):
            items = items.find(Branch.id == current_branch.id)
        self.decrease_list.add_list(list(items))

        items = InventoryItemsView.find_by_product(self.store, self.model.product)
        if api.sysparam.get_bool('SYNCHRONIZED_MODE'):
            items = items.find(Branch.id == current_branch.id)
        self.inventory_list.add_list(items)

        items = self.store.find(ReturnedSaleItemsView, sellable_id=self.model.id)
        if api.sysparam.get_bool('SYNCHRONIZED_MODE'):
            items = items.find(Branch.id == current_branch.id)
        self.returned_list.add_list(items)

        value_format = '<b>%s</b>'
        total_label = "<b>%s</b>" % api.escape(_("Total:"))
        receiving_summary_label = SummaryLabel(klist=self.receiving_list,
                                               column='quantity',
                                               label=total_label,
                                               value_format=value_format)
        receiving_summary_label.show()
        self.receiving_vbox.pack_start(receiving_summary_label, False)

        sales_summary_label = SummaryLabel(klist=self.sales_list,
                                           column='quantity',
                                           label=total_label,
                                           value_format=value_format)
        sales_summary_label.show()
        self.sales_vbox.pack_start(sales_summary_label, False)

        transfer_summary_label = SummaryLabel(klist=self.transfer_list,
                                              column='quantity',
                                              label=total_label,
                                              value_format=value_format)
        transfer_summary_label.show()
        self.transfer_vbox.pack_start(transfer_summary_label, False)

        loan_summary_label = SummaryLabel(klist=self.loan_list,
                                          column='quantity',
                                          label=total_label,
                                          value_format=value_format)
        self.loan_vbox.pack_start(loan_summary_label, False)

        decrease_summary_label = SummaryLabel(klist=self.decrease_list,
                                              column='quantity',
                                              label=total_label,
                                              value_format=value_format)
        decrease_summary_label.show()
        self.decrease_vbox.pack_start(decrease_summary_label, False)
Exemplo n.º 35
0
    def _setup_summary_labels(self):
        value_format = "<b>%s</b>"
        total_label = "<b>%s</b>" % api.escape(_("Total:"))
        total_summary_label = SummaryLabel(klist=self.payments_list,
                                           column='value',
                                           label=total_label,
                                           value_format=value_format)
        total_summary_label.show()
        self.payments_vbox.pack_start(total_summary_label, False, True, 0)

        total_label = "<b>%s</b>" % api.escape(_("Total paid:"))
        total_paid_summary_label = SummaryLabel(klist=self.payments_list,
                                                column='paid_value',
                                                label=total_label,
                                                value_format=value_format)
        total_paid_summary_label.show()
        self.payments_vbox.pack_start(total_paid_summary_label, False, True, 0)

        total_label = "<b>%s</b>" % api.escape(_("Total:"))
        transaction_summary_label = SummaryLabel(
            klist=self.stock_transactions_list,
            column='total',
            label=total_label,
            value_format=value_format)
        transaction_summary_label.show()
        self.stock_transactions_vbox.pack_start(transaction_summary_label,
                                                False, True, 0)

        total_label = "<b>%s</b>" % api.escape(_("Total:"))
        sale_summary_label = SummaryLabel(klist=self.sales_list,
                                          column='total',
                                          label=total_label,
                                          value_format=value_format)
        sale_summary_label.show()
        self.sales_vbox.pack_start(sale_summary_label, False, True, 0)
Exemplo n.º 36
0
class SellableItemSlave(BaseEditorSlave):
    """A slave for selecting sellable items.

    It defines the following:

      - barcode entry
      - quantity spinbutton
      - cost entry
      - add button
      - find product button
      - sellable objectlist

    Optionally buttons to modify the list

      - Add
      - Remove
      - Edit

    Subclasses should define a sellable_view property and a
    get_sellable_view_query, both used to define what sellables can be added
    to the step.

    The view used should have the following properties:

     - barcode
     - description
     - category_description

    and should also provide an acessor that returns the sellable object.

    """

    gsignal("sellable-selected", object)

    gladefile = "SellableItemSlave"
    proxy_widgets = ("quantity", "unit_label", "cost", "minimum_quantity", "stock_quantity", "sellable_description")
    summary_label_text = None
    summary_label_column = "total"
    value_column = "cost"
    sellable_view = ProductFullStockItemView
    sellable_editable = False
    validate_stock = False

    #: If we should also validate the price of the sellable. (checking if it is
    #: respecting the rules of discount
    validate_price = False

    # FIXME: s/cost/value/
    cost_editable = True
    item_editor = None
    batch_selection_dialog = None

    #: the sellable search class used to select a sellable to add on the list
    sellable_search = SellableSearch

    #: if we should allow to add an item without available batches (no stock).
    #: Can happen when selecting a product that control batches for decrease,
    #: in that case, :meth:`.get_order_item` will receive *batch=None*
    allow_no_batch = False

    #: the mode to pass to the
    #: :class:`stoqlib.gui.widgets.calculator.CalculatorPopup`.
    #: If ``None``, the calculator will not be attached
    calculator_mode = None

    #: If we should add the sellable on the list when activating the barcode.
    #: This is useful when the barcode is supposed to work with barcode
    #: readers. Note that, if the sellable with the given barcode wasn't found,
    #: it'll just be cleared and no error message will be displayed
    add_sellable_on_barcode_activate = False

    #: If we should make visible a label showing the stock and the minimum
    #: quantity of a sellable when one is selected. Note that sellables
    #: without storables (e.g. services) won't have them shown anyway
    stock_labels_visible = True

    def __init__(self, store, parent, model=None, visual_mode=None):
        self.parent = parent

        # The manager is someone who can allow a bigger discount for a sale item
        self.manager = None

        # This is used by add_sellable to know what item represents
        # a given sellable/batch/value so it can be removed without
        # needing to ask for the children class
        self._items_cache = {}

        super(SellableItemSlave, self).__init__(store, model=model, visual_mode=visual_mode)
        self._setup_widgets()

    #
    #  BaseEditorSlave
    #

    def setup_proxies(self):
        if self.calculator_mode is not None:
            self.calculator_popup = CalculatorPopup(self.cost, self.calculator_mode)
        self.proxy = self.add_proxy(None, self.proxy_widgets)

    def setup_slaves(self):
        self.slave = AdditionListSlave(
            self.store,
            self.get_columns(),
            editor_class=self.item_editor,
            klist_objects=self.get_saved_items(),
            restore_name=self.__class__.__name__,
            visual_mode=self.visual_mode,
        )
        self.slave.klist.connect("cell-edited", self._on_klist__cell_edited)
        self.slave.connect("before-delete-items", self._on_list_slave__before_delete_items)
        self.slave.connect("after-delete-items", self._on_list_slave__after_delete_items)
        self.slave.connect("on-edit-item", self._on_list_slave__edit_item)
        self.slave.connect("on-add-item", self._on_list_slave__add_item)
        self.attach_slave("list_holder", self.slave)

    def update_visual_mode(self):
        for widget in [self.barcode, self.product_button]:
            widget.set_sensitive(False)

    #
    # Public API
    #

    def add_sellable(self, sellable):
        """Add a sellable to the current step

        This will call step.get_order_item to create the correct item for the
        current model, and this created item will be returned.
        """
        quantity = self.get_quantity()
        value = self.cost.read()
        storable = sellable.product_storable
        order_items = []

        batch = self.proxy.model.batch
        # If a batch_number is selected, we will add that item directly. But
        # we need to adjust the batch's type since places using any
        # batch selection different from BatchDecreaseSelectionDialog will
        # be expecting the batch number
        if batch and not issubclass(self.batch_selection_dialog, BatchDecreaseSelectionDialog):
            batch = batch.batch_number

        if storable is not None and storable.is_batch and batch is None and self.batch_selection_dialog is not None:
            order_items.extend(self.get_batch_order_items(sellable, value, quantity))
        else:
            order_item = self.get_order_item(sellable, value, quantity, batch=batch)
            if order_item is not None:
                order_items.append(order_item)

        for item in order_items:
            if item in self.slave.klist:
                self.slave.klist.update(item)
            else:
                self.slave.klist.append(item)

        self.update_total()

        if len(order_items):
            self._reset_sellable()

        # After an item is added, reset manager to None so the discount is only
        # authorized for one item at a time.
        self.manager = None

    def remove_items(self, items):
        """Remove items from the current :class:`IContainer`.

        Subclasses can override this if special logic is necessary.
        """
        for item in items:
            self.model.remove_item(item)

    def hide_item_addition_toolbar(self):
        self.item_table.hide()

    def hide_add_button(self):
        """Hides the add button
        """
        self.slave.hide_add_button()

    def hide_del_button(self):
        """Hides the del button
        """
        self.slave.hide_del_button()

    def hide_edit_button(self):
        """Hides the edit button
        """
        self.slave.hide_edit_button()

    def get_quantity(self):
        """Returns the quantity of the current model or 1 if there is no model
        :returns: the quantity
        """
        return self.proxy.model and self.proxy.model.quantity or Decimal(1)

    def get_model_item_by_sellable(self, sellable):
        """Returns a model instance by the given sellable.
        :returns: a model instance or None if we could not find the model.
        """
        for item in self.slave.klist:
            if item.sellable == sellable:
                return item

    def get_remaining_quantity(self, sellable, batch=None):
        """Returns the remaining quantity in stock for the given *sellable*

        This will check the remaining quantity in stock taking the
        items on the list in consideration. This is very useful since
        these items still haven't decreased stock.

        :param sellable: the |sellable| to be checked for remaining
            quantity
        :param batch: if not ``None``, the remaining quantity will
            be checked taking the |batch| in consideration
        :return: the remaining quantity or ``None`` if the sellable
            doesn't control stock (e.g. a service)
        """
        if sellable.service or sellable.product_storable is None:
            return None

        total_quatity = sum(i.quantity for i in self.slave.klist if (i.sellable, i.batch) == (sellable, batch))

        branch = self.model.branch
        storable = sellable.product_storable
        # FIXME: It would be better to just use storable.get_balance_for_branch
        # and pass batch=batch there. That would avoid this if
        if batch is not None:
            balance = batch.get_balance_for_branch(branch)
        else:
            balance = storable.get_balance_for_branch(branch)

        return balance - total_quatity

    def update_total(self):
        """Update the summary label with the current total"""
        if self.summary:
            self.summary.update_total()
        self.force_validation()

    def get_parent(self):
        return self.get_toplevel().get_toplevel()

    def validate(self, value):
        self.add_sellable_button.set_sensitive(value and bool(self.proxy.model) and bool(self.proxy.model.sellable))

    #
    # Hooks
    #

    def get_sellable_view_query(self):
        """This method should return a tuple containing the viewable that should
        be used and a query that should filter the sellables that can and cannot
        be added to this step.
        """
        return (self.sellable_view, Sellable.get_unblocked_sellables_query(self.store))

    def get_order_item(self, sellable, value, quantity, batch=None):
        """Adds the sellable to the current model

        This method is called when the user added the sellable in the wizard
        step. Subclasses should implement this method to add the sellable to the
        current model.

        :param sellable: the selected |sellable|
        :param value: the value selected for the sellable
        :param quantity: the quantity selected for the sellable
        :param batch: the batch that was selected for the sellable.
            Note that this argument will only be passed if
            :attr:`.batch_selection_dialog` is defined.
        """
        raise NotImplementedError("This method must be defined on child")

    def get_saved_items(self):
        raise NotImplementedError("This method must be defined on child")

    def get_columns(self):
        raise NotImplementedError("This method must be defined on child")

    def can_add_sellable(self, sellable):
        """Whether we can add a sellable to the list or not

        This is a hook method that gets called when trying to add a
        sellable to the list. It can be rewritten on child classes for
        extra functionality
        :param sellable: the selected sellable
        :returns: True or False (True by default)
        """
        return True

    def sellable_selected(self, sellable, batch=None):
        """This will be called when a sellable is selected in the combo.
        It can be overriden in a subclass if they wish to do additional
        logic at that point

        :param sellable: the selected |sellable|
        :param batch: the |batch|, if the |sellable| was selected
            by it's batch_number
        """
        has_storable = False
        minimum = Decimal(0)
        stock = Decimal(0)
        cost = currency(0)
        quantity = Decimal(0)
        description = u""
        unit_label = u""

        if sellable:
            description = "<b>%s</b>" % api.escape(sellable.get_description())
            cost = getattr(sellable, self.value_column)
            quantity = Decimal(1)
            storable = sellable.product_storable
            unit_label = sellable.unit_description
            if storable:
                has_storable = True
                minimum = storable.minimum_quantity
                stock = storable.get_balance_for_branch(self.model.branch)

        model = Settable(
            quantity=quantity,
            cost=cost,
            sellable=sellable,
            minimum_quantity=minimum,
            stock_quantity=stock,
            sellable_description=description,
            unit_label=unit_label,
            batch=batch,
        )

        self.proxy.set_model(model)

        has_sellable = bool(sellable)
        self.add_sellable_button.set_sensitive(has_sellable)
        self.force_validation()
        self.quantity.set_sensitive(has_sellable)
        self.cost.set_sensitive(has_sellable and self.cost_editable)
        self._update_product_labels_visibility(has_storable)
        self.emit("sellable-selected", sellable)

    def get_batch_items(self):
        """Get batch items for sellables inside this slave

        :returns: a dict mapping the batch to it's quantity
        """
        batch_items = collections.OrderedDict()
        for item in self.slave.klist:
            if item.batch is None:
                continue
            batch_items.setdefault(item.batch, 0)
            # Sum all quantities of the same batch
            batch_items[item.batch] += item.quantity

        return batch_items

    def get_batch_order_items(self, sellable, value, quantity):
        """Get order items for sellable considering it's |batches|

        By default, this will run :obj:`.batch_selection_dialog` to get
        the batches and their quantities and then call :meth:`.get_order_item`
        on each one.

        :param sellable: a |sellable|
        :param value: the value (e.g. price, cost) of the sellable
        :param quantity: the quantity of the sellable
        """
        order_items = []
        storable = sellable.product_storable
        original_batch_items = self.get_batch_items()
        if issubclass(self.batch_selection_dialog, BatchDecreaseSelectionDialog):
            extra_kw = dict(decreased_batches=original_batch_items)
            available_batches = list(storable.get_available_batches(self.model.branch))
            # If there're no available batches (no stock) and we are allowing
            # no batches, add the item without the batch.
            if len(available_batches) == 0 and self.allow_no_batch:
                return [self.get_order_item(sellable, value, quantity=quantity)]
            # The trivial case, where there's just one batch, and since this
            # is a decrease, we can select it directly
            if len(available_batches) == 1:
                batch = available_batches[0]
                return [self.get_order_item(sellable, value, quantity=quantity, batch=batch)]
        else:
            extra_kw = dict(original_batches=original_batch_items)

        retval = run_dialog(
            self.batch_selection_dialog,
            self.get_parent(),
            store=self.store,
            model=storable,
            quantity=quantity,
            **extra_kw
        )
        retval = retval or {}

        for batch, b_quantity in retval.items():
            order_item = self.get_order_item(sellable, value, quantity=b_quantity, batch=batch)
            if order_item is None:
                continue
            order_items.append(order_item)

        return order_items

    def get_extra_discount(self, sellable):
        """Called to get an extra discount for the sellable being added

        Subclasses can implement this to allow some extra discount for the
        sellable being added. For example, one can implement this to
        allow some extra discount based on the unused discount on the
        already added items

        Note that, if you need to get the manager to check for max discount,
        you can use :obj:`.manager`

        :param sellable: the sellable being added
        :returns: the extra discount for the sellable being added,
            or ``None`` if not extra discount should be allowed
        """
        return None

    def get_sellable_search_extra_kwargs(self):
        """Called to get extra args for :attr:`.sellable_search`

        A subclass can override this and return a dict with extra keywords
        to pass to the sellable search defined on the class.

        :returns: a ``dict`` of extra keywords
        """
        return {}

    #
    #  Private
    #

    def _setup_widgets(self):
        self._update_product_labels_visibility(False)
        self.quantity.set_sensitive(False)
        self.cost.set_sensitive(False)
        self.add_sellable_button.set_sensitive(False)
        self.unit_label.set_bold(True)

        for widget in [self.quantity, self.cost]:
            widget.set_adjustment(gtk.Adjustment(lower=0, upper=MAX_INT, step_incr=1))

        self._reset_sellable()
        self._setup_summary()
        self.cost.set_digits(sysparam.get_int("COST_PRECISION_DIGITS"))
        self.quantity.set_digits(3)

        self.barcode.grab_focus()
        self.item_table.set_focus_chain(
            [self.barcode, self.quantity, self.cost, self.add_sellable_button, self.product_button]
        )
        self.register_validate_function(self.validate)

    def _setup_summary(self):
        # FIXME: Move this into AdditionListSlave
        if not self.summary_label_column:
            self.summary = None
            return
        self.summary = SummaryLabel(
            klist=self.slave.klist,
            column=self.summary_label_column,
            label=self.summary_label_text,
            value_format="<b>%s</b>",
        )
        self.summary.show()
        self.slave.list_vbox.pack_start(self.summary, expand=False)

    def _run_advanced_search(self, search_str=None):
        table, query = self.get_sellable_view_query()
        ret = run_dialog(
            self.sellable_search,
            self.get_parent(),
            self.store,
            search_spec=table,
            search_query=query,
            search_str=search_str,
            hide_toolbar=not self.sellable_editable,
            **self.get_sellable_search_extra_kwargs()
        )
        if not ret:
            return

        # We receive different items depend on if we
        # - selected an item in the search
        # - created a new item and it closed the dialog for us
        if not isinstance(
            ret,
            (
                Product,
                ProductFullStockItemView,
                ProductComponentView,
                SellableFullStockView,
                ServiceView,
                ProductWithStockView,
            ),
        ):
            raise AssertionError(ret)

        sellable = ret.sellable
        if not self.can_add_sellable(sellable):
            return
        if sellable.barcode:
            self.barcode.set_text(sellable.barcode)
        self.sellable_selected(sellable)
        self.quantity.grab_focus()

    def _find_sellable_and_batch(self, text):
        """Find a sellable given a code, barcode or batch_number

        When searching using the code attribute of the sellable, the search will
        be case insensitive.

        :param text: the code, barcode or batch_number
        :returns: The sellable that matches the given barcode or code or
          ``None`` if nothing was found.
        """
        viewable, default_query = self.get_sellable_view_query()

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

        # First try barcode, then code since there might be a product
        # with a code equal to another product's barcode
        for attr in [viewable.barcode, viewable.code]:
            query = Lower(attr) == text.lower()
            if default_query:
                query = And(query, default_query)

            result = self.store.find(viewable, query).one()
            if result:
                return result.sellable, None

        # if none of the above worked, try to find by batch number
        query = Lower(StorableBatch.batch_number) == text.lower()
        batch = self.store.find(StorableBatch, query).one()
        if batch:
            sellable = batch.storable.product.sellable
            query = viewable.id == sellable.id
            if default_query:
                query = And(query, default_query)
            # Make sure batch's sellable is in the view
            if not self.store.find(viewable, query).is_empty():
                return sellable, batch

        return None, None

    def _get_sellable_and_batch(self):
        """This method always read the barcode and searches de database.

        If you only need the current selected sellable, use
        self.proxy.model.sellable
        """
        barcode = self.barcode.get_text()
        if not barcode:
            return None, None
        barcode = unicode(barcode, "utf-8")

        sellable, batch = self._find_sellable_and_batch(barcode)

        if not sellable:
            return None, None
        elif not self.can_add_sellable(sellable):
            return None, None

        return sellable, batch

    def _add_sellable(self):
        sellable = self.proxy.model.sellable
        assert sellable

        sellable = self.store.fetch(sellable)

        self.add_sellable(sellable)
        self.barcode.grab_focus()

    def _reset_sellable(self):
        self.proxy.set_model(None)
        self.sellable_selected(None)

    def _update_product_labels_visibility(self, visible):
        for widget in [self.minimum_quantity_lbl, self.minimum_quantity, self.stock_quantity, self.stock_quantity_lbl]:
            widget.set_visible(self.stock_labels_visible and visible)

    def _try_get_sellable(self):
        """Try to get the sellable based on the barcode typed
        This will try to get the sellable using the barcode the user entered.
           If one is not found, than an advanced search will be displayed for
        the user, and the string he typed in the barcode entry will be
        used to filter the results.
        """
        sellable, batch = self._get_sellable_and_batch()

        if not sellable:
            if self.add_sellable_on_barcode_activate:
                return
            search_str = unicode(self.barcode.get_text())
            self._run_advanced_search(search_str)
            return

        self.sellable_selected(sellable, batch=batch)

        if self.add_sellable_on_barcode_activate and self.add_sellable_button.get_sensitive():
            self._add_sellable()
        else:
            self.quantity.grab_focus()

    #
    #  Callbacks
    #

    def _on_klist__cell_edited(self, klist, obj, attr):
        self.update_total()

    def _on_list_slave__before_delete_items(self, slave, items):
        self.remove_items(items)
        self.force_validation()

    def _on_list_slave__after_delete_items(self, slave):
        self.update_total()

    def _on_list_slave__add_item(self, slave, item):
        self.update_total()

    def _on_list_slave__edit_item(self, slave, item):
        self.update_total()

    def on_add_sellable_button__clicked(self, button):
        self._add_sellable()

    def on_product_button__clicked(self, button):
        self._try_get_sellable()

    def on_barcode__activate(self, widget):
        self._try_get_sellable()

    def on_quantity__activate(self, entry):
        if self.add_sellable_button.get_sensitive():
            self._add_sellable()

    def on_cost__activate(self, entry):
        if self.add_sellable_button.get_sensitive():
            self._add_sellable()

    def on_quantity__validate(self, entry, value):
        if not self.proxy.model.sellable:
            return

        # only support positive quantities
        if value <= 0:
            return ValidationError(_(u"The quantity must be positive"))

        sellable = self.proxy.model.sellable
        if sellable and not sellable.is_valid_quantity(value):
            return ValidationError(
                _(u"This product unit (%s) does not " u"support fractions.") % sellable.unit_description
            )

        storable = sellable.product_storable
        if not self.validate_stock or not storable:
            return
        remaining_quantity = self.get_remaining_quantity(sellable)
        if remaining_quantity is None:
            return
        if value > remaining_quantity:
            return ValidationError(_("This quantity is not available in stock"))

    def on_cost__validate(self, widget, value):
        sellable = self.proxy.model.sellable
        if not sellable:
            return

        if value <= 0:
            return ValidationError(_(u"Cost must be greater than zero."))

        if self.validate_price:
            category = getattr(self.model, "client_category", None)
            default_price = sellable.get_price_for_category(category)
            if not sysparam.get_bool("ALLOW_HIGHER_SALE_PRICE") and value > default_price:
                return ValidationError(_(u"The sell price cannot be greater " "than %s.") % default_price)

            manager = self.manager or api.get_current_user(self.store)
            client = getattr(self.model, "client", None)
            category = client and client.category
            extra_discount = self.get_extra_discount(sellable)
            valid_data = sellable.is_valid_price(value, category, manager, extra_discount=extra_discount)

            if not valid_data["is_valid"]:
                return ValidationError((_(u"Max discount for this product is %.2f%%.") % valid_data["max_discount"]))

    def on_cost__icon_press(self, entry, icon_pos, event):
        if icon_pos != gtk.ENTRY_ICON_PRIMARY:
            return

        # No need to check credentials if it is not a price
        if not self.validate_price:
            return

        # Ask for the credentials of a different user that can possibly allow a
        # bigger discount.
        self.manager = run_dialog(CredentialsDialog, self.parent, self.store)
        if self.manager:
            self.cost.validate(force=True)
Exemplo n.º 37
0
class ProductComponentSlave(BaseEditorSlave):
    gladefile = 'ProductComponentSlave'
    model_type = TemporaryProductComponent
    proxy_widgets = ['production_time']

    def __init__(self, store, product=None, visual_mode=False):
        self._product = product
        self._remove_component_list = []
        BaseEditorSlave.__init__(self,
                                 store,
                                 model=None,
                                 visual_mode=visual_mode)
        self._setup_widgets()

    def _get_columns(self):
        return [
            Column('code',
                   title=_(u'Code'),
                   data_type=int,
                   expander=True,
                   sorted=True),
            Column('quantity', title=_(u'Quantity'), data_type=Decimal),
            Column('unit', title=_(u'Unit'), data_type=str),
            Column('description',
                   title=_(u'Description'),
                   data_type=str,
                   expand=True),
            Column('category', title=_(u'Category'), data_type=str),
            # Translators: Ref. is for Reference (as in design reference)
            Column('design_reference', title=_(u'Ref.'), data_type=str),
            Column('production_cost',
                   title=_(u'Production Cost'),
                   format_func=get_formatted_cost,
                   data_type=currency),
            Column('total_production_cost',
                   title=_(u'Total'),
                   format_func=get_formatted_cost,
                   data_type=currency),
        ]

    def _setup_widgets(self):
        component_list = list(self._get_products())
        if component_list:
            self.component_combo.prefill(component_list)
        else:
            self.sort_components_check.set_sensitive(False)

        self.component_tree.set_columns(self._get_columns())
        self._populate_component_tree()
        self.component_label = SummaryLabel(klist=self.component_tree,
                                            column='total_production_cost',
                                            label='<b>%s</b>' %
                                            api.escape(_(u'Total:')),
                                            value_format='<b>%s</b>')
        self.component_label.show()
        self.component_tree_vbox.pack_start(self.component_label, False)
        self.info_label.set_bold(True)
        self._update_widgets()
        if self.visual_mode:
            self.component_combo.set_sensitive(False)
            self.add_button.set_sensitive(False)
            self.sort_components_check.set_sensitive(False)

    def _get_products(self, sort_by_name=True):
        # FIXME: This is a kind of workaround until we have the
        # SQLCompletion funcionality, then we will not need to sort the
        # data.
        if sort_by_name:
            attr = ProductFullStockView.description
        else:
            attr = ProductFullStockView.category_description

        products = []
        query = Eq(Product.is_grid, False)
        for product_view in self.store.find(ProductFullStockView,
                                            query).order_by(attr):
            if product_view.product is self._product:
                continue

            description = product_view.get_product_and_category_description()
            products.append((description, product_view.product))

        return products

    def _update_widgets(self):
        has_selected = self.component_combo.read() is not None
        self.add_button.set_sensitive(has_selected)
        has_selected = self.component_tree.get_selected() is not None
        self.edit_button.set_sensitive(has_selected)
        self.remove_button.set_sensitive(has_selected)

        # FIXME: This is wrong. Summary label already calculates the total. We
        # are duplicating this.
        value = self.get_component_cost()
        self.component_label.set_value(get_formatted_cost(value))

        if not self._validate_components():
            self.component_combo.set_sensitive(False)
            self.add_button.set_sensitive(False)
            self.edit_button.set_sensitive(False)
            self.remove_button.set_sensitive(False)
            self.info_label.set_text(
                _(u"This product is being produced. "
                  "Can't change components."))

    def _populate_component_tree(self):
        self._add_to_component_tree()

    def _get_components(self, product):
        for component in self.store.find(ProductComponent, product=product):
            yield TemporaryProductComponent(
                product=component.product,
                component=component.component,
                quantity=component.quantity,
                design_reference=component.design_reference)

    def _add_to_component_tree(self, component=None):
        parent = None
        if component is None:
            # load all components that already exists
            subcomponents = self._get_components(self._product)
        else:
            if component not in self.component_tree:
                self.component_tree.append(None, component)
            subcomponents = self._get_components(component.component)
            parent = component

        for subcomponent in subcomponents:
            self.component_tree.append(parent, subcomponent)
            # recursively add the children
            self._add_to_component_tree(subcomponent)

    def _can_add_component(self, component):
        if component.component.is_composed_by(self._product):
            return False
        return True

    def _run_product_component_dialog(self, product_component=None):
        update = True
        if product_component is None:
            update = False
            component = self.component_combo.get_selected_data()
            product_component = TemporaryProductComponent(
                product=self._product, component=component)
            # If we try to add a component which is already in tree,
            # just edit it
            for component in self.component_tree:
                if component.component == product_component.component:
                    update = True
                    product_component = component
                    break

        if not self._can_add_component(product_component):
            product_desc = self._product.sellable.get_description()
            component_desc = product_component.description
            info(
                _(u'You can not add this product as component, since '
                  '%s is composed by %s' % (component_desc, product_desc)))
            return

        toplevel = self.get_toplevel().get_toplevel()
        # We cant use savepoint here, since product_component
        # is not an ORM object.
        model = run_dialog(ProductComponentEditor, toplevel, self.store,
                           product_component)
        if not model:
            return

        if update:
            self.component_tree.update(model)
        else:
            self._add_to_component_tree(model)
        self._update_widgets()

    def _edit_component(self):
        # Only allow edit the root components, since its the component
        # that really belongs to the current product
        selected = self.component_tree.get_selected()
        root = self.component_tree.get_root(selected)
        self._run_product_component_dialog(root)

    def _totally_remove_component(self, component):
        descendants = self.component_tree.get_descendants(component)
        for descendant in descendants:
            # we can not remove an item twice
            if descendant not in self.component_tree:
                continue
            else:
                self._totally_remove_component(descendant)
        self.component_tree.remove(component)

    def _remove_component(self, component):
        # Only allow remove the root components, since its the component
        # that really belongs to the current product
        root_component = self.component_tree.get_root(component)

        msg = _("This will remove the component \"%s\". Are you sure?") % (
            root_component.description)
        if not yesno(msg, gtk.RESPONSE_NO, _("Remove component"),
                     _("Keep component")):
            return

        self._remove_component_list.append(root_component)
        self._totally_remove_component(root_component)
        self._update_widgets()

    def _validate_components(self):
        return not ProductionOrderProducingView.is_product_being_produced(
            self.model.product)

    #
    # BaseEditorSlave
    #

    def setup_proxies(self):
        self.proxy = self.add_proxy(self._product, self.proxy_widgets)

    def create_model(self, store):
        return TemporaryProductComponent(product=self._product)

    def on_confirm(self):
        for component in self._remove_component_list:
            component.delete_product_component(self.store)

        for component in self.component_tree:
            component.add_or_update_product_component(self.store)

    def validate_confirm(self):
        if not len(self.component_tree) > 0:
            info(_(u'There is no component in this product.'))
            return False
        return True

    def get_component_cost(self):
        value = Decimal('0')
        for component in self.component_tree:
            if self.component_tree.get_parent(component):
                continue
            value += component.get_total_production_cost()
        return quantize(value)

    #
    # Kiwi Callbacks
    #

    def on_component_combo__content_changed(self, widget):
        self._update_widgets()

    def on_component_tree__selection_changed(self, widget, value):
        if self.visual_mode:
            return
        self._update_widgets()

    def on_component_tree__row_activated(self, widget, selected):
        if self.visual_mode:
            return

        if not self._validate_components():
            return

        self._edit_component()

    def on_component_tree__row_expanded(self, widget, value):
        if self.visual_mode:
            return
        self._update_widgets()

    def on_add_button__clicked(self, widget):
        self._run_product_component_dialog()

    def on_edit_button__clicked(self, widget):
        self._edit_component()

    def on_remove_button__clicked(self, widget):
        selected = self.component_tree.get_selected()
        self._remove_component(selected)

    def on_sort_components_check__toggled(self, widget):
        sort_by_name = not widget.get_active()
        self.component_combo.prefill(
            self._get_products(sort_by_name=sort_by_name))
        self.component_combo.select_item_by_position(0)
Exemplo n.º 38
0
 def _setup_summary(self):
     self.summary = SummaryLabel(klist=self.product_list,
                                 column='total_received',
                                 value_format='<b>%s</b>')
     self.summary.show()
     self.vbox1.pack_start(self.summary, expand=False)
Exemplo n.º 39
0
class ProductComponentSlave(BaseEditorSlave):
    gladefile = 'ProductComponentSlave'
    model_type = TemporaryProductComponent
    proxy_widgets = ['production_time']

    def __init__(self, store, product=None, visual_mode=False):
        self._product = product
        self._remove_component_list = []
        BaseEditorSlave.__init__(self, store, model=None, visual_mode=visual_mode)
        self._setup_widgets()

    def _get_columns(self):
        return [Column('code', title=_(u'Code'), data_type=int,
                       expander=True, sorted=True),
                Column('quantity', title=_(u'Quantity'),
                       data_type=Decimal),
                Column('unit', title=_(u'Unit'), data_type=str),
                Column('description', title=_(u'Description'),
                       data_type=str, expand=True),
                Column('category', title=_(u'Category'), data_type=str),
                # Translators: Ref. is for Reference (as in design reference)
                Column('design_reference', title=_(u'Ref.'), data_type=str),
                Column('production_cost', title=_(u'Production Cost'),
                       format_func=get_formatted_cost, data_type=currency),
                Column('total_production_cost', title=_(u'Total'),
                       format_func=get_formatted_cost, data_type=currency),
                ]

    def _setup_widgets(self):
        component_list = list(self._get_products())
        if component_list:
            self.component_combo.prefill(component_list)
        else:
            self.sort_components_check.set_sensitive(False)

        self.component_tree.set_columns(self._get_columns())
        self._populate_component_tree()
        self.component_label = SummaryLabel(
            klist=self.component_tree,
            column='total_production_cost',
            label='<b>%s</b>' % api.escape(_(u'Total:')),
            value_format='<b>%s</b>')
        self.component_label.show()
        self.component_tree_vbox.pack_start(self.component_label, False)
        self.info_label.set_bold(True)
        self._update_widgets()
        if self.visual_mode:
            self.component_combo.set_sensitive(False)
            self.add_button.set_sensitive(False)
            self.sort_components_check.set_sensitive(False)

    def _get_products(self, sort_by_name=True):
        # FIXME: This is a kind of workaround until we have the
        # SQLCompletion funcionality, then we will not need to sort the
        # data.
        if sort_by_name:
            attr = ProductFullStockView.description
        else:
            attr = ProductFullStockView.category_description

        products = []
        query = Eq(Product.is_grid, False)
        for product_view in self.store.find(ProductFullStockView, query).order_by(attr):
            if product_view.product is self._product:
                continue

            description = product_view.get_product_and_category_description()
            products.append((description, product_view.product))

        return products

    def _update_widgets(self):
        has_selected = self.component_combo.read() is not None
        self.add_button.set_sensitive(has_selected)
        has_selected = self.component_tree.get_selected() is not None
        self.edit_button.set_sensitive(has_selected)
        self.remove_button.set_sensitive(has_selected)

        # FIXME: This is wrong. Summary label already calculates the total. We
        # are duplicating this.
        value = self.get_component_cost()
        self.component_label.set_value(get_formatted_cost(value))

        if not self._validate_components():
            self.component_combo.set_sensitive(False)
            self.add_button.set_sensitive(False)
            self.edit_button.set_sensitive(False)
            self.remove_button.set_sensitive(False)
            self.info_label.set_text(_(u"This product is being produced. "
                                       "Can't change components."))

    def _populate_component_tree(self):
        self._add_to_component_tree()

    def _get_components(self, product):
        for component in self.store.find(ProductComponent, product=product):
            yield TemporaryProductComponent(product=component.product,
                                            component=component.component,
                                            quantity=component.quantity,
                                            design_reference=component.design_reference)

    def _add_to_component_tree(self, component=None):
        parent = None
        if component is None:
            # load all components that already exists
            subcomponents = self._get_components(self._product)
        else:
            if component not in self.component_tree:
                self.component_tree.append(None, component)
            subcomponents = self._get_components(component.component)
            parent = component

        for subcomponent in subcomponents:
            self.component_tree.append(parent, subcomponent)
            # recursively add the children
            self._add_to_component_tree(subcomponent)

    def _can_add_component(self, component):
        if component.component.is_composed_by(self._product):
            return False
        return True

    def _run_product_component_dialog(self, product_component=None):
        update = True
        if product_component is None:
            update = False
            component = self.component_combo.get_selected_data()
            product_component = TemporaryProductComponent(
                product=self._product, component=component)
            # If we try to add a component which is already in tree,
            # just edit it
            for component in self.component_tree:
                if component.component == product_component.component:
                    update = True
                    product_component = component
                    break

        if not self._can_add_component(product_component):
            product_desc = self._product.sellable.get_description()
            component_desc = product_component.description
            info(_(u'You can not add this product as component, since '
                   '%s is composed by %s' % (component_desc, product_desc)))
            return

        toplevel = self.get_toplevel().get_toplevel()
        # We cant use savepoint here, since product_component
        # is not an ORM object.
        model = run_dialog(ProductComponentEditor, toplevel, self.store,
                           product_component)
        if not model:
            return

        if update:
            self.component_tree.update(model)
        else:
            self._add_to_component_tree(model)
        self._update_widgets()

    def _edit_component(self):
        # Only allow edit the root components, since its the component
        # that really belongs to the current product
        selected = self.component_tree.get_selected()
        root = self.component_tree.get_root(selected)
        self._run_product_component_dialog(root)

    def _totally_remove_component(self, component):
        descendants = self.component_tree.get_descendants(component)
        for descendant in descendants:
            # we can not remove an item twice
            if descendant not in self.component_tree:
                continue
            else:
                self._totally_remove_component(descendant)
        self.component_tree.remove(component)

    def _remove_component(self, component):
        # Only allow remove the root components, since its the component
        # that really belongs to the current product
        root_component = self.component_tree.get_root(component)

        msg = _("This will remove the component \"%s\". Are you sure?") % (
            root_component.description)
        if not yesno(msg, gtk.RESPONSE_NO,
                     _("Remove component"),
                     _("Keep component")):
            return

        self._remove_component_list.append(root_component)
        self._totally_remove_component(root_component)
        self._update_widgets()

    def _validate_components(self):
        return not ProductionOrderProducingView.is_product_being_produced(
            self.model.product)

    #
    # BaseEditorSlave
    #

    def setup_proxies(self):
        self.proxy = self.add_proxy(self._product, self.proxy_widgets)

    def create_model(self, store):
        return TemporaryProductComponent(product=self._product)

    def on_confirm(self):
        for component in self._remove_component_list:
            component.delete_product_component(self.store)

        for component in self.component_tree:
            component.add_or_update_product_component(self.store)

    def validate_confirm(self):
        if not len(self.component_tree) > 0:
            info(_(u'There is no component in this product.'))
            return False
        return True

    def get_component_cost(self):
        value = Decimal('0')
        for component in self.component_tree:
            if self.component_tree.get_parent(component):
                continue
            value += component.get_total_production_cost()
        return quantize(value)

    #
    # Kiwi Callbacks
    #

    def on_component_combo__content_changed(self, widget):
        self._update_widgets()

    def on_component_tree__selection_changed(self, widget, value):
        if self.visual_mode:
            return
        self._update_widgets()

    def on_component_tree__row_activated(self, widget, selected):
        if self.visual_mode:
            return

        if not self._validate_components():
            return

        self._edit_component()

    def on_component_tree__row_expanded(self, widget, value):
        if self.visual_mode:
            return
        self._update_widgets()

    def on_add_button__clicked(self, widget):
        self._run_product_component_dialog()

    def on_edit_button__clicked(self, widget):
        self._edit_component()

    def on_remove_button__clicked(self, widget):
        selected = self.component_tree.get_selected()
        self._remove_component(selected)

    def on_sort_components_check__toggled(self, widget):
        sort_by_name = not widget.get_active()
        self.component_combo.prefill(
            self._get_products(sort_by_name=sort_by_name))
        self.component_combo.select_item_by_position(0)
Exemplo n.º 40
0
 def _setup_summary(self):
     self.summary = SummaryLabel(klist=self.product_list,
                                 column='total_received',
                                 value_format='<b>%s</b>')
     self.summary.show()
     self.vbox1.pack_start(self.summary, expand=False)
Exemplo n.º 41
0
data = (Person('Evandro', 23,
               'Belo Horizonte'), Person('Daniel', 22, 'São Carlos'),
        Person('Henrique', 21,
               'São Carlos'), Person('Gustavo', 23, 'São Jose do Santos'),
        Person('Johan', 23, 'Göteborg'), Person('Lorenzo', 26, 'Granada'))

win = Gtk.Window()
win.set_size_request(850, 300)
win.connect('destroy', Gtk.main_quit)

vbox = Gtk.VBox()
win.add(vbox)

l = ObjectList(columns, mode=Gtk.SelectionMode.MULTIPLE)
l.extend(data)
l.append(Person('Nando', 29 + len(l), 'Santos'))

# add an extra person

vbox.pack_start(l, True, True, 0)

label = SummaryLabel(klist=l,
                     column='salary',
                     label='<b>Total:</b>',
                     value_format='<b>%s</b>')
vbox.pack_start(label, False, True, 6)

win.show_all()

Gtk.main()
Exemplo n.º 42
0
class SearchContainer(gtk.VBox):
    """
    A search container is a widget which consists of:
    - search entry (w/ a label) (L{StringSearchFilter})
    - search button
    - objectlist result (L{SearchResult})
    - a query executer (L{kiwi.db.query.QueryExecuter})

    Additionally you can add a number of search filters to the SearchContainer.
    You can chose if you want to add the filter in the top-left corner
    of bottom, see L{SearchFilterPosition}
    """
    __gtype_name__ = 'SearchContainer'
    filter_label = gobject.property(type=str)
    results_class = SearchResults
    gsignal("search-completed", object)

    def __init__(self, columns=None, chars=25):
        """
        Create a new SearchContainer object.
        @param columns: a list of L{kiwi.ui.objectlist.Column}
        @param chars: maximum number of chars used by the search entry
        """
        gtk.VBox.__init__(self)
        self._columns = columns
        self._search_filters = []
        self._query_executer = None
        self._auto_search = True
        self._summary_label = None

        search_filter = StringSearchFilter(_('Search:'), chars=chars)
        search_filter.connect('changed', self._on_search_filter__changed)
        self._search_filters.append(search_filter)
        self._primary_filter = search_filter

        self._create_ui()


    #
    # GObject
    #

    def do_set_property(self, pspec, value):
        if pspec.name == 'filter-label':
            self._primary_filter.set_label(value)
        else:
            raise AssertionError(pspec.name)

    def do_get_property(self, pspec):
        if pspec.name == 'filter-label':
            return self._primary_filter.get_label()
        else:
            raise AssertionError(pspec.name)

    #
    # GtkContainer
    #

    def do_set_child_property(self, child, property_id, value, pspec):
        if pspec.name == 'filter-position':
            if value == 'top':
                pos = SearchFilterPosition.TOP
            elif value == 'bottom':
                pos = SearchFilterPosition.BOTTOM
            else:
                raise Exception(pos)
            self.set_filter_position(child, pos)
        else:
            raise AssertionError(pspec.name)

    def do_get_child_property(self, child, property_id, pspec):
        if pspec.name == 'filter-position':
            return self.get_filter_position(child)
        else:
            raise AssertionError(pspec.name)


    #
    # Public API
    #

    def add_filter(self, search_filter, position=SearchFilterPosition.BOTTOM,
                   columns=None, callback=None):
        """
        Adds a search filter
        @param search_filter: the search filter
        @param postition: a L{SearchFilterPosition} enum
        @param columns:
        @param callback:
        """

        if not isinstance(search_filter, SearchFilter):
            raise TypeError("search_filter must be a SearchFilter subclass, "
                            "not %r" % (search_filter,))

        if columns and callback:
            raise TypeError("Cannot specify both column and callback")

        executer = self.get_query_executer()
        if executer:
            if columns:
                executer.set_filter_columns(search_filter, columns)
            if callback:
                if not callable(callback):
                    raise TypeError("callback must be callable")
                executer.add_filter_query_callback(search_filter, callback)
        else:
            if columns or callback:
                raise TypeError(
                    "You need to set an executor before calling set_filters "
                    "with columns or callback set")

        assert not search_filter.parent
        self.set_filter_position(search_filter, position)
        search_filter.connect('changed', self._on_search_filter__changed)
        search_filter.connect('removed', self._on_search_filter__remove)
        self._search_filters.append(search_filter)

    def remove_filter(self, filter):
        self.filters_box.remove(filter)
        self._search_filters.remove(filter)
        filter.destroy()

        if self._auto_search:
            self.search()

    def get_search_filters(self):
        return self._search_filters

    def set_filter_position(self, search_filter, position):
        """
        Set the the filter position.
        @param search_filter:
        @param position:
        """
        if search_filter.parent:
            search_filter.parent.remove(search_filter)

        if position == SearchFilterPosition.TOP:
            self.hbox.pack_start(search_filter, False, False)
            self.hbox.reorder_child(search_filter, 0)
        elif position == SearchFilterPosition.BOTTOM:
            self.filters_box.pack_start(search_filter, False, False)
        search_filter.show()

    def get_filter_position(self, search_filter):
        """
        Get filter by position.
        @param search_filter:
        """
        if search_filter.parent == self.hbox:
            return SearchFilterPosition.TOP
        elif search_filter.parent == self:
            return SearchFilterPosition.BOTTOM
        else:
            raise AssertionError(search_filter)

    def set_query_executer(self, querty_executer):
        """
        Ties a QueryExecuter instance to the SearchContainer class
        @param querty_executer: a querty executer
        @type querty_executer: a L{QueryExecuter} subclass
        """
        if not isinstance(querty_executer, QueryExecuter):
            raise TypeError("querty_executer must be a QueryExecuter instance")

        self._query_executer = querty_executer

    def get_query_executer(self):
        """
        Fetchs the QueryExecuter for the SearchContainer
        @returns: a querty executer
        @rtype: a L{QueryExecuter} subclass
        """
        return self._query_executer

    def get_primary_filter(self):
        """
        Fetches the primary filter for the SearchContainer.
        The primary filter is the filter attached to the standard entry
        normally used to do free text searching
        @returns: the primary filter
        """
        return self._primary_filter

    def search(self):
        """
        Starts a search.
        Fetches the states of all filters and send it to a query executer and
        finally puts the result in the result class
        """
        if not self._query_executer:
            raise ValueError("A query executer needs to be set at this point")
        states = [(sf.get_state()) for sf in self._search_filters]
        results = self._query_executer.search(states)
        self.add_results(results)
        self.emit("search-completed", self.results)
        if self._summary_label:
            self._summary_label.update_total()

    def set_auto_search(self, auto_search):
        """
        Enables/Disables auto search which means that the search result box
        is automatically populated when a filter changes
        @param auto_search: True to enable, False to disable
        """
        self._auto_search = auto_search

    def set_text_field_columns(self, columns):
        if self._primary_filter is None:
            raise ValueError("The primary filter is disabled")

        if not self._query_executer:
            raise ValueError("A query executer needs to be set at this point")

        self._query_executer.set_filter_columns(self._primary_filter, columns)

    def disable_search_entry(self):
        """
        Disables the search entry
        """
        self.search_entry.hide()
        self._primary_filter.hide()
        self._search_filters.remove(self._primary_filter)
        self._primary_filter = None

    def set_summary_label(self, column, label='Total:', format='%s'):
        """
        Adds a summary label to the result set
        @param column: the column to sum from
        @param label: the label to use, defaults to 'Total:'
        @param format: the format, defaults to '%%s', must include '%%s'
        """
        if not '%s' in format:
            raise ValueError("format must contain %s")

        try:
            self.results.get_column_by_name(column)
        except LookupError:
            raise ValueError("%s is not a valid column" % (column,))

        if self._summary_label:
            self._summary_label.parent.remove(self._summary_label)
        self._summary_label = SummaryLabel(klist=self.results,
                                           column=column,
                                           label=label,
                                           value_format=format)
        self.pack_end(self._summary_label, False, False)
        self.reorder_child(self._summary_label, 1)
        self._summary_label.show()

    def enable_advanced_search(self):
        self._create_advanced_search()

    def add_results(self, results):
        self.results.clear()
        self.results.extend(results)

    #
    # Callbacks
    #

    def _on_search_button__clicked(self, button):
        self.search()

    def _on_search_entry__activate(self, button):
        self.search()

    def _on_search_filter__remove(self, filter):
        self.remove_filter(filter)

    def _on_search_filter__changed(self, search_filter):
        if self._auto_search:
            self.search()

    #
    # Private
    #

    def _create_ui(self):
        self._create_basic_search()

        self.results = self.results_class(self._columns)
        self.pack_end(self.results, True, True, 6)
        self.results.show()

    def _create_basic_search(self):
        filters_box = gtk.VBox()
        filters_box.show()
        self.pack_start(filters_box, expand=False)

        hbox = gtk.HBox()
        hbox.set_border_width(3)
        filters_box.pack_start(hbox, False, False)
        hbox.show()
        self.hbox = hbox

        widget = self._primary_filter
        self.hbox.pack_start(widget, False, False)
        widget.show()

        self.search_entry = self._primary_filter.entry
        self.search_entry.connect('activate',
                                  self._on_search_entry__activate)

        button = SearchFilterButton(stock=gtk.STOCK_FIND)
        button.connect('clicked', self._on_search_button__clicked)
        hbox.pack_start(button, False, False)
        button.show()

        self.filters_box = filters_box

    def _create_advanced_search(self):
        self.label_group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
        self.combo_group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)

        add = SearchFilterButton(label=_('Filter'), stock=gtk.STOCK_ADD)
        add.connect('clicked', self._on_add_field_clicked)
        add.show()
        self.hbox.pack_end(add, False, False, 0)

        self.add_filter_button = add

        self.menu = gtk.Menu()
        for column in self._columns:
            if not isinstance(column, SearchColumn):
                continue

            if column.data_type not in (datetime.date, Decimal, int, currency,
                                        str):
                continue

            title = column.long_title or column.title

            menu_item = gtk.MenuItem(title)
            menu_item.set_data('column', column)
            menu_item.show()
            menu_item.connect('activate', self._on_menu_item_activate)
            self.menu.append(menu_item)

        if not len(self.menu):
            self.add_filter_button.hide()

    def _position_filter_menu(self, data):
        alloc = self.add_filter_button.get_allocation()
        x, y = self.add_filter_button.window.get_origin()

        return (x + alloc.x, y + alloc.y + alloc.height, True)

    def _on_add_field_clicked(self, button):
        self.menu.popup(None, None, self._position_filter_menu, 0, 0L)

    def _on_menu_item_activate(self, item):
        column = item.get_data('column')

        if column is None:
            return

        title = (column.long_title or column.title) + ':'

        if column.data_type == datetime.date:
            filter = DateSearchFilter(title)
            if column.valid_values:
                filter.clear_options()
                filter.add_custom_options()
                for opt in column.valid_values:
                    filter.add_option(opt)
                filter.select(column.valid_values[0])

        elif (column.data_type == Decimal or
              column.data_type == int or
              column.data_type == currency):
            filter = NumberSearchFilter(title)
            if column.data_type != int:
                filter.set_digits(2)
        elif column.data_type == str:
            if column.valid_values:
                filter = ComboSearchFilter(title, column.valid_values)
            else:
                filter = StringSearchFilter(title)
                filter.enable_advaced()
        else:
            # TODO: Boolean
            raise NotImplementedError

        filter.set_removable()
        attr = column.search_attribute or column.attribute
        self.add_filter(filter, columns=[attr])

        label = filter.get_title_label()
        label.set_alignment(1.0, 0.5)
        self.label_group.add_widget(label)
        combo = filter.get_mode_combo()
        if combo:
            self.combo_group.add_widget(combo)