class SalesApp(ShellApp): app_title = _('Sales') gladefile = 'sales_app' search_spec = SaleView search_label = _('matching:') report_table = SalesReport cols_info = { Sale.STATUS_INITIAL: 'open_date', Sale.STATUS_CONFIRMED: 'confirm_date', Sale.STATUS_ORDERED: 'open_date', Sale.STATUS_CANCELLED: 'cancel_date', Sale.STATUS_QUOTE: 'open_date', Sale.STATUS_RETURNED: 'return_date', Sale.STATUS_RENEGOTIATED: 'close_date' } action_permissions = { "SalesPrintInvoice": ('app.sales.print_invoice', PermissionManager.PERM_SEARCH), } def __init__(self, window, store=None): self.summary_label = None self._visible_date_col = None ShellApp.__init__(self, window, store=store) # # Application # def create_actions(self): group = get_accels('app.sales') actions = [ # File ("SaleQuote", None, _("Sale quote..."), '', _('Create a new quote for a sale')), ("WorkOrderQuote", None, _("Sale with work order..."), '', _('Create a new quote for a sale with work orders')), ("LoanNew", None, _("Loan...")), ("LoanClose", None, _("Close loan...")), # Search ("SearchSoldItemsByBranch", None, _("Sold items by branch..."), group.get("search_sold_items_by_branch"), _("Search for sold items by branch")), ("SearchSalesByPaymentMethod", None, _("Sales by payment method..."), group.get("search_sales_by_payment")), ("SearchSalesPersonSales", None, _("Total sales made by salesperson..."), group.get("search_salesperson_sales"), _("Search for sales by payment method")), ("SearchProduct", STOQ_PRODUCTS, _("Products..."), group.get("search_products"), _("Search for products")), ("SearchService", STOQ_SERVICES, _("Services..."), group.get("search_services"), _("Search for services")), ("SearchDelivery", STOQ_DELIVERY, _("Deliveries..."), group.get("search_deliveries"), _("Search for deliveries")), ("SearchClient", STOQ_CLIENTS, _("Clients..."), group.get("search_clients"), _("Search for clients")), ("SearchClientCalls", None, _("Client Calls..."), group.get("search_client_calls"), _("Search for client calls")), ("SearchCreditCheckHistory", None, _("Client credit check history..."), group.get("search_credit_check_history"), _("Search for client check history")), ("SearchCommission", None, _("Commissions..."), group.get("search_commissions"), _("Search for salespersons commissions")), ("LoanSearch", None, _("Loans..."), group.get("search_loans")), ("LoanSearchItems", None, _("Loan items..."), group.get("search_loan_items")), ("ReturnedSaleSearch", None, _("Returned sales..."), group.get("returned_sales")), ("SearchUnconfirmedSaleItems", None, _("Unconfirmed sale items..."), group.get("search_reserved_product"), _("Search for unconfirmed sale items")), ("SearchClientsWithSale", None, _("Clients with sales..."), None, _("Search for regular clients")), ("SearchClientsWithCredit", None, _("Clients with credit..."), None, _("Search for clients that have credit")), ("SearchSoldItemsByClient", None, _("Sold items by client..."), None, _("Search for products sold by client")), # Sale ("SaleMenu", None, _("Sale")), ("SalesCancel", None, _("Cancel...")), ("ChangeClient", gtk.STOCK_EDIT, _("Change client...")), ("ChangeSalesperson", gtk.STOCK_EDIT, _("Change salesperson...")), ("SalesPrintInvoice", gtk.STOCK_PRINT, _("_Print invoice...")), ("Return", gtk.STOCK_CANCEL, _("Return..."), '', _("Return the selected sale, canceling it's payments")), ("Edit", gtk.STOCK_EDIT, _("Edit..."), '', _("Edit the selected sale, allowing you to change the details " "of it")), ("Details", gtk.STOCK_INFO, _("Details..."), '', _("Show details of the selected sale")) ] self.sales_ui = self.add_ui_actions("", actions, filename="sales.xml") self.SaleQuote.set_short_label(_("New Sale Quote")) self.SaleQuote.set_short_label(_("New Sale Quote with Work Order")) self.SearchClient.set_short_label(_("Clients")) self.SearchProduct.set_short_label(_("Products")) self.SearchService.set_short_label(_("Services")) self.SearchDelivery.set_short_label(_("Deliveries")) self.SalesCancel.set_short_label(_("Cancel")) self.ChangeClient.set_short_label(_("Change Client")) self.ChangeSalesperson.set_short_label(_("Change Salesperson")) self.Edit.set_short_label(_("Edit")) self.Return.set_short_label(_("Return")) self.Details.set_short_label(_("Details")) self.set_help_section(_("Sales help"), 'app-sales') def create_ui(self): if api.sysparam.get_bool('SMART_LIST_LOADING'): self.search.enable_lazy_search() if not api.sysparam.get_bool('CHANGE_CLIENT_AFTER_CONFIRMED'): self.ChangeClient.set_visible(False) if not api.sysparam.get_bool('CHANGE_SALESPERSON_AFTER_CONFIRMED'): self.ChangeSalesperson.set_visible(False) self.popup = self.uimanager.get_widget('/SaleSelection') self._setup_columns() self._setup_widgets() self.window.add_new_items([self.SaleQuote, self.WorkOrderQuote]) self.window.add_search_items([ self.SearchProduct, self.SearchClient, self.SearchService, self.SearchDelivery ]) self.window.Print.set_tooltip(_("Print a report of these sales")) def activate(self, refresh=True): if refresh: self.refresh() self.check_open_inventory() self._update_toolbar() self.search.focus_search_entry() def deactivate(self): self.uimanager.remove_ui(self.sales_ui) def new_activate(self): self._new_sale_quote(wizard=SaleQuoteWizard) def search_activate(self): self._search_product() def set_open_inventory(self): self.set_sensitive(self._inventory_widgets, False) def create_filters(self): self.set_text_field_columns( ['client_name', 'salesperson_name', 'identifier_str']) status_filter = ComboSearchFilter(_('Show sales'), self._get_filter_options()) status_filter.combo.set_row_separator_func( lambda model, titer: model[titer][0] == 'sep') executer = self.search.get_query_executer() executer.add_filter_query_callback(status_filter, self._get_status_query) self.add_filter(status_filter, position=SearchFilterPosition.TOP) self.create_branch_filter(column=Sale.branch_id) def get_columns(self): self._status_col = SearchColumn('status_name', title=_('Status'), data_type=str, width=80, visible=False, search_attribute='status', valid_values=self._get_status_values()) cols = [ IdentifierColumn('identifier', title=_('Sale #'), sorted=True), SearchColumn('coupon_id', title=_('Coupon #'), width=100, data_type=int, visible=False), SearchColumn('paid', title=_('Paid'), width=120, data_type=bool, visible=False), SearchColumn('open_date', title=_('Open date'), width=120, data_type=date, justify=gtk.JUSTIFY_RIGHT, visible=False), SearchColumn('close_date', title=_('Close date'), width=120, data_type=date, justify=gtk.JUSTIFY_RIGHT, visible=False), SearchColumn('confirm_date', title=_('Confirm date'), data_type=date, justify=gtk.JUSTIFY_RIGHT, visible=False, width=120), SearchColumn('cancel_date', title=_('Cancel date'), width=120, data_type=date, justify=gtk.JUSTIFY_RIGHT, visible=False), SearchColumn('return_date', title=_('Return date'), width=120, data_type=date, justify=gtk.JUSTIFY_RIGHT, visible=False), SearchColumn('expire_date', title=_('Expire date'), width=120, data_type=date, justify=gtk.JUSTIFY_RIGHT, visible=False), self._status_col, SearchColumn('client_name', title=_('Client'), data_type=str, width=140, expand=True, ellipsize=pango.ELLIPSIZE_END), SearchColumn('salesperson_name', title=_('Salesperson'), data_type=str, width=130, ellipsize=pango.ELLIPSIZE_END), SearchColumn('total_quantity', title=_('Items'), data_type=decimal.Decimal, width=60, format_func=format_quantity), SearchColumn('total', title=_('Total'), data_type=currency, width=120, search_attribute='_total') ] return cols # # Private # def _create_summary_label(self): self.search.set_summary_label(column='total', label='<b>Total:</b>', format='<b>%s</b>', parent=self.get_statusbar_message_area()) def _setup_widgets(self): self._setup_slaves() self._inventory_widgets = [ self.sale_toolbar.return_sale_button, self.Return, self.LoanNew, self.LoanClose ] self.register_sensitive_group(self._inventory_widgets, lambda: not self.has_open_inventory()) def _setup_slaves(self): # This is only here to reuse the logic in it. self.sale_toolbar = SaleListToolbar(self.store, self.results, parent=self) def _update_toolbar(self, *args): sale_view = self.results.get_selected() # FIXME: Disable invoice printing if the sale was returned. Remove this # when we add proper support for returned sales invoice. can_print_invoice = bool(sale_view and sale_view.client_name is not None and sale_view.status != Sale.STATUS_RETURNED) self.set_sensitive([self.SalesPrintInvoice], can_print_invoice) self.set_sensitive([self.SalesCancel], bool(sale_view and sale_view.can_cancel())) self.set_sensitive([self.sale_toolbar.return_sale_button, self.Return], bool(sale_view and sale_view.can_return())) self.set_sensitive( [self.sale_toolbar.return_sale_button, self.Details], bool(sale_view)) self.set_sensitive([self.sale_toolbar.edit_button, self.Edit], bool(sale_view and sale_view.can_edit())) # If the sale cannot be edit anymore, we only allow to change the client self.set_sensitive([self.ChangeClient], bool(sale_view and not sale_view.can_edit())) self.set_sensitive([self.ChangeSalesperson], bool(sale_view and not sale_view.can_edit())) self.sale_toolbar.set_report_filters(self.search.get_search_filters()) def _print_invoice(self): sale_view = self.results.get_selected() assert sale_view sale = sale_view.sale station = api.get_current_station(self.store) printer = InvoicePrinter.get_by_station(station, self.store) if printer is None: info(_("There are no invoice printer configured for this station")) return assert printer.layout invoice = SaleInvoice(sale, printer.layout) if not invoice.has_invoice_number() or sale.invoice_number: print_sale_invoice(invoice, printer) else: store = api.new_store() retval = self.run_dialog(SaleInvoicePrinterDialog, store, store.fetch(sale), printer) store.confirm(retval) store.close() def _setup_columns(self, sale_status=Sale.STATUS_CONFIRMED): self._status_col.visible = False if sale_status is None: # When there is no filter for sale status, show the # 'date started' column by default sale_status = Sale.STATUS_INITIAL self._status_col.visible = True if self._visible_date_col: self._visible_date_col.visible = False col = self.search.get_column_by_attribute(self.cols_info[sale_status]) if col is not None: self._visible_date_col = col col.visible = True self.results.set_columns(self.search.columns) # Adding summary label again and make it properly aligned with the # new columns setup self._create_summary_label() def _get_status_values(self): items = [(value, key) for key, value in Sale.statuses.items()] items.insert(0, (_('Any'), None)) return items def _get_filter_options(self): options = [ (_('All Sales'), None), (_('Confirmed today'), FilterItem('custom', 'sold-today')), (_('Confirmed in the last 7 days'), FilterItem('custom', 'sold-7days')), (_('Confirmed in the last 28 days'), FilterItem('custom', 'sold-28days')), (_('Expired quotes'), FilterItem('custom', 'expired-quotes')), ('sep', None), ] for key, value in Sale.statuses.items(): options.append((value, FilterItem('status', key))) return options def _get_status_query(self, state): if state.value is None: # FIXME; We cannot return None here, otherwise, the query will have # a 'AND NULL', which will return nothing. return True if state.value.name == 'custom': self._setup_columns(None) return SALES_FILTERS[state.value.value] elif state.value.name == 'status': self._setup_columns(state.value.value) return SaleView.status == state.value.value raise AssertionError(state.value.name, state.value.value) def _new_sale_quote(self, wizard): if self.check_open_inventory(): warning(_("You cannot create a quote with an open inventory.")) return store = api.new_store() model = self.run_dialog(wizard, store) store.confirm(model) store.close() if model: self.refresh() def _search_product(self): hide_cost_column = not api.sysparam.get_bool( 'SHOW_COST_COLUMN_IN_SALES') self.run_dialog(ProductSearch, self.store, hide_footer=True, hide_toolbar=True, hide_cost_column=hide_cost_column) def _change_sale_client(self): sale_view = self.results.get_selected() with api.new_store() as store: sale = store.fetch(sale_view.sale) self.run_dialog(SaleClientEditor, store=store, model=sale) if store.committed: self.refresh() def _change_salesperson(self): sale_view = self.results.get_selected() with api.new_store() as store: sale = store.fetch(sale_view.sale) self.run_dialog(SalesPersonEditor, store=store, model=sale) if store.committed: self.refresh() # # Kiwi callbacks # def _on_sale_toolbar__sale_returned(self, toolbar, sale): self.refresh() def _on_sale_toolbar__sale_edited(self, toolbar, sale): self.refresh() def on_results__selection_changed(self, results, sale): self._update_toolbar() def on_results__has_rows(self, results, has_rows): self._update_toolbar() def on_results__right_click(self, results, result, event): self.popup.popup(None, None, None, event.button, event.time) # Sales def on_SaleQuote__activate(self, action): self._new_sale_quote(wizard=SaleQuoteWizard) def on_WorkOrderQuote__activate(self, action): self._new_sale_quote(wizard=WorkOrderQuoteWizard) def on_SalesCancel__activate(self, action): sale_view = self.results.get_selected() can_cancel = api.sysparam.get_bool('ALLOW_CANCEL_LAST_COUPON') # A plugin (e.g. ECF) can avoid the cancelation of a sale # because it wants it to be cancelled using another way if can_cancel and SaleAvoidCancelEvent.emit(sale_view.sale): return store = api.new_store() sale = store.fetch(sale_view.sale) msg_text = _(u"This will cancel the sale, Are you sure?") model = SaleComment(store=store, sale=sale, author=api.get_current_user(store)) retval = self.run_dialog(NoteEditor, store, model=model, attr_name='comment', message_text=msg_text, label_text=_(u"Reason"), mandatory=True, ok_button_label=_(u"Cancel sale"), cancel_button_label=_(u"Don't cancel")) if not retval: store.rollback() return sale.cancel() store.commit(close=True) self.refresh() def on_ChangeClient__activate(self, action): self._change_sale_client() def on_ChangeSalesperson__activate(self, action): self._change_salesperson() def on_SalesPrintInvoice__activate(self, action): return self._print_invoice() # Loan def on_LoanNew__activate(self, action): if self.check_open_inventory(): return store = api.new_store() model = self.run_dialog(NewLoanWizard, store) store.confirm(model) store.close() def on_LoanClose__activate(self, action): if self.check_open_inventory(): return store = api.new_store() model = self.run_dialog(CloseLoanWizard, store) store.confirm(model) store.close() def on_LoanSearch__activate(self, action): self.run_dialog(LoanSearch, self.store) def on_LoanSearchItems__activate(self, action): self.run_dialog(LoanItemSearch, self.store) def on_ReturnedSaleSearch__activate(self, action): self.run_dialog(ReturnedSaleSearch, self.store) def on_SearchUnconfirmedSaleItems__activate(self, action): self.run_dialog(UnconfirmedSaleItemsSearch, self.store) # Search def on_SearchClient__activate(self, button): self.run_dialog(ClientSearch, self.store, hide_footer=True) def on_SearchProduct__activate(self, button): self._search_product() def on_SearchCommission__activate(self, button): self.run_dialog(CommissionSearch, self.store) def on_SearchClientCalls__activate(self, action): self.run_dialog(ClientCallsSearch, self.store) def on_SearchCreditCheckHistory__activate(self, action): self.run_dialog(CreditCheckHistorySearch, self.store) def on_SearchService__activate(self, button): self.run_dialog(ServiceSearch, self.store, hide_toolbar=True) def on_SearchSoldItemsByBranch__activate(self, button): self.run_dialog(SoldItemsByBranchSearch, self.store) def on_SearchSalesByPaymentMethod__activate(self, button): self.run_dialog(SalesByPaymentMethodSearch, self.store) def on_SearchDelivery__activate(self, action): self.run_dialog(DeliverySearch, self.store) def on_SearchSalesPersonSales__activate(self, action): self.run_dialog(SalesPersonSalesSearch, self.store) def on_SearchClientsWithSale__activate(self, action): self.run_dialog(ClientsWithSaleSearch, self.store) def on_SearchClientsWithCredit__activate(self, action): self.run_dialog(ClientsWithCreditSearch, self.store) def on_SearchSoldItemsByClient__activate(self, action): self.run_dialog(SoldItemsByClientSearch, self.store) # Toolbar def on_Edit__activate(self, action): self.sale_toolbar.edit() def on_Details__activate(self, action): self.sale_toolbar.show_details() def on_Return__activate(self, action): if self.check_open_inventory(): return self.sale_toolbar.return_sale() # Sale toobar def on_sale_toolbar__sale_edited(self, widget, sale): self.refresh() def on_sale_toolbar__sale_returned(self, widget, sale): self.refresh()
class SalesApp(ShellApp): app_title = _('Sales') gladefile = 'sales_app' search_spec = SaleView search_label = _('matching:') report_table = SalesReport cols_info = {Sale.STATUS_INITIAL: 'open_date', Sale.STATUS_CONFIRMED: 'confirm_date', Sale.STATUS_PAID: 'close_date', Sale.STATUS_ORDERED: 'open_date', Sale.STATUS_CANCELLED: 'cancel_date', Sale.STATUS_QUOTE: 'open_date', Sale.STATUS_RETURNED: 'return_date', Sale.STATUS_RENEGOTIATED: 'close_date'} action_permissions = { "SalesPrintInvoice": ('app.sales.print_invoice', PermissionManager.PERM_SEARCH), } def __init__(self, window, store=None): self.summary_label = None self._visible_date_col = None ShellApp.__init__(self, window, store=store) # # Application # def create_actions(self): group = get_accels('app.sales') actions = [ # File ("SaleQuote", None, _("Sale quote..."), '', _('Create a new quote for a sale')), ("WorkOrderQuote", None, _("Sale with work order..."), '', _('Create a new quote for a sale with work orders')), ("LoanNew", None, _("Loan...")), ("LoanClose", None, _("Close loan...")), # Search ("SearchSoldItemsByBranch", None, _("Sold items by branch..."), group.get("search_sold_items_by_branch"), _("Search for sold items by branch")), ("SearchSalesByPaymentMethod", None, _("Sales by payment method..."), group.get("search_sales_by_payment")), ("SearchSalesPersonSales", None, _("Total sales made by salesperson..."), group.get("search_salesperson_sales"), _("Search for sales by payment method")), ("SearchProduct", STOQ_PRODUCTS, _("Products..."), group.get("search_products"), _("Search for products")), ("SearchService", STOQ_SERVICES, _("Services..."), group.get("search_services"), _("Search for services")), ("SearchDelivery", STOQ_DELIVERY, _("Deliveries..."), group.get("search_deliveries"), _("Search for deliveries")), ("SearchClient", STOQ_CLIENTS, _("Clients..."), group.get("search_clients"), _("Search for clients")), ("SearchClientCalls", None, _("Client Calls..."), group.get("search_client_calls"), _("Search for client calls")), ("SearchCreditCheckHistory", None, _("Client credit check history..."), group.get("search_credit_check_history"), _("Search for client check history")), ("SearchCommission", None, _("Commissions..."), group.get("search_commissions"), _("Search for salespersons commissions")), ("LoanSearch", None, _("Loans..."), group.get("search_loans")), ("LoanSearchItems", None, _("Loan items..."), group.get("search_loan_items")), ("ReturnedSaleSearch", None, _("Returned sales..."), group.get("returned_sales")), ("SearchUnconfirmedSaleItems", None, _("Unconfirmed sale items..."), group.get("search_reserved_product"), _("Search for unconfirmed sale items")), ("SearchClientsWithSale", None, _("Clients with sales..."), None, _("Search for regular clients")), ("SearchClientsWithCredit", None, _("Clients with credit..."), None, _("Search for clients that have credit")), ("SearchSoldItemsByClient", None, _("Sold items by client..."), None, _("Search for products sold by client")), # Sale ("SaleMenu", None, _("Sale")), ("SalesCancel", None, _("Cancel...")), ("ChangeClient", gtk.STOCK_EDIT, _("Change client...")), ("SalesPrintInvoice", gtk.STOCK_PRINT, _("_Print invoice...")), ("Return", gtk.STOCK_CANCEL, _("Return..."), '', _("Return the selected sale, canceling it's payments")), ("Edit", gtk.STOCK_EDIT, _("Edit..."), '', _("Edit the selected sale, allowing you to change the details " "of it")), ("Details", gtk.STOCK_INFO, _("Details..."), '', _("Show details of the selected sale")) ] self.sales_ui = self.add_ui_actions("", actions, filename="sales.xml") self.SaleQuote.set_short_label(_("New Sale Quote")) self.SaleQuote.set_short_label(_("New Sale Quote with Work Order")) self.SearchClient.set_short_label(_("Clients")) self.SearchProduct.set_short_label(_("Products")) self.SearchService.set_short_label(_("Services")) self.SearchDelivery.set_short_label(_("Deliveries")) self.SalesCancel.set_short_label(_("Cancel")) self.ChangeClient.set_short_label(_("Change Client")) self.Edit.set_short_label(_("Edit")) self.Return.set_short_label(_("Return")) self.Details.set_short_label(_("Details")) self.set_help_section(_("Sales help"), 'app-sales') def create_ui(self): if api.sysparam.get_bool('SMART_LIST_LOADING'): self.search.enable_lazy_search() if not api.sysparam.get_bool('CHANGE_CLIENT_AFTER_CONFIRMED'): self.ChangeClient.set_visible(False) self.popup = self.uimanager.get_widget('/SaleSelection') self._setup_columns() self._setup_widgets() self.window.add_new_items([self.SaleQuote, self.WorkOrderQuote]) self.window.add_search_items([ self.SearchProduct, self.SearchClient, self.SearchService, self.SearchDelivery]) self.window.Print.set_tooltip(_("Print a report of these sales")) def activate(self, refresh=True): if refresh: self.refresh() self.check_open_inventory() self._update_toolbar() def setup_focus(self): self.refresh() def deactivate(self): self.uimanager.remove_ui(self.sales_ui) def new_activate(self): self._new_sale_quote(wizard=SaleQuoteWizard) def search_activate(self): self._search_product() def set_open_inventory(self): self.set_sensitive(self._inventory_widgets, False) def create_filters(self): self.set_text_field_columns(['client_name', 'salesperson_name', 'identifier_str']) status_filter = ComboSearchFilter(_('Show sales'), self._get_filter_options()) status_filter.combo.set_row_separator_func( lambda model, titer: model[titer][0] == 'sep') executer = self.search.get_query_executer() executer.add_filter_query_callback( status_filter, self._get_status_query) self.add_filter(status_filter, position=SearchFilterPosition.TOP) self.create_branch_filter(column=Sale.branch_id) def get_columns(self): self._status_col = SearchColumn('status_name', title=_('Status'), data_type=str, width=80, visible=False, search_attribute='status', valid_values=self._get_status_values()) cols = [IdentifierColumn('identifier', long_title=_('Order #'), sorted=True), SearchColumn('open_date', title=_('Open date'), width=120, data_type=date, justify=gtk.JUSTIFY_RIGHT, visible=False), SearchColumn('close_date', title=_('Close date'), width=120, data_type=date, justify=gtk.JUSTIFY_RIGHT, visible=False), SearchColumn('confirm_date', title=_('Confirm date'), data_type=date, justify=gtk.JUSTIFY_RIGHT, visible=False, width=120), SearchColumn('cancel_date', title=_('Cancel date'), width=120, data_type=date, justify=gtk.JUSTIFY_RIGHT, visible=False), SearchColumn('return_date', title=_('Return date'), width=120, data_type=date, justify=gtk.JUSTIFY_RIGHT, visible=False), SearchColumn('expire_date', title=_('Expire date'), width=120, data_type=date, justify=gtk.JUSTIFY_RIGHT, visible=False), self._status_col, SearchColumn('client_name', title=_('Client'), data_type=str, width=140, expand=True, ellipsize=pango.ELLIPSIZE_END), SearchColumn('salesperson_name', title=_('Salesperson'), data_type=str, width=130, ellipsize=pango.ELLIPSIZE_END), SearchColumn('total_quantity', title=_('Items'), data_type=decimal.Decimal, width=60, format_func=format_quantity), SearchColumn('total', title=_('Total'), data_type=currency, width=120)] return cols # # Private # def _create_summary_label(self): self.search.set_summary_label(column='total', label='<b>Total:</b>', format='<b>%s</b>', parent=self.get_statusbar_message_area()) def _setup_widgets(self): self._setup_slaves() self._inventory_widgets = [self.sale_toolbar.return_sale_button, self.Return, self.LoanNew, self.LoanClose] self.register_sensitive_group(self._inventory_widgets, lambda: not self.has_open_inventory()) def _setup_slaves(self): # This is only here to reuse the logic in it. self.sale_toolbar = SaleListToolbar(self.store, self.results, parent=self) def _update_toolbar(self, *args): sale_view = self.results.get_selected() # FIXME: Disable invoice printing if the sale was returned. Remove this # when we add proper support for returned sales invoice. can_print_invoice = bool(sale_view and sale_view.client_name is not None and sale_view.status != Sale.STATUS_RETURNED) self.set_sensitive([self.SalesPrintInvoice], can_print_invoice) self.set_sensitive([self.SalesCancel], bool(sale_view and sale_view.can_cancel())) self.set_sensitive([self.sale_toolbar.return_sale_button, self.Return], bool(sale_view and sale_view.can_return())) self.set_sensitive([self.sale_toolbar.return_sale_button, self.Details], bool(sale_view)) self.set_sensitive([self.sale_toolbar.edit_button, self.Edit], bool(sale_view and sale_view.can_edit())) # If the sale cannot be edit anymore, we only allow to change the client self.set_sensitive([self.ChangeClient], bool(sale_view and not sale_view.can_edit())) self.sale_toolbar.set_report_filters(self.search.get_search_filters()) def _print_invoice(self): sale_view = self.results.get_selected() assert sale_view sale = sale_view.sale station = api.get_current_station(self.store) printer = InvoicePrinter.get_by_station(station, self.store) if printer is None: info(_("There are no invoice printer configured for this station")) return assert printer.layout invoice = SaleInvoice(sale, printer.layout) if not invoice.has_invoice_number() or sale.invoice_number: print_sale_invoice(invoice, printer) else: store = api.new_store() retval = self.run_dialog(SaleInvoicePrinterDialog, store, store.fetch(sale), printer) store.confirm(retval) store.close() def _setup_columns(self, sale_status=Sale.STATUS_CONFIRMED): self._status_col.visible = False if sale_status is None: # When there is no filter for sale status, show the # 'date started' column by default sale_status = Sale.STATUS_INITIAL self._status_col.visible = True if self._visible_date_col: self._visible_date_col.visible = False col = self.search.get_column_by_attribute(self.cols_info[sale_status]) if col is not None: self._visible_date_col = col col.visible = True self.results.set_columns(self.search.columns) # Adding summary label again and make it properly aligned with the # new columns setup self._create_summary_label() def _get_status_values(self): items = [(value, key) for key, value in Sale.statuses.items()] items.insert(0, (_('Any'), None)) return items def _get_filter_options(self): options = [ (_('All Sales'), None), (_('Sold'), FilterItem('custom', 'sold')), (_('Sold today'), FilterItem('custom', 'sold-today')), (_('Sold in the last 7 days'), FilterItem('custom', 'sold-7days')), (_('Sold in the last 28 days'), FilterItem('custom', 'sold-28days')), (_('Expired quotes'), FilterItem('custom', 'expired-quotes')), ('sep', None), ] for key, value in Sale.statuses.items(): options.append((value, FilterItem('status', key))) return options def _get_status_query(self, state): if state.value is None: # FIXME; We cannot return None here, otherwise, the query will have # a 'AND NULL', which will return nothing. return True if state.value.name == 'custom': self._setup_columns(None) return SALES_FILTERS[state.value.value] elif state.value.name == 'status': self._setup_columns(state.value.value) return SaleView.status == state.value.value raise AssertionError(state.value.name, state.value.value) def _new_sale_quote(self, wizard): if self.check_open_inventory(): warning(_("You cannot create a quote with an open inventory.")) return store = api.new_store() model = self.run_dialog(wizard, store) store.confirm(model) store.close() if model: self.refresh() def _search_product(self): hide_cost_column = not api.sysparam.get_bool('SHOW_COST_COLUMN_IN_SALES') self.run_dialog(ProductSearch, self.store, hide_footer=True, hide_toolbar=True, hide_cost_column=hide_cost_column) def _change_sale_client(self): sale_view = self.results.get_selected() with api.new_store() as store: sale = store.fetch(sale_view.sale) self.run_dialog(SaleClientEditor, store=store, model=sale) if store.committed: self.refresh() # # Kiwi callbacks # def _on_sale_toolbar__sale_returned(self, toolbar, sale): self.refresh() def _on_sale_toolbar__sale_edited(self, toolbar, sale): self.refresh() def on_results__selection_changed(self, results, sale): self._update_toolbar() def on_results__has_rows(self, results, has_rows): self._update_toolbar() def on_results__right_click(self, results, result, event): self.popup.popup(None, None, None, event.button, event.time) # Sales def on_SaleQuote__activate(self, action): self._new_sale_quote(wizard=SaleQuoteWizard) def on_WorkOrderQuote__activate(self, action): self._new_sale_quote(wizard=WorkOrderQuoteWizard) def on_SalesCancel__activate(self, action): sale_view = self.results.get_selected() can_cancel = api.sysparam.get_bool('ALLOW_CANCEL_LAST_COUPON') if can_cancel and ECFIsLastSaleEvent.emit(sale_view.sale): info(_("That is last sale in ECF. Return using the menu " "ECF - Cancel Last Document")) return store = api.new_store() sale = store.fetch(sale_view.sale) msg_text = _(u"This will cancel the sale, Are you sure?") model = SaleComment(store=store, sale=sale, author=api.get_current_user(store)) retval = self.run_dialog( NoteEditor, store, model=model, attr_name='comment', message_text=msg_text, label_text=_(u"Reason"), mandatory=True, ok_button_label=_(u"Cancel sale"), cancel_button_label=_(u"Don't cancel")) if not retval: store.rollback() return sale.cancel() store.commit(close=True) self.refresh() def on_ChangeClient__activate(self, action): self._change_sale_client() def on_SalesPrintInvoice__activate(self, action): return self._print_invoice() # Loan def on_LoanNew__activate(self, action): if self.check_open_inventory(): return store = api.new_store() model = self.run_dialog(NewLoanWizard, store) store.confirm(model) store.close() def on_LoanClose__activate(self, action): if self.check_open_inventory(): return store = api.new_store() model = self.run_dialog(CloseLoanWizard, store) store.confirm(model) store.close() def on_LoanSearch__activate(self, action): self.run_dialog(LoanSearch, self.store) def on_LoanSearchItems__activate(self, action): self.run_dialog(LoanItemSearch, self.store) def on_ReturnedSaleSearch__activate(self, action): self.run_dialog(ReturnedSaleSearch, self.store) def on_SearchUnconfirmedSaleItems__activate(self, action): self.run_dialog(UnconfirmedSaleItemsSearch, self.store) # Search def on_SearchClient__activate(self, button): self.run_dialog(ClientSearch, self.store, hide_footer=True) def on_SearchProduct__activate(self, button): self._search_product() def on_SearchCommission__activate(self, button): self.run_dialog(CommissionSearch, self.store) def on_SearchClientCalls__activate(self, action): self.run_dialog(ClientCallsSearch, self.store) def on_SearchCreditCheckHistory__activate(self, action): self.run_dialog(CreditCheckHistorySearch, self.store) def on_SearchService__activate(self, button): self.run_dialog(ServiceSearch, self.store, hide_toolbar=True) def on_SearchSoldItemsByBranch__activate(self, button): self.run_dialog(SoldItemsByBranchSearch, self.store) def on_SearchSalesByPaymentMethod__activate(self, button): self.run_dialog(SalesByPaymentMethodSearch, self.store) def on_SearchDelivery__activate(self, action): self.run_dialog(DeliverySearch, self.store) def on_SearchSalesPersonSales__activate(self, action): self.run_dialog(SalesPersonSalesSearch, self.store) def on_SearchClientsWithSale__activate(self, action): self.run_dialog(ClientsWithSaleSearch, self.store) def on_SearchClientsWithCredit__activate(self, action): self.run_dialog(ClientsWithCreditSearch, self.store) def on_SearchSoldItemsByClient__activate(self, action): self.run_dialog(SoldItemsByClientSearch, self.store) # Toolbar def on_Edit__activate(self, action): self.sale_toolbar.edit() def on_Details__activate(self, action): self.sale_toolbar.show_details() def on_Return__activate(self, action): if self.check_open_inventory(): return self.sale_toolbar.return_sale() # Sale toobar def on_sale_toolbar__sale_edited(self, widget, sale): self.refresh() def on_sale_toolbar__sale_returned(self, widget, sale): self.refresh()
class SalesApp(ShellApp): app_title = _('Sales') gladefile = 'sales_app' search_table = SaleView search_label = _('matching:') report_table = SalesReport cols_info = { Sale.STATUS_INITIAL: 'open_date', Sale.STATUS_CONFIRMED: 'confirm_date', Sale.STATUS_PAID: 'close_date', Sale.STATUS_CANCELLED: 'cancel_date', Sale.STATUS_QUOTE: 'open_date', Sale.STATUS_RETURNED: 'return_date', Sale.STATUS_RENEGOTIATED: 'close_date' } action_permissions = { "SalesPrintInvoice": ('app.sales.print_invoice', PermissionManager.PERM_SEARCH), } def __init__(self, window, store=None): self.summary_label = None self._visible_date_col = None ShellApp.__init__(self, window, store=store) # # Application # def create_actions(self): group = get_accels('app.sales') actions = [ # File ("SaleQuote", None, _("Sale quote..."), '', _('Create a new quote for a sale')), ("LoanNew", None, _("Loan...")), ("LoanClose", None, _("Close loan...")), # Search ("SearchSoldItemsByBranch", None, _("Sold items by branch..."), group.get("search_sold_items_by_branch"), _("Search for sold items by branch")), ("SearchSalesByPaymentMethod", None, _("Sales by payment method..."), group.get("search_sales_by_payment")), ("SearchSalesPersonSales", None, _("Total sales made by salesperson..."), group.get("search_salesperson_sales"), _("Search for sales by payment method")), ("SearchProduct", STOQ_PRODUCTS, _("Products..."), group.get("search_products"), _("Search for products")), ("SearchService", STOQ_SERVICES, _("Services..."), group.get("search_services"), _("Search for services")), ("SearchDelivery", STOQ_DELIVERY, _("Deliveries..."), group.get("search_deliveries"), _("Search for deliveries")), ("SearchClient", STOQ_CLIENTS, _("Clients..."), group.get("search_clients"), _("Search for clients")), ("SearchClientCalls", None, _("Client Calls..."), group.get("search_client_calls"), _("Search for client calls")), ("SearchCreditCheckHistory", None, _("Client credit check history..."), group.get("search_credit_check_history"), _("Search for client check history")), ("SearchCommission", None, _("Commissions..."), group.get("search_commissions"), _("Search for salespersons commissions")), ("LoanSearch", None, _("Loans..."), group.get("search_loans")), ("LoanSearchItems", None, _("Loan items..."), group.get("search_loan_items")), # Sale ("SaleMenu", None, _("Sale")), ("SalesCancel", None, _("Cancel quote...")), ("SalesPrintInvoice", gtk.STOCK_PRINT, _("_Print invoice...")), ("Return", gtk.STOCK_CANCEL, _("Return..."), '', _("Return the selected sale, canceling it's payments")), ("Edit", gtk.STOCK_EDIT, _("Edit..."), '', _("Edit the selected sale, allowing you to change the details " "of it")), ("Details", gtk.STOCK_INFO, _("Details..."), '', _("Show details of the selected sale")) ] self.sales_ui = self.add_ui_actions("", actions, filename="sales.xml") self.SaleQuote.set_short_label(_("New Sale Quote")) self.SearchClient.set_short_label(_("Clients")) self.SearchProduct.set_short_label(_("Products")) self.SearchService.set_short_label(_("Services")) self.SearchDelivery.set_short_label(_("Deliveries")) self.SalesCancel.set_short_label(_("Cancel")) self.Edit.set_short_label(_("Edit")) self.Return.set_short_label(_("Return")) self.Details.set_short_label(_("Details")) self.set_help_section(_("Sales help"), 'app-sales') def create_ui(self): if api.sysparam(self.store).SMART_LIST_LOADING: self.search.enable_lazy_search() self.popup = self.uimanager.get_widget('/SaleSelection') self._columns = self.get_columns() self._setup_columns() self._setup_widgets() self.window.add_new_items([self.SaleQuote]) self.window.add_search_items([ self.SearchProduct, self.SearchClient, self.SearchService, self.SearchDelivery ]) self.window.Print.set_tooltip(_("Print a report of these sales")) def activate(self, params): self.check_open_inventory() self._update_toolbar() def setup_focus(self): self.search.refresh() def deactivate(self): self.uimanager.remove_ui(self.sales_ui) def new_activate(self): self._new_sale_quote() def search_activate(self): self._search_product() def set_open_inventory(self): self.set_sensitive(self._inventory_widgets, False) def create_filters(self): self.executer.set_query(self._query) self.set_text_field_columns(['client_name', 'salesperson_name']) status_filter = ComboSearchFilter(_('Show sales'), self._get_filter_options()) status_filter.combo.set_row_separator_func( lambda model, titer: model[titer][0] == 'sep') self.executer.add_filter_query_callback(status_filter, self._get_status_query) self.add_filter(status_filter, position=SearchFilterPosition.TOP) def get_columns(self): self._status_col = SearchColumn('status_name', title=_('Status'), data_type=str, width=80, visible=False, search_attribute='status', valid_values=self._get_status_values()) cols = [ IdentifierColumn('identifier', long_title=_('Order #'), sorted=True), SearchColumn('open_date', title=_('Open date'), width=120, data_type=date, justify=gtk.JUSTIFY_RIGHT, visible=False), SearchColumn('close_date', title=_('Close date'), width=120, data_type=date, justify=gtk.JUSTIFY_RIGHT, visible=False), SearchColumn('confirm_date', title=_('Confirm date'), data_type=date, justify=gtk.JUSTIFY_RIGHT, visible=False, width=120), SearchColumn('cancel_date', title=_('Cancel date'), width=120, data_type=date, justify=gtk.JUSTIFY_RIGHT, visible=False), SearchColumn('return_date', title=_('Return date'), width=120, data_type=date, justify=gtk.JUSTIFY_RIGHT, visible=False), SearchColumn('expire_date', title=_('Expire date'), width=120, data_type=date, justify=gtk.JUSTIFY_RIGHT, visible=False), self._status_col, SearchColumn('client_name', title=_('Client'), data_type=str, width=140, expand=True, ellipsize=pango.ELLIPSIZE_END), SearchColumn('salesperson_name', title=_('Salesperson'), data_type=str, width=130, ellipsize=pango.ELLIPSIZE_END), SearchColumn('total_quantity', title=_('Items'), data_type=decimal.Decimal, width=60, format_func=format_quantity), SearchColumn('total', title=_('Total'), data_type=currency, width=120) ] return cols # # Private # def _create_summary_label(self): self.search.set_summary_label(column='total', label='<b>Total:</b>', format='<b>%s</b>', parent=self.get_statusbar_message_area()) def _setup_widgets(self): self._setup_slaves() self._inventory_widgets = [ self.sale_toolbar.return_sale_button, self.Return, self.LoanNew, self.LoanClose ] self.register_sensitive_group(self._inventory_widgets, lambda: not self.has_open_inventory()) def _setup_slaves(self): # This is only here to reuse the logic in it. self.sale_toolbar = SaleListToolbar(self.store, self.results, parent=self) def _can_cancel(self, view): # Here we want to cancel only quoting sales. This is why we don't use # Sale.can_cancel here. return bool(view and view.status == Sale.STATUS_QUOTE) def _can_edit(self, view): return bool(view and view.status == Sale.STATUS_QUOTE) def _update_toolbar(self, *args): sale_view = self.results.get_selected() # FIXME: Disable invoice printing if the sale was returned. Remove this # when we add proper support for returned sales invoice. can_print_invoice = bool(sale_view and sale_view.client_name is not None and sale_view.status != Sale.STATUS_RETURNED) self.set_sensitive([self.SalesPrintInvoice], can_print_invoice) self.set_sensitive([self.SalesCancel], self._can_cancel(sale_view)) self.set_sensitive([self.sale_toolbar.return_sale_button, self.Return], bool(sale_view and sale_view.can_return())) self.set_sensitive( [self.sale_toolbar.return_sale_button, self.Details], bool(sale_view)) self.set_sensitive([self.sale_toolbar.edit_button, self.Edit], self._can_edit(sale_view)) self.sale_toolbar.set_report_filters(self.search.get_search_filters()) def _print_invoice(self): sale_view = self.results.get_selected() assert sale_view sale = sale_view.sale station = api.get_current_station(self.store) printer = InvoicePrinter.get_by_station(station, self.store) if printer is None: info(_("There are no invoice printer configured for this station")) return assert printer.layout invoice = SaleInvoice(sale, printer.layout) if not invoice.has_invoice_number() or sale.invoice_number: print_sale_invoice(invoice, printer) else: store = api.new_store() retval = self.run_dialog(SaleInvoicePrinterDialog, store, store.fetch(sale), printer) store.confirm(retval) store.close() def _setup_columns(self, sale_status=Sale.STATUS_CONFIRMED): self._status_col.visible = False if sale_status is None: # When there is no filter for sale status, show the # 'date started' column by default sale_status = Sale.STATUS_INITIAL self._status_col.visible = True if self._visible_date_col: self._visible_date_col.visible = False for col in self._columns: if col.attribute == self.cols_info[sale_status]: self._visible_date_col = col col.visible = True break self.results.set_columns(self._columns) # Adding summary label again and make it properly aligned with the # new columns setup self._create_summary_label() def _get_status_values(self): items = [ (value, key) for key, value in Sale.statuses.items() # No reason to show orders in sales app if key != Sale.STATUS_ORDERED ] items.insert(0, (_('Any'), None)) return items def _get_filter_options(self): options = [ (_('All Sales'), None), (_('Sold today'), FilterItem('custom', 'sold-today')), (_('Sold in the last 7 days'), FilterItem('custom', 'sold-7days')), (_('Sold in the last 28 days'), FilterItem('custom', 'sold-28days')), (_('Expired quotes'), FilterItem('custom', 'expired-quotes')), ('sep', None), ] for key, value in Sale.statuses.items(): if key == Sale.STATUS_ORDERED: continue options.append((value, FilterItem('status', key))) return options def _get_status_query(self, state): if state.value is None: return SaleView.status != Sale.STATUS_ORDERED if state.value.name == 'custom': self._setup_columns(None) return SALES_FILTERS[state.value.value] elif state.value.name == 'status': self._setup_columns(state.value.value) return SaleView.status == state.value.value raise AssertionError(state.value.name, state.value.value) def _query(self, store): branch = api.get_current_branch(self.store) return self.search_table.find_by_branch(store, branch) def _new_sale_quote(self): store = api.new_store() model = self.run_dialog(SaleQuoteWizard, store) store.confirm(model) store.close() def _search_product(self): hide_cost_column = not api.sysparam( self.store).SHOW_COST_COLUMN_IN_SALES self.run_dialog(ProductSearch, self.store, hide_footer=True, hide_toolbar=True, hide_cost_column=hide_cost_column) # # Kiwi callbacks # def _on_sale_toolbar__sale_returned(self, toolbar, sale): self.search.refresh() def _on_sale_toolbar__sale_edited(self, toolbar, sale): self.search.refresh() def on_results__selection_changed(self, results, sale): self._update_toolbar() def on_results__has_rows(self, results, has_rows): self._update_toolbar() def on_results__right_click(self, results, result, event): self.popup.popup(None, None, None, event.button, event.time) # Sales def on_SaleQuote__activate(self, action): self._new_sale_quote() def on_SalesCancel__activate(self, action): if not yesno(_('This will cancel the selected quote. Are you sure?'), gtk.RESPONSE_NO, _("Cancel quote"), _("Don't cancel")): return store = api.new_store() sale_view = self.results.get_selected() sale = store.fetch(sale_view.sale) sale.cancel() store.confirm(True) store.close() self.search.refresh() def on_SalesPrintInvoice__activate(self, action): return self._print_invoice() # Loan def on_LoanNew__activate(self, action): if self.check_open_inventory(): return store = api.new_store() model = self.run_dialog(NewLoanWizard, store) store.confirm(model) store.close() def on_LoanClose__activate(self, action): if self.check_open_inventory(): return store = api.new_store() model = self.run_dialog(CloseLoanWizard, store) store.confirm(model) store.close() def on_LoanSearch__activate(self, action): self.run_dialog(LoanSearch, self.store) def on_LoanSearchItems__activate(self, action): self.run_dialog(LoanItemSearch, self.store) # Search def on_SearchClient__activate(self, button): self.run_dialog(ClientSearch, self.store, hide_footer=True) def on_SearchProduct__activate(self, button): self._search_product() def on_SearchCommission__activate(self, button): self.run_dialog(CommissionSearch, self.store) def on_SearchClientCalls__activate(self, action): self.run_dialog(ClientCallsSearch, self.store) def on_SearchCreditCheckHistory__activate(self, action): self.run_dialog(CreditCheckHistorySearch, self.store) def on_SearchService__activate(self, button): self.run_dialog(ServiceSearch, self.store, hide_toolbar=True) def on_SearchSoldItemsByBranch__activate(self, button): self.run_dialog(SoldItemsByBranchSearch, self.store) def on_SearchSalesByPaymentMethod__activate(self, button): self.run_dialog(SalesByPaymentMethodSearch, self.store) def on_SearchDelivery__activate(self, action): self.run_dialog(DeliverySearch, self.store) def on_SearchSalesPersonSales__activate(self, action): self.run_dialog(SalesPersonSalesSearch, self.store) # Toolbar def on_Edit__activate(self, action): self.sale_toolbar.edit() def on_Details__activate(self, action): self.sale_toolbar.show_details() def on_Return__activate(self, action): if self.check_open_inventory(): return self.sale_toolbar.return_sale()