Add basic customer lookup

This commit is contained in:
Lance Edgar 2023-09-23 20:16:24 -05:00
parent 545f115dc8
commit 8f937d040a
4 changed files with 803 additions and 56 deletions

View file

@ -43,6 +43,12 @@ def main(page: ft.Page):
page.window_full_screen = True page.window_full_screen = True
# page.vertical_alignment = ft.MainAxisAlignment.CENTER # page.vertical_alignment = ft.MainAxisAlignment.CENTER
# global defaults for button/text styles etc.
page.data = {
'default_button_height_pos': 100,
'default_button_height_dlg': 80,
}
# nb. track current user, txn etc. # nb. track current user, txn etc.
page.shared = {} page.shared = {}
@ -53,6 +59,8 @@ def main(page: ft.Page):
with open(path, 'rt') as f: with open(path, 'rt') as f:
page.shared = json.loads(f.read()) page.shared = json.loads(f.read())
page.shared.pop('txn_display', None) # TODO page.shared.pop('txn_display', None) # TODO
page.shared.pop('cust_uuid', None) # TODO
page.shared.pop('cust_display', None) # TODO
if page.shared and page.shared.get('user_uuid'): if page.shared and page.shared.get('user_uuid'):
handler = app.get_batch_handler('pos') handler = app.get_batch_handler('pos')
session = app.make_session() session = app.make_session()
@ -61,6 +69,13 @@ def main(page: ft.Page):
batch = handler.get_current_batch(user, create=False) batch = handler.get_current_batch(user, create=False)
if batch: if batch:
page.shared['txn_display'] = batch.id_str page.shared['txn_display'] = batch.id_str
page.shared['cust_uuid'] = batch.customer_uuid
if batch.customer:
key = app.get_customer_key_field()
value = getattr(batch.customer, key)
page.shared['cust_display'] = str(value or '') or None
else:
page.shared['cust_display'] = None
session.close() session.close()
def clean_exit(): def clean_exit():

View file

@ -34,6 +34,7 @@ class WuttaHeader(WuttaControl):
def build(self): def build(self):
self.txn_display = ft.Text("Txn: N", weight=ft.FontWeight.BOLD, size=20) self.txn_display = ft.Text("Txn: N", weight=ft.FontWeight.BOLD, size=20)
self.cust_display = ft.Text("Cust: N", weight=ft.FontWeight.BOLD, size=20)
self.user_display = ft.Text("User: N", weight=ft.FontWeight.BOLD, size=20) self.user_display = ft.Text("User: N", weight=ft.FontWeight.BOLD, size=20)
self.logout_button = ft.FilledButton("Logout", on_click=self.logout_click, visible=False) self.logout_button = ft.FilledButton("Logout", on_click=self.logout_click, visible=False)
self.logout_divider = ft.VerticalDivider(visible=False) self.logout_divider = ft.VerticalDivider(visible=False)
@ -41,7 +42,7 @@ class WuttaHeader(WuttaControl):
controls = [ controls = [
self.txn_display, self.txn_display,
ft.VerticalDivider(), ft.VerticalDivider(),
ft.Text(f"Cust: N", weight=ft.FontWeight.BOLD, size=20), self.cust_display,
ft.VerticalDivider(), ft.VerticalDivider(),
WuttaTimestamp(self.config, expand=True, WuttaTimestamp(self.config, expand=True,
weight=ft.FontWeight.BOLD, size=20), weight=ft.FontWeight.BOLD, size=20),
@ -56,6 +57,7 @@ class WuttaHeader(WuttaControl):
def did_mount(self): def did_mount(self):
self.update_txn_display() self.update_txn_display()
self.update_cust_display()
self.update_user_display() self.update_user_display()
self.update() self.update()
@ -67,6 +69,14 @@ class WuttaHeader(WuttaControl):
self.txn_display.value = f"Txn: {txn_display}" self.txn_display.value = f"Txn: {txn_display}"
def update_cust_display(self):
cust_display = "N"
if self.page and self.page.shared and self.page.shared.get('cust_display'):
cust_display = self.page.shared['cust_display']
self.cust_display.value = f"Cust: {cust_display}"
def update_user_display(self): def update_user_display(self):
user_display = "N" user_display = "N"
@ -84,5 +94,6 @@ class WuttaHeader(WuttaControl):
'user_uuid': None, 'user_uuid': None,
'user_display': None, 'user_display': None,
'txn_display': None, 'txn_display': None,
'cust_display': None,
}) })
self.page.go('/login') self.page.go('/login')

