add initial contents for terminal gui app

this is pretty rough yet, all needs a refactor
This commit is contained in:
Lance Edgar 2026-01-01 20:39:20 -06:00
parent 67c97d0847
commit b566c72a86
40 changed files with 5436 additions and 1 deletions

View file

@ -42,7 +42,8 @@ dependencies = [
[project.optional-dependencies] [project.optional-dependencies]
server = ["WuttaWeb[continuum]"] server = ["WuttaWeb[continuum]"]
terminal = ["flet[all]<0.80.0"] # terminal = ["flet[all]<0.80.0"]
terminal = ["flet[all]<0.21"]
[project.scripts] [project.scripts]

View file

@ -31,3 +31,20 @@ class WuttaPosAppProvider(base.AppProvider):
""" """
Custom :term:`app provider` for WuttaPOS. 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"]

View file

@ -28,3 +28,5 @@ from .base import wuttapos_typer
# nb. must bring in all modules for discovery to work # nb. must bring in all modules for discovery to work
from . import install from . import install
from . import run
from . import serve

39
src/wuttapos/cli/run.py Normal file
View 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
View 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,
)

View 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>

View file

@ -0,0 +1,15 @@
## -*- coding: utf-8; -*-
# User feedback from POS
**User Name**
${user_name}
**Referring View**
${referrer}
**Message**
${message}

View file

@ -0,0 +1,36 @@
## -*- coding: utf-8; -*-
<html>
<body>
<h2>Uncaught Exception</h2>
<p>
The following error was not handled properly.&nbsp; 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
View 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

View file

@ -35,3 +35,27 @@ def includeme(config):
config.include("wuttapos.server.views.products") config.include("wuttapos.server.views.products")
config.include("wuttapos.server.views.inventory_adjustments") config.include("wuttapos.server.views.inventory_adjustments")
config.include("wuttapos.server.views.customers") 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")

View 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
"""

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View 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
"""

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

View 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"],
]

View 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"],
]

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

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

View 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"],
]

View 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

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

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

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

View 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

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

View 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"
),
]
),
]

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

View 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"),
]
),
]

View 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"}},
),
]
),
]

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

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

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

View 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"],
]

View 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

View 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
"""

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

View 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")

File diff suppressed because it is too large Load diff