From 89238722b350fb0a8060aba16dca2be2e6e10a77 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 1 Jan 2026 19:23:11 -0600 Subject: [PATCH] initial basic server web app --- CHANGELOG.md | 6 + README.md | 35 +++ pyproject.toml | 83 +++++ src/wuttapos/__init__.py | 27 ++ src/wuttapos/_version.py | 6 + src/wuttapos/app.py | 33 ++ src/wuttapos/cli/__init__.py | 30 ++ src/wuttapos/cli/base.py | 32 ++ src/wuttapos/cli/install.py | 47 +++ src/wuttapos/config.py | 58 ++++ src/wuttapos/db/__init__.py | 0 .../versions/148d701e4ac7_add_departments.py | 93 ++++++ .../versions/6f02663c2220_add_products.py | 239 +++++++++++++++ src/wuttapos/db/model/__init__.py | 35 +++ src/wuttapos/db/model/departments.py | 88 ++++++ src/wuttapos/db/model/products.py | 283 ++++++++++++++++++ src/wuttapos/server/__init__.py | 0 src/wuttapos/server/app.py | 66 ++++ src/wuttapos/server/forms/__init__.py | 0 src/wuttapos/server/forms/schema.py | 96 ++++++ src/wuttapos/server/menus.py | 83 +++++ src/wuttapos/server/static/__init__.py | 47 +++ src/wuttapos/server/static/libcache/README | 2 + src/wuttapos/server/subscribers.py | 37 +++ src/wuttapos/server/templates/base_meta.mako | 16 + src/wuttapos/server/views/__init__.py | 36 +++ src/wuttapos/server/views/departments.py | 126 ++++++++ .../server/views/inventory_adjustments.py | 266 ++++++++++++++++ src/wuttapos/server/views/products.py | 145 +++++++++ 29 files changed, 2015 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 src/wuttapos/__init__.py create mode 100644 src/wuttapos/_version.py create mode 100644 src/wuttapos/app.py create mode 100644 src/wuttapos/cli/__init__.py create mode 100644 src/wuttapos/cli/base.py create mode 100644 src/wuttapos/cli/install.py create mode 100644 src/wuttapos/config.py create mode 100644 src/wuttapos/db/__init__.py create mode 100644 src/wuttapos/db/alembic/versions/148d701e4ac7_add_departments.py create mode 100644 src/wuttapos/db/alembic/versions/6f02663c2220_add_products.py create mode 100644 src/wuttapos/db/model/__init__.py create mode 100644 src/wuttapos/db/model/departments.py create mode 100644 src/wuttapos/db/model/products.py create mode 100644 src/wuttapos/server/__init__.py create mode 100644 src/wuttapos/server/app.py create mode 100644 src/wuttapos/server/forms/__init__.py create mode 100644 src/wuttapos/server/forms/schema.py create mode 100644 src/wuttapos/server/menus.py create mode 100644 src/wuttapos/server/static/__init__.py create mode 100644 src/wuttapos/server/static/libcache/README create mode 100644 src/wuttapos/server/subscribers.py create mode 100644 src/wuttapos/server/templates/base_meta.mako create mode 100644 src/wuttapos/server/views/__init__.py create mode 100644 src/wuttapos/server/views/departments.py create mode 100644 src/wuttapos/server/views/inventory_adjustments.py create mode 100644 src/wuttapos/server/views/products.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..01a466d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ + +# Changelog +All notable changes to WuttaPOS will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ee77d4 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ + +# WuttaPOS + +This project includes two primary components: + +- web app and related daemons, to run on the server +- standalone GUI app, to run on the lanes + +This project is in the very early stages and is not yet documented. +It is based on an earlier effort, which used Rattail: +[rattail/wuttapos](https://forgejo.wuttaproject.org/rattail/wuttapos) + +However this project uses Wutta Framework, has no Rattail +dependencies, and "starts over" for (mostly) everything. + + +## Server + +Make a virtual environment and install the app: + + python3 -m venv wuttapos + source wuttapos/bin/activate + pip install WuttaPOS[server] + wuttapos install + +For more info see +https://docs.wuttaproject.org/wuttjamaican/narr/install/index.html + + +## Terminal + +It uses [Flet](https://flet.dev/) for the GUI toolkit. The intended +use case is to run as a proper standalone GUI app on the lane desktop. + +(Code for this app has not yet been added.) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e758f15 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,83 @@ + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + + +[project] +name = "WuttaPOS" +# nb. 0.3.0 was the last release of the previous rattail WuttaPOS app. +# pretty sure this will bump to 0.4.0 and then i can remove this note. +version = "0.3.0" +description = "Point of Sale system based on Wutta Framework" +readme = "README.md" +authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}] +classifiers = [ + # TODO: remove this if you intend to publish your project + # (it's here by default, to prevent accidental publishing) + "Private :: Do Not Upload", + + "Development Status :: 3 - Alpha", + "Environment :: Win32 (MS Windows)", + "Environment :: X11 Applications", + "Framework :: Pyramid", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Office/Business", +] +dependencies = [ + "psycopg2", + "WuttJamaican[db]>=0.28.2", + "WuttaSync", +] + + +[project.optional-dependencies] +server = ["WuttaWeb[continuum]"] +terminal = ["flet[all]<0.80.0"] + + +[project.scripts] +"wuttapos" = "wuttapos.cli:wuttapos_typer" + +[project.entry-points."paste.app_factory"] +"main" = "wuttapos.server.app:main" + +[project.entry-points."wutta.app.providers"] +wuttapos = "wuttapos.app:WuttaPosAppProvider" + +[project.entry-points."wutta.config.extensions"] +"wuttapos" = "wuttapos.config:WuttaPosConfigExtension" + +# TODO: (why) is this needed again? +[project.entry-points."wutta.web.menus"] +"wuttapos" = "wuttapos.server.menus:serverMenuHandler" + + +[project.urls] +Homepage = "https://wuttaproject.org/" +Repository = "https://forgejo.wuttaproject.org/wutta/wuttapos" +Issues = "https://forgejo.wuttaproject.org/wutta/wuttapos/issues" +Changelog = "https://forgejo.wuttaproject.org/wutta/wuttapos/src/branch/master/CHANGELOG.md" + + +[tool.commitizen] +version_provider = "pep621" +tag_format = "v$version" +update_changelog_on_bump = true + +# [tool.hatch.build.targets.sdist] +# exclude = [ +# "htmlcov/", +# ] + +[tool.hatch.build.targets.wheel] +packages = ["src/wuttapos"] diff --git a/src/wuttapos/__init__.py b/src/wuttapos/__init__.py new file mode 100644 index 0000000..7c19072 --- /dev/null +++ b/src/wuttapos/__init__.py @@ -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 . +# +################################################################################ +""" +WuttaPOS - package root +""" + +from ._version import __version__ diff --git a/src/wuttapos/_version.py b/src/wuttapos/_version.py new file mode 100644 index 0000000..57167d2 --- /dev/null +++ b/src/wuttapos/_version.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8; -*- + +from importlib.metadata import version + + +__version__ = version("WuttaPOS") diff --git a/src/wuttapos/app.py b/src/wuttapos/app.py new file mode 100644 index 0000000..4e0859c --- /dev/null +++ b/src/wuttapos/app.py @@ -0,0 +1,33 @@ +# -*- 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 . +# +################################################################################ +""" +WuttaPOS app +""" + +from wuttjamaican import app as base + + +class WuttaPosAppProvider(base.AppProvider): + """ + Custom :term:`app provider` for WuttaPOS. + """ diff --git a/src/wuttapos/cli/__init__.py b/src/wuttapos/cli/__init__.py new file mode 100644 index 0000000..3dc0190 --- /dev/null +++ b/src/wuttapos/cli/__init__.py @@ -0,0 +1,30 @@ +# -*- 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 . +# +################################################################################ +""" +WuttaPOS - ``wuttapos`` subcommands +""" + +from .base import wuttapos_typer + +# nb. must bring in all modules for discovery to work +from . import install diff --git a/src/wuttapos/cli/base.py b/src/wuttapos/cli/base.py new file mode 100644 index 0000000..11e10ff --- /dev/null +++ b/src/wuttapos/cli/base.py @@ -0,0 +1,32 @@ +# -*- 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 . +# +################################################################################ +""" +``wuttapos`` command +""" + +from wuttjamaican.cli import make_typer + + +wuttapos_typer = make_typer( + name="wuttapos", help="Point of Sale system based on Wutta Framework" +) diff --git a/src/wuttapos/cli/install.py b/src/wuttapos/cli/install.py new file mode 100644 index 0000000..12165e5 --- /dev/null +++ b/src/wuttapos/cli/install.py @@ -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 . +# +################################################################################ +""" +WuttaPOS CLI +""" + +import typer + +from wuttapos.cli import wuttapos_typer + + +@wuttapos_typer.command() +def install( + ctx: typer.Context, +): + """ + Install the server app + """ + config = ctx.parent.wutta_config + app = config.get_app() + install = app.get_install_handler( + pkg_name="wuttapos", + app_title="WuttaPOS", + pypi_name="WuttaPOS", + egg_name="WuttaPOS", + ) + install.run() diff --git a/src/wuttapos/config.py b/src/wuttapos/config.py new file mode 100644 index 0000000..99d6c1f --- /dev/null +++ b/src/wuttapos/config.py @@ -0,0 +1,58 @@ +# -*- 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 . +# +################################################################################ +""" +WuttaPOS config extension +""" + +from wuttjamaican.conf import WuttaConfigExtension + + +class WuttaPosConfigExtension(WuttaConfigExtension): + """ + The :term`config extension` for WuttaPOS. + """ + + key = "wuttapos" + + def configure(self, config): + + # app info + config.setdefault(f"{config.appname}.app_title", "WuttaPOS") + config.setdefault(f"{config.appname}.app_dist", "WuttaPOS") + + # app model + config.setdefault(f"{config.appname}.model_spec", "wuttapos.db.model") + + # # auth handler + # config.setdefault( + # f"{config.appname}.auth.handler", "wuttapos.auth:WuttaPosAuthHandler" + # ) + + # server menu handler + config.setdefault( + f"{config.appname}.web.menus.handler.spec", + "wuttapos.server.menus:serverMenuHandler", + ) + + # # web app libcache + # #config.setdefault('wuttaweb.static_libcache.module', 'wuttapos.server.static') diff --git a/src/wuttapos/db/__init__.py b/src/wuttapos/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/wuttapos/db/alembic/versions/148d701e4ac7_add_departments.py b/src/wuttapos/db/alembic/versions/148d701e4ac7_add_departments.py new file mode 100644 index 0000000..bb28521 --- /dev/null +++ b/src/wuttapos/db/alembic/versions/148d701e4ac7_add_departments.py @@ -0,0 +1,93 @@ +"""add Departments + +Revision ID: 148d701e4ac7 +Revises: +Create Date: 2026-01-01 17:26:20.695393 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import wuttjamaican.db.util + + +# revision identifiers, used by Alembic. +revision: str = "148d701e4ac7" +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = ("wuttapos",) +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + + # department + op.create_table( + "department", + sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), + sa.Column("department_id", sa.String(length=20), nullable=False), + sa.Column("name", sa.String(length=100), nullable=False), + sa.Column("for_products", sa.Boolean(), nullable=False), + sa.Column("for_personnel", sa.Boolean(), nullable=False), + sa.Column("exempt_from_gross_sales", sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint("uuid", name=op.f("pk_department")), + ) + op.create_table( + "department_version", + sa.Column( + "uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False + ), + sa.Column( + "department_id", sa.String(length=20), autoincrement=False, nullable=True + ), + sa.Column("name", sa.String(length=100), autoincrement=False, nullable=True), + sa.Column("for_products", sa.Boolean(), autoincrement=False, nullable=True), + sa.Column("for_personnel", sa.Boolean(), autoincrement=False, nullable=True), + sa.Column( + "exempt_from_gross_sales", sa.Boolean(), autoincrement=False, nullable=True + ), + sa.Column( + "transaction_id", sa.BigInteger(), autoincrement=False, nullable=False + ), + sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), + sa.Column("operation_type", sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint( + "uuid", "transaction_id", name=op.f("pk_department_version") + ), + ) + op.create_index( + op.f("ix_department_version_end_transaction_id"), + "department_version", + ["end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_department_version_operation_type"), + "department_version", + ["operation_type"], + unique=False, + ) + op.create_index( + op.f("ix_department_version_transaction_id"), + "department_version", + ["transaction_id"], + unique=False, + ) + + +def downgrade() -> None: + + # department + op.drop_index( + op.f("ix_department_version_transaction_id"), table_name="department_version" + ) + op.drop_index( + op.f("ix_department_version_operation_type"), table_name="department_version" + ) + op.drop_index( + op.f("ix_department_version_end_transaction_id"), + table_name="department_version", + ) + op.drop_table("department_version") + op.drop_table("department") diff --git a/src/wuttapos/db/alembic/versions/6f02663c2220_add_products.py b/src/wuttapos/db/alembic/versions/6f02663c2220_add_products.py new file mode 100644 index 0000000..8c6e00e --- /dev/null +++ b/src/wuttapos/db/alembic/versions/6f02663c2220_add_products.py @@ -0,0 +1,239 @@ +"""add Products + +Revision ID: 6f02663c2220 +Revises: 148d701e4ac7 +Create Date: 2026-01-01 18:18:22.958598 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import wuttjamaican.db.util + + +# revision identifiers, used by Alembic. +revision: str = "6f02663c2220" +down_revision: Union[str, None] = "148d701e4ac7" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + + # product + op.create_table( + "product", + sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), + sa.Column("product_id", sa.String(length=20), nullable=False), + sa.Column("brand_name", sa.String(length=100), nullable=True), + sa.Column("description", sa.String(length=255), nullable=False), + sa.Column("size", sa.String(length=30), nullable=True), + sa.Column("sold_by_weight", sa.Boolean(), nullable=False), + sa.Column("department_uuid", wuttjamaican.db.util.UUID(), nullable=False), + sa.Column("special_order", sa.Boolean(), nullable=True), + sa.Column("case_size", sa.Numeric(precision=9, scale=4), nullable=True), + sa.Column("unit_cost", sa.Numeric(precision=9, scale=5), nullable=True), + sa.Column("unit_price_reg", sa.Numeric(precision=8, scale=3), nullable=True), + sa.Column("notes", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["department_uuid"], + ["department.uuid"], + name=op.f("fk_product_department_uuid_department"), + ), + sa.PrimaryKeyConstraint("uuid", name=op.f("pk_product")), + ) + op.create_table( + "product_version", + sa.Column( + "uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False + ), + sa.Column( + "product_id", sa.String(length=20), autoincrement=False, nullable=True + ), + sa.Column( + "brand_name", sa.String(length=100), autoincrement=False, nullable=True + ), + sa.Column( + "description", sa.String(length=255), autoincrement=False, nullable=True + ), + sa.Column("size", sa.String(length=30), autoincrement=False, nullable=True), + sa.Column("sold_by_weight", sa.Boolean(), autoincrement=False, nullable=True), + sa.Column( + "department_uuid", + wuttjamaican.db.util.UUID(), + autoincrement=False, + nullable=True, + ), + sa.Column("special_order", sa.Boolean(), autoincrement=False, nullable=True), + sa.Column( + "case_size", + sa.Numeric(precision=9, scale=4), + autoincrement=False, + nullable=True, + ), + sa.Column( + "unit_cost", + sa.Numeric(precision=9, scale=5), + autoincrement=False, + nullable=True, + ), + sa.Column( + "unit_price_reg", + sa.Numeric(precision=8, scale=3), + autoincrement=False, + nullable=True, + ), + sa.Column("notes", sa.Text(), autoincrement=False, nullable=True), + sa.Column( + "transaction_id", sa.BigInteger(), autoincrement=False, nullable=False + ), + sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), + sa.Column("operation_type", sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint( + "uuid", "transaction_id", name=op.f("pk_product_version") + ), + ) + op.create_index( + op.f("ix_product_version_end_transaction_id"), + "product_version", + ["end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_product_version_operation_type"), + "product_version", + ["operation_type"], + unique=False, + ) + op.create_index( + op.f("ix_product_version_transaction_id"), + "product_version", + ["transaction_id"], + unique=False, + ) + + # product_inventory + op.create_table( + "product_inventory", + sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), + sa.Column("product_uuid", wuttjamaican.db.util.UUID(), nullable=False), + sa.Column("on_hand", sa.Numeric(precision=9, scale=4), nullable=True), + sa.Column("on_order", sa.Numeric(precision=9, scale=4), nullable=True), + sa.ForeignKeyConstraint( + ["product_uuid"], + ["product.uuid"], + name=op.f("fk_product_inventory_product_uuid_product"), + ), + sa.PrimaryKeyConstraint("uuid", name=op.f("pk_product_inventory")), + ) + + # inventory_adjustment_type + op.create_table( + "inventory_adjustment_type", + sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), + sa.Column("type_code", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=100), nullable=False), + sa.PrimaryKeyConstraint("uuid", name=op.f("pk_inventory_adjustment_type")), + sa.UniqueConstraint( + "type_code", name=op.f("uq_inventory_adjustment_type_type_code") + ), + ) + op.create_table( + "inventory_adjustment_type_version", + sa.Column( + "uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False + ), + sa.Column("type_code", sa.Integer(), autoincrement=False, nullable=True), + sa.Column("name", sa.String(length=100), autoincrement=False, nullable=True), + sa.Column( + "transaction_id", sa.BigInteger(), autoincrement=False, nullable=False + ), + sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), + sa.Column("operation_type", sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint( + "uuid", "transaction_id", name=op.f("pk_inventory_adjustment_type_version") + ), + ) + op.create_index( + op.f("ix_inventory_adjustment_type_version_end_transaction_id"), + "inventory_adjustment_type_version", + ["end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_inventory_adjustment_type_version_operation_type"), + "inventory_adjustment_type_version", + ["operation_type"], + unique=False, + ) + op.create_index( + op.f("ix_inventory_adjustment_type_version_transaction_id"), + "inventory_adjustment_type_version", + ["transaction_id"], + unique=False, + ) + + # inventory_adjustment + op.create_table( + "inventory_adjustment", + sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), + sa.Column("inventory_uuid", wuttjamaican.db.util.UUID(), nullable=True), + sa.Column("adjusted", sa.DateTime(), nullable=False), + sa.Column("effective_date", sa.Date(), nullable=False), + sa.Column("adjustment_type_uuid", wuttjamaican.db.util.UUID(), nullable=True), + sa.Column("amount", sa.Numeric(precision=9, scale=4), nullable=False), + sa.Column("source", sa.String(length=100), nullable=True), + sa.ForeignKeyConstraint( + ["adjustment_type_uuid"], + ["inventory_adjustment_type.uuid"], + name=op.f( + "fk_inventory_adjustment_adjustment_type_uuid_inventory_adjustment_type" + ), + ), + sa.ForeignKeyConstraint( + ["inventory_uuid"], + ["product_inventory.uuid"], + name=op.f("fk_inventory_adjustment_inventory_uuid_product_inventory"), + ), + sa.PrimaryKeyConstraint("uuid", name=op.f("pk_inventory_adjustment")), + ) + + +def downgrade() -> None: + + # inventory_adjustment + op.drop_table("inventory_adjustment") + + # inventory_adjustment_type + op.drop_index( + op.f("ix_inventory_adjustment_type_version_transaction_id"), + table_name="inventory_adjustment_type_version", + ) + op.drop_index( + op.f("ix_inventory_adjustment_type_version_operation_type"), + table_name="inventory_adjustment_type_version", + ) + op.drop_index( + op.f("ix_inventory_adjustment_type_version_end_transaction_id"), + table_name="inventory_adjustment_type_version", + ) + op.drop_table("inventory_adjustment_type_version") + op.drop_table("inventory_adjustment_type") + + # product_inventory + op.drop_table("product_inventory") + + # product + op.drop_index( + op.f("ix_product_version_transaction_id"), table_name="product_version" + ) + op.drop_index( + op.f("ix_product_version_operation_type"), table_name="product_version" + ) + op.drop_index( + op.f("ix_product_version_end_transaction_id"), table_name="product_version" + ) + op.drop_table("product_version") + op.drop_table("product") diff --git a/src/wuttapos/db/model/__init__.py b/src/wuttapos/db/model/__init__.py new file mode 100644 index 0000000..c99b602 --- /dev/null +++ b/src/wuttapos/db/model/__init__.py @@ -0,0 +1,35 @@ +# -*- 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 . +# +################################################################################ +""" +WuttaPOS - data model +""" + +from wuttjamaican.db.model import * + +from .departments import Department +from .products import ( + Product, + ProductInventory, + InventoryAdjustmentType, + InventoryAdjustment, +) diff --git a/src/wuttapos/db/model/departments.py b/src/wuttapos/db/model/departments.py new file mode 100644 index 0000000..a3f554e --- /dev/null +++ b/src/wuttapos/db/model/departments.py @@ -0,0 +1,88 @@ +# -*- 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 . +# +################################################################################ +""" +Model for Departments +""" + +import sqlalchemy as sa +from sqlalchemy import orm + +from wuttjamaican.db import model + + +class Department(model.Base): + """ + Represents an organizational department, for products and/or personnel. + """ + + __tablename__ = "department" + __versioned__ = {} + __wutta_hint__ = { + "model_title": "Department", + "model_title_plural": "Departments", + } + + uuid = model.uuid_column() + + department_id = sa.Column( + sa.String(length=20), + nullable=False, + doc=""" + Unique identifier for the department. + """, + ) + + name = sa.Column( + sa.String(length=100), + nullable=False, + doc=""" + Name of the department. + """, + ) + + for_products = sa.Column( + sa.Boolean(), + nullable=False, + doc=""" + Indicates the department exists to organize products. + """, + ) + + for_personnel = sa.Column( + sa.Boolean(), + nullable=False, + doc=""" + Indicates the department exists to organize personnel. + """, + ) + + exempt_from_gross_sales = sa.Column( + sa.Boolean(), + nullable=True, + doc=""" + Indicates products in this department do not count toward gross sales. + """, + ) + + def __str__(self): + return self.name or "" diff --git a/src/wuttapos/db/model/products.py b/src/wuttapos/db/model/products.py new file mode 100644 index 0000000..84129bf --- /dev/null +++ b/src/wuttapos/db/model/products.py @@ -0,0 +1,283 @@ +# -*- coding: utf-8; -*- +""" +Model definition for Products +""" + +import sqlalchemy as sa +from sqlalchemy import orm + +from wuttjamaican.db import model +from wuttjamaican.db.util import UUID + + +class Product(model.Base): + """ + Represents an item for sale (usually). + """ + + __tablename__ = "product" + __versioned__ = {} + __wutta_hint__ = { + "model_title": "Product", + "model_title_plural": "Products", + } + + uuid = model.uuid_column() + + product_id = sa.Column( + sa.String(length=20), + nullable=False, + doc=""" + Unique identifier for the product. + """, + ) + + brand_name = sa.Column( + sa.String(length=100), + nullable=True, + doc=""" + Brand name for the product, if applicable. + """, + ) + + description = sa.Column( + sa.String(length=255), + nullable=False, + doc=""" + Description of the product. + """, + ) + + size = sa.Column( + sa.String(length=30), + nullable=True, + doc=""" + Size of the product. + """, + ) + + sold_by_weight = sa.Column( + sa.Boolean(), + nullable=False, + doc=""" + Indicates the item is sold by weight, vs. by single units. + """, + ) + + department_uuid = model.uuid_fk_column("department.uuid", nullable=False) + department = orm.relationship( + "Department", + doc=""" + Department to which the product belongs. + """, + ) + + special_order = sa.Column( + sa.Boolean(), + nullable=True, + doc=""" + Indicates the item is not normally carried, must be ordered specially. + """, + ) + + case_size = sa.Column( + sa.Numeric(precision=9, scale=4), + nullable=True, + doc=""" + Number of units in a case for this item, if applicable. + """, + ) + + unit_cost = sa.Column( + sa.Numeric(precision=9, scale=5), + nullable=True, + doc=""" + Current cost (from the vendor, to the retailer) for one unit of the item. + """, + ) + + unit_price_reg = sa.Column( + sa.Numeric(precision=8, scale=3), + nullable=True, + doc=""" + Regular price (to the customer) for one unit of product. + """, + ) + + notes = sa.Column( + sa.Text(), + nullable=True, + doc=""" + Arbitrary notes for the product. + """, + ) + + inventory = orm.relationship( + "ProductInventory", + doc=""" + Reference to the live inventory record for this product. + """, + uselist=False, + back_populates="product", + cascade="all, delete-orphan", + ) + + @property + def full_description(self): + fields = [self.brand_name or "", self.description or "", self.size or ""] + fields = [f.strip() for f in fields if f.strip()] + return " ".join(fields) + + def __str__(self): + return self.full_description + + +class ProductInventory(model.Base): + """ + Contains the live inventory counts for products. + """ + + __tablename__ = "product_inventory" + __wutta_hint__ = { + "model_title": "Product Inventory", + "model_title_plural": "Product Inventory", + } + + uuid = model.uuid_column() + + product_uuid = model.uuid_fk_column("product.uuid", nullable=False) + product = orm.relationship( + "Product", + doc=""" + Reference to the product. + """, + back_populates="inventory", + ) + + on_hand = sa.Column( + sa.Numeric(precision=9, scale=4), + nullable=True, + doc=""" + Unit quantity of product which is currently on hand. + """, + ) + + on_order = sa.Column( + sa.Numeric(precision=9, scale=4), + nullable=True, + doc=""" + Unit quantity of product which is currently on order. + """, + ) + + adjustments = orm.relationship( + "InventoryAdjustment", + back_populates="inventory", + cascade="all, delete-orphan", + ) + + def __str__(self): + return str(self.product or "") + + +class InventoryAdjustmentType(model.Base): + """ + Possible types of inventory adjustments. + """ + + __tablename__ = "inventory_adjustment_type" + __versioned__ = {} + __wutta_hint__ = { + "model_title": "Inventory Adjustment Type", + "model_title_plural": "Inventory Adjustment Types", + } + + uuid = model.uuid_column() + + type_code = sa.Column( + sa.Integer(), + nullable=False, + unique=True, + doc=""" + Code indicating the type of inventory adjustment. + """, + ) + + name = sa.Column( + sa.String(length=100), + nullable=False, + doc=""" + Name for the adjustment type. + """, + ) + + def __str__(self): + return self.name or "" + + +class InventoryAdjustment(model.Base): + """ + Represents any adjustment to inventory. + """ + + __tablename__ = "inventory_adjustment" + __wutta_hint__ = { + "model_title": "Inventory Adjustment", + "model_title_plural": "Inventory Adjustments", + } + + uuid = model.uuid_column() + + inventory_uuid = model.uuid_fk_column("product_inventory.uuid", nullable=True) + inventory = orm.relationship( + "ProductInventory", + doc=""" + Reference to the product inventory record. + """, + back_populates="adjustments", + ) + + adjusted = sa.Column( + sa.DateTime(), + nullable=False, + doc=""" + Date and time (in UTC) when the adjustment occurred. + """, + ) + + effective_date = sa.Column( + sa.Date(), + nullable=False, + doc=""" + Effective date (in local time zone) for the adjustment. + """, + ) + + adjustment_type_uuid = model.uuid_fk_column( + "inventory_adjustment_type.uuid", nullable=True + ) + adjustment_type = orm.relationship( + "InventoryAdjustmentType", + doc=""" + Reference to the adjustment type record. + """, + ) + + amount = sa.Column( + sa.Numeric(precision=9, scale=4), + nullable=False, + doc=""" + Amount of the adjustment; may be positive or negative. + """, + ) + + source = sa.Column( + sa.String(length=100), + nullable=True, + doc=""" + Arbitrary string identifying the source of the adjustment, if applicable. + """, + ) + + def __str__(self): + return str(self.inventory or "") diff --git a/src/wuttapos/server/__init__.py b/src/wuttapos/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/wuttapos/server/app.py b/src/wuttapos/server/app.py new file mode 100644 index 0000000..394e076 --- /dev/null +++ b/src/wuttapos/server/app.py @@ -0,0 +1,66 @@ +# -*- 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 . +# +################################################################################ +""" +WuttaPOS server web app +""" + +from wuttaweb import app as base + + +def main(global_config, **settings): + """ + Make and return the WSGI app (Paste entry point). + """ + # prefer wuttapos templates over wuttaweb + settings.setdefault( + "mako.directories", + [ + "wuttapos.server:templates", + "wuttaweb:templates", + ], + ) + + # make config objects + wutta_config = base.make_wutta_config(settings) + pyramid_config = base.make_pyramid_config(settings) + + # bring in the rest of wuttapos + pyramid_config.include("wuttapos.server.static") + pyramid_config.include("wuttapos.server.subscribers") + pyramid_config.include("wuttapos.server.views") + + return pyramid_config.make_wsgi_app() + + +def make_wsgi_app(): + """ + Make and return the WSGI app (generic entry point). + """ + return base.make_wsgi_app(main) + + +def make_asgi_app(): + """ + Make and return the ASGI app (generic entry point). + """ + return base.make_asgi_app(main) diff --git a/src/wuttapos/server/forms/__init__.py b/src/wuttapos/server/forms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/wuttapos/server/forms/schema.py b/src/wuttapos/server/forms/schema.py new file mode 100644 index 0000000..dc040d1 --- /dev/null +++ b/src/wuttapos/server/forms/schema.py @@ -0,0 +1,96 @@ +# -*- 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 . +# +################################################################################ +""" +Form schema types +""" + +from wuttaweb.forms.schema import ObjectRef + + +class DepartmentRef(ObjectRef): + """ + Schema type for a + :class:`~wuttapos.db.model.departments.Department` reference field. + + This is a subclass of + :class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`. + """ + + @property + def model_class(self): + model = self.app.model + return model.Department + + def sort_query(self, query): + return query.order_by(self.model_class.name) + + def get_object_url(self, obj): + department = obj + return self.request.route_url("departments.view", uuid=department.uuid) + + +class ProductRef(ObjectRef): + """ + Schema type for a + :class:`~wuttapos.db.model.products.Product` reference field. + + This is a subclass of + :class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`. + """ + + @property + def model_class(self): + model = self.app.model + return model.Product + + def sort_query(self, query): + return query.order_by(self.model_class.description) + + def get_object_url(self, obj): + product = obj + return self.request.route_url("products.view", uuid=product.uuid) + + +class InventoryAdjustmentTypeRef(ObjectRef): + """ + Schema type for a + :class:`~wuttapos.db.model.products.InventoryAdjustmentType` + reference field. + + This is a subclass of + :class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`. + """ + + @property + def model_class(self): + model = self.app.model + return model.InventoryAdjustmentType + + def sort_query(self, query): + return query.order_by(self.model_class.name) + + def get_object_url(self, obj): + adjustment_type = obj + return self.request.route_url( + "inventory_adjustment_types.view", uuid=adjustment_type.uuid + ) diff --git a/src/wuttapos/server/menus.py b/src/wuttapos/server/menus.py new file mode 100644 index 0000000..2bfcad4 --- /dev/null +++ b/src/wuttapos/server/menus.py @@ -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 . +# +################################################################################ +""" +WuttaPOS server menu +""" + +from wuttaweb import menus as base + + +class WuttaPosMenuHandler(base.MenuHandler): + """ + WuttaPOS menu handler + """ + + def make_menus(self, request, **kwargs): + + # nb. the products menu is just an example; you should + # replace it and add more as needed + + return [ + self.make_products_menu(request), + self.make_admin_menu(request, include_people=True), + ] + + def make_products_menu(self, request): + return { + "title": "Products", + "type": "menu", + "items": [ + { + "title": "Products", + "route": "products", + "perm": "products.list", + }, + {"type": "sep"}, + { + "title": "Departments", + "route": "departments", + "perm": "departments.list", + }, + {"type": "sep"}, + { + "title": "Inventory Adjustments", + "route": "inventory_adjustments", + "perm": "inventory_adjustments.list", + }, + { + "title": "New Inventory Adjustment", + "route": "inventory_adjustments.create", + "perm": "inventory_adjustments.create", + }, + { + "title": "Inventory Adjustment Types", + "route": "inventory_adjustment_types", + "perm": "inventory_adjustment_types.list", + }, + # { + # "title": "Vendors", + # "route": "vendors", + # "perm": "vendors.list", + # }, + ], + } diff --git a/src/wuttapos/server/static/__init__.py b/src/wuttapos/server/static/__init__.py new file mode 100644 index 0000000..5e3a92a --- /dev/null +++ b/src/wuttapos/server/static/__init__.py @@ -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 . +# +################################################################################ +""" +Static assets +""" + +# from fanstatic import Library, Resource + + +# # libcache +# libcache = Library('wuttapos_libcache', 'libcache') +# bb_vue_js = Resource(libcache, 'vue.esm-browser-3.3.11.prod.js') +# bb_oruga_js = Resource(libcache, 'oruga-0.8.10.js') +# bb_oruga_bulma_js = Resource(libcache, 'oruga-bulma-0.3.0.js') +# bb_oruga_bulma_css = Resource(libcache, 'oruga-bulma-0.3.0.css') +# bb_fontawesome_svg_core_js = Resource(libcache, 'fontawesome-svg-core-6.5.2.js') +# bb_free_solid_svg_icons_js = Resource(libcache, 'free-solid-svg-icons-6.5.2.js') +# bb_vue_fontawesome_js = Resource(libcache, 'vue-fontawesome-3.0.6.index.es.js') + + +def includeme(config): + config.include("wuttaweb.static") + config.add_static_view( + "wuttapos", + "wuttapos.server:static", + cache_max_age=3600, + ) diff --git a/src/wuttapos/server/static/libcache/README b/src/wuttapos/server/static/libcache/README new file mode 100644 index 0000000..b1f26be --- /dev/null +++ b/src/wuttapos/server/static/libcache/README @@ -0,0 +1,2 @@ +Place files in this folder, which correspond to the Resource() +definitions found in `wuttapos/server/static/__init__.py` diff --git a/src/wuttapos/server/subscribers.py b/src/wuttapos/server/subscribers.py new file mode 100644 index 0000000..973ffec --- /dev/null +++ b/src/wuttapos/server/subscribers.py @@ -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 . +# +################################################################################ +""" +Pyramid event subscribers +""" + +import wuttapos + + +def add_wuttapos_to_context(event): + renderer_globals = event + renderer_globals["wuttapos"] = wuttapos + + +def includeme(config): + config.include("wuttaweb.subscribers") + config.add_subscriber(add_wuttapos_to_context, "pyramid.events.BeforeRender") diff --git a/src/wuttapos/server/templates/base_meta.mako b/src/wuttapos/server/templates/base_meta.mako new file mode 100644 index 0000000..78c1d53 --- /dev/null +++ b/src/wuttapos/server/templates/base_meta.mako @@ -0,0 +1,16 @@ +<%inherit file="wuttaweb:templates/base_meta.mako" /> + +## TODO: you can override parent template as needed below, or you +## can simply delete this file if no customizations are needed + +<%def name="favicon()"> + ${parent.favicon()} + + +<%def name="header_logo()"> + ${parent.header_logo()} + + +<%def name="footer()"> + ${parent.footer()} + diff --git a/src/wuttapos/server/views/__init__.py b/src/wuttapos/server/views/__init__.py new file mode 100644 index 0000000..b0800d5 --- /dev/null +++ b/src/wuttapos/server/views/__init__.py @@ -0,0 +1,36 @@ +# -*- 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 . +# +################################################################################ +""" +WuttaPOS server views +""" + + +def includeme(config): + + # wuttaweb + config.include("wuttaweb.views.essential") + + # wuttapos + config.include("wuttapos.server.views.departments") + config.include("wuttapos.server.views.products") + config.include("wuttapos.server.views.inventory_adjustments") diff --git a/src/wuttapos/server/views/departments.py b/src/wuttapos/server/views/departments.py new file mode 100644 index 0000000..224dd3b --- /dev/null +++ b/src/wuttapos/server/views/departments.py @@ -0,0 +1,126 @@ +# -*- 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 . +# +################################################################################ +""" +Master view for Departments +""" + +from wuttaweb.views import MasterView + +from wuttapos.db.model import Department, Product + + +class DepartmentView(MasterView): + """ + Master view for Departments + """ + + model_class = Department + model_title = "Department" + model_title_plural = "Departments" + + route_prefix = "departments" + url_prefix = "/departments" + + creatable = True + editable = True + deletable = True + + labels = { + "department_id": "Department ID", + } + + grid_columns = [ + "department_id", + "name", + "for_products", + "for_personnel", + "exempt_from_gross_sales", + ] + + form_fields = [ + "department_id", + "name", + "for_products", + "for_personnel", + "exempt_from_gross_sales", + ] + + has_rows = True + row_model_class = Product + + row_grid_columns = [ + "product_id", + "brand_name", + "description", + "size", + "sold_by_weight", + "case_size", + "special_order", + "unit_price_reg", + ] + + def configure_grid(self, grid): + g = grid + super().configure_grid(g) + + # name + g.set_link("name") + + def get_row_grid_data(self, obj): + department = obj + model = self.app.model + session = self.app.get_session(department) + + return session.query(model.Product).filter( + model.Product.department == department + ) + + def configure_row_grid(self, grid): + g = grid + super().configure_row_grid(g) + + # links + g.set_link("product_id") + g.set_link("brand_name") + g.set_link("description") + g.set_link("size") + + # currency + g.set_renderer("unit_price_reg", "currency") + + # view action + def view_url(product, i): + return self.request.route_url("products.view", uuid=product.uuid) + + g.add_action("view", url=view_url, icon="eye") + + +def defaults(config, **kwargs): + base = globals() + + DepartmentView = kwargs.get("DepartmentView", base["DepartmentView"]) + DepartmentView.defaults(config) + + +def includeme(config): + defaults(config) diff --git a/src/wuttapos/server/views/inventory_adjustments.py b/src/wuttapos/server/views/inventory_adjustments.py new file mode 100644 index 0000000..9e6046d --- /dev/null +++ b/src/wuttapos/server/views/inventory_adjustments.py @@ -0,0 +1,266 @@ +# -*- 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 . +# +################################################################################ +""" +Master view for Inventory Adjustments +""" + +from collections import OrderedDict + +from wuttaweb.views import MasterView +from wuttaweb.forms import widgets +from wuttaweb.forms.schema import WuttaQuantity + +from wuttapos.db.model.products import ( + InventoryAdjustment, + InventoryAdjustmentType, +) +from wuttapos.server.forms.schema import ProductRef, InventoryAdjustmentTypeRef + + +def render_grid_product(adjustment, field, value): + product = adjustment.inventory.product + return str(product) + + +class InventoryAdjustmentTypeView(MasterView): + """ + Master view for Inventory Adjustment Types + """ + + model_class = InventoryAdjustmentType + model_title = "Inventory Adjustment Type" + model_title_plural = "Inventory Adjustment Types" + + route_prefix = "inventory_adjustment_types" + url_prefix = "/inventory/adjustment-types" + + creatable = True + editable = True + deletable = True + + grid_columns = [ + "type_code", + "name", + ] + + form_fields = [ + "type_code", + "name", + ] + + has_rows = True + row_model_class = InventoryAdjustment + row_model_title_plural = "Inventory Adjustments" + + row_grid_columns = [ + "product", + "adjusted", + "effective_date", + "adjustment_type", + "amount", + "source", + ] + + rows_sort_defaults = ("adjusted", "desc") + + def configure_grid(self, grid): + g = grid + super().configure_grid(g) + + # links + g.set_link("type_code") + g.set_link("name") + + def get_row_grid_data(self, obj): + adjustment_type = obj + model = self.app.model + session = self.app.get_session(adjustment_type) + + return session.query(model.InventoryAdjustment).filter( + model.InventoryAdjustment.adjustment_type == adjustment_type + ) + + def configure_row_grid(self, grid): + g = grid + super().configure_row_grid(g) + model = self.app.model + session = self.Session() + + # product + g.set_renderer("product", render_grid_product) + g.set_link("product") + + # view action + def view_url(adjustment, i): + return self.request.route_url( + "inventory_adjustments.view", uuid=adjustment.uuid + ) + + g.add_action("view", url=view_url, icon="eye") + + +class InventoryAdjustmentView(MasterView): + """ + Master view for Inventory Adjustments + """ + + model_class = InventoryAdjustment + model_title = "Inventory Adjustment" + model_title_plural = "Inventory Adjustments" + + route_prefix = "inventory_adjustments" + url_prefix = "/inventory/adjustments" + + creatable = True + editable = False + deletable = False + + grid_columns = [ + "product", + "adjusted", + "effective_date", + "adjustment_type", + "amount", + "source", + ] + + form_fields = [ + "product", + "effective_date", + "adjusted", + "adjustment_type", + "amount", + "source", + ] + + def configure_grid(self, grid): + g = grid + super().configure_grid(g) + model = self.app.model + session = self.Session() + + # product + g.set_renderer("product", render_grid_product) + g.set_link("product") + + # adjustment_type + g.set_joiner( + "adjustment_type", + lambda q: q.outerjoin( + model.InventoryAdjustmentType, + model.InventoryAdjustmentType.type_code + == self.model_class.adjustment_type_code, + ), + ) + g.set_sorter("adjustment_type", model.InventoryAdjustmentType.name) + g.remove_filter("adjustment_type_code") + types = session.query(model.InventoryAdjustmentType).order_by( + model.InventoryAdjustmentType.name + ) + choices = OrderedDict([(typ.type_code, typ.name) for typ in types]) + g.set_filter( + "adjustment_type", + model.InventoryAdjustmentType.type_code, + verbs=["equal", "not_equal"], + choices=choices, + ) + + def configure_form(self, form): + f = form + super().configure_form(f) + model = self.app.model + session = self.Session() + adjustment = f.model_instance + + # product + f.set_node("product", ProductRef(self.request)) + if self.creating: + if uuid := self.request.GET.get("product"): + if product := session.get(model.Product, uuid): + f.set_default("product", product) + f.fields.insert_after("product", "on_hand") + f.set_node("on_hand", WuttaQuantity(self.request)) + f.set_readonly("on_hand") + f.set_default( + "on_hand", + product.inventory.on_hand if product.inventory else None, + ) + else: + f.set_default("product", adjustment.inventory.product) + + # adjustment_type + f.set_node( + "adjustment_type", + InventoryAdjustmentTypeRef(self.request, empty_option=True), + ) + f.set_required("adjustment_type", False) + + # adjusted + if self.creating: + f.remove("adjusted") + + # effective_date + if self.creating: + f.remove("effective_date") + + # amount + f.set_node("amount", WuttaQuantity(self.request)) + + def objectify(self, form): + model = self.app.model + adjustment = super().objectify(form) + + if self.creating: + + inventory = form.validated["product"].inventory + if not inventory: + inventory = model.ProductInventory(product=form.validated["product"]) + + adjustment.inventory = inventory + adjustment.adjusted = self.app.make_utc() + adjustment.effective_date = self.app.localtime().date() + + if adjustment.adjustment_type == -99: + adjustment.adjustment_type = None + + inventory.on_hand = (inventory.on_hand or 0) + adjustment.amount + + return adjustment + + +def defaults(config, **kwargs): + base = globals() + + InventoryAdjustmentTypeView = kwargs.get( + "InventoryAdjustmentTypeView", base["InventoryAdjustmentTypeView"] + ) + InventoryAdjustmentTypeView.defaults(config) + + InventoryAdjustmentView = kwargs.get( + "InventoryAdjustmentView", base["InventoryAdjustmentView"] + ) + InventoryAdjustmentView.defaults(config) + + +def includeme(config): + defaults(config) diff --git a/src/wuttapos/server/views/products.py b/src/wuttapos/server/views/products.py new file mode 100644 index 0000000..1a59a51 --- /dev/null +++ b/src/wuttapos/server/views/products.py @@ -0,0 +1,145 @@ +# -*- 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 . +# +################################################################################ +""" +Master view for Products +""" + +from wuttaweb.views import MasterView +from wuttaweb.forms.schema import WuttaQuantity, WuttaMoney + +from wuttapos.db.model.products import Product +from wuttapos.server.forms.schema import DepartmentRef + + +class ProductView(MasterView): + """ + Master view for Products + """ + + model_class = Product + model_title = "Product" + model_title_plural = "Products" + + route_prefix = "products" + url_prefix = "/products" + + creatable = True + editable = True + deletable = True + + labels = { + "product_id": "Product ID", + } + + grid_columns = [ + "product_id", + "brand_name", + "description", + "size", + "sold_by_weight", + "case_size", + "department", + "special_order", + "unit_price_reg", + ] + + form_fields = [ + "product_id", + "brand_name", + "description", + "size", + "department", + "sold_by_weight", + "case_size", + "special_order", + "unit_cost", + "unit_price_reg", + "notes", + "on_hand", + "on_order", + ] + + def configure_grid(self, grid): + g = grid + super().configure_grid(g) + + # links + g.set_link("product_id") + g.set_link("brand_name") + g.set_link("description") + g.set_link("size") + + # currency + g.set_renderer("unit_cost", "currency", scale=4) + g.set_renderer("unit_price_reg", "currency") + + def configure_form(self, form): + f = form + super().configure_form(f) + product = f.model_instance + + # department + f.set_node("department", DepartmentRef(self.request)) + + # case_size + f.set_node("case_size", WuttaQuantity(self.request)) + + # unit_cost + f.set_node("unit_cost", WuttaMoney(self.request, scale=4)) + + # unit_price_reg + f.set_node("unit_price_reg", WuttaMoney(self.request)) + + # notes + f.set_widget("notes", "notes") + + # on_hand + f.set_node("on_hand", WuttaQuantity(self.request)) + if self.creating or self.editing: + f.remove("on_hand") + else: + f.set_default( + "on_hand", + product.inventory.on_hand if product.inventory else None, + ) + + # on_order + f.set_node("on_order", WuttaQuantity(self.request)) + if self.creating or self.editing: + f.remove("on_order") + else: + f.set_default( + "on_order", + product.inventory.on_order if product.inventory else None, + ) + + +def defaults(config, **kwargs): + base = globals() + + ProductView = kwargs.get("ProductView", base["ProductView"]) + ProductView.defaults(config) + + +def includeme(config): + defaults(config)