View file

@ -0,0 +1,179 @@
# -*- 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 - keyboard control
"""
import flet as ft
from .base import WuttaControl
class WuttaKeyboard(WuttaControl):
default_font_size = 20
default_button_size = 80
def __init__(self, *args, **kwargs):
self.on_keypress = kwargs.pop('on_keypress', None)
super().__init__(*args, **kwargs)
self.caps_lock = False
self.caps_map = dict([(k, k.upper())
for k in 'abcdefghijklmnopqrstuvwxyz'])
self.shift = False
self.shift_map = {
'`': '~',
'1': '!',
'2': '@',
'3': '#',
'4': '$',
'5': '%',
'6': '^',
'7': '&',
'8': '*',
'9': '(',
'0': ')',
'-': '_',
'=': '+',
'[': '{',
']': '}',
'\\': '|',
';': ':',
"'": '"',
',': '<',
'.': '>',
'/': '?',
}
def update_caps_lock(self, caps_lock):
self.caps_lock = caps_lock
if self.caps_lock:
self.caps_key.bgcolor = 'blue'
else:
self.caps_key.bgcolor = None
for key, button in self.keys.items():
if key in self.caps_map:
if self.caps_lock:
button.content.value = self.caps_map[key]
else:
button.content.value = key
self.update()
def update_shift(self, shift):
self.shift = shift
if self.shift:
self.shift_key.bgcolor = 'blue'
else:
self.shift_key.bgcolor = None
for key, button in self.keys.items():
if key in self.caps_map:
if self.shift:
button.content.value = self.caps_map[key]
else:
button.content.value = key
elif key in self.shift_map:
if self.shift:
button.content.value = self.shift_map[key]
else:
button.content.value = key
self.update()
def simple_keypress(self, e):
# maybe inform parent
if self.on_keypress:
key = e.control.content.value
# avoid callback for certain keys
if key not in ('CAPS', 'SHIFT'):
# translate certain keys
if key == 'SPACE':
key = ' '
# let 'em know
self.on_keypress(key)
# turn off shift key if set
if self.shift:
self.update_shift(False)
def build(self):
self.keys = {}
def make_key(key, data=None, on_click=self.simple_keypress,
width=self.default_button_size,
bgcolor=None):
button = ft.Container(content=ft.Text(key, size=self.default_font_size,
weight=ft.FontWeight.BOLD),
data=data or key,
height=self.default_button_size,
width=width,
on_click=on_click,
alignment=ft.alignment.center,
border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5),
bgcolor=bgcolor,
)
self.keys[key] = button
return button
def caps_click(e):
self.update_caps_lock(not self.caps_lock)
self.caps_key = make_key('CAPS', on_click=caps_click)
if self.caps_lock:
self.caps_key.bgcolor = 'blue'
def shift_click(e):
self.update_shift(not self.shift)
self.shift_key = make_key('SHIFT', on_click=shift_click)
if self.shift:
self.shift_key.bgcolor = 'blue'
rows = [
[make_key(k) for k in "`1234567890-="] + [make_key('', bgcolor='yellow')],
[make_key(k) for k in "qwertyuiop[]\\"],
[self.caps_key] + [make_key(k) for k in "asdfghjkl;'"] + [make_key('', bgcolor='blue')],
[self.shift_key] + [make_key(k) for k in "zxcvbnm,./"],
[make_key('SPACE', width=self.default_button_size * 5)],
]
rows = [ft.Row(controls, alignment=ft.MainAxisAlignment.CENTER)
for controls in rows]
return ft.Container(
content=ft.Column(
rows,
),
)

View file

@ -24,9 +24,16 @@
WuttaPOS - POS view WuttaPOS - POS view
""" """
import decimal
import logging
import time
import flet as ft import flet as ft
from .base import WuttaView from .base import WuttaView
from wuttapos.controls.keyboard import WuttaKeyboard
log = logging.getLogger(__name__)
class POSView(WuttaView): class POSView(WuttaView):
@ -34,6 +41,415 @@ class POSView(WuttaView):
Main POS view for WuttaPOS Main POS view for WuttaPOS
""" """
# TODO: should be configurable?
default_button_size = 100
default_font_size = 40
disabled_bgcolor = '#aaaaaa'
def get_batch_handler(self):
return self.app.get_batch_handler('pos')
def set_customer(self, customer, batch=None):
session = self.app.get_session(customer)
if not batch:
batch = self.get_current_batch(session)
handler = self.get_batch_handler()
handler.set_customer(batch, customer)
key = self.app.get_customer_key_field()
value = getattr(customer, key)
self.page.shared['cust_uuid'] = customer.uuid
self.page.shared['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.snack_bar = ft.SnackBar(ft.Text(f"CUSTOMER SET: {customer}",
color='black',
weight=ft.FontWeight.BOLD),
bgcolor='green',
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
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)
self.set_customer(customer)
session.commit()
session.close()
dlg.open = False
self.main_input.focus()
self.page.update()
def cancel(e):
dlg.open = False
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,
],
),
],
),
)
self.page.dialog = dlg
dlg.open = True
self.page.update()
def customer_info(self):
clientele = self.app.get_clientele_handler()
session = self.app.make_session()
entry = self.main_input.value
if entry:
different = True
customer = clientele.locate_customer_for_entry(session, entry)
if not customer:
session.close()
self.page.snack_bar = ft.SnackBar(ft.Text(f"CUSTOMER NOT FOUND: {entry}",
color='black',
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.page.update()
return
else:
different = False
customer = session.get(self.model.Customer, self.page.shared['cust_uuid'])
assert customer
info = clientele.get_customer_info_markdown(customer)
session.close()
def close(e):
dlg.open = False
self.main_input.value = ''
self.main_input.focus()
self.page.update()
font_size = self.default_font_size * 0.8
dlg = ft.AlertDialog(
# modal=True,
title=ft.Text("Customer Info"),
content=ft.Container(ft.Column(
[
ft.Container(
content=ft.Text("NOTE: this is a DIFFERENT customer than the txn has!"),
bgcolor='yellow',
visible=different,
),
ft.Markdown(info),
],
height=500,
width=500,
)),
actions=[
ft.Container(content=ft.Text("Close",
size=font_size,
weight=ft.FontWeight.BOLD),
height=self.default_button_size * 0.8,
width=self.default_button_size * 1.2,
alignment=ft.alignment.center,
bgcolor='blue',
border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5),
on_click=close),
],
# actions_alignment=ft.MainAxisAlignment.END,
)
self.page.dialog = dlg
dlg.open = True
self.page.update()
def customer_prompt(self):
def view_info(e):
dlg.open = False
self.page.update()
# cf. https://github.com/flet-dev/flet/issues/1670
time.sleep(0.1)
self.customer_info()
def replace(e):
dlg.open = False
self.page.update()
# cf. https://github.com/flet-dev/flet/issues/1670
time.sleep(0.1)
entry = self.main_input.value
if entry:
self.attempt_set_customer(entry)
else:
self.customer_lookup()
def cancel(e):
dlg.open = False
self.main_input.value = ''
self.main_input.focus()
self.page.update()
font_size = self.default_font_size * 0.8
dlg = ft.AlertDialog(
# modal=True,
title=ft.Text("Customer Already Selected"),
content=ft.Text("What would you like to do?", size=20),
actions=[
ft.Container(content=ft.Text("View Info",
size=font_size,
weight=ft.FontWeight.BOLD),
height=self.default_button_size,
width=self.default_button_size * 2.5,
alignment=ft.alignment.center,
bgcolor='blue',
border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5),
on_click=view_info),
ft.Container(content=ft.Text("Replace",
size=font_size,
color='black',
weight=ft.FontWeight.BOLD),
height=self.default_button_size,
width=self.default_button_size * 2.5,
alignment=ft.alignment.center,
bgcolor='yellow',
border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5),
on_click=replace),
ft.Container(content=ft.Text("Cancel",
size=font_size,
# color='black',
weight=ft.FontWeight.BOLD),
height=self.default_button_size,
width=self.default_button_size * 2.5,
alignment=ft.alignment.center,
border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5),
on_click=cancel),
])
self.page.dialog = dlg
dlg.open = True
self.page.update()
def attempt_set_customer(self, entry=None):
session = self.app.make_session()
customer = self.app.get_clientele_handler().locate_customer_for_entry(session, entry)
if customer:
self.set_customer(customer)
self.main_input.value = ''
self.main_input.focus()
self.page.update()
else: # customer not found
self.page.snack_bar = ft.SnackBar(ft.Text(f"CUSTOMER NOT FOUND: {entry}",
color='black',
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.main_input.focus()
self.page.update()
session.commit()
session.close()
def customer_click(self, e):
# prompt user to replace customer if already set
if self.page.shared['cust_uuid']:
self.customer_prompt()
else:
value = self.main_input.value
if value:
# okay try to set it with given value
self.attempt_set_customer(value)
else:
# no value provided, so do lookup
self.customer_lookup()
def build_controls(self): def build_controls(self):
self.main_input = ft.TextField(on_submit=self.main_submit, self.main_input = ft.TextField(on_submit=self.main_submit,
@ -41,14 +457,17 @@ class POSView(WuttaView):
text_style=ft.TextStyle(weight=ft.FontWeight.BOLD), text_style=ft.TextStyle(weight=ft.FontWeight.BOLD),
autofocus=True) autofocus=True)
self.items = ft.ListView() self.items = ft.ListView(
height=800,
)
self.txn_total = ft.Text("", size=40) self.txn_total = ft.Text("", size=40)
self.items_column = ft.Column( self.items_column = ft.Column(
controls=[ controls=[
ft.Container(content=self.items, ft.Container(
padding=ft.padding.only(10, 0, 10, 0)), content=self.items,
padding=ft.padding.only(10, 0, 10, 0)),
ft.Row([self.txn_total], ft.Row([self.txn_total],
alignment=ft.MainAxisAlignment.END), alignment=ft.MainAxisAlignment.END),
], ],
@ -59,37 +478,99 @@ class POSView(WuttaView):
value = e.control.content.value value = e.control.content.value
if value == 'ENTER': if value == 'ENTER':
print('TODO: handle enter') self.main_submit()
elif value == '': # backspace elif value == '': # backspace
if self.main_input.value: if self.main_input.value:
self.main_input.value = self.main_input.value[:-1] self.main_input.value = self.main_input.value[:-1]
self.page.update() self.main_input.focus()
self.page.update()
elif value == 'CE': # clear entry
if self.main_input.value:
self.main_input.value = ""
elif self.set_quantity.data is not None:
self.set_quantity.data = None
self.set_quantity.value = None
self.main_input.focus()
self.page.update()
elif value == '@':
quantity = self.main_input.value
valid = False
if self.set_quantity.data is not None:
quantity = self.set_quantity.data
self.page.snack_bar = ft.SnackBar(ft.Text(f"QUANTITY ALREADY SET: {quantity}",
color='black',
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
else:
try:
quantity = decimal.Decimal(quantity)
valid = True
except decimal.InvalidOperation:
pass
if valid and quantity:
self.set_quantity.data = quantity
self.set_quantity.value = self.app.render_quantity(quantity) + " @ "
self.main_input.value = ""
self.main_input.focus()
else:
self.page.snack_bar = ft.SnackBar(ft.Text(f"INVALID @ QUANTITY: {quantity}",
color='black',
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.page.update()
elif value == '': elif value == '':
pass # TODO self.items.scroll_to(delta=-50, duration=250)
self.page.update()
elif value == '': elif value == '':
pass # TODO self.items.scroll_to(delta=50, duration=250)
self.page.update()
else: else:
self.main_input.value = f"{self.main_input.value or ''}{value}" self.main_input.value = f"{self.main_input.value or ''}{value}"
self.main_input.focus()
self.page.update() self.page.update()
# TODO: should be configurable? def up_long_press(e):
tenkey_button_size = 100 self.items.scroll_to(delta=-500, duration=250)
tenkey_font_size = 40 self.page.update()
def tenkey_button(text, width=tenkey_button_size): def down_long_press(e):
self.items.scroll_to(delta=500, duration=250)
self.page.update()
tenkey_button_size = self.default_button_size
tenkey_font_size = self.default_font_size
def tenkey_button(text,
bgcolor='green',
height=tenkey_button_size,
width=tenkey_button_size,
on_long_press=None,
):
return ft.Container(content=ft.Text(text, size=tenkey_font_size, return ft.Container(content=ft.Text(text, size=tenkey_font_size,
weight=ft.FontWeight.BOLD), weight=ft.FontWeight.BOLD),
height=tenkey_button_size, height=height,
width=width, width=width,
on_click=tenkey_click, on_click=tenkey_click,
on_long_press=on_long_press,
alignment=ft.alignment.center, alignment=ft.alignment.center,
border=ft.border.all(1, 'black'), border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5), border_radius=ft.border_radius.all(5),
bgcolor='green') bgcolor=bgcolor)
self.tenkey_menu = ft.Container( self.tenkey_menu = ft.Container(
content=ft.Column( content=ft.Column(
@ -99,7 +580,7 @@ class POSView(WuttaView):
tenkey_button("1"), tenkey_button("1"),
tenkey_button("2"), tenkey_button("2"),
tenkey_button("3"), tenkey_button("3"),
tenkey_button(""), tenkey_button("@"),
], ],
spacing=0, spacing=0,
), ),
@ -108,7 +589,7 @@ class POSView(WuttaView):
tenkey_button("4"), tenkey_button("4"),
tenkey_button("5"), tenkey_button("5"),
tenkey_button("6"), tenkey_button("6"),
tenkey_button(""), tenkey_button("", on_long_press=up_long_press),
], ],
spacing=0, spacing=0,
), ),
@ -117,14 +598,15 @@ class POSView(WuttaView):
tenkey_button("7"), tenkey_button("7"),
tenkey_button("8"), tenkey_button("8"),
tenkey_button("9"), tenkey_button("9"),
tenkey_button(""), tenkey_button("", on_long_press=down_long_press),
], ],
spacing=0, spacing=0,
), ),
ft.Row( ft.Row(
[ [
tenkey_button("0"), tenkey_button("0"),
tenkey_button("00"), # tenkey_button("00"),
tenkey_button("."),
tenkey_button("ENTER", width=tenkey_button_size * 2), tenkey_button("ENTER", width=tenkey_button_size * 2),
], ],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN, alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
@ -139,12 +621,11 @@ class POSView(WuttaView):
meta_button_width = meta_button_height * 2 meta_button_width = meta_button_height * 2
meta_font_size = tenkey_font_size meta_font_size = tenkey_font_size
def meta_button(text, on_click=None, bgcolor='yellow'): def meta_button(text, on_click=None, bgcolor='blue'):
return ft.Container(content=ft.Text(text, size=meta_font_size, return ft.Container(content=ft.Text(text, size=meta_font_size,
weight=ft.FontWeight.BOLD), weight=ft.FontWeight.BOLD),
height=meta_button_height, height=meta_button_height,
width=meta_button_width, width=meta_button_width,
# on_click=meta_click,
on_click=on_click, on_click=on_click,
alignment=ft.alignment.center, alignment=ft.alignment.center,
border=ft.border.all(1, 'black'), border=ft.border.all(1, 'black'),
@ -156,29 +637,29 @@ class POSView(WuttaView):
[ [
ft.Row( ft.Row(
[ [
meta_button("MGR"), meta_button("MGR", bgcolor='blue', on_click=self.not_supported),
meta_button("VOID", bgcolor='red', on_click=self.void_click), meta_button("VOID", bgcolor='red', on_click=self.void_click),
], ],
spacing=0, spacing=0,
), ),
ft.Row( ft.Row(
[ [
meta_button("ITEM"), meta_button("ITEM", bgcolor='blue', on_click=self.not_supported),
meta_button("CUST"), meta_button("CUST", bgcolor='blue', on_click=self.customer_click),
], ],
spacing=0, spacing=0,
), ),
ft.Row( ft.Row(
[ [
meta_button("TODO"), meta_button("TODO", bgcolor='blue', on_click=self.not_supported),
meta_button("TODO"), meta_button("TODO", bgcolor='blue', on_click=self.not_supported),
], ],
spacing=0, spacing=0,
), ),
ft.Row( ft.Row(
[ [
meta_button("TODO"), meta_button("TODO", bgcolor='blue', on_click=self.not_supported),
meta_button("TODO"), meta_button("TODO", bgcolor='blue', on_click=self.not_supported),
], ],
spacing=0, spacing=0,
), ),
@ -217,11 +698,18 @@ class POSView(WuttaView):
), ),
expand=0) expand=0)
self.set_quantity = ft.Text(value=None, data=None, weight=ft.FontWeight.BOLD, size=40)
return [ return [
self.build_header(), self.build_header(),
ft.Row( ft.Row(
[self.main_input], [
self.set_quantity,
self.main_input,
tenkey_button("", height=70, width=70),
tenkey_button("CE", height=70, width=70),
],
alignment=ft.MainAxisAlignment.CENTER, alignment=ft.MainAxisAlignment.CENTER,
), ),
@ -253,22 +741,41 @@ class POSView(WuttaView):
kwargs.setdefault('size', 24) kwargs.setdefault('size', 24)
return ft.Text(*args, **kwargs) return ft.Text(*args, **kwargs)
def did_mount(self): def get_current_batch(self, session, user=None, create=True):
model = self.model if not user:
session = self.app.make_session() user = session.get(self.model.User, self.page.shared['user_uuid'])
handler = self.app.get_batch_handler('pos') handler = self.app.get_batch_handler('pos')
user = session.get(model.User, self.page.shared['user_uuid']) return handler.get_current_batch(user, create=create)
batch = handler.get_current_batch(user, create=True)
def did_mount(self):
session = self.app.make_session()
batch = self.get_current_batch(session)
self.page.shared['txn_display'] = batch.id_str self.page.shared['txn_display'] = batch.id_str
self.items.controls.clear() self.items.controls.clear()
for row in batch.active_rows(): for row in batch.active_rows():
self.add_row_item(row) self.add_row_item(row)
self.items.scroll_to(offset=-1, duration=100)
self.txn_total.value = self.app.render_currency(batch.sales_total) self.txn_total.value = self.app.render_currency(batch.sales_total)
session.commit() session.commit()
session.close() session.close()
self.page.update()
def not_supported(self, e=None, feature=None):
text = "NOT YET SUPPORTED"
if not feature and e:
feature = e.control.content.value
if feature:
text += f": {feature}"
self.page.snack_bar = ft.SnackBar(ft.Text(text, color='black',
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.page.update()
def add_row_item(self, row): def add_row_item(self, row):
quantity = self.app.render_quantity(row.quantity) quantity = self.app.render_quantity(row.quantity)
@ -282,12 +789,14 @@ class POSView(WuttaView):
self.make_text(f"× {quantity} @ {pretty_price}", self.make_text(f"× {quantity} @ {pretty_price}",
weight=None, italic=True, size=20), weight=None, italic=True, size=20),
]), ]),
self.make_text(pretty_price), self.make_text(self.app.render_currency(row.sales_total)),
], ],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN, alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
), ),
border=ft.border.only(bottom=ft.border.BorderSide(1, 'gray')), border=ft.border.only(bottom=ft.border.BorderSide(1, 'gray')),
padding=ft.padding.only(0, 5, 0, 5))) padding=ft.padding.only(0, 5, 0, 5),
))
def void_click(self, e): def void_click(self, e):
@ -303,21 +812,16 @@ class POSView(WuttaView):
# void current batch # void current batch
handler.void_batch(batch, user) handler.void_batch(batch, user)
session.flush() session.flush()
self.clear_all()
# make new batch # make new batch
batch = handler.get_current_batch(user, create=True) batch = handler.get_current_batch(user, create=True)
self.page.shared['txn_display'] = batch.id_str
# commit changes
session.commit() session.commit()
session.close() session.close()
# reset txn display self.main_input.focus()
self.items.controls.clear()
self.txn_total.value = None
self.page.shared['txn_display'] = batch.id_str
self.header.update_txn_display()
# TODO: not clear why must call update() for header too?
self.header.update()
self.page.update() self.page.update()
def cancel(e): def cancel(e):
@ -325,12 +829,30 @@ class POSView(WuttaView):
self.page.update() self.page.update()
dlg = ft.AlertDialog( dlg = ft.AlertDialog(
modal=True, # modal=True,
title=ft.Text("Confirm VOID"), title=ft.Text("Confirm VOID"),
content=ft.Text("Really VOID this transaction?"), content=ft.Text("Really VOID this transaction?"),
actions=[ actions=[
ft.TextButton("Yes", on_click=confirm), ft.Container(content=ft.Text("Yes, VOID",
ft.TextButton("No", on_click=cancel), size=self.default_font_size,
color='black',
weight=ft.FontWeight.BOLD),
height=self.default_button_size,
width=self.default_button_size * 2.5,
alignment=ft.alignment.center,
bgcolor='red',
border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5),
on_click=confirm),
ft.Container(content=ft.Text("Cancel",
size=self.default_font_size,
weight=ft.FontWeight.BOLD),
height=self.default_button_size,
width=self.default_button_size * 2.5,
alignment=ft.alignment.center,
border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5),
on_click=cancel),
]) ])
self.page.dialog = dlg self.page.dialog = dlg
@ -347,24 +869,39 @@ class POSView(WuttaView):
# tender / execute batch # tender / execute batch
tender = e.control.content.value tender = e.control.content.value
handler.tender_and_execute(batch, user, tender) handler.tender_and_execute(batch, user, tender)
self.clear_all()
# make new batch # make new batch
batch = handler.get_current_batch(user, create=True) batch = handler.get_current_batch(user, create=True)
self.page.shared['txn_display'] = batch.id_str
self.header.update_txn_display()
self.header.update()
session.commit() session.commit()
session.close() session.close()
# reset txn display
self.items.controls.clear()
self.txn_total.value = None
self.page.shared['txn_display'] = batch.id_str
self.header.update_txn_display()
# TODO: not clear why must call update() for header too?
self.header.update()
self.main_input.focus() self.main_input.focus()
self.page.update() self.page.update()
def main_submit(self, e): def clear_all(self):
self.items.controls.clear()
self.txn_total.value = None
self.page.shared['txn_display'] = None
self.page.shared['cust_uuid'] = None
self.page.shared['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()
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.app.get_batch_handler('pos') handler = self.app.get_batch_handler('pos')
session = self.app.make_session() session = self.app.make_session()
model = self.model model = self.model
@ -372,10 +909,13 @@ class POSView(WuttaView):
user = session.get(model.User, self.page.shared['user_uuid']) user = session.get(model.User, self.page.shared['user_uuid'])
batch = handler.get_current_batch(user) batch = handler.get_current_batch(user)
value = self.main_input.value kwargs = {}
row = handler.process_entry(batch, value) if self.set_quantity.data is not None:
kwargs['quantity'] = self.set_quantity.data
row = handler.process_entry(batch, value, **kwargs)
if row: if row:
self.add_row_item(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) self.txn_total.value = self.app.render_currency(batch.sales_total)
else: else:
@ -391,4 +931,6 @@ class POSView(WuttaView):
self.main_input.value = "" self.main_input.value = ""
self.main_input.focus() self.main_input.focus()
self.set_quantity.data = None
self.set_quantity.value = None
self.page.update() self.page.update()