diff --git a/wuttapos/app.py b/wuttapos/app.py index f7e0f38..ea6c5c7 100644 --- a/wuttapos/app.py +++ b/wuttapos/app.py @@ -118,13 +118,22 @@ def main(page: ft.Page): if user: page.session.set('user_uuid', uuid) page.session.set('user_display', str(user)) + handler = get_pos_batch_handler(config) + batch = handler.get_current_batch(user, create=False) + if batch: + page.session.set('txn_display', batch.id_str) + if batch.customer: + page.session.set('cust_uuid', batch.customer.uuid) + key = app.get_customer_key_field() + value = getattr(batch.customer, key) + page.session.set('cust_display', str(value or '')) + session.close() + page.go('/pos') + return + session.close() - if user: - page.go('/pos') - else: - page.go('/login') - else: - page.go('/login') + + page.go('/login') # TODO: can we inject config to the main() via ft.app() kwargs somehow? diff --git a/wuttapos/controls/base.py b/wuttapos/controls/base.py index 85d8bc3..001e687 100644 --- a/wuttapos/controls/base.py +++ b/wuttapos/controls/base.py @@ -33,3 +33,6 @@ class WuttaControl(ft.UserControl): super().__init__(*args, **kwargs) self.config = config self.app = config.get_app() + + def informed_refresh(self, **kwargs): + pass diff --git a/wuttapos/controls/custlookup.py b/wuttapos/controls/custlookup.py new file mode 100644 index 0000000..afeee66 --- /dev/null +++ b/wuttapos/controls/custlookup.py @@ -0,0 +1,236 @@ +# -*- 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 - customer lookup control +""" + +import flet as ft + +from .base import WuttaControl +from .keyboard import WuttaKeyboard + + +class WuttaCustomerLookup(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_customer = kwargs.pop('on_customer', None) + self.on_cancel = kwargs.pop('on_cancel', None) + super().__init__(*args, **kwargs) + + # 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), + 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 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) diff --git a/wuttapos/controls/header.py b/wuttapos/controls/header.py index 858f340..e54be63 100644 --- a/wuttapos/controls/header.py +++ b/wuttapos/controls/header.py @@ -56,6 +56,9 @@ class WuttaHeader(WuttaControl): return ft.Row(controls) def did_mount(self): + self.informed_refresh() + + def informed_refresh(self): self.update_txn_display() self.update_cust_display() self.update_user_display() @@ -84,13 +87,10 @@ class WuttaHeader(WuttaControl): self.logout_divider.visible = True def logout_click(self, e): + # TODO: hacky but works for now if not self.config.production(): self.page.client_storage.set('user_uuid', '') - self.page.session.set('user_uuid', None) - self.page.session.set('user_display', None) - self.page.session.set('txn_display', None) - self.page.session.set('cust_display', None) - + self.page.session.clear() self.page.go('/login') diff --git a/wuttapos/views/pos.py b/wuttapos/views/pos.py index 2d200e7..32bed80 100644 --- a/wuttapos/views/pos.py +++ b/wuttapos/views/pos.py @@ -31,7 +31,7 @@ import time import flet as ft from .base import WuttaView -from wuttapos.controls.keyboard import WuttaKeyboard +from wuttapos.controls.custlookup import WuttaCustomerLookup from wuttapos.util import get_pos_batch_handler @@ -49,6 +49,20 @@ class POSView(WuttaView): disabled_bgcolor = '#aaaaaa' + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # keep a list of "informed" controls - i.e. child controls + # within this view, which need to stay abreast of global + # changes to the transaction, customer etc. + self.informed_controls = [] + if hasattr(self, 'header'): + self.informed_controls.append(self.header) + + def informed_refresh(self): + for control in self.informed_controls: + control.informed_refresh() + def get_batch_handler(self): return get_pos_batch_handler(self.config) @@ -60,14 +74,11 @@ class POSView(WuttaView): handler = self.get_batch_handler() handler.set_customer(batch, customer) - key = self.app.get_customer_key_field() - value = getattr(customer, key) + key_field = self.app.get_customer_key_field() + customer_key = getattr(customer, key_field) self.page.session.set('cust_uuid', customer.uuid) - self.page.session.set('cust_display', str(value or '')) - self.header.update_cust_display() - self.header.update() - # TODO: can we assume caller will do this? - # self.page.update() + self.page.session.set('cust_display', str(customer_key or '')) + self.informed_refresh() self.page.snack_bar = ft.SnackBar(ft.Text(f"CUSTOMER SET: {customer}", color='black', @@ -76,100 +87,11 @@ class POSView(WuttaView): duration=1500) self.page.snack_bar.open = True - def customer_lookup(self): - font_size = self.default_font_size * 0.8 - selected_customer_uuid = None - - def lookup(e=None): - global selected_customer_uuid - entry = searchbox.value + def customer_lookup(self, value=None): + def select_customer(uuid): session = self.app.make_session() - results = self.app.get_clientele_handler().search_customers(session, entry) - - search_results.rows.clear() - selected_customer_uuid = None - select_button.disabled = True - select_button.bgcolor = self.disabled_bgcolor - - if results: - for customer in results: - search_results.rows.append(ft.DataRow( - cells=[ - make_cell(customer['_customer_key_']), - make_cell(customer['name']), - make_cell(customer['phone_number']), - make_cell(customer['email_address']), - ], - on_select_changed=select_changed, - data={'uuid': customer['uuid']}, - )) - no_results.visible = False - - else: - no_results.value = f"NO RESULTS FOR: {entry}" - no_results.visible = True - - searchbox.value = '' - searchbox.focus() - self.page.update() - - searchbox = ft.TextField("", text_size=font_size * 0.8, - on_submit=lookup, - autofocus=True, - expand=True) - - def keypress(key): - if key == '⏎': - lookup() - else: - if key == '⌫': - searchbox.value = searchbox.value[:-1] - else: - searchbox.value += key - searchbox.focus() - self.page.update() - - def make_cell_text(text): - return ft.Text(text, size=32) - - def make_cell(text): - return ft.DataCell(make_cell_text(text)) - - def reset(e): - global selected_customer_uuid - searchbox.value = "" - search_results.rows.clear() - no_results.visible = False - selected_customer_uuid = None - select_button.disabled = True - select_button.bgcolor = self.disabled_bgcolor - searchbox.focus() - self.page.update() - - def select_changed(e): - global selected_customer_uuid - - if e.data: # selected - selected_customer_uuid = e.control.data['uuid'] - select_button.disabled = False - select_button.bgcolor = 'blue' - e.control.color = ft.colors.BLUE - else: - selected_customer_uuid = None - select_button.disabled = True - select_button.bgcolor = self.disabled_bgcolor - e.control.color = None - - self.page.update() - - def select_customer(e): - global selected_customer_uuid - if not selected_customer_uuid: - raise RuntimeError("no customer selected?") - - session = self.app.make_session() - customer = session.get(self.model.Customer, selected_customer_uuid) + customer = session.get(self.model.Customer, uuid) self.set_customer(customer) session.commit() session.close() @@ -183,88 +105,11 @@ class POSView(WuttaView): self.main_input.focus() self.page.update() - search_results = ft.DataTable( - columns=[ - ft.DataColumn(make_cell_text(self.app.get_customer_key_label())), - ft.DataColumn(make_cell_text("Name")), - ft.DataColumn(make_cell_text("Phone")), - ft.DataColumn(make_cell_text("Email")), - ], - ) - - no_results = ft.Text("NO RESULTS", size=32, color='red', - weight=ft.FontWeight.BOLD, - visible=False) - - select_button = ft.Container( - content=ft.Text("Select", size=font_size * 0.8), - alignment=ft.alignment.center, - height=self.page.data['default_button_height_dlg'] * 0.8, - width=self.page.data['default_button_height_dlg'] * 1.3, - border=ft.border.all(1, 'black'), - border_radius=ft.border_radius.all(5), - on_click=select_customer, - disabled=True, - bgcolor=self.disabled_bgcolor, - ) - dlg = ft.AlertDialog( modal=True, title=ft.Text("Customer Lookup"), - content=ft.Column( - [ - ft.Row( - [ - ft.Text("SEARCH FOR:"), - searchbox, - ft.Container( - content=ft.Text("Lookup", size=font_size * 0.8), - alignment=ft.alignment.center, - height=self.page.data['default_button_height_dlg'] * 0.8, - width=self.page.data['default_button_height_dlg'] * 1.3, - border=ft.border.all(1, 'black'), - border_radius=ft.border_radius.all(5), - on_click=lookup, - bgcolor='blue', - ), - ft.Container( - content=ft.Text("Reset", size=font_size * 0.8), - alignment=ft.alignment.center, - height=self.page.data['default_button_height_dlg'] * 0.8, - width=self.page.data['default_button_height_dlg'] * 1.3, - border=ft.border.all(1, 'black'), - border_radius=ft.border_radius.all(5), - on_click=reset, - bgcolor='yellow', - ), - ft.Container( - content=ft.Text("Cancel", size=font_size * 0.8), - alignment=ft.alignment.center, - height=self.page.data['default_button_height_dlg'] * 0.8, - width=self.page.data['default_button_height_dlg'] * 1.3, - border=ft.border.all(1, 'black'), - border_radius=ft.border_radius.all(5), - on_click=cancel, - ), - ], - ), - ft.Divider(), - WuttaKeyboard(self.config, on_keypress=keypress), - ft.Divider(), - ft.Row( - [ - ft.Column( - [ - search_results, - no_results, - ], - expand=True, - ), - select_button, - ], - ), - ], - ), + content=WuttaCustomerLookup(self.config, initial_search=value, + on_customer=select_customer, on_cancel=cancel), ) self.page.dialog = dlg @@ -359,7 +204,8 @@ class POSView(WuttaView): entry = self.main_input.value if entry: - self.attempt_set_customer(entry) + if not self.attempt_set_customer(entry): + self.customer_lookup(entry) else: self.customer_lookup() @@ -435,6 +281,7 @@ class POSView(WuttaView): session.commit() session.close() + return bool(customer) def customer_click(self, e): @@ -446,7 +293,8 @@ class POSView(WuttaView): value = self.main_input.value if value: # okay try to set it with given value - self.attempt_set_customer(value) + if not self.attempt_set_customer(value): + self.customer_lookup(value) else: # no value provided, so do lookup @@ -977,10 +825,7 @@ class POSView(WuttaView): self.page.session.set('txn_display', None) self.page.session.set('cust_uuid', None) self.page.session.set('cust_display', None) - self.header.update_txn_display() - self.header.update_cust_display() - # TODO: not clear why must call update() for header too? - self.header.update() + self.informed_refresh() def main_submit(self, e=None): value = self.main_input.value