Move customer lookup into a control

This commit is contained in:
Lance Edgar 2023-09-25 21:10:44 -05:00
parent 943f723ae6
commit 1fd78d0c2f
5 changed files with 289 additions and 196 deletions

View file

@ -118,13 +118,22 @@ def main(page: ft.Page):
if user: if user:
page.session.set('user_uuid', uuid) page.session.set('user_uuid', uuid)
page.session.set('user_display', str(user)) 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() session.close()
if user:
page.go('/pos') page.go('/login')
else:
page.go('/login')
else:
page.go('/login')
# TODO: can we inject config to the main() via ft.app() kwargs somehow? # TODO: can we inject config to the main() via ft.app() kwargs somehow?

View file

@ -33,3 +33,6 @@ class WuttaControl(ft.UserControl):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.config = config self.config = config
self.app = config.get_app() self.app = config.get_app()
def informed_refresh(self, **kwargs):
pass

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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)

View file

@ -56,6 +56,9 @@ class WuttaHeader(WuttaControl):
return ft.Row(controls) return ft.Row(controls)
def did_mount(self): def did_mount(self):
self.informed_refresh()
def informed_refresh(self):
self.update_txn_display() self.update_txn_display()
self.update_cust_display() self.update_cust_display()
self.update_user_display() self.update_user_display()
@ -84,13 +87,10 @@ class WuttaHeader(WuttaControl):
self.logout_divider.visible = True self.logout_divider.visible = True
def logout_click(self, e): def logout_click(self, e):
# TODO: hacky but works for now # TODO: hacky but works for now
if not self.config.production(): if not self.config.production():
self.page.client_storage.set('user_uuid', '') self.page.client_storage.set('user_uuid', '')
self.page.session.set('user_uuid', None) self.page.session.clear()
self.page.session.set('user_display', None)
self.page.session.set('txn_display', None)
self.page.session.set('cust_display', None)
self.page.go('/login') self.page.go('/login')

View file

@ -31,7 +31,7 @@ import time
import flet as ft import flet as ft
from .base import WuttaView from .base import WuttaView
from wuttapos.controls.keyboard import WuttaKeyboard from wuttapos.controls.custlookup import WuttaCustomerLookup
from wuttapos.util import get_pos_batch_handler from wuttapos.util import get_pos_batch_handler
@ -49,6 +49,20 @@ class POSView(WuttaView):
disabled_bgcolor = '#aaaaaa' 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): def get_batch_handler(self):
return get_pos_batch_handler(self.config) return get_pos_batch_handler(self.config)
@ -60,14 +74,11 @@ class POSView(WuttaView):
handler = self.get_batch_handler() handler = self.get_batch_handler()
handler.set_customer(batch, customer) handler.set_customer(batch, customer)
key = self.app.get_customer_key_field() key_field = self.app.get_customer_key_field()
value = getattr(customer, key) customer_key = getattr(customer, key_field)
self.page.session.set('cust_uuid', customer.uuid) self.page.session.set('cust_uuid', customer.uuid)
self.page.session.set('cust_display', str(value or '')) self.page.session.set('cust_display', str(customer_key or ''))
self.header.update_cust_display() self.informed_refresh()
self.header.update()
# TODO: can we assume caller will do this?
# self.page.update()
self.page.snack_bar = ft.SnackBar(ft.Text(f"CUSTOMER SET: {customer}", self.page.snack_bar = ft.SnackBar(ft.Text(f"CUSTOMER SET: {customer}",
color='black', color='black',
@ -76,100 +87,11 @@ class POSView(WuttaView):
duration=1500) duration=1500)
self.page.snack_bar.open = True self.page.snack_bar.open = True
def customer_lookup(self): def customer_lookup(self, value=None):
font_size = self.default_font_size * 0.8
selected_customer_uuid = None
def lookup(e=None):
global selected_customer_uuid
entry = searchbox.value
def select_customer(uuid):
session = self.app.make_session() session = self.app.make_session()
results = self.app.get_clientele_handler().search_customers(session, entry) customer = session.get(self.model.Customer, uuid)
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)
self.set_customer(customer) self.set_customer(customer)
session.commit() session.commit()
session.close() session.close()
@ -183,88 +105,11 @@ class POSView(WuttaView):
self.main_input.focus() self.main_input.focus()
self.page.update() 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( dlg = ft.AlertDialog(
modal=True, modal=True,
title=ft.Text("Customer Lookup"), title=ft.Text("Customer Lookup"),
content=ft.Column( content=WuttaCustomerLookup(self.config, initial_search=value,
[ on_customer=select_customer, on_cancel=cancel),
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,
],
),
],
),
) )
self.page.dialog = dlg self.page.dialog = dlg
@ -359,7 +204,8 @@ class POSView(WuttaView):
entry = self.main_input.value entry = self.main_input.value
if entry: if entry:
self.attempt_set_customer(entry) if not self.attempt_set_customer(entry):
self.customer_lookup(entry)
else: else:
self.customer_lookup() self.customer_lookup()
@ -435,6 +281,7 @@ class POSView(WuttaView):
session.commit() session.commit()
session.close() session.close()
return bool(customer)
def customer_click(self, e): def customer_click(self, e):
@ -446,7 +293,8 @@ class POSView(WuttaView):
value = self.main_input.value value = self.main_input.value
if value: if value:
# okay try to set it with given 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: else:
# no value provided, so do lookup # no value provided, so do lookup
@ -977,10 +825,7 @@ class POSView(WuttaView):
self.page.session.set('txn_display', None) self.page.session.set('txn_display', None)
self.page.session.set('cust_uuid', None) self.page.session.set('cust_uuid', None)
self.page.session.set('cust_display', None) self.page.session.set('cust_display', None)
self.header.update_txn_display() self.informed_refresh()
self.header.update_cust_display()
# TODO: not clear why must call update() for header too?
self.header.update()
def main_submit(self, e=None): def main_submit(self, e=None):
value = self.main_input.value value = self.main_input.value