diff --git a/docs/api/sideshow.web.util.rst b/docs/api/sideshow.web.util.rst new file mode 100644 index 0000000..9ac524f --- /dev/null +++ b/docs/api/sideshow.web.util.rst @@ -0,0 +1,6 @@ + +``sideshow.web.util`` +===================== + +.. automodule:: sideshow.web.util + :members: diff --git a/docs/index.rst b/docs/index.rst index 3bb4efc..9647bbe 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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 diff --git a/src/sideshow/db/model/batch/neworder.py b/src/sideshow/db/model/batch/neworder.py index c5dfb94..dd55029 100644 --- a/src/sideshow/db/model/batch/neworder.py +++ b/src/sideshow/db/model/batch/neworder.py @@ -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 ` used for entering new :term:`orders ` 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 "") diff --git a/src/sideshow/db/model/orders.py b/src/sideshow/db/model/orders.py index b0d7f08..d1ff229 100644 --- a/src/sideshow/db/model/orders.py +++ b/src/sideshow/db/model/orders.py @@ -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, diff --git a/src/sideshow/web/util.py b/src/sideshow/web/util.py new file mode 100644 index 0000000..a66834a --- /dev/null +++ b/src/sideshow/web/util.py @@ -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 . +# +################################################################################ +""" +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 diff --git a/src/sideshow/web/views/customers.py b/src/sideshow/web/views/customers.py index f3999bd..3f34a4b 100644 --- a/src/sideshow/web/views/customers.py +++ b/src/sideshow/web/views/customers.py @@ -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 diff --git a/src/sideshow/web/views/products.py b/src/sideshow/web/views/products.py index 7829b5d..6f7fe54 100644 --- a/src/sideshow/web/views/products.py +++ b/src/sideshow/web/views/products.py @@ -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 diff --git a/src/sideshow/web/views/shared.py b/src/sideshow/web/views/shared.py new file mode 100644 index 0000000..68e3abf --- /dev/null +++ b/src/sideshow/web/views/shared.py @@ -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 . +# +################################################################################ +""" +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))