diff --git a/wuttapos/app.py b/wuttapos/app.py index 8690065..dd4b410 100644 --- a/wuttapos/app.py +++ b/wuttapos/app.py @@ -43,6 +43,12 @@ def main(page: ft.Page): page.window_full_screen = True # 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. page.shared = {} @@ -53,6 +59,8 @@ def main(page: ft.Page): with open(path, 'rt') as f: page.shared = json.loads(f.read()) 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'): handler = app.get_batch_handler('pos') session = app.make_session() @@ -61,6 +69,13 @@ def main(page: ft.Page): batch = handler.get_current_batch(user, create=False) if batch: 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() def clean_exit(): diff --git a/wuttapos/controls/header.py b/wuttapos/controls/header.py index e1aa287..889d4e8 100644 --- a/wuttapos/controls/header.py +++ b/wuttapos/controls/header.py @@ -34,6 +34,7 @@ class WuttaHeader(WuttaControl): def build(self): 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.logout_button = ft.FilledButton("Logout", on_click=self.logout_click, visible=False) self.logout_divider = ft.VerticalDivider(visible=False) @@ -41,7 +42,7 @@ class WuttaHeader(WuttaControl): controls = [ self.txn_display, ft.VerticalDivider(), - ft.Text(f"Cust: N", weight=ft.FontWeight.BOLD, size=20), + self.cust_display, ft.VerticalDivider(), WuttaTimestamp(self.config, expand=True, weight=ft.FontWeight.BOLD, size=20), @@ -56,6 +57,7 @@ class WuttaHeader(WuttaControl): def did_mount(self): self.update_txn_display() + self.update_cust_display() self.update_user_display() self.update() @@ -67,6 +69,14 @@ class WuttaHeader(WuttaControl): 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): user_display = "N" @@ -84,5 +94,6 @@ class WuttaHeader(WuttaControl): 'user_uuid': None, 'user_display': None, 'txn_display': None, + 'cust_display': None, }) self.page.go('/login') diff --git a/wuttapos/controls/keyboard.py b/wuttapos/controls/keyboard.py new file mode 100644 index 0000000..e1dabf9 --- /dev/null +++ b/wuttapos/controls/keyboard.py @@ -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 . +# +################################################################################ +""" +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, + ), + ) diff --git a/wuttapos/views/pos.py b/wuttapos/views/pos.py index cfb8b85..aa0aea7 100644 --- a/wuttapos/views/pos.py +++ b/wuttapos/views/pos.py @@ -24,9 +24,16 @@ WuttaPOS - POS view """ +import decimal +import logging +import time + import flet as ft from .base import WuttaView +from wuttapos.controls.keyboard import WuttaKeyboard + +log = logging.getLogger(__name__) class POSView(WuttaView): @@ -34,6 +41,415 @@ class POSView(WuttaView): 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): 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), autofocus=True) - self.items = ft.ListView() + self.items = ft.ListView( + height=800, + ) self.txn_total = ft.Text("", size=40) self.items_column = ft.Column( controls=[ - ft.Container(content=self.items, - padding=ft.padding.only(10, 0, 10, 0)), + ft.Container( + content=self.items, + padding=ft.padding.only(10, 0, 10, 0)), ft.Row([self.txn_total], alignment=ft.MainAxisAlignment.END), ], @@ -59,37 +478,99 @@ class POSView(WuttaView): value = e.control.content.value if value == 'ENTER': - print('TODO: handle enter') + self.main_submit() elif value == '⌫': # backspace if self.main_input.value: 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 == '↑': - pass # TODO + self.items.scroll_to(delta=-50, duration=250) + self.page.update() elif value == '↓': - pass # TODO + self.items.scroll_to(delta=50, duration=250) + self.page.update() else: self.main_input.value = f"{self.main_input.value or ''}{value}" + self.main_input.focus() self.page.update() - # TODO: should be configurable? - tenkey_button_size = 100 - tenkey_font_size = 40 + def up_long_press(e): + self.items.scroll_to(delta=-500, duration=250) + 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, weight=ft.FontWeight.BOLD), - height=tenkey_button_size, + height=height, width=width, on_click=tenkey_click, + on_long_press=on_long_press, alignment=ft.alignment.center, border=ft.border.all(1, 'black'), border_radius=ft.border_radius.all(5), - bgcolor='green') + bgcolor=bgcolor) self.tenkey_menu = ft.Container( content=ft.Column( @@ -99,7 +580,7 @@ class POSView(WuttaView): tenkey_button("1"), tenkey_button("2"), tenkey_button("3"), - tenkey_button("⌫"), + tenkey_button("@"), ], spacing=0, ), @@ -108,7 +589,7 @@ class POSView(WuttaView): tenkey_button("4"), tenkey_button("5"), tenkey_button("6"), - tenkey_button("↑"), + tenkey_button("↑", on_long_press=up_long_press), ], spacing=0, ), @@ -117,14 +598,15 @@ class POSView(WuttaView): tenkey_button("7"), tenkey_button("8"), tenkey_button("9"), - tenkey_button("↓"), + tenkey_button("↓", on_long_press=down_long_press), ], spacing=0, ), ft.Row( [ tenkey_button("0"), - tenkey_button("00"), + # tenkey_button("00"), + tenkey_button("."), tenkey_button("ENTER", width=tenkey_button_size * 2), ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN, @@ -139,12 +621,11 @@ class POSView(WuttaView): meta_button_width = meta_button_height * 2 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, weight=ft.FontWeight.BOLD), height=meta_button_height, width=meta_button_width, - # on_click=meta_click, on_click=on_click, alignment=ft.alignment.center, border=ft.border.all(1, 'black'), @@ -156,29 +637,29 @@ class POSView(WuttaView): [ 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), ], spacing=0, ), ft.Row( [ - meta_button("ITEM"), - meta_button("CUST"), + meta_button("ITEM", bgcolor='blue', on_click=self.not_supported), + meta_button("CUST", bgcolor='blue', on_click=self.customer_click), ], spacing=0, ), ft.Row( [ - meta_button("TODO"), - meta_button("TODO"), + meta_button("TODO", bgcolor='blue', on_click=self.not_supported), + meta_button("TODO", bgcolor='blue', on_click=self.not_supported), ], spacing=0, ), ft.Row( [ - meta_button("TODO"), - meta_button("TODO"), + meta_button("TODO", bgcolor='blue', on_click=self.not_supported), + meta_button("TODO", bgcolor='blue', on_click=self.not_supported), ], spacing=0, ), @@ -217,11 +698,18 @@ class POSView(WuttaView): ), expand=0) + self.set_quantity = ft.Text(value=None, data=None, weight=ft.FontWeight.BOLD, size=40) + return [ self.build_header(), 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, ), @@ -253,22 +741,41 @@ class POSView(WuttaView): kwargs.setdefault('size', 24) return ft.Text(*args, **kwargs) - def did_mount(self): - model = self.model - session = self.app.make_session() + def get_current_batch(self, session, user=None, create=True): + if not user: + user = session.get(self.model.User, self.page.shared['user_uuid']) handler = self.app.get_batch_handler('pos') - user = session.get(model.User, self.page.shared['user_uuid']) - batch = handler.get_current_batch(user, create=True) + return handler.get_current_batch(user, create=create) + + def did_mount(self): + session = self.app.make_session() + batch = self.get_current_batch(session) self.page.shared['txn_display'] = batch.id_str self.items.controls.clear() for row in batch.active_rows(): self.add_row_item(row) + self.items.scroll_to(offset=-1, duration=100) + self.txn_total.value = self.app.render_currency(batch.sales_total) session.commit() 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): quantity = self.app.render_quantity(row.quantity) @@ -282,12 +789,14 @@ class POSView(WuttaView): self.make_text(f"× {quantity} @ {pretty_price}", 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, ), 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): @@ -303,21 +812,16 @@ class POSView(WuttaView): # void current batch handler.void_batch(batch, user) session.flush() + self.clear_all() # make new batch batch = handler.get_current_batch(user, create=True) + self.page.shared['txn_display'] = batch.id_str - # commit changes session.commit() 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.page.update() def cancel(e): @@ -325,12 +829,30 @@ class POSView(WuttaView): self.page.update() dlg = ft.AlertDialog( - modal=True, + # modal=True, title=ft.Text("Confirm VOID"), content=ft.Text("Really VOID this transaction?"), actions=[ - ft.TextButton("Yes", on_click=confirm), - ft.TextButton("No", on_click=cancel), + ft.Container(content=ft.Text("Yes, VOID", + 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 @@ -347,24 +869,39 @@ class POSView(WuttaView): # tender / execute batch tender = e.control.content.value handler.tender_and_execute(batch, user, tender) + self.clear_all() # make new batch 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.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.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') session = self.app.make_session() model = self.model @@ -372,10 +909,13 @@ class POSView(WuttaView): user = session.get(model.User, self.page.shared['user_uuid']) batch = handler.get_current_batch(user) - value = self.main_input.value - row = handler.process_entry(batch, value) + 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: @@ -391,4 +931,6 @@ class POSView(WuttaView): self.main_input.value = "" self.main_input.focus() + self.set_quantity.data = None + self.set_quantity.value = None self.page.update()