fix: consolidate some duplicated code

i feel like there was a reason i hadn't done that to begin with, but
now can't recall for sure.  pylint certainly thinks it is duplicated
so mostly cleaning this up per its suggestion...
This commit is contained in:
Lance Edgar 2025-09-01 18:40:21 -05:00
parent 49261a696d
commit 4e7c8d393c
8 changed files with 402 additions and 738 deletions

View file

@ -0,0 +1,6 @@
``sideshow.web.util``
=====================
.. automodule:: sideshow.web.util
:members:

View file

@ -59,6 +59,7 @@ For an online demo see https://demo.wuttaproject.org/
api/sideshow.web.forms.schema
api/sideshow.web.menus
api/sideshow.web.static
api/sideshow.web.util
api/sideshow.web.views
api/sideshow.web.views.batch
api/sideshow.web.views.batch.neworder

View file

@ -33,8 +33,10 @@ from sqlalchemy.ext.declarative import declared_attr
from wuttjamaican.db import model
from sideshow.db.model.orders import OrderMixin, OrderItemMixin
class NewOrderBatch(model.BatchMixin, model.Base):
class NewOrderBatch(model.BatchMixin, OrderMixin, model.Base):
"""
:term:`Batch <batch>` used for entering new :term:`orders <order>`
into the system. Each batch ultimately becomes an
@ -73,25 +75,6 @@ class NewOrderBatch(model.BatchMixin, model.Base):
STATUS_OK: "ok",
}
store_id = sa.Column(
sa.String(length=10),
nullable=True,
doc="""
ID of the store to which the order pertains, if applicable.
""",
)
customer_id = sa.Column(
sa.String(length=20),
nullable=True,
doc="""
Proper account ID for the :term:`external customer` to which the
order pertains, if applicable.
See also :attr:`local_customer` and :attr:`pending_customer`.
""",
)
local_customer_uuid = sa.Column(model.UUID(), nullable=True)
@declared_attr
@ -100,6 +83,7 @@ class NewOrderBatch(model.BatchMixin, model.Base):
):
return orm.relationship(
"LocalCustomer",
cascade_backrefs=False,
back_populates="new_order_batches",
doc="""
Reference to the
@ -118,6 +102,7 @@ class NewOrderBatch(model.BatchMixin, model.Base):
):
return orm.relationship(
"PendingCustomer",
cascade_backrefs=False,
back_populates="new_order_batches",
doc="""
Reference to the
@ -128,40 +113,8 @@ class NewOrderBatch(model.BatchMixin, model.Base):
""",
)
customer_name = sa.Column(
sa.String(length=100),
nullable=True,
doc="""
Name for the customer account.
""",
)
phone_number = sa.Column(
sa.String(length=20),
nullable=True,
doc="""
Phone number for the customer.
""",
)
email_address = sa.Column(
sa.String(length=255),
nullable=True,
doc="""
Email address for the customer.
""",
)
total_price = sa.Column(
sa.Numeric(precision=10, scale=3),
nullable=True,
doc="""
Full price (not including tax etc.) for all items on the order.
""",
)
class NewOrderBatchRow(model.BatchRowMixin, model.Base):
class NewOrderBatchRow(model.BatchRowMixin, OrderItemMixin, model.Base):
"""
Row of data within a :class:`NewOrderBatch`. Each row ultimately
becomes an :class:`~sideshow.db.model.orders.OrderItem`.
@ -212,17 +165,6 @@ class NewOrderBatchRow(model.BatchRowMixin, model.Base):
Dict of possible status code -> label options.
"""
product_id = sa.Column(
sa.String(length=20),
nullable=True,
doc="""
Proper ID for the :term:`external product` which the order item
represents, if applicable.
See also :attr:`local_product` and :attr:`pending_product`.
""",
)
local_product_uuid = sa.Column(model.UUID(), nullable=True)
@declared_attr
@ -231,6 +173,7 @@ class NewOrderBatchRow(model.BatchRowMixin, model.Base):
):
return orm.relationship(
"LocalProduct",
cascade_backrefs=False,
back_populates="new_order_batch_rows",
doc="""
Reference to the
@ -249,6 +192,7 @@ class NewOrderBatchRow(model.BatchRowMixin, model.Base):
):
return orm.relationship(
"PendingProduct",
cascade_backrefs=False,
back_populates="new_order_batch_rows",
doc="""
Reference to the
@ -259,224 +203,5 @@ class NewOrderBatchRow(model.BatchRowMixin, model.Base):
""",
)
product_scancode = sa.Column(
sa.String(length=14),
nullable=True,
doc="""
Scancode for the product, as string.
.. note::
This column allows 14 chars, so can store a full GPC with check
digit. However as of writing the actual format used here does
not matter to Sideshow logic; "anything" should work.
That may change eventually, depending on POS integration
scenarios that come up. Maybe a config option to declare
whether check digit should be included or not, etc.
""",
)
product_brand = sa.Column(
sa.String(length=100),
nullable=True,
doc="""
Brand name for the product - up to 100 chars.
""",
)
product_description = sa.Column(
sa.String(length=255),
nullable=True,
doc="""
Description for the product - up to 255 chars.
""",
)
product_size = sa.Column(
sa.String(length=30),
nullable=True,
doc="""
Size of the product, as string - up to 30 chars.
""",
)
product_weighed = sa.Column(
sa.Boolean(),
nullable=True,
doc="""
Flag indicating the product is sold by weight; default is null.
""",
)
department_id = sa.Column(
sa.String(length=10),
nullable=True,
doc="""
ID of the department to which the product belongs, if known.
""",
)
department_name = sa.Column(
sa.String(length=30),
nullable=True,
doc="""
Name of the department to which the product belongs, if known.
""",
)
special_order = sa.Column(
sa.Boolean(),
nullable=True,
doc="""
Flag indicating the item is a "special order" - e.g. something not
normally carried by the store. Default is null.
""",
)
vendor_name = sa.Column(
sa.String(length=50),
nullable=True,
doc="""
Name of vendor from which product may be purchased, if known. See
also :attr:`vendor_item_code`.
""",
)
vendor_item_code = sa.Column(
sa.String(length=20),
nullable=True,
doc="""
Item code (SKU) to use when ordering this product from the vendor
identified by :attr:`vendor_name`, if known.
""",
)
case_size = sa.Column(
sa.Numeric(precision=10, scale=4),
nullable=True,
doc="""
Case pack count for the product, if known.
If this is not set, then customer cannot order a "case" of the item.
""",
)
order_qty = sa.Column(
sa.Numeric(precision=10, scale=4),
nullable=False,
doc="""
Quantity (as decimal) of product being ordered.
This must be interpreted along with :attr:`order_uom` to determine
the *complete* order quantity, e.g. "2 cases".
""",
)
order_uom = sa.Column(
sa.String(length=10),
nullable=False,
doc="""
Code indicating the unit of measure for product being ordered.
This should be one of the codes from
:data:`~sideshow.enum.ORDER_UOM`.
Sideshow will treat :data:`~sideshow.enum.ORDER_UOM_CASE`
differently but :data:`~sideshow.enum.ORDER_UOM_UNIT` and others
are all treated the same (i.e. "unit" is assumed).
""",
)
unit_cost = sa.Column(
sa.Numeric(precision=9, scale=5),
nullable=True,
doc="""
Cost of goods amount for one "unit" (not "case") of the product,
as decimal to 4 places.
""",
)
unit_price_reg = sa.Column(
sa.Numeric(precision=8, scale=3),
nullable=True,
doc="""
Regular price for the item unit. Unless a sale is in effect,
:attr:`unit_price_quoted` will typically match this value.
""",
)
unit_price_sale = sa.Column(
sa.Numeric(precision=8, scale=3),
nullable=True,
doc="""
Sale price for the item unit, if applicable. If set, then
:attr:`unit_price_quoted` will typically match this value. See
also :attr:`sale_ends`.
""",
)
sale_ends = sa.Column(
sa.DateTime(timezone=True),
nullable=True,
doc="""
End date/time for the sale in effect, if any.
This is only relevant if :attr:`unit_price_sale` is set.
""",
)
unit_price_quoted = sa.Column(
sa.Numeric(precision=8, scale=3),
nullable=True,
doc="""
Quoted price for the item unit. This is the "effective" unit
price, which is used to calculate :attr:`total_price`.
This price does *not* reflect the :attr:`discount_percent`. It
normally should match either :attr:`unit_price_reg` or
:attr:`unit_price_sale`.
See also :attr:`case_price_quoted`, if applicable.
""",
)
case_price_quoted = sa.Column(
sa.Numeric(precision=8, scale=3),
nullable=True,
doc="""
Quoted price for a "case" of the item, if applicable.
This is mostly for display purposes; :attr:`unit_price_quoted` is
used for calculations.
""",
)
discount_percent = sa.Column(
sa.Numeric(precision=5, scale=3),
nullable=True,
doc="""
Discount percent to apply when calculating :attr:`total_price`, if
applicable.
""",
)
total_price = sa.Column(
sa.Numeric(precision=8, scale=3),
nullable=True,
doc="""
Full price (not including tax etc.) which the customer is quoted
for the order item.
This is calculated using values from:
* :attr:`unit_price_quoted`
* :attr:`order_qty`
* :attr:`order_uom`
* :attr:`case_size`
* :attr:`discount_percent`
""",
)
def __str__(self):
return str(self.pending_product or self.product_description or "")

View file

@ -33,36 +33,12 @@ from sqlalchemy.ext.orderinglist import ordering_list
from wuttjamaican.db import model
class Order(model.Base): # pylint: disable=too-few-public-methods
class OrderMixin: # pylint: disable=too-few-public-methods
"""
Represents an :term:`order` for a customer. Each order has one or
more :attr:`items`.
Usually, orders are created by way of a
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch`.
Mixin class providing common columns for orders and new order
batches.
"""
__tablename__ = "sideshow_order"
# TODO: this feels a bit hacky yet but it does avoid problems
# showing the Orders grid for a PendingCustomer
__colanderalchemy_config__ = {
"excludes": ["items"],
}
uuid = model.uuid_column()
order_id = sa.Column(
sa.Integer(),
nullable=False,
doc="""
Unique ID for the order.
When the order is created from New Order Batch, this order ID will
match the batch ID.
""",
)
store_id = sa.Column(
sa.String(length=10),
nullable=True,
@ -71,16 +47,6 @@ class Order(model.Base): # pylint: disable=too-few-public-methods
""",
)
store = orm.relationship(
"Store",
primaryjoin="Store.store_id == Order.store_id",
foreign_keys="Order.store_id",
doc="""
Reference to the :class:`~sideshow.db.model.stores.Store`
record, if applicable.
""",
)
customer_id = sa.Column(
sa.String(length=20),
nullable=True,
@ -92,38 +58,6 @@ class Order(model.Base): # pylint: disable=too-few-public-methods
""",
)
local_customer_uuid = model.uuid_fk_column(
"sideshow_customer_local.uuid", nullable=True
)
local_customer = orm.relationship(
"LocalCustomer",
cascade_backrefs=False,
back_populates="orders",
doc="""
Reference to the
:class:`~sideshow.db.model.customers.LocalCustomer` record
for the order, if applicable.
See also :attr:`customer_id` and :attr:`pending_customer`.
""",
)
pending_customer_uuid = model.uuid_fk_column(
"sideshow_customer_pending.uuid", nullable=True
)
pending_customer = orm.relationship(
"PendingCustomer",
cascade_backrefs=False,
back_populates="orders",
doc="""
Reference to the
:class:`~sideshow.db.model.customers.PendingCustomer` record
for the order, if applicable.
See also :attr:`customer_id` and :attr:`local_customer`.
""",
)
customer_name = sa.Column(
sa.String(length=100),
nullable=True,
@ -156,76 +90,13 @@ class Order(model.Base): # pylint: disable=too-few-public-methods
""",
)
created = sa.Column(
sa.DateTime(timezone=True),
nullable=False,
default=datetime.datetime.now,
doc="""
Timestamp when the order was created.
If the order is created via New Order Batch, this will match the
batch execution timestamp.
""",
)
created_by_uuid = model.uuid_fk_column("user.uuid", nullable=False)
created_by = orm.relationship(
model.User,
cascade_backrefs=False,
doc="""
Reference to the
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
created the order.
""",
)
items = orm.relationship(
"OrderItem",
collection_class=ordering_list("sequence", count_from=1),
cascade="all, delete-orphan",
cascade_backrefs=False,
back_populates="order",
doc="""
List of :class:`OrderItem` records belonging to the order.
""",
)
def __str__(self):
return str(self.order_id)
class OrderItem(model.Base):
class OrderItemMixin: # pylint: disable=too-few-public-methods
"""
Represents an :term:`order item` within an :class:`Order`.
Usually these are created from
:class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
records.
Mixin class providing common columns for order items and new order
batch rows.
"""
__tablename__ = "sideshow_order_item"
uuid = model.uuid_column()
order_uuid = model.uuid_fk_column("sideshow_order.uuid", nullable=False)
order = orm.relationship(
Order,
cascade_backrefs=False,
back_populates="items",
doc="""
Reference to the :class:`Order` to which the item belongs.
""",
)
sequence = sa.Column(
sa.Integer(),
nullable=False,
doc="""
1-based numeric sequence for the item, i.e. its line number within
the order.
""",
)
product_id = sa.Column(
sa.String(length=20),
nullable=True,
@ -237,38 +108,6 @@ class OrderItem(model.Base):
""",
)
local_product_uuid = model.uuid_fk_column(
"sideshow_product_local.uuid", nullable=True
)
local_product = orm.relationship(
"LocalProduct",
cascade_backrefs=False,
back_populates="order_items",
doc="""
Reference to the
:class:`~sideshow.db.model.products.LocalProduct` record for
the order item, if applicable.
See also :attr:`product_id` and :attr:`pending_product`.
""",
)
pending_product_uuid = model.uuid_fk_column(
"sideshow_product_pending.uuid", nullable=True
)
pending_product = orm.relationship(
"PendingProduct",
cascade_backrefs=False,
back_populates="order_items",
doc="""
Reference to the
:class:`~sideshow.db.model.products.PendingProduct` record for
the order item, if applicable.
See also :attr:`product_id` and :attr:`local_product`.
""",
)
product_scancode = sa.Column(
sa.String(length=14),
nullable=True,
@ -367,6 +206,8 @@ class OrderItem(model.Base):
nullable=True,
doc="""
Case pack count for the product, if known.
If this is not set, then customer cannot order a "case" of the item.
""",
)
@ -389,6 +230,10 @@ class OrderItem(model.Base):
This should be one of the codes from
:data:`~sideshow.enum.ORDER_UOM`.
Sideshow will treat :data:`~sideshow.enum.ORDER_UOM_CASE`
differently but :data:`~sideshow.enum.ORDER_UOM_UNIT` and others
are all treated the same (i.e. "unit" is assumed).
""",
)
@ -440,6 +285,8 @@ class OrderItem(model.Base):
This price does *not* reflect the :attr:`discount_percent`. It
normally should match either :attr:`unit_price_reg` or
:attr:`unit_price_sale`.
See also :attr:`case_price_quoted`, if applicable.
""",
)
@ -480,6 +327,181 @@ class OrderItem(model.Base):
""",
)
class Order(OrderMixin, model.Base): # pylint: disable=too-few-public-methods
"""
Represents an :term:`order` for a customer. Each order has one or
more :attr:`items`.
Usually, orders are created by way of a
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch`.
"""
__tablename__ = "sideshow_order"
# TODO: this feels a bit hacky yet but it does avoid problems
# showing the Orders grid for a PendingCustomer
__colanderalchemy_config__ = {
"excludes": ["items"],
}
uuid = model.uuid_column()
order_id = sa.Column(
sa.Integer(),
nullable=False,
doc="""
Unique ID for the order.
When the order is created from New Order Batch, this order ID will
match the batch ID.
""",
)
store = orm.relationship(
"Store",
primaryjoin="Store.store_id == Order.store_id",
foreign_keys="Order.store_id",
doc="""
Reference to the :class:`~sideshow.db.model.stores.Store`
record, if applicable.
""",
)
local_customer_uuid = model.uuid_fk_column(
"sideshow_customer_local.uuid", nullable=True
)
local_customer = orm.relationship(
"LocalCustomer",
cascade_backrefs=False,
back_populates="orders",
doc="""
Reference to the
:class:`~sideshow.db.model.customers.LocalCustomer` record
for the order, if applicable.
See also :attr:`customer_id` and :attr:`pending_customer`.
""",
)
pending_customer_uuid = model.uuid_fk_column(
"sideshow_customer_pending.uuid", nullable=True
)
pending_customer = orm.relationship(
"PendingCustomer",
cascade_backrefs=False,
back_populates="orders",
doc="""
Reference to the
:class:`~sideshow.db.model.customers.PendingCustomer` record
for the order, if applicable.
See also :attr:`customer_id` and :attr:`local_customer`.
""",
)
created = sa.Column(
sa.DateTime(timezone=True),
nullable=False,
default=datetime.datetime.now,
doc="""
Timestamp when the order was created.
If the order is created via New Order Batch, this will match the
batch execution timestamp.
""",
)
created_by_uuid = model.uuid_fk_column("user.uuid", nullable=False)
created_by = orm.relationship(
model.User,
cascade_backrefs=False,
doc="""
Reference to the
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
created the order.
""",
)
items = orm.relationship(
"OrderItem",
collection_class=ordering_list("sequence", count_from=1),
cascade="all, delete-orphan",
cascade_backrefs=False,
back_populates="order",
doc="""
List of :class:`OrderItem` records belonging to the order.
""",
)
def __str__(self):
return str(self.order_id)
class OrderItem(OrderItemMixin, model.Base):
"""
Represents an :term:`order item` within an :class:`Order`.
Usually these are created from
:class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
records.
"""
__tablename__ = "sideshow_order_item"
uuid = model.uuid_column()
order_uuid = model.uuid_fk_column("sideshow_order.uuid", nullable=False)
order = orm.relationship(
Order,
cascade_backrefs=False,
back_populates="items",
doc="""
Reference to the :class:`Order` to which the item belongs.
""",
)
sequence = sa.Column(
sa.Integer(),
nullable=False,
doc="""
1-based numeric sequence for the item, i.e. its line number within
the order.
""",
)
local_product_uuid = model.uuid_fk_column(
"sideshow_product_local.uuid", nullable=True
)
local_product = orm.relationship(
"LocalProduct",
cascade_backrefs=False,
back_populates="order_items",
doc="""
Reference to the
:class:`~sideshow.db.model.products.LocalProduct` record for
the order item, if applicable.
See also :attr:`product_id` and :attr:`pending_product`.
""",
)
pending_product_uuid = model.uuid_fk_column(
"sideshow_product_pending.uuid", nullable=True
)
pending_product = orm.relationship(
"PendingProduct",
cascade_backrefs=False,
back_populates="order_items",
doc="""
Reference to the
:class:`~sideshow.db.model.products.PendingProduct` record for
the order item, if applicable.
See also :attr:`product_id` and :attr:`local_product`.
""",
)
status_code = sa.Column(
sa.Integer(),
nullable=False,

102
src/sideshow/web/util.py Normal file
View file

@ -0,0 +1,102 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Sideshow -- Case/Special Order Tracker
# Copyright © 2024-2025 Lance Edgar
#
# This file is part of Sideshow.
#
# Sideshow 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.
#
# Sideshow 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 Sideshow. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Web Utility Functions
"""
def make_new_order_batches_grid(request, **kwargs):
"""
Make and return the grid for the New Order Batches field.
"""
config = request.wutta_config
app = config.get_app()
model = app.model
web = app.get_web_handler()
if "key" not in kwargs:
route_prefix = kwargs.pop("route_prefix")
kwargs["key"] = f"{route_prefix}.view.new_order_batches"
kwargs.setdefault("model_class", model.NewOrderBatch)
kwargs.setdefault(
"columns",
[
"id",
"total_price",
"created",
"created_by",
"executed",
],
)
kwargs.setdefault("labels", {"id": "Batch ID"})
kwargs.setdefault("renderers", {"id": "batch_id", "total_price": "currency"})
grid = web.make_grid(request, **kwargs)
if request.has_perm("neworder_batches.view"):
def view_url(batch, i): # pylint: disable=unused-argument
return request.route_url("neworder_batches.view", uuid=batch.uuid)
grid.add_action("view", icon="eye", url=view_url)
grid.set_link("id")
return grid
def make_orders_grid(request, **kwargs):
"""
Make and return the grid for the Orders field.
"""
config = request.wutta_config
app = config.get_app()
model = app.model
web = app.get_web_handler()
if "key" not in kwargs:
route_prefix = kwargs.pop("route_prefix")
kwargs["key"] = f"{route_prefix}.view.orders"
kwargs.setdefault("model_class", model.Order)
kwargs.setdefault(
"columns",
[
"order_id",
"total_price",
"created",
"created_by",
],
)
kwargs.setdefault("labels", {"order_id": "Order ID"})
kwargs.setdefault("renderers", {"total_price": "currency"})
grid = web.make_grid(request, **kwargs)
if request.has_perm("orders.view"):
def view_url(order, i): # pylint: disable=unused-argument
return request.route_url("orders.view", uuid=order.uuid)
grid.add_action("view", icon="eye", url=view_url)
grid.set_link("order_id")
return grid

View file

@ -25,9 +25,11 @@ Views for Customers
"""
from wuttaweb.views import MasterView
from wuttaweb.forms.schema import UserRef, WuttaEnum
from wuttaweb.forms.schema import WuttaEnum
from sideshow.db.model import LocalCustomer, PendingCustomer
from sideshow.web.views.shared import PendingMixin
from sideshow.web.util import make_new_order_batches_grid, make_orders_grid
class LocalCustomerView(MasterView): # pylint: disable=abstract-method
@ -120,72 +122,20 @@ class LocalCustomerView(MasterView): # pylint: disable=abstract-method
"""
Make and return the grid for the Orders field.
"""
model = self.app.model
route_prefix = self.get_route_prefix()
grid = self.make_grid(
key=f"{route_prefix}.view.orders",
model_class=model.Order,
data=customer.orders,
columns=[
"order_id",
"total_price",
"created",
"created_by",
],
labels={
"order_id": "Order ID",
},
return make_orders_grid(
self.request, route_prefix=self.get_route_prefix(), data=customer.orders
)
grid.set_renderer("total_price", grid.render_currency)
if self.request.has_perm("orders.view"):
def view_url(order, i): # pylint: disable=unused-argument
return self.request.route_url("orders.view", uuid=order.uuid)
grid.add_action("view", icon="eye", url=view_url)
grid.set_link("order_id")
return grid
def make_new_order_batches_grid(self, customer):
"""
Make and return the grid for the New Order Batches field.
"""
model = self.app.model
route_prefix = self.get_route_prefix()
grid = self.make_grid(
key=f"{route_prefix}.view.new_order_batches",
model_class=model.NewOrderBatch,
return make_new_order_batches_grid(
self.request,
route_prefix=self.get_route_prefix(),
data=customer.new_order_batches,
columns=[
"id",
"total_price",
"created",
"created_by",
"executed",
],
labels={
"id": "Batch ID",
},
renderers={
"id": "batch_id",
"total_price": "currency",
},
)
if self.request.has_perm("neworder_batches.view"):
def view_url(batch, i): # pylint: disable=unused-argument
return self.request.route_url("neworder_batches.view", uuid=batch.uuid)
grid.add_action("view", icon="eye", url=view_url)
grid.set_link("id")
return grid
def objectify(self, form): # pylint: disable=empty-docstring
""" """
customer = super().objectify(form)
@ -197,7 +147,7 @@ class LocalCustomerView(MasterView): # pylint: disable=abstract-method
return customer
class PendingCustomerView(MasterView): # pylint: disable=abstract-method
class PendingCustomerView(PendingMixin, MasterView): # pylint: disable=abstract-method
"""
Master view for
:class:`~sideshow.db.model.customers.PendingCustomer`; route
@ -270,7 +220,8 @@ class PendingCustomerView(MasterView): # pylint: disable=abstract-method
f = form
super().configure_form(f)
enum = self.app.enum
customer = f.model_instance
self.configure_form_pending(f)
# customer_id
if self.creating:
@ -285,101 +236,24 @@ class PendingCustomerView(MasterView): # pylint: disable=abstract-method
f.set_node("status", WuttaEnum(self.request, enum.PendingCustomerStatus))
f.set_readonly("status")
# created
if self.creating:
f.remove("created")
else:
f.set_readonly("created")
# created_by
if self.creating:
f.remove("created_by")
else:
f.set_node("created_by", UserRef(self.request))
f.set_readonly("created_by")
# orders
if self.creating or self.editing:
f.remove("orders")
else:
f.set_grid("orders", self.make_orders_grid(customer))
# new_order_batches
if self.creating or self.editing:
f.remove("new_order_batches")
else:
f.set_grid("new_order_batches", self.make_new_order_batches_grid(customer))
def make_orders_grid(self, customer):
"""
Make and return the grid for the Orders field.
"""
model = self.app.model
route_prefix = self.get_route_prefix()
grid = self.make_grid(
key=f"{route_prefix}.view.orders",
model_class=model.Order,
data=customer.orders,
columns=[
"order_id",
"total_price",
"created",
"created_by",
],
labels={
"order_id": "Order ID",
},
return make_orders_grid(
self.request, route_prefix=self.get_route_prefix(), data=customer.orders
)
grid.set_renderer("total_price", grid.render_currency)
if self.request.has_perm("orders.view"):
def view_url(order, i): # pylint: disable=unused-argument
return self.request.route_url("orders.view", uuid=order.uuid)
grid.add_action("view", icon="eye", url=view_url)
grid.set_link("order_id")
return grid
def make_new_order_batches_grid(self, customer):
"""
Make and return the grid for the New Order Batches field.
"""
model = self.app.model
route_prefix = self.get_route_prefix()
grid = self.make_grid(
key=f"{route_prefix}.view.new_order_batches",
model_class=model.NewOrderBatch,
return make_new_order_batches_grid(
self.request,
route_prefix=self.get_route_prefix(),
data=customer.new_order_batches,
columns=[
"id",
"total_price",
"created",
"created_by",
"executed",
],
labels={
"id": "Batch ID",
},
renderers={
"id": "batch_id",
"total_price": "currency",
},
)
if self.request.has_perm("neworder_batches.view"):
def view_url(batch, i): # pylint: disable=unused-argument
return self.request.route_url("neworder_batches.view", uuid=batch.uuid)
grid.add_action("view", icon="eye", url=view_url)
grid.set_link("id")
return grid
def objectify(self, form): # pylint: disable=empty-docstring
""" """
enum = self.app.enum

View file

@ -25,10 +25,12 @@ Views for Products
"""
from wuttaweb.views import MasterView
from wuttaweb.forms.schema import UserRef, WuttaMoney, WuttaQuantity
from wuttaweb.forms.schema import WuttaMoney, WuttaQuantity
from sideshow.enum import PendingProductStatus
from sideshow.db.model import LocalProduct, PendingProduct
from sideshow.web.views.shared import PendingMixin
from sideshow.web.util import make_new_order_batches_grid, make_orders_grid
class LocalProductView(MasterView): # pylint: disable=abstract-method
@ -153,82 +155,28 @@ class LocalProductView(MasterView): # pylint: disable=abstract-method
"""
Make and return the grid for the Orders field.
"""
model = self.app.model
route_prefix = self.get_route_prefix()
orders = {item.order for item in product.order_items}
orders = sorted(orders, key=lambda order: order.order_id)
grid = self.make_grid(
key=f"{route_prefix}.view.orders",
model_class=model.Order,
data=orders,
columns=[
"order_id",
"total_price",
"created",
"created_by",
],
labels={
"order_id": "Order ID",
},
renderers={
"total_price": "currency",
},
return make_orders_grid(
self.request, route_prefix=self.get_route_prefix(), data=orders
)
if self.request.has_perm("orders.view"):
def view_url(order, i): # pylint: disable=unused-argument
return self.request.route_url("orders.view", uuid=order.uuid)
grid.add_action("view", icon="eye", url=view_url)
grid.set_link("order_id")
return grid
def make_new_order_batches_grid(self, product):
"""
Make and return the grid for the New Order Batches field.
"""
model = self.app.model
route_prefix = self.get_route_prefix()
batches = {row.batch for row in product.new_order_batch_rows}
batches = sorted(batches, key=lambda batch: batch.id)
grid = self.make_grid(
key=f"{route_prefix}.view.new_order_batches",
model_class=model.NewOrderBatch,
return make_new_order_batches_grid(
self.request,
route_prefix=self.get_route_prefix(),
data=batches,
columns=[
"id",
"total_price",
"created",
"created_by",
"executed",
],
labels={
"id": "Batch ID",
"status_code": "Status",
},
renderers={
"id": "batch_id",
},
)
if self.request.has_perm("neworder_batches.view"):
def view_url(batch, i): # pylint: disable=unused-argument
return self.request.route_url("neworder_batches.view", uuid=batch.uuid)
grid.add_action("view", icon="eye", url=view_url)
grid.set_link("id")
return grid
class PendingProductView(MasterView): # pylint: disable=abstract-method
class PendingProductView(PendingMixin, MasterView): # pylint: disable=abstract-method
"""
Master view for
:class:`~sideshow.db.model.products.PendingProduct`; route
@ -330,7 +278,8 @@ class PendingProductView(MasterView): # pylint: disable=abstract-method
""" """
f = form
super().configure_form(f)
product = f.model_instance
self.configure_form_pending(f)
# product_id
if self.creating:
@ -344,109 +293,30 @@ class PendingProductView(MasterView): # pylint: disable=abstract-method
# notes
f.set_widget("notes", "notes")
# created
if self.creating:
f.remove("created")
else:
f.set_readonly("created")
# created_by
if self.creating:
f.remove("created_by")
else:
f.set_node("created_by", UserRef(self.request))
f.set_readonly("created_by")
# orders
if self.creating or self.editing:
f.remove("orders")
else:
f.set_grid("orders", self.make_orders_grid(product))
# new_order_batches
if self.creating or self.editing:
f.remove("new_order_batches")
else:
f.set_grid("new_order_batches", self.make_new_order_batches_grid(product))
def make_orders_grid(self, product):
"""
Make and return the grid for the Orders field.
"""
model = self.app.model
route_prefix = self.get_route_prefix()
orders = {item.order for item in product.order_items}
orders = sorted(orders, key=lambda order: order.order_id)
grid = self.make_grid(
key=f"{route_prefix}.view.orders",
model_class=model.Order,
data=orders,
columns=[
"order_id",
"total_price",
"created",
"created_by",
],
labels={
"order_id": "Order ID",
},
renderers={
"total_price": "currency",
},
return make_orders_grid(
self.request, route_prefix=self.get_route_prefix(), data=orders
)
if self.request.has_perm("orders.view"):
def view_url(order, i): # pylint: disable=unused-argument
return self.request.route_url("orders.view", uuid=order.uuid)
grid.add_action("view", icon="eye", url=view_url)
grid.set_link("order_id")
return grid
def make_new_order_batches_grid(self, product):
"""
Make and return the grid for the New Order Batches field.
"""
model = self.app.model
route_prefix = self.get_route_prefix()
batches = {row.batch for row in product.new_order_batch_rows}
batches = sorted(batches, key=lambda batch: batch.id)
grid = self.make_grid(
key=f"{route_prefix}.view.new_order_batches",
model_class=model.NewOrderBatch,
return make_new_order_batches_grid(
self.request,
route_prefix=self.get_route_prefix(),
data=batches,
columns=[
"id",
"total_price",
"created",
"created_by",
"executed",
],
labels={
"id": "Batch ID",
"status_code": "Status",
},
renderers={
"id": "batch_id",
},
)
if self.request.has_perm("neworder_batches.view"):
def view_url(batch, i): # pylint: disable=unused-argument
return self.request.route_url("neworder_batches.view", uuid=batch.uuid)
grid.add_action("view", icon="eye", url=view_url)
grid.set_link("id")
return grid
def get_template_context(self, context): # pylint: disable=empty-docstring
""" """
enum = self.app.enum

View file

@ -0,0 +1,64 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Sideshow -- Case/Special Order Tracker
# Copyright © 2024-2025 Lance Edgar
#
# This file is part of Sideshow.
#
# Sideshow 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.
#
# Sideshow 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 Sideshow. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Shared View Logic
"""
from wuttaweb.forms.schema import UserRef
class PendingMixin: # pylint: disable=too-few-public-methods
"""
Mixin class with logic shared by Pending Customer and Pending
Product views.
"""
def configure_form_pending(self, form): # pylint: disable=empty-docstring
""" """
f = form
obj = f.model_instance
# created
if self.creating:
f.remove("created")
else:
f.set_readonly("created")
# created_by
if self.creating:
f.remove("created_by")
else:
f.set_node("created_by", UserRef(self.request))
f.set_readonly("created_by")
# orders
if self.creating or self.editing:
f.remove("orders")
else:
f.set_grid("orders", self.make_orders_grid(obj))
# new_order_batches
if self.creating or self.editing:
f.remove("new_order_batches")
else:
f.set_grid("new_order_batches", self.make_new_order_batches_grid(obj))