diff --git a/docs/api/sideshow.app.rst b/docs/api/sideshow.app.rst deleted file mode 100644 index 7c738b1..0000000 --- a/docs/api/sideshow.app.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``sideshow.app`` -================ - -.. automodule:: sideshow.app - :members: diff --git a/docs/api/sideshow.db.model.stores.rst b/docs/api/sideshow.db.model.stores.rst deleted file mode 100644 index b114a9b..0000000 --- a/docs/api/sideshow.db.model.stores.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``sideshow.db.model.stores`` -============================ - -.. automodule:: sideshow.db.model.stores - :members: diff --git a/docs/api/sideshow.web.views.stores.rst b/docs/api/sideshow.web.views.stores.rst deleted file mode 100644 index 896a0d7..0000000 --- a/docs/api/sideshow.web.views.stores.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``sideshow.web.views.stores`` -============================= - -.. automodule:: sideshow.web.views.stores - :members: diff --git a/docs/index.rst b/docs/index.rst index d91ae5e..29882dd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,7 +30,6 @@ For an online demo see https://demo.wuttaproject.org/ :caption: Package API: api/sideshow - api/sideshow.app api/sideshow.batch api/sideshow.batch.neworder api/sideshow.cli @@ -44,7 +43,6 @@ For an online demo see https://demo.wuttaproject.org/ api/sideshow.db.model.customers api/sideshow.db.model.orders api/sideshow.db.model.products - api/sideshow.db.model.stores api/sideshow.enum api/sideshow.orders api/sideshow.web @@ -60,4 +58,3 @@ For an online demo see https://demo.wuttaproject.org/ api/sideshow.web.views.customers api/sideshow.web.views.orders api/sideshow.web.views.products - api/sideshow.web.views.stores diff --git a/pyproject.toml b/pyproject.toml index ff4a4bc..e1d42a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,9 +51,6 @@ sideshow_libcache = "sideshow.web.static:libcache" [project.entry-points."paste.app_factory"] "main" = "sideshow.web.app:main" -[project.entry-points."wutta.app.providers"] -sideshow = "sideshow.app:SideshowAppProvider" - [project.entry-points."wutta.batch.neworder"] "sideshow" = "sideshow.batch.neworder:NewOrderBatchHandler" diff --git a/src/sideshow/app.py b/src/sideshow/app.py deleted file mode 100644 index 0fbcf2e..0000000 --- a/src/sideshow/app.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# Sideshow -- Case/Special Order Tracker -# Copyright © 2024 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 . -# -################################################################################ -""" -Sideshow app provider -""" - -from wuttjamaican import app as base - - -class SideshowAppProvider(base.AppProvider): - """ - The :term:`app provider` for Sideshow. - - This adds the :meth:`get_order_handler()` method to the :term:`app - handler`. - """ - - def get_order_handler(self, **kwargs): - """ - Get the configured :term:`order handler` for the app. - - You can specify a custom handler in your :term:`config file` - like: - - .. code-block:: ini - - [sideshow] - orders.handler_spec = poser.orders:PoserOrderHandler - - :returns: Instance of :class:`~sideshow.orders.OrderHandler`. - """ - if 'order_handler' not in self.__dict__: - spec = self.config.get('sideshow.orders.handler_spec', - default='sideshow.orders:OrderHandler') - self.order_handler = self.app.load_object(spec)(self.config) - return self.order_handler diff --git a/src/sideshow/batch/neworder.py b/src/sideshow/batch/neworder.py index 28e2627..bfa04ea 100644 --- a/src/sideshow/batch/neworder.py +++ b/src/sideshow/batch/neworder.py @@ -26,7 +26,6 @@ New Order Batch Handler import datetime import decimal -from collections import OrderedDict import sqlalchemy as sa @@ -51,14 +50,6 @@ class NewOrderBatchHandler(BatchHandler): """ model_class = NewOrderBatch - def get_default_store_id(self): - """ - Returns the configured default value for - :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.store_id`, - or ``None``. - """ - return self.config.get('sideshow.orders.default_store_id') - def use_local_customers(self): """ Returns boolean indicating whether :term:`local customer` @@ -174,18 +165,6 @@ class NewOrderBatchHandler(BatchHandler): 'label': customer.full_name} return [result(c) for c in customers] - def init_batch(self, batch, session=None, progress=None, **kwargs): - """ - Initialize a new batch. - - This sets the - :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.store_id`, - if the batch does not yet have one and a default is - configured. - """ - if not batch.store_id: - batch.store_id = self.get_default_store_id() - def set_customer(self, batch, customer_info, user=None): """ Set/update customer info for the batch. @@ -380,21 +359,6 @@ class NewOrderBatchHandler(BatchHandler): 'label': product.full_description} return [result(c) for c in products] - def get_default_uom_choices(self): - """ - Returns a list of ordering UOM choices which should be - presented to the user by default. - - The built-in logic here will return everything from - :data:`~sideshow.enum.ORDER_UOM`. - - :returns: List of dicts, each with ``key`` and ``value`` - corresponding to the UOM code and label, respectively. - """ - enum = self.app.enum - return [{'key': key, 'value': val} - for key, val in enum.ORDER_UOM.items()] - def get_product_info_external(self, session, product_id, user=None): """ Returns basic info for an :term:`external product` as pertains @@ -404,8 +368,7 @@ class NewOrderBatchHandler(BatchHandler): choose order quantity and UOM based on case size, pricing etc., this method is called to retrieve the product info. - There is no default logic here; subclass must implement. See - also :meth:`get_product_info_local()`. + There is no default logic here; subclass must implement. :param session: Current app :term:`db session`. @@ -461,58 +424,21 @@ class NewOrderBatchHandler(BatchHandler): def get_product_info_local(self, session, uuid, user=None): """ - Returns basic info for a :term:`local product` as pertains to - ordering. + Returns basic info for a + :class:`~sideshow.db.model.products.LocalProduct` as pertains + to ordering. When user has located a product via search, and must then choose order quantity and UOM based on case size, pricing etc., this method is called to retrieve the product info. See :meth:`get_product_info_external()` for more explanation. - - This method will locate the - :class:`~sideshow.db.model.products.LocalProduct` record, then - (if found) it calls :meth:`normalize_local_product()` and - returns the result. - - :param session: Current :term:`db session`. - - :param uuid: UUID for the desired - :class:`~sideshow.db.model.products.LocalProduct`. - - :param user: - :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who - is performing the action, if known. - - :returns: Dict of product info. """ model = self.app.model product = session.get(model.LocalProduct, uuid) if not product: raise ValueError(f"Local Product not found: {uuid}") - return self.normalize_local_product(product) - - def normalize_local_product(self, product): - """ - Returns a normalized dict of info for the given :term:`local - product`. - - This is called by: - - * :meth:`get_product_info_local()` - * :meth:`get_past_products()` - - :param product: - :class:`~sideshow.db.model.products.LocalProduct` instance. - - :returns: Dict of product info. - - The keys for this dict should essentially one-to-one for the - product fields, with one exception: - - * ``product_id`` will be set to the product UUID as string - """ return { 'product_id': product.uuid.hex, 'scancode': product.scancode, @@ -530,109 +456,6 @@ class NewOrderBatchHandler(BatchHandler): 'vendor_item_code': product.vendor_item_code, } - def get_past_orders(self, batch): - """ - Retrieve a (possibly empty) list of past :term:`orders - ` for the batch customer. - - This is called by :meth:`get_past_products()`. - - :param batch: - :class:`~sideshow.db.model.batch.neworder.NewOrderBatch` - instance. - - :returns: List of :class:`~sideshow.db.model.orders.Order` - records. - """ - model = self.app.model - session = self.app.get_session(batch) - orders = session.query(model.Order) - - if batch.customer_id: - orders = orders.filter(model.Order.customer_id == batch.customer_id) - elif batch.local_customer: - orders = orders.filter(model.Order.local_customer == batch.local_customer) - else: - raise ValueError(f"batch has no customer: {batch}") - - orders = orders.order_by(model.Order.created.desc()) - return orders.all() - - def get_past_products(self, batch, user=None): - """ - Retrieve a (possibly empty) list of products which have been - previously ordered by the batch customer. - - Note that this does not return :term:`order items `, nor does it return true product records, but rather it - returns a list of dicts. Each will have product info but will - *not* have order quantity etc. - - This method calls :meth:`get_past_orders()` and then iterates - through each order item therein. Any duplicated products - encountered will be skipped, so the final list contains unique - products. - - Each dict in the result is obtained by calling one of: - - * :meth:`normalize_local_product()` - * :meth:`get_product_info_external()` - - :param batch: - :class:`~sideshow.db.model.batch.neworder.NewOrderBatch` - instance. - - :param user: - :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who - is performing the action, if known. - - :returns: List of product info dicts. - """ - model = self.app.model - session = self.app.get_session(batch) - use_local = self.use_local_products() - user = user or batch.created_by - products = OrderedDict() - - # track down all order items for batch contact - for order in self.get_past_orders(batch): - for item in order.items: - - # nb. we only need the first match for each product - if use_local: - product = item.local_product - if product and product.uuid not in products: - products[product.uuid] = self.normalize_local_product(product) - elif item.product_id and item.product_id not in products: - products[item.product_id] = self.get_product_info_external( - session, item.product_id, user=user) - - products = list(products.values()) - for product in products: - - price = product['unit_price_reg'] - - if 'unit_price_reg_display' not in product: - product['unit_price_reg_display'] = self.app.render_currency(price) - - if 'unit_price_quoted' not in product: - product['unit_price_quoted'] = price - - if 'unit_price_quoted_display' not in product: - product['unit_price_quoted_display'] = product['unit_price_reg_display'] - - if ('case_price_quoted' not in product - and product.get('unit_price_quoted') is not None - and product.get('case_size') is not None): - product['case_price_quoted'] = product['unit_price_quoted'] * product['case_size'] - - if ('case_price_quoted_display' not in product - and 'case_price_quoted' in product): - product['case_price_quoted_display'] = self.app.render_currency( - product['case_price_quoted']) - - return products - def add_item(self, batch, product_info, order_qty, order_uom, discount_percent=None, user=None): """ diff --git a/src/sideshow/db/alembic/versions/a4273360d379_add_stores.py b/src/sideshow/db/alembic/versions/a4273360d379_add_stores.py deleted file mode 100644 index 79e6242..0000000 --- a/src/sideshow/db/alembic/versions/a4273360d379_add_stores.py +++ /dev/null @@ -1,39 +0,0 @@ -"""add stores - -Revision ID: a4273360d379 -Revises: 7a6df83afbd4 -Create Date: 2025-01-27 17:48:20.638664 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = 'a4273360d379' -down_revision: Union[str, None] = '7a6df83afbd4' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # sideshow_store - op.create_table('sideshow_store', - sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False), - sa.Column('store_id', sa.String(length=10), nullable=False), - sa.Column('name', sa.String(length=100), nullable=False), - sa.Column('archived', sa.Boolean(), nullable=False), - sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_store')), - sa.UniqueConstraint('store_id', name=op.f('uq_sideshow_store_store_id')), - sa.UniqueConstraint('name', name=op.f('uq_sideshow_store_name')) - ) - - -def downgrade() -> None: - - # sideshow_store - op.drop_table('sideshow_store') diff --git a/src/sideshow/db/model/__init__.py b/src/sideshow/db/model/__init__.py index 056ccfc..f53dd27 100644 --- a/src/sideshow/db/model/__init__.py +++ b/src/sideshow/db/model/__init__.py @@ -30,7 +30,6 @@ This namespace exposes everything from Primary :term:`data models `: -* :class:`~sideshow.db.model.stores.Store` * :class:`~sideshow.db.model.orders.Order` * :class:`~sideshow.db.model.orders.OrderItem` * :class:`~sideshow.db.model.orders.OrderItemEvent` @@ -49,7 +48,6 @@ And the :term:`batch` models: from wuttjamaican.db.model import * # sideshow models -from .stores import Store from .customers import LocalCustomer, PendingCustomer from .products import LocalProduct, PendingProduct from .orders import Order, OrderItem, OrderItemEvent diff --git a/src/sideshow/db/model/orders.py b/src/sideshow/db/model/orders.py index 5455d01..2cadeaa 100644 --- a/src/sideshow/db/model/orders.py +++ b/src/sideshow/db/model/orders.py @@ -62,15 +62,6 @@ class Order(model.Base): ID of the store to which the order pertains, if applicable. """) - 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, doc=""" Proper account ID for the :term:`external customer` to which the order pertains, if applicable. diff --git a/src/sideshow/db/model/stores.py b/src/sideshow/db/model/stores.py deleted file mode 100644 index 4b01d02..0000000 --- a/src/sideshow/db/model/stores.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- 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 . -# -################################################################################ -""" -Data models for Stores -""" - -import sqlalchemy as sa - -from wuttjamaican.db import model - - -class Store(model.Base): - """ - Represents a physical location for the business. - """ - __tablename__ = 'sideshow_store' - - uuid = model.uuid_column() - - store_id = sa.Column(sa.String(length=10), nullable=False, unique=True, doc=""" - Unique ID for the store. - """) - - name = sa.Column(sa.String(length=100), nullable=False, unique=True, doc=""" - Display name for the store (must be unique!). - """) - - archived = sa.Column(sa.Boolean(), nullable=False, default=False, doc=""" - Indicates the store has been "retired" essentially, and mostly - hidden from view. - """) - - def __str__(self): - return self.get_display() - - def get_display(self): - """ - Returns the display string for the store, e.g. "001 Acme Goods". - """ - return ' '.join([(self.store_id or '').strip(), - (self.name or '').strip()])\ - .strip() diff --git a/src/sideshow/orders.py b/src/sideshow/orders.py index 868cada..9f99e53 100644 --- a/src/sideshow/orders.py +++ b/src/sideshow/orders.py @@ -37,14 +37,6 @@ class OrderHandler(GenericHandler): handler is responsible for creation logic.) """ - def expose_store_id(self): - """ - Returns boolean indicating whether the ``store_id`` field - should be exposed at all. This is false by default. - """ - return self.config.get_bool('sideshow.orders.expose_store_id', - default=False) - def get_order_qty_uom_text(self, order_qty, order_uom, case_size=None, html=False): """ Return the display text for a given order quantity. diff --git a/src/sideshow/web/menus.py b/src/sideshow/web/menus.py index 9da61c0..1641c72 100644 --- a/src/sideshow/web/menus.py +++ b/src/sideshow/web/menus.py @@ -162,12 +162,4 @@ class SideshowMenuHandler(base.MenuHandler): def make_admin_menu(self, request, **kwargs): """ """ kwargs['include_people'] = True - menu = super().make_admin_menu(request, **kwargs) - - menu['items'].insert(0, { - 'title': "Stores", - 'route': 'stores', - 'perm': 'stores.list', - }) - - return menu + return super().make_admin_menu(request, **kwargs) diff --git a/src/sideshow/web/templates/order-items/view.mako b/src/sideshow/web/templates/order-items/view.mako index cb86977..3cd210a 100644 --- a/src/sideshow/web/templates/order-items/view.mako +++ b/src/sideshow/web/templates/order-items/view.mako @@ -29,17 +29,6 @@ ${h.link_to(f"Order ID {order.order_id}", url('orders.view', uuid=order.uuid))} — Item #${item.sequence} - % if expose_store_id: - - - % if order.store: - ${h.link_to(order.store.get_display(), url('stores.view', uuid=order.store.uuid))} - % elif order.store_id: - ${order.store_id} - % endif - - - % endif ${order_qty_uom_text|n} @@ -234,10 +223,7 @@ ${app.render_boolean(item.product_weighed)} - - ${item.department_id} - - + ${item.department_name} diff --git a/src/sideshow/web/templates/orders/configure.mako b/src/sideshow/web/templates/orders/configure.mako index 144e5f0..e247b2f 100644 --- a/src/sideshow/web/templates/orders/configure.mako +++ b/src/sideshow/web/templates/orders/configure.mako @@ -3,28 +3,6 @@ <%def name="form_content()"> -

