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()