diff --git a/wuttapos/controls/custlookup.py b/wuttapos/controls/custlookup.py index c35263c..c99ae11 100644 --- a/wuttapos/controls/custlookup.py +++ b/wuttapos/controls/custlookup.py @@ -24,219 +24,26 @@ WuttaPOS - customer lookup control """ -import flet as ft - -from .base import WuttaControl -from .keyboard import WuttaKeyboard +from .lookup import WuttaLookup -class WuttaCustomerLookup(WuttaControl): +class WuttaCustomerLookup(WuttaLookup): - default_font_size = 40 - font_size = default_font_size * 0.8 - default_button_height_dlg = 80 - disabled_bgcolor = '#aaaaaa' + def get_results_columns(self): + return [ + self.app.get_customer_key_label(), + "Name", + "Phone", + "Email", + ] - def __init__(self, *args, **kwargs): - self.initial_search = kwargs.pop('initial_search', None) - self.on_customer = kwargs.pop('on_customer', None) - self.on_cancel = kwargs.pop('on_cancel', None) - super().__init__(*args, **kwargs) + def get_results(self, session, entry): + return self.app.get_clientele_handler().search_customers(session, entry) - # track current selection - self.selected_customer_uuid = None - self.selected_control = None - - def build(self): - - self.searchbox = ft.TextField("", text_size=self.font_size * 0.8, - on_submit=self.lookup, - autofocus=True, - expand=True) - - self.search_results = ft.DataTable( - columns=[ - ft.DataColumn(self.make_cell_text(self.app.get_customer_key_label())), - ft.DataColumn(self.make_cell_text("Name")), - ft.DataColumn(self.make_cell_text("Phone")), - ft.DataColumn(self.make_cell_text("Email")), - ], - ) - - self.no_results = ft.Text("NO RESULTS", size=32, color='red', - weight=ft.FontWeight.BOLD, - visible=False) - - self.select_button = ft.Container( - content=ft.Text("Select", size=self.font_size * 0.8), - alignment=ft.alignment.center, - height=self.default_button_height_dlg * 0.8, - width=self.default_button_height_dlg * 1.3, - border=ft.border.all(1, 'black'), - border_radius=ft.border_radius.all(5), - on_click=self.select_customer, - disabled=True, - bgcolor=self.disabled_bgcolor, - ) - - return ft.Column( - [ - ft.Row( - [ - ft.Text("SEARCH FOR:"), - self.searchbox, - ft.Container( - content=ft.Text("Lookup", size=self.font_size * 0.8), - alignment=ft.alignment.center, - height=self.default_button_height_dlg * 0.8, - width=self.default_button_height_dlg * 1.3, - border=ft.border.all(1, 'black'), - border_radius=ft.border_radius.all(5), - on_click=self.lookup, - bgcolor='blue', - ), - ft.Container( - content=ft.Text("Reset", size=self.font_size * 0.8), - alignment=ft.alignment.center, - height=self.default_button_height_dlg * 0.8, - width=self.default_button_height_dlg * 1.3, - border=ft.border.all(1, 'black'), - border_radius=ft.border_radius.all(5), - on_click=self.reset, - bgcolor='yellow', - ), - ft.Container( - content=ft.Text("Cancel", size=self.font_size * 0.8), - alignment=ft.alignment.center, - height=self.default_button_height_dlg * 0.8, - width=self.default_button_height_dlg * 1.3, - border=ft.border.all(1, 'black'), - border_radius=ft.border_radius.all(5), - on_click=self.cancel, - ), - ], - ), - ft.Divider(), - WuttaKeyboard(self.config, on_keypress=self.keypress, - on_long_backspace=self.long_backspace), - ft.Divider(), - ft.Row( - [ - ft.Column( - [ - self.search_results, - self.no_results, - ], - expand=True, - ), - self.select_button, - ], - ), - ], - ) - - def did_mount(self): - if self.initial_search: - self.searchbox.value = self.initial_search - self.initial_search = None # only do it once - self.update() - self.lookup() - - def make_cell_text(self, text): - return ft.Text(text, size=32) - - def make_cell(self, text): - return ft.DataCell(self.make_cell_text(text)) - - def cancel(self, e): - if self.on_cancel: - self.on_cancel(e) - - def keypress(self, key): - if key == '⏎': - self.lookup() - else: - if key == '⌫': - self.searchbox.value = self.searchbox.value[:-1] - else: - self.searchbox.value += key - self.searchbox.focus() - self.update() - - def long_backspace(self): - self.searchbox.value = self.searchbox.value[:-10] - self.searchbox.focus() - self.update() - - def lookup(self, e=None): - entry = self.searchbox.value - if not entry: - self.searchbox.focus() - self.update() - return - - session = self.app.make_session() - results = self.app.get_clientele_handler().search_customers(session, entry) - - self.search_results.rows.clear() - self.selected_customer_uuid = None - self.select_button.disabled = True - self.select_button.bgcolor = self.disabled_bgcolor - - if results: - for customer in results: - self.search_results.rows.append(ft.DataRow( - cells=[ - self.make_cell(customer['_customer_key_']), - self.make_cell(customer['name']), - self.make_cell(customer['phone_number']), - self.make_cell(customer['email_address']), - ], - on_select_changed=self.select_changed, - data={'uuid': customer['uuid']}, - )) - self.no_results.visible = False - - else: - self.no_results.value = f"NO RESULTS FOR: {entry}" - self.no_results.visible = True - - self.searchbox.focus() - self.update() - - def reset(self, e): - self.searchbox.value = "" - self.search_results.rows.clear() - self.no_results.visible = False - self.selected_customer_uuid = None - self.select_button.disabled = True - self.select_button.bgcolor = self.disabled_bgcolor - self.searchbox.focus() - self.update() - - def select_changed(self, e): - - if e.data: # selected - if self.selected_control: - self.selected_control.color = None - self.selected_customer_uuid = e.control.data['uuid'] - self.selected_control = e.control - self.selected_control.color = ft.colors.BLUE - self.select_button.disabled = False - self.select_button.bgcolor = 'blue' - else: - if self.selected_control: - self.selected_control.color = None - self.selected_control = None - self.selected_customer_uuid = None - self.select_button.disabled = True - self.select_button.bgcolor = self.disabled_bgcolor - e.control.color = None - - self.update() - - def select_customer(self, e): - if not self.selected_customer_uuid: - raise RuntimeError("no customer selected?") - if self.on_customer: - self.on_customer(self.selected_customer_uuid) + def make_result_row(self, customer): + return [ + customer['_customer_key_'], + customer['name'], + customer['phone_number'], + customer['email_address'], + ] diff --git a/wuttapos/controls/itemlookup.py b/wuttapos/controls/itemlookup.py new file mode 100644 index 0000000..5bf7058 --- /dev/null +++ b/wuttapos/controls/itemlookup.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# WuttaPOS -- Pythonic Point of Sale System +# Copyright © 2023 Lance Edgar +# +# This file is part of WuttaPOS. +# +# WuttaPOS is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# WuttaPOS. If not, see . +# +################################################################################ +""" +WuttaPOS - item lookup control +""" + +from .lookup import WuttaLookup + + +class WuttaProductLookup(WuttaLookup): + + def get_results_columns(self): + return [ + self.app.get_product_key_label(), + "Description", + "Price", + ] + + def get_results(self, session, entry): + return self.app.get_products_handler().search_products(session, entry) + + def make_result_row(self, product): + return [ + product['product_key'], + product['full_description'], + product['unit_price_display'], + ] diff --git a/wuttapos/controls/lookup.py b/wuttapos/controls/lookup.py new file mode 100644 index 0000000..7f4c4a3 --- /dev/null +++ b/wuttapos/controls/lookup.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# WuttaPOS -- Pythonic Point of Sale System +# Copyright © 2023 Lance Edgar +# +# This file is part of WuttaPOS. +# +# WuttaPOS is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# WuttaPOS. If not, see . +# +################################################################################ +""" +WuttaPOS - base lookup control +""" + +import flet as ft + +from .base import WuttaControl +from .keyboard import WuttaKeyboard + + +class WuttaLookup(WuttaControl): + + default_font_size = 40 + font_size = default_font_size * 0.8 + default_button_height_dlg = 80 + disabled_bgcolor = '#aaaaaa' + + def __init__(self, *args, **kwargs): + self.initial_search = kwargs.pop('initial_search', None) + self.on_select = kwargs.pop('on_select', None) + self.on_cancel = kwargs.pop('on_cancel', None) + super().__init__(*args, **kwargs) + + # track current selection + self.selected_uuid = None + self.selected_control = None + + def build(self): + + self.searchbox = ft.TextField("", text_size=self.font_size * 0.8, + on_submit=self.lookup, + autofocus=True, + expand=True) + + self.search_results = ft.DataTable( + columns=[ft.DataColumn(self.make_cell_text(text)) + for text in self.get_results_columns()], + ) + + self.no_results = ft.Text("NO RESULTS", size=32, color='red', + weight=ft.FontWeight.BOLD, + visible=False) + + self.select_button = ft.Container( + content=ft.Text("Select", size=self.font_size * 0.8), + alignment=ft.alignment.center, + height=self.default_button_height_dlg * 0.8, + width=self.default_button_height_dlg * 1.3, + border=ft.border.all(1, 'black'), + border_radius=ft.border_radius.all(5), + on_click=self.select_object, + disabled=True, + bgcolor=self.disabled_bgcolor, + ) + + return ft.Column( + [ + ft.Row( + [ + ft.Text("SEARCH FOR:"), + self.searchbox, + ft.Container( + content=ft.Text("Lookup", size=self.font_size * 0.8), + alignment=ft.alignment.center, + height=self.default_button_height_dlg * 0.8, + width=self.default_button_height_dlg * 1.3, + border=ft.border.all(1, 'black'), + border_radius=ft.border_radius.all(5), + on_click=self.lookup, + bgcolor='blue', + ), + ft.Container( + content=ft.Text("Reset", size=self.font_size * 0.8), + alignment=ft.alignment.center, + height=self.default_button_height_dlg * 0.8, + width=self.default_button_height_dlg * 1.3, + border=ft.border.all(1, 'black'), + border_radius=ft.border_radius.all(5), + on_click=self.reset, + bgcolor='yellow', + ), + ft.Container( + content=ft.Text("Cancel", size=self.font_size * 0.8), + alignment=ft.alignment.center, + height=self.default_button_height_dlg * 0.8, + width=self.default_button_height_dlg * 1.3, + border=ft.border.all(1, 'black'), + border_radius=ft.border_radius.all(5), + on_click=self.cancel, + ), + ], + ), + ft.Divider(), + WuttaKeyboard(self.config, on_keypress=self.keypress, + on_long_backspace=self.long_backspace), + ft.Divider(), + ft.Row( + [ + ft.Column( + [ + self.search_results, + self.no_results, + ], + expand=True, + ), + self.select_button, + ], + ), + ], + ) + + def get_results_columns(self): + raise NotImplementedError + + def did_mount(self): + if self.initial_search: + self.searchbox.value = self.initial_search + self.initial_search = None # only do it once + self.update() + self.lookup() + + def make_cell_text(self, text): + return ft.Text(text, size=32) + + def make_cell(self, text): + return ft.DataCell(self.make_cell_text(text)) + + def cancel(self, e): + if self.on_cancel: + self.on_cancel(e) + + def keypress(self, key): + if key == '⏎': + self.lookup() + else: + if key == '⌫': + self.searchbox.value = self.searchbox.value[:-1] + else: + self.searchbox.value += key + self.searchbox.focus() + self.update() + + def long_backspace(self): + self.searchbox.value = self.searchbox.value[:-10] + self.searchbox.focus() + self.update() + + def get_results(self, session, entry): + raise NotImplementedError + + def make_result_row(self, obj): + raise NotImplementedError + + def lookup(self, e=None): + entry = self.searchbox.value + if not entry: + self.searchbox.focus() + self.update() + return + + session = self.app.make_session() + results = self.get_results(session, entry) + + self.search_results.rows.clear() + self.selected_uuid = None + self.select_button.disabled = True + self.select_button.bgcolor = self.disabled_bgcolor + + if results: + for obj in results: + self.search_results.rows.append(ft.DataRow( + cells=[self.make_cell(row) + for row in self.make_result_row(obj)], + on_select_changed=self.select_changed, + data={'uuid': obj['uuid']}, + )) + self.no_results.visible = False + + else: + self.no_results.value = f"NO RESULTS FOR: {entry}" + self.no_results.visible = True + + self.searchbox.focus() + self.update() + + def reset(self, e): + self.searchbox.value = "" + self.search_results.rows.clear() + self.no_results.visible = False + self.selected_uuid = None + self.select_button.disabled = True + self.select_button.bgcolor = self.disabled_bgcolor + self.searchbox.focus() + self.update() + + def select_changed(self, e): + + if e.data: # selected + if self.selected_control: + self.selected_control.color = None + self.selected_uuid = e.control.data['uuid'] + self.selected_control = e.control + self.selected_control.color = ft.colors.BLUE + self.select_button.disabled = False + self.select_button.bgcolor = 'blue' + else: + if self.selected_control: + self.selected_control.color = None + self.selected_control = None + self.selected_uuid = None + self.select_button.disabled = True + self.select_button.bgcolor = self.disabled_bgcolor + e.control.color = None + + self.update() + + def select_object(self, e): + if not self.selected_uuid: + raise RuntimeError("no record selected?") + if self.on_select: + self.on_select(self.selected_uuid) diff --git a/wuttapos/views/pos.py b/wuttapos/views/pos.py index 4a4098c..3c98308 100644 --- a/wuttapos/views/pos.py +++ b/wuttapos/views/pos.py @@ -32,6 +32,7 @@ import flet as ft from .base import WuttaView from wuttapos.controls.custlookup import WuttaCustomerLookup +from wuttapos.controls.itemlookup import WuttaProductLookup from wuttapos.util import get_pos_batch_handler @@ -66,7 +67,7 @@ class POSView(WuttaView): def get_batch_handler(self): return get_pos_batch_handler(self.config) - def reset(self, e=None, update=True): + def reset(self, e=None, clear_quantity=True, update=True): """ This is a convenience method, meant only to clear the main input and set focus to it. Will also update() the page @@ -75,8 +76,9 @@ class POSView(WuttaView): The ``e`` arg is ignored and accepted only so this method may be registered as an event handler, e.g. ``on_cancel``. """ - self.set_quantity.data = None - self.set_quantity.value = None + if clear_quantity: + self.set_quantity.data = None + self.set_quantity.value = None self.main_input.value = '' self.main_input.focus() if update: @@ -102,9 +104,78 @@ class POSView(WuttaView): duration=1500) self.page.snack_bar.open = True + def item_click(self, e): + + value = self.main_input.value + if value: + if not self.attempt_add_product(): + self.item_lookup(value) + + else: + self.item_lookup() + + def attempt_add_product(self, uuid=None): + session = self.app.make_session() + handler = self.get_batch_handler() + batch = self.get_current_batch(session) + entry = self.main_input.value + + kw = {} + if self.set_quantity.data is not None: + kw['quantity'] = self.set_quantity.data + + product = None + if uuid: + product = session.get(self.model.Product, uuid) + assert product + + row = handler.process_entry(batch, product or entry, **kw) + + if row: + session.commit() + self.add_row_item(row) + self.items.scroll_to(offset=-1, duration=250) + self.txn_total.value = self.app.render_currency(batch.sales_total) + self.reset() + + else: + self.page.snack_bar = ft.SnackBar(ft.Text(f"PRODUCT NOT FOUND: {entry}", + color='black', + weight=ft.FontWeight.BOLD), + bgcolor='yellow', + duration=1500) + self.page.snack_bar.open = True + + session.commit() + session.close() + self.page.update() + return bool(row) + + def item_lookup(self, value=None): + + def select(uuid): + self.attempt_add_product(uuid=uuid) + dlg.open = False + self.reset() + + def cancel(e): + dlg.open = False + self.reset(clear_quantity=False) + + dlg = ft.AlertDialog( + modal=True, + title=ft.Text("Product Lookup"), + content=WuttaProductLookup(self.config, initial_search=value, + on_select=select, on_cancel=cancel), + ) + + self.page.dialog = dlg + dlg.open = True + self.page.update() + def customer_lookup(self, value=None): - def select_customer(uuid): + def select(uuid): session = self.app.make_session() customer = session.get(self.model.Customer, uuid) self.set_customer(customer) @@ -122,7 +193,7 @@ class POSView(WuttaView): modal=True, title=ft.Text("Customer Lookup"), content=WuttaCustomerLookup(self.config, initial_search=value, - on_customer=select_customer, on_cancel=cancel), + on_select=select, on_cancel=cancel), ) self.page.dialog = dlg @@ -579,7 +650,7 @@ class POSView(WuttaView): ), ft.Row( [ - meta_button("ITEM", bgcolor='blue', on_click=self.not_supported), + meta_button("ITEM", bgcolor='blue', on_click=self.item_click), meta_button("CUST", bgcolor='blue', on_click=self.customer_click), ], spacing=0, @@ -682,10 +753,18 @@ class POSView(WuttaView): return ft.Text(*args, **kwargs) def get_current_batch(self, session, user=None, create=True): + handler = self.get_batch_handler() + if not user: user = session.get(self.model.User, self.page.session.get('user_uuid')) - handler = self.get_batch_handler() - return handler.get_current_batch(user, create=create) + + batch, created = handler.get_current_batch(user, create=create, return_created=True) + + if created: + self.page.session.set('txn_display', handler.get_screen_txn_display(batch)) + self.informed_refresh() + + return batch def did_mount(self): session = self.app.make_session() @@ -850,40 +929,5 @@ class POSView(WuttaView): self.informed_refresh() def main_submit(self, e=None): - value = self.main_input.value - if not value: - self.main_input.focus() - self.items.scroll_to(offset=-1, duration=250) - self.page.update() - return - - handler = self.get_batch_handler() - session = self.app.make_session() - model = self.model - - user = session.get(model.User, self.page.session.get('user_uuid')) - batch, created = handler.get_current_batch(user, return_created=True) - if created: - self.page.session.set('txn_display', handler.get_screen_txn_display(batch)) - self.informed_refresh() - - kwargs = {} - if self.set_quantity.data is not None: - kwargs['quantity'] = self.set_quantity.data - row = handler.process_entry(batch, value, **kwargs) - if row: - self.add_row_item(row) - self.items.scroll_to(offset=-1, duration=250) - self.txn_total.value = self.app.render_currency(batch.sales_total) - - else: - self.page.snack_bar = ft.SnackBar(ft.Text(f"UNRECOGNIZED: {value}", - color='black', - weight=ft.FontWeight.BOLD), - bgcolor='yellow', - duration=1500) - self.page.snack_bar.open = True - - session.commit() - session.close() + self.attempt_add_product() self.reset()