initial basic server web app

This commit is contained in:
Lance Edgar 2026-01-01 19:23:11 -06:00
commit 89238722b3
29 changed files with 2015 additions and 0 deletions

6
CHANGELOG.md Normal file
View file

@ -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).

35
README.md Normal file
View file

@ -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.)

83
pyproject.toml Normal file
View file

@ -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"]

27
src/wuttapos/__init__.py Normal file
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 - package root
"""
from ._version import __version__

6
src/wuttapos/_version.py Normal file
View file

@ -0,0 +1,6 @@
# -*- coding: utf-8; -*-
from importlib.metadata import version
__version__ = version("WuttaPOS")

33
src/wuttapos/app.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
WuttaPOS app
"""
from wuttjamaican import app as base
class WuttaPosAppProvider(base.AppProvider):
"""
Custom :term:`app provider` for WuttaPOS.
"""

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
WuttaPOS - ``wuttapos`` subcommands
"""
from .base import wuttapos_typer
# nb. must bring in all modules for discovery to work
from . import install

32
src/wuttapos/cli/base.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``wuttapos`` command
"""
from wuttjamaican.cli import make_typer
wuttapos_typer = make_typer(
name="wuttapos", help="Point of Sale system based on Wutta Framework"
)

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

58
src/wuttapos/config.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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')

View file

View file

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

View file

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

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
WuttaPOS - data model
"""
from wuttjamaican.db.model import *
from .departments import Department
from .products import (
Product,
ProductInventory,
InventoryAdjustmentType,
InventoryAdjustment,
)

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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 ""

View file

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

View file

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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)

View file

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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
)

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

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/>.
#
################################################################################
"""
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,
)

View file

@ -0,0 +1,2 @@
Place files in this folder, which correspond to the Resource()
definitions found in `wuttapos/server/static/__init__.py`

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/>.
#
################################################################################
"""
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")

View file

@ -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>
<%def name="header_logo()">
${parent.header_logo()}
</%def>
<%def name="footer()">
${parent.footer()}
</%def>

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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")

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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)

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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)

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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)