270 lines
8.8 KiB
Python
270 lines
8.8 KiB
Python
# -*- coding: utf-8; -*-
|
|
################################################################################
|
|
#
|
|
# WuttaPOS -- Pythonic Point of Sale System
|
|
# Copyright © 2023 Lance Edgar
|
|
#
|
|
# This file is part of WuttaPOS.
|
|
#
|
|
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
|
# terms of the GNU General Public License as published by the Free Software
|
|
# Foundation, either version 3 of the License, or (at your option) any later
|
|
# version.
|
|
#
|
|
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along with
|
|
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
################################################################################
|
|
"""
|
|
WuttaPOS app
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import socket
|
|
import sys
|
|
import threading
|
|
from collections import OrderedDict
|
|
from traceback import format_exception
|
|
|
|
from rattail import app as base
|
|
from rattail.config import make_config
|
|
from rattail.files import resource_path
|
|
from rattail.util import simple_error
|
|
|
|
import flet as ft
|
|
|
|
import wuttapos
|
|
from wuttapos.util import get_pos_batch_handler, make_button
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class WuttaAppHandler(base.AppHandler):
|
|
"""
|
|
Custom app handler for WuttaPOS
|
|
"""
|
|
|
|
def get_title(self, **kwargs):
|
|
kwargs.setdefault('default', 'WuttaPOS')
|
|
return super().get_title(**kwargs)
|
|
|
|
def get_node_title(self, **kwargs):
|
|
kwargs.setdefault('default', 'WuttaPOS')
|
|
return super().get_node_title(**kwargs)
|
|
|
|
|
|
def main(page: ft.Page):
|
|
config = make_config()
|
|
app = config.get_app()
|
|
model = app.model
|
|
handler = get_pos_batch_handler(config)
|
|
|
|
# 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):
|
|
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
|
|
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': simple_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_email(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__}"
|
|
page.window_full_screen = True
|
|
# page.vertical_alignment = ft.MainAxisAlignment.CENTER
|
|
|
|
# global defaults for button/text styles etc.
|
|
page.data = {
|
|
'default_button_height_pos': 100,
|
|
'default_button_height_dlg': 80,
|
|
}
|
|
|
|
def clean_exit():
|
|
# TODO: this doesn't do anything interesting now, but it could
|
|
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
|
|
page.window_prevent_close = True
|
|
page.on_window_event = window_event
|
|
|
|
# TODO: probably these should be auto-loaded from spec
|
|
from wuttapos.views.pos import POSView
|
|
from wuttapos.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'))
|
|
|
|
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():
|
|
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', uuid)
|
|
page.session.set('user_display', str(user))
|
|
batch = handler.get_current_batch(user, create=False)
|
|
if batch:
|
|
page.session.set('txn_display', batch.id_str)
|
|
if batch.customer:
|
|
page.session.set('cust_uuid', batch.customer.uuid)
|
|
key = app.get_customer_key_field()
|
|
value = getattr(batch.customer, key)
|
|
page.session.set('cust_display', str(value or ''))
|
|
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:assets'))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
run_app()
|