Stores

-
- - - - Show/choose the Store ID for each order - - - - - - - -
-

Customers

@@ -36,12 +14,41 @@ -

Products

+ + + Allow per-item discounts + + + + + + Allow discount even if item is on sale + + + +
+
Default item discount
+
+ +
+
%
+
+
-

Pricing

-
- - - - Allow per-item discounts - - - - - - Allow discount even if item is on sale - - - -
- Global default item discount - - % -
- -
-
-

Per-Department default item discounts

-
- - Add - - - <${b}-modal has-modal-card - % if request.use_oruga: - v-model:active="deptItemDiscountShowDialog" - % else: - :active.sync="deptItemDiscountShowDialog" - % endif - > - - -
-
- <${b}-table :data="deptItemDiscounts"> - <${b}-table-column field="department_id" - label="Dept. ID" - v-slot="props"> - {{ props.row.department_id }} - - <${b}-table-column field="department_name" - label="Department Name" - v-slot="props"> - {{ props.row.department_name }} - - <${b}-table-column field="default_item_discount" - label="Discount" - v-slot="props"> - {{ props.row.default_item_discount }} % - - <${b}-table-column label="Actions" - v-slot="props"> - - - Edit - - - - Delete - - - -
-
-

Batches

@@ -241,62 +118,5 @@ ThisPageData.batchHandlers = ${json.dumps(batch_handlers)|n} - ThisPageData.deptItemDiscounts = ${json.dumps(dept_item_discounts)|n} - ThisPageData.deptItemDiscountShowDialog = false - ThisPageData.deptItemDiscountRow = null - ThisPageData.deptItemDiscountDeptID = null - ThisPageData.deptItemDiscountDeptName = null - ThisPageData.deptItemDiscountPercent = null - - ThisPage.computed.deptItemDiscountSaveDisabled = function() { - if (!this.deptItemDiscountDeptID) { - return true - } - if (!this.deptItemDiscountDeptName) { - return true - } - if (!this.deptItemDiscountPercent) { - return true - } - return false - } - - ThisPage.methods.deptItemDiscountDelete = function(row) { - const i = this.deptItemDiscounts.indexOf(row) - this.deptItemDiscounts.splice(i, 1) - this.settingsNeedSaved = true - } - - ThisPage.methods.deptItemDiscountInit = function(row) { - this.deptItemDiscountRow = row - this.deptItemDiscountDeptID = row?.department_id - this.deptItemDiscountDeptName = row?.department_name - this.deptItemDiscountPercent = row?.default_item_discount - this.deptItemDiscountShowDialog = true - this.$nextTick(() => { - if (row) { - this.$refs.deptItemDiscountPercent.focus() - } else { - this.$refs.deptItemDiscountDeptID.focus() - } - }) - } - - ThisPage.methods.deptItemDiscountSave = function() { - if (this.deptItemDiscountRow) { - this.deptItemDiscountRow.department_id = this.deptItemDiscountDeptID - this.deptItemDiscountRow.department_name = this.deptItemDiscountDeptName - this.deptItemDiscountRow.default_item_discount = this.deptItemDiscountPercent - } else { - this.deptItemDiscounts.push({ - department_id: this.deptItemDiscountDeptID, - department_name: this.deptItemDiscountDeptName, - default_item_discount: this.deptItemDiscountPercent, - }) - } - this.deptItemDiscountShowDialog = false - this.settingsNeedSaved = true - } - diff --git a/src/sideshow/web/templates/orders/create.mako b/src/sideshow/web/templates/orders/create.mako index 5dd39b7..2ac6f0a 100644 --- a/src/sideshow/web/templates/orders/create.mako +++ b/src/sideshow/web/templates/orders/create.mako @@ -42,25 +42,7 @@