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