add initial contents for terminal gui app
this is pretty rough yet, all needs a refactor
This commit is contained in:
parent
67c97d0847
commit
b566c72a86
40 changed files with 5436 additions and 1 deletions
|
|
@ -42,7 +42,8 @@ dependencies = [
|
|||
|
||||
[project.optional-dependencies]
|
||||
server = ["WuttaWeb[continuum]"]
|
||||
terminal = ["flet[all]<0.80.0"]
|
||||
# terminal = ["flet[all]<0.80.0"]
|
||||
terminal = ["flet[all]<0.21"]
|
||||
|
||||
|
||||
[project.scripts]
|
||||
|
|
|
|||
|
|
@ -31,3 +31,20 @@ class WuttaPosAppProvider(base.AppProvider):
|
|||
"""
|
||||
Custom :term:`app provider` for WuttaPOS.
|
||||
"""
|
||||
|
||||
email_templates = ["wuttapos:email-templates"]
|
||||
|
||||
def get_transaction_handler(self):
|
||||
"""
|
||||
Get the configured :term:`transaction handler`.
|
||||
|
||||
:rtype: :class:`~wuttapos.handler.TransactionHandler`
|
||||
"""
|
||||
if "transaction" not in self.app.handlers:
|
||||
spec = self.config.get(
|
||||
"wuttapos.transaction_handler",
|
||||
default="wuttapos.handler:TransactionHandler",
|
||||
)
|
||||
factory = self.app.load_object(spec)
|
||||
self.app.handlers["transaction"] = factory(self.config)
|
||||
return self.app.handlers["transaction"]
|
||||
|
|
|
|||
|
|
@ -28,3 +28,5 @@ from .base import wuttapos_typer
|
|||
|
||||
# nb. must bring in all modules for discovery to work
|
||||
from . import install
|
||||
from . import run
|
||||
from . import serve
|
||||
|
|
|
|||
39
src/wuttapos/cli/run.py
Normal file
39
src/wuttapos/cli/run.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
See also: :ref:`wuttapos-run`
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
from wuttapos.cli import wuttapos_typer
|
||||
from wuttapos.terminal.app import run_app
|
||||
|
||||
|
||||
@wuttapos_typer.command()
|
||||
def run(ctx: typer.Context):
|
||||
"""
|
||||
Run the WuttaPOS GUI app
|
||||
"""
|
||||
config = ctx.parent.wutta_config
|
||||
run_app(config)
|
||||
69
src/wuttapos/cli/serve.py
Normal file
69
src/wuttapos/cli/serve.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
See also: :ref:`wuttapos-serve`
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import typer
|
||||
|
||||
from wuttapos.cli import wuttapos_typer
|
||||
from wuttjamaican.util import resource_path
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@wuttapos_typer.command()
|
||||
def serve(ctx: typer.Context):
|
||||
"""
|
||||
Run the WuttaPOS web service
|
||||
"""
|
||||
import flet as ft
|
||||
from wuttapos.terminal.app import main
|
||||
|
||||
config = ctx.parent.wutta_config
|
||||
kw = {}
|
||||
|
||||
host = config.get("wuttapos.serve.host", default="0.0.0.0")
|
||||
kw["host"] = host
|
||||
|
||||
port = config.get_int("wuttapos.serve.port", default=8332)
|
||||
kw["port"] = port
|
||||
|
||||
# TODO: we technically "support" this, in that we do pass the
|
||||
# value on to Flet, but in practice it does not work right
|
||||
path = config.get("wuttapos.serve.path", default="")
|
||||
if path:
|
||||
path = path.strip("/") + "/"
|
||||
kw["name"] = path
|
||||
# kw['route_url_strategy'] = 'hash'
|
||||
|
||||
log.info(f"will serve WuttaPOS on http://{host}:{port}/{path}")
|
||||
ft.app(
|
||||
target=main,
|
||||
view=None,
|
||||
assets_dir=resource_path("wuttapos.terminal:assets"),
|
||||
**kw,
|
||||
)
|
||||
31
src/wuttapos/email-templates/pos_feedback.html.mako
Normal file
31
src/wuttapos/email-templates/pos_feedback.html.mako
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<html>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-top: 1em;
|
||||
}
|
||||
p {
|
||||
margin: 1em 0 1em 1.5em;
|
||||
}
|
||||
p.msg {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>User feedback from POS</h1>
|
||||
|
||||
<label>User Name</label>
|
||||
<p>${user_name}</p>
|
||||
|
||||
<label>Referring View</label>
|
||||
<p>${referrer}</p>
|
||||
|
||||
<label>Message</label>
|
||||
<p class="msg">${message}</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
15
src/wuttapos/email-templates/pos_feedback.txt.mako
Normal file
15
src/wuttapos/email-templates/pos_feedback.txt.mako
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
|
||||
# User feedback from POS
|
||||
|
||||
**User Name**
|
||||
|
||||
${user_name}
|
||||
|
||||
**Referring View**
|
||||
|
||||
${referrer}
|
||||
|
||||
**Message**
|
||||
|
||||
${message}
|
||||
36
src/wuttapos/email-templates/uncaught_exception.html.mako
Normal file
36
src/wuttapos/email-templates/uncaught_exception.html.mako
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<html>
|
||||
<body>
|
||||
<h2>Uncaught Exception</h2>
|
||||
|
||||
<p>
|
||||
The following error was not handled properly. Please investigate and fix ASAP.
|
||||
</p>
|
||||
|
||||
<h3>Context</h3>
|
||||
|
||||
% if extra_context is not Undefined and extra_context:
|
||||
<ul>
|
||||
% for key, value in extra_context.items():
|
||||
<li>
|
||||
<span style="font-weight: bold;">${key}:</span>
|
||||
${value}
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
% else:
|
||||
<p>N/A</p>
|
||||
% endif
|
||||
|
||||
<h3>Error</h3>
|
||||
|
||||
<p style="font-weight: bold; padding-left: 2rem;">
|
||||
${error}
|
||||
</p>
|
||||
|
||||
<h3>Traceback</h3>
|
||||
|
||||
<pre class="indent">${traceback}</pre>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
100
src/wuttapos/handler.py
Normal file
100
src/wuttapos/handler.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Transaction Handler
|
||||
"""
|
||||
|
||||
import decimal
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from wuttjamaican.app import GenericHandler
|
||||
|
||||
|
||||
class TransactionHandler(GenericHandler):
|
||||
"""
|
||||
Base class and default implementation for the :term:`transaction
|
||||
handler`.
|
||||
|
||||
This is responsible for business logic while a transaction is
|
||||
being rang up.
|
||||
"""
|
||||
|
||||
def get_terminal_id(self):
|
||||
"""
|
||||
Returns the ID string for current POS terminal.
|
||||
"""
|
||||
return self.config.get("wuttapos.terminal_id")
|
||||
|
||||
def get_screen_txn_display(self, txn, **kwargs):
|
||||
"""
|
||||
Should return the text to be used for displaying transaction
|
||||
identifier within the header of POS screen.
|
||||
"""
|
||||
return "-".join([txn["terminal_id"], txn["cashier_id"], txn["transaction_id"]])
|
||||
|
||||
def get_screen_cust_display(self, txn=None, customer=None, **kwargs):
|
||||
"""
|
||||
Should return the text to be used for displaying customer
|
||||
identifier / name etc. within the header of POS screen.
|
||||
"""
|
||||
|
||||
if not customer and txn:
|
||||
return txn["customer_id"]
|
||||
|
||||
if not customer:
|
||||
return
|
||||
|
||||
# TODO: what about person_number
|
||||
return str(customer.card_number)
|
||||
|
||||
# TODO: should also filter this by terminal?
|
||||
def get_current_transaction(
|
||||
self,
|
||||
user,
|
||||
# terminal_id=None,
|
||||
training_mode=False,
|
||||
# create=True,
|
||||
return_created=False,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Get the "current" POS transaction for the given user, creating
|
||||
it as needed.
|
||||
|
||||
:param training_mode: Flag indicating whether the transaction
|
||||
should be in training mode. The lookup will be restricted
|
||||
according to the value of this flag. If a new batch is
|
||||
created, it will be assigned this flag value.
|
||||
"""
|
||||
if not user:
|
||||
raise ValueError("must specify a user")
|
||||
|
||||
# TODO
|
||||
created = False
|
||||
lines = []
|
||||
if not lines:
|
||||
# if not create:
|
||||
if return_created:
|
||||
return None, False
|
||||
return None
|
||||
|
|
@ -35,3 +35,27 @@ def includeme(config):
|
|||
config.include("wuttapos.server.views.products")
|
||||
config.include("wuttapos.server.views.inventory_adjustments")
|
||||
config.include("wuttapos.server.views.customers")
|
||||
|
||||
# TODO: these should probably live elsewhere?
|
||||
config.add_wutta_permission_group("pos", "POS", overwrite=False)
|
||||
|
||||
config.add_wutta_permission(
|
||||
"pos", "pos.test_error", "Force error to test error handling"
|
||||
)
|
||||
config.add_wutta_permission(
|
||||
"pos", "pos.ring_sales", "Make transactions (ring sales)"
|
||||
)
|
||||
config.add_wutta_permission(
|
||||
"pos", "pos.override_price", "Override price for any item"
|
||||
)
|
||||
config.add_wutta_permission(
|
||||
"pos", "pos.del_customer", "Remove customer from current transaction"
|
||||
)
|
||||
# config.add_wutta_permission('pos', 'pos.resume',
|
||||
# "Resume previously-suspended transaction")
|
||||
config.add_wutta_permission("pos", "pos.toggle_training", "Start/end training mode")
|
||||
config.add_wutta_permission("pos", "pos.suspend", "Suspend current transaction")
|
||||
config.add_wutta_permission(
|
||||
"pos", "pos.swap_customer", "Swap customer for current transaction"
|
||||
)
|
||||
config.add_wutta_permission("pos", "pos.void_txn", "Void current transaction")
|
||||
|
|
|
|||
25
src/wuttapos/terminal/__init__.py
Normal file
25
src/wuttapos/terminal/__init__.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS Terminal
|
||||
"""
|
||||
292
src/wuttapos/terminal/app.py
Normal file
292
src/wuttapos/terminal/app.py
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS app
|
||||
"""
|
||||
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
from traceback import format_exception
|
||||
|
||||
from wuttjamaican.conf import make_config
|
||||
from wuttjamaican.util import resource_path
|
||||
|
||||
import flet as ft
|
||||
|
||||
import wuttapos
|
||||
from wuttapos.terminal.controls.buttons import make_button
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main(page: ft.Page):
|
||||
config = make_config()
|
||||
app = config.get_app()
|
||||
model = app.model
|
||||
handler = app.get_transaction_handler()
|
||||
|
||||
# nb. as of python 3.10 the original hook is accessible, we needn't save the ref
|
||||
# cf. https://docs.python.org/3/library/threading.html#threading.__excepthook__
|
||||
orig_thread_hook = threading.excepthook
|
||||
|
||||
hostname = socket.gethostname()
|
||||
email_context = OrderedDict(
|
||||
[
|
||||
("hostname", hostname),
|
||||
("ipaddress", socket.gethostbyname(hostname)),
|
||||
("terminal", handler.get_terminal_id() or "??"),
|
||||
]
|
||||
)
|
||||
|
||||
def handle_error(exc_type, exc_value, exc_traceback):
|
||||
|
||||
log.exception("unhandled error in POS")
|
||||
|
||||
# nb. ignore this particular error; it is benign
|
||||
if exc_type is RuntimeError and str(exc_value) == "Event loop is closed":
|
||||
log.debug("ignoring error for closed event loop", exc_info=True)
|
||||
return
|
||||
|
||||
extra_context = OrderedDict(email_context)
|
||||
traceback = "".join(
|
||||
format_exception(exc_type, exc_value, exc_traceback)
|
||||
).strip()
|
||||
|
||||
try:
|
||||
uuid = page.session.get("user_uuid")
|
||||
if uuid:
|
||||
session = app.make_session()
|
||||
user = session.get(model.User, uuid)
|
||||
extra_context["username"] = user.username
|
||||
# TODO
|
||||
# batch = handler.get_current_batch(user, create=False)
|
||||
# if batch:
|
||||
# extra_context['batchid'] = batch.id_str
|
||||
session.close()
|
||||
else:
|
||||
extra_context["username"] = "n/a"
|
||||
|
||||
app.send_email(
|
||||
"uncaught_exception",
|
||||
{
|
||||
"extra_context": extra_context,
|
||||
"error": app.render_error(exc_value),
|
||||
"traceback": traceback,
|
||||
},
|
||||
)
|
||||
|
||||
except:
|
||||
log.exception("failed to send error email")
|
||||
|
||||
try:
|
||||
|
||||
def close_bs(e):
|
||||
bs.open = False
|
||||
bs.update()
|
||||
|
||||
bs = ft.BottomSheet(
|
||||
ft.Container(
|
||||
ft.Column(
|
||||
[
|
||||
ft.Text(
|
||||
"Unexpected Error", size=24, weight=ft.FontWeight.BOLD
|
||||
),
|
||||
ft.Divider(),
|
||||
ft.Text(
|
||||
"Please be advised, something unexpected has gone wrong.\n"
|
||||
"The state of your transaction may be questionable.\n\n"
|
||||
"If possible you should consult the IT administrator.\n"
|
||||
"(They may have already received an email about this.)",
|
||||
size=20,
|
||||
),
|
||||
ft.Container(
|
||||
content=make_button(
|
||||
"Dismiss", on_click=close_bs, height=80, width=120
|
||||
),
|
||||
alignment=ft.alignment.center,
|
||||
expand=1,
|
||||
),
|
||||
],
|
||||
),
|
||||
bgcolor="yellow",
|
||||
padding=20,
|
||||
),
|
||||
open=True,
|
||||
dismissible=False,
|
||||
)
|
||||
|
||||
page.overlay.append(bs)
|
||||
page.update()
|
||||
|
||||
except:
|
||||
log.exception("failed to show error bottomsheet")
|
||||
|
||||
def sys_exc_hook(exc_type, exc_value, exc_traceback):
|
||||
handle_error(exc_type, exc_value, exc_traceback)
|
||||
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
||||
|
||||
def thread_exc_hook(args):
|
||||
handle_error(args.exc_type, args.exc_value, args.exc_traceback)
|
||||
# nb. as of python 3.10 could just call threading.__excepthook__ instead
|
||||
# cf. https://docs.python.org/3/library/threading.html#threading.__excepthook__
|
||||
orig_thread_hook(args)
|
||||
|
||||
# custom exception hook for main process
|
||||
sys.excepthook = sys_exc_hook
|
||||
|
||||
# custom exception hook for threads (requires python 3.8)
|
||||
# cf. https://docs.python.org/3/library/threading.html#threading.excepthook
|
||||
v = sys.version_info
|
||||
if v.major >= 3 and v.minor >= 8:
|
||||
threading.excepthook = thread_exc_hook
|
||||
|
||||
page.title = f"WuttaPOS v{wuttapos.__version__}"
|
||||
if hasattr(page, "window"):
|
||||
page.window.full_screen = True
|
||||
else:
|
||||
page.window_full_screen = True
|
||||
|
||||
# global defaults for button/text styles etc.
|
||||
page.data = {
|
||||
"default_button_height_pos": 100,
|
||||
"default_button_height_dlg": 80,
|
||||
}
|
||||
|
||||
def clean_exit():
|
||||
# TODO: this doesn't do anything interesting now, but it could
|
||||
if hasattr(page, "window"):
|
||||
page.window.destroy()
|
||||
else:
|
||||
page.window_destroy()
|
||||
|
||||
def keyboard(e):
|
||||
# exit on ctrl+Q
|
||||
if e.ctrl and e.key == "Q":
|
||||
if not e.shift and not e.alt and not e.meta:
|
||||
clean_exit()
|
||||
|
||||
page.on_keyboard_event = keyboard
|
||||
|
||||
def window_event(e):
|
||||
if e.data == "close":
|
||||
clean_exit()
|
||||
|
||||
# cf. https://flet.dev/docs/controls/page/#window_destroy
|
||||
if hasattr(page, "window"):
|
||||
page.window.prevent_close = True
|
||||
page.window.on_event = window_event
|
||||
else:
|
||||
page.window_prevent_close = True
|
||||
page.window_on_event = window_event
|
||||
|
||||
# TODO: probably these should be auto-loaded from spec
|
||||
from wuttapos.terminal.views.pos import POSView
|
||||
from wuttapos.terminal.views.login import LoginView
|
||||
|
||||
# cf .https://flet.dev/docs/guides/python/navigation-and-routing#building-views-on-route-change
|
||||
|
||||
def route_change(e):
|
||||
page.views.clear()
|
||||
|
||||
redirect = None
|
||||
user_uuid = page.session.get("user_uuid")
|
||||
if page.route == "/login" and user_uuid:
|
||||
redirect = "/pos"
|
||||
other = "/pos"
|
||||
elif page.route == "/pos" and not user_uuid:
|
||||
redirect = "/login"
|
||||
other = "/login"
|
||||
else:
|
||||
redirect = "/pos" if user_uuid else "/login"
|
||||
|
||||
if redirect and page.route != redirect:
|
||||
page.go(redirect)
|
||||
return
|
||||
|
||||
if page.route == "/pos":
|
||||
page.views.append(POSView(config, "/pos"))
|
||||
|
||||
elif page.route == "/login":
|
||||
page.views.append(LoginView(config, "/login"))
|
||||
|
||||
if hasattr(page, "window"):
|
||||
page.window.full_screen = True
|
||||
else:
|
||||
page.window_full_screen = True
|
||||
|
||||
page.update()
|
||||
|
||||
# TODO: this was in example docs but not sure what it's for?
|
||||
# def view_pop(view):
|
||||
# page.views.pop()
|
||||
# top_view = page.views[-1]
|
||||
# page.go(top_view.route)
|
||||
|
||||
page.on_route_change = route_change
|
||||
# page.on_view_pop = view_pop
|
||||
|
||||
# TODO: this may be too hacky but is useful for now/dev
|
||||
if not config.production():
|
||||
|
||||
training = page.client_storage.get("training")
|
||||
page.session.set("training", training)
|
||||
|
||||
user = None
|
||||
uuid = page.client_storage.get("user_uuid")
|
||||
if uuid:
|
||||
session = app.make_session()
|
||||
user = session.get(model.User, uuid)
|
||||
if user:
|
||||
page.session.set("user_uuid", user.uuid.hex)
|
||||
page.session.set("user_display", str(user))
|
||||
|
||||
txn = handler.get_current_transaction(user, create=False)
|
||||
if txn:
|
||||
page.session.set("txn_display", handler.get_screen_txn_display(txn))
|
||||
if txn["customer_id"]:
|
||||
page.session.set("cust_uuid", txn["customer_id"])
|
||||
page.session.set(
|
||||
"cust_display", handler.get_screen_cust_display(txn=txn)
|
||||
)
|
||||
|
||||
session.close()
|
||||
page.go("/pos")
|
||||
return
|
||||
|
||||
session.close()
|
||||
|
||||
page.go("/login")
|
||||
|
||||
|
||||
# TODO: can we inject config to the main() via ft.app() kwargs somehow?
|
||||
# pretty sure the `wuttapos open` command is trying to anyway..
|
||||
def run_app(config=None):
|
||||
ft.app(target=main, assets_dir=resource_path("wuttapos.terminal:assets"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_app()
|
||||
BIN
src/wuttapos/terminal/assets/header_logo.png
Normal file
BIN
src/wuttapos/terminal/assets/header_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
src/wuttapos/terminal/assets/testing.png
Normal file
BIN
src/wuttapos/terminal/assets/testing.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
25
src/wuttapos/terminal/controls/__init__.py
Normal file
25
src/wuttapos/terminal/controls/__init__.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - flet controls
|
||||
"""
|
||||
83
src/wuttapos/terminal/controls/buttons.py
Normal file
83
src/wuttapos/terminal/controls/buttons.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - 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)
|
||||
49
src/wuttapos/terminal/controls/custlookup.py
Normal file
49
src/wuttapos/terminal/controls/custlookup.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - customer lookup control
|
||||
"""
|
||||
|
||||
from .lookup import WuttaLookup
|
||||
|
||||
|
||||
class WuttaCustomerLookup(WuttaLookup):
|
||||
|
||||
def get_results_columns(self):
|
||||
return [
|
||||
self.app.get_customer_key_label(),
|
||||
"Name",
|
||||
"Phone",
|
||||
"Email",
|
||||
]
|
||||
|
||||
def get_results(self, session, entry):
|
||||
return self.app.get_clientele_handler().search_customers(session, entry)
|
||||
|
||||
def make_result_row(self, customer):
|
||||
return [
|
||||
customer["_customer_key_"],
|
||||
customer["name"],
|
||||
customer["phone_number"],
|
||||
customer["email_address"],
|
||||
]
|
||||
76
src/wuttapos/terminal/controls/deptlookup.py
Normal file
76
src/wuttapos/terminal/controls/deptlookup.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - department lookup control
|
||||
"""
|
||||
|
||||
from .lookup import WuttaLookup
|
||||
|
||||
|
||||
class WuttaDepartmentLookup(WuttaLookup):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# nb. this forces first query
|
||||
kwargs.setdefault("initial_search", "")
|
||||
kwargs.setdefault("allow_empty_query", True)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_results_columns(self):
|
||||
return [
|
||||
"Number",
|
||||
"Name",
|
||||
]
|
||||
|
||||
def get_results(self, session, entry):
|
||||
corepos = self.app.get_corepos_handler()
|
||||
op_model = corepos.get_model_lane_op()
|
||||
op_session = corepos.make_session_lane_op()
|
||||
|
||||
query = op_session.query(op_model.Department).order_by(
|
||||
op_model.Department.number
|
||||
)
|
||||
|
||||
if entry:
|
||||
query = query.filter(op_model.Department.name.ilike(f"%{entry}%"))
|
||||
|
||||
departments = []
|
||||
for dept in query:
|
||||
departments.append(
|
||||
{
|
||||
# TODO
|
||||
# 'uuid': dept.uuid,
|
||||
"uuid": str(dept.number),
|
||||
"number": str(dept.number),
|
||||
"name": dept.name,
|
||||
}
|
||||
)
|
||||
op_session.close()
|
||||
return departments
|
||||
|
||||
def make_result_row(self, dept):
|
||||
return [
|
||||
dept["number"],
|
||||
dept["name"],
|
||||
]
|
||||
201
src/wuttapos/terminal/controls/feedback.py
Normal file
201
src/wuttapos/terminal/controls/feedback.py
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - feedback control
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import flet as ft
|
||||
|
||||
from .keyboard import WuttaKeyboard
|
||||
from wuttapos.terminal.util import show_snackbar
|
||||
|
||||
|
||||
class WuttaFeedback(ft.Container):
|
||||
|
||||
default_font_size = 20
|
||||
default_button_height = 60
|
||||
|
||||
def __init__(self, config, page=None, *args, **kwargs):
|
||||
self.on_reset = kwargs.pop("on_reset", None)
|
||||
self.on_send = kwargs.pop("on_send", None)
|
||||
self.on_cancel = kwargs.pop("on_cancel", None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.config = config
|
||||
self.app = config.get_app()
|
||||
self.enum = self.app.enum
|
||||
|
||||
# TODO: why must we save this aside from self.page ?
|
||||
# but sometimes self.page gets set to None, so we must..
|
||||
self.mypage = page
|
||||
|
||||
self.content = ft.Text(
|
||||
"Feedback", size=self.default_font_size, weight=ft.FontWeight.BOLD
|
||||
)
|
||||
self.height = self.default_button_height
|
||||
self.width = self.default_button_height * 3
|
||||
self.on_click = self.initial_click
|
||||
self.alignment = ft.alignment.center
|
||||
self.border = ft.border.all(1, "black")
|
||||
self.border_radius = ft.border_radius.all(5)
|
||||
self.bgcolor = "blue"
|
||||
|
||||
def informed_refresh(self, **kwargs):
|
||||
pass
|
||||
|
||||
def reset(self, e=None):
|
||||
if self.on_reset:
|
||||
self.on_reset(e=e)
|
||||
|
||||
def initial_click(self, e):
|
||||
|
||||
self.message = ft.TextField(
|
||||
label="Message", multiline=True, min_lines=5, autofocus=True
|
||||
)
|
||||
|
||||
self.dlg = ft.AlertDialog(
|
||||
modal=True,
|
||||
title=ft.Text("User Feedback"),
|
||||
content=ft.Container(
|
||||
content=ft.Column(
|
||||
[
|
||||
ft.Text(
|
||||
"Questions, suggestions, comments, complaints, etc. "
|
||||
"are welcome and may be submitted below. "
|
||||
),
|
||||
ft.Divider(),
|
||||
self.message,
|
||||
ft.Divider(),
|
||||
WuttaKeyboard(
|
||||
self.config,
|
||||
on_keypress=self.keypress,
|
||||
on_long_backspace=self.long_backspace,
|
||||
),
|
||||
],
|
||||
expand=True,
|
||||
),
|
||||
height=800,
|
||||
),
|
||||
actions=[
|
||||
ft.Row(
|
||||
[
|
||||
ft.Container(
|
||||
content=ft.Text(
|
||||
"Send Message",
|
||||
size=self.default_font_size,
|
||||
color="black",
|
||||
weight=ft.FontWeight.BOLD,
|
||||
),
|
||||
height=self.default_button_height,
|
||||
width=self.default_button_height * 3,
|
||||
alignment=ft.alignment.center,
|
||||
bgcolor="blue",
|
||||
border=ft.border.all(1, "black"),
|
||||
border_radius=ft.border_radius.all(5),
|
||||
on_click=self.send_feedback,
|
||||
),
|
||||
ft.Container(
|
||||
content=ft.Text(
|
||||
"Cancel",
|
||||
size=self.default_font_size,
|
||||
weight=ft.FontWeight.BOLD,
|
||||
),
|
||||
height=self.default_button_height,
|
||||
width=self.default_button_height * 2.5,
|
||||
alignment=ft.alignment.center,
|
||||
border=ft.border.all(1, "black"),
|
||||
border_radius=ft.border_radius.all(5),
|
||||
on_click=self.cancel,
|
||||
),
|
||||
],
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# # TODO: leaving this for reference just in case..but hoping
|
||||
# the latest flet does not require this hack?
|
||||
|
||||
# if self.mypage.dialog and self.mypage.dialog.open and self.mypage.dialog is not self.dlg:
|
||||
# self.mypage.dialog.open = False
|
||||
# self.mypage.update()
|
||||
# # cf. https://github.com/flet-dev/flet/issues/1670
|
||||
# time.sleep(0.1)
|
||||
|
||||
# self.mypage.open(self.dlg)
|
||||
|
||||
self.mypage.dialog = self.dlg
|
||||
self.dlg.open = True
|
||||
self.mypage.update()
|
||||
|
||||
def keypress(self, key):
|
||||
if key == "⏎":
|
||||
self.message.value += "\n"
|
||||
elif key == "⌫":
|
||||
self.message.value = self.message.value[:-1]
|
||||
else:
|
||||
self.message.value += key
|
||||
|
||||
self.message.focus()
|
||||
|
||||
# TODO: why is keypress happening with no page?
|
||||
if self.page:
|
||||
self.update()
|
||||
|
||||
def long_backspace(self):
|
||||
self.message.value = self.message.value[:-10]
|
||||
self.message.focus()
|
||||
self.update()
|
||||
|
||||
def cancel(self, e):
|
||||
self.dlg.open = False
|
||||
self.mypage.update()
|
||||
|
||||
if self.on_cancel:
|
||||
self.on_cancel(e)
|
||||
|
||||
def send_feedback(self, e):
|
||||
if self.message.value:
|
||||
|
||||
self.app.send_email(
|
||||
"pos_feedback",
|
||||
{
|
||||
"user_name": self.mypage.session.get("user_display"),
|
||||
"referrer": self.mypage.route,
|
||||
"message": self.message.value,
|
||||
},
|
||||
)
|
||||
|
||||
self.dlg.open = False
|
||||
show_snackbar(self.mypage, "MESSAGE WAS SENT", bgcolor="green")
|
||||
self.mypage.update()
|
||||
|
||||
if self.on_send:
|
||||
self.on_send()
|
||||
|
||||
else:
|
||||
self.message.focus()
|
||||
self.mypage.update()
|
||||
281
src/wuttapos/terminal/controls/header.py
Normal file
281
src/wuttapos/terminal/controls/header.py
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - header control
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
import flet as ft
|
||||
|
||||
import wuttapos
|
||||
from .timestamp import WuttaTimestamp
|
||||
from .feedback import WuttaFeedback
|
||||
from wuttapos.terminal.controls.buttons import make_button
|
||||
|
||||
|
||||
class WuttaHeader(ft.Stack):
|
||||
|
||||
def __init__(self, config, page=None, *args, **kwargs):
|
||||
self.terminal_id = kwargs.pop("terminal_id", None)
|
||||
self.on_reset = kwargs.pop("on_reset", None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.config = config
|
||||
self.app = config.get_app()
|
||||
self.enum = self.app.enum
|
||||
|
||||
self.txn_display = ft.Text("Txn: N", weight=ft.FontWeight.BOLD, size=20)
|
||||
self.cust_display = ft.Text("Cust: N", weight=ft.FontWeight.BOLD, size=20)
|
||||
|
||||
self.training_mode = ft.Text(size=40, weight=ft.FontWeight.BOLD)
|
||||
|
||||
self.user_display = ft.Text("User: N", weight=ft.FontWeight.BOLD, size=20)
|
||||
self.logout_button = ft.OutlinedButton(
|
||||
"Logout", on_click=self.logout_click, visible=False
|
||||
)
|
||||
self.logout_divider = ft.VerticalDivider(visible=False)
|
||||
self.title_button = ft.FilledButton(
|
||||
self.app.get_title(), on_click=self.title_click
|
||||
)
|
||||
|
||||
terminal_style = ft.TextStyle(size=20, weight=ft.FontWeight.BOLD)
|
||||
if not self.terminal_id:
|
||||
terminal_style.bgcolor = "red"
|
||||
terminal_style.color = "white"
|
||||
|
||||
self.controls = [
|
||||
ft.Container(
|
||||
content=ft.Row(
|
||||
[
|
||||
ft.Container(
|
||||
content=self.training_mode,
|
||||
bgcolor="yellow",
|
||||
),
|
||||
],
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
),
|
||||
),
|
||||
ft.Row(
|
||||
[
|
||||
ft.Row(
|
||||
[
|
||||
self.txn_display,
|
||||
ft.VerticalDivider(),
|
||||
self.cust_display,
|
||||
ft.VerticalDivider(),
|
||||
WuttaTimestamp(
|
||||
self.config, weight=ft.FontWeight.BOLD, size=20
|
||||
),
|
||||
],
|
||||
),
|
||||
ft.Row(
|
||||
[
|
||||
self.user_display,
|
||||
ft.VerticalDivider(),
|
||||
self.logout_button,
|
||||
self.logout_divider,
|
||||
ft.Text(
|
||||
spans=[
|
||||
ft.TextSpan(
|
||||
style=terminal_style,
|
||||
text=f"Term: {self.terminal_id or '??'}",
|
||||
),
|
||||
],
|
||||
),
|
||||
ft.VerticalDivider(),
|
||||
self.title_button,
|
||||
],
|
||||
),
|
||||
],
|
||||
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
|
||||
),
|
||||
]
|
||||
|
||||
def reset(self, e=None):
|
||||
if self.on_reset:
|
||||
self.on_reset(e=e)
|
||||
|
||||
def did_mount(self):
|
||||
self.informed_refresh()
|
||||
|
||||
def informed_refresh(self):
|
||||
self.update_txn_display()
|
||||
self.update_cust_display()
|
||||
self.update_user_display()
|
||||
self.update_training_display()
|
||||
self.update()
|
||||
|
||||
def update_txn_display(self):
|
||||
txn_display = None
|
||||
if self.page:
|
||||
txn_display = self.page.session.get("txn_display")
|
||||
self.txn_display.value = f"Txn: {txn_display or 'N'}"
|
||||
|
||||
def update_cust_display(self):
|
||||
cust_display = None
|
||||
if self.page:
|
||||
cust_display = self.page.session.get("cust_display")
|
||||
self.cust_display.value = f"Cust: {cust_display or 'N'}"
|
||||
|
||||
def update_training_display(self):
|
||||
if self.page.session.get("training"):
|
||||
self.training_mode.value = " TRAINING MODE "
|
||||
else:
|
||||
self.training_mode.value = ""
|
||||
|
||||
def update_user_display(self):
|
||||
user_display = None
|
||||
if self.page:
|
||||
user_display = self.page.session.get("user_display")
|
||||
self.user_display.value = f"User: {user_display or 'N'}"
|
||||
|
||||
if self.page and self.page.session.get("user_uuid"):
|
||||
self.logout_button.visible = True
|
||||
self.logout_divider.visible = True
|
||||
|
||||
def logout_click(self, e):
|
||||
|
||||
# TODO: hacky but works for now
|
||||
if not self.config.production():
|
||||
self.page.client_storage.set("user_uuid", "")
|
||||
|
||||
self.page.session.clear()
|
||||
self.page.go("/login")
|
||||
|
||||
def title_click(self, e):
|
||||
title = self.app.get_title()
|
||||
|
||||
year = self.app.localtime().year
|
||||
if year > 2026:
|
||||
year_range = f"2026 - {year}"
|
||||
else:
|
||||
year_range = year
|
||||
|
||||
license = f"""\
|
||||
WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
Copyright © {year_range} Lance Edgar
|
||||
|
||||
WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation, either version 3 of the License, or (at your option) any later
|
||||
version.
|
||||
|
||||
WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
buttons = []
|
||||
|
||||
user_uuid = self.page.session.get("user_uuid")
|
||||
if user_uuid:
|
||||
|
||||
session = self.app.make_session()
|
||||
model = self.app.model
|
||||
user = session.get(model.User, user_uuid)
|
||||
auth = self.app.get_auth_handler()
|
||||
has_perm = auth.has_permission(session, user, "pos.test_error")
|
||||
session.close()
|
||||
if has_perm:
|
||||
test_error = make_button(
|
||||
"TEST ERROR",
|
||||
font_size=24,
|
||||
height=60,
|
||||
width=60 * 3,
|
||||
bgcolor="red",
|
||||
on_click=self.test_error_click,
|
||||
)
|
||||
buttons.append(test_error)
|
||||
|
||||
feedback = WuttaFeedback(
|
||||
self.config, page=self.page, on_send=self.reset, on_cancel=self.reset
|
||||
)
|
||||
buttons.append(feedback)
|
||||
|
||||
self.dlg = ft.AlertDialog(
|
||||
title=ft.Text(title),
|
||||
content=ft.Container(
|
||||
content=ft.Column(
|
||||
[
|
||||
ft.Divider(),
|
||||
ft.Text(f"{title} v{wuttapos.__version__}"),
|
||||
ft.Divider(),
|
||||
ft.Text(license),
|
||||
ft.Container(
|
||||
content=ft.Row(
|
||||
controls=buttons,
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
),
|
||||
alignment=ft.alignment.center,
|
||||
expand=True,
|
||||
),
|
||||
],
|
||||
expand=True,
|
||||
),
|
||||
height=600,
|
||||
),
|
||||
actions=[
|
||||
ft.Row(
|
||||
[
|
||||
ft.Container(
|
||||
content=ft.Text(
|
||||
"Close", size=20, weight=ft.FontWeight.BOLD
|
||||
),
|
||||
height=60,
|
||||
width=60 * 2.5,
|
||||
alignment=ft.alignment.center,
|
||||
border=ft.border.all(1, "black"),
|
||||
border_radius=ft.border_radius.all(5),
|
||||
on_click=self.close_dlg,
|
||||
),
|
||||
],
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# self.page.open(self.dlg)
|
||||
|
||||
self.page.dialog = self.dlg
|
||||
self.dlg.open = True
|
||||
self.page.update()
|
||||
|
||||
def test_error_click(self, e):
|
||||
|
||||
# first get the dialog out of the way
|
||||
self.dlg.open = False
|
||||
self.reset()
|
||||
self.page.update()
|
||||
|
||||
raise RuntimeError("FAKE ERROR - to test error handling")
|
||||
|
||||
def close_dlg(self, e):
|
||||
self.dlg.open = False
|
||||
self.reset()
|
||||
self.page.update()
|
||||
47
src/wuttapos/terminal/controls/itemlookup.py
Normal file
47
src/wuttapos/terminal/controls/itemlookup.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - item lookup control
|
||||
"""
|
||||
|
||||
from .lookup import WuttaLookup
|
||||
|
||||
|
||||
class WuttaProductLookup(WuttaLookup):
|
||||
|
||||
def get_results_columns(self):
|
||||
return [
|
||||
self.app.get_product_key_label(),
|
||||
"Description",
|
||||
"Price",
|
||||
]
|
||||
|
||||
def get_results(self, session, entry):
|
||||
return self.app.get_products_handler().search_products(session, entry)
|
||||
|
||||
def make_result_row(self, product):
|
||||
return [
|
||||
product["product_key"],
|
||||
product["full_description"],
|
||||
product["unit_price_display"],
|
||||
]
|
||||
70
src/wuttapos/terminal/controls/itemlookup_dept.py
Normal file
70
src/wuttapos/terminal/controls/itemlookup_dept.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - item lookup for department
|
||||
"""
|
||||
|
||||
from .itemlookup import WuttaProductLookup
|
||||
|
||||
|
||||
class WuttaProductLookupByDepartment(WuttaProductLookup):
|
||||
|
||||
def __init__(self, config, department, *args, **kwargs):
|
||||
|
||||
# nb. this forces first query
|
||||
kwargs.setdefault("initial_search", True)
|
||||
|
||||
kwargs.setdefault("show_search", False)
|
||||
|
||||
super().__init__(config, *args, **kwargs)
|
||||
model = self.app.model
|
||||
|
||||
if isinstance(department, model.Department):
|
||||
self.department_key = department.uuid
|
||||
else:
|
||||
self.department_key = department
|
||||
|
||||
# TODO: should somehow combine these 2 approaches, so the user can
|
||||
# still filter items within a department
|
||||
|
||||
# def get_results(self, session, entry):
|
||||
# return self.app.get_products_handler().search_products(session, entry)
|
||||
|
||||
def get_results(self, session, entry):
|
||||
org = self.app.get_org_handler()
|
||||
prod = self.app.get_products_handler()
|
||||
model = self.app.model
|
||||
|
||||
department = org.get_department(session, self.department_key)
|
||||
if not department:
|
||||
raise ValueError(f"department not found: {self.department_key}")
|
||||
|
||||
products = (
|
||||
session.query(model.Product)
|
||||
.filter(model.Product.department == department)
|
||||
.all()
|
||||
)
|
||||
|
||||
products = [prod.normalize_product(p) for p in products]
|
||||
|
||||
return products
|
||||
200
src/wuttapos/terminal/controls/keyboard.py
Normal file
200
src/wuttapos/terminal/controls/keyboard.py
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - keyboard control
|
||||
"""
|
||||
|
||||
import flet as ft
|
||||
|
||||
|
||||
class WuttaKeyboard(ft.Container):
|
||||
|
||||
default_font_size = 20
|
||||
default_button_size = 80
|
||||
|
||||
def __init__(self, config, page=None, *args, **kwargs):
|
||||
self.on_reset = kwargs.pop("on_reset", None)
|
||||
self.on_keypress = kwargs.pop("on_keypress", None)
|
||||
self.on_long_backspace = kwargs.pop("on_long_backspace", None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.config = config
|
||||
self.app = config.get_app()
|
||||
self.enum = self.app.enum
|
||||
|
||||
self.caps_lock = False
|
||||
self.caps_map = dict([(k, k.upper()) for k in "abcdefghijklmnopqrstuvwxyz"])
|
||||
|
||||
self.shift = False
|
||||
self.shift_map = {
|
||||
"`": "~",
|
||||
"1": "!",
|
||||
"2": "@",
|
||||
"3": "#",
|
||||
"4": "$",
|
||||
"5": "%",
|
||||
"6": "^",
|
||||
"7": "&",
|
||||
"8": "*",
|
||||
"9": "(",
|
||||
"0": ")",
|
||||
"-": "_",
|
||||
"=": "+",
|
||||
"[": "{",
|
||||
"]": "}",
|
||||
"\\": "|",
|
||||
";": ":",
|
||||
"'": '"',
|
||||
",": "<",
|
||||
".": ">",
|
||||
"/": "?",
|
||||
}
|
||||
|
||||
self.keys = {}
|
||||
|
||||
def make_key(
|
||||
key,
|
||||
data=None,
|
||||
on_click=self.simple_keypress,
|
||||
on_long_press=None,
|
||||
width=self.default_button_size,
|
||||
bgcolor=None,
|
||||
):
|
||||
button = ft.Container(
|
||||
content=ft.Text(
|
||||
key, size=self.default_font_size, weight=ft.FontWeight.BOLD
|
||||
),
|
||||
data=data or key,
|
||||
height=self.default_button_size,
|
||||
width=width,
|
||||
on_click=on_click,
|
||||
on_long_press=on_long_press,
|
||||
alignment=ft.alignment.center,
|
||||
border=ft.border.all(1, "black"),
|
||||
border_radius=ft.border_radius.all(5),
|
||||
bgcolor=bgcolor,
|
||||
)
|
||||
self.keys[key] = button
|
||||
return button
|
||||
|
||||
def caps_click(e):
|
||||
self.update_caps_lock(not self.caps_lock)
|
||||
|
||||
self.caps_key = make_key("CAPS", on_click=caps_click)
|
||||
if self.caps_lock:
|
||||
self.caps_key.bgcolor = "blue"
|
||||
|
||||
def shift_click(e):
|
||||
self.update_shift(not self.shift)
|
||||
|
||||
self.shift_key = make_key("SHIFT", on_click=shift_click)
|
||||
if self.shift:
|
||||
self.shift_key.bgcolor = "blue"
|
||||
|
||||
rows = [
|
||||
[make_key(k) for k in "`1234567890-="]
|
||||
+ [make_key("⌫", bgcolor="yellow", on_long_press=self.long_backspace)],
|
||||
[make_key(k) for k in "qwertyuiop[]\\"],
|
||||
[self.caps_key]
|
||||
+ [make_key(k) for k in "asdfghjkl;'"]
|
||||
+ [make_key("⏎", bgcolor="blue")],
|
||||
[self.shift_key] + [make_key(k) for k in "zxcvbnm,./"],
|
||||
[make_key("SPACE", width=self.default_button_size * 5)],
|
||||
]
|
||||
|
||||
rows = [
|
||||
ft.Row(controls, alignment=ft.MainAxisAlignment.CENTER) for controls in rows
|
||||
]
|
||||
|
||||
self.content = ft.Column(rows)
|
||||
|
||||
def informed_refresh(self, **kwargs):
|
||||
pass
|
||||
|
||||
def reset(self, e=None):
|
||||
if self.on_reset:
|
||||
self.on_reset(e=e)
|
||||
|
||||
def update_caps_lock(self, caps_lock):
|
||||
self.caps_lock = caps_lock
|
||||
|
||||
if self.caps_lock:
|
||||
self.caps_key.bgcolor = "blue"
|
||||
else:
|
||||
self.caps_key.bgcolor = None
|
||||
|
||||
for key, button in self.keys.items():
|
||||
if key in self.caps_map:
|
||||
if self.caps_lock or self.shift:
|
||||
button.content.value = self.caps_map[key]
|
||||
else:
|
||||
button.content.value = key
|
||||
|
||||
self.update()
|
||||
|
||||
def update_shift(self, shift):
|
||||
self.shift = shift
|
||||
|
||||
if self.shift:
|
||||
self.shift_key.bgcolor = "blue"
|
||||
else:
|
||||
self.shift_key.bgcolor = None
|
||||
|
||||
for key, button in self.keys.items():
|
||||
if key in self.caps_map:
|
||||
if self.shift or self.caps_lock:
|
||||
button.content.value = self.caps_map[key]
|
||||
else:
|
||||
button.content.value = key
|
||||
elif key in self.shift_map:
|
||||
if self.shift:
|
||||
button.content.value = self.shift_map[key]
|
||||
else:
|
||||
button.content.value = key
|
||||
|
||||
self.update()
|
||||
|
||||
def simple_keypress(self, e):
|
||||
|
||||
# maybe inform parent
|
||||
if self.on_keypress:
|
||||
key = e.control.content.value
|
||||
|
||||
# avoid callback for certain keys
|
||||
if key not in ("CAPS", "SHIFT"):
|
||||
|
||||
# translate certain keys
|
||||
if key == "SPACE":
|
||||
key = " "
|
||||
|
||||
# let 'em know
|
||||
self.on_keypress(key)
|
||||
|
||||
# turn off shift key if set
|
||||
if self.shift:
|
||||
self.update_shift(False)
|
||||
|
||||
def long_backspace(self, e):
|
||||
if self.on_long_backspace:
|
||||
self.on_long_backspace()
|
||||
313
src/wuttapos/terminal/controls/loginform.py
Normal file
313
src/wuttapos/terminal/controls/loginform.py
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - login form control
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import flet as ft
|
||||
|
||||
from wuttapos.terminal.controls.buttons import make_button
|
||||
from wuttapos.terminal.controls.keyboard import WuttaKeyboard
|
||||
from wuttapos.terminal.controls.menus.tenkey import WuttaTenkeyMenu
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WuttaLoginForm(ft.Column):
|
||||
|
||||
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
|
||||
self.perm_required = kwargs.pop("perm_required", "pos.ring_sales")
|
||||
|
||||
# may or may not show the username field
|
||||
# nb. must set this before normal __init__
|
||||
if "show_username" in kwargs:
|
||||
self.show_username = kwargs.pop("show_username")
|
||||
else:
|
||||
self.show_username = config.get_bool(
|
||||
"wuttapos.login.show_username", default=True
|
||||
)
|
||||
|
||||
# may or may not show 10-key menu instead of full keyboard
|
||||
if "use_tenkey" in kwargs:
|
||||
self.use_tenkey = kwargs.pop("use_tenkey")
|
||||
else:
|
||||
self.use_tenkey = config.get_bool(
|
||||
"wuttapos.login.use_tenkey", default=False
|
||||
)
|
||||
|
||||
self.on_login_failure = kwargs.pop("on_login_failure", None)
|
||||
self.on_authz_failure = kwargs.pop("on_authz_failure", None)
|
||||
self.on_login_success = kwargs.pop("on_login_success", None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
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
|
||||
|
||||
login_form = self.build_login_form()
|
||||
self.expand = True
|
||||
self.alignment = ft.MainAxisAlignment.CENTER
|
||||
|
||||
if self.use_tenkey:
|
||||
self.controls = [
|
||||
ft.Row(
|
||||
[
|
||||
login_form,
|
||||
ft.VerticalDivider(),
|
||||
WuttaTenkeyMenu(
|
||||
self.config,
|
||||
pos=self.pos,
|
||||
simple=True,
|
||||
on_char=self.tenkey_char,
|
||||
on_enter=self.tenkey_enter,
|
||||
),
|
||||
],
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
),
|
||||
]
|
||||
|
||||
else: # full keyboard
|
||||
self.controls = [
|
||||
login_form,
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
WuttaKeyboard(
|
||||
self.config,
|
||||
on_keypress=self.keyboard_keypress,
|
||||
on_long_backspace=self.keyboard_long_backspace,
|
||||
),
|
||||
]
|
||||
|
||||
def informed_refresh(self, **kwargs):
|
||||
pass
|
||||
|
||||
def reset(self, e=None):
|
||||
if self.on_reset:
|
||||
self.on_reset(e=e)
|
||||
|
||||
def build_login_form(self):
|
||||
form_fields = []
|
||||
|
||||
self.password = ft.TextField(
|
||||
label="Password",
|
||||
width=200,
|
||||
password=True,
|
||||
on_submit=self.password_submit,
|
||||
on_focus=self.password_focus,
|
||||
autofocus=not self.show_username,
|
||||
)
|
||||
self.focused = self.password
|
||||
|
||||
if self.show_username:
|
||||
self.username = ft.TextField(
|
||||
label="Login",
|
||||
width=200,
|
||||
on_submit=self.username_submit,
|
||||
on_focus=self.username_focus,
|
||||
autofocus=True,
|
||||
)
|
||||
form_fields.append(self.username)
|
||||
self.focused = self.username
|
||||
|
||||
form_fields.append(self.password)
|
||||
|
||||
login_button = make_button(
|
||||
"Login",
|
||||
height=60,
|
||||
width=60 * 2.5,
|
||||
bgcolor="blue",
|
||||
on_click=self.attempt_login,
|
||||
)
|
||||
|
||||
reset_button = make_button(
|
||||
"Clear", height=60, width=60 * 2.5, on_click=self.clear_login
|
||||
)
|
||||
|
||||
if self.use_tenkey:
|
||||
form_fields.extend(
|
||||
[
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
ft.Row(
|
||||
[
|
||||
reset_button,
|
||||
login_button,
|
||||
],
|
||||
),
|
||||
]
|
||||
)
|
||||
return ft.Column(
|
||||
controls=form_fields,
|
||||
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
)
|
||||
|
||||
else: # full keyboard
|
||||
form_fields.extend(
|
||||
[
|
||||
login_button,
|
||||
reset_button,
|
||||
]
|
||||
)
|
||||
return ft.Row(
|
||||
controls=form_fields,
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
)
|
||||
|
||||
def keyboard_keypress(self, key):
|
||||
assert self.focused
|
||||
|
||||
if key == "⏎": # ENTER
|
||||
|
||||
# attempt to submit the login form..
|
||||
if self.show_username and self.focused is self.username:
|
||||
self.username_submit()
|
||||
else:
|
||||
if self.password_submit():
|
||||
|
||||
# here the login has totally worked, which means
|
||||
# this form has fulfilled its purpose. hence must
|
||||
# exit early to avoid update() in case we are to
|
||||
# be redirected etc. otherwise may get errors
|
||||
# trying to update controls which have already
|
||||
# been dropped from the page..
|
||||
return
|
||||
|
||||
elif key == "⌫":
|
||||
self.focused.value = self.focused.value[:-1]
|
||||
|
||||
else:
|
||||
self.focused.value += key
|
||||
|
||||
self.focused.focus()
|
||||
self.update()
|
||||
|
||||
def keyboard_long_backspace(self):
|
||||
assert self.focused
|
||||
self.focused.value = ""
|
||||
self.focused.focus()
|
||||
self.update()
|
||||
|
||||
def tenkey_char(self, key):
|
||||
if key == "@":
|
||||
return
|
||||
|
||||
self.focused.value = f"{self.focused.value or ''}{key}"
|
||||
self.update()
|
||||
|
||||
def tenkey_enter(self, e):
|
||||
if self.show_username and self.focused is self.username:
|
||||
self.username_submit(e)
|
||||
self.update()
|
||||
else:
|
||||
if not self.password_submit(e):
|
||||
self.update()
|
||||
|
||||
def username_focus(self, e):
|
||||
self.focused = self.username
|
||||
|
||||
def username_submit(self, e=None):
|
||||
if self.username.value:
|
||||
self.password.focus()
|
||||
else:
|
||||
self.username.focus()
|
||||
|
||||
def password_focus(self, e):
|
||||
self.focused = self.password
|
||||
|
||||
def password_submit(self, e=None):
|
||||
if self.password.value:
|
||||
return self.attempt_login(e)
|
||||
else:
|
||||
self.password.focus()
|
||||
return False
|
||||
|
||||
def attempt_login(self, e=None):
|
||||
if self.show_username and not self.username.value:
|
||||
self.username.focus()
|
||||
return False
|
||||
if not self.password.value:
|
||||
self.password.focus()
|
||||
return False
|
||||
|
||||
session = self.app.make_session()
|
||||
auth = self.app.get_auth_handler()
|
||||
try:
|
||||
user = auth.authenticate_user(
|
||||
session,
|
||||
self.username.value if self.show_username else None,
|
||||
self.password.value,
|
||||
)
|
||||
except:
|
||||
log.exception("user authentication error")
|
||||
session.close()
|
||||
if self.on_login_failure:
|
||||
self.on_login_failure(e)
|
||||
self.clear_login()
|
||||
return False
|
||||
|
||||
user_display = str(user) if user else None
|
||||
has_perm = (
|
||||
auth.has_permission(session, user, self.perm_required) if user else False
|
||||
)
|
||||
session.close()
|
||||
|
||||
if user:
|
||||
|
||||
if has_perm:
|
||||
if self.on_login_success:
|
||||
self.on_login_success(user, user_display)
|
||||
return True
|
||||
|
||||
else:
|
||||
if self.on_authz_failure:
|
||||
self.on_authz_failure(user, user_display)
|
||||
self.clear_login()
|
||||
|
||||
else:
|
||||
if self.on_login_failure:
|
||||
self.on_login_failure(e)
|
||||
self.clear_login()
|
||||
|
||||
return False
|
||||
|
||||
def clear_login(self, e=None):
|
||||
if self.show_username:
|
||||
self.username.value = ""
|
||||
self.password.value = ""
|
||||
if self.show_username:
|
||||
self.username.focus()
|
||||
else:
|
||||
self.password.focus()
|
||||
self.update()
|
||||
364
src/wuttapos/terminal/controls/lookup.py
Normal file
364
src/wuttapos/terminal/controls/lookup.py
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - base lookup control
|
||||
"""
|
||||
|
||||
import flet as ft
|
||||
|
||||
from .keyboard import WuttaKeyboard
|
||||
from wuttapos.terminal.controls.buttons import make_button
|
||||
|
||||
|
||||
class WuttaLookup(ft.Container):
|
||||
|
||||
default_font_size = 40
|
||||
font_size = default_font_size * 0.8
|
||||
default_button_height_dlg = 80
|
||||
disabled_bgcolor = "#aaaaaa"
|
||||
|
||||
long_scroll_delta = 500
|
||||
|
||||
def __init__(self, config, page=None, *args, **kwargs):
|
||||
self.on_reset = kwargs.pop("on_reset", None)
|
||||
self.show_search = kwargs.pop("show_search", True)
|
||||
self.initial_search = kwargs.pop("initial_search", None)
|
||||
self.allow_empty_query = kwargs.pop("allow_empty_query", False)
|
||||
self.on_select = kwargs.pop("on_select", None)
|
||||
self.on_cancel = kwargs.pop("on_cancel", None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.config = config
|
||||
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
|
||||
|
||||
self.search_results = ft.DataTable(
|
||||
columns=[
|
||||
ft.DataColumn(self.make_cell_text(text))
|
||||
for text in self.get_results_columns()
|
||||
],
|
||||
data_row_min_height=50,
|
||||
)
|
||||
|
||||
self.no_results = ft.Text(
|
||||
"NO RESULTS", size=32, color="red", weight=ft.FontWeight.BOLD, visible=False
|
||||
)
|
||||
|
||||
self.select_button = make_button(
|
||||
"Select",
|
||||
font_size=self.font_size * 0.8,
|
||||
height=self.default_button_height_dlg * 0.8,
|
||||
width=self.default_button_height_dlg * 1.3,
|
||||
on_click=self.select_click,
|
||||
disabled=True,
|
||||
bgcolor=self.disabled_bgcolor,
|
||||
)
|
||||
|
||||
self.up_button = make_button(
|
||||
"↑",
|
||||
font_size=self.font_size,
|
||||
height=self.default_button_height_dlg,
|
||||
width=self.default_button_height_dlg,
|
||||
on_click=self.up_click,
|
||||
on_long_press=self.up_longpress,
|
||||
)
|
||||
|
||||
self.down_button = make_button(
|
||||
"↓",
|
||||
font_size=self.font_size,
|
||||
height=self.default_button_height_dlg,
|
||||
width=self.default_button_height_dlg,
|
||||
on_click=self.down_click,
|
||||
on_long_press=self.down_longpress,
|
||||
)
|
||||
|
||||
self.search_results_wrapper = ft.Column(
|
||||
[
|
||||
self.search_results,
|
||||
self.no_results,
|
||||
],
|
||||
expand=True,
|
||||
height=400,
|
||||
scroll=ft.ScrollMode.AUTO,
|
||||
)
|
||||
|
||||
controls = []
|
||||
|
||||
if self.show_search:
|
||||
|
||||
self.searchbox = ft.TextField(
|
||||
"",
|
||||
text_size=self.font_size * 0.8,
|
||||
on_submit=self.lookup,
|
||||
autofocus=True,
|
||||
expand=True,
|
||||
)
|
||||
|
||||
controls.extend(
|
||||
[
|
||||
ft.Row(
|
||||
[
|
||||
ft.Text("SEARCH FOR:"),
|
||||
self.searchbox,
|
||||
make_button(
|
||||
"Lookup",
|
||||
font_size=self.font_size * 0.8,
|
||||
height=self.default_button_height_dlg * 0.8,
|
||||
width=self.default_button_height_dlg * 1.3,
|
||||
on_click=self.lookup,
|
||||
bgcolor="blue",
|
||||
),
|
||||
make_button(
|
||||
"Reset",
|
||||
font_size=self.font_size * 0.8,
|
||||
height=self.default_button_height_dlg * 0.8,
|
||||
width=self.default_button_height_dlg * 1.3,
|
||||
on_click=self.reset,
|
||||
bgcolor="yellow",
|
||||
),
|
||||
],
|
||||
),
|
||||
ft.Divider(),
|
||||
WuttaKeyboard(
|
||||
self.config,
|
||||
on_keypress=self.keypress,
|
||||
on_long_backspace=self.long_backspace,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
controls.extend(
|
||||
[
|
||||
ft.Divider(),
|
||||
ft.Row(
|
||||
[
|
||||
self.search_results_wrapper,
|
||||
ft.VerticalDivider(),
|
||||
ft.Column(
|
||||
[
|
||||
self.select_button,
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
self.up_button,
|
||||
self.down_button,
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
make_button(
|
||||
"Cancel",
|
||||
font_size=self.font_size * 0.8,
|
||||
height=self.default_button_height_dlg * 0.8,
|
||||
width=self.default_button_height_dlg * 1.3,
|
||||
on_click=self.cancel,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
vertical_alignment=ft.CrossAxisAlignment.START,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
self.content = ft.Column(controls=controls)
|
||||
self.height = None if self.show_search else 600
|
||||
|
||||
def informed_refresh(self, **kwargs):
|
||||
pass
|
||||
|
||||
def reset(self, e=None):
|
||||
if self.on_reset:
|
||||
self.on_reset(e=e)
|
||||
|
||||
def get_results_columns(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def did_mount(self):
|
||||
if self.initial_search is not None:
|
||||
if self.show_search:
|
||||
self.searchbox.value = self.initial_search
|
||||
self.initial_search = None # only do it once
|
||||
self.update()
|
||||
self.lookup()
|
||||
|
||||
def make_cell_text(self, text):
|
||||
return ft.Text(text, size=32)
|
||||
|
||||
def make_cell(self, text):
|
||||
return ft.DataCell(self.make_cell_text(text))
|
||||
|
||||
def cancel(self, e):
|
||||
if self.on_cancel:
|
||||
self.on_cancel(e)
|
||||
|
||||
def keypress(self, key):
|
||||
if key == "⏎":
|
||||
self.lookup()
|
||||
else:
|
||||
if key == "⌫":
|
||||
self.searchbox.value = self.searchbox.value[:-1]
|
||||
else:
|
||||
self.searchbox.value += key
|
||||
self.searchbox.focus()
|
||||
self.update()
|
||||
|
||||
def long_backspace(self):
|
||||
self.searchbox.value = self.searchbox.value[:-10]
|
||||
self.searchbox.focus()
|
||||
self.update()
|
||||
|
||||
def get_results(self, session, entry):
|
||||
raise NotImplementedError
|
||||
|
||||
def make_result_row(self, obj):
|
||||
return obj
|
||||
|
||||
def lookup(self, e=None):
|
||||
|
||||
if self.show_search:
|
||||
entry = self.searchbox.value
|
||||
if not entry and not self.allow_empty_query:
|
||||
self.searchbox.focus()
|
||||
self.update()
|
||||
return
|
||||
else:
|
||||
entry = None
|
||||
|
||||
session = self.app.make_session()
|
||||
results = self.get_results(session, entry)
|
||||
|
||||
self.search_results.rows.clear()
|
||||
self.selected_uuid = None
|
||||
self.select_button.disabled = True
|
||||
self.select_button.bgcolor = self.disabled_bgcolor
|
||||
|
||||
if results:
|
||||
for obj in results:
|
||||
self.search_results.rows.append(
|
||||
ft.DataRow(
|
||||
cells=[
|
||||
self.make_cell(row) for row in self.make_result_row(obj)
|
||||
],
|
||||
on_select_changed=self.select_changed,
|
||||
data={"uuid": obj["uuid"]},
|
||||
)
|
||||
)
|
||||
self.no_results.visible = False
|
||||
|
||||
else:
|
||||
if self.show_search:
|
||||
self.no_results.value = f"NO RESULTS FOR: {entry}"
|
||||
else:
|
||||
self.no_results.value = "NO RESULTS FOUND"
|
||||
self.no_results.visible = True
|
||||
|
||||
if self.show_search:
|
||||
self.searchbox.focus()
|
||||
self.update()
|
||||
|
||||
def reset(self, e):
|
||||
if self.show_search:
|
||||
self.searchbox.value = ""
|
||||
self.search_results.rows.clear()
|
||||
self.no_results.visible = False
|
||||
self.selected_uuid = None
|
||||
self.selected_datarow = None
|
||||
self.select_button.disabled = True
|
||||
self.select_button.bgcolor = self.disabled_bgcolor
|
||||
if self.show_search:
|
||||
self.searchbox.focus()
|
||||
self.update()
|
||||
|
||||
def set_selection(self, row):
|
||||
if self.selected_datarow:
|
||||
self.selected_datarow.color = None
|
||||
|
||||
row.color = ft.colors.BLUE
|
||||
self.selected_uuid = row.data["uuid"]
|
||||
self.selected_datarow = row
|
||||
|
||||
self.select_button.disabled = False
|
||||
self.select_button.bgcolor = "blue"
|
||||
|
||||
def select_changed(self, e):
|
||||
if e.data: # selected
|
||||
self.set_selection(e.control)
|
||||
self.update()
|
||||
|
||||
def up_click(self, e):
|
||||
|
||||
# select previous row, if selection in progress
|
||||
if self.selected_datarow:
|
||||
i = self.search_results.rows.index(self.selected_datarow)
|
||||
if i > 0:
|
||||
self.search_results_wrapper.scroll_to(delta=-48, duration=100)
|
||||
self.set_selection(self.search_results.rows[i - 1])
|
||||
self.update()
|
||||
return
|
||||
|
||||
self.search_results_wrapper.scroll_to(delta=-50, duration=100)
|
||||
self.update()
|
||||
|
||||
def up_longpress(self, e):
|
||||
self.search_results_wrapper.scroll_to(
|
||||
delta=-self.long_scroll_delta, duration=100
|
||||
)
|
||||
self.update()
|
||||
|
||||
def down_click(self, e):
|
||||
|
||||
# select next row, if selection in progress
|
||||
if self.selected_datarow:
|
||||
i = self.search_results.rows.index(self.selected_datarow)
|
||||
if (i + 1) < len(self.search_results.rows):
|
||||
self.search_results_wrapper.scroll_to(delta=48, duration=100)
|
||||
self.set_selection(self.search_results.rows[i + 1])
|
||||
self.update()
|
||||
return
|
||||
|
||||
self.search_results_wrapper.scroll_to(delta=50, duration=100)
|
||||
self.update()
|
||||
|
||||
def down_longpress(self, e):
|
||||
self.search_results_wrapper.scroll_to(
|
||||
delta=self.long_scroll_delta, duration=100
|
||||
)
|
||||
self.update()
|
||||
|
||||
def select_click(self, e):
|
||||
if not self.selected_uuid:
|
||||
raise RuntimeError("no record selected?")
|
||||
if self.on_select:
|
||||
self.on_select(self.selected_uuid)
|
||||
27
src/wuttapos/terminal/controls/menus/__init__.py
Normal file
27
src/wuttapos/terminal/controls/menus/__init__.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - button menus
|
||||
"""
|
||||
|
||||
from .base import WuttaMenu
|
||||
59
src/wuttapos/terminal/controls/menus/base.py
Normal file
59
src/wuttapos/terminal/controls/menus/base.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - button menus
|
||||
"""
|
||||
|
||||
import flet as ft
|
||||
|
||||
from wuttapos.terminal.controls.buttons import make_button, WuttaButtonRow
|
||||
|
||||
|
||||
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)
|
||||
|
||||
def make_button_row(self, *args, **kwargs):
|
||||
return WuttaButtonRow(*args, **kwargs)
|
||||
46
src/wuttapos/terminal/controls/menus/context.py
Normal file
46
src/wuttapos/terminal/controls/menus/context.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - "context" menu
|
||||
"""
|
||||
|
||||
from .base import WuttaMenu
|
||||
|
||||
|
||||
class WuttaContextMenu(WuttaMenu):
|
||||
|
||||
def build_controls(self):
|
||||
# TODO: this should be empty by default, just giving
|
||||
# a couple of exmples until more functionality exists
|
||||
return [
|
||||
self.make_button_row(
|
||||
[
|
||||
self.make_button(
|
||||
"Refresh", bgcolor="blue", font_size=30, pos_cmd="refresh_txn"
|
||||
),
|
||||
self.make_button(
|
||||
"No-op", bgcolor="blue", font_size=30, pos_cmd="noop"
|
||||
),
|
||||
]
|
||||
),
|
||||
]
|
||||
132
src/wuttapos/terminal/controls/menus/master.py
Normal file
132
src/wuttapos/terminal/controls/menus/master.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - menus master control
|
||||
"""
|
||||
|
||||
import flet as ft
|
||||
|
||||
from wuttapos.terminal.controls.menus.tenkey import WuttaTenkeyMenu
|
||||
from wuttapos.terminal.controls.menus.meta import WuttaMetaMenu
|
||||
from wuttapos.terminal.controls.menus.suspend import WuttaSuspendMenu
|
||||
|
||||
|
||||
class WuttaMenuMaster(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):
|
||||
spec = self.config.get(
|
||||
"wuttapos.menus.context.spec",
|
||||
default="wuttapos.terminal.controls.menus.context:WuttaContextMenu",
|
||||
)
|
||||
factory = self.app.load_object(spec)
|
||||
return factory(self.config, pos=self.pos)
|
||||
|
||||
def replace_context_menu(self, menu):
|
||||
controls = menu.build_controls()
|
||||
self.context_menu.content.controls = controls
|
||||
self.update()
|
||||
|
||||
##############################
|
||||
# suspend
|
||||
##############################
|
||||
|
||||
def build_suspend_menu(self):
|
||||
return WuttaSuspendMenu(self.config, pos=self.pos)
|
||||
70
src/wuttapos/terminal/controls/menus/meta.py
Normal file
70
src/wuttapos/terminal/controls/menus/meta.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - "meta" menu
|
||||
"""
|
||||
|
||||
from .base import WuttaMenu
|
||||
|
||||
|
||||
class WuttaMetaMenu(WuttaMenu):
|
||||
|
||||
def build_controls(self):
|
||||
return [
|
||||
self.make_button_row(
|
||||
[
|
||||
self.make_button("CUST", bgcolor="blue", pos_cmd="customer_dwim"),
|
||||
self.make_button("VOID", bgcolor="red", pos_cmd="void_dwim"),
|
||||
]
|
||||
),
|
||||
self.make_button_row(
|
||||
[
|
||||
self.make_button("ITEM", bgcolor="blue", pos_cmd="item_dwim"),
|
||||
self.make_button("MGR", bgcolor="yellow", pos_cmd="manager_dwim"),
|
||||
]
|
||||
),
|
||||
self.make_button_row(
|
||||
[
|
||||
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"
|
||||
),
|
||||
]
|
||||
),
|
||||
self.make_button_row(
|
||||
[
|
||||
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"),
|
||||
]
|
||||
),
|
||||
]
|
||||
69
src/wuttapos/terminal/controls/menus/suspend.py
Normal file
69
src/wuttapos/terminal/controls/menus/suspend.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - "suspend" menu
|
||||
"""
|
||||
|
||||
from .base import WuttaMenu
|
||||
|
||||
|
||||
class WuttaSuspendMenu(WuttaMenu):
|
||||
|
||||
def build_controls(self):
|
||||
return [
|
||||
self.make_button_row(
|
||||
[
|
||||
self.make_button(
|
||||
"SUSPEND", bgcolor="purple", pos_cmd="suspend_txn"
|
||||
),
|
||||
self.make_button("RESUME", bgcolor="purple", pos_cmd="resume_txn"),
|
||||
]
|
||||
),
|
||||
self.make_button_row(
|
||||
[
|
||||
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"}},
|
||||
),
|
||||
]
|
||||
),
|
||||
self.make_button_row(
|
||||
[
|
||||
self.make_button(
|
||||
"Food Stamps",
|
||||
bgcolor="orange",
|
||||
font_size=34,
|
||||
pos_cmd="tender",
|
||||
pos_cmd_kwargs={"tender": {"code": "FS"}},
|
||||
),
|
||||
]
|
||||
),
|
||||
]
|
||||
164
src/wuttapos/terminal/controls/menus/tenkey.py
Normal file
164
src/wuttapos/terminal/controls/menus/tenkey.py
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - ten-key menu
|
||||
"""
|
||||
|
||||
import flet as ft
|
||||
|
||||
from .base import WuttaMenu
|
||||
|
||||
|
||||
class WuttaTenkeyMenu(WuttaMenu):
|
||||
|
||||
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)
|
||||
self.on_up_click = kwargs.pop("on_up_click", None)
|
||||
self.on_up_longpress = kwargs.pop("on_up_longpress", None)
|
||||
self.on_down_click = kwargs.pop("on_down_click", None)
|
||||
self.on_down_longpress = kwargs.pop("on_down_longpress", None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def build_controls(self):
|
||||
|
||||
row1 = [
|
||||
self.make_tenkey_button("1"),
|
||||
self.make_tenkey_button("2"),
|
||||
self.make_tenkey_button("3"),
|
||||
]
|
||||
if not self.simple:
|
||||
row1.extend(
|
||||
[
|
||||
self.make_tenkey_button("-"),
|
||||
]
|
||||
)
|
||||
|
||||
row2 = [
|
||||
self.make_tenkey_button("4"),
|
||||
self.make_tenkey_button("5"),
|
||||
self.make_tenkey_button("6"),
|
||||
]
|
||||
if not self.simple:
|
||||
row2.extend(
|
||||
[
|
||||
self.make_tenkey_button("↑", on_long_press=self.up_long_press),
|
||||
]
|
||||
)
|
||||
|
||||
row3 = [
|
||||
self.make_tenkey_button("7"),
|
||||
self.make_tenkey_button("8"),
|
||||
self.make_tenkey_button("9"),
|
||||
]
|
||||
if not self.simple:
|
||||
row3.extend(
|
||||
[
|
||||
self.make_tenkey_button("↓", on_long_press=self.down_long_press),
|
||||
]
|
||||
)
|
||||
|
||||
row4 = [
|
||||
self.make_tenkey_button("0"),
|
||||
# self.make_tenkey_button("00"),
|
||||
self.make_tenkey_button("."),
|
||||
]
|
||||
if self.simple:
|
||||
row4.extend(
|
||||
[
|
||||
self.make_tenkey_button("⏎"),
|
||||
]
|
||||
)
|
||||
else:
|
||||
row4.extend(
|
||||
[
|
||||
self.make_tenkey_button(
|
||||
"ENTER", width=self.default_button_size * 2
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
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,
|
||||
width=None,
|
||||
on_long_press=None,
|
||||
):
|
||||
if not width:
|
||||
width = self.default_button_size
|
||||
|
||||
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
|
||||
|
||||
if value in ("ENTER", "⏎"):
|
||||
if self.on_enter:
|
||||
self.on_enter(e)
|
||||
|
||||
elif value == "↑": # UP
|
||||
if self.on_up_click:
|
||||
self.on_up_click(e)
|
||||
|
||||
elif value == "↓": # DOWN
|
||||
if self.on_down_click:
|
||||
self.on_down_click(e)
|
||||
|
||||
else: # normal char key
|
||||
if self.on_char:
|
||||
self.on_char(value)
|
||||
|
||||
def up_long_press(self, e):
|
||||
if self.on_up_longpress:
|
||||
self.on_up_longpress(e)
|
||||
|
||||
def down_long_press(self, e):
|
||||
if self.on_down_longpress:
|
||||
self.on_down_longpress(e)
|
||||
74
src/wuttapos/terminal/controls/timestamp.py
Normal file
74
src/wuttapos/terminal/controls/timestamp.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - timestamp control
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import threading
|
||||
import time
|
||||
|
||||
import flet as ft
|
||||
|
||||
|
||||
class WuttaTimestamp(ft.Text):
|
||||
|
||||
def __init__(self, config, page=None, *args, **kwargs):
|
||||
self.on_reset = kwargs.pop("on_reset", None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.config = config
|
||||
self.app = self.config.get_app()
|
||||
|
||||
self.value = self.render_time(datetime.datetime.now())
|
||||
|
||||
def did_mount(self):
|
||||
self.running = True
|
||||
if hasattr(self.page, "run_task"):
|
||||
self.page.run_task(self.update_display)
|
||||
else:
|
||||
# nb. daemonized thread should be stopped when app exits
|
||||
# cf. https://docs.python.org/3/library/threading.html#thread-objects
|
||||
thread = threading.Thread(target=self.update_display_blocking, daemon=True)
|
||||
thread.start()
|
||||
|
||||
def will_unmount(self):
|
||||
self.running = False
|
||||
|
||||
def render_time(self, value):
|
||||
return value.strftime("%a %d %b %Y - %I:%M:%S %p")
|
||||
|
||||
async def update_display(self):
|
||||
while self.running:
|
||||
self.value = self.render_time(datetime.datetime.now())
|
||||
self.update()
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
def update_display_blocking(self):
|
||||
while self.running:
|
||||
# self.value = self.render_time(self.app.localtime())
|
||||
self.value = self.render_time(datetime.datetime.now())
|
||||
self.update()
|
||||
time.sleep(0.5)
|
||||
175
src/wuttapos/terminal/controls/txnitem.py
Normal file
175
src/wuttapos/terminal/controls/txnitem.py
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - txn item control
|
||||
"""
|
||||
|
||||
import flet as ft
|
||||
|
||||
|
||||
class WuttaTxnItem(ft.Row):
|
||||
"""
|
||||
Control for displaying a transaction line item within main POS
|
||||
items list.
|
||||
"""
|
||||
|
||||
font_size = 24
|
||||
|
||||
def __init__(self, config, line, page=None, *args, **kwargs):
|
||||
self.on_reset = kwargs.pop("on_reset", None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.config = config
|
||||
self.app = config.get_app()
|
||||
self.enum = self.app.enum
|
||||
|
||||
self.line = line
|
||||
|
||||
self.major_style = ft.TextStyle(size=self.font_size, weight=ft.FontWeight.BOLD)
|
||||
|
||||
self.minor_style = ft.TextStyle(size=int(self.font_size * 0.8), italic=True)
|
||||
|
||||
# if self.row.row_type in (self.enum.POS_ROW_TYPE_SELL,
|
||||
# self.enum.POS_ROW_TYPE_OPEN_RING):
|
||||
# self.build_item_sell()
|
||||
|
||||
# elif self.row.row_type in (self.enum.POS_ROW_TYPE_TENDER,
|
||||
# self.enum.POS_ROW_TYPE_CHANGE_BACK):
|
||||
# self.build_item_tender()
|
||||
|
||||
if self.line.trans_type in ("I",):
|
||||
self.build_item_sell()
|
||||
|
||||
elif self.line.trans_type in ("T",):
|
||||
self.build_item_tender()
|
||||
|
||||
def build_item_sell(self):
|
||||
|
||||
self.quantity = ft.TextSpan(style=self.minor_style)
|
||||
self.txn_price = ft.TextSpan(style=self.minor_style)
|
||||
|
||||
self.sales_total_style = ft.TextStyle(
|
||||
size=self.font_size, weight=ft.FontWeight.BOLD
|
||||
)
|
||||
|
||||
self.sales_total = ft.TextSpan(style=self.sales_total_style)
|
||||
|
||||
self.fs_flag = ft.TextSpan(style=self.minor_style)
|
||||
self.tax_flag = ft.TextSpan(style=self.minor_style)
|
||||
|
||||
# set initial text display values
|
||||
self.refresh(update=False)
|
||||
|
||||
self.controls = [
|
||||
ft.Text(
|
||||
spans=[
|
||||
ft.TextSpan(f"{self.line.description}", style=self.major_style),
|
||||
ft.TextSpan("× ", style=self.minor_style),
|
||||
self.quantity,
|
||||
ft.TextSpan(" @ ", style=self.minor_style),
|
||||
self.txn_price,
|
||||
],
|
||||
),
|
||||
ft.Text(
|
||||
spans=[
|
||||
self.fs_flag,
|
||||
self.tax_flag,
|
||||
self.sales_total,
|
||||
],
|
||||
),
|
||||
]
|
||||
self.alignment = ft.MainAxisAlignment.SPACE_BETWEEN
|
||||
|
||||
def build_item_tender(self):
|
||||
self.controls = [
|
||||
ft.Text(
|
||||
spans=[
|
||||
ft.TextSpan(f"{self.line.description}", style=self.major_style),
|
||||
],
|
||||
),
|
||||
ft.Text(
|
||||
spans=[
|
||||
ft.TextSpan(
|
||||
self.app.render_currency(self.line.total),
|
||||
style=self.major_style,
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
self.alignment = ft.MainAxisAlignment.SPACE_BETWEEN
|
||||
|
||||
def informed_refresh(self, **kwargs):
|
||||
pass
|
||||
|
||||
def reset(self, e=None):
|
||||
if self.on_reset:
|
||||
self.on_reset(e=e)
|
||||
|
||||
def refresh(self, update=True):
|
||||
|
||||
# if self.row.void:
|
||||
if self.line.voided:
|
||||
self.major_style.color = None
|
||||
self.major_style.decoration = ft.TextDecoration.LINE_THROUGH
|
||||
self.major_style.weight = None
|
||||
self.minor_style.color = None
|
||||
self.minor_style.decoration = ft.TextDecoration.LINE_THROUGH
|
||||
else:
|
||||
self.major_style.color = None
|
||||
self.major_style.decoration = None
|
||||
self.major_style.weight = ft.FontWeight.BOLD
|
||||
self.minor_style.color = None
|
||||
self.minor_style.decoration = None
|
||||
|
||||
# if self.row.row_type in (self.enum.POS_ROW_TYPE_SELL,
|
||||
# self.enum.POS_ROW_TYPE_OPEN_RING):
|
||||
if self.line.trans_type in ("I",):
|
||||
self.quantity.text = self.app.render_quantity(self.line.ItemQtty)
|
||||
self.txn_price.text = self.app.render_currency(self.line.unitPrice)
|
||||
self.sales_total.text = self.app.render_currency(self.line.total)
|
||||
self.fs_flag.text = "FS " if self.line.foodstamp else ""
|
||||
self.tax_flag.text = f"T{self.line.tax} " if self.line.tax else ""
|
||||
|
||||
if self.line.voided:
|
||||
self.sales_total_style.color = None
|
||||
self.sales_total_style.decoration = ft.TextDecoration.LINE_THROUGH
|
||||
self.sales_total_style.weight = None
|
||||
else:
|
||||
# if (self.row.row_type == self.enum.POS_ROW_TYPE_SELL
|
||||
# and self.row.txn_price_adjusted):
|
||||
# self.sales_total_style.color = 'orange'
|
||||
# elif (self.row.row_type == self.enum.POS_ROW_TYPE_SELL
|
||||
# and self.row.cur_price and self.row.cur_price < self.row.reg_price):
|
||||
# self.sales_total_style.color = 'green'
|
||||
# else:
|
||||
# self.sales_total_style.color = None
|
||||
|
||||
# TODO
|
||||
self.sales_total_style.color = None
|
||||
|
||||
self.sales_total_style.decoration = None
|
||||
self.sales_total_style.weight = ft.FontWeight.BOLD
|
||||
|
||||
if update:
|
||||
self.update()
|
||||
97
src/wuttapos/terminal/controls/txnlookup.py
Normal file
97
src/wuttapos/terminal/controls/txnlookup.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - transaction lookup control
|
||||
"""
|
||||
|
||||
from .lookup import WuttaLookup
|
||||
|
||||
|
||||
class WuttaTransactionLookup(WuttaLookup):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# nb. this forces first query
|
||||
kwargs.setdefault("initial_search", True)
|
||||
|
||||
# TODO: how to deal with 'modes'
|
||||
self.mode = kwargs.pop("mode", None)
|
||||
if not self.mode:
|
||||
raise ValueError("must specify mode")
|
||||
if self.mode != "resume":
|
||||
raise ValueError("only 'resume' mode is supported")
|
||||
|
||||
kwargs.setdefault("show_search", False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_results_columns(self):
|
||||
return [
|
||||
"Date/Time",
|
||||
"Terminal",
|
||||
"Txn ID",
|
||||
"Cashier",
|
||||
"Customer",
|
||||
"Balance",
|
||||
]
|
||||
|
||||
def get_results(self, session, entry):
|
||||
# model = self.app.model
|
||||
|
||||
# # TODO: how to deal with 'modes'
|
||||
# assert self.mode == 'resume'
|
||||
# training = bool(self.mypage.session.get('training'))
|
||||
# query = session.query(model.POSBatch)\
|
||||
# .filter(model.POSBatch.status_code == model.POSBatch.STATUS_SUSPENDED)\
|
||||
# .filter(model.POSBatch.executed == None)\
|
||||
# .filter(model.POSBatch.training_mode == training)\
|
||||
# .order_by(model.POSBatch.created.desc())
|
||||
|
||||
# transactions = []
|
||||
# for batch in query:
|
||||
# # TODO: should use 'suspended' timestamp instead here?
|
||||
# # dt = self.app.localtime(batch.created, from_utc=True)
|
||||
# dt = batch.created
|
||||
# transactions.append({
|
||||
# 'uuid': batch.uuid,
|
||||
# 'datetime': self.app.render_datetime(dt),
|
||||
# 'terminal': batch.terminal_id,
|
||||
# 'txnid': batch.id_str,
|
||||
# 'cashier': str(batch.cashier or ''),
|
||||
# 'customer': str(batch.customer or ''),
|
||||
# 'balance': self.app.render_currency(batch.get_balance()),
|
||||
# })
|
||||
# return transactions
|
||||
|
||||
# TODO
|
||||
return []
|
||||
|
||||
def make_result_row(self, txn):
|
||||
return [
|
||||
txn["datetime"],
|
||||
txn["terminal"],
|
||||
txn["txnid"],
|
||||
txn["cashier"],
|
||||
txn["customer"],
|
||||
txn["balance"],
|
||||
]
|
||||
37
src/wuttapos/terminal/util.py
Normal file
37
src/wuttapos/terminal/util.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS utilities
|
||||
"""
|
||||
|
||||
import flet as ft
|
||||
|
||||
|
||||
def show_snackbar(page, text, bgcolor="yellow"):
|
||||
snack_bar = ft.SnackBar(
|
||||
ft.Text(text, color="black", size=40, weight=ft.FontWeight.BOLD),
|
||||
bgcolor=bgcolor,
|
||||
duration=1500,
|
||||
)
|
||||
page.overlay.append(snack_bar)
|
||||
snack_bar.open = True
|
||||
25
src/wuttapos/terminal/views/__init__.py
Normal file
25
src/wuttapos/terminal/views/__init__.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - flet views
|
||||
"""
|
||||
107
src/wuttapos/terminal/views/base.py
Normal file
107
src/wuttapos/terminal/views/base.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - flet views (base class)
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from wuttjamaican.util import resource_path
|
||||
|
||||
import flet as ft
|
||||
|
||||
from wuttapos.terminal.controls.header import WuttaHeader
|
||||
from wuttapos.terminal.controls.buttons import make_button
|
||||
from wuttapos.terminal.util import show_snackbar
|
||||
|
||||
|
||||
class WuttaView(ft.View):
|
||||
"""
|
||||
Base class for all Flet views used in WuttaPOS
|
||||
"""
|
||||
|
||||
def __init__(self, config, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.config = config
|
||||
self.app = self.config.get_app()
|
||||
|
||||
controls = self.build_controls()
|
||||
self.controls = [
|
||||
WuttaViewContainer(
|
||||
self.config, content=ft.Column(controls=controls), expand=True
|
||||
),
|
||||
]
|
||||
|
||||
def build_controls(self):
|
||||
return [self.build_header()]
|
||||
|
||||
def build_header(self):
|
||||
handler = self.get_transaction_handler()
|
||||
self.header = WuttaHeader(
|
||||
self.config, on_reset=self.reset, terminal_id=handler.get_terminal_id()
|
||||
)
|
||||
return self.header
|
||||
|
||||
def get_transaction_handler(self):
|
||||
return self.app.get_transaction_handler()
|
||||
|
||||
def make_button(self, *args, **kwargs):
|
||||
return make_button(*args, **kwargs)
|
||||
|
||||
def reset(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def make_logo_image(self, **kwargs):
|
||||
|
||||
# we have a default header logo, but prefer custom if present
|
||||
custom = resource_path("wuttapos.terminal:assets/custom_header_logo.png")
|
||||
if os.path.exists(custom):
|
||||
logo = "/custom_header_logo.png"
|
||||
else:
|
||||
logo = "/header_logo.png"
|
||||
|
||||
# but config can override in any case
|
||||
logo = self.config.get("wuttapos.header.logo", default=logo)
|
||||
|
||||
kwargs.setdefault("height", 100)
|
||||
return ft.Image(src=logo, **kwargs)
|
||||
|
||||
def show_snackbar(self, text, bgcolor="yellow"):
|
||||
show_snackbar(self.page, text, bgcolor=bgcolor)
|
||||
|
||||
|
||||
class WuttaViewContainer(ft.Container):
|
||||
"""
|
||||
Main container class to wrap all controls for a view. Used for
|
||||
displaying background image etc.
|
||||
"""
|
||||
|
||||
def __init__(self, config, *args, **kwargs):
|
||||
self.config = config
|
||||
|
||||
# # add testing watermark when not in production
|
||||
# if "image_src" not in kwargs and not self.config.production():
|
||||
# kwargs["image_src"] = "/testing.png"
|
||||
# kwargs.setdefault("image_repeat", ft.ImageRepeat.REPEAT)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
90
src/wuttapos/terminal/views/login.py
Normal file
90
src/wuttapos/terminal/views/login.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - login view
|
||||
"""
|
||||
|
||||
import flet as ft
|
||||
|
||||
from .base import WuttaView
|
||||
from wuttapos.terminal.controls.loginform import WuttaLoginForm
|
||||
|
||||
|
||||
class LoginView(WuttaView):
|
||||
"""
|
||||
Main POS view for WuttaPOS
|
||||
"""
|
||||
|
||||
def build_controls(self):
|
||||
title = self.app.get_title()
|
||||
|
||||
controls = [
|
||||
ft.Row(
|
||||
[self.make_logo_image(height=200)],
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
),
|
||||
ft.Row(
|
||||
[
|
||||
ft.Text(
|
||||
value=f"Welcome to {title}", weight=ft.FontWeight.BOLD, size=28
|
||||
)
|
||||
],
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
),
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
WuttaLoginForm(
|
||||
self.config,
|
||||
on_login_failure=self.login_failure,
|
||||
on_authz_failure=self.authz_failure,
|
||||
on_login_success=self.login_success,
|
||||
),
|
||||
]
|
||||
|
||||
return [
|
||||
self.build_header(),
|
||||
ft.Column(
|
||||
controls=controls, expand=True, alignment=ft.MainAxisAlignment.CENTER
|
||||
),
|
||||
]
|
||||
|
||||
def login_failure(self, e):
|
||||
self.show_snackbar("Login failed!", bgcolor="yellow")
|
||||
self.page.update()
|
||||
|
||||
def authz_failure(self, user, user_display):
|
||||
self.show_snackbar(
|
||||
f"User not allowed to ring sales: {user_display}", bgcolor="yellow"
|
||||
)
|
||||
self.page.update()
|
||||
|
||||
def login_success(self, user, user_display):
|
||||
self.page.session.set("user_uuid", user.uuid.hex)
|
||||
self.page.session.set("user_display", user_display)
|
||||
|
||||
# TODO: hacky but works for now
|
||||
if not self.config.production():
|
||||
self.page.client_storage.set("user_uuid", user.uuid.hex)
|
||||
|
||||
self.page.go("/pos")
|
||||
1903
src/wuttapos/terminal/views/pos.py
Normal file
1903
src/wuttapos/terminal/views/pos.py
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue