diff --git a/docs/api/sideshow.app.rst b/docs/api/sideshow.app.rst
new file mode 100644
index 0000000..7c738b1
--- /dev/null
+++ b/docs/api/sideshow.app.rst
@@ -0,0 +1,6 @@
+
+``sideshow.app``
+================
+
+.. automodule:: sideshow.app
+   :members:
diff --git a/docs/api/sideshow.db.model.stores.rst b/docs/api/sideshow.db.model.stores.rst
new file mode 100644
index 0000000..b114a9b
--- /dev/null
+++ b/docs/api/sideshow.db.model.stores.rst
@@ -0,0 +1,6 @@
+
+``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
new file mode 100644
index 0000000..896a0d7
--- /dev/null
+++ b/docs/api/sideshow.web.views.stores.rst
@@ -0,0 +1,6 @@
+
+``sideshow.web.views.stores``
+=============================
+
+.. automodule:: sideshow.web.views.stores
+   :members:
diff --git a/docs/index.rst b/docs/index.rst
index 29882dd..d91ae5e 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -30,6 +30,7 @@ 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
@@ -43,6 +44,7 @@ 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
@@ -58,3 +60,4 @@ 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 e1d42a8..ff4a4bc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -51,6 +51,9 @@ 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
new file mode 100644
index 0000000..0fbcf2e
--- /dev/null
+++ b/src/sideshow/app.py
@@ -0,0 +1,56 @@
+# -*- 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 <http://www.gnu.org/licenses/>.
+#
+################################################################################
+"""
+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 bfa04ea..28e2627 100644
--- a/src/sideshow/batch/neworder.py
+++ b/src/sideshow/batch/neworder.py
@@ -26,6 +26,7 @@ New Order Batch Handler
 
 import datetime
 import decimal
+from collections import OrderedDict
 
 import sqlalchemy as sa
 
@@ -50,6 +51,14 @@ 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`
@@ -165,6 +174,18 @@ 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.
@@ -359,6 +380,21 @@ 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
@@ -368,7 +404,8 @@ 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.
+        There is no default logic here; subclass must implement.  See
+        also :meth:`get_product_info_local()`.
 
         :param session: Current app :term:`db session`.
 
@@ -424,21 +461,58 @@ class NewOrderBatchHandler(BatchHandler):
 
     def get_product_info_local(self, session, uuid, user=None):
         """
-        Returns basic info for a
-        :class:`~sideshow.db.model.products.LocalProduct` as pertains
-        to ordering.
+        Returns basic info for a :term:`local product` 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,
@@ -456,6 +530,109 @@ 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
+        <order>` 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 <order
+        item>`, 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
new file mode 100644
index 0000000..79e6242
--- /dev/null
+++ b/src/sideshow/db/alembic/versions/a4273360d379_add_stores.py
@@ -0,0 +1,39 @@
+"""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 f53dd27..056ccfc 100644
--- a/src/sideshow/db/model/__init__.py
+++ b/src/sideshow/db/model/__init__.py
@@ -30,6 +30,7 @@ This namespace exposes everything from
 
 Primary :term:`data models <data model>`:
 
+* :class:`~sideshow.db.model.stores.Store`
 * :class:`~sideshow.db.model.orders.Order`
 * :class:`~sideshow.db.model.orders.OrderItem`
 * :class:`~sideshow.db.model.orders.OrderItemEvent`
@@ -48,6 +49,7 @@ 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 2cadeaa..5455d01 100644
--- a/src/sideshow/db/model/orders.py
+++ b/src/sideshow/db/model/orders.py
@@ -62,6 +62,15 @@ 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
new file mode 100644
index 0000000..4b01d02
--- /dev/null
+++ b/src/sideshow/db/model/stores.py
@@ -0,0 +1,62 @@
+# -*- 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/>.
+#
+################################################################################
+"""
+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 9f99e53..868cada 100644
--- a/src/sideshow/orders.py
+++ b/src/sideshow/orders.py
@@ -37,6 +37,14 @@ 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 1641c72..9da61c0 100644
--- a/src/sideshow/web/menus.py
+++ b/src/sideshow/web/menus.py
@@ -162,4 +162,12 @@ class SideshowMenuHandler(base.MenuHandler):
     def make_admin_menu(self, request, **kwargs):
         """ """
         kwargs['include_people'] = True
-        return super().make_admin_menu(request, **kwargs)
+        menu = super().make_admin_menu(request, **kwargs)
+
+        menu['items'].insert(0, {
+            'title': "Stores",
+            'route': 'stores',
+            'perm': 'stores.list',
+        })
+
+        return menu
diff --git a/src/sideshow/web/templates/order-items/view.mako b/src/sideshow/web/templates/order-items/view.mako
index 3cd210a..cb86977 100644
--- a/src/sideshow/web/templates/order-items/view.mako
+++ b/src/sideshow/web/templates/order-items/view.mako
@@ -29,6 +29,17 @@
             <b-field horizontal label="ID">
               <span>${h.link_to(f"Order ID {order.order_id}", url('orders.view', uuid=order.uuid))} &mdash; Item #${item.sequence}</span>
             </b-field>
+            % if expose_store_id:
+                <b-field horizontal label="Store">
+                  <span>
+                    % 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
+                  </span>
+                </b-field>
+            % endif
             <b-field horizontal label="Order Qty">
               <span>${order_qty_uom_text|n}</span>
             </b-field>
@@ -223,7 +234,10 @@
             <b-field horizontal label="Sold by Weight">
               <span>${app.render_boolean(item.product_weighed)}</span>
             </b-field>
-            <b-field horizontal label="Department">
+            <b-field horizontal label="Department ID">
+              <span>${item.department_id}</span>
+            </b-field>
+            <b-field horizontal label="Department Name">
               <span>${item.department_name}</span>
             </b-field>
             <b-field horizontal label="Special Order">
diff --git a/src/sideshow/web/templates/orders/configure.mako b/src/sideshow/web/templates/orders/configure.mako
index e247b2f..144e5f0 100644
--- a/src/sideshow/web/templates/orders/configure.mako
+++ b/src/sideshow/web/templates/orders/configure.mako
@@ -3,6 +3,28 @@
 
 <%def name="form_content()">
 
+  <h3 class="block is-size-3">Stores</h3>
+  <div class="block" style="padding-left: 2rem;">
+
+    <b-field>
+      <b-checkbox name="sideshow.orders.expose_store_id"
+                  v-model="simpleSettings['sideshow.orders.expose_store_id']"
+                  native-value="true"
+                  @input="settingsNeedSaved = true">
+        Show/choose the Store ID for each order
+      </b-checkbox>
+    </b-field>
+
+    <b-field v-show="simpleSettings['sideshow.orders.expose_store_id']"
+             label="Default Store ID">
+      <b-input name="sideshow.orders.default_store_id"
+               v-model="simpleSettings['sideshow.orders.default_store_id']"
+               @input="settingsNeedSaved = true"
+               style="width: 25rem;" />
+    </b-field>
+
+  </div>
+
   <h3 class="block is-size-3">Customers</h3>
   <div class="block" style="padding-left: 2rem;">
 
@@ -14,41 +36,12 @@
         <option value="false">External Customers (e.g. in POS)</option>
       </b-select>
     </b-field>
+
   </div>
 
   <h3 class="block is-size-3">Products</h3>
   <div class="block" style="padding-left: 2rem;">
 
-    <b-field>
-      <b-checkbox name="sideshow.orders.allow_item_discounts"
-                  v-model="simpleSettings['sideshow.orders.allow_item_discounts']"
-                  native-value="true"
-                  @input="settingsNeedSaved = true">
-        Allow per-item discounts
-      </b-checkbox>
-    </b-field>
-
-    <b-field v-show="simpleSettings['sideshow.orders.allow_item_discounts']">
-      <b-checkbox name="sideshow.orders.allow_item_discounts_if_on_sale"
-                  v-model="simpleSettings['sideshow.orders.allow_item_discounts_if_on_sale']"
-                  native-value="true"
-                  @input="settingsNeedSaved = true">
-        Allow discount even if item is on sale
-      </b-checkbox>
-    </b-field>
-
-    <div v-show="simpleSettings['sideshow.orders.allow_item_discounts']"
-         class="level-left block">
-      <div class="level-item">Default item discount</div>
-      <div class="level-item">
-        <b-input name="sideshow.orders.default_item_discount"
-                 v-model="simpleSettings['sideshow.orders.default_item_discount']"
-                 @input="settingsNeedSaved = true"
-                 style="width: 5rem;" />
-      </div>
-      <div class="level-item">%</div>
-    </div>
-
     <b-field label="Product Source">
       <b-select name="sideshow.orders.use_local_products"
                   v-model="simpleSettings['sideshow.orders.use_local_products']"
@@ -92,6 +85,136 @@
     </div>
   </div>
 
+  <h3 class="block is-size-3">Pricing</h3>
+  <div class="block" style="padding-left: 2rem;">
+
+    <b-field>
+      <b-checkbox name="sideshow.orders.allow_item_discounts"
+                  v-model="simpleSettings['sideshow.orders.allow_item_discounts']"
+                  native-value="true"
+                  @input="settingsNeedSaved = true">
+        Allow per-item discounts
+      </b-checkbox>
+    </b-field>
+
+    <b-field v-show="simpleSettings['sideshow.orders.allow_item_discounts']">
+      <b-checkbox name="sideshow.orders.allow_item_discounts_if_on_sale"
+                  v-model="simpleSettings['sideshow.orders.allow_item_discounts_if_on_sale']"
+                  native-value="true"
+                  @input="settingsNeedSaved = true">
+        Allow discount even if item is on sale
+      </b-checkbox>
+    </b-field>
+
+    <div v-show="simpleSettings['sideshow.orders.allow_item_discounts']"
+         class="block"
+         style="display: flex; gap: 0.5rem; align-items: center;">
+      <span>Global default item discount</span>
+      <b-input name="sideshow.orders.default_item_discount"
+               v-model="simpleSettings['sideshow.orders.default_item_discount']"
+               @input="settingsNeedSaved = true"
+               style="width: 5rem;" />
+      <span>%</span>
+    </div>
+
+    <div v-show="simpleSettings['sideshow.orders.allow_item_discounts']"
+         style="width: 50%;">
+      <div style="display: flex; gap: 1rem; align-items: center;">
+        <p>Per-Department default item discounts</p>
+        <div>
+          <b-button type="is-primary"
+                    @click="deptItemDiscountInit()"
+                    icon-pack="fas"
+                    icon-left="plus">
+            Add
+          </b-button>
+          <input type="hidden" name="dept_item_discounts" :value="JSON.stringify(deptItemDiscounts)" />
+          <${b}-modal has-modal-card
+                      % if request.use_oruga:
+                          v-model:active="deptItemDiscountShowDialog"
+                      % else:
+                          :active.sync="deptItemDiscountShowDialog"
+                      % endif
+                      >
+            <div class="modal-card">
+
+              <header class="modal-card-head">
+                <p class="modal-card-title">Default Discount for Department</p>
+              </header>
+
+              <section class="modal-card-body">
+                <div style="display: flex; gap: 1rem;">
+                  <b-field label="Dept. ID"
+                           :type="deptItemDiscountDeptID ? null : 'is-danger'">
+                    <b-input v-model="deptItemDiscountDeptID"
+                             ref="deptItemDiscountDeptID"
+                             style="width: 6rem;;" />
+                  </b-field>
+                  <b-field label="Department Name"
+                           :type="deptItemDiscountDeptName ? null : 'is-danger'"
+                           style="flex-grow: 1;">
+                    <b-input v-model="deptItemDiscountDeptName" />
+                  </b-field>
+                  <b-field label="Discount"
+                           :type="deptItemDiscountPercent ? null : 'is-danger'">
+                    <div style="display: flex; gap: 0.5rem; align-items: center;">
+                      <b-input v-model="deptItemDiscountPercent"
+                               ref="deptItemDiscountPercent"
+                               style="width: 6rem;" />
+                      <span>%</span>
+                    </div>
+                  </b-field>
+                </div>
+              </section>
+
+              <footer class="modal-card-foot">
+                <b-button type="is-primary"
+                          icon-pack="fas"
+                          icon-left="save"
+                          :disabled="deptItemDiscountSaveDisabled"
+                          @click="deptItemDiscountSave()">
+                  Save
+                </b-button>
+                <b-button @click="deptItemDiscountShowDialog = false">
+                  Cancel
+                </b-button>
+              </footer>
+            </div>
+          </${b}-modal>
+        </div>
+      </div>
+      <${b}-table :data="deptItemDiscounts">
+        <${b}-table-column field="department_id"
+                           label="Dept. ID"
+                           v-slot="props">
+          {{ props.row.department_id }}
+        </${b}-table-column>
+        <${b}-table-column field="department_name"
+                           label="Department Name"
+                           v-slot="props">
+          {{ props.row.department_name }}
+        </${b}-table-column>
+        <${b}-table-column field="default_item_discount"
+                           label="Discount"
+                           v-slot="props">
+          {{ props.row.default_item_discount }} %
+        </${b}-table-column>
+        <${b}-table-column label="Actions"
+                           v-slot="props">
+          <a href="#" @click.prevent="deptItemDiscountInit(props.row)">
+            <i class="fas fa-edit" />
+            Edit
+          </a>
+          <a href="#" @click.prevent="deptItemDiscountDelete(props.row)"
+             class="has-text-danger">
+            <i class="fas fa-trash" />
+            Delete
+          </a>
+        </${b}-table-column>
+      </${b}-table>
+    </div>
+  </div>
+
   <h3 class="block is-size-3">Batches</h3>
   <div class="block" style="padding-left: 2rem;">
 
@@ -118,5 +241,62 @@
 
     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
+    }
+
   </script>
 </%def>
diff --git a/src/sideshow/web/templates/orders/create.mako b/src/sideshow/web/templates/orders/create.mako
index 2ac6f0a..5dd39b7 100644
--- a/src/sideshow/web/templates/orders/create.mako
+++ b/src/sideshow/web/templates/orders/create.mako
@@ -42,7 +42,25 @@
   <script type="text/x-template" id="order-creator-template">
     <div>
 
-      ${self.order_form_buttons()}
+      <div style="display: flex; justify-content: space-between; margin-bottom: 1.5rem;">
+        <div>
+          % if expose_store_id:
+              <b-loading v-model="storeLoading" is-full-page />
+              <b-field label="Store" horizontal
+                       :type="storeID ? null : 'is-danger'">
+                <b-select v-model="storeID"
+                          @input="storeChanged">
+                  <option v-for="store in stores"
+                          :key="store.store_id"
+                          :value="store.store_id">
+                    {{ store.display }}
+                  </option>
+                </b-select>
+              </b-field>
+          % endif
+        </div>
+        ${self.order_form_buttons()}
+      </div>
 
       <${b}-collapse class="panel"
                      :class="customerPanelType"
@@ -337,6 +355,135 @@
                         @click="showAddItemDialog()">
                 Add Item
               </b-button>
+              % if allow_past_item_reorder:
+                  <b-button v-if="customerIsKnown && customerID"
+                            icon-pack="fas"
+                            icon-left="plus"
+                            @click="showAddPastItem()">
+                    Add Past Item
+                  </b-button>
+
+                  <${b}-modal
+                    % if request.use_oruga:
+                        v-model:active="pastItemsShowDialog"
+                    % else:
+                        :active.sync="pastItemsShowDialog"
+                    % endif
+                    >
+                    <div class="card">
+                      <div class="card-content">
+
+                        <${b}-table :data="pastItems"
+                                    icon-pack="fas"
+                                    :loading="pastItemsLoading"
+                                    % if request.use_oruga:
+                                        v-model:selected="pastItemsSelected"
+                                    % else:
+                                        :selected.sync="pastItemsSelected"
+                                    % endif
+                                    sortable
+                                    paginated
+                                    per-page="5"
+                                    ## :debounce-search="1000"
+                                    >
+
+                          <${b}-table-column label="Scancode"
+                                          field="key"
+                                          v-slot="props"
+                                          sortable>
+                            {{ props.row.scancode }}
+                          </${b}-table-column>
+
+                          <${b}-table-column label="Brand"
+                                          field="brand_name"
+                                          v-slot="props"
+                                          sortable
+                                          searchable>
+                            {{ props.row.brand_name }}
+                          </${b}-table-column>
+
+                          <${b}-table-column label="Description"
+                                          field="description"
+                                          v-slot="props"
+                                          sortable
+                                          searchable>
+                            {{ props.row.description }}
+                            {{ props.row.size }}
+                          </${b}-table-column>
+
+                          <${b}-table-column label="Unit Price"
+                                          field="unit_price_reg_display"
+                                          v-slot="props"
+                                          sortable>
+                            {{ props.row.unit_price_reg_display }}
+                          </${b}-table-column>
+
+                          <${b}-table-column label="Sale Price"
+                                          field="sale_price"
+                                          v-slot="props"
+                                          sortable>
+                            <span class="has-background-warning">
+                              {{ props.row.sale_price_display }}
+                            </span>
+                          </${b}-table-column>
+
+                          <${b}-table-column label="Sale Ends"
+                                          field="sale_ends"
+                                          v-slot="props"
+                                          sortable>
+                            <span class="has-background-warning">
+                              {{ props.row.sale_ends_display }}
+                            </span>
+                          </${b}-table-column>
+
+                          <${b}-table-column label="Department"
+                                          field="department_name"
+                                          v-slot="props"
+                                          sortable
+                                          searchable>
+                            {{ props.row.department_name }}
+                          </${b}-table-column>
+
+                          <${b}-table-column label="Vendor"
+                                          field="vendor_name"
+                                          v-slot="props"
+                                          sortable
+                                          searchable>
+                            {{ props.row.vendor_name }}
+                          </${b}-table-column>
+
+                          <template #empty>
+                            <div class="content has-text-grey has-text-centered">
+                              <p>
+                                <b-icon
+                                  pack="fas"
+                                  icon="sad-tear"
+                                  size="is-large">
+                                </b-icon>
+                              </p>
+                              <p>Nothing here.</p>
+                            </div>
+                          </template>
+                        </${b}-table>
+
+                        <div class="buttons">
+                          <b-button @click="pastItemsShowDialog = false">
+                            Cancel
+                          </b-button>
+                          <b-button type="is-primary"
+                                    icon-pack="fas"
+                                    icon-left="plus"
+                                    @click="pastItemsAddSelected()"
+                                    :disabled="!pastItemsSelected">
+                            Add Selected Item
+                          </b-button>
+                        </div>
+
+                      </div>
+                    </div>
+                  </${b}-modal>
+
+              % endif
             </div>
 
             <${b}-modal
@@ -486,7 +633,16 @@
                             <b-input v-model="pendingProduct.scancode" />
                           </b-field>
 
-                          <b-field label="Department"
+                          <b-field label="Dept. ID"
+                                   % if 'department_id' in pending_product_required_fields:
+                                   :type="pendingProduct.department_id ? null : 'is-danger'"
+                                   % endif
+                                   style="width: 15rem;">
+                            <b-input v-model="pendingProduct.department_id"
+                                     @input="updateDiscount" />
+                          </b-field>
+
+                          <b-field label="Department Name"
                                    % if 'department_name' in pending_product_required_fields:
                                    :type="pendingProduct.department_name ? null : 'is-danger'"
                                    % endif
@@ -494,8 +650,7 @@
                             <b-input v-model="pendingProduct.department_name" />
                           </b-field>
 
-                          <b-field label="Special Order"
-                                   style="width: 100%;">
+                          <b-field label="Special Order">
                             <b-checkbox v-model="pendingProduct.special_order" />
                           </b-field>
 
@@ -732,7 +887,7 @@
 
               <${b}-table-column label="Department"
                               v-slot="props">
-                {{ props.row.department_display }}
+                {{ props.row.department_name }}
               </${b}-table-column>
 
               <${b}-table-column label="Quantity"
@@ -837,6 +992,12 @@
 
                 batchTotalPriceDisplay: ${json.dumps(normalized_batch['total_price_display'])|n},
 
+                % if expose_store_id:
+                    stores: ${json.dumps(stores)|n},
+                    storeID: ${json.dumps(batch.store_id)|n},
+                    storeLoading: false,
+                % endif
+
                 customerPanelOpen: false,
                 customerLoading: false,
                 customerIsKnown: ${json.dumps(customer_is_known)|n},
@@ -898,8 +1059,10 @@
                 productCaseSize: null,
 
                 % if allow_item_discounts:
-                    productDiscountPercent: ${json.dumps(default_item_discount)|n},
+                    defaultItemDiscount: ${json.dumps(default_item_discount)|n},
+                    deptItemDiscounts: ${json.dumps(dept_item_discounts)|n},
                     allowDiscountsIfOnSale: ${json.dumps(allow_item_discounts_if_on_sale)|n},
+                    productDiscountPercent: null,
                 % endif
 
                 pendingProduct: {},
