diff --git a/pyproject.toml b/pyproject.toml
index a7f6141..3cad60a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -56,7 +56,7 @@ terminal = ["flet[all]<0.21"]
wuttapos = "wuttapos.app:WuttaPosAppProvider"
[project.entry-points."wutta.config.extensions"]
-"wuttapos" = "wuttapos.config:WuttaPosConfigExtension"
+"wuttapos" = "wuttapos.conf:WuttaPosConfigExtension"
# TODO: (why) is this needed again?
[project.entry-points."wutta.web.menus"]
diff --git a/src/wuttapos/app.py b/src/wuttapos/app.py
index a3f9d2e..bc28689 100644
--- a/src/wuttapos/app.py
+++ b/src/wuttapos/app.py
@@ -27,17 +27,16 @@ WuttaPOS app
from wuttjamaican import app as base
-class WuttaPosAppProvider(base.AppProvider):
+class WuttaPosAppHandler(base.AppHandler):
"""
- Custom :term:`app provider` for WuttaPOS.
+ Custom :term:`app handler` for WuttaPOS.
"""
default_people_handler_spec = "wuttapos.people:PeopleHandler"
default_employment_handler_spec = "wuttapos.employment:EmploymentHandler"
default_clientele_handler_spec = "wuttapos.clientele:ClienteleHandler"
default_products_handler_spec = "wuttapos.products:ProductsHandler"
-
- email_templates = ["wuttapos:email-templates"]
+ default_install_handler_spec = "wuttapos.install:InstallHandler"
def get_clientele_handler(self):
"""
@@ -94,3 +93,11 @@ class WuttaPosAppProvider(base.AppProvider):
:meth:`~wuttjamaican.people.PeopleHandler.get_person()`.
"""
return self.get_employment_handler().get_employee(obj)
+
+
+class WuttaPosAppProvider(base.AppProvider):
+ """
+ Custom :term:`app provider` for WuttaPOS.
+ """
+
+ email_templates = ["wuttapos:email-templates"]
diff --git a/src/wuttapos/cli/run.py b/src/wuttapos/cli/run.py
index 867be54..da47109 100644
--- a/src/wuttapos/cli/run.py
+++ b/src/wuttapos/cli/run.py
@@ -27,7 +27,6 @@ See also: :ref:`wuttapos-run`
import typer
from wuttapos.cli import wuttapos_typer
-from wuttapos.terminal.app import run_app
@wuttapos_typer.command()
@@ -35,5 +34,7 @@ def run(ctx: typer.Context):
"""
Run the WuttaPOS GUI app
"""
+ from wuttapos.terminal.app import run_app
+
config = ctx.parent.wutta_config
run_app(config)
diff --git a/src/wuttapos/config.py b/src/wuttapos/conf.py
similarity index 93%
rename from src/wuttapos/config.py
rename to src/wuttapos/conf.py
index f4902f7..a4876a8 100644
--- a/src/wuttapos/config.py
+++ b/src/wuttapos/conf.py
@@ -40,6 +40,11 @@ class WuttaPosConfigExtension(WuttaConfigExtension):
config.setdefault(f"{config.appname}.app_title", "WuttaPOS")
config.setdefault(f"{config.appname}.app_dist", "WuttaPOS")
+ # app handler
+ config.setdefault(
+ f"{config.appname}.app.handler", "wuttapos.app:WuttaPosAppHandler"
+ )
+
# app model
config.setdefault(f"{config.appname}.model_spec", "wuttapos.db.model")
config.setdefault(f"{config.appname}.enum_spec", "wuttapos.enum")
diff --git a/src/wuttapos/install.py b/src/wuttapos/install.py
new file mode 100644
index 0000000..d780e1c
--- /dev/null
+++ b/src/wuttapos/install.py
@@ -0,0 +1,78 @@
+# -*- 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 .
+#
+################################################################################
+"""
+Install handler for WuttaPOS
+"""
+
+import subprocess
+import sys
+
+from wuttjamaican import install as base
+
+
+class InstallHandler(base.InstallHandler):
+ """
+ Custom install handler for WuttaPOS
+ """
+
+ template_paths = ["wuttapos:installer-templates"]
+
+ def do_install_steps(self):
+
+ # prompt for install type first
+ self.get_install_type()
+
+ # then everything else
+ super().do_install_steps()
+
+ def get_install_type(self):
+
+ # prompt user
+ install_type = None
+ while install_type not in ("server", "terminal"):
+ install_type = self.prompt_generic(
+ "install type (server/terminal)", default="server"
+ )
+
+ # remember the answer
+ self.install_type = install_type
+
+ if self.install_type != "server":
+ self.rprint(
+ "[bold red]sorry, terminal install is not yet implemented[/bold red]\n"
+ )
+ sys.exit(1)
+
+ # install dependencies
+ if self.install_type == "server":
+ self.install_server_deps()
+
+ def install_server_deps(self):
+ subprocess.check_call(
+ [sys.executable, "-m", "pip", "install", "WuttaPOS[server]"]
+ )
+
+ def make_template_context(self, dbinfo, **kwargs):
+ context = super().make_template_context(dbinfo, **kwargs)
+ context["install_type"] = self.install_type
+ return context
diff --git a/src/wuttapos/installer-templates/upgrade.sh.mako b/src/wuttapos/installer-templates/upgrade.sh.mako
new file mode 100755
index 0000000..aadc31a
--- /dev/null
+++ b/src/wuttapos/installer-templates/upgrade.sh.mako
@@ -0,0 +1,29 @@
+#!/bin/sh -e
+<%text>##################################################%text>
+#
+# ${app_title} - upgrade script
+#
+<%text>##################################################%text>
+
+if [ "$1" = "--verbose" ]; then
+ VERBOSE='--verbose'
+ QUIET=
+else
+ VERBOSE=
+ QUIET='--quiet'
+fi
+
+cd ${envdir}
+
+PIP='bin/pip'
+ALEMBIC='bin/alembic'
+
+# upgrade pip and friends
+$PIP install $QUIET --disable-pip-version-check --upgrade pip
+$PIP install $QUIET --upgrade setuptools wheel
+
+# upgrade app proper
+$PIP install $QUIET --upgrade --upgrade-strategy eager '${pypi_name}'
+
+# migrate schema
+$ALEMBIC -c app/wutta.conf upgrade heads
diff --git a/src/wuttapos/installer-templates/web.conf.mako b/src/wuttapos/installer-templates/web.conf.mako
new file mode 100644
index 0000000..4d2b3c7
--- /dev/null
+++ b/src/wuttapos/installer-templates/web.conf.mako
@@ -0,0 +1,90 @@
+## -*- mode: conf; -*-
+
+<%text>############################################################%text>
+#
+# ${app_title} - web app config
+#
+<%text>############################################################%text>
+
+
+<%text>##############################%text>
+# wutta
+<%text>##############################%text>
+
+${self.section_wutta_config()}
+
+
+<%text>##############################%text>
+# pyramid
+<%text>##############################%text>
+
+${self.section_app_main()}
+
+${self.section_server_main()}
+
+
+<%text>##############################%text>
+# logging
+<%text>##############################%text>
+
+${self.sectiongroup_logging()}
+
+
+######################################################################
+## section templates below
+######################################################################
+
+<%def name="section_wutta_config()">
+[wutta.config]
+require = %(here)s/wutta.conf
+%def>
+
+<%def name="section_app_main()">
+[app:main]
+#use = egg:wuttaweb
+use = egg:${egg_name}
+
+pyramid.reload_templates = true
+pyramid.debug_all = true
+pyramid.default_locale_name = en
+#pyramid.includes = pyramid_debugtoolbar
+
+beaker.session.type = file
+beaker.session.data_dir = %(here)s/cache/sessions/data
+beaker.session.lock_dir = %(here)s/cache/sessions/lock
+beaker.session.secret = ${beaker_secret}
+beaker.session.key = ${beaker_key}
+
+exclog.extra_info = true
+
+# required for wuttaweb
+wutta.config = %(__file__)s
+%def>
+
+<%def name="section_server_main()">
+[server:main]
+use = egg:waitress#main
+host = ${pyramid_host}
+port = ${pyramid_port}
+
+# NOTE: this is needed for local reverse proxy stuff to work with HTTPS
+# https://docs.pylonsproject.org/projects/waitress/en/latest/reverse-proxy.html
+# https://docs.pylonsproject.org/projects/waitress/en/latest/arguments.html
+trusted_proxy = 127.0.0.1
+trusted_proxy_headers = x-forwarded-for x-forwarded-host x-forwarded-proto x-forwarded-port
+clear_untrusted_proxy_headers = True
+
+# TODO: leave this empty if proxy serves as root site, e.g. https://wutta.example.com/
+# url_prefix =
+
+# TODO: or, if proxy serves as subpath of root site, e.g. https://wutta.example.com/backend/
+# url_prefix = /backend
+%def>
+
+<%def name="sectiongroup_logging()">
+[handler_console]
+level = INFO
+
+[handler_file]
+args = (${repr(os.path.join(appdir, 'log', 'web.log'))}, 'a', 1000000, 100, 'utf_8')
+%def>
diff --git a/src/wuttapos/installer-templates/wutta.conf.mako b/src/wuttapos/installer-templates/wutta.conf.mako
new file mode 100644
index 0000000..f819a4f
--- /dev/null
+++ b/src/wuttapos/installer-templates/wutta.conf.mako
@@ -0,0 +1,168 @@
+## -*- mode: conf; -*-
+##<%inherit file="wuttjamaican:templates/install/wutta.conf.mako" />
+
+<%text>############################################################%text>
+#
+# ${app_title} - base config
+#
+<%text>############################################################%text>
+
+
+<%text>##############################%text>
+# wutta
+<%text>##############################%text>
+
+${self.section_wutta()}
+
+${self.section_wutta_config()}
+
+${self.section_wutta_db()}
+
+${self.section_wutta_mail()}
+
+${self.section_wutta_upgrades()}
+
+
+<%text>##############################%text>
+# alembic
+<%text>##############################%text>
+
+${self.section_alembic()}
+
+
+<%text>##############################%text>
+# logging
+<%text>##############################%text>
+
+${self.sectiongroup_logging()}
+
+
+######################################################################
+## section templates below
+######################################################################
+
+<%def name="section_wutta()">
+[wutta]
+app_title = ${app_title}
+%def>
+
+<%def name="section_wutta_config()">
+[wutta.config]
+#require = /etc/wutta/wutta.conf
+configure_logging = true
+usedb = true
+preferdb = true
+%def>
+
+<%def name="section_wutta_db()">
+[wutta.db]
+default.url = ${db_url}
+
+% if install_type == "server":
+[wutta_continuum]
+enable_versioning = true
+% endif
+%def>
+
+<%def name="section_wutta_mail()">
+[wutta.mail]
+
+# this is the global email shutoff switch
+#send_emails = false
+
+# recommended setup is to always talk to postfix on localhost and then
+# it can handle any need complexities, e.g. sending to relay
+smtp.server = localhost
+
+# by default only email templates from wuttjamaican are used
+templates = wuttjamaican:templates/mail
+
+## TODO
+## # this is the "default" email profile, from which all others initially
+## # inherit, but most/all profiles will override these values
+## default.prefix = [${app_title}]
+## default.from = wutta@localhost
+## default.to = root@localhost
+# nb. in test environment it can be useful to disable by default, and
+# then selectively enable certain (e.g. feedback, upgrade) emails
+#default.enabled = false
+%def>
+
+<%def name="section_wutta_upgrades()">
+## TODO
+## [wutta.upgrades]
+## command = ${os.path.join(appdir, 'upgrade.sh')} --verbose
+## files = ${os.path.join(appdir, 'data', 'upgrades')}
+%def>
+
+<%def name="section_alembic()">
+[alembic]
+script_location = wuttjamaican.db:alembic
+% if install_type == "server":
+version_locations = wuttapos.db:alembic/versions wutta_continuum.db:alembic/versions wuttjamaican.db:alembic/versions
+% else:
+version_locations = wuttapos.db:alembic/versions wuttjamaican.db:alembic/versions
+% endif
+%def>
+
+<%def name="sectiongroup_logging()">
+[loggers]
+keys = root, beaker, exc_logger, sqlalchemy, txn
+
+[handlers]
+keys = file, console, email
+
+[formatters]
+keys = generic, console
+
+[logger_root]
+handlers = file, console
+level = DEBUG
+
+[logger_beaker]
+qualname = beaker
+handlers =
+level = INFO
+
+[logger_exc_logger]
+qualname = exc_logger
+handlers = email
+level = ERROR
+
+[logger_sqlalchemy]
+qualname = sqlalchemy.engine
+handlers =
+# handlers = file
+# level = INFO
+
+[logger_txn]
+qualname = txn
+handlers =
+level = INFO
+
+[handler_file]
+class = handlers.RotatingFileHandler
+args = (${repr(os.path.join(appdir, 'log', 'wutta.log'))}, 'a', 1000000, 100, 'utf_8')
+formatter = generic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+formatter = console
+# formatter = generic
+# level = INFO
+# level = WARNING
+
+[handler_email]
+class = handlers.SMTPHandler
+args = ('localhost', 'wutta@localhost', ['root@localhost'], "[${app_title}] Logging")
+formatter = generic
+level = ERROR
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(funcName)s: %(message)s
+datefmt = %Y-%m-%d %H:%M:%S
+
+[formatter_console]
+format = %(levelname)-5.5s [%(name)s][%(threadName)s] %(funcName)s: %(message)s
+%def>
diff --git a/src/wuttapos/server/views/__init__.py b/src/wuttapos/server/views/__init__.py
index b2d5f29..f25671c 100644
--- a/src/wuttapos/server/views/__init__.py
+++ b/src/wuttapos/server/views/__init__.py
@@ -24,11 +24,15 @@
WuttaPOS server views
"""
+from wuttaweb.views import essential
+
def includeme(config):
# wuttaweb
- config.include("wuttaweb.views.essential")
+ essential.defaults(
+ config, **{"wuttaweb.views.common": "wuttapos.server.views.common"}
+ )
# wuttapos
config.include("wuttapos.server.views.stores")
diff --git a/src/wuttapos/server/views/common.py b/src/wuttapos/server/views/common.py
new file mode 100644
index 0000000..e163b42
--- /dev/null
+++ b/src/wuttapos/server/views/common.py
@@ -0,0 +1,105 @@
+# -*- 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 .
+#
+################################################################################
+"""
+Common views
+"""
+
+from wuttaweb.views import common as base
+
+
+class CommonView(base.CommonView):
+ """
+ Common views
+ """
+
+ def setup_enhance_admin_user(self, user):
+ """ """
+ model = self.app.model
+ session = self.app.get_session(user)
+ auth = self.app.get_auth_handler()
+
+ site_admin = session.query(model.Role).filter_by(name="Site Admin").first()
+ if site_admin:
+ site_admin_perms = [
+ "batch.pos.list",
+ "batch.pos.view",
+ "customers.list",
+ "customers.create",
+ "customers.view",
+ "customers.edit",
+ "departments.list",
+ "departments.create",
+ "departments.view",
+ "departments.edit",
+ "employees.list",
+ "employees.create",
+ "employees.view",
+ "employees.edit",
+ "inventory_adjustment_types.list",
+ "inventory_adjustment_types.create",
+ "inventory_adjustment_types.view",
+ "inventory_adjustment_types.edit",
+ "inventory_adjustments.list",
+ "inventory_adjustments.create",
+ "inventory_adjustments.view",
+ "pos.test_error",
+ "pos.ring_sales",
+ "pos.override_price",
+ "pos.del_customer",
+ "pos.toggle_training",
+ "pos.suspend",
+ "pos.swap_customer",
+ "pos.void_txn",
+ "products.list",
+ "products.create",
+ "products.view",
+ "products.edit",
+ "stores.list",
+ "stores.create",
+ "stores.view",
+ "stores.edit",
+ "taxes.list",
+ "taxes.create",
+ "taxes.view",
+ "taxes.edit",
+ "tenders.list",
+ "tenders.create",
+ "tenders.view",
+ "tenders.edit",
+ "terminals.list",
+ "terminals.create",
+ "terminals.view",
+ "terminals.edit",
+ ]
+ for perm in site_admin_perms:
+ auth.grant_permission(site_admin, perm)
+
+
+def defaults(config, **kwargs):
+ local = globals()
+ CommonView = kwargs.get("CommonView", local["CommonView"])
+ base.defaults(config, **{"CommonView": CommonView})
+
+
+def includeme(config):
+ defaults(config)