Add initial support for item lookup dialog

This commit is contained in:
Lance Edgar 2023-09-26 13:44:29 -05:00
parent ced1d37edd
commit 55203afc4d
4 changed files with 396 additions and 255 deletions

View file

@ -24,219 +24,26 @@
WuttaPOS - customer lookup control WuttaPOS - customer lookup control
""" """
import flet as ft from .lookup import WuttaLookup
from .base import WuttaControl
from .keyboard import WuttaKeyboard
class WuttaCustomerLookup(WuttaControl): class WuttaCustomerLookup(WuttaLookup):
default_font_size = 40 def get_results_columns(self):
font_size = default_font_size * 0.8 return [
default_button_height_dlg = 80 self.app.get_customer_key_label(),
disabled_bgcolor = '#aaaaaa' "Name",
"Phone",
"Email",
]
def __init__(self, *args, **kwargs): def get_results(self, session, entry):
self.initial_search = kwargs.pop('initial_search', None) return self.app.get_clientele_handler().search_customers(session, entry)
self.on_customer = kwargs.pop('on_customer', None)
self.on_cancel = kwargs.pop('on_cancel', None)
super().__init__(*args, **kwargs)
# track current selection def make_result_row(self, customer):
self.selected_customer_uuid = None return [
self.selected_control = None customer['_customer_key_'],
customer['name'],
def build(self): customer['phone_number'],
customer['email_address'],
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)

View file

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

243
wuttapos/controls/lookup.py Normal file
View file

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

View file

@ -32,6 +32,7 @@ import flet as ft
from .base import WuttaView from .base import WuttaView
from wuttapos.controls.custlookup import WuttaCustomerLookup from wuttapos.controls.custlookup import WuttaCustomerLookup
from wuttapos.controls.itemlookup import WuttaProductLookup
from wuttapos.util import get_pos_batch_handler from wuttapos.util import get_pos_batch_handler
@ -66,7 +67,7 @@ class POSView(WuttaView):
def get_batch_handler(self): def get_batch_handler(self):
return get_pos_batch_handler(self.config) 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 This is a convenience method, meant only to clear the main
input and set focus to it. Will also update() the page 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 The ``e`` arg is ignored and accepted only so this method may
be registered as an event handler, e.g. ``on_cancel``. be registered as an event handler, e.g. ``on_cancel``.
""" """
self.set_quantity.data = None if clear_quantity:
self.set_quantity.value = None self.set_quantity.data = None
self.set_quantity.value = None
self.main_input.value = '' self.main_input.value = ''
self.main_input.focus() self.main_input.focus()
if update: if update:
@ -102,9 +104,78 @@ class POSView(WuttaView):
duration=1500) duration=1500)
self.page.snack_bar.open = True 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 customer_lookup(self, value=None):
def select_customer(uuid): def select(uuid):
session = self.app.make_session() session = self.app.make_session()
customer = session.get(self.model.Customer, uuid) customer = session.get(self.model.Customer, uuid)
self.set_customer(customer) self.set_customer(customer)
@ -122,7 +193,7 @@ class POSView(WuttaView):
modal=True, modal=True,
title=ft.Text("Customer Lookup"), title=ft.Text("Customer Lookup"),
content=WuttaCustomerLookup(self.config, initial_search=value, content=WuttaCustomerLookup(self.config, initial_search=value,
on_customer=select_customer, on_cancel=cancel), on_select=select, on_cancel=cancel),
) )
self.page.dialog = dlg self.page.dialog = dlg
@ -579,7 +650,7 @@ class POSView(WuttaView):
), ),
ft.Row( 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), meta_button("CUST", bgcolor='blue', on_click=self.customer_click),
], ],
spacing=0, spacing=0,
@ -682,10 +753,18 @@ class POSView(WuttaView):
return ft.Text(*args, **kwargs) return ft.Text(*args, **kwargs)
def get_current_batch(self, session, user=None, create=True): def get_current_batch(self, session, user=None, create=True):
handler = self.get_batch_handler()
if not user: if not user:
user = session.get(self.model.User, self.page.session.get('user_uuid')) 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): def did_mount(self):
session = self.app.make_session() session = self.app.make_session()
@ -850,40 +929,5 @@ class POSView(WuttaView):
self.informed_refresh() self.informed_refresh()
def main_submit(self, e=None): def main_submit(self, e=None):
value = self.main_input.value self.attempt_add_product()
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.reset() self.reset()