@@ -908,6 +1071,13 @@
                 ## departmentOptions: ${json.dumps(department_options)|n},
                 departmentOptions: [],
 
+                % if allow_past_item_reorder:
+                    pastItemsShowDialog: false,
+                    pastItemsLoading: false,
+                    pastItems: [],
+                    pastItemsSelected: null,
+                % endif
+
                 // nb. hack to force refresh for vue3
                 refreshProductDescription: 1,
                 refreshTotalPrice: 1,
@@ -1160,8 +1330,31 @@
                 })
             },
 
+            % if expose_store_id:
+
+                storeChanged(storeID) {
+                    this.storeLoading = true
+                    const params = {
+                        action: 'set_store',
+                        store_id: storeID,
+                    }
+                    this.submitBatchData(params, ({data}) => {
+                        this.storeLoading = false
+                    }, response => {
+                        this.$buefy.toast.open({
+                            message: "Update failed: " + (response.data.error || "(unknown error)"),
+                            type: 'is-danger',
+                            duration: 2000, // 2 seconds
+                        })
+                        this.storeLoading = false
+                    })
+                },
+
+            % endif
+
             customerChanged(customerID, callback) {
                 this.customerLoading = true
+                this.pastItems = []
 
                 const params = {}
                 if (customerID) {
@@ -1213,6 +1406,23 @@
                 })
             },
 
+            % if allow_item_discounts:
+
+                updateDiscount(deptID) {
+                    if (deptID) {
+                        // nb. our map requires ID as string
+                        deptID = deptID.toString()
+                    }
+                    const i = Object.keys(this.deptItemDiscounts).indexOf(deptID)
+                    if (i == -1) {
+                        this.productDiscountPercent = this.defaultItemDiscount
+                    } else {
+                        this.productDiscountPercent = this.deptItemDiscounts[deptID]
+                    }
+                },
+
+            % endif
+
             editNewCustomerSave() {
                 this.editNewCustomerSaving = true
 
@@ -1351,7 +1561,7 @@
                 this.productUnitChoices = this.defaultUnitChoices
 
                 % if allow_item_discounts:
-                    this.productDiscountPercent = ${json.dumps(default_item_discount)|n}
+                    this.productDiscountPercent = this.defaultItemDiscount
                 % endif
             },
 
@@ -1390,7 +1600,15 @@
                         this.productSaleEndsDisplay = data.sale_ends_display
 
                         % if allow_item_discounts:
-                            this.productDiscountPercent = this.allowItemDiscount ? data.default_item_discount : null
+                            if (this.allowItemDiscount) {
+                                if (data?.default_item_discount != null) {
+                                    this.productDiscountPercent = data.default_item_discount
+                                } else {
+                                    this.updateDiscount(data?.department_id)
+                                }
+                            } else {
+                                this.productDiscountPercent = null
+                            }
                         % endif
 
                         // this.setProductUnitChoices(data.uom_choices)
@@ -1468,7 +1686,7 @@
                 this.productUOM = this.defaultUOM
 
                 % if allow_item_discounts:
-                    this.productDiscountPercent = ${json.dumps(default_item_discount)|n}
+                    this.productDiscountPercent = this.defaultItemDiscount
                 % endif
 
                 % if request.use_oruga:
@@ -1522,7 +1740,7 @@
                 ## this.productSpecialOrder = row.special_order
 
                 this.productQuantity = row.order_qty
-                this.productUnitChoices = row.order_uom_choices
+                this.productUnitChoices = row?.order_uom_choices || this.defaultUnitChoices
                 this.productUOM = row.order_uom
 
                 % if allow_item_discounts:
@@ -1564,12 +1782,77 @@
                 })
             },
 
+            % if allow_past_item_reorder:
+
+                showAddPastItem() {
+                    this.pastItemsSelected = null
+                    if (!this.pastItems.length) {
+                        this.pastItemsLoading = true
+                        const params = {action: 'get_past_products'}
+                        this.submitBatchData(params, ({data}) => {
+                            this.pastItems = data
+                            this.pastItemsLoading = false
+                        })
+                    }
+                    this.pastItemsShowDialog = true
+                },
+
+                pastItemsAddSelected() {
+                    this.pastItemsShowDialog = false
+                    const selected = this.pastItemsSelected
+
+                    this.editItemRow = null
+                    this.productIsKnown = true
+                    this.productID = selected.product_id
+
+                    this.selectedProduct = {
+                        product_id: selected.product_id,
+                        full_description: selected.full_description,
+                        // url: selected.product_url,
+                    }
+
+                    this.productDisplay = selected.full_description
+                    this.productScancode = selected.scancode
+                    this.productSize = selected.size
+                    this.productCaseQuantity = selected.case_size
+                    this.productUnitPrice = selected.unit_price_quoted
+                    this.productUnitPriceDisplay = selected.unit_price_quoted_display
+                    this.productUnitRegularPriceDisplay = selected.unit_price_reg_display
+                    this.productCasePrice = selected.case_price_quoted
+                    this.productCasePriceDisplay = selected.case_price_quoted_display
+                    this.productSalePrice = selected.unit_price_sale
+                    this.productSalePriceDisplay = selected.unit_price_sale_display
+                    this.productSaleEndsDisplay = selected.sale_ends_display
+                    this.productSpecialOrder = selected.special_order
+
+                    this.productQuantity = 1
+                    this.productUnitChoices = selected?.order_uom_choices || this.defaultUnitChoices
+                    this.productUOM = selected?.order_uom || this.defaultUOM
+
+                    % if allow_item_discounts:
+                        this.updateDiscount(selected.department_id)
+                    % endif
+
+                    // nb. hack to force refresh for vue3
+                    this.refreshProductDescription += 1
+                    this.refreshTotalPrice += 1
+
+                    % if request.use_oruga:
+                        this.itemDialogTab = 'quantity'
+                    % else:
+                        this.itemDialogTabIndex = 1
+                    % endif
+                    this.editItemShowDialog = true
+                },
+
+            % endif
+
             itemDialogAttemptSave() {
                 this.itemDialogSaving = true
                 this.editItemLoading = true
 
                 const params = {
-                    order_qty: this.productQuantity,
+                    order_qty: parseFloat(this.productQuantity),
                     order_uom: this.productUOM,
                 }
 
@@ -1580,7 +1863,9 @@
                 }
 
                 % if allow_item_discounts:
-                    params.discount_percent = this.productDiscountPercent
+                    if (this.productDiscountPercent) {
+                        params.discount_percent = parseFloat(this.productDiscountPercent)
+                    }
                 % endif
 
                 if (this.editItemRow) {
diff --git a/src/sideshow/web/views/__init__.py b/src/sideshow/web/views/__init__.py
index 13a468c..e5a14ac 100644
--- a/src/sideshow/web/views/__init__.py
+++ b/src/sideshow/web/views/__init__.py
@@ -35,6 +35,7 @@ def includeme(config):
     })
 
     # sideshow views
+    config.include('sideshow.web.views.stores')
     config.include('sideshow.web.views.customers')
     config.include('sideshow.web.views.products')
     config.include('sideshow.web.views.orders')
diff --git a/src/sideshow/web/views/batch/neworder.py b/src/sideshow/web/views/batch/neworder.py
index fd7fbe3..5103517 100644
--- a/src/sideshow/web/views/batch/neworder.py
+++ b/src/sideshow/web/views/batch/neworder.py
@@ -126,6 +126,10 @@ class NewOrderBatchView(BatchMasterView):
         'status_code',
     ]
 
+    def __init__(self, request, context=None):
+        super().__init__(request, context=context)
+        self.order_handler = self.app.get_order_handler()
+
     def get_batch_handler(self):
         """ """
         # TODO: call self.app.get_batch_handler()
@@ -135,6 +139,10 @@ class NewOrderBatchView(BatchMasterView):
         """ """
         super().configure_grid(g)
 
+        # store_id
+        if not self.order_handler.expose_store_id():
+            g.remove('store_id')
+
         # total_price
         g.set_renderer('total_price', 'currency')
 
@@ -142,6 +150,10 @@ class NewOrderBatchView(BatchMasterView):
         """ """
         super().configure_form(f)
 
+        # store_id
+        if not self.order_handler.expose_store_id():
+            f.remove('store_id')
+
         # local_customer
         f.set_node('local_customer', LocalCustomerRef(self.request))
 
diff --git a/src/sideshow/web/views/orders.py b/src/sideshow/web/views/orders.py
index 9783de7..8aa7534 100644
--- a/src/sideshow/web/views/orders.py
+++ b/src/sideshow/web/views/orders.py
@@ -25,7 +25,9 @@ Views for Orders
 """
 
 import decimal
+import json
 import logging
+import re
 
 import colander
 import sqlalchemy as sa
@@ -35,9 +37,9 @@ from webhelpers2.html import tags, HTML
 
 from wuttaweb.views import MasterView
 from wuttaweb.forms.schema import UserRef, WuttaMoney, WuttaQuantity, WuttaEnum, WuttaDictEnum
+from wuttaweb.util import make_json_safe
 
 from sideshow.db.model import Order, OrderItem
-from sideshow.orders import OrderHandler
 from sideshow.batch.neworder import NewOrderBatchHandler
 from sideshow.web.forms.schema import (OrderRef,
                                        LocalCustomerRef, LocalProductRef,
@@ -65,13 +67,13 @@ class OrderView(MasterView):
     .. attribute:: order_handler
 
        Reference to the :term:`order handler` as returned by
-       :meth:`get_order_handler()`.  This gets set in the constructor.
+       :meth:`~sideshow.app.SideshowAppProvider.get_order_handler()`.
+       This gets set in the constructor.
 
     .. attribute:: batch_handler
 
-       Reference to the :term:`new order batch` handler, as returned
-       by :meth:`get_batch_handler()`.  This gets set in the
-       constructor.
+       Reference to the :term:`new order batch` handler.  This gets
+       set in the constructor.
     """
     model_class = Order
     editable = False
@@ -145,6 +147,7 @@ class OrderView(MasterView):
         'brand_name',
         'description',
         'size',
+        'department_id',
         'department_name',
         'vendor_name',
         'vendor_item_code',
@@ -155,41 +158,17 @@ class OrderView(MasterView):
 
     def __init__(self, request, context=None):
         super().__init__(request, context=context)
