From 4eb9442fce18733909131b94789eed3bc9d68ddf Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 6 Jul 2024 23:32:51 -0500 Subject: [PATCH] feat: begin abstraction for more flexible button menus plenty more to do here yet, but i'm mostly happy with these patterns. i don't love the `pos` reference being passed from View down to Button, since any intermediary controls may not (yet?) need the ref but must maintain it so buttons can run pos.cmd() also the `entry` param for cmd() may not be that useful since we can pass arbitrary kwargs? --- wuttapos/app.py | 3 +- wuttapos/controls/buttons/__init__.py | 27 + wuttapos/controls/buttons/base.py | 82 ++ wuttapos/controls/buttons/master.py | 123 +++ wuttapos/controls/header.py | 2 +- wuttapos/controls/loginform.py | 12 +- wuttapos/controls/lookup.py | 5 +- wuttapos/controls/menus/__init__.py | 27 + wuttapos/controls/menus/base.py | 60 ++ wuttapos/controls/menus/context.py | 57 ++ wuttapos/controls/menus/meta.py | 81 ++ wuttapos/controls/menus/suspend.py | 46 + wuttapos/controls/{ => menus}/tenkey.py | 80 +- wuttapos/controls/timestamp.py | 2 +- wuttapos/util.py | 17 +- wuttapos/views/base.py | 3 +- wuttapos/views/pos.py | 1142 ++++++++++------------- 17 files changed, 1052 insertions(+), 717 deletions(-) create mode 100644 wuttapos/controls/buttons/__init__.py create mode 100644 wuttapos/controls/buttons/base.py create mode 100644 wuttapos/controls/buttons/master.py create mode 100644 wuttapos/controls/menus/__init__.py create mode 100644 wuttapos/controls/menus/base.py create mode 100644 wuttapos/controls/menus/context.py create mode 100644 wuttapos/controls/menus/meta.py create mode 100644 wuttapos/controls/menus/suspend.py rename wuttapos/controls/{ => menus}/tenkey.py (69%) diff --git a/wuttapos/app.py b/wuttapos/app.py index 7d690cf..42db530 100644 --- a/wuttapos/app.py +++ b/wuttapos/app.py @@ -41,7 +41,8 @@ from rattail.util import simple_error import flet as ft import wuttapos -from wuttapos.util import get_pos_batch_handler, make_button +from wuttapos.util import get_pos_batch_handler +from wuttapos.controls.buttons import make_button log = logging.getLogger(__name__) diff --git a/wuttapos/controls/buttons/__init__.py b/wuttapos/controls/buttons/__init__.py new file mode 100644 index 0000000..822a2cc --- /dev/null +++ b/wuttapos/controls/buttons/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# WuttaPOS -- Pythonic Point of Sale System +# Copyright © 2023-2024 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 - buttons +""" + +from .base import make_button, WuttaButton, WuttaButtonRow diff --git a/wuttapos/controls/buttons/base.py b/wuttapos/controls/buttons/base.py new file mode 100644 index 0000000..593d22b --- /dev/null +++ b/wuttapos/controls/buttons/base.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# WuttaPOS -- Pythonic Point of Sale System +# Copyright © 2023-2024 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 - buttons +""" + +import flet as ft + + +def make_button(text, font_size=24, font_bold=True, font_weight=None, **kwargs): + """ + Generic function for making a button. + """ + if 'content' not in kwargs: + if not font_weight and font_bold: + font_weight = ft.FontWeight.BOLD + text = ft.Text(text, size=font_size, weight=font_weight, + text_align=ft.TextAlign.CENTER) + kwargs['content'] = text + + return WuttaButton(**kwargs) + + +class WuttaButton(ft.Container): + """ + Base class for buttons to be shown in the POS menu etc. + """ + + def __init__( + self, + pos=None, + pos_cmd=None, + pos_cmd_entry=None, + pos_cmd_kwargs={}, + *args, **kwargs + ): + kwargs.setdefault('alignment', ft.alignment.center) + kwargs.setdefault('border', ft.border.all(1, 'black')) + kwargs.setdefault('border_radius', ft.border_radius.all(5)) + super().__init__(*args, **kwargs) + + self.pos = pos + self.pos_cmd = pos_cmd + self.pos_cmd_entry = pos_cmd_entry + self.pos_cmd_kwargs = pos_cmd_kwargs + + if not kwargs.get('on_click') and self.pos and self.pos_cmd: + self.on_click = self.handle_click + + def handle_click(self, e): + self.pos.cmd(self.pos_cmd, entry=self.pos_cmd_entry, + **self.pos_cmd_kwargs) + + +class WuttaButtonRow(ft.Row): + """ + Base class for a row of buttons + """ + + def __init__(self, *args, **kwargs): + kwargs.setdefault('spacing', 0) + super().__init__(*args, **kwargs) diff --git a/wuttapos/controls/buttons/master.py b/wuttapos/controls/buttons/master.py new file mode 100644 index 0000000..9d9ddc7 --- /dev/null +++ b/wuttapos/controls/buttons/master.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# WuttaPOS -- Pythonic Point of Sale System +# Copyright © 2023-2024 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 - buttons +""" + +import flet as ft + +from wuttapos.controls.menus.tenkey import WuttaTenkeyMenu +from wuttapos.controls.menus.meta import WuttaMetaMenu +from wuttapos.controls.menus.context import WuttaContextMenu +from wuttapos.controls.menus.suspend import WuttaSuspendMenu + + +class WuttaButtonsMaster(ft.Column): + """ + Base class and default implementation for "buttons master" + control. This represents the overall button area in POS view. + """ + + def __init__(self, config, pos=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.config = config + self.app = self.config.get_app() + self.pos = pos + self.controls = self.build_controls() + + def build_controls(self): + self.tenkey_menu = self.build_tenkey_menu() + self.meta_menu = self.build_meta_menu() + self.context_menu = self.build_context_menu() + self.suspend_menu = self.build_suspend_menu() + + return [ + ft.Row( + [ + self.tenkey_menu, + self.meta_menu, + ], + ), + ft.Row( + [ + self.context_menu, + self.suspend_menu, + ], + vertical_alignment=ft.CrossAxisAlignment.START, + ), + ] + + ############################## + # tenkey + ############################## + + def build_tenkey_menu(self): + return WuttaTenkeyMenu( + self.config, + pos=self.pos, + on_char=self.tenkey_char, + on_enter=self.tenkey_enter, + on_up_click=self.tenkey_up_click, + on_up_longpress=self.tenkey_up_longpress, + on_down_click=self.tenkey_down_click, + on_down_longpress=self.tenkey_down_longpress, + ) + + def tenkey_char(self, key): + self.pos.cmd('entry_append', key) + + def tenkey_enter(self, e): + self.pos.cmd('entry_submit') + + def tenkey_up_click(self, e): + self.pos.cmd('scroll_up') + + def tenkey_up_longpress(self, e): + self.pos.cmd('scroll_up_page') + + def tenkey_down_click(self, e): + self.pos.cmd('scroll_down') + + def tenkey_down_longpress(self, e): + self.pos.cmd('scroll_down_page') + + ############################## + # meta + ############################## + + def build_meta_menu(self): + return WuttaMetaMenu(self.config, pos=self.pos) + + ############################## + # context + ############################## + + def build_context_menu(self): + return WuttaContextMenu(self.config, pos=self.pos) + + ############################## + # suspend + ############################## + + def build_suspend_menu(self): + return WuttaSuspendMenu(self.config, pos=self.pos) diff --git a/wuttapos/controls/header.py b/wuttapos/controls/header.py index d2335ee..3a073f1 100644 --- a/wuttapos/controls/header.py +++ b/wuttapos/controls/header.py @@ -31,7 +31,7 @@ import flet as ft import wuttapos from .timestamp import WuttaTimestamp from .feedback import WuttaFeedback -from wuttapos.util import make_button +from wuttapos.controls.buttons import make_button class WuttaHeader(ft.Stack): diff --git a/wuttapos/controls/loginform.py b/wuttapos/controls/loginform.py index 5f59842..7ea8433 100644 --- a/wuttapos/controls/loginform.py +++ b/wuttapos/controls/loginform.py @@ -26,14 +26,14 @@ WuttaPOS - login form control import flet as ft -from .keyboard import WuttaKeyboard -from .tenkey import WuttaTenkeyMenu -from wuttapos.util import make_button +from wuttapos.controls.buttons import make_button +from wuttapos.controls.keyboard import WuttaKeyboard +from wuttapos.controls.menus.tenkey import WuttaTenkeyMenu class WuttaLoginForm(ft.Column): - def __init__(self, config, page=None, *args, **kwargs): + def __init__(self, config, page=None, pos=None, *args, **kwargs): self.on_reset = kwargs.pop('on_reset', None) # permission to be checked for login to succeed @@ -63,6 +63,7 @@ class WuttaLoginForm(ft.Column): self.config = config self.app = config.get_app() self.enum = self.app.enum + self.pos = pos # track which login input has focus self.focused = None @@ -77,7 +78,8 @@ class WuttaLoginForm(ft.Column): [ login_form, ft.VerticalDivider(), - WuttaTenkeyMenu(self.config, simple=True, + WuttaTenkeyMenu(self.config, pos=self.pos, + simple=True, on_char=self.tenkey_char, on_enter=self.tenkey_enter), ], diff --git a/wuttapos/controls/lookup.py b/wuttapos/controls/lookup.py index 750ad37..ef18de5 100644 --- a/wuttapos/controls/lookup.py +++ b/wuttapos/controls/lookup.py @@ -27,7 +27,7 @@ WuttaPOS - base lookup control import flet as ft from .keyboard import WuttaKeyboard -from wuttapos.util import make_button +from wuttapos.controls.buttons import make_button class WuttaLookup(ft.Container): @@ -53,6 +53,9 @@ class WuttaLookup(ft.Container): self.app = config.get_app() self.enum = self.app.enum + # TODO: this feels hacky + self.mypage = page + # track current selection self.selected_uuid = None self.selected_datarow = None diff --git a/wuttapos/controls/menus/__init__.py b/wuttapos/controls/menus/__init__.py new file mode 100644 index 0000000..5f5839d --- /dev/null +++ b/wuttapos/controls/menus/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# WuttaPOS -- Pythonic Point of Sale System +# Copyright © 2023-2024 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 - button menus +""" + +from .base import WuttaMenu diff --git a/wuttapos/controls/menus/base.py b/wuttapos/controls/menus/base.py new file mode 100644 index 0000000..decb22c --- /dev/null +++ b/wuttapos/controls/menus/base.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# WuttaPOS -- Pythonic Point of Sale System +# Copyright © 2023-2024 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 - button menus +""" + +import flet as ft + +from wuttapos.controls.buttons import make_button + + +class WuttaMenu(ft.Container): + """ + Base class for button menu controls. + """ + + # TODO: should be configurable somehow + default_button_size = 100 + default_font_size = 40 + + def __init__(self, config, + pos=None, + *args, **kwargs): + + super().__init__(*args, **kwargs) + + self.config = config + self.app = self.config.get_app() + self.pos = pos + + self.content = ft.Column( + controls=self.build_controls(), + spacing=0) + + def make_button(self, *args, **kwargs): + kwargs.setdefault('font_size', self.default_font_size) + kwargs.setdefault('height', self.default_button_size) + kwargs.setdefault('width', self.default_button_size * 2) + kwargs.setdefault('pos', self.pos) + return make_button(*args, **kwargs) diff --git a/wuttapos/controls/menus/context.py b/wuttapos/controls/menus/context.py new file mode 100644 index 0000000..d08d21b --- /dev/null +++ b/wuttapos/controls/menus/context.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# WuttaPOS -- Pythonic Point of Sale System +# Copyright © 2023-2024 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 - "context" menu +""" + +from .base import WuttaMenu +from wuttapos.controls.buttons import WuttaButtonRow + + +class WuttaContextMenu(WuttaMenu): + + def build_controls(self): + return [ + + WuttaButtonRow([ + + self.make_button("Cash", + bgcolor='orange', + pos_cmd='tender', + pos_cmd_kwargs={'tender': {'code': 'CA'}}), + + self.make_button("Check", + bgcolor='orange', + pos_cmd='tender', + pos_cmd_kwargs={'tender': {'code': 'CK'}}), + ]), + + WuttaButtonRow([ + + self.make_button("Food Stamps", + bgcolor='orange', + font_size=34, + pos_cmd='tender', + pos_cmd_kwargs={'tender': {'code': 'FS'}}), + ]), + ] diff --git a/wuttapos/controls/menus/meta.py b/wuttapos/controls/menus/meta.py new file mode 100644 index 0000000..5c8b0e9 --- /dev/null +++ b/wuttapos/controls/menus/meta.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# WuttaPOS -- Pythonic Point of Sale System +# Copyright © 2023-2024 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 - "meta" menu +""" + +from .base import WuttaMenu +from wuttapos.controls.buttons import WuttaButtonRow + + +class WuttaMetaMenu(WuttaMenu): + + def build_controls(self): + return [ + + WuttaButtonRow([ + + self.make_button("CUST", + bgcolor='blue', + pos_cmd='customer_dwim'), + + self.make_button("VOID", + bgcolor='red', + pos_cmd='void_dwim'), + ]), + + WuttaButtonRow([ + + self.make_button("ITEM", + bgcolor='blue', + pos_cmd='item_dwim'), + + self.make_button("MGR", + bgcolor='yellow', + pos_cmd='manager_dwim'), + ]), + + WuttaButtonRow([ + + self.make_button("OPEN RING", + font_size=32, + bgcolor='blue', + pos_cmd='open_ring_dwim'), + + self.make_button("NO SALE", + bgcolor='yellow', + pos_cmd='no_sale_dwim'), + ]), + + WuttaButtonRow([ + + self.make_button("Adjust\nPrice", + font_size=30, + bgcolor='yellow', + pos_cmd='adjust_price_dwim'), + + self.make_button("REFUND", + bgcolor='red', + pos_cmd='refund_dwim'), + ]), + ] diff --git a/wuttapos/controls/menus/suspend.py b/wuttapos/controls/menus/suspend.py new file mode 100644 index 0000000..6e56770 --- /dev/null +++ b/wuttapos/controls/menus/suspend.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# WuttaPOS -- Pythonic Point of Sale System +# Copyright © 2023-2024 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 - "suspend" menu +""" + +from .base import WuttaMenu +from wuttapos.controls.buttons import WuttaButtonRow + + +class WuttaSuspendMenu(WuttaMenu): + + def build_controls(self): + return [ + + WuttaButtonRow([ + + self.make_button("SUSPEND", + bgcolor='purple', + pos_cmd='suspend_txn'), + + self.make_button("RESUME", + bgcolor='purple', + pos_cmd='resume_txn'), + ]), + ] diff --git a/wuttapos/controls/tenkey.py b/wuttapos/controls/menus/tenkey.py similarity index 69% rename from wuttapos/controls/tenkey.py rename to wuttapos/controls/menus/tenkey.py index effecff..9b83248 100644 --- a/wuttapos/controls/tenkey.py +++ b/wuttapos/controls/menus/tenkey.py @@ -21,21 +21,17 @@ # ################################################################################ """ -WuttaPOS - ten-key control +WuttaPOS - ten-key menu """ import flet as ft -from wuttapos.util import make_button +from .base import WuttaMenu -class WuttaTenkeyMenu(ft.Container): +class WuttaTenkeyMenu(WuttaMenu): - default_font_size = 40 - default_button_size = 100 - - def __init__(self, config, page=None, *args, **kwargs): - self.on_reset = kwargs.pop('on_reset', None) + def __init__(self, *args, **kwargs): self.simple = kwargs.pop('simple', False) self.on_char = kwargs.pop('on_char', None) self.on_enter = kwargs.pop('on_enter', None) @@ -46,9 +42,7 @@ class WuttaTenkeyMenu(ft.Container): super().__init__(*args, **kwargs) - self.config = config - self.app = config.get_app() - self.enum = self.app.enum + def build_controls(self): row1 = [ self.make_tenkey_button("1"), @@ -94,57 +88,39 @@ class WuttaTenkeyMenu(ft.Container): self.make_tenkey_button("ENTER", width=self.default_button_size * 2), ]) - self.content = ft.Column( - [ - ft.Row( - controls=row1, - spacing=0, - ), - ft.Row( - controls=row2, - spacing=0, - ), - ft.Row( - controls=row3, - spacing=0, - ), - ft.Row( - controls=row4, - spacing=0, - ), - ], - spacing=0, - ) - - def informed_refresh(self, **kwargs): - pass - - def reset(self, e=None): - if self.on_reset: - self.on_reset(e=e) + return [ + ft.Row( + controls=row1, + spacing=0, + ), + ft.Row( + controls=row2, + spacing=0, + ), + ft.Row( + controls=row3, + spacing=0, + ), + ft.Row( + controls=row4, + spacing=0, + ), + ] def make_tenkey_button( self, text, - font_size=None, - height=None, width=None, - on_click=None, on_long_press=None, ): - if not font_size: - font_size = self.default_font_size - if not height: - height = self.default_button_size if not width: width = self.default_button_size - if not on_click: - on_click = self.tenkey_click - return make_button(text, font_size=font_size, bgcolor='green', - height=height, width=width, - on_click=on_click, - on_long_press=on_long_press) + return self.make_button(text, + bgcolor='green', + width=width, + on_click=self.tenkey_click, + on_long_press=on_long_press) def tenkey_click(self, e): value = e.control.content.value diff --git a/wuttapos/controls/timestamp.py b/wuttapos/controls/timestamp.py index a3c1c85..d3ff2bc 100644 --- a/wuttapos/controls/timestamp.py +++ b/wuttapos/controls/timestamp.py @@ -49,7 +49,7 @@ class WuttaTimestamp(ft.Text): self.running = False def render_time(self, value): - return value.strftime('%a %d %b %Y %I:%M:%S %p') + return value.strftime('%a %d %b %Y - %I:%M:%S %p') async def update_display(self): while self.running: diff --git a/wuttapos/util.py b/wuttapos/util.py index dab44bd..e892225 100644 --- a/wuttapos/util.py +++ b/wuttapos/util.py @@ -2,7 +2,7 @@ ################################################################################ # # WuttaPOS -- Pythonic Point of Sale System -# Copyright © 2023 Lance Edgar +# Copyright © 2023-2024 Lance Edgar # # This file is part of WuttaPOS. # @@ -38,21 +38,6 @@ def get_pos_batch_handler(config): return app.get_batch_handler('pos', default='rattail.batch.pos:POSBatchHandler') -def make_button(text, font_size=24, font_bold=True, font_weight=None, **kwargs): - """ - Generic function for making a Container button. - """ - if not font_weight and font_bold: - font_weight = ft.FontWeight.BOLD - text = ft.Text(text, size=font_size, weight=font_weight, - text_align=ft.TextAlign.CENTER) - - kwargs.setdefault('alignment', ft.alignment.center) - kwargs.setdefault('border', ft.border.all(1, 'black')) - kwargs.setdefault('border_radius', ft.border_radius.all(5)) - return ft.Container(content=text, **kwargs) - - def show_snackbar(page, text, bgcolor='yellow'): page.snack_bar = ft.SnackBar(ft.Text(text, color='black', size=40, diff --git a/wuttapos/views/base.py b/wuttapos/views/base.py index d8a1f21..3915441 100644 --- a/wuttapos/views/base.py +++ b/wuttapos/views/base.py @@ -31,7 +31,8 @@ from rattail.files import resource_path import flet as ft from wuttapos.controls.header import WuttaHeader -from wuttapos.util import get_pos_batch_handler, make_button, show_snackbar +from wuttapos.controls.buttons import make_button +from wuttapos.util import get_pos_batch_handler, show_snackbar class WuttaView(ft.View): diff --git a/wuttapos/views/pos.py b/wuttapos/views/pos.py index 08b4cd4..13877b0 100644 --- a/wuttapos/views/pos.py +++ b/wuttapos/views/pos.py @@ -2,7 +2,7 @@ ################################################################################ # # WuttaPOS -- Pythonic Point of Sale System -# Copyright © 2023 Lance Edgar +# Copyright © 2023-2024 Lance Edgar # # This file is part of WuttaPOS. # @@ -37,7 +37,8 @@ from wuttapos.controls.itemlookup import WuttaProductLookup from wuttapos.controls.deptlookup import WuttaDepartmentLookup from wuttapos.controls.txnlookup import WuttaTransactionLookup from wuttapos.controls.txnitem import WuttaTxnItem -from wuttapos.controls.tenkey import WuttaTenkeyMenu +from wuttapos.controls.menus.tenkey import WuttaTenkeyMenu +from wuttapos.controls.buttons.master import WuttaButtonsMaster log = logging.getLogger(__name__) @@ -109,25 +110,6 @@ class POSView(WuttaView): self.show_snackbar(f"CUSTOMER SET: {customer}", bgcolor='green') - def item_click(self, e): - - value = self.main_input.value - if value: - if not self.attempt_add_product(): - self.item_lookup(value) - - elif self.selected_item: - row = self.selected_item.data['row'] - if row.product_uuid: - if self.attempt_add_product(uuid=row.product_uuid): - self.clear_item_selection() - self.page.update() - else: - self.item_lookup() - - else: - self.item_lookup() - def refresh_totals(self, batch): reg = ft.TextStyle(size=22) bold = ft.TextStyle(size=24, weight=ft.FontWeight.BOLD) @@ -508,65 +490,6 @@ class POSView(WuttaView): session.close() return bool(customer) - def customer_click(self, e): - - # prompt user to replace customer if already set - if self.page.session.get('cust_uuid'): - self.customer_prompt() - - else: - value = self.main_input.value - if value: - # okay try to set it with given value - if not self.attempt_set_customer(value): - self.customer_lookup(value) - - else: - # no value provided, so do lookup - self.customer_lookup() - - def tenkey_char(self, key): - self.main_input.value = f"{self.main_input.value or ''}{key}" - self.main_input.focus() - self.page.update() - - def tenkey_enter(self, e): - self.main_submit() - - def tenkey_up_click(self, e): - - # select previous item, if selection in progress - if self.selected_item: - i = self.items.controls.index(self.selected_item) - if i > 0: - self.items.scroll_to(delta=-50, duration=100) - self.select_txn_item(self.items.controls[i - 1]) - return - - self.items.scroll_to(delta=-50, duration=100) - self.page.update() - - def tenkey_up_longpress(self, e): - self.items.scroll_to(delta=-500, duration=100) - self.page.update() - - def tenkey_down_click(self, e): - - # select next item, if selection in progress - if self.selected_item: - i = self.items.controls.index(self.selected_item) - if (i + 1) < len(self.items.controls): - self.items.scroll_to(delta=50, duration=100) - self.select_txn_item(self.items.controls[i + 1]) - return - - self.items.scroll_to(delta=50, duration=100) - self.page.update() - - def tenkey_down_longpress(self, e): - self.items.scroll_to(delta=500, duration=100) - self.page.update() - def build_controls(self): session = self.app.make_session() @@ -636,117 +559,6 @@ class POSView(WuttaView): self.main_input.focus() self.page.update() - self.tenkey_menu = WuttaTenkeyMenu(self.config, - on_char=self.tenkey_char, - on_enter=self.tenkey_enter, - on_up_click=self.tenkey_up_click, - on_up_longpress=self.tenkey_up_longpress, - on_down_click=self.tenkey_down_click, - on_down_longpress=self.tenkey_down_longpress) - - meta_button_height = self.default_button_size - meta_button_width = meta_button_height * 2 - meta_font_size = self.default_font_size - - def meta_button(text, font_size=None, bgcolor='blue', data=None, on_click=None): - return self.make_button(text, - font_size=font_size or meta_font_size, - height=meta_button_height, - width=meta_button_width, - bgcolor=bgcolor, - data=data, - on_click=on_click) - - self.meta_menu = ft.Container( - content=ft.Column( - [ - ft.Row( - [ - meta_button("CUST", bgcolor='blue', on_click=self.customer_click), - meta_button("VOID", bgcolor='red', on_click=self.void_click), - ], - spacing=0, - ), - ft.Row( - [ - meta_button("ITEM", bgcolor='blue', on_click=self.item_click), - meta_button("MGR", bgcolor='yellow', on_click=self.manager_click), - ], - spacing=0, - ), - ft.Row( - [ - meta_button("OPEN RING", font_size=32, bgcolor='blue', - on_click=self.open_ring_click), - meta_button("NO SALE", bgcolor='yellow', on_click=self.nosale_click), - ], - spacing=0, - ), - ft.Row( - [ - meta_button("Adjust\nPrice", font_size=30, bgcolor='yellow', - on_click=self.adjust_price_click), - meta_button("REFUND", bgcolor='red', on_click=self.refund_click), - ], - spacing=0, - ), - ], - spacing=0, - ), - expand=0) - - context_button_height = self.default_button_size - context_button_width = context_button_height * 2 - context_font_size = self.default_font_size - - def context_button(text, on_click=None, data=None): - return ft.Container(content=ft.Text(text, size=context_font_size, - weight=ft.FontWeight.BOLD), - height=context_button_height, - width=context_button_width, - on_click=on_click, - data=data, - alignment=ft.alignment.center, - border=ft.border.all(1, 'black'), - border_radius=ft.border_radius.all(5), - bgcolor='orange') - - self.context_menu = ft.Container( - content=ft.Column( - [ - ft.Row( - [ - self.make_cash_button(), - self.make_check_button(), - ], - spacing=0, - ), - ft.Row( - [ - self.make_foodstamp_button(), - ], - spacing=0, - ), - ], - spacing=0, - ), - expand=0) - - self.suspend_menu = ft.Container( - content=ft.Column( - [ - ft.Row( - [ - self.make_suspend_button(), - self.make_resume_button(), - ], - spacing=0, - ), - ], - spacing=0, - ), - expand=0) - self.set_quantity = ft.Text(value=None, data=None, weight=ft.FontWeight.BOLD, size=40) self.set_quantity_button = self.make_button("@", @@ -791,28 +603,16 @@ class POSView(WuttaView): ft.Row( [ self.items_column, - ft.Column( - [ - ft.Row( - [ - self.tenkey_menu, - self.meta_menu, - ], - ), - ft.Row( - [ - self.context_menu, - self.suspend_menu, - ], - vertical_alignment=ft.CrossAxisAlignment.START, - ), - ], - ), + WuttaButtonsMaster(self.config, pos=self), ], vertical_alignment=ft.CrossAxisAlignment.START, ), ] + def make_button(self, *args, **kwargs): + kwargs.setdefault('pos', self) + return super().make_button(*args, **kwargs) + def make_text(self, *args, **kwargs): kwargs.setdefault('weight', ft.FontWeight.BOLD) kwargs.setdefault('size', 24) @@ -845,91 +645,6 @@ class POSView(WuttaView): self.page.update() - def make_tender_button(self, tender, **kwargs): - if isinstance(tender, self.model.Tender): - info = {'tender_code': tender.code, - 'tender_name': tender.name} - else: - info = tender - - kw = { - 'font_size': self.default_font_size, - 'height': self.default_button_size, - 'width': self.default_button_size * 2, - 'on_click': self.tender_click, - 'bgcolor': 'orange', - } - kw.update(kwargs) - kw['data'] = info - return self.make_button(info['tender_name'], **kw) - - def make_cash_button(self, **kwargs): - cash = self.tender_cash or {'tender_code': 'CA', - 'tender_name': "Cash"} - return self.make_tender_button(cash, **kwargs) - - def make_check_button(self, **kwargs): - check = self.tender_check or {'tender_code': 'CK', - 'tender_name': "Check"} - return self.make_tender_button(check, **kwargs) - - def make_foodstamp_button(self, **kwargs): - foodstamp = self.tender_foodstamp or {'tender_code': 'FS', - 'tender_name': "Food Stamps"} - kwargs.setdefault('font_size', 34) - return self.make_tender_button(foodstamp, **kwargs) - - def make_suspend_button(self, **kwargs): - return self.make_button("SUSPEND", bgcolor='purple', - font_size=self.default_font_size, - height=self.default_button_size, - width=self.default_button_size * 2, - on_click=self.suspend_click) - - def suspend_click(self, e): - session = self.app.make_session() - batch = self.get_current_batch(session, create=False) - session.close() - - # nothing to suspend if no batch - if not batch: - self.show_snackbar("NO TRANSACTION", bgcolor='yellow') - self.reset() - return - - def confirm(e): - dlg.open = False - self.page.update() - - # nb. do this just in case we must show login dialog - # cf. https://github.com/flet-dev/flet/issues/1670 - time.sleep(0.1) - - self.authorized_action('pos.suspend', self.suspend_transaction, - message="Suspend Transaction") - - def cancel(e): - dlg.open = False - self.reset() - - # prompt to suspend - dlg = ft.AlertDialog( - title=ft.Text("Confirm SUSPEND"), - content=ft.Text("Really SUSPEND transaction?"), - actions=[ - self.make_button(f"Yes, SUSPEND", font_size=self.default_font_size, - height=self.default_button_size, - width=self.default_button_size * 3, - bgcolor='yellow', - on_click=confirm), - self.make_button("Cancel", font_size=self.default_font_size, - height=self.default_button_size, - width=self.default_button_size * 2.5, - on_click=cancel), - ]) - - self.page.open(dlg) - def suspend_transaction(self, user): session = self.app.make_session() batch = self.get_current_batch(session) @@ -943,56 +658,6 @@ class POSView(WuttaView): self.clear_all() self.reset() - def make_resume_button(self, **kwargs): - return self.make_button("RESUME", bgcolor='purple', - font_size=self.default_font_size, - height=self.default_button_size, - width=self.default_button_size * 2, - on_click=self.resume_click) - - def resume_click(self, e): - session = self.app.make_session() - batch = self.get_current_batch(session, create=False) - session.close() - - # can't resume if txn in progress - if batch: - self.show_snackbar("TRANSACTION IN PROGRESS", bgcolor='yellow') - self.reset() - return - - def select(uuid): - session = self.app.make_session() - user = self.get_current_user(session) - handler = self.get_batch_handler() - - # TODO: this would need to work differently if suspended - # txns are kept in a central server DB - batch = session.get(self.app.model.POSBatch, uuid) - - batch = handler.resume_transaction(batch, user) - session.commit() - - session.refresh(batch) - self.load_batch(batch) - session.close() - - dlg.open = False - self.reset() - - def cancel(e): - dlg.open = False - self.reset() - - # prompt to choose txn - dlg = ft.AlertDialog( - title=ft.Text("Resume Transaction"), - content=WuttaTransactionLookup(self.config, page=self.page, mode='resume', - on_select=select, on_cancel=cancel), - ) - - self.page.open(dlg) - def get_current_user(self, session): uuid = self.page.session.get('user_uuid') if uuid: @@ -1083,74 +748,6 @@ class POSView(WuttaView): return amount - def open_ring_click(self, e): - value = self.main_input.value or None - if not value: - self.show_snackbar("Must first enter an amount") - self.reset() - return - - amount = self.require_decimal(value) - if amount is False: - self.reset() - return - - def select(uuid): - session = self.app.make_session() - user = self.get_current_user(session) - batch = self.get_current_batch(session, user=user) - handler = self.get_batch_handler() - - quantity = 1 - if self.set_quantity.data is not None: - quantity = self.set_quantity.data - - row = handler.add_open_ring(batch, uuid, amount, quantity=quantity, user=user) - session.commit() - - self.add_row_item(row, scroll=True) - self.refresh_totals(batch) - session.close() - - dlg.open = False - self.reset() - - def cancel(e): - dlg.open = False - self.reset(clear_quantity=False) - - dlg = ft.AlertDialog( - modal=True, - title=ft.Text(f"Department Lookup - for {self.app.render_currency(amount)} OPEN RING"), - content=WuttaDepartmentLookup(self.config, on_select=select, on_cancel=cancel), - ) - - self.page.open(dlg) - - def adjust_price_click(self, e): - - if not len(self.items.controls): - self.show_snackbar("There are no line items", bgcolor='yellow') - self.reset() - return - - if not self.selected_item: - self.show_snackbar("Must first select a line item", bgcolor='yellow') - self.main_input.focus() - self.page.update() - return - - row = self.selected_item.data['row'] - if row.void or row.row_type not in (self.enum.POS_ROW_TYPE_SELL, - self.enum.POS_ROW_TYPE_OPEN_RING): - self.show_snackbar("This item cannot be adjusted", bgcolor='yellow') - self.main_input.focus() - self.page.update() - return - - self.authorized_action('pos.override_price', self.adjust_price, - message="Adjust Price") - def adjust_price(self, user): def cancel(e): @@ -1308,70 +905,6 @@ class POSView(WuttaView): self.informed_refresh() self.reset() - def manager_click(self, e): - - def toggle_training(e): - dlg.open = False - self.page.update() - - session = self.app.make_session() - batch = self.get_current_batch(session, create=False) - session.close() - if batch: - self.show_snackbar("TRANSACTION IN PROGRESS") - self.reset() - - else: - # nb. do this just in case we must show login dialog - # cf. https://github.com/flet-dev/flet/issues/1670 - time.sleep(0.1) - - training = self.page.session.get('training') - toggle = "End" if training else "Start" - self.authorized_action('pos.toggle_training', self.toggle_training_mode, - message=f"{toggle} Training Mode") - - def cancel(e): - dlg.open = False - self.reset() - - font_size = 32 - toggle = "End" if self.page.session.get('training') else "Start" - dlg = ft.AlertDialog( - title=ft.Text("Manager Menu"), - content=ft.Text("What would you like to do?", size=20), - actions=[ - self.make_button(f"{toggle} Training", - font_size=font_size, - height=self.default_button_size, - width=self.default_button_size * 2.5, - bgcolor='yellow', - on_click=toggle_training), - self.make_button("Cancel", - font_size=font_size, - height=self.default_button_size, - width=self.default_button_size * 2.5, - on_click=cancel), - ]) - - self.page.open(dlg) - - def nosale_click(self, e): - session = self.app.make_session() - batch = self.get_current_batch(session, create=False) - session.close() - - if batch: - self.show_snackbar("TRANSACTION IN PROGRESS", bgcolor='yellow') - self.page.update() - return - - self.kick_drawer() - - def refund_click(self, e): - self.show_snackbar("TODO: handle refund") - self.page.update() - def kick_drawer(self): self.show_snackbar("TODO: Drawer Kick", bgcolor='yellow') self.page.update() @@ -1463,6 +996,7 @@ class POSView(WuttaView): [ ft.Divider(), WuttaLoginForm(self.config, + pos=self, perm_required=perm, on_login_success=login_success, on_login_failure=login_failure, @@ -1479,7 +1013,500 @@ class POSView(WuttaView): self.page.open(dlg) - def void_click(self, e): + def void_transaction(self, user): + session = self.app.make_session() + batch = self.get_current_batch(session) + user = session.get(user.__class__, user.uuid) + handler = self.get_batch_handler() + + handler.void_batch(batch, user) + + session.commit() + session.close() + self.clear_all() + self.reset() + + def clear_item_selection(self): + if self.selected_item: + self.selected_item.bgcolor = 'white' + self.selected_item.content.refresh() + self.selected_item = None + + def clear_all(self): + self.items.controls.clear() + + self.subtotals.spans.clear() + self.fs_balance.spans.clear() + self.balances.spans.clear() + self.totals_row.bgcolor = None + + self.page.session.set('txn_display', None) + self.page.session.set('cust_uuid', None) + self.page.session.set('cust_display', None) + self.informed_refresh() + + def main_submit(self, e=None): + if self.main_input.value: + self.attempt_add_product(record_badscan=True) + self.reset() + + ############################## + # pos cmd methods + ############################## + + def cmd(self, cmdname, entry=None, **kwargs): + """ + Run a POS command. + """ + meth = getattr(self, f'cmd_{cmdname}', None) + if meth: + meth(entry=entry, **kwargs) + else: + log.warning("unknown cmd requested: %s, entry=%s, %s", + cmdname, repr(entry), kwargs) + self.show_snackbar(f"Unknown command: {cmdname}", bgcolor='yellow') + self.page.update() + + def cmd_adjust_price_dwim(self, entry=None, **kwargs): + + if not len(self.items.controls): + self.show_snackbar("There are no line items", bgcolor='yellow') + self.reset() + return + + if not self.selected_item: + self.show_snackbar("Must first select a line item", bgcolor='yellow') + self.main_input.focus() + self.page.update() + return + + row = self.selected_item.data['row'] + if row.void or row.row_type not in (self.enum.POS_ROW_TYPE_SELL, + self.enum.POS_ROW_TYPE_OPEN_RING): + self.show_snackbar("This item cannot be adjusted", bgcolor='yellow') + self.main_input.focus() + self.page.update() + return + + self.authorized_action('pos.override_price', self.adjust_price, + message="Adjust Price") + + def cmd_customer_dwim(self, entry=None, **kwargs): + + # prompt user to replace customer if already set + if self.page.session.get('cust_uuid'): + self.customer_prompt() + + else: + value = self.main_input.value + if value: + # okay try to set it with given value + if not self.attempt_set_customer(value): + self.customer_lookup(value) + + else: + # no value provided, so do lookup + self.customer_lookup() + + def cmd_entry_append(self, entry=None, **kwargs): + """ + Run a POS command. + """ + if entry is not None: + self.main_input.value = f"{self.main_input.value or ''}{entry}" + self.main_input.focus() + self.page.update() + + def cmd_entry_submit(self, entry=None, **kwargs): + self.main_submit() + + def cmd_item_dwim(self, entry=None, **kwargs): + + value = self.main_input.value + if value: + if not self.attempt_add_product(): + self.item_lookup(value) + + elif self.selected_item: + row = self.selected_item.data['row'] + if row.product_uuid: + if self.attempt_add_product(uuid=row.product_uuid): + self.clear_item_selection() + self.page.update() + else: + self.item_lookup() + + else: + self.item_lookup() + + def cmd_manager_dwim(self, entry=None, **kwargs): + + def toggle_training(e): + dlg.open = False + self.page.update() + + session = self.app.make_session() + batch = self.get_current_batch(session, create=False) + session.close() + if batch: + self.show_snackbar("TRANSACTION IN PROGRESS") + self.reset() + + else: + # nb. do this just in case we must show login dialog + # cf. https://github.com/flet-dev/flet/issues/1670 + time.sleep(0.1) + + training = self.page.session.get('training') + toggle = "End" if training else "Start" + self.authorized_action('pos.toggle_training', self.toggle_training_mode, + message=f"{toggle} Training Mode") + + def cancel(e): + dlg.open = False + self.reset() + + font_size = 32 + toggle = "End" if self.page.session.get('training') else "Start" + dlg = ft.AlertDialog( + title=ft.Text("Manager Menu"), + content=ft.Text("What would you like to do?", size=20), + actions=[ + self.make_button(f"{toggle} Training", + font_size=font_size, + height=self.default_button_size, + width=self.default_button_size * 2.5, + bgcolor='yellow', + on_click=toggle_training), + self.make_button("Cancel", + font_size=font_size, + height=self.default_button_size, + width=self.default_button_size * 2.5, + on_click=cancel), + ]) + + self.page.open(dlg) + + def cmd_no_sale_dwim(self, entry=None, **kwargs): + + session = self.app.make_session() + batch = self.get_current_batch(session, create=False) + session.close() + + if batch: + self.show_snackbar("TRANSACTION IN PROGRESS", bgcolor='yellow') + self.page.update() + return + + self.kick_drawer() + + def cmd_open_ring_dwim(self, entry=None, **kwargs): + + value = self.main_input.value or None + if not value: + self.show_snackbar("Must first enter an amount") + self.reset() + return + + amount = self.require_decimal(value) + if amount is False: + self.reset() + return + + def select(uuid): + session = self.app.make_session() + user = self.get_current_user(session) + batch = self.get_current_batch(session, user=user) + handler = self.get_batch_handler() + + quantity = 1 + if self.set_quantity.data is not None: + quantity = self.set_quantity.data + + row = handler.add_open_ring(batch, uuid, amount, quantity=quantity, user=user) + session.commit() + + self.add_row_item(row, scroll=True) + self.refresh_totals(batch) + session.close() + + dlg.open = False + self.reset() + + def cancel(e): + dlg.open = False + self.reset(clear_quantity=False) + + dlg = ft.AlertDialog( + modal=True, + title=ft.Text(f"Department Lookup - for {self.app.render_currency(amount)} OPEN RING"), + content=WuttaDepartmentLookup(self.config, on_select=select, on_cancel=cancel), + ) + + self.page.open(dlg) + + def cmd_refund_dwim(self, entry=None, **kwargs): + self.show_snackbar("TODO: handle refund") + self.page.update() + + def cmd_resume_txn(self, entry=None, **kwargs): + session = self.app.make_session() + batch = self.get_current_batch(session, create=False) + session.close() + + # can't resume if txn in progress + if batch: + self.show_snackbar("TRANSACTION IN PROGRESS", bgcolor='yellow') + self.reset() + return + + def select(uuid): + session = self.app.make_session() + user = self.get_current_user(session) + handler = self.get_batch_handler() + + # TODO: this would need to work differently if suspended + # txns are kept in a central server DB + batch = session.get(self.app.model.POSBatch, uuid) + + batch = handler.resume_transaction(batch, user) + session.commit() + + session.refresh(batch) + self.load_batch(batch) + session.close() + + dlg.open = False + self.reset() + + def cancel(e): + dlg.open = False + self.reset() + + # prompt to choose txn + dlg = ft.AlertDialog( + title=ft.Text("Resume Transaction"), + content=WuttaTransactionLookup(self.config, page=self.page, mode='resume', + on_select=select, on_cancel=cancel), + ) + + self.page.open(dlg) + + def cmd_scroll_down(self, entry=None, **kwargs): + + # select next item, if selection in progress + if self.selected_item: + i = self.items.controls.index(self.selected_item) + if (i + 1) < len(self.items.controls): + self.items.scroll_to(delta=50, duration=100) + self.select_txn_item(self.items.controls[i + 1]) + return + + self.items.scroll_to(delta=50, duration=100) + self.page.update() + + def cmd_scroll_down_page(self, entry=None, **kwargs): + self.items.scroll_to(delta=500, duration=100) + self.page.update() + + def cmd_scroll_up(self, entry=None, **kwargs): + + # select previous item, if selection in progress + if self.selected_item: + i = self.items.controls.index(self.selected_item) + if i > 0: + self.items.scroll_to(delta=-50, duration=100) + self.select_txn_item(self.items.controls[i - 1]) + return + + self.items.scroll_to(delta=-50, duration=100) + self.page.update() + + def cmd_scroll_up_page(self, entry=None, **kwargs): + self.items.scroll_to(delta=-500, duration=100) + self.page.update() + + def cmd_suspend_txn(self, entry=None, **kwargs): + session = self.app.make_session() + batch = self.get_current_batch(session, create=False) + session.close() + + # nothing to suspend if no batch + if not batch: + self.show_snackbar("NO TRANSACTION", bgcolor='yellow') + self.reset() + return + + def confirm(e): + dlg.open = False + self.page.update() + + # nb. do this just in case we must show login dialog + # cf. https://github.com/flet-dev/flet/issues/1670 + time.sleep(0.1) + + self.authorized_action('pos.suspend', self.suspend_transaction, + message="Suspend Transaction") + + def cancel(e): + dlg.open = False + self.reset() + + # prompt to suspend + dlg = ft.AlertDialog( + title=ft.Text("Confirm SUSPEND"), + content=ft.Text("Really SUSPEND transaction?"), + actions=[ + self.make_button(f"Yes, SUSPEND", font_size=self.default_font_size, + height=self.default_button_size, + width=self.default_button_size * 3, + bgcolor='yellow', + on_click=confirm), + self.make_button("Cancel", font_size=self.default_font_size, + height=self.default_button_size, + width=self.default_button_size * 2.5, + on_click=cancel), + ]) + + self.page.open(dlg) + + def cmd_tender(self, entry=None, **kwargs): + model = self.app.model + session = self.app.make_session() + handler = self.get_batch_handler() + user = self.get_current_user(session) + batch = self.get_current_batch(session, user=user, create=False) + + tender = kwargs.get('tender') + if isinstance(tender, model.Tender): + code = tender.code + elif tender: + code = tender['code'] + elif entry: + code = entry + if not code: + raise ValueError("must specify tender code") + + # nothing to do if no transaction + if not batch: + session.close() + self.show_snackbar("NO TRANSACTION", bgcolor='yellow') + self.reset() + return + + # nothing to do if zero sales + if not batch.get_balance(): + session.close() + self.show_snackbar("NO SALES", bgcolor='yellow') + self.reset() + return + + # nothing to do if no amount provided + if not self.main_input.value: + session.close() + self.show_snackbar("MUST SPECIFY AMOUNT", bgcolor='yellow') + self.reset() + return + + # nothing to do if amount not valid + amount = self.require_decimal(self.main_input.value) + if amount is False: + session.close() + self.reset() + return + + # do nothing if @ quantity present + if self.set_quantity.data: + session.close() + self.show_snackbar(f"QUANTITY NOT ALLOWED FOR TENDER: {self.set_quantity.value}", + bgcolor='yellow') + self.reset() + return + + # tender / execute batch + try: + + # apply tender amount to batch + # nb. this *may* execute the batch! + # nb. we negate the amount supplied by user + rows = handler.apply_tender(batch, user, tender, -amount) + + except Exception as error: + session.rollback() + log.exception("failed to apply tender '%s' for %s in batch %s", + code, amount, batch.id_str) + self.show_snackbar(f"ERROR: {error}", bgcolor='red') + + else: + session.commit() + + # update screen to reflect new items/balance + for row in rows: + self.add_row_item(row, scroll=True) + self.refresh_totals(batch) + + # executed batch means txn was finalized + if batch.executed: + + # look for "change back" row, if found then show alert + last_row = rows[-1] + if last_row.row_type == self.enum.POS_ROW_TYPE_CHANGE_BACK: + + def close_bs(e): + # user dismissed the change back alert; clear screen + bs.open = False + bs.update() + self.clear_all() + self.reset() + + bs = ft.BottomSheet( + ft.Container( + ft.Column( + [ + ft.Text("Change Due", size=24, + weight=ft.FontWeight.BOLD), + ft.Divider(), + ft.Text("Please give customer their change:", + size=20), + ft.Text(self.app.render_currency(last_row.tender_total), + size=32, weight=ft.FontWeight.BOLD), + ft.Container( + content=self.make_button("Dismiss", on_click=close_bs, + height=80, width=120), + alignment=ft.alignment.center, + expand=1, + ), + ], + ), + bgcolor='green', + padding=20, + ), + open=True, + dismissible=False, + ) + + # show change back alert + # nb. we do *not* clear screen yet + self.page.overlay.append(bs) + + else: + # txn finalized but no change back; clear screen + self.clear_all() + + # kick drawer if accepting any tender which requires + # that, or if we are giving change back + first_row = rows[0] + if ((first_row.tender and first_row.tender.kick_drawer) + or last_row.row_type == self.enum.POS_ROW_TYPE_CHANGE_BACK): + self.kick_drawer() + + finally: + session.close() + + self.reset() + + def cmd_void_dwim(self, entry=None, **kwargs): + session = self.app.make_session() batch = self.get_current_batch(session, create=False) session.close() @@ -1559,166 +1586,3 @@ class POSView(WuttaView): ]) self.page.open(dlg) - - def void_transaction(self, user): - session = self.app.make_session() - batch = self.get_current_batch(session) - user = session.get(user.__class__, user.uuid) - handler = self.get_batch_handler() - - handler.void_batch(batch, user) - - session.commit() - session.close() - self.clear_all() - self.reset() - - def tender_click(self, e): - session = self.app.make_session() - handler = self.get_batch_handler() - user = self.get_current_user(session) - batch = self.get_current_batch(session, user=user, create=False) - code = e.control.data['tender_code'] - tender = handler.get_tender(session, code) - - # nothing to do if no transaction - if not batch: - session.close() - self.show_snackbar("NO TRANSACTION", bgcolor='yellow') - self.reset() - return - - # nothing to do if zero sales - if not batch.get_balance(): - session.close() - self.show_snackbar("NO SALES", bgcolor='yellow') - self.reset() - return - - # nothing to do if no amount provided - if not self.main_input.value: - session.close() - self.show_snackbar("MUST SPECIFY AMOUNT", bgcolor='yellow') - self.reset() - return - - # nothing to do if amount not valid - amount = self.require_decimal(self.main_input.value) - if amount is False: - session.close() - self.reset() - return - - # do nothing if @ quantity present - if self.set_quantity.data: - session.close() - self.show_snackbar(f"QUANTITY NOT ALLOWED FOR TENDER: {self.set_quantity.value}", - bgcolor='yellow') - self.reset() - return - - # tender / execute batch - try: - - # apply tender amount to batch - # nb. this *may* execute the batch! - # nb. we negate the amount supplied by user - rows = handler.apply_tender(batch, user, tender or code, -amount) - - except Exception as error: - session.rollback() - log.exception("failed to apply tender '%s' for %s in batch %s", - code, amount, batch.id_str) - self.show_snackbar(f"ERROR: {error}", bgcolor='red') - - else: - session.commit() - - # update screen to reflect new items/balance - for row in rows: - self.add_row_item(row, scroll=True) - self.refresh_totals(batch) - - # executed batch means txn was finalized - if batch.executed: - - # look for "change back" row, if found then show alert - last_row = rows[-1] - if last_row.row_type == self.enum.POS_ROW_TYPE_CHANGE_BACK: - - def close_bs(e): - # user dismissed the change back alert; clear screen - bs.open = False - bs.update() - self.clear_all() - self.reset() - - bs = ft.BottomSheet( - ft.Container( - ft.Column( - [ - ft.Text("Change Due", size=24, - weight=ft.FontWeight.BOLD), - ft.Divider(), - ft.Text("Please give customer their change:", - size=20), - ft.Text(self.app.render_currency(last_row.tender_total), - size=32, weight=ft.FontWeight.BOLD), - ft.Container( - content=self.make_button("Dismiss", on_click=close_bs, - height=80, width=120), - alignment=ft.alignment.center, - expand=1, - ), - ], - ), - bgcolor='green', - padding=20, - ), - open=True, - dismissible=False, - ) - - # show change back alert - # nb. we do *not* clear screen yet - self.page.overlay.append(bs) - - else: - # txn finalized but no change back; clear screen - self.clear_all() - - # kick drawer if accepting any tender which requires - # that, or if we are giving change back - first_row = rows[0] - if ((first_row.tender and first_row.tender.kick_drawer) - or last_row.row_type == self.enum.POS_ROW_TYPE_CHANGE_BACK): - self.kick_drawer() - - finally: - session.close() - - self.reset() - - def clear_item_selection(self): - if self.selected_item: - self.selected_item.bgcolor = 'white' - self.selected_item.content.refresh() - self.selected_item = None - - def clear_all(self): - self.items.controls.clear() - - self.subtotals.spans.clear() - self.fs_balance.spans.clear() - self.balances.spans.clear() - self.totals_row.bgcolor = None - - self.page.session.set('txn_display', None) - self.page.session.set('cust_uuid', None) - self.page.session.set('cust_display', None) - self.informed_refresh() - - def main_submit(self, e=None): - if self.main_input.value: - self.attempt_add_product(record_badscan=True) - self.reset()