diff --git a/pyproject.toml b/pyproject.toml
index ff4a4bc..d951e9e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -38,7 +38,8 @@ dependencies = [
[project.optional-dependencies]
postgres = ["psycopg2"]
mysql = ["mysql-connector-python"]
-docs = ["Sphinx", "furo", "sphinxcontrib-programoutput", "enum-tools[sphinx]"]
+# TODO: remove sphinx version cap after new sphinx-toolbox release?
+docs = ["Sphinx<8.2", "furo", "sphinxcontrib-programoutput", "enum-tools[sphinx]"]
tests = ["pytest-cov", "tox"]
diff --git a/src/sideshow/batch/neworder.py b/src/sideshow/batch/neworder.py
index 28e2627..1c10a4e 100644
--- a/src/sideshow/batch/neworder.py
+++ b/src/sideshow/batch/neworder.py
@@ -938,6 +938,8 @@ class NewOrderBatchHandler(BatchHandler):
row.department_id = product.department_id
row.department_name = product.department_name
row.special_order = product.special_order
+ row.vendor_name = product.vendor_name
+ row.vendor_item_code = product.vendor_item_code
row.case_size = product.case_size
row.unit_cost = product.unit_cost
row.unit_price_reg = product.unit_price_reg
@@ -959,6 +961,8 @@ class NewOrderBatchHandler(BatchHandler):
row.department_id = product.department_id
row.department_name = product.department_name
row.special_order = product.special_order
+ row.vendor_name = product.vendor_name
+ row.vendor_item_code = product.vendor_item_code
row.case_size = product.case_size
row.unit_cost = product.unit_cost
row.unit_price_reg = product.unit_price_reg
@@ -1017,8 +1021,14 @@ class NewOrderBatchHandler(BatchHandler):
def why_not_execute(self, batch, **kwargs):
"""
By default this checks to ensure the batch has a customer with
- phone number, and at least one item.
+ phone number, and at least one item. It also may check to
+ ensure the store is assigned, if applicable.
"""
+ if not batch.store_id:
+ order_handler = self.app.get_order_handler()
+ if order_handler.expose_store_id():
+ return "Must assign the store"
+
if not batch.customer_name:
return "Must assign the customer"
@@ -1190,6 +1200,8 @@ class NewOrderBatchHandler(BatchHandler):
'product_weighed',
'department_id',
'department_name',
+ 'vendor_name',
+ 'vendor_item_code',
'case_size',
'order_qty',
'order_uom',
diff --git a/src/sideshow/db/alembic/versions/13af2ffbc0e0_add_order_item_vendor.py b/src/sideshow/db/alembic/versions/13af2ffbc0e0_add_order_item_vendor.py
new file mode 100644
index 0000000..52fe552
--- /dev/null
+++ b/src/sideshow/db/alembic/versions/13af2ffbc0e0_add_order_item_vendor.py
@@ -0,0 +1,41 @@
+"""add order_item.vendor*
+
+Revision ID: 13af2ffbc0e0
+Revises: a4273360d379
+Create Date: 2025-02-19 19:36:30.308840
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+import wuttjamaican.db.util
+
+
+# revision identifiers, used by Alembic.
+revision: str = '13af2ffbc0e0'
+down_revision: Union[str, None] = 'a4273360d379'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+
+ # sideshow_batch_neworder_row
+ op.add_column('sideshow_batch_neworder_row', sa.Column('vendor_name', sa.String(length=50), nullable=True))
+ op.add_column('sideshow_batch_neworder_row', sa.Column('vendor_item_code', sa.String(length=20), nullable=True))
+
+ # sideshow_order_item
+ op.add_column('sideshow_order_item', sa.Column('vendor_name', sa.String(length=50), nullable=True))
+ op.add_column('sideshow_order_item', sa.Column('vendor_item_code', sa.String(length=20), nullable=True))
+
+
+def downgrade() -> None:
+
+ # sideshow_order_item
+ op.drop_column('sideshow_order_item', 'vendor_item_code')
+ op.drop_column('sideshow_order_item', 'vendor_name')
+
+ # sideshow_batch_neworder_row
+ op.drop_column('sideshow_batch_neworder_row', 'vendor_item_code')
+ op.drop_column('sideshow_batch_neworder_row', 'vendor_name')
diff --git a/src/sideshow/db/model/batch/neworder.py b/src/sideshow/db/model/batch/neworder.py
index 9121dc6..6a64372 100644
--- a/src/sideshow/db/model/batch/neworder.py
+++ b/src/sideshow/db/model/batch/neworder.py
@@ -252,6 +252,16 @@ class NewOrderBatchRow(model.BatchRowMixin, model.Base):
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.
diff --git a/src/sideshow/db/model/orders.py b/src/sideshow/db/model/orders.py
index 5455d01..8d01d27 100644
--- a/src/sideshow/db/model/orders.py
+++ b/src/sideshow/db/model/orders.py
@@ -253,6 +253,16 @@ class OrderItem(model.Base):
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.
""")
diff --git a/src/sideshow/web/templates/order-items/view.mako b/src/sideshow/web/templates/order-items/view.mako
index cb86977..e7d85ce 100644
--- a/src/sideshow/web/templates/order-items/view.mako
+++ b/src/sideshow/web/templates/order-items/view.mako
@@ -243,6 +243,12 @@
${app.render_boolean(item.special_order)}
+
+ ${item.vendor_name}
+
+
+ ${item.vendor_item_code}
+
diff --git a/src/sideshow/web/views/orders.py b/src/sideshow/web/views/orders.py
index 8aa7534..76399ff 100644
--- a/src/sideshow/web/views/orders.py
+++ b/src/sideshow/web/views/orders.py
@@ -1239,6 +1239,9 @@ class OrderItemView(MasterView):
# customer_name
g.set_label('customer_name', "Customer", column_only=True)
+ g.set_renderer('customer_name', self.render_order_attr)
+ g.set_sorter('customer_name', model.Order.customer_name)
+ g.set_filter('customer_name', model.Order.customer_name)
# # sequence
# g.set_label('sequence', "Seq.", column_only=True)
@@ -1562,6 +1565,26 @@ class PlacementView(OrderItemView):
route_prefix = 'order_items_placement'
url_prefix = '/placement'
+ grid_columns = [
+ 'order_id',
+ 'store_id',
+ 'customer_name',
+ 'product_brand',
+ 'product_description',
+ 'product_size',
+ 'department_name',
+ 'special_order',
+ 'vendor_name',
+ 'vendor_item_code',
+ 'order_qty',
+ 'order_uom',
+ 'total_price',
+ ]
+
+ filter_defaults = {
+ 'vendor_name': {'active': True},
+ }
+
def get_query(self, session=None):
""" """
query = super().get_query(session=session)
@@ -1664,6 +1687,26 @@ class ReceivingView(OrderItemView):
route_prefix = 'order_items_receiving'
url_prefix = '/receiving'
+ grid_columns = [
+ 'order_id',
+ 'store_id',
+ 'customer_name',
+ 'product_brand',
+ 'product_description',
+ 'product_size',
+ 'department_name',
+ 'special_order',
+ 'vendor_name',
+ 'vendor_item_code',
+ 'order_qty',
+ 'order_uom',
+ 'total_price',
+ ]
+
+ filter_defaults = {
+ 'vendor_name': {'active': True},
+ }
+
def get_query(self, session=None):
""" """
query = super().get_query(session=session)
diff --git a/tests/batch/test_neworder.py b/tests/batch/test_neworder.py
index ed2179a..c6dca19 100644
--- a/tests/batch/test_neworder.py
+++ b/tests/batch/test_neworder.py
@@ -818,6 +818,8 @@ class TestNewOrderBatchHandler(DataTestCase):
brand_name='Bragg',
description='Vinegar',
size='32oz',
+ vendor_name='Acme Distributors',
+ vendor_item_code='1234',
created_by=user,
status=enum.PendingProductStatus.PENDING)
row = handler.make_row(pending_product=product, order_qty=1, order_uom=enum.ORDER_UOM_UNIT)
@@ -830,6 +832,8 @@ class TestNewOrderBatchHandler(DataTestCase):
self.assertEqual(row.product_brand, 'Bragg')
self.assertEqual(row.product_description, 'Vinegar')
self.assertEqual(row.product_size, '32oz')
+ self.assertEqual(row.vendor_name, 'Acme Distributors')
+ self.assertEqual(row.vendor_item_code, '1234')
self.assertIsNone(row.case_size)
self.assertIsNone(row.unit_cost)
self.assertIsNone(row.unit_price_reg)
@@ -1114,9 +1118,15 @@ class TestNewOrderBatchHandler(DataTestCase):
self.session.add(row)
self.session.flush()
+ # batch is okay to execute..
reason = handler.why_not_execute(batch)
self.assertIsNone(reason)
+ # unless we also require store
+ self.config.setdefault('sideshow.orders.expose_store_id', 'true')
+ reason = handler.why_not_execute(batch)
+ self.assertEqual(reason, "Must assign the store")
+
def test_make_local_customer(self):
model = self.app.model
enum = self.app.enum