-        self.order_handler = self.get_order_handler()
-
-    def get_order_handler(self):
-        """
-        Returns the configured :term:`order handler`.
-
-        You normally would not need to call this, and can use
-        :attr:`order_handler` instead.
-
-        :rtype: :class:`~sideshow.orders.OrderHandler`
-        """
-        if hasattr(self, 'order_handler'):
-            return self.order_handler
-        return OrderHandler(self.config)
-
-    def get_batch_handler(self):
-        """
-        Returns the configured :term:`handler` for :term:`new order
-        batches <new order batch>`.
-
-        You normally would not need to call this, and can use
-        :attr:`batch_handler` instead.
-
-        :returns:
-           :class:`~sideshow.batch.neworder.NewOrderBatchHandler`
-           instance.
-        """
-        if hasattr(self, 'batch_handler'):
-            return self.batch_handler
-        return self.app.get_batch_handler('neworder')
+        self.order_handler = self.app.get_order_handler()
+        self.batch_handler = self.app.get_batch_handler('neworder')
 
     def configure_grid(self, g):
         """ """
         super().configure_grid(g)
 
+        # store_id
+        if not self.order_handler.expose_store_id():
+            g.remove('store_id')
+
         # order_id
         g.set_link('order_id')
 
@@ -223,6 +202,7 @@ class OrderView(MasterView):
 
         * :meth:`start_over()`
         * :meth:`cancel_order()`
+        * :meth:`set_store()`
         * :meth:`assign_customer()`
         * :meth:`unassign_customer()`
         * :meth:`set_pending_customer()`
@@ -232,10 +212,11 @@ class OrderView(MasterView):
         * :meth:`delete_item()`
         * :meth:`submit_order()`
         """
+        model = self.app.model
         enum = self.app.enum
-        self.creating = True
-        self.batch_handler = self.get_batch_handler()
+        session = self.Session()
         batch = self.get_current_batch()
+        self.creating = True
 
         context = self.get_context_customer(batch)
 
@@ -254,6 +235,7 @@ class OrderView(MasterView):
             data = dict(self.request.json_body)
             action = data.pop('action')
             json_actions = [
+                'set_store',
                 'assign_customer',
                 'unassign_customer',
                 # 'update_phone_number',
@@ -262,7 +244,7 @@ class OrderView(MasterView):
                 # 'get_customer_info',
                 # # 'set_customer_data',
                 'get_product_info',
-                # 'get_past_items',
+                'get_past_products',
                 'add_item',
                 'update_item',
                 'delete_item',
@@ -283,20 +265,36 @@ class OrderView(MasterView):
             'normalized_batch': self.normalize_batch(batch),
             'order_items': [self.normalize_row(row)
                             for row in batch.rows],
-            'default_uom_choices': self.get_default_uom_choices(),
+            'default_uom_choices': self.batch_handler.get_default_uom_choices(),
             'default_uom': None, # TODO?
+            'expose_store_id': self.order_handler.expose_store_id(),
             'allow_item_discounts': self.batch_handler.allow_item_discounts(),
             'allow_unknown_products': (self.batch_handler.allow_unknown_products()
                                        and self.has_perm('create_unknown_product')),
             'pending_product_required_fields': self.get_pending_product_required_fields(),
+            'allow_past_item_reorder': True, # TODO: make configurable?
         })
 
+        if context['expose_store_id']:
+            stores = session.query(model.Store)\
+                            .filter(model.Store.archived == False)\
+                            .order_by(model.Store.store_id)\
+                            .all()
+            context['stores'] = [{'store_id': store.store_id, 'display': store.get_display()}
+                                 for store in stores]
+
+            # set default so things just work
+            if not batch.store_id:
+                batch.store_id = self.batch_handler.get_default_store_id()
+
         if context['allow_item_discounts']:
             context['allow_item_discounts_if_on_sale'] = self.batch_handler\
                                                              .allow_item_discounts_if_on_sale()
             # nb. render quantity so that '10.0' => '10'
             context['default_item_discount'] = self.app.render_quantity(
                 self.batch_handler.get_default_item_discount())
+            context['dept_item_discounts'] = dict([(d['department_id'], d['default_item_discount'])
+                                                   for d in self.get_dept_item_discounts()])
 
         return self.render_to_response('create', context)
 
@@ -352,7 +350,7 @@ class OrderView(MasterView):
         if not term:
             return []
 
-        handler = self.get_batch_handler()
+        handler = self.batch_handler
         if handler.use_local_customers():
             return handler.autocomplete_customers_local(session, term, user=self.request.user)
         else:
@@ -376,7 +374,7 @@ class OrderView(MasterView):
         if not term:
             return []
 
-        handler = self.get_batch_handler()
+        handler = self.batch_handler
         if handler.use_local_products():
             return handler.autocomplete_products_local(session, term, user=self.request.user)
         else:
@@ -394,6 +392,43 @@ class OrderView(MasterView):
                 required.append(field)
         return required
 
+    def get_dept_item_discounts(self):
+        """
+        Returns the list of per-department default item discount settings.
+
+        Each entry in the list will look like::
+
+           {
+               'department_id': '42',
+               'department_name': 'Grocery',
+               'default_item_discount': 10,
+           }
+
+        :returns: List of department settings as shown above.
+        """
+        model = self.app.model
+        session = self.Session()
+        pattern = re.compile(r'^sideshow\.orders\.departments\.([^.]+)\.default_item_discount$')
+
+        dept_item_discounts = []
+        settings = session.query(model.Setting)\
+                          .filter(model.Setting.name.like('sideshow.orders.departments.%.default_item_discount'))\
+                          .all()
+        for setting in settings:
+            match = pattern.match(setting.name)
+            if not match:
+                log.warning("invalid setting name: %s", setting.name)
+                continue
+            deptid = match.group(1)
+            name = self.app.get_setting(session, f'sideshow.orders.departments.{deptid}.name')
+            dept_item_discounts.append({
+                'department_id': deptid,
+                'department_name': name,
+                'default_item_discount': setting.value,
+            })
+        dept_item_discounts.sort(key=lambda d: d['department_name'])
+        return dept_item_discounts
+
     def start_over(self, batch):
         """
         This will delete the user's current batch, then redirect user
@@ -436,9 +471,26 @@ class OrderView(MasterView):
         url = self.get_index_url()
         return self.redirect(url)
 
+    def set_store(self, batch, data):
+        """
+        Assign the
+        :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.store_id`
+        for a batch.
+
+        This is a "batch action" method which may be called from
+        :meth:`create()`.
+        """
+        store_id = data.get('store_id')
+        if not store_id:
+            return {'error': "Must provide store_id"}
+
+        batch.store_id = store_id
+        return self.get_context_customer(batch)
+
     def get_context_customer(self, batch):
         """ """
         context = {
+            'store_id': batch.store_id,
             'customer_is_known': True,
             'customer_id': None,
             'customer_name': batch.customer_name,
@@ -531,18 +583,20 @@ class OrderView(MasterView):
 
     def get_product_info(self, batch, data):
         """
-        Fetch data for a specific product.  (Nothing is modified.)
+        Fetch data for a specific product.
 
-        Depending on config, this will fetch a :term:`local product`
-        or :term:`external product` to get the data.
+        Depending on config, this calls one of the following to get
+        its primary data:
 
-        This should invoke a configured handler for the query
-        behavior, but that is not yet implemented.  For now it uses
-        built-in logic only, which queries the
-        :class:`~sideshow.db.model.products.LocalProduct` table.
+        * :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.get_product_info_local()`
+        * :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.get_product_info_external()`
+
+        It then may supplement the data with additional fields.
 
         This is a "batch action" method which may be called from
         :meth:`create()`.
+
+        :returns: Dict of product info.
         """
         product_id = data.get('product_id')
         if not product_id:
@@ -574,9 +628,6 @@ class OrderView(MasterView):
         if 'case_price_quoted' in data and 'case_price_quoted_display' not in data:
             data['case_price_quoted_display'] = self.app.render_currency(data['case_price_quoted'])
 
-        if 'default_item_discount' not in data:
-            data['default_item_discount'] = self.batch_handler.get_default_item_discount()
-
         decimal_fields = [
             'case_size',
             'unit_price_reg',
@@ -593,6 +644,22 @@ class OrderView(MasterView):
 
         return data
 
+    def get_past_products(self, batch, data):
+        """
+        Fetch past products for convenient re-ordering.
+
+        This essentially calls
+        :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.get_past_products()`
+        on the :attr:`batch_handler` and returns the result.
+
+        This is a "batch action" method which may be called from
+        :meth:`create()`.
+
+        :returns: List of product info dicts.
+        """
+        past_products = self.batch_handler.get_past_products(batch)
+        return make_json_safe(past_products)
+
     def add_item(self, batch, data):
         """
         This adds a row to the user's current new order batch.
@@ -709,12 +776,6 @@ class OrderView(MasterView):
             'status_text': batch.status_text,
         }
 
-    def get_default_uom_choices(self):
-        """ """
-        enum = self.app.enum
-        return [{'key': key, 'value': val}
-                for key, val in enum.ORDER_UOM.items()]
-
     def normalize_row(self, row):
         """ """
         data = {
@@ -729,12 +790,12 @@ class OrderView(MasterView):
                                                                 row.product_description,
                                                                 row.product_size),
             'product_weighed': row.product_weighed,
-            'department_display': row.department_name,
+            'department_id': row.department_id,
+            'department_name': row.department_name,
             'special_order': row.special_order,
             'case_size': float(row.case_size) if row.case_size is not None else None,
             'order_qty': float(row.order_qty),
             'order_uom': row.order_uom,
-            'order_uom_choices': self.get_default_uom_choices(),
             'discount_percent': self.app.render_quantity(row.discount_percent),
             'unit_price_quoted': float(row.unit_price_quoted) if row.unit_price_quoted is not None else None,
             'unit_price_quoted_display': self.app.render_currency(row.unit_price_quoted),
@@ -810,6 +871,10 @@ class OrderView(MasterView):
         super().configure_form(f)
         order = f.model_instance
 
+        # store_id
+        if not self.order_handler.expose_store_id():
+            f.remove('store_id')
+
         # local_customer
         if order.customer_id and not order.local_customer:
             f.remove('local_customer')
@@ -910,8 +975,10 @@ class OrderView(MasterView):
         """ """
         settings = [
 
-            # batches
-            {'name': 'wutta.batch.neworder.handler.spec'},
+            # stores
+            {'name': 'sideshow.orders.expose_store_id',
+             'type': bool},
+            {'name': 'sideshow.orders.default_store_id'},
 
             # customers
             {'name': 'sideshow.orders.use_local_customers',
@@ -920,12 +987,6 @@ class OrderView(MasterView):
              'default': 'true'},
 
             # products
-            {'name': 'sideshow.orders.allow_item_discounts',
-             'type': bool},
-            {'name': 'sideshow.orders.allow_item_discounts_if_on_sale',
-             'type': bool},
-            {'name': 'sideshow.orders.default_item_discount',
-             'type': float},
             {'name': 'sideshow.orders.use_local_products',
              # nb. this is really a bool but we present as string in config UI
              #'type': bool,
@@ -933,6 +994,17 @@ class OrderView(MasterView):
             {'name': 'sideshow.orders.allow_unknown_products',
              'type': bool,
              'default': True},
+
+            # pricing
+            {'name': 'sideshow.orders.allow_item_discounts',
+             'type': bool},
+            {'name': 'sideshow.orders.allow_item_discounts_if_on_sale',
+             'type': bool},
+            {'name': 'sideshow.orders.default_item_discount',
+             'type': float},
+
+            # batches
+            {'name': 'wutta.batch.neworder.handler.spec'},
         ]
 
         # required fields for new product entry
@@ -955,8 +1027,39 @@ class OrderView(MasterView):
         handlers = [{'spec': spec} for spec in handlers]
         context['batch_handlers'] = handlers
 
+        context['dept_item_discounts'] = self.get_dept_item_discounts()
+
         return context
 
+    def configure_gather_settings(self, data, simple_settings=None):
+        """ """
+        settings = super().configure_gather_settings(data, simple_settings=simple_settings)
+
+        for dept in json.loads(data['dept_item_discounts']):
+            deptid = dept['department_id']
+            settings.append({'name': f'sideshow.orders.departments.{deptid}.name',
+                             'value': dept['department_name']})
+            settings.append({'name': f'sideshow.orders.departments.{deptid}.default_item_discount',
+                             'value': dept['default_item_discount']})
+
+        return settings
+
+    def configure_remove_settings(self, **kwargs):
+        """ """
+        model = self.app.model
+        session = self.Session()
+
+        super().configure_remove_settings(**kwargs)
+
+        to_delete = session.query(model.Setting)\
+                           .filter(sa.or_(
+                               model.Setting.name.like('sideshow.orders.departments.%.name'),
+                               model.Setting.name.like('sideshow.orders.departments.%.default_item_discount')))\
+                           .all()
+        for setting in to_delete:
+            self.app.delete_setting(session, setting.name)
+
+
     @classmethod
     def defaults(cls, config):
         cls._order_defaults(config)
@@ -1037,6 +1140,7 @@ class OrderItemView(MasterView):
 
     labels = {
         'order_id': "Order ID",
+        'store_id': "Store ID",
         'product_id': "Product ID",
         'product_scancode': "Scancode",
         'product_brand': "Brand",
@@ -1050,6 +1154,7 @@ class OrderItemView(MasterView):
 
     grid_columns = [
         'order_id',
+        'store_id',
         'customer_name',
         # 'sequence',
         'product_scancode',
@@ -1099,20 +1204,7 @@ class OrderItemView(MasterView):
 
     def __init__(self, request, context=None):
         super().__init__(request, context=context)
-        self.order_handler = self.get_order_handler()
-
-    def get_order_handler(self):
-        """
-        Returns the configured :term:`order handler`.
-
-        You normally would not need to call this, and can use
-        :attr:`order_handler` instead.
-
-        :rtype: :class:`~sideshow.orders.OrderHandler`
-        """
-        if hasattr(self, 'order_handler'):
-            return self.order_handler
-        return OrderHandler(self.config)
+        self.order_handler = self.app.get_order_handler()
 
     def get_fallback_templates(self, template):
         """ """
@@ -1132,11 +1224,19 @@ class OrderItemView(MasterView):
         model = self.app.model
         # enum = self.app.enum
 
+        # store_id
+        if not self.order_handler.expose_store_id():
+            g.remove('store_id')
+
         # order_id
         g.set_sorter('order_id', model.Order.order_id)
-        g.set_renderer('order_id', self.render_order_id)
+        g.set_renderer('order_id', self.render_order_attr)
         g.set_link('order_id')
 
+        # store_id
+        g.set_sorter('store_id', model.Order.store_id)
+        g.set_renderer('store_id', self.render_order_attr)
+
         # customer_name
         g.set_label('customer_name', "Customer", column_only=True)
 
@@ -1165,9 +1265,10 @@ class OrderItemView(MasterView):
         # status_code
         g.set_renderer('status_code', self.render_status_code)
 
-    def render_order_id(self, item, key, value):
+    def render_order_attr(self, item, key, value):
         """ """
-        return item.order.order_id
+        order = item.order
+        return getattr(order, key)
 
     def render_status_code(self, item, key, value):
         """ """
@@ -1237,6 +1338,8 @@ class OrderItemView(MasterView):
             item = context['instance']
             form = context['form']
 
+            context['expose_store_id'] = self.order_handler.expose_store_id()
+
             context['item'] = item
             context['order'] = item.order
             context['order_qty_uom_text'] = self.order_handler.get_order_qty_uom_text(
diff --git a/src/sideshow/web/views/products.py b/src/sideshow/web/views/products.py
index 98341b7..010bb66 100644
--- a/src/sideshow/web/views/products.py
+++ b/src/sideshow/web/views/products.py
@@ -235,6 +235,7 @@ class PendingProductView(MasterView):
     url_prefix = '/pending/products'
 
     labels = {
+        'department_id': "Department ID",
         'product_id': "Product ID",
     }
 
diff --git a/src/sideshow/web/views/stores.py b/src/sideshow/web/views/stores.py
new file mode 100644
index 0000000..0eaf41d
--- /dev/null
+++ b/src/sideshow/web/views/stores.py
@@ -0,0 +1,120 @@
+# -*- 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/>.
+#
+################################################################################
+"""
+Views for Stores
+"""
+
+from wuttaweb.views import MasterView
+
+from sideshow.db.model import Store
+
+
+class StoreView(MasterView):
+    """
+    Master view for
+    :class:`~sideshow.db.model.stores.Store`; route prefix
+    is ``stores``.
+
+    Notable URLs provided by this class:
+
+    * ``/stores/``
+    * ``/stores/new``
+    * ``/stores/XXX``
+    * ``/stores/XXX/edit``
+    * ``/stores/XXX/delete``
+    """
+    model_class = Store
+
+    labels = {
+        'store_id': "Store ID",
+    }
+
+    filter_defaults = {
+        'archived': {'active': True, 'verb': 'is_false'},
+    }
+
+    sort_defaults = 'store_id'
+
+    def configure_grid(self, g):
+        """ """
+        super().configure_grid(g)
+
+        # links
+        g.set_link('store_id')
+        g.set_link('name')
+
+    def grid_row_class(self, store, data, i):
+        """ """
+        if store.archived:
+            return 'has-background-warning'
+
+    def configure_form(self, f):
+        """ """
+        super().configure_form(f)
+
+        # store_id
+        f.set_validator('store_id', self.unique_store_id)
+
+        # name
+        f.set_validator('name', self.unique_name)
+
+    def unique_store_id(self, node, value):
+        """ """
+        model = self.app.model
+        session = self.Session()
+
+        query = session.query(model.Store)\
+                       .filter(model.Store.store_id == value)
+
+        if self.editing:
+            uuid = self.request.matchdict['uuid']
+            query = query.filter(model.Store.uuid != uuid)
+
+        if query.count():
+            node.raise_invalid("Store ID must be unique")
+
+    def unique_name(self, node, value):
+        """ """
+        model = self.app.model
+        session = self.Session()
+
+        query = session.query(model.Store)\
+                       .filter(model.Store.name == value)
+
+        if self.editing:
+            uuid = self.request.matchdict['uuid']
+            query = query.filter(model.Store.uuid != uuid)
+
+        if query.count():
+            node.raise_invalid("Name must be unique")
+
+
+def defaults(config, **kwargs):
+    base = globals()
+
+    StoreView = kwargs.get('StoreView', base['StoreView'])
+    StoreView.defaults(config)
+
+
+def includeme(config):
+    defaults(config)
diff --git a/tests/batch/test_neworder.py b/tests/batch/test_neworder.py
index 56a3efd..ed2179a 100644
--- a/tests/batch/test_neworder.py
+++ b/tests/batch/test_neworder.py
@@ -4,6 +4,8 @@ import datetime
 import decimal
 from unittest.mock import patch
 
+import sqlalchemy as sa
+
 from wuttjamaican.testing import DataTestCase
 
 from sideshow.batch import neworder as mod
@@ -20,6 +22,16 @@ class TestNewOrderBatchHandler(DataTestCase):
     def make_handler(self):
         return mod.NewOrderBatchHandler(self.config)
 
+    def test_get_default_store_id(self):
+        handler = self.make_handler()
+
+        # null by default
+        self.assertIsNone(handler.get_default_store_id())
+
+        # whatever is configured
+        self.config.setdefault('sideshow.orders.default_store_id', '042')
+        self.assertEqual(handler.get_default_store_id(), '042')
+
     def test_use_local_customers(self):
         handler = self.make_handler()
 
@@ -108,6 +120,23 @@ class TestNewOrderBatchHandler(DataTestCase):
         # search for sally finds nothing
         self.assertEqual(handler.autocomplete_customers_local(self.session, 'sally'), [])
 
+    def test_init_batch(self):
+        model = self.app.model
+        handler = self.make_handler()
+
+        # store_id is null by default
+        batch = handler.model_class()
+        self.assertIsNone(batch.store_id)
+        handler.init_batch(batch)
+        self.assertIsNone(batch.store_id)
+
+        # but default can be configured
+        self.config.setdefault('sideshow.orders.default_store_id', '042')
+        batch = handler.model_class()
+        self.assertIsNone(batch.store_id)
+        handler.init_batch(batch)
+        self.assertEqual(batch.store_id, '042')
+
     def test_set_customer(self):
         model = self.app.model
         handler = self.make_handler()
@@ -240,6 +269,14 @@ class TestNewOrderBatchHandler(DataTestCase):
         # search for juice finds nothing
         self.assertEqual(handler.autocomplete_products_local(self.session, 'juice'), [])
 
+    def test_get_default_uom_choices(self):
+        enum = self.app.enum
+        handler = self.make_handler()
+
+        uoms = handler.get_default_uom_choices()
+        self.assertEqual(uoms, [{'key': key, 'value': val}
+                                for key, val in enum.ORDER_UOM.items()])
+
     def test_get_product_info_external(self):
         handler = self.make_handler()
         self.assertRaises(NotImplementedError, handler.get_product_info_external,
@@ -281,6 +318,174 @@ class TestNewOrderBatchHandler(DataTestCase):
         mock_uuid = self.app.make_true_uuid()
         self.assertRaises(ValueError, handler.get_product_info_local, self.session, mock_uuid.hex)
 
+    def test_normalize_local_product(self):
+        model = self.app.model
+        handler = self.make_handler()
+
+        product = model.LocalProduct(scancode='07430500132',
+                                     brand_name="Bragg's",
+                                     description="Apple Cider Vinegar",
+                                     size="32oz",
+                                     department_name="Grocery",
+                                     case_size=12,
+                                     unit_price_reg=5.99,
+                                     vendor_name="UNFI",
+                                     vendor_item_code='1234')
+        self.session.add(product)
+        self.session.flush()
+
+        info = handler.normalize_local_product(product)
+        self.assertIsInstance(info, dict)
+        self.assertEqual(info['product_id'], product.uuid.hex)
+        for prop in sa.inspect(model.LocalProduct).column_attrs:
+            if prop.key == 'uuid':
+                continue
+            if prop.key not in info:
+                continue
+            self.assertEqual(info[prop.key], getattr(product, prop.key))
+
+    def test_get_past_orders(self):
+        model = self.app.model
+        handler = self.make_handler()
+
+        user = model.User(username='barney')
+        self.session.add(user)
+        batch = handler.make_batch(self.session, created_by=user)
+        self.session.add(batch)
+        self.session.flush()
+
+        # ..will test local customers first
+
+        # error if no customer
+        self.assertRaises(ValueError, handler.get_past_orders, batch)
+
+        # empty history for customer
+        customer = model.LocalCustomer(full_name='Fred Flintstone')
+        batch.local_customer = customer
+        self.session.flush()
+        orders = handler.get_past_orders(batch)
+        self.assertEqual(len(orders), 0)
+
+        # mock historical order
+        order = model.Order(order_id=42, local_customer=customer, created_by=user)
+        self.session.add(order)
+        self.session.flush()
+
+        # that should now be returned
+        orders = handler.get_past_orders(batch)
+        self.assertEqual(len(orders), 1)
+        self.assertIs(orders[0], order)
+
+        # ..now we test external customers, w/ new batch
+        with patch.object(handler, 'use_local_customers', return_value=False):
+            batch2 = handler.make_batch(self.session, created_by=user)
+            self.session.add(batch2)
+            self.session.flush()
+
+            # error if no customer
+            self.assertRaises(ValueError, handler.get_past_orders, batch2)
+
+            # empty history for customer
+            batch2.customer_id = '123'
+            self.session.flush()
+            orders = handler.get_past_orders(batch2)
+            self.assertEqual(len(orders), 0)
+
+            # mock historical order
+            order2 = model.Order(order_id=42, customer_id='123', created_by=user)
+            self.session.add(order2)
+            self.session.flush()
+
+            # that should now be returned
+            orders = handler.get_past_orders(batch2)
+            self.assertEqual(len(orders), 1)
+            self.assertIs(orders[0], order2)
+
+    def test_get_past_products(self):
+        model = self.app.model
+        enum = self.app.enum
+        handler = self.make_handler()
+
+        user = model.User(username='barney')
+        self.session.add(user)
+        batch = handler.make_batch(self.session, created_by=user)
+        self.session.add(batch)
+        self.session.flush()
+
+        # (nb. this all assumes local customers)
+
+        # ..will test local products first
+
+        # error if no customer
+        self.assertRaises(ValueError, handler.get_past_products, batch)
+
+        # empty history for customer
+        customer = model.LocalCustomer(full_name='Fred Flintstone')
+        batch.local_customer = customer
+        self.session.flush()
+        products = handler.get_past_products(batch)
+        self.assertEqual(len(products), 0)
+
+        # mock historical order
+        order = model.Order(order_id=42, local_customer=customer, created_by=user)
+        product = model.LocalProduct(scancode='07430500132', description='Vinegar',
+                                     unit_price_reg=5.99, case_size=12)
+        item = model.OrderItem(local_product=product, order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
+                               status_code=enum.ORDER_ITEM_STATUS_READY)
+        order.items.append(item)
+        self.session.add(order)
+        self.session.flush()
+        self.session.refresh(product)
+
+        # that should now be returned
+        products = handler.get_past_products(batch)
+        self.assertEqual(len(products), 1)
+        self.assertEqual(products[0]['product_id'], product.uuid.hex)
+        self.assertEqual(products[0]['scancode'], '07430500132')
+        self.assertEqual(products[0]['description'], 'Vinegar')
+        self.assertEqual(products[0]['case_price_quoted'], decimal.Decimal('71.88'))
+        self.assertEqual(products[0]['case_price_quoted_display'], '$71.88')
+
+        # ..now we test external products, w/ new batch
+        with patch.object(handler, 'use_local_products', return_value=False):
+            batch2 = handler.make_batch(self.session, created_by=user)
+            self.session.add(batch2)
+            self.session.flush()
+
+            # error if no customer
+            self.assertRaises(ValueError, handler.get_past_products, batch2)
+
+            # empty history for customer
+            batch2.local_customer = customer
+            self.session.flush()
+            products = handler.get_past_products(batch2)
+            self.assertEqual(len(products), 0)
+
+            # mock historical order
+            order2 = model.Order(order_id=44, local_customer=customer, created_by=user)
+            self.session.add(order2)
+            item2 = model.OrderItem(product_id='07430500116',
+                                    order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
+                                    status_code=enum.ORDER_ITEM_STATUS_READY)
+            order2.items.append(item2)
+            self.session.flush()
+
+            # its product should now be returned
+            with patch.object(handler, 'get_product_info_external', return_value={
+                    'product_id': '07430500116',
+                    'scancode': '07430500116',
+                    'description': 'VINEGAR',
+                    'unit_price_reg': decimal.Decimal('3.99'),
+                    'case_size': 12,
+            }):
+                products = handler.get_past_products(batch2)
+            self.assertEqual(len(products), 1)
+            self.assertEqual(products[0]['product_id'], '07430500116')
+            self.assertEqual(products[0]['scancode'], '07430500116')
+            self.assertEqual(products[0]['description'], 'VINEGAR')
+            self.assertEqual(products[0]['case_price_quoted'], decimal.Decimal('47.88'))
+            self.assertEqual(products[0]['case_price_quoted_display'], '$47.88')
+
     def test_add_item(self):
         model = self.app.model
         enum = self.app.enum
diff --git a/tests/db/model/test_stores.py b/tests/db/model/test_stores.py
new file mode 100644
index 0000000..a44ebf7
--- /dev/null
+++ b/tests/db/model/test_stores.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8; -*-
+
+from wuttjamaican.testing import DataTestCase
+
+from sideshow.db.model import stores as mod
+
+
+class TestPendingCustomer(DataTestCase):
+
+    def test_str(self):
+        store = mod.Store()
+        self.assertEqual(str(store), "")
+
+        store.name = "Acme Goods"
+        self.assertEqual(str(store), "Acme Goods")
+
+        store.store_id = "001"
+        self.assertEqual(str(store), "001 Acme Goods")
+
+    def test_get_display(self):
+        store = mod.Store()
+        self.assertEqual(store.get_display(), "")
+
+        store.name = "Acme Goods"
+        self.assertEqual(store.get_display(), "Acme Goods")
+
+        store.store_id = "001"
+        self.assertEqual(store.get_display(), "001 Acme Goods")
diff --git a/tests/test_app.py b/tests/test_app.py
new file mode 100644
index 0000000..2d68e69
--- /dev/null
+++ b/tests/test_app.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8; -*-
+
+from wuttjamaican.testing import ConfigTestCase
+
+from sideshow import app as mod
+from sideshow.orders import OrderHandler
+
+
+class TestSideshowAppProvider(ConfigTestCase):
+
+    def make_provider(self):
+        return mod.SideshowAppProvider(self.config)
+
+    def test_get_order_handler(self):
+        provider = self.make_provider()
+        handler = provider.get_order_handler()
+        self.assertIsInstance(handler, OrderHandler)
diff --git a/tests/test_orders.py b/tests/test_orders.py
index 5937045..6e5609e 100644
--- a/tests/test_orders.py
+++ b/tests/test_orders.py
@@ -16,6 +16,16 @@ class TestOrderHandler(DataTestCase):
     def make_handler(self):
         return mod.OrderHandler(self.config)
 
+    def test_expose_store_id(self):
+        handler = self.make_handler()
+
+        # false by default
+        self.assertFalse(handler.expose_store_id())
+
+        # config can enable
+        self.config.setdefault('sideshow.orders.expose_store_id', 'true')
+        self.assertTrue(handler.expose_store_id())
+
     def test_get_order_qty_uom_text(self):
         enum = self.app.enum
         handler = self.make_handler()
diff --git a/tests/web/views/batch/test_neworder.py b/tests/web/views/batch/test_neworder.py
index fbf2335..b832986 100644
--- a/tests/web/views/batch/test_neworder.py
+++ b/tests/web/views/batch/test_neworder.py
@@ -30,10 +30,19 @@ class TestNewOrderBatchView(WebTestCase):
     def test_configure_grid(self):
         model = self.app.model
         view = self.make_view()
+
+        # store_id not exposed by default
         grid = view.make_grid(model_class=model.NewOrderBatch)
-        self.assertNotIn('total_price', grid.renderers)
+        self.assertIn('store_id', grid.columns)
         view.configure_grid(grid)
-        self.assertIn('total_price', grid.renderers)
+        self.assertNotIn('store_id', grid.columns)
+
+        # store_id is exposed if configured
+        self.config.setdefault('sideshow.orders.expose_store_id', 'true')
+        grid = view.make_grid(model_class=model.NewOrderBatch)
+        self.assertIn('store_id', grid.columns)
+        view.configure_grid(grid)
+        self.assertIn('store_id', grid.columns)
 
     def test_configure_form(self):
         model = self.app.model
@@ -58,6 +67,19 @@ class TestNewOrderBatchView(WebTestCase):
             self.assertIsInstance(schema['pending_customer'].typ, PendingCustomerRef)
             self.assertIsInstance(schema['total_price'].typ, WuttaMoney)
 
+            # store_id not exposed by default
+            form = view.make_form(model_instance=batch)
+            self.assertIn('store_id', form)
+            view.configure_form(form)
+            self.assertNotIn('store_id', form)
+
+            # store_id is exposed if configured
+            self.config.setdefault('sideshow.orders.expose_store_id', 'true')
+            form = view.make_form(model_instance=batch)
+            self.assertIn('store_id', form)
+            view.configure_form(form)
+            self.assertIn('store_id', form)
+
     def test_configure_row_grid(self):
         model = self.app.model
         view = self.make_view()
diff --git a/tests/web/views/test_orders.py b/tests/web/views/test_orders.py
index e4425ab..1af1a69 100644
--- a/tests/web/views/test_orders.py
+++ b/tests/web/views/test_orders.py
@@ -2,6 +2,7 @@
 
 import datetime
 import decimal
+import json
 from unittest.mock import patch
 
 from sqlalchemy import orm
@@ -15,6 +16,7 @@ from sideshow.orders import OrderHandler
 from sideshow.testing import WebTestCase
 from sideshow.web.views import orders as mod
 from sideshow.web.forms.schema import OrderRef, PendingProductRef
+from sideshow.config import SideshowConfig
 
 
 class TestIncludeme(WebTestCase):
@@ -25,33 +27,39 @@ class TestIncludeme(WebTestCase):
 
 class TestOrderView(WebTestCase):
 
+    def make_config(self, **kw):
+        config = super().make_config(**kw)
+        SideshowConfig().configure(config)
+        return config
+
     def make_view(self):
         return mod.OrderView(self.request)
 
     def make_handler(self):
         return NewOrderBatchHandler(self.config)
 
-    def test_order_handler(self):
-        view = self.make_view()
-        handler = view.order_handler
-        self.assertIsInstance(handler, OrderHandler)
-        handler2 = view.get_order_handler()
-        self.assertIs(handler2, handler)
-
     def test_configure_grid(self):
         model = self.app.model
         view = self.make_view()
-        grid = view.make_grid(model_class=model.PendingProduct)
-        self.assertNotIn('order_id', grid.linked_columns)
-        self.assertNotIn('total_price', grid.renderers)
+
+        # store_id hidden by default
+        grid = view.make_grid(model_class=model.Order, columns=['store_id', 'order_id'])
+        self.assertIn('store_id', grid.columns)
         view.configure_grid(grid)
-        self.assertIn('order_id', grid.linked_columns)
-        self.assertIn('total_price', grid.renderers)
+        self.assertNotIn('store_id', grid.columns)
+
+        # store_id is shown if configured
+        self.config.setdefault('sideshow.orders.expose_store_id', 'true')
+        grid = view.make_grid(model_class=model.Order, columns=['store_id', 'order_id'])
+        self.assertIn('store_id', grid.columns)
+        view.configure_grid(grid)
+        self.assertIn('store_id', grid.columns)
 
     def test_create(self):
         self.pyramid_config.include('sideshow.web.views')
         self.config.setdefault('wutta.batch.neworder.handler.spec',
                                'sideshow.batch.neworder:NewOrderBatchHandler')
+        self.config.setdefault('sideshow.orders.expose_store_id', 'true')
         self.config.setdefault('sideshow.orders.allow_item_discounts', 'true')
         model = self.app.model
         enum = self.app.enum
@@ -59,6 +67,10 @@ class TestOrderView(WebTestCase):
 
         user = model.User(username='barney')
         self.session.add(user)
+        store = model.Store(store_id='001', name='Acme Goods')
+        self.session.add(store)
+        store = model.Store(store_id='002', name='Acme Services')
+        self.session.add(store)
         self.session.flush()
 
         with patch.object(view, 'Session', return_value=self.session):
@@ -101,6 +113,7 @@ class TestOrderView(WebTestCase):
                         self.assertIsInstance(response, Response)
                         self.assertEqual(response.content_type, 'application/json')
                         self.assertEqual(response.json_body, {
+                            'store_id': None,
                             'customer_is_known': False,
                             'customer_id': None,
                             'customer_name': 'Fred Flintstone',
@@ -285,6 +298,50 @@ class TestOrderView(WebTestCase):
         fields = view.get_pending_product_required_fields()
         self.assertEqual(fields, ['brand_name', 'size', 'unit_price_reg'])
 
+    def test_get_dept_item_discounts(self):
+        model = self.app.model
+        view = self.make_view()
+
+        with patch.object(view, 'Session', return_value=self.session):
+
+            # empty list by default
+            discounts = view.get_dept_item_discounts()
+            self.assertEqual(discounts, [])
+
+            # mock settings
+            self.app.save_setting(self.session, 'sideshow.orders.departments.5.name', 'Bulk')
+            self.app.save_setting(self.session, 'sideshow.orders.departments.5.default_item_discount', '15')
+            self.app.save_setting(self.session, 'sideshow.orders.departments.6.name', 'Produce')
+            self.app.save_setting(self.session, 'sideshow.orders.departments.6.default_item_discount', '5')
+            discounts = view.get_dept_item_discounts()
+            self.assertEqual(len(discounts), 2)
+            self.assertEqual(discounts[0], {
+                'department_id': '5',
+                'department_name': 'Bulk',
+                'default_item_discount': '15',
+            })
+            self.assertEqual(discounts[1], {
+                'department_id': '6',
+                'department_name': 'Produce',
+                'default_item_discount': '5',
+            })
+
+            # invalid setting
+            self.app.save_setting(self.session, 'sideshow.orders.departments.I.N.V.A.L.I.D.name', 'Bad News')
+            self.app.save_setting(self.session, 'sideshow.orders.departments.I.N.V.A.L.I.D.default_item_discount', '42')
+            discounts = view.get_dept_item_discounts()
+            self.assertEqual(len(discounts), 2)
+            self.assertEqual(discounts[0], {
+                'department_id': '5',
+                'department_name': 'Bulk',
+                'default_item_discount': '15',
+            })
+            self.assertEqual(discounts[1], {
+                'department_id': '6',
+                'department_name': 'Produce',
+                'default_item_discount': '5',
+            })
+
     def test_get_context_customer(self):
         self.pyramid_config.add_route('orders', '/orders/')
         model = self.app.model
@@ -304,6 +361,7 @@ class TestOrderView(WebTestCase):
             self.session.flush()
             context = view.get_context_customer(batch)
             self.assertEqual(context, {
+                'store_id': None,
                 'customer_is_known': True,
                 'customer_id': 42,
                 'customer_name': 'Fred Flintstone',
@@ -321,6 +379,7 @@ class TestOrderView(WebTestCase):
         self.session.flush()
         context = view.get_context_customer(batch)
         self.assertEqual(context, {
+            'store_id': None,
             'customer_is_known': True,
             'customer_id': local.uuid.hex,
             'customer_name': 'Betty Boop',
@@ -339,6 +398,7 @@ class TestOrderView(WebTestCase):
         self.session.flush()
         context = view.get_context_customer(batch)
         self.assertEqual(context, {
+            'store_id': None,
             'customer_is_known': False,
             'customer_id': None,
             'customer_name': 'Fred Flintstone',
@@ -357,6 +417,7 @@ class TestOrderView(WebTestCase):
         self.session.flush()
         context = view.get_context_customer(batch)
         self.assertEqual(context, {
+            'store_id': None,
             'customer_is_known': True, # nb. this is for UI default
             'customer_id': None,
             'customer_name': None,
@@ -408,6 +469,34 @@ class TestOrderView(WebTestCase):
                     self.session.flush()
                     self.assertEqual(self.session.query(model.NewOrderBatch).count(), 0)
 
+    def test_set_store(self):
+        model = self.app.model
+        view = self.make_view()
+        handler = NewOrderBatchHandler(self.config)
+
+        user = model.User(username='barney')
+        self.session.add(user)
+        self.session.flush()
+
+        with patch.object(view, 'batch_handler', create=True, new=handler):
+            with patch.object(view, 'Session', return_value=self.session):
+                with patch.object(self.request, 'user', new=user):
+
+                    batch = view.get_current_batch()
+                    self.assertIsNone(batch.store_id)
+
+                    # store_id is required
+                    result = view.set_store(batch, {})
+                    self.assertEqual(result, {'error': "Must provide store_id"})
+                    result = view.set_store(batch, {'store_id': ''})
+                    self.assertEqual(result, {'error': "Must provide store_id"})
+
+                    # store_id is set on batch
+                    result = view.set_store(batch, {'store_id': '042'})
+                    self.assertEqual(batch.store_id, '042')
+                    self.assertIn('store_id', result)
+                    self.assertEqual(result['store_id'], '042')
+
     def test_assign_customer(self):
         self.pyramid_config.add_route('orders.create', '/orders/new')
         model = self.app.model
@@ -432,6 +521,7 @@ class TestOrderView(WebTestCase):
                     self.assertIsNone(batch.pending_customer)
                     self.assertIs(batch.local_customer, weirdal)
                     self.assertEqual(context, {
+                        'store_id': None,
                         'customer_is_known': True,
                         'customer_id': weirdal.uuid.hex,
                         'customer_name': 'Weird Al',
@@ -470,6 +560,7 @@ class TestOrderView(WebTestCase):
                     self.assertIsNone(batch.customer_name)
                     self.assertIsNone(batch.local_customer)
                     self.assertEqual(context, {
+                        'store_id': None,
                         'customer_is_known': True,
                         'customer_id': None,
                         'customer_name': None,
@@ -510,6 +601,7 @@ class TestOrderView(WebTestCase):
                     context = view.set_pending_customer(batch, data)
                     self.assertIsInstance(batch.pending_customer, model.PendingCustomer)
                     self.assertEqual(context, {
+                        'store_id': None,
                         'customer_is_known': False,
                         'customer_id': None,
                         'customer_name': 'Fred Flintstone',
@@ -575,6 +667,51 @@ class TestOrderView(WebTestCase):
                             context = view.get_product_info(batch, {'product_id': '42'})
                             self.assertEqual(context, {'error': "something smells fishy"})
 
+    def test_get_past_products(self):
+        model = self.app.model
+        enum = self.app.enum
+        view = self.make_view()
+        handler = view.batch_handler
+
+        user = model.User(username='barney')
+        self.session.add(user)
+        batch = handler.make_batch(self.session, created_by=user)
+        self.session.add(batch)
+        self.session.flush()
+
+        # (nb. this all assumes local customers and products)
+
+        # error if no customer
+        self.assertRaises(ValueError, view.get_past_products, batch, {})
+
+        # empty history for customer
+        customer = model.LocalCustomer(full_name='Fred Flintstone')
+        batch.local_customer = customer
+        self.session.flush()
+        products = view.get_past_products(batch, {})
+        self.assertEqual(len(products), 0)
+
+        # mock historical order
+        order = model.Order(order_id=42, local_customer=customer, created_by=user)
+        product = model.LocalProduct(scancode='07430500132', description='Vinegar',
+                                     unit_price_reg=5.99, case_size=12)
+        item = model.OrderItem(local_product=product, order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
+                               status_code=enum.ORDER_ITEM_STATUS_READY)
+        order.items.append(item)
+        self.session.add(order)
+        self.session.flush()
+        self.session.refresh(product)
+
+        # that should now be returned
+        products = view.get_past_products(batch, {})
+        self.assertEqual(len(products), 1)
+        self.assertEqual(products[0]['product_id'], product.uuid.hex)
+        self.assertEqual(products[0]['scancode'], '07430500132')
+        self.assertEqual(products[0]['description'], 'Vinegar')
+        # nb. this is a float, since result is JSON-safe
+        self.assertEqual(products[0]['case_price_quoted'], 71.88)
+        self.assertEqual(products[0]['case_price_quoted_display'], '$71.88')
+
     def test_add_item(self):
         model = self.app.model
         enum = self.app.enum
@@ -825,14 +962,6 @@ class TestOrderView(WebTestCase):
                         'error': f"ValueError: batch has already been executed: {batch}",
                     })
 
-    def test_get_default_uom_choices(self):
-        enum = self.app.enum
-        view = self.make_view()
-
-        uoms = view.get_default_uom_choices()
-        self.assertEqual(uoms, [{'key': key, 'value': val}
-                                for key, val in enum.ORDER_UOM.items()])
-
     def test_normalize_batch(self):
         model = self.app.model
         enum = self.app.enum
@@ -1078,7 +1207,11 @@ class TestOrderView(WebTestCase):
             form = view.make_form(model_instance=order)
             # nb. this is to avoid include/exclude ambiguity
             form.remove('items')
+            # nb. store_id gets hidden by default
+            form.append('store_id')
+            self.assertIn('store_id', form)
             view.configure_form(form)
+            self.assertNotIn('store_id', form)
             schema = form.get_schema()
             self.assertIn('pending_customer', form)
             self.assertIsInstance(schema['total_price'].typ, WuttaMoney)
@@ -1089,13 +1222,20 @@ class TestOrderView(WebTestCase):
         self.session.add(local)
         self.session.flush()
 
+        # nb. from now on we include store_id
+        self.config.setdefault('sideshow.orders.expose_store_id', 'true')
+
         # viewing (local customer)
         with patch.object(view, 'viewing', new=True):
             with patch.object(order, 'local_customer', new=local):
                 form = view.make_form(model_instance=order)
                 # nb. this is to avoid include/exclude ambiguity
                 form.remove('items')
+                # nb. store_id will now remain
+                form.append('store_id')
+                self.assertIn('store_id', form)
                 view.configure_form(form)
+                self.assertIn('store_id', form)
                 self.assertNotIn('pending_customer', form)
                 schema = form.get_schema()
                 self.assertIsInstance(schema['total_price'].typ, WuttaMoney)
@@ -1236,6 +1376,12 @@ class TestOrderView(WebTestCase):
         model = self.app.model
         view = self.make_view()
 
+        self.app.save_setting(self.session, 'sideshow.orders.departments.5.name', 'Bulk')
+        self.app.save_setting(self.session, 'sideshow.orders.departments.5.default_item_discount', '15')
+        self.app.save_setting(self.session, 'sideshow.orders.departments.6.name', 'Produce')
+        self.app.save_setting(self.session, 'sideshow.orders.departments.6.default_item_discount', '5')
+        self.session.commit()
+
         with patch.object(view, 'Session', return_value=self.session):
             with patch.multiple(self.config, usedb=True, preferdb=True):
 
@@ -1243,7 +1389,19 @@ class TestOrderView(WebTestCase):
                 allowed = self.config.get_bool('sideshow.orders.allow_unknown_products',
                                                session=self.session)
                 self.assertIsNone(allowed)
-                self.assertEqual(self.session.query(model.Setting).count(), 0)
+                self.assertEqual(self.session.query(model.Setting).count(), 4)
+                discounts = view.get_dept_item_discounts()
+                self.assertEqual(len(discounts), 2)
+                self.assertEqual(discounts[0], {
+                    'department_id': '5',
+                    'department_name': 'Bulk',
+                    'default_item_discount': '15',
+                })
+                self.assertEqual(discounts[1], {
+                    'department_id': '6',
+                    'department_name': 'Produce',
+                    'default_item_discount': '5',
+                })
 
                 # fetch initial page
                 response = view.configure()
@@ -1253,13 +1411,18 @@ class TestOrderView(WebTestCase):
                 allowed = self.config.get_bool('sideshow.orders.allow_unknown_products',
                                                session=self.session)
                 self.assertIsNone(allowed)
-                self.assertEqual(self.session.query(model.Setting).count(), 0)
+                self.assertEqual(self.session.query(model.Setting).count(), 4)
 
                 # post new settings
                 with patch.multiple(self.request, create=True,
                                     method='POST',
                                     POST={
                                         'sideshow.orders.allow_unknown_products': 'true',
+                                        'dept_item_discounts': json.dumps([{
+                                            'department_id': '5',
+                                            'department_name': 'Grocery',
+                                            'default_item_discount': 10,
+                                        }])
                                     }):
                     response = view.configure()
                 self.assertIsInstance(response, HTTPFound)
@@ -1268,17 +1431,17 @@ class TestOrderView(WebTestCase):
                                                session=self.session)
                 self.assertTrue(allowed)
                 self.assertTrue(self.session.query(model.Setting).count() > 1)
+                discounts = view.get_dept_item_discounts()
+                self.assertEqual(len(discounts), 1)
+                self.assertEqual(discounts[0], {
+                    'department_id': '5',
+                    'department_name': 'Grocery',
+                    'default_item_discount': '10',
+                })
 
 
 class OrderItemViewTestMixin:
 
-    def test_common_order_handler(self):
-        view = self.make_view()
-        handler = view.order_handler
-        self.assertIsInstance(handler, OrderHandler)
-        handler2 = view.get_order_handler()
-        self.assertIs(handler2, handler)
-
     def test_common_get_fallback_templates(self):
         view = self.make_view()
 
@@ -1294,18 +1457,29 @@ class OrderItemViewTestMixin:
     def test_common_configure_grid(self):
         model = self.app.model
         view = self.make_view()
-        grid = view.make_grid(model_class=model.OrderItem)
-        self.assertNotIn('order_id', grid.linked_columns)
-        view.configure_grid(grid)
-        self.assertIn('order_id', grid.linked_columns)
 
-    def test_common_render_order_id(self):
+        # store_id is removed by default
+        grid = view.make_grid(model_class=model.OrderItem)
+        grid.append('store_id')
+        self.assertIn('store_id', grid.columns)
+        view.configure_grid(grid)
+        self.assertNotIn('store_id', grid.columns)
+
+        # store_id is shown if configured
+        self.config.setdefault('sideshow.orders.expose_store_id', 'true')
+        grid = view.make_grid(model_class=model.OrderItem)
+        grid.append('store_id')
+        self.assertIn('store_id', grid.columns)
+        view.configure_grid(grid)
+        self.assertIn('store_id', grid.columns)
+
+    def test_common_render_order_attr(self):
         model = self.app.model
         view = self.make_view()
         order = model.Order(order_id=42)
         item = model.OrderItem()
         order.items.append(item)
-        self.assertEqual(view.render_order_id(item, None, None), 42)
+        self.assertEqual(view.render_order_attr(item, 'order_id', None), 42)
 
     def test_common_render_status_code(self):
         enum = self.app.enum
diff --git a/tests/web/views/test_stores.py b/tests/web/views/test_stores.py
new file mode 100644
index 0000000..ab69171
--- /dev/null
+++ b/tests/web/views/test_stores.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8; -*-
+
+from unittest.mock import patch
+
+import colander
+
+from sideshow.testing import WebTestCase
+from sideshow.web.views import stores as mod
+
+
+class TestIncludeme(WebTestCase):
+
+    def test_coverage(self):
+        mod.includeme(self.pyramid_config)
+
+
+class TestStoreView(WebTestCase):
+
+    def make_view(self):
+        return mod.StoreView(self.request)
+
+    def test_configure_grid(self):
+        model = self.app.model
+        view = self.make_view()
+        grid = view.make_grid(model_class=model.Store)
+        self.assertNotIn('store_id', grid.linked_columns)
+        self.assertNotIn('name', grid.linked_columns)
+        view.configure_grid(grid)
+        self.assertIn('store_id', grid.linked_columns)
+        self.assertIn('name', grid.linked_columns)
+
+    def test_grid_row_class(self):
+        model = self.app.model
+        view = self.make_view()
+
+        store = model.Store()
+        self.assertFalse(store.archived)
+        self.assertIsNone(view.grid_row_class(store, {}, 0))
+
+        store = model.Store(archived=True)
+        self.assertTrue(store.archived)
+        self.assertEqual(view.grid_row_class(store, {}, 0), 'has-background-warning')
+
+    def test_configure_form(self):
+        model = self.app.model
+        view = self.make_view()
+
+        # unique validators are set
+        form = view.make_form(model_class=model.Store)
+        self.assertNotIn('store_id', form.validators)
+        self.assertNotIn('name', form.validators)
+        view.configure_form(form)
+        self.assertIn('store_id', form.validators)
+        self.assertIn('name', form.validators)
+
+    def test_unique_store_id(self):
+        model = self.app.model
+        view = self.make_view()
+
+        store = model.Store(store_id='001', name='whatever')
+        self.session.add(store)
+        self.session.commit()
+
+        with patch.object(view, 'Session', return_value=self.session):
+
+            # invalid if same store_id in data
+            node = colander.SchemaNode(colander.String(), name='store_id')
+            self.assertRaises(colander.Invalid, view.unique_store_id, node, '001')
+
+            # but not if store_id belongs to current store
+            with patch.object(self.request, 'matchdict', new={'uuid': store.uuid}):
+                with patch.object(view, 'editing', new=True):
+                    node = colander.SchemaNode(colander.String(), name='store_id')
+                    self.assertIsNone(view.unique_store_id(node, '001'))
+
+    def test_unique_name(self):
+        model = self.app.model
+        view = self.make_view()
+
+        store = model.Store(store_id='001', name='Acme Goods')
+        self.session.add(store)
+        self.session.commit()
+
+        with patch.object(view, 'Session', return_value=self.session):
+
+            # invalid if same name in data
+            node = colander.SchemaNode(colander.String(), name='name')
+            self.assertRaises(colander.Invalid, view.unique_name, node, 'Acme Goods')
+
+            # but not if name belongs to current store
+            with patch.object(self.request, 'matchdict', new={'uuid': store.uuid}):
+                with patch.object(view, 'editing', new=True):
+                    node = colander.SchemaNode(colander.String(), name='name')
+                    self.assertIsNone(view.unique_name(node, 'Acme Goods'))