diff --git a/docs/glossary.rst b/docs/glossary.rst
index f4cf836..d639262 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -5,46 +5,6 @@ Glossary
 .. glossary::
    :sorted:
 
-   external customer
-     A customer account from an external system.  Sideshow can be
-     configured to lookup customer data from external system(s) when
-     creating an :term:`order`.
-
-     See also :term:`local customer` and :term:`pending customer`.
-
-   external product
-     A product record from an external system.  Sideshow can be
-     configured to lookup customer data from external system(s) when
-     creating an :term:`order`.
-
-     See also :term:`local product` and :term:`pending product`.
-
-   local customer
-     A customer account in the :term:`app database`.  By default,
-     Sideshow will use its native "Local Customers" table for lookup
-     when creating an :term:`order`.
-
-     The data model for this is
-     :class:`~sideshow.db.model.customers.LocalCustomer`.
-
-     See also :term:`external customer` and :term:`pending customer`.
-
-   local product
-     A product record in the :term:`app database`.  By default,
-     Sideshow will use its native "Local Products" table for lookup
-     when creating an :term:`order`.
-
-     The data model for this is
-     :class:`~sideshow.db.model.products.LocalProduct`.
-
-     See also :term:`external product` and :term:`pending product`.
-
-   new order batch
-     When user is creating a new order, under the hood a :term:`batch`
-     is employed to keep track of user input.  When user ultimately
-     "submits" the order, the batch is executed which creates a true
-     :term:`order`.
-
    order
      This is the central focus of the app; it refers to a customer
      case/special order which is tracked over time, from placement to
@@ -60,19 +20,17 @@ Glossary
      sibling items.
 
    pending customer
-     A "temporary" customer record used when creating an :term:`order`
-     for new/unknown customer.
+     Generally refers to a "new / unknown" customer, e.g. for whom a
+     new order is being created.  This allows the order lifecycle to
+     get going before the customer has a proper account in the system.
 
-     The data model for this is
-     :class:`~sideshow.db.model.customers.PendingCustomer`.
-
-     See also :term:`external customer` and :term:`pending customer`.
+     See :class:`~sideshow.db.model.customers.PendingCustomer` for the
+     data model.
 
    pending product
-     A "temporary" product record used when creating an :term:`order`
-     for new/unknown product.
+     Generally refers to a "new / unknown" product, e.g. for which a
+     new order is being created.  This allows the order lifecycle to
+     get going before the product has a true record in the system.
 
-     The data model for this is
-     :class:`~sideshow.db.model.products.PendingProduct`.
-
-     See also :term:`external product` and :term:`pending product`.
+     See :class:`~sideshow.db.model.products.PendingProduct` for the
+     data model.
diff --git a/src/sideshow/batch/neworder.py b/src/sideshow/batch/neworder.py
index 353af9c..5ec9b8d 100644
--- a/src/sideshow/batch/neworder.py
+++ b/src/sideshow/batch/neworder.py
@@ -27,8 +27,6 @@ New Order Batch Handler
 import datetime
 import decimal
 
-import sqlalchemy as sa
-
 from wuttjamaican.batch import BatchHandler
 
 from sideshow.db.model import NewOrderBatch
@@ -36,8 +34,7 @@ from sideshow.db.model import NewOrderBatch
 
 class NewOrderBatchHandler(BatchHandler):
     """
-    The :term:`batch handler` for :term:`new order batches <new order
-    batch>`.
+    The :term:`batch handler` for New Order Batches.
 
     This is responsible for business logic around the creation of new
     :term:`orders <order>`.  A
@@ -47,333 +44,188 @@ class NewOrderBatchHandler(BatchHandler):
     """
     model_class = NewOrderBatch
 
-    def use_local_customers(self):
+    def set_pending_customer(self, batch, data):
         """
-        Returns boolean indicating whether :term:`local customer`
-        accounts should be used.  This is true by default, but may be
-        false for :term:`external customer` lookups.
-        """
-        return self.config.get_bool('sideshow.orders.use_local_customers',
-                                    default=True)
+        Set (add or update) pending customer info for the batch.
 
-    def use_local_products(self):
-        """
-        Returns boolean indicating whether :term:`local product`
-        records should be used.  This is true by default, but may be
-        false for :term:`external product` lookups.
-        """
-        return self.config.get_bool('sideshow.orders.use_local_products',
-                                    default=True)
-
-    def allow_unknown_products(self):
-        """
-        Returns boolean indicating whether :term:`pending products
-        <pending product>` are allowed when creating an order.
-
-        This is true by default, so user can enter new/unknown product
-        when creating an order.  This can be disabled, to force user
-        to choose existing local/external product.
-        """
-        return self.config.get_bool('sideshow.orders.allow_unknown_products',
-                                    default=True)
-
-    def set_customer(self, batch, customer_info, user=None):
-        """
-        Set/update customer info for the batch.
-
-        This will first set one of the following:
-
-        * :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.customer_id`
-        * :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.local_customer`
-        * :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`
-
-        Note that a new
-        :class:`~sideshow.db.model.customers.PendingCustomer` record
-        is created if necessary.
-
-        And then it will update these accordingly:
-
-        * :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.customer_name`
-        * :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.phone_number`
-        * :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.email_address`
-
-        Note that ``customer_info`` may be ``None``, which will cause
-        all the above to be set to ``None`` also.
+        This will clear the
+        :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.customer_id`
+        and set the
+        :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`,
+        creating a new record if needed.  It then updates the pending
+        customer record per the given ``data``.
 
         :param batch:
-           :class:`~sideshow.db.model.batch.neworder.NewOrderBatch` to
-           update.
+           :class:`~sideshow.db.model.batch.neworder.NewOrderBatch`
+           to be updated.
 
-        :param customer_info: Customer ID string, or dict of
-           :class:`~sideshow.db.model.customers.PendingCustomer` data,
-           or ``None`` to clear the customer info.
-
-        :param user:
-           :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
-           is performing the action.  This is used to set
-           :attr:`~sideshow.db.model.customers.PendingCustomer.created_by`
-           on the pending customer, if applicable.  If not specified,
-           the batch creator is assumed.
+        :param data: Dict of field data for the
+           :class:`~sideshow.db.model.customers.PendingCustomer`
+           record.
         """
         model = self.app.model
         enum = self.app.enum
-        session = self.app.get_session(batch)
-        use_local = self.use_local_customers()
 
-        # set customer info
-        if isinstance(customer_info, str):
-            if use_local:
+        # remove customer account if set
+        batch.customer_id = None
 
-                # local_customer
-                customer = session.get(model.LocalCustomer, customer_info)
-                if not customer:
-                    raise ValueError("local customer not found")
-                batch.local_customer = customer
-                batch.customer_name = customer.full_name
-                batch.phone_number = customer.phone_number
-                batch.email_address = customer.email_address
+        # create pending customer if needed
+        pending = batch.pending_customer
+        if not pending:
+            kw = dict(data)
+            kw.setdefault('status', enum.PendingCustomerStatus.PENDING)
+            pending = model.PendingCustomer(**kw)
+            batch.pending_customer = pending
 
-            else: # external customer_id
-                #batch.customer_id = customer_info
-                raise NotImplementedError
+        # update pending customer
+        if 'first_name' in data:
+            pending.first_name = data['first_name']
+        if 'last_name' in data:
+            pending.last_name = data['last_name']
+        if 'full_name' in data:
+            pending.full_name = data['full_name']
+        elif 'first_name' in data or 'last_name' in data:
+            pending.full_name = self.app.make_full_name(data.get('first_name'),
+                                                        data.get('last_name'))
+        if 'phone_number' in data:
+            pending.phone_number = data['phone_number']
+        if 'email_address' in data:
+            pending.email_address = data['email_address']
 
-        elif customer_info:
+        # update batch per pending customer
+        batch.customer_name = pending.full_name
+        batch.phone_number = pending.phone_number
+        batch.email_address = pending.email_address
 
-            # pending_customer
-            batch.customer_id = None
-            batch.local_customer = None
-            customer = batch.pending_customer
-            if not customer:
-                customer = model.PendingCustomer(status=enum.PendingCustomerStatus.PENDING,
-                                                 created_by=user or batch.created_by)
-                session.add(customer)
-                batch.pending_customer = customer
-            fields = [
-                'full_name',
-                'first_name',
-                'last_name',
-                'phone_number',
-                'email_address',
-            ]
-            for key in fields:
-                setattr(customer, key, customer_info.get(key))
-            if 'full_name' not in customer_info:
-                customer.full_name = self.app.make_full_name(customer.first_name,
-                                                             customer.last_name)
-            batch.customer_name = customer.full_name
-            batch.phone_number = customer.phone_number
-            batch.email_address = customer.email_address
-
-        else:
-
-            # null
-            batch.customer_id = None
-            batch.local_customer = None
-            batch.customer_name = None
-            batch.phone_number = None
-            batch.email_address = None
-
-        session.flush()
-
-    def add_item(self, batch, product_info, order_qty, order_uom, user=None):
+    def add_pending_product(self, batch, pending_info,
+                            order_qty, order_uom):
         """
-        Add a new item/row to the batch, for given product and quantity.
+        Add a new row to the batch, for the given "pending" product
+        and order quantity.
 
-        See also :meth:`update_item()`.
+        See also :meth:`set_pending_product()` to update an existing row.
 
         :param batch:
            :class:`~sideshow.db.model.batch.neworder.NewOrderBatch` to
-           update.
+           which the row should be added.
 
-        :param product_info: Product ID string, or dict of
-           :class:`~sideshow.db.model.products.PendingProduct` data.
+        :param pending_info: Dict of kwargs to use when constructing a
+           new :class:`~sideshow.db.model.products.PendingProduct`.
 
-        :param order_qty:
-           :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_qty`
-           value for the new row.
+        :param order_qty: Quantity of the product to be added to the
+           order.
 
-        :param order_uom:
-           :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_uom`
-           value for the new row.
-
-        :param user:
-           :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
-           is performing the action.  This is used to set
-           :attr:`~sideshow.db.model.products.PendingProduct.created_by`
-           on the pending product, if applicable.  If not specified,
-           the batch creator is assumed.
+        :param order_uom: UOM for the order quantity; must be a code
+           from :data:`~sideshow.enum.ORDER_UOM`.
 
         :returns:
            :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
-           instance.
+           which was added to the batch.
         """
         model = self.app.model
         enum = self.app.enum
         session = self.app.get_session(batch)
-        use_local = self.use_local_products()
-        row = self.make_row()
 
-        # set product info
-        if isinstance(product_info, str):
-            if use_local:
+        # make new pending product
+        kw = dict(pending_info)
+        kw.setdefault('status', enum.PendingProductStatus.PENDING)
+        product = model.PendingProduct(**kw)
+        session.add(product)
+        session.flush()
+        # nb. this may convert float to decimal etc.
+        session.refresh(product)
 
-                # local_product
-                local = session.get(model.LocalProduct, product_info)
-                if not local:
-                    raise ValueError("local product not found")
-                row.local_product = local
-
-            else: # external product_id
-                #row.product_id = product_info
-                raise NotImplementedError
-
-        else:
-            # pending_product
-            if not self.allow_unknown_products():
-                raise TypeError("unknown/pending product not allowed for new orders")
-            row.product_id = None
-            row.local_product = None
-            pending = model.PendingProduct(status=enum.PendingProductStatus.PENDING,
-                                           created_by=user or batch.created_by)
-            fields = [
-                'scancode',
-                'brand_name',
-                'description',
-                'size',
-                'weighed',
-                'department_id',
-                'department_name',
-                'special_order',
-                'vendor_name',
-                'vendor_item_code',
-                'case_size',
-                'unit_cost',
-                'unit_price_reg',
-                'notes',
-            ]
-            for key in fields:
-                setattr(pending, key, product_info.get(key))
-
-            # nb. this may convert float to decimal etc.
-            session.add(pending)
-            session.flush()
-            session.refresh(pending)
-            row.pending_product = pending
-
-        # set order info
-        row.order_qty = order_qty
-        row.order_uom = order_uom
-
-        # add row to batch
+        # make/add new row, w/ pending product
+        row = self.make_row(pending_product=product,
+                            order_qty=order_qty, order_uom=order_uom)
         self.add_row(batch, row)
+        session.add(row)
         session.flush()
         return row
 
-    def update_item(self, row, product_info, order_qty, order_uom, user=None):
+    def set_pending_product(self, row, data):
         """
-        Update an item/row, per given product and quantity.
+        Set (add or update) pending product info for the given batch row.
 
-        See also :meth:`add_item()`.
+        This will clear the
+        :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.product_id`
+        and set the
+        :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.pending_product`,
+        creating a new record if needed.  It then updates the pending
+        product record per the given ``data``, and finally calls
+        :meth:`refresh_row()`.
+
+        Note that this does not update order quantity for the item.
+
+        See also :meth:`add_pending_product()` to add a new row
+        instead of updating.
 
         :param row:
            :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
-           to update.
+           to be updated.
 
-        :param product_info: Product ID string, or dict of
-           :class:`~sideshow.db.model.products.PendingProduct` data.
-
-        :param order_qty: New
-           :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_qty`
-           value for the row.
-
-        :param order_uom: New
-           :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_uom`
-           value for the row.
-
-        :param user:
-           :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
-           is performing the action.  This is used to set
-           :attr:`~sideshow.db.model.products.PendingProduct.created_by`
-           on the pending product, if applicable.  If not specified,
-           the batch creator is assumed.
+        :param data: Dict of field data for the
+           :class:`~sideshow.db.model.products.PendingProduct` record.
         """
         model = self.app.model
         enum = self.app.enum
         session = self.app.get_session(row)
-        use_local = self.use_local_products()
 
-        # set product info
-        if isinstance(product_info, str):
-            if use_local:
+        # values for these fields can be used as-is
+        simple_fields = [
+            'scancode',
+            'brand_name',
+            'description',
+            'size',
+            'weighed',
+            'department_id',
+            'department_name',
+            'special_order',
+            'vendor_name',
+            'vendor_item_code',
+            'notes',
+            'unit_cost',
+            'case_size',
+            'case_cost',
+            'unit_price_reg',
+        ]
 
-                # local_product
-                local = session.get(model.LocalProduct, product_info)
-                if not local:
-                    raise ValueError("local product not found")
-                row.local_product = local
+        # clear true product id
+        row.product_id = None
 
-            else: # external product_id
-                #row.product_id = product_info
-                raise NotImplementedError
-
-        else:
-            # pending_product
-            if not self.allow_unknown_products():
-                raise TypeError("unknown/pending product not allowed for new orders")
-            row.product_id = None
-            row.local_product = None
-            pending = row.pending_product
-            if not pending:
-                pending = model.PendingProduct(status=enum.PendingProductStatus.PENDING,
-                                               created_by=user or row.batch.created_by)
-                session.add(pending)
-                row.pending_product = pending
-            fields = [
-                'scancode',
-                'brand_name',
-                'description',
-                'size',
-                'weighed',
-                'department_id',
-                'department_name',
-                'special_order',
-                'vendor_name',
-                'vendor_item_code',
-                'case_size',
-                'unit_cost',
-                'unit_price_reg',
-                'notes',
-            ]
-            for key in fields:
-                setattr(pending, key, product_info.get(key))
-
-            # nb. this may convert float to decimal etc.
+        # make pending product if needed
+        product = row.pending_product
+        if not product:
+            kw = dict(data)
+            kw.setdefault('status', enum.PendingProductStatus.PENDING)
+            product = model.PendingProduct(**kw)
+            session.add(product)
+            row.pending_product = product
             session.flush()
-            session.refresh(pending)
 
-        # set order info
-        row.order_qty = order_qty
-        row.order_uom = order_uom
+        # update pending product
+        for field in simple_fields:
+            if field in data:
+                setattr(product, field, data[field])
 
         # nb. this may convert float to decimal etc.
         session.flush()
-        session.refresh(row)
+        session.refresh(product)
 
         # refresh per new info
         self.refresh_row(row)
 
-    def refresh_row(self, row):
+    def refresh_row(self, row, now=None):
         """
-        Refresh data for the row.  This is called when adding a new
-        row to the batch, or anytime the row is updated (e.g. when
+        Refresh all data for the row.  This is called when adding a
+        new row to the batch, or anytime the row is updated (e.g. when
         changing order quantity).
 
         This calls one of the following to update product-related
-        attributes:
+        attributes for the row:
 
-        * :meth:`refresh_row_from_external_product()`
-        * :meth:`refresh_row_from_local_product()`
         * :meth:`refresh_row_from_pending_product()`
+        * :meth:`refresh_row_from_true_product()`
 
         It then re-calculates the row's
         :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.total_price`
@@ -387,7 +239,7 @@ class NewOrderBatchHandler(BatchHandler):
         row.status_text = None
 
         # ensure product
-        if not row.product_id and not row.local_product and not row.pending_product:
+        if not row.product_id and not row.pending_product:
             row.status_code = row.STATUS_MISSING_PRODUCT
             return
 
@@ -398,9 +250,7 @@ class NewOrderBatchHandler(BatchHandler):
 
         # update product attrs on row
         if row.product_id:
-            self.refresh_row_from_external_product(row)
-        elif row.local_product:
-            self.refresh_row_from_local_product(row)
+            self.refresh_row_from_true_product(row)
         else:
             self.refresh_row_from_pending_product(row)
 
@@ -412,7 +262,7 @@ class NewOrderBatchHandler(BatchHandler):
         row.case_price_quoted = None
         if row.unit_price_sale is not None and (
                 not row.sale_ends
-                or row.sale_ends > datetime.datetime.now()):
+                or row.sale_ends > (now or datetime.datetime.now())):
             row.unit_price_quoted = row.unit_price_sale
         else:
             row.unit_price_quoted = row.unit_price_reg
@@ -440,27 +290,6 @@ class NewOrderBatchHandler(BatchHandler):
         # all ok
         row.status_code = row.STATUS_OK
 
-    def refresh_row_from_local_product(self, row):
-        """
-        Update product-related attributes on the row, from its
-        :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.local_product`
-        record.
-
-        This is called automatically from :meth:`refresh_row()`.
-        """
-        product = row.local_product
-        row.product_scancode = product.scancode
-        row.product_brand = product.brand_name
-        row.product_description = product.description
-        row.product_size = product.size
-        row.product_weighed = product.weighed
-        row.department_id = product.department_id
-        row.department_name = product.department_name
-        row.special_order = product.special_order
-        row.case_size = product.case_size
-        row.unit_cost = product.unit_cost
-        row.unit_price_reg = product.unit_price_reg
-
     def refresh_row_from_pending_product(self, row):
         """
         Update product-related attributes on the row, from its
@@ -470,6 +299,7 @@ class NewOrderBatchHandler(BatchHandler):
         This is called automatically from :meth:`refresh_row()`.
         """
         product = row.pending_product
+
         row.product_scancode = product.scancode
         row.product_brand = product.brand_name
         row.product_description = product.description
@@ -482,10 +312,10 @@ class NewOrderBatchHandler(BatchHandler):
         row.unit_cost = product.unit_cost
         row.unit_price_reg = product.unit_price_reg
 
-    def refresh_row_from_external_product(self, row):
+    def refresh_row_from_true_product(self, row):
         """
-        Update product-related attributes on the row, from its
-        :term:`external product` record indicated by
+        Update product-related attributes on the row, from its "true"
+        product record indicated by
         :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.product_id`.
 
         This is called automatically from :meth:`refresh_row()`.
@@ -510,39 +340,31 @@ class NewOrderBatchHandler(BatchHandler):
 
     def do_delete(self, batch, user, **kwargs):
         """
-        Delete a batch completely.
+        Delete the given batch entirely.
 
-        If the batch has :term:`pending customer` or :term:`pending
-        product` records, they are also deleted - unless still
-        referenced by some order(s).
+        If the batch has a
+        :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`
+        record, that is deleted also.
         """
-        session = self.app.get_session(batch)
-
-        # maybe delete pending customer
-        customer = batch.pending_customer
-        if customer and not customer.orders:
-            session.delete(customer)
-
-        # maybe delete pending products
-        for row in batch.rows:
-            product = row.pending_product
-            if product and not product.order_items:
-                session.delete(product)
+        # maybe delete pending customer record, if it only exists for
+        # sake of this batch
+        if batch.pending_customer:
+            if len(batch.pending_customer.new_order_batches) == 1:
+                # TODO: check for past orders too
+                session = self.app.get_session(batch)
+                session.delete(batch.pending_customer)
 
         # continue with normal deletion
         super().do_delete(batch, user, **kwargs)
 
     def why_not_execute(self, batch, **kwargs):
         """
-        By default this checks to ensure the batch has a customer with
-        phone number, and at least one item.
+        By default this checks to ensure the batch has a customer and
+        at least one item.
         """
-        if not batch.customer_name:
+        if not batch.customer_id and not batch.pending_customer:
             return "Must assign the customer"
 
-        if not batch.phone_number:
-            return "Customer phone number is required"
-
         rows = self.get_effective_rows(batch)
         if not rows:
             return "Must add at least one valid item"
@@ -559,113 +381,17 @@ class NewOrderBatchHandler(BatchHandler):
 
     def execute(self, batch, user=None, progress=None, **kwargs):
         """
-        Execute the batch; this should make a proper :term:`order`.
-
-        By default, this will call:
-
-        * :meth:`make_local_customer()`
-        * :meth:`make_local_products()`
-        * :meth:`make_new_order()`
-
-        And will return the new
-        :class:`~sideshow.db.model.orders.Order` instance.
+        By default, this will call :meth:`make_new_order()` and return
+        the new :class:`~sideshow.db.model.orders.Order` instance.
 
         Note that callers should use
         :meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.do_execute()`
         instead, which calls this method automatically.
         """
         rows = self.get_effective_rows(batch)
-        self.make_local_customer(batch)
-        self.make_local_products(batch, rows)
         order = self.make_new_order(batch, rows, user=user, progress=progress, **kwargs)
         return order
 
-    def make_local_customer(self, batch):
-        """
-        If applicable, this converts the batch :term:`pending
-        customer` into a :term:`local customer`.
-
-        This is called automatically from :meth:`execute()`.
-
-        This logic will happen only if :meth:`use_local_customers()`
-        returns true, and the batch has pending instead of local
-        customer (so far).
-
-        It will create a new
-        :class:`~sideshow.db.model.customers.LocalCustomer` record and
-        populate it from the batch
-        :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`.
-        The latter is then deleted.
-        """
-        if not self.use_local_customers():
-            return
-
-        # nothing to do if no pending customer
-        pending = batch.pending_customer
-        if not pending:
-            return
-
-        session = self.app.get_session(batch)
-
-        # maybe convert pending to local customer
-        if not batch.local_customer:
-            model = self.app.model
-            inspector = sa.inspect(model.LocalCustomer)
-            local = model.LocalCustomer()
-            for prop in inspector.column_attrs:
-                if hasattr(pending, prop.key):
-                    setattr(local, prop.key, getattr(pending, prop.key))
-            session.add(local)
-            batch.local_customer = local
-
-        # remove pending customer
-        batch.pending_customer = None
-        session.delete(pending)
-        session.flush()
-
-    def make_local_products(self, batch, rows):
-        """
-        If applicable, this converts all :term:`pending products
-        <pending product>` into :term:`local products <local
-        product>`.
-
-        This is called automatically from :meth:`execute()`.
-
-        This logic will happen only if :meth:`use_local_products()`
-        returns true, and the batch has pending instead of local items
-        (so far).
-
-        For each affected row, it will create a new
-        :class:`~sideshow.db.model.products.LocalProduct` record and
-        populate it from the row
-        :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.pending_product`.
-        The latter is then deleted.
-        """
-        if not self.use_local_products():
-            return
-
-        model = self.app.model
-        session = self.app.get_session(batch)
-        inspector = sa.inspect(model.LocalProduct)
-        for row in rows:
-
-            if row.local_product or not row.pending_product:
-                continue
-
-            pending = row.pending_product
-            local = model.LocalProduct()
-
-            for prop in inspector.column_attrs:
-                if hasattr(pending, prop.key):
-                    setattr(local, prop.key, getattr(pending, prop.key))
-            session.add(local)
-
-            row.local_product = local
-            row.pending_product = None
-            session.delete(pending)
-
-        session.flush()
-
     def make_new_order(self, batch, rows, user=None, progress=None, **kwargs):
         """
         Create a new :term:`order` from the batch data.
@@ -689,7 +415,6 @@ class NewOrderBatchHandler(BatchHandler):
         batch_fields = [
             'store_id',
             'customer_id',
-            'local_customer',
             'pending_customer',
             'customer_name',
             'phone_number',
@@ -698,9 +423,7 @@ class NewOrderBatchHandler(BatchHandler):
         ]
 
         row_fields = [
-            'product_id',
-            'local_product',
-            'pending_product',
+            'pending_product_uuid',
             'product_scancode',
             'product_brand',
             'product_description',
diff --git a/src/sideshow/db/alembic/versions/7a6df83afbd4_initial_order_tables.py b/src/sideshow/db/alembic/versions/7a6df83afbd4_initial_order_tables.py
index be6eee8..da1d591 100644
--- a/src/sideshow/db/alembic/versions/7a6df83afbd4_initial_order_tables.py
+++ b/src/sideshow/db/alembic/versions/7a6df83afbd4_initial_order_tables.py
@@ -26,8 +26,8 @@ def upgrade() -> None:
     sa.Enum('PENDING', 'READY', 'RESOLVED', name='pendingcustomerstatus').create(op.get_bind())
     sa.Enum('PENDING', 'READY', 'RESOLVED', name='pendingproductstatus').create(op.get_bind())
 
-    # sideshow_customer_pending
-    op.create_table('sideshow_customer_pending',
+    # sideshow_pending_customer
+    op.create_table('sideshow_pending_customer',
                     sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False),
                     sa.Column('customer_id', sa.String(length=20), nullable=True),
                     sa.Column('full_name', sa.String(length=100), nullable=True),
@@ -38,24 +38,12 @@ def upgrade() -> None:
                     sa.Column('status', postgresql.ENUM('PENDING', 'READY', 'RESOLVED', name='pendingcustomerstatus', create_type=False), nullable=False),
                     sa.Column('created', sa.DateTime(timezone=True), nullable=False),
                     sa.Column('created_by_uuid', wuttjamaican.db.util.UUID(), nullable=False),
-                    sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_customer_pending_created_by_uuid_user')),
-                    sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_customer_pending'))
+                    sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_pending_customer_created_by_uuid_user')),
+                    sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_pending_customer'))
                     )
 
-    # sideshow_customer_local
-    op.create_table('sideshow_customer_local',
-                    sa.Column('full_name', sa.String(length=100), nullable=True),
-                    sa.Column('first_name', sa.String(length=50), nullable=True),
-                    sa.Column('last_name', sa.String(length=50), nullable=True),
-                    sa.Column('phone_number', sa.String(length=20), nullable=True),
-                    sa.Column('email_address', sa.String(length=255), nullable=True),
-                    sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False),
-                    sa.Column('external_id', sa.String(length=20), nullable=True),
-                    sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_customer_local'))
-                    )
-
-    # sideshow_product_pending
-    op.create_table('sideshow_product_pending',
+    # sideshow_pending_product
+    op.create_table('sideshow_pending_product',
                     sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False),
                     sa.Column('product_id', sa.String(length=20), nullable=True),
                     sa.Column('scancode', sa.String(length=14), nullable=True),
@@ -75,29 +63,8 @@ def upgrade() -> None:
                     sa.Column('status', postgresql.ENUM('PENDING', 'READY', 'RESOLVED', name='pendingproductstatus', create_type=False), nullable=False),
                     sa.Column('created', sa.DateTime(timezone=True), nullable=False),
                     sa.Column('created_by_uuid', wuttjamaican.db.util.UUID(), nullable=False),
-                    sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_product_pending_created_by_uuid_user')),
-                    sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_product_pending'))
-                    )
-
-    # sideshow_product_local
-    op.create_table('sideshow_product_local',
-                    sa.Column('scancode', sa.String(length=14), nullable=True),
-                    sa.Column('brand_name', sa.String(length=100), nullable=True),
-                    sa.Column('description', sa.String(length=255), nullable=True),
-                    sa.Column('size', sa.String(length=30), nullable=True),
-                    sa.Column('weighed', sa.Boolean(), nullable=True),
-                    sa.Column('department_id', sa.String(length=10), nullable=True),
-                    sa.Column('department_name', sa.String(length=30), nullable=True),
-                    sa.Column('special_order', sa.Boolean(), nullable=True),
-                    sa.Column('vendor_name', sa.String(length=50), nullable=True),
-                    sa.Column('vendor_item_code', sa.String(length=20), nullable=True),
-                    sa.Column('case_size', sa.Numeric(precision=9, scale=4), nullable=True),
-                    sa.Column('unit_cost', sa.Numeric(precision=9, scale=5), nullable=True),
-                    sa.Column('unit_price_reg', sa.Numeric(precision=8, scale=3), nullable=True),
-                    sa.Column('notes', sa.Text(), nullable=True),
-                    sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False),
-                    sa.Column('external_id', sa.String(length=20), nullable=True),
-                    sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_product_local'))
+                    sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_pending_product_created_by_uuid_user')),
+                    sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_pending_product'))
                     )
 
     # sideshow_order
@@ -106,7 +73,6 @@ def upgrade() -> None:
                     sa.Column('order_id', sa.Integer(), nullable=False),
                     sa.Column('store_id', sa.String(length=10), nullable=True),
                     sa.Column('customer_id', sa.String(length=20), nullable=True),
-                    sa.Column('local_customer_uuid', wuttjamaican.db.util.UUID(), nullable=True),
                     sa.Column('pending_customer_uuid', wuttjamaican.db.util.UUID(), nullable=True),
                     sa.Column('customer_name', sa.String(length=100), nullable=True),
                     sa.Column('phone_number', sa.String(length=20), nullable=True),
@@ -114,8 +80,7 @@ def upgrade() -> None:
                     sa.Column('total_price', sa.Numeric(precision=10, scale=3), nullable=True),
                     sa.Column('created', sa.DateTime(timezone=True), nullable=False),
                     sa.Column('created_by_uuid', wuttjamaican.db.util.UUID(), nullable=False),
-                    sa.ForeignKeyConstraint(['local_customer_uuid'], ['sideshow_customer_local.uuid'], name=op.f('fk_order_local_customer_uuid_local_customer')),
-                    sa.ForeignKeyConstraint(['pending_customer_uuid'], ['sideshow_customer_pending.uuid'], name=op.f('fk_order_pending_customer_uuid_pending_customer')),
+                    sa.ForeignKeyConstraint(['pending_customer_uuid'], ['sideshow_pending_customer.uuid'], name=op.f('fk_order_pending_customer_uuid_pending_customer')),
                     sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_order_created_by_uuid_user')),
                     sa.PrimaryKeyConstraint('uuid', name=op.f('pk_order'))
                     )
@@ -126,7 +91,6 @@ def upgrade() -> None:
                     sa.Column('order_uuid', wuttjamaican.db.util.UUID(), nullable=False),
                     sa.Column('sequence', sa.Integer(), nullable=False),
                     sa.Column('product_id', sa.String(length=20), nullable=True),
-                    sa.Column('local_product_uuid', wuttjamaican.db.util.UUID(), nullable=True),
                     sa.Column('pending_product_uuid', wuttjamaican.db.util.UUID(), nullable=True),
                     sa.Column('product_scancode', sa.String(length=14), nullable=True),
                     sa.Column('product_brand', sa.String(length=100), nullable=True),
@@ -151,8 +115,7 @@ def upgrade() -> None:
                     sa.Column('paid_amount', sa.Numeric(precision=8, scale=3), nullable=False),
                     sa.Column('payment_transaction_number', sa.String(length=20), nullable=True),
                     sa.ForeignKeyConstraint(['order_uuid'], ['sideshow_order.uuid'], name=op.f('fk_sideshow_order_item_order_uuid_order')),
-                    sa.ForeignKeyConstraint(['local_product_uuid'], ['sideshow_product_local.uuid'], name=op.f('fk_sideshow_order_item_local_product_uuid_local_product')),
-                    sa.ForeignKeyConstraint(['pending_product_uuid'], ['sideshow_product_pending.uuid'], name=op.f('fk_sideshow_order_item_pending_product_uuid_pending_product')),
+                    sa.ForeignKeyConstraint(['pending_product_uuid'], ['sideshow_pending_product.uuid'], name=op.f('fk_sideshow_order_item_pending_product_uuid_pending_product')),
                     sa.PrimaryKeyConstraint('uuid', name=op.f('pk_order_item'))
                     )
 
@@ -171,7 +134,6 @@ def upgrade() -> None:
                     sa.Column('executed_by_uuid', wuttjamaican.db.util.UUID(), nullable=True),
                     sa.Column('store_id', sa.String(length=10), nullable=True),
                     sa.Column('customer_id', sa.String(length=20), nullable=True),
-                    sa.Column('local_customer_uuid', wuttjamaican.db.util.UUID(), nullable=True),
                     sa.Column('pending_customer_uuid', wuttjamaican.db.util.UUID(), nullable=True),
                     sa.Column('customer_name', sa.String(length=100), nullable=True),
                     sa.Column('phone_number', sa.String(length=20), nullable=True),
@@ -179,8 +141,7 @@ def upgrade() -> None:
                     sa.Column('total_price', sa.Numeric(precision=10, scale=3), nullable=True),
                     sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_batch_neworder_created_by_uuid_user')),
                     sa.ForeignKeyConstraint(['executed_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_batch_neworder_executed_by_uuid_user')),
-                    sa.ForeignKeyConstraint(['local_customer_uuid'], ['sideshow_customer_local.uuid'], name=op.f('fk_sideshow_batch_neworder_local_customer_uuid_local_customer')),
-                    sa.ForeignKeyConstraint(['pending_customer_uuid'], ['sideshow_customer_pending.uuid'], name=op.f('fk_sideshow_batch_neworder_pending_customer_uuid_pending_customer')),
+                    sa.ForeignKeyConstraint(['pending_customer_uuid'], ['sideshow_pending_customer.uuid'], name=op.f('fk_sideshow_batch_neworder_pending_customer_uuid_pending_customer')),
                     sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_batch_neworder'))
                     )
 
@@ -191,9 +152,8 @@ def upgrade() -> None:
                     sa.Column('sequence', sa.Integer(), nullable=False),
                     sa.Column('status_text', sa.String(length=255), nullable=True),
                     sa.Column('modified', sa.DateTime(timezone=True), nullable=True),
-                    sa.Column('product_id', sa.String(length=20), nullable=True),
-                    sa.Column('local_product_uuid', wuttjamaican.db.util.UUID(), nullable=True),
                     sa.Column('pending_product_uuid', wuttjamaican.db.util.UUID(), nullable=True),
+                    sa.Column('product_id', sa.String(length=20), nullable=True),
                     sa.Column('product_scancode', sa.String(length=14), nullable=True),
                     sa.Column('product_brand', sa.String(length=100), nullable=True),
                     sa.Column('product_description', sa.String(length=255), nullable=True),
@@ -213,10 +173,9 @@ def upgrade() -> None:
                     sa.Column('case_price_quoted', sa.Numeric(precision=8, scale=3), nullable=True),
                     sa.Column('discount_percent', sa.Numeric(precision=5, scale=3), nullable=True),
                     sa.Column('total_price', sa.Numeric(precision=8, scale=3), nullable=True),
-                    sa.Column('status_code', sa.Integer(), nullable=True),
+                    sa.Column('status_code', sa.Integer(), nullable=False),
                     sa.ForeignKeyConstraint(['batch_uuid'], ['sideshow_batch_neworder.uuid'], name=op.f('fk_sideshow_batch_neworder_row_batch_uuid_batch_neworder')),
-                    sa.ForeignKeyConstraint(['local_product_uuid'], ['sideshow_product_local.uuid'], name=op.f('fk_sideshow_batch_neworder_row_local_product_uuid_local_product')),
-                    sa.ForeignKeyConstraint(['pending_product_uuid'], ['sideshow_product_pending.uuid'], name=op.f('fk_sideshow_batch_neworder_row_pending_product_uuid_pending_product')),
+                    sa.ForeignKeyConstraint(['pending_product_uuid'], ['sideshow_pending_product.uuid'], name=op.f('fk_sideshow_batch_neworder_row_pending_product_uuid_pending_product')),
                     sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_batch_neworder_row'))
                     )
 
@@ -233,17 +192,11 @@ def downgrade() -> None:
     # sideshow_order
     op.drop_table('sideshow_order')
 
-    # sideshow_product_local
-    op.drop_table('sideshow_product_local')
+    # sideshow_pending_product
+    op.drop_table('sideshow_pending_product')
 
-    # sideshow_product_pending
-    op.drop_table('sideshow_product_pending')
-
-    # sideshow_customer_local
-    op.drop_table('sideshow_customer_local')
-
-    # sideshow_customer_pending
-    op.drop_table('sideshow_customer_pending')
+    # sideshow_pending_customer
+    op.drop_table('sideshow_pending_customer')
 
     # enums
     sa.Enum('PENDING', 'READY', 'RESOLVED', name='pendingproductstatus').drop(op.get_bind())
diff --git a/src/sideshow/db/model/__init__.py b/src/sideshow/db/model/__init__.py
index 28d09f3..1395d59 100644
--- a/src/sideshow/db/model/__init__.py
+++ b/src/sideshow/db/model/__init__.py
@@ -32,8 +32,6 @@ Primary :term:`data models <data model>`:
 
 * :class:`~sideshow.db.model.orders.Order`
 * :class:`~sideshow.db.model.orders.OrderItem`
-* :class:`~sideshow.db.model.customers.LocalCustomer`
-* :class:`~sideshow.db.model.products.LocalProduct`
 * :class:`~sideshow.db.model.customers.PendingCustomer`
 * :class:`~sideshow.db.model.products.PendingProduct`
 
@@ -47,8 +45,8 @@ And the :term:`batch` models:
 from wuttjamaican.db.model import *
 
 # sideshow models
-from .customers import LocalCustomer, PendingCustomer
-from .products import LocalProduct, PendingProduct
+from .customers import PendingCustomer
+from .products import PendingProduct
 from .orders import Order, OrderItem
 
 # batch models
diff --git a/src/sideshow/db/model/batch/neworder.py b/src/sideshow/db/model/batch/neworder.py
index 9121dc6..f121b5c 100644
--- a/src/sideshow/db/model/batch/neworder.py
+++ b/src/sideshow/db/model/batch/neworder.py
@@ -58,8 +58,7 @@ class NewOrderBatch(model.BatchMixin, model.Base):
     @declared_attr
     def __table_args__(cls):
         return cls.__default_table_args__() + (
-            sa.ForeignKeyConstraint(['local_customer_uuid'], ['sideshow_customer_local.uuid']),
-            sa.ForeignKeyConstraint(['pending_customer_uuid'], ['sideshow_customer_pending.uuid']),
+            sa.ForeignKeyConstraint(['pending_customer_uuid'], ['sideshow_pending_customer.uuid']),
         )
 
     STATUS_OK                           = 1
@@ -73,27 +72,13 @@ class NewOrderBatch(model.BatchMixin, model.Base):
     """)
 
     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.
+    ID of the proper customer account to which the order pertains, if
+    applicable.
 
-    See also :attr:`local_customer` and :attr:`pending_customer`.
+    This will be set only when an "existing" customer account can be
+    selected for the order.  See also :attr:`pending_customer`.
     """)
 
-    local_customer_uuid = sa.Column(model.UUID(), nullable=True)
-
-    @declared_attr
-    def local_customer(cls):
-        return orm.relationship(
-            'LocalCustomer',
-            back_populates='new_order_batches',
-            doc="""
-            Reference to the
-            :class:`~sideshow.db.model.customers.LocalCustomer` record
-            for the order, if applicable.
-
-            See also :attr:`customer_id` and :attr:`pending_customer`.
-            """)
-
     pending_customer_uuid = sa.Column(model.UUID(), nullable=True)
 
     @declared_attr
@@ -106,7 +91,8 @@ class NewOrderBatch(model.BatchMixin, model.Base):
             :class:`~sideshow.db.model.customers.PendingCustomer`
             record for the order, if applicable.
 
-            See also :attr:`customer_id` and :attr:`local_customer`.
+            This is set only when making an order for a "new /
+            unknown" customer.  See also :attr:`customer_id`.
             """)
 
     customer_name = sa.Column(sa.String(length=100), nullable=True, doc="""
@@ -140,8 +126,7 @@ class NewOrderBatchRow(model.BatchRowMixin, model.Base):
     @declared_attr
     def __table_args__(cls):
         return cls.__default_table_args__() + (
-            sa.ForeignKeyConstraint(['local_product_uuid'], ['sideshow_product_local.uuid']),
-            sa.ForeignKeyConstraint(['pending_product_uuid'], ['sideshow_product_pending.uuid']),
+            sa.ForeignKeyConstraint(['pending_product_uuid'], ['sideshow_pending_product.uuid']),
         )
 
     STATUS_OK                           = 1
@@ -173,27 +158,13 @@ class NewOrderBatchRow(model.BatchRowMixin, model.Base):
     """
 
     product_id = sa.Column(sa.String(length=20), nullable=True, doc="""
-    Proper ID for the :term:`external product` which the order item
-    represents, if applicable.
+    ID of the true product which the order item represents, if
+    applicable.
 
-    See also :attr:`local_product` and :attr:`pending_product`.
+    This will be set only when an "existing" product can be selected
+    for the order.  See also :attr:`pending_product`.
     """)
 
-    local_product_uuid = sa.Column(model.UUID(), nullable=True)
-
-    @declared_attr
-    def local_product(cls):
-        return orm.relationship(
-            'LocalProduct',
-            back_populates='new_order_batch_rows',
-            doc="""
-            Reference to the
-            :class:`~sideshow.db.model.products.LocalProduct` record
-            for the order item, if applicable.
-
-            See also :attr:`product_id` and :attr:`pending_product`.
-            """)
-
     pending_product_uuid = sa.Column(model.UUID(), nullable=True)
 
     @declared_attr
@@ -206,7 +177,8 @@ class NewOrderBatchRow(model.BatchRowMixin, model.Base):
             :class:`~sideshow.db.model.products.PendingProduct` record
             for the order item, if applicable.
 
-            See also :attr:`product_id` and :attr:`local_product`.
+            This is set only when making an order for a "new /
+            unknown" product.  See also :attr:`product_id`.
             """)
 
     product_scancode = sa.Column(sa.String(length=14), nullable=True, doc="""
diff --git a/src/sideshow/db/model/customers.py b/src/sideshow/db/model/customers.py
index aa62098..f845b30 100644
--- a/src/sideshow/db/model/customers.py
+++ b/src/sideshow/db/model/customers.py
@@ -34,13 +34,19 @@ from wuttjamaican.db import model
 from sideshow.enum import PendingCustomerStatus
 
 
-class CustomerMixin:
+class PendingCustomer(model.Base):
     """
-    Base class for customer tables.  This has shared columns, used by e.g.:
+    A "pending" customer record, used when entering an :term:`order`
+    for new/unknown customer.
+    """
+    __tablename__ = 'sideshow_pending_customer'
 
-    * :class:`LocalCustomer`
-    * :class:`PendingCustomer`
-    """
+    uuid = model.uuid_column()
+
+    customer_id = sa.Column(sa.String(length=20), nullable=True, doc="""
+    ID of the proper customer account associated with this record, if
+    applicable.
+    """)
 
     full_name = sa.Column(sa.String(length=100), nullable=True, doc="""
     Full display name for the customer account.
@@ -62,74 +68,6 @@ class CustomerMixin:
     Email address for the customer.
     """)
 
-    def __str__(self):
-        return self.full_name or ""
-
-
-class LocalCustomer(CustomerMixin, model.Base):
-    """
-    This table contains the :term:`local customer` records.
-
-    Sideshow will do customer lookups against this table by default,
-    unless it's configured to use :term:`external customers <external
-    customer>` instead.
-
-    Also by default, when a :term:`new order batch` with a
-    :term:`pending customer` is executed, a new record is added to
-    this local customers table, for lookup next time.
-    """
-    __tablename__ = 'sideshow_customer_local'
-
-    uuid = model.uuid_column()
-
-    external_id = sa.Column(sa.String(length=20), nullable=True, doc="""
-    ID of the proper customer account associated with this record, if
-    applicable.
-    """)
-
-    orders = orm.relationship(
-        'Order',
-        order_by='Order.order_id.desc()',
-        back_populates='local_customer',
-        cascade_backrefs=False,
-        doc="""
-        List of :class:`~sideshow.db.model.orders.Order` records
-        associated with this customer.
-        """)
-
-    new_order_batches = orm.relationship(
-        'NewOrderBatch',
-        order_by='NewOrderBatch.id.desc()',
-        back_populates='local_customer',
-        cascade_backrefs=False,
-        doc="""
-        List of
-        :class:`~sideshow.db.model.batch.neworder.NewOrderBatch`
-        records associated with this customer.
-        """)
-
-
-class PendingCustomer(CustomerMixin, model.Base):
-    """
-    This table contains the :term:`pending customer` records, used
-    when creating an :term:`order` for new/unknown customer.
-
-    Sideshow will automatically create and (hopefully) delete these
-    records as needed.
-
-    By default, when a :term:`new order batch` with a pending customer
-    is executed, a new record is added to the :term:`local customers
-    <local customer>` table, for lookup next time.
-    """
-    __tablename__ = 'sideshow_customer_pending'
-
-    uuid = model.uuid_column()
-
-    customer_id = sa.Column(sa.String(length=20), nullable=True, doc="""
-    ID of the proper customer account associated with this record, if
-    applicable.
-    """)
-
     status = sa.Column(sa.Enum(PendingCustomerStatus), nullable=False, doc="""
     Status code for the customer record.
     """)
@@ -169,3 +107,6 @@ class PendingCustomer(CustomerMixin, model.Base):
         :class:`~sideshow.db.model.batch.neworder.NewOrderBatch`
         records associated with this customer.
         """)
+
+    def __str__(self):
+        return self.full_name or ""
diff --git a/src/sideshow/db/model/orders.py b/src/sideshow/db/model/orders.py
index f694028..c3392f7 100644
--- a/src/sideshow/db/model/orders.py
+++ b/src/sideshow/db/model/orders.py
@@ -63,26 +63,14 @@ class Order(model.Base):
     """)
 
     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.
+    ID of the proper customer account to which the order pertains, if
+    applicable.
 
-    See also :attr:`local_customer` and :attr:`pending_customer`.
+    This will be set only when an "existing" customer account can be
+    assigned for the order.  See also :attr:`pending_customer`.
     """)
 
-    local_customer_uuid = model.uuid_fk_column('sideshow_customer_local.uuid', nullable=True)
-    local_customer = orm.relationship(
-        'LocalCustomer',
-        cascade_backrefs=False,
-        back_populates='orders',
-        doc="""
-        Reference to the
-        :class:`~sideshow.db.model.customers.LocalCustomer` record
-        for the order, if applicable.
-
-        See also :attr:`customer_id` and :attr:`pending_customer`.
-        """)
-
-    pending_customer_uuid = model.uuid_fk_column('sideshow_customer_pending.uuid', nullable=True)
+    pending_customer_uuid = model.uuid_fk_column('sideshow_pending_customer.uuid', nullable=True)
     pending_customer = orm.relationship(
         'PendingCustomer',
         cascade_backrefs=False,
@@ -92,7 +80,8 @@ class Order(model.Base):
         :class:`~sideshow.db.model.customers.PendingCustomer` record
         for the order, if applicable.
 
-        See also :attr:`customer_id` and :attr:`local_customer`.
+        This is set only when the order is for a "new / unknown"
+        customer.  See also :attr:`customer_id`.
         """)
 
     customer_name = sa.Column(sa.String(length=100), nullable=True, doc="""
@@ -169,26 +158,14 @@ class OrderItem(model.Base):
     """)
 
     product_id = sa.Column(sa.String(length=20), nullable=True, doc="""
-    Proper ID for the :term:`external product` which the order item
-    represents, if applicable.
+    ID of the true product which the order item represents, if
+    applicable.
 
-    See also :attr:`local_product` and :attr:`pending_product`.
+    This will be set only when an "existing" product can be selected
+    for the order.  See also :attr:`pending_product`.
     """)
 
-    local_product_uuid = model.uuid_fk_column('sideshow_product_local.uuid', nullable=True)
-    local_product = orm.relationship(
-        'LocalProduct',
-        cascade_backrefs=False,
-        back_populates='order_items',
-        doc="""
-        Reference to the
-        :class:`~sideshow.db.model.products.LocalProduct` record for
-        the order item, if applicable.
-
-        See also :attr:`product_id` and :attr:`pending_product`.
-        """)
-
-    pending_product_uuid = model.uuid_fk_column('sideshow_product_pending.uuid', nullable=True)
+    pending_product_uuid = model.uuid_fk_column('sideshow_pending_product.uuid', nullable=True)
     pending_product = orm.relationship(
         'PendingProduct',
         cascade_backrefs=False,
@@ -198,7 +175,8 @@ class OrderItem(model.Base):
         :class:`~sideshow.db.model.products.PendingProduct` record for
         the order item, if applicable.
 
-        See also :attr:`product_id` and :attr:`local_product`.
+        This is set only when the order item is for a "new / unknown"
+        product.  See also :attr:`product_id`.
         """)
 
     product_scancode = sa.Column(sa.String(length=14), nullable=True, doc="""
@@ -332,15 +310,5 @@ class OrderItem(model.Base):
     applicable/known.
     """)
 
-    @property
-    def full_description(self):
-        """ """
-        fields = [
-            self.product_brand or '',
-            self.product_description or '',
-            self.product_size or '']
-        fields = [f.strip() for f in fields if f.strip()]
-        return ' '.join(fields)
-
     def __str__(self):
-        return self.full_description
+        return str(self.pending_product or self.product_description or "")
diff --git a/src/sideshow/db/model/products.py b/src/sideshow/db/model/products.py
index 70c166d..6113621 100644
--- a/src/sideshow/db/model/products.py
+++ b/src/sideshow/db/model/products.py
@@ -34,13 +34,18 @@ from wuttjamaican.db import model
 from sideshow.enum import PendingProductStatus
 
 
-class ProductMixin:
+class PendingProduct(model.Base):
     """
-    Base class for product tables.  This has shared columns, used by e.g.:
+    A "pending" product record, used when entering an :term:`order
+    item` for new/unknown product.
+    """
+    __tablename__ = 'sideshow_pending_product'
 
-    * :class:`LocalProduct`
-    * :class:`PendingProduct`
-    """
+    uuid = model.uuid_column()
+
+    product_id = sa.Column(sa.String(length=20), nullable=True, doc="""
+    ID of the true product associated with this record, if applicable.
+    """)
 
     scancode = sa.Column(sa.String(length=14), nullable=True, doc="""
     Scancode for the product, as string.
@@ -112,82 +117,6 @@ class ProductMixin:
     Arbitrary notes regarding the product, if applicable.
     """)
 
-    @property
-    def full_description(self):
-        """ """
-        fields = [
-            self.brand_name or '',
-            self.description or '',
-            self.size or '']
-        fields = [f.strip() for f in fields if f.strip()]
-        return ' '.join(fields)
-
-    def __str__(self):
-        return self.full_description
-
-
-class LocalProduct(ProductMixin, model.Base):
-    """
-    This table contains the :term:`local product` records.
-
-    Sideshow will do customer lookups against this table by default,
-    unless it's configured to use :term:`external products <external
-    product>` instead.
-
-    Also by default, when a :term:`new order batch` with
-    :term:`pending product(s) <pending product>` is executed, new
-    record(s) will be added to this local products table, for lookup
-    next time.
-    """
-    __tablename__ = 'sideshow_product_local'
-
-    uuid = model.uuid_column()
-
-    external_id = sa.Column(sa.String(length=20), nullable=True, doc="""
-    ID of the true external product associated with this record, if
-    applicable.
-    """)
-
-    order_items = orm.relationship(
-        'OrderItem',
-        back_populates='local_product',
-        cascade_backrefs=False,
-        doc="""
-        List of :class:`~sideshow.db.model.orders.OrderItem` records
-        associated with this product.
-        """)
-
-    new_order_batch_rows = orm.relationship(
-        'NewOrderBatchRow',
-        back_populates='local_product',
-        cascade_backrefs=False,
-        doc="""
-        List of
-        :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
-        records associated with this product.
-        """)
-
-
-class PendingProduct(ProductMixin, model.Base):
-    """
-    This table contains the :term:`pending product` records, used when
-    creating an :term:`order` for new/unknown product(s).
-
-    Sideshow will automatically create and (hopefully) delete these
-    records as needed.
-
-    By default, when a :term:`new order batch` with pending product(s)
-    is executed, new record(s) will be added to the :term:`local
-    products <local product>` table, for lookup next time.
-    """
-    __tablename__ = 'sideshow_product_pending'
-
-    uuid = model.uuid_column()
-
-    product_id = sa.Column(sa.String(length=20), nullable=True, doc="""
-    ID of the true product associated with this record, if applicable.
-    """)
-
     status = sa.Column(sa.Enum(PendingProductStatus), nullable=False, doc="""
     Status code for the product record.
     """)
@@ -209,8 +138,10 @@ class PendingProduct(ProductMixin, model.Base):
 
     order_items = orm.relationship(
         'OrderItem',
-        back_populates='pending_product',
+        # TODO
+        # order_by='NewOrderBatchRow.id.desc()',
         cascade_backrefs=False,
+        back_populates='pending_product',
         doc="""
         List of :class:`~sideshow.db.model.orders.OrderItem` records
         associated with this product.
@@ -218,10 +149,25 @@ class PendingProduct(ProductMixin, model.Base):
 
     new_order_batch_rows = orm.relationship(
         'NewOrderBatchRow',
-        back_populates='pending_product',
+        # TODO
+        # order_by='NewOrderBatchRow.id.desc()',
         cascade_backrefs=False,
+        back_populates='pending_product',
         doc="""
         List of
         :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
         records associated with this product.
         """)
+
+    @property
+    def full_description(self):
+        """ """
+        fields = [
+            self.brand_name or '',
+            self.description or '',
+            self.size or '']
+        fields = [f.strip() for f in fields if f.strip()]
+        return ' '.join(fields)
+
+    def __str__(self):
+        return self.full_description
diff --git a/src/sideshow/web/forms/schema.py b/src/sideshow/web/forms/schema.py
index 85269c9..4b78a4b 100644
--- a/src/sideshow/web/forms/schema.py
+++ b/src/sideshow/web/forms/schema.py
@@ -29,7 +29,7 @@ from wuttaweb.forms.schema import ObjectRef
 
 class OrderRef(ObjectRef):
     """
-    Schema type for an :class:`~sideshow.db.model.orders.Order`
+    Custom schema type for an :class:`~sideshow.db.model.orders.Order`
     reference field.
 
     This is a subclass of
@@ -51,34 +51,9 @@ class OrderRef(ObjectRef):
         return self.request.route_url('orders.view', uuid=order.uuid)
 
 
-class LocalCustomerRef(ObjectRef):
-    """
-    Schema type for a
-    :class:`~sideshow.db.model.customers.LocalCustomer` reference
-    field.
-
-    This is a subclass of
-    :class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`.
-    """
-
-    @property
-    def model_class(self):
-        """ """
-        model = self.app.model
-        return model.LocalCustomer
-
-    def sort_query(self, query):
-        """ """
-        return query.order_by(self.model_class.full_name)
-
-    def get_object_url(self, customer):
-        """ """
-        return self.request.route_url('local_customers.view', uuid=customer.uuid)
-
-
 class PendingCustomerRef(ObjectRef):
     """
-    Schema type for a
+    Custom schema type for a
     :class:`~sideshow.db.model.customers.PendingCustomer` reference
     field.
 
@@ -101,33 +76,9 @@ class PendingCustomerRef(ObjectRef):
         return self.request.route_url('pending_customers.view', uuid=customer.uuid)
 
 
-class LocalProductRef(ObjectRef):
-    """
-    Schema type for a
-    :class:`~sideshow.db.model.products.LocalProduct` reference field.
-
-    This is a subclass of
-    :class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`.
-    """
-
-    @property
-    def model_class(self):
-        """ """
-        model = self.app.model
-        return model.LocalProduct
-
-    def sort_query(self, query):
-        """ """
-        return query.order_by(self.model_class.scancode)
-
-    def get_object_url(self, product):
-        """ """
-        return self.request.route_url('local_products.view', uuid=product.uuid)
-
-
 class PendingProductRef(ObjectRef):
     """
-    Schema type for a
+    Custom schema type for a
     :class:`~sideshow.db.model.products.PendingProduct` reference
     field.
 
diff --git a/src/sideshow/web/menus.py b/src/sideshow/web/menus.py
index 2c319cc..5feb017 100644
--- a/src/sideshow/web/menus.py
+++ b/src/sideshow/web/menus.py
@@ -36,15 +36,14 @@ class SideshowMenuHandler(base.MenuHandler):
         """ """
         return [
             self.make_orders_menu(request),
-            self.make_customers_menu(request),
-            self.make_products_menu(request),
+            self.make_pending_menu(request),
             self.make_batch_menu(request),
             self.make_admin_menu(request),
         ]
 
     def make_orders_menu(self, request, **kwargs):
         """
-        Generate the Orders menu.
+        Generate a typical Orders menu.
         """
         return {
             'title': "Orders",
@@ -56,55 +55,34 @@ class SideshowMenuHandler(base.MenuHandler):
                     'perm': 'orders.create',
                 },
                 {'type': 'sep'},
-                {
-                    'title': "All Order Items",
-                    'route': 'order_items',
-                    'perm': 'order_items.list',
-                },
                 {
                     'title': "All Orders",
                     'route': 'orders',
                     'perm': 'orders.list',
                 },
+                {
+                    'title': "All Order Items",
+                    'route': 'order_items',
+                    'perm': 'order_items.list',
+                },
             ],
         }
 
-    def make_customers_menu(self, request, **kwargs):
+    def make_pending_menu(self, request, **kwargs):
         """
-        Generate the Customers menu.
+        Generate a typical Pending menu.
         """
         return {
-            'title': "Customers",
+            'title': "Pending",
             'type': 'menu',
             'items': [
                 {
-                    'title': "Local Customers",
-                    'route': 'local_customers',
-                    'perm': 'local_customers.list',
-                },
-                {
-                    'title': "Pending Customers",
+                    'title': "Customers",
                     'route': 'pending_customers',
                     'perm': 'pending_customers.list',
                 },
-            ],
-        }
-
-    def make_products_menu(self, request, **kwargs):
-        """
-        Generate the Products menu.
-        """
-        return {
-            'title': "Products",
-            'type': 'menu',
-            'items': [
                 {
-                    'title': "Local Products",
-                    'route': 'local_products',
-                    'perm': 'local_products.list',
-                },
-                {
-                    'title': "Pending Products",
+                    'title': "Products",
                     'route': 'pending_products',
                     'perm': 'pending_products.list',
                 },
@@ -113,7 +91,7 @@ class SideshowMenuHandler(base.MenuHandler):
 
     def make_batch_menu(self, request, **kwargs):
         """
-        Generate the Batch menu.
+        Generate a typical Batch menu.
         """
         return {
             'title': "Batches",
diff --git a/src/sideshow/web/templates/orders/configure.mako b/src/sideshow/web/templates/orders/configure.mako
deleted file mode 100644
index 4dc23f4..0000000
--- a/src/sideshow/web/templates/orders/configure.mako
+++ /dev/null
@@ -1,41 +0,0 @@
-## -*- coding: utf-8; -*-
-<%inherit file="/configure.mako" />
-
-<%def name="form_content()">
-
-  <h3 class="block is-size-3">Products</h3>
-  <div class="block" style="padding-left: 2rem;">
-
-    <b-field message="If set, user can enter details of an arbitrary new &quot;pending&quot; product.">
-      <b-checkbox name="sideshow.orders.allow_unknown_products"
-                  v-model="simpleSettings['sideshow.orders.allow_unknown_products']"
-                  native-value="true"
-                  @input="settingsNeedSaved = true">
-        Allow creating orders for "unknown" products
-      </b-checkbox>
-    </b-field>
-
-    <div v-show="simpleSettings['sideshow.orders.allow_unknown_products']"
-         style="padding-left: 2rem;">
-
-      <p class="block">
-        Require these fields for new product:
-      </p>
-
-      <div class="block"
-           style="margin-left: 2rem;">
-        % for field in pending_product_fields:
-            <b-field>
-              <b-checkbox name="sideshow.orders.unknown_product.fields.${field}.required"
-                          v-model="simpleSettings['sideshow.orders.unknown_product.fields.${field}.required']"
-                          native-value="true"
-                          @input="settingsNeedSaved = true">
-                ${field}
-              </b-checkbox>
-            </b-field>
-        % endfor
-      </div>
-
-    </div>
-  </div>
-</%def>
diff --git a/src/sideshow/web/templates/orders/create.mako b/src/sideshow/web/templates/orders/create.mako
index 72345b5..7763775 100644
--- a/src/sideshow/web/templates/orders/create.mako
+++ b/src/sideshow/web/templates/orders/create.mako
@@ -130,20 +130,28 @@
 
                 <b-field label="Customer">
                   <div style="display: flex; gap: 1rem; width: 100%;">
-                    <wutta-autocomplete ref="customerAutocomplete"
-                                        v-model="customerID"
-                                        :display="customerName"
-                                        service-url="${url(f'{route_prefix}.customer_autocomplete')}"
-                                        placeholder="Enter name or phone number"
+                    <b-autocomplete ref="customerAutocomplete"
+                                    v-model="customerID"
+                                    :style="{'flex-grow': customerID ? '0' : '1'}"
+                                    expanded
+                                    placeholder="Enter name or phone number"
+                                    ## TODO
+                                    ## serviceUrl="${url(f'{route_prefix}.customer_autocomplete')}"
+                                    % if request.use_oruga:
+                                        ## :assigned-label="customerName"
+                                        @update:model-value="customerChanged"
+                                    % else:
+                                        ## :initial-label="customerName"
                                         @input="customerChanged"
-                                        :style="{'flex-grow': customerID ? '0' : '1'}"
-                                        expanded />
+                                    % endif
+                                    >
+                    </b-autocomplete>
                     <b-button v-if="customerID"
                               @click="refreshCustomer"
                               icon-pack="fas"
                               icon-left="redo"
                               :disabled="refreshingCustomer">
-                      {{ refreshingCustomer ? "Refreshing" : "Refresh" }}
+                      {{ refreshingCustomer ? "Refreshig" : "Refresh" }}
                     </b-button>
                   </div>
                 </b-field>
@@ -340,9 +348,9 @@
 
             <${b}-modal
               % if request.use_oruga:
-                  v-model:active="editItemShowDialog"
+                  v-model:active="showingItemDialog"
               % else:
-                  :active.sync="editItemShowDialog"
+                  :active.sync="showingItemDialog"
               % endif
               :can-cancel="['escape', 'x']"
               >
@@ -374,19 +382,28 @@
 
                         <div style="flex-grow: 1;">
                           <b-field label="Product">
-                            <wutta-autocomplete ref="productAutocomplete"
-                                                v-model="productID"
-                                                :display="productDisplay"
-                                                service-url="${url(f'{route_prefix}.product_autocomplete')}"
-                                                placeholder="Enter brand, description etc."
-                                                @input="productChanged" />
+                            <b-autocomplete ref="productLookup"
+                                            v-model="productID"
+                                            ## :style="{'flex-grow': customerID ? '0' : '1'}"
+                                            ## expanded
+                                            ## placeholder="Enter name or phone number"
+                                            ## ## serviceUrl="${url(f'{route_prefix}.customer_autocomplete')}"
+                                            ## % if request.use_oruga:
+                                            ## ## :assigned-label="customerName"
+                                            ## @update:model-value="customerChanged"
+                                            ## % else:
+                                            ## ## :initial-label="customerName"
+                                            ## @input="customerChanged"
+                                            ## % endif
+                                            >
+                            </b-autocomplete>
                           </b-field>
 
                           <div v-if="productID">
 
                             <b-field grouped>
-                              <b-field label="Scancode">
-                                <span>{{ productScancode }}</span>
+                              <b-field :label="productKeyLabel">
+                                <span>{{ productKey }}</span>
                               </b-field>
 
                               <b-field label="Unit Size">
@@ -426,16 +443,16 @@
                           </div>
                         </div>
 
-##                         <img v-if="productID"
-##                              :src="productImageURL"
-##                              style="max-height: 150px; max-width: 150px; "/>
+                        <img v-if="productID"
+                             :src="productImageURL"
+                             style="max-height: 150px; max-width: 150px; "/>
 
                       </div>
 
                       <br />
                       <div class="field">
                         <b-radio v-model="productIsKnown"
-                                 % if not allow_unknown_products:
+                                 % if not allow_unknown_product:
                                      disabled
                                  % endif
                                  :native-value="false">
@@ -476,12 +493,12 @@
 
                         <div style="display: flex; gap: 1rem;">
 
-                          <b-field label="Scancode"
-                                   % if 'scancode' in pending_product_required_fields:
-                                   :type="pendingProduct.scancode ? null : 'is-danger'"
+                          <b-field :label="productKeyLabel"
+                                   % if 'key' in pending_product_required_fields:
+                                   :type="pendingProduct[productKeyField] ? null : 'is-danger'"
                                    % endif
                                    style="width: 100%;">
-                            <b-input v-model="pendingProduct.scancode" />
+                            <b-input v-model="pendingProduct[productKeyField]" />
                           </b-field>
 
                           <b-field label="Department"
@@ -688,7 +705,7 @@
                   </${b}-tabs>
 
                   <div class="buttons">
-                    <b-button @click="editItemShowDialog = false">
+                    <b-button @click="showingItemDialog = false">
                       Cancel
                     </b-button>
                     <b-button type="is-primary"
@@ -696,7 +713,7 @@
                               :disabled="itemDialogSaveDisabled"
                               icon-pack="fas"
                               icon-left="save">
-                      {{ itemDialogSaving ? "Working, please wait..." : (this.editItemRow ? "Update Item" : "Add Item") }}
+                      {{ itemDialogSaving ? "Working, please wait..." : (this.editingItem ? "Update Item" : "Add Item") }}
                     </b-button>
                   </div>
 
@@ -708,9 +725,9 @@
                      :data="items"
                      :row-class="(row, i) => row.product_id ? null : 'has-text-success'">
 
-              <${b}-table-column label="Scancode"
+              <${b}-table-column :label="productKeyLabel"
                               v-slot="props">
-                {{ props.row.product_scancode }}
+                {{ props.row.product_key }}
               </${b}-table-column>
 
               <${b}-table-column label="Brand"
@@ -740,10 +757,8 @@
 
               <${b}-table-column label="Unit Price"
                               v-slot="props">
-                <span
-                  ##:class="props.row.pricing_reflects_sale ? 'has-background-warning' : null"
-                  >
-                  {{ props.row.unit_price_quoted_display }}
+                <span :class="props.row.pricing_reflects_sale ? 'has-background-warning' : null">
+                  {{ props.row.unit_price_display }}
                 </span>
               </${b}-table-column>
 
@@ -756,15 +771,17 @@
 
               <${b}-table-column label="Vendor"
                               v-slot="props">
-                {{ props.row.vendor_name }}
+                {{ props.row.vendor_display }}
               </${b}-table-column>
 
               <${b}-table-column field="actions"
                               label="Actions"
                               v-slot="props">
                 <a href="#"
-                   @click.prevent="editItemInit(props.row)">
-
+                   % if not request.use_oruga:
+                       class="grid-action"
+                   % endif
+                   @click.prevent="showEditItemDialog(props.row)">
                   % if request.use_oruga:
                       <span class="icon-text">
                         <o-icon icon="edit" />
@@ -829,14 +846,13 @@
                 batchTotalPriceDisplay: ${json.dumps(normalized_batch['total_price_display'])|n},
 
                 customerPanelOpen: false,
-                customerIsKnown: ${json.dumps(customer_is_known)|n},
-                customerID: ${json.dumps(customer_id)|n},
-                customerName: ${json.dumps(customer_name)|n},
-                orderPhoneNumber: ${json.dumps(phone_number)|n},
-                orderEmailAddress: ${json.dumps(email_address)|n},
+                customerIsKnown: ${json.dumps(bool(batch.customer_id))|n},
+                customerID: ${json.dumps(batch.customer_id)|n},
+                customerName: ${json.dumps(batch.customer_name)|n},
+                orderPhoneNumber: ${json.dumps(batch.phone_number)|n},
+                orderEmailAddress: ${json.dumps(batch.email_address)|n},
                 refreshingCustomer: false,
 
-                newCustomerFullName: ${json.dumps(new_customer_full_name)|n},
                 newCustomerFirstName: ${json.dumps(new_customer_first_name)|n},
                 newCustomerLastName: ${json.dumps(new_customer_last_name)|n},
                 newCustomerPhone: ${json.dumps(new_customer_phone)|n},
@@ -851,8 +867,8 @@
 
                 items: ${json.dumps(order_items)|n},
 
-                editItemRow: null,
-                editItemShowDialog: false,
+                editingItem: null,
+                showingItemDialog: false,
                 itemDialogSaving: false,
                 % if request.use_oruga:
                     itemDialogTab: 'product',
@@ -860,11 +876,16 @@
                     itemDialogTabIndex: 0,
                 % endif
 
-                productIsKnown: true,
+                ## TODO
+                productIsKnown: false,
+
                 selectedProduct: null,
                 productID: null,
                 productDisplay: null,
-                productScancode: null,
+                ## TODO
+                productKey: null,
+                productKeyField: 'scancode',
+                productKeyLabel: "Scancode",
                 productSize: null,
                 productCaseQuantity: null,
                 productUnitPrice: null,
@@ -905,8 +926,20 @@
             customerPanelHeader() {
                 let text = "Customer"
 
-                if (this.customerName) {
-                    text = "Customer: " + this.customerName
+                if (this.customerIsKnown) {
+                    if (this.customerID) {
+                        ## TODO
+                        text = "Customer: TODO"
+##                         if (this.$refs.customerAutocomplete) {
+##                             text = "Customer: " + this.$refs.customerAutocomplete.getDisplayText()
+##                         } else {
+##                             text = "Customer: " + this.customerName
+##                         }
+                    }
+                } else {
+                    if (this.customerName) {
+                        text = "Customer: " + this.customerName
+                    }
                 }
 
                 if (!this.customerPanelOpen) {
@@ -1049,29 +1082,41 @@
             }
         },
 
-        watch: {
-
-            customerIsKnown: function(val) {
-
-                if (val) {
-                    // user clicks "customer is in the system"
-
-                    // clear customer
-                    this.customerChanged(null)
-
-                    // focus customer autocomplete
-                    this.$nextTick(() => {
-                        this.$refs.customerAutocomplete.focus()
-                    })
-
-                } else {
-                    // user clicks "customer is NOT in the system"
-
-                    // remove true customer; set pending (or null)
-                    this.setPendingCustomer()
-                }
-            },
-        },
+## TODO
+##         watch: {
+## 
+##             contactIsKnown: function(val) {
+## 
+##                 // when user clicks "contact is known" then we want to
+##                 // set focus to the autocomplete component
+##                 if (val) {
+##                     this.$nextTick(() => {
+##                         this.$refs.customerAutocomplete.focus()
+##                     })
+## 
+##                 // if user has already specified a proper contact,
+##                 // i.e.  `contactUUID` is not null, *and* user has
+##                 // clicked the "contact is not yet in the system"
+##                 // button, i.e. `val` is false, then we want to *clear
+##                 // out* the existing contact selection.  this is
+##                 // primarily to avoid any ambiguity.
+##                 } else if (this.contactUUID) {
+##                     this.$refs.customerAutocomplete.clearSelection()
+##                 }
+##             },
+## 
+##             productIsKnown(newval, oldval) {
+##                 // TODO: seems like this should be better somehow?
+##                 // e.g. maybe we should not be clearing *everything*
+##                 // in case user accidentally clicks, and then clicks
+##                 // "is known" again?  and if we *should* clear all,
+##                 // why does that require 2 steps?
+##                 if (!newval) {
+##                     this.selectedProduct = null
+##                     this.clearProduct()
+##                 }
+##             },
+##         },
 
         methods: {
 
@@ -1117,7 +1162,7 @@
                 this.submittingOrder = true
 
                 const params = {
-                    action: 'submit_order',
+                    action: 'submit_new_order',
                 }
 
                 this.submitBatchData(params, response => {
@@ -1133,28 +1178,29 @@
 
             customerChanged(customerID, callback) {
 
-                const params = {}
-                if (customerID) {
-                    params.action = 'assign_customer'
-                    params.customer_id = customerID
+                let params
+                if (!customerID) {
+                    params = {
+                        action: 'unassign_contact',
+                    }
                 } else {
-                    params.action = 'unassign_customer'
+                    params = {
+                        action: 'assign_contact',
+                        customer_id: customerID,
+                    }
                 }
-
-                this.submitBatchData(params, ({data}) => {
-                    this.customerID = data.customer_id
-                    this.customerName = data.customer_name
-                    this.orderPhoneNumber = data.phone_number
-                    this.orderEmailAddress = data.email_address
+                this.submitBatchData(params, response => {
+                    this.customerID = response.data.customer_id
+                    this.customerName = response.data.customer_name
+                    this.orderPhoneNumber = response.data.phone_number
+                    this.orderEmailAddress = response.data.email_address
+                    this.addOtherPhoneNumber = response.data.add_phone_number
+                    this.addOtherEmailAddress = response.data.add_email_address
+                    this.contactPhones = response.data.contact_phones
+                    this.contactEmails = response.data.contact_emails
                     if (callback) {
                         callback()
                     }
-                }, response => {
-                    this.$buefy.toast.open({
-                        message: "Update failed: " + (response.data.error || "(unknown error)"),
-                        type: 'is-danger',
-                        duration: 2000, // 2 seconds
-                    })
                 })
             },
 
@@ -1193,8 +1239,7 @@
                 }
 
                 this.submitBatchData(params, response => {
-                    this.customerName = response.data.new_customer_full_name
-                    this.newCustomerFullName = response.data.new_customer_full_name
+                    this.customerName = response.data.new_customer_name
                     this.newCustomerFirstName = response.data.new_customer_first_name
                     this.newCustomerLastName = response.data.new_customer_last_name
                     this.newCustomerPhone = response.data.phone_number
@@ -1214,40 +1259,6 @@
 
             },
 
-            // remove true customer; set pending customer if present
-            // (else null). this happens when user clicks "customer is
-            // NOT in the system"
-            setPendingCustomer() {
-
-                let params
-                if (this.newCustomerFirstName) {
-                    params = {
-                        action: 'set_pending_customer',
-                        first_name: this.newCustomerFirstName,
-                        last_name: this.newCustomerLastName,
-                        phone_number: this.newCustomerPhone,
-                        email_address: this.newCustomerEmail,
-                    }
-                } else {
-                    params = {
-                        action: 'unassign_customer',
-                    }
-                }
-
-                this.submitBatchData(params, ({data}) => {
-                    this.customerID = data.customer_id
-                    this.customerName = data.new_customer_full_name
-                    this.orderPhoneNumber = data.phone_number
-                    this.orderEmailAddress = data.email_address
-                }, response => {
-                    this.$buefy.toast.open({
-                        message: "Update failed: " + (response.data.error || "(unknown error)"),
-                        type: 'is-danger',
-                        duration: 2000, // 2 seconds
-                    })
-                })
-            },
-
             getCasePriceDisplay() {
                 if (this.productIsKnown) {
                     return this.productCasePriceDisplay
@@ -1302,76 +1313,6 @@
                 }
             },
 
-            clearProduct() {
-                this.productID = null
-                this.productDisplay = null
-                this.productScancode = null
-                this.productSize = null
-                this.productCaseQuantity = null
-                this.productUnitPrice = null
-                this.productUnitPriceDisplay = null
-                this.productUnitRegularPriceDisplay = null
-                this.productCasePrice = null
-                this.productCasePriceDisplay = null
-                this.productSalePrice = null
-                this.productSalePriceDisplay = null
-                this.productSaleEndsDisplay = null
-                this.productUnitChoices = this.defaultUnitChoices
-            },
-
-            productChanged(productID) {
-                if (productID) {
-                    const params = {
-                        action: 'get_product_info',
-                        product_id: productID,
-                    }
-                    // nb. it is possible for the handler to "swap"
-                    // the product selection, i.e. user chooses a "per
-                    // LB" item but the handler only allows selling by
-                    // the "case" item.  so we do not assume the uuid
-                    // received above is the correct one, but just use
-                    // whatever came back from handler
-                    this.submitBatchData(params, ({data}) => {
-                        this.selectedProduct = data
-
-                        this.productID = data.product_id
-                        this.productScancode = data.scancode
-                        this.productDisplay = data.full_description
-                        this.productSize = data.size
-                        this.productCaseQuantity = data.case_size
-
-                        // TODO: what is the difference here
-                        this.productUnitPrice = data.unit_price_reg
-                        this.productUnitPriceDisplay = data.unit_price_reg_display
-                        this.productUnitRegularPriceDisplay = data.unit_price_display
-
-                        this.productCasePrice = data.case_price_quoted
-                        this.productCasePriceDisplay = data.case_price_quoted_display
-
-                        this.productSalePrice = data.unit_price_sale
-                        this.productSalePriceDisplay = data.unit_price_sale_display
-                        this.productSaleEndsDisplay = data.sale_ends_display
-
-                        // this.setProductUnitChoices(data.uom_choices)
-
-                        % if request.use_oruga:
-                            this.itemDialogTab = 'quantity'
-                        % else:
-                            this.itemDialogTabIndex = 1
-                        % endif
-
-                        // nb. hack to force refresh for vue3
-                        this.refreshProductDescription += 1
-                        this.refreshTotalPrice += 1
-
-                    }, response => {
-                        this.clearProduct()
-                    })
-                } else {
-                    this.clearProduct()
-                }
-            },
-
 ## TODO
 ##             productLookupSelected(selected) {
 ##                 // TODO: this still is a hack somehow, am sure of it.
@@ -1399,12 +1340,14 @@
 
             showAddItemDialog() {
                 this.customerPanelOpen = false
-                this.editItemRow = null
-                this.productIsKnown = true
+                this.editingItem = null
+                // TODO
+                // this.productIsKnown = true
+                this.productIsKnown = false
                 ## this.selectedProduct = null
                 this.productID = null
                 this.productDisplay = null
-                this.productScancode = null
+                ## this.productKey = null
                 this.productSize = null
                 this.productCaseQuantity = null
                 this.productUnitPrice = null
@@ -1428,15 +1371,14 @@
                 % else:
                     this.itemDialogTabIndex = 0
                 % endif
-                this.editItemShowDialog = true
+                this.showingItemDialog = true
                 this.$nextTick(() => {
-                    // this.$refs.productLookup.focus()
-                    this.$refs.productAutocomplete.focus()
+                    this.$refs.productLookup.focus()
                 })
             },
 
-            editItemInit(row) {
-                this.editItemRow = row
+            showEditItemDialog(row) {
+                this.editingItem = row
 
                 this.productIsKnown = !!row.product_id
                 this.productID = row.product_id
@@ -1460,9 +1402,10 @@
                 this.pendingProduct = pending
 
                 this.productDisplay = row.product_full_description
-                this.productScancode = row.product_scancode
+                this.productKey = row.product_key
                 this.productSize = row.product_size
-                this.productCaseQuantity = row.case_size
+                this.productCaseQuantity = row.case_quantity
+                this.productURL = row.product_url
                 this.productUnitPrice = row.unit_price_quoted
                 this.productUnitPriceDisplay = row.unit_price_quoted_display
                 this.productUnitRegularPriceDisplay = row.unit_price_reg_display
@@ -1486,7 +1429,7 @@
                 % else:
                     this.itemDialogTabIndex = 1
                 % endif
-                this.editItemShowDialog = true
+                this.showingItemDialog = true
             },
 
             deleteItem(index) {
@@ -1515,20 +1458,25 @@
             itemDialogAttemptSave() {
                 this.itemDialogSaving = true
 
-                const params = {
+                let params = {
+                    product_is_known: this.productIsKnown,
                     order_qty: this.productQuantity,
                     order_uom: this.productUOM,
                 }
 
+                % if allow_item_discounts:
+                    params.discount_percent = this.productDiscountPercent
+                % endif
+
                 if (this.productIsKnown) {
-                    params.product_info = this.productID
+                    params.product_uuid = this.productUUID
                 } else {
-                    params.product_info = this.pendingProduct
+                    params.pending_product = this.pendingProduct
                 }
 
-                if (this.editItemRow) {
+                if (this.editingItem) {
                     params.action = 'update_item'
-                    params.uuid = this.editItemRow.uuid
+                    params.uuid = this.editingItem.uuid
                 } else {
                     params.action = 'add_item'
                 }
@@ -1543,7 +1491,7 @@
                         // overwriting the item record, or else display will
                         // not update properly
                         for (let [key, value] of Object.entries(response.data.row)) {
-                            this.editItemRow[key] = value
+                            this.editingItem[key] = value
                         }
                     }
 
@@ -1551,7 +1499,7 @@
                     this.batchTotalPriceDisplay = response.data.batch.total_price_display
 
                     this.itemDialogSaving = false
-                    this.editItemShowDialog = false
+                    this.showingItemDialog = false
                 }, response => {
                     this.itemDialogSaving = false
                 })
diff --git a/src/sideshow/web/views/batch/neworder.py b/src/sideshow/web/views/batch/neworder.py
index 5e45da1..0c0aad5 100644
--- a/src/sideshow/web/views/batch/neworder.py
+++ b/src/sideshow/web/views/batch/neworder.py
@@ -29,7 +29,7 @@ from wuttaweb.forms.schema import WuttaMoney
 
 from sideshow.db.model import NewOrderBatch
 from sideshow.batch.neworder import NewOrderBatchHandler
-from sideshow.web.forms.schema import LocalCustomerRef, PendingCustomerRef
+from sideshow.web.forms.schema import PendingCustomerRef
 
 
 class NewOrderBatchView(BatchMasterView):
@@ -87,7 +87,6 @@ class NewOrderBatchView(BatchMasterView):
         'id',
         'store_id',
         'customer_id',
-        'local_customer',
         'pending_customer',
         'customer_name',
         'phone_number',
@@ -116,11 +115,9 @@ class NewOrderBatchView(BatchMasterView):
         'product_description',
         'product_size',
         'special_order',
-        'unit_price_quoted',
-        'case_size',
-        'case_price_quoted',
         'order_qty',
         'order_uom',
+        'case_size',
         'total_price',
         'status_code',
     ]
@@ -141,9 +138,6 @@ class NewOrderBatchView(BatchMasterView):
         """ """
         super().configure_form(f)
 
-        # local_customer
-        f.set_node('local_customer', LocalCustomerRef(self.request))
-
         # pending_customer
         f.set_node('pending_customer', PendingCustomerRef(self.request))
 
@@ -159,14 +153,6 @@ class NewOrderBatchView(BatchMasterView):
         # order_uom
         #g.set_renderer('order_uom', self.grid_render_enum, enum=enum.ORDER_UOM)
 
-        # unit_price_quoted
-        g.set_label('unit_price_quoted', "Unit Price", column_only=True)
-        g.set_renderer('unit_price_quoted', 'currency')
-
-        # case_price_quoted
-        g.set_label('case_price_quoted', "Case Price", column_only=True)
-        g.set_renderer('case_price_quoted', 'currency')
-
         # total_price
         g.set_renderer('total_price', 'currency')
 
diff --git a/src/sideshow/web/views/customers.py b/src/sideshow/web/views/customers.py
index 850ec5e..9d1720e 100644
--- a/src/sideshow/web/views/customers.py
+++ b/src/sideshow/web/views/customers.py
@@ -27,161 +27,7 @@ Views for Customers
 from wuttaweb.views import MasterView
 from wuttaweb.forms.schema import UserRef, WuttaEnum
 
-from sideshow.db.model import LocalCustomer, PendingCustomer
-
-
-class LocalCustomerView(MasterView):
-    """
-    Master view for
-    :class:`~sideshow.db.model.customers.LocalCustomer`; route prefix
-    is ``local_customers``.
-
-    Notable URLs provided by this class:
-
-    * ``/local/customers/``
-    * ``/local/customers/new``
-    * ``/local/customers/XXX``
-    * ``/local/customers/XXX/edit``
-    * ``/local/customers/XXX/delete``
-    """
-    model_class = LocalCustomer
-    model_title = "Local Customer"
-    route_prefix = 'local_customers'
-    url_prefix = '/local/customers'
-
-    labels = {
-        'external_id': "External ID",
-    }
-
-    grid_columns = [
-        'external_id',
-        'full_name',
-        'first_name',
-        'last_name',
-        'phone_number',
-        'email_address',
-    ]
-
-    sort_defaults = 'full_name'
-
-    form_fields = [
-        'external_id',
-        'full_name',
-        'first_name',
-        'last_name',
-        'phone_number',
-        'email_address',
-        'orders',
-        'new_order_batches',
-    ]
-
-    def configure_grid(self, g):
-        """ """
-        super().configure_grid(g)
-
-        # links
-        g.set_link('full_name')
-        g.set_link('first_name')
-        g.set_link('last_name')
-        g.set_link('phone_number')
-        g.set_link('email_address')
-
-    def configure_form(self, f):
-        """ """
-        super().configure_form(f)
-        customer = f.model_instance
-
-        # external_id
-        if self.creating:
-            f.remove('external_id')
-        else:
-            f.set_readonly('external_id')
-
-        # full_name
-        if self.creating or self.editing:
-            f.remove('full_name')
-
-        # orders
-        if self.creating or self.editing:
-            f.remove('orders')
-        else:
-            f.set_grid('orders', self.make_orders_grid(customer))
-
-        # new_order_batches
-        if self.creating or self.editing:
-            f.remove('new_order_batches')
-        else:
-            f.set_grid('new_order_batches', self.make_new_order_batches_grid(customer))
-
-    def make_orders_grid(self, customer):
-        """
-        Make and return the grid for the Orders field.
-        """
-        model = self.app.model
-        route_prefix = self.get_route_prefix()
-
-        grid = self.make_grid(key=f'{route_prefix}.view.orders',
-                              model_class=model.Order,
-                              data=customer.orders,
-                              columns=[
-                                  'order_id',
-                                  'total_price',
-                                  'created',
-                                  'created_by',
-                              ],
-                              labels={
-                                  'order_id': "Order ID",
-                              })
-        grid.set_renderer('total_price', grid.render_currency)
-
-        if self.request.has_perm('orders.view'):
-            url = lambda order, i: self.request.route_url('orders.view', uuid=order.uuid)
-            grid.add_action('view', icon='eye', url=url)
-            grid.set_link('order_id')
-
-        return grid
-
-    def make_new_order_batches_grid(self, customer):
-        """
-        Make and return the grid for the New Order Batches field.
-        """
-        model = self.app.model
-        route_prefix = self.get_route_prefix()
-
-        grid = self.make_grid(key=f'{route_prefix}.view.new_order_batches',
-                              model_class=model.NewOrderBatch,
-                              data=customer.new_order_batches,
-                              columns=[
-                                  'id',
-                                  'total_price',
-                                  'created',
-                                  'created_by',
-                                  'executed',
-                              ],
-                              labels={
-                                  'id': "Batch ID",
-                              },
-                              renderers={
-                                  'id': 'batch_id',
-                                  'total_price': 'currency',
-                              })
-
-        if self.request.has_perm('neworder_batches.view'):
-            url = lambda batch, i: self.request.route_url('neworder_batches.view', uuid=batch.uuid)
-            grid.add_action('view', icon='eye', url=url)
-            grid.set_link('id')
-
-        return grid
-
-    def objectify(self, form):
-        """ """
-        enum = self.app.enum
-        customer = super().objectify(form)
-
-        customer.full_name = self.app.make_full_name(customer.first_name,
-                                                     customer.last_name)
-
-        return customer
+from sideshow.db.model import PendingCustomer
 
 
 class PendingCustomerView(MasterView):
@@ -225,9 +71,12 @@ class PendingCustomerView(MasterView):
         'customer_id',
         'full_name',
         'first_name',
+        'middle_name',
         'last_name',
         'phone_number',
+        'phone_type',
         'email_address',
+        'email_type',
         'status',
         'created',
         'created_by',
@@ -389,9 +238,6 @@ class PendingCustomerView(MasterView):
 def defaults(config, **kwargs):
     base = globals()
 
-    LocalCustomerView = kwargs.get('LocalCustomerView', base['LocalCustomerView'])
-    LocalCustomerView.defaults(config)
-
     PendingCustomerView = kwargs.get('PendingCustomerView', base['PendingCustomerView'])
     PendingCustomerView.defaults(config)
 
diff --git a/src/sideshow/web/views/orders.py b/src/sideshow/web/views/orders.py
index bbd5811..76b49f3 100644
--- a/src/sideshow/web/views/orders.py
+++ b/src/sideshow/web/views/orders.py
@@ -31,13 +31,11 @@ import colander
 from sqlalchemy import orm
 
 from wuttaweb.views import MasterView
-from wuttaweb.forms.schema import UserRef, WuttaMoney, WuttaQuantity, WuttaEnum, WuttaDictEnum
+from wuttaweb.forms.schema import UserRef, WuttaMoney, WuttaQuantity, WuttaEnum
 
 from sideshow.db.model import Order, OrderItem
 from sideshow.batch.neworder import NewOrderBatchHandler
-from sideshow.web.forms.schema import (OrderRef,
-                                       LocalCustomerRef, LocalProductRef,
-                                       PendingCustomerRef, PendingProductRef)
+from sideshow.web.forms.schema import OrderRef, PendingCustomerRef, PendingProductRef
 
 
 log = logging.getLogger(__name__)
@@ -60,7 +58,6 @@ class OrderView(MasterView):
     """
     model_class = Order
     editable = False
-    configurable = True
 
     labels = {
         'order_id': "Order ID",
@@ -84,7 +81,6 @@ class OrderView(MasterView):
         'order_id',
         'store_id',
         'customer_id',
-        'local_customer',
         'pending_customer',
         'customer_name',
         'phone_number',
@@ -126,14 +122,15 @@ class OrderView(MasterView):
 
     PENDING_PRODUCT_ENTRY_FIELDS = [
         'scancode',
+        'department_id',
+        'department_name',
         'brand_name',
         'description',
         'size',
-        'department_name',
         'vendor_name',
         'vendor_item_code',
-        'case_size',
         'unit_cost',
+        'case_size',
         'unit_price_reg',
     ]
 
@@ -168,20 +165,6 @@ class OrderView(MasterView):
         batch, which in turn creates a true
         :class:`~sideshow.db.model.orders.Order`, and user is
         redirected to the "view order" page.
-
-        See also these methods which may be called from this one,
-        based on user actions:
-
-        * :meth:`start_over()`
-        * :meth:`cancel_order()`
-        * :meth:`assign_customer()`
-        * :meth:`unassign_customer()`
-        * :meth:`set_pending_customer()`
-        * :meth:`get_product_info()`
-        * :meth:`add_item()`
-        * :meth:`update_item()`
-        * :meth:`delete_item()`
-        * :meth:`submit_order()`
         """
         enum = self.app.enum
         self.creating = True
@@ -205,25 +188,22 @@ class OrderView(MasterView):
             data = dict(self.request.json_body)
             action = data.pop('action')
             json_actions = [
-                'assign_customer',
-                'unassign_customer',
+                # 'assign_contact',
+                # 'unassign_contact',
                 # 'update_phone_number',
                 # 'update_email_address',
                 'set_pending_customer',
                 # 'get_customer_info',
                 # # 'set_customer_data',
-                'get_product_info',
+                # 'get_product_info',
                 # 'get_past_items',
                 'add_item',
                 'update_item',
                 'delete_item',
-                'submit_order',
+                'submit_new_order',
             ]
             if action in json_actions:
-                try:
-                    result = getattr(self, action)(batch, data)
-                except Exception as error:
-                    result = {'error': self.app.render_error(error)}
+                result = getattr(self, action)(batch, data)
                 return self.json_response(result)
 
             return self.json_response({'error': "unknown form action"})
@@ -233,10 +213,10 @@ class OrderView(MasterView):
             'normalized_batch': self.normalize_batch(batch),
             'order_items': [self.normalize_row(row)
                             for row in batch.rows],
+
+            'allow_unknown_product': True, # TODO
             'default_uom_choices': self.get_default_uom_choices(),
             'default_uom': None, # TODO?
-            '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(),
         })
         return self.render_to_response('create', context)
@@ -275,96 +255,6 @@ class OrderView(MasterView):
 
         return batch
 
-    def customer_autocomplete(self):
-        """
-        AJAX view for customer autocomplete, when entering new order.
-
-        This should invoke a configured handler for the autocomplete
-        behavior, but that is not yet implemented.  For now it uses
-        built-in logic only, which queries the
-        :class:`~sideshow.db.model.customers.LocalCustomer` table.
-        """
-        session = self.Session()
-        term = self.request.GET.get('term', '').strip()
-        if not term:
-            return []
-        return self.mock_autocomplete_customers(session, term, user=self.request.user)
-
-    # TODO: move this to some handler
-    def mock_autocomplete_customers(self, session, term, user=None):
-        """ """
-        import sqlalchemy as sa
-
-        model = self.app.model
-
-        # base query
-        query = session.query(model.LocalCustomer)
-
-        # filter query
-        criteria = [model.LocalCustomer.full_name.ilike(f'%{word}%')
-                    for word in term.split()]
-        query = query.filter(sa.and_(*criteria))
-
-        # sort query
-        query = query.order_by(model.LocalCustomer.full_name)
-
-        # get data
-        # TODO: need max_results option
-        customers = query.all()
-
-        # get results
-        def result(customer):
-            return {'value': customer.uuid.hex,
-                    'label': customer.full_name}
-        return [result(c) for c in customers]
-
-    def product_autocomplete(self):
-        """
-        AJAX view for product autocomplete, when entering new order.
-
-        This should invoke a configured handler for the autocomplete
-        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.
-        """
-        session = self.Session()
-        term = self.request.GET.get('term', '').strip()
-        if not term:
-            return []
-        return self.mock_autocomplete_products(session, term, user=self.request.user)
-
-    # TODO: move this to some handler
-    def mock_autocomplete_products(self, session, term, user=None):
-        """ """
-        import sqlalchemy as sa
-
-        model = self.app.model
-
-        # base query
-        query = session.query(model.LocalProduct)
-
-        # filter query
-        criteria = []
-        for word in term.split():
-            criteria.append(sa.or_(
-                model.LocalProduct.brand_name.ilike(f'%{word}%'),
-                model.LocalProduct.description.ilike(f'%{word}%')))
-        query = query.filter(sa.and_(*criteria))
-
-        # sort query
-        query = query.order_by(model.LocalProduct.brand_name,
-                               model.LocalProduct.description)
-
-        # get data
-        # TODO: need max_results option
-        products = query.all()
-
-        # get results
-        def result(product):
-            return {'value': product.uuid.hex,
-                    'label': product.full_description}
-        return [result(c) for c in products]
-
     def get_pending_product_required_fields(self):
         """ """
         required = []
@@ -384,10 +274,7 @@ class OrderView(MasterView):
         new batch for them.
 
         This is a "batch action" method which may be called from
-        :meth:`create()`.  See also:
-
-        * :meth:`cancel_order()`
-        * :meth:`submit_order()`
+        :meth:`create()`.
         """
         # drop current batch
         self.batch_handler.do_delete(batch, self.request.user)
@@ -404,10 +291,7 @@ class OrderView(MasterView):
         back to "List Orders" page.
 
         This is a "batch action" method which may be called from
-        :meth:`create()`.  See also:
-
-        * :meth:`start_over()`
-        * :meth:`submit_order()`
+        :meth:`create()`.
         """
         self.batch_handler.do_delete(batch, self.request.user)
         self.Session.flush()
@@ -422,193 +306,86 @@ class OrderView(MasterView):
     def get_context_customer(self, batch):
         """ """
         context = {
-            'customer_is_known': True,
-            'customer_id': None,
+            'customer_id': batch.customer_id,
             'customer_name': batch.customer_name,
             'phone_number': batch.phone_number,
             'email_address': batch.email_address,
+            'new_customer_name': None,
+            'new_customer_first_name': None,
+            'new_customer_last_name': None,
+            'new_customer_phone': None,
+            'new_customer_email': None,
         }
 
-        # customer_id
-        use_local = self.batch_handler.use_local_customers()
-        if use_local:
-            local = batch.local_customer
-            if local:
-                context['customer_id'] = local.uuid.hex
-        else: # use external
-            context['customer_id'] = batch.customer_id
-
-        # pending customer
         pending = batch.pending_customer
         if pending:
             context.update({
                 'new_customer_first_name': pending.first_name,
                 'new_customer_last_name': pending.last_name,
-                'new_customer_full_name': pending.full_name,
+                'new_customer_name': pending.full_name,
                 'new_customer_phone': pending.phone_number,
                 'new_customer_email': pending.email_address,
             })
 
-        # declare customer "not known" only if pending is in use
-        if (pending
-            and not batch.customer_id and not batch.local_customer
-            and batch.customer_name):
-            context['customer_is_known'] = False
+        # figure out if customer is "known" from user's perspective.
+        # if we have an ID then it's definitely known, otherwise if we
+        # have a pending customer then it's definitely *not* known,
+        # but if no pending customer yet then we can still "assume" it
+        # is known, by default, until user specifies otherwise.
+        if batch.customer_id:
+            context['customer_is_known'] = True
+        else:
+            context['customer_is_known'] = not pending
 
         return context
 
-    def assign_customer(self, batch, data):
-        """
-        Assign the true customer account for a batch.
-
-        This calls
-        :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.set_customer()`
-        for the heavy lifting.
-
-        This is a "batch action" method which may be called from
-        :meth:`create()`.  See also:
-
-        * :meth:`unassign_customer()`
-        * :meth:`set_pending_customer()`
-        """
-        customer_id = data.get('customer_id')
-        if not customer_id:
-            return {'error': "Must provide customer_id"}
-
-        self.batch_handler.set_customer(batch, customer_id)
-        return self.get_context_customer(batch)
-
-    def unassign_customer(self, batch, data):
-        """
-        Clear the customer info for a batch.
-
-        This calls
-        :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.set_customer()`
-        for the heavy lifting.
-
-        This is a "batch action" method which may be called from
-        :meth:`create()`.  See also:
-
-        * :meth:`assign_customer()`
-        * :meth:`set_pending_customer()`
-        """
-        self.batch_handler.set_customer(batch, None)
-        return self.get_context_customer(batch)
-
     def set_pending_customer(self, batch, data):
         """
         This will set/update the batch pending customer info.
 
         This calls
-        :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.set_customer()`
+        :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.set_pending_customer()`
         for the heavy lifting.
 
-        This is a "batch action" method which may be called from
-        :meth:`create()`.  See also:
-
-        * :meth:`assign_customer()`
-        * :meth:`unassign_customer()`
-        """
-        self.batch_handler.set_customer(batch, data, user=self.request.user)
-        return self.get_context_customer(batch)
-
-    def get_product_info(self, batch, data):
-        """
-        Fetch data for a specific product.  (Nothing is modified.)
-
-        Depending on config, this will fetch a :term:`local product`
-        or :term:`external product` to get the 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.
-
         This is a "batch action" method which may be called from
         :meth:`create()`.
         """
-        product_id = data.get('product_id')
-        if not product_id:
-            return {'error': "Must specify a product ID"}
+        data['created_by'] = self.request.user
+        try:
+            self.batch_handler.set_pending_customer(batch, data)
+        except Exception as error:
+            return {'error': self.app.render_error(error)}
 
-        use_local = self.batch_handler.use_local_products()
-        if use_local:
-            data = self.get_local_product_info(product_id)
-        else:
-            raise NotImplementedError("TODO: add integration handler")
-
-        if 'error' in data:
-            return data
-
-        if 'unit_price_reg' in data and 'unit_price_reg_display' not in data:
-            data['unit_price_reg_display'] = self.app.render_currency(data['unit_price_reg'])
-
-        if 'unit_price_reg' in data and 'unit_price_quoted' not in data:
-            data['unit_price_quoted'] = data['unit_price_reg']
-
-        if 'unit_price_quoted' in data and 'unit_price_quoted_display' not in data:
-            data['unit_price_quoted_display'] = self.app.render_currency(data['unit_price_quoted'])
-
-        if 'case_price_quoted' not in data:
-            if data.get('unit_price_quoted') is not None and data.get('case_size') is not None:
-                data['case_price_quoted'] = data['unit_price_quoted'] * data['case_size']
-
-        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'])
-
-        decimal_fields = [
-            'case_size',
-            'unit_price_reg',
-            'unit_price_quoted',
-            'case_price_quoted',
-        ]
-
-        for field in decimal_fields:
-            if field in list(data):
-                value = data[field]
-                if isinstance(value, decimal.Decimal):
-                    data[field] = float(value)
-
-        return data
-
-    # TODO: move this to some handler
-    def get_local_product_info(self, product_id):
-        """ """
-        model = self.app.model
-        session = self.Session()
-        product = session.get(model.LocalProduct, product_id)
-        if not product:
-            return {'error': "Product not found"}
-
-        return {
-            'product_id': product.uuid.hex,
-            'scancode': product.scancode,
-            'brand_name': product.brand_name,
-            'description': product.description,
-            'size': product.size,
-            'full_description': product.full_description,
-            'weighed': product.weighed,
-            'special_order': product.special_order,
-            'department_id': product.department_id,
-            'department_name': product.department_name,
-            'case_size': product.case_size,
-            'unit_price_reg': product.unit_price_reg,
-            'vendor_name': product.vendor_name,
-            'vendor_item_code': product.vendor_item_code,
-        }
+        self.Session.flush()
+        context = self.get_context_customer(batch)
+        return context
 
     def add_item(self, batch, data):
         """
         This adds a row to the user's current new order batch.
 
         This is a "batch action" method which may be called from
-        :meth:`create()`.  See also:
-
-        * :meth:`update_item()`
-        * :meth:`delete_item()`
+        :meth:`create()`.
         """
-        row = self.batch_handler.add_item(batch, data['product_info'],
-                                          data['order_qty'], data['order_uom'])
+        order_qty = decimal.Decimal(data.get('order_qty') or '0')
+        order_uom = data['order_uom']
+
+        if data.get('product_is_known'):
+            raise NotImplementedError
+
+        else: # unknown product; add pending
+            pending = data['pending_product']
+
+            for field in ('unit_cost', 'unit_price_reg', 'case_size'):
+                if field in pending:
+                    try:
+                        pending[field] = decimal.Decimal(pending[field])
+                    except decimal.InvalidOperation:
+                        return {'error': f"Invalid entry for field: {field}"}
+
+            pending['created_by'] = self.request.user
+            row = self.batch_handler.add_pending_product(batch, pending,
+                                                         order_qty, order_uom)
 
         return {'batch': self.normalize_batch(batch),
                 'row': self.normalize_row(row)}
@@ -618,17 +395,15 @@ class OrderView(MasterView):
         This updates a row in the user's current new order batch.
 
         This is a "batch action" method which may be called from
-        :meth:`create()`.  See also:
-
-        * :meth:`add_item()`
-        * :meth:`delete_item()`
+        :meth:`create()`.
         """
         model = self.app.model
+        enum = self.app.enum
         session = self.Session()
 
         uuid = data.get('uuid')
         if not uuid:
-            return {'error': "Must specify row UUID"}
+            return {'error': "Must specify a row UUID"}
 
         row = session.get(model.NewOrderBatchRow, uuid)
         if not row:
@@ -637,8 +412,20 @@ class OrderView(MasterView):
         if row.batch is not batch:
             return {'error': "Row is for wrong batch"}
 
-        self.batch_handler.update_item(row, data['product_info'],
-                                       data['order_qty'], data['order_uom'])
+        order_qty = decimal.Decimal(data.get('order_qty') or '0')
+        order_uom = data['order_uom']
+
+        if data.get('product_is_known'):
+            raise NotImplementedError
+
+        else: # pending product
+
+            # set these first, since row will be refreshed below
+            row.order_qty = order_qty
+            row.order_uom = order_uom
+
+            # nb. this will refresh the row
+            self.batch_handler.set_pending_product(row, data['pending_product'])
 
         return {'batch': self.normalize_batch(batch),
                 'row': self.normalize_row(row)}
@@ -648,10 +435,7 @@ class OrderView(MasterView):
         This deletes a row from the user's current new order batch.
 
         This is a "batch action" method which may be called from
-        :meth:`create()`.  See also:
-
-        * :meth:`add_item()`
-        * :meth:`update_item()`
+        :meth:`create()`.
         """
         model = self.app.model
         session = self.app.get_session(batch)
@@ -668,18 +452,16 @@ class OrderView(MasterView):
             return {'error': "Row is for wrong batch"}
 
         self.batch_handler.do_remove_row(row)
+        session.flush()
         return {'batch': self.normalize_batch(batch)}
 
-    def submit_order(self, batch, data):
+    def submit_new_order(self, batch, data):
         """
         This submits the user's current new order batch, hence
         executing the batch and creating the true order.
 
         This is a "batch action" method which may be called from
-        :meth:`create()`.  See also:
-
-        * :meth:`start_over()`
-        * :meth:`cancel_order()`
+        :meth:`create()`.
         """
         user = self.request.user
         reason = self.batch_handler.why_not_execute(batch, user=user)
@@ -720,7 +502,6 @@ class OrderView(MasterView):
         data = {
             'uuid': row.uuid.hex,
             'sequence': row.sequence,
-            'product_id': None,
             'product_scancode': row.product_scancode,
             'product_brand': row.product_brand,
             'product_description': row.product_description,
@@ -728,8 +509,8 @@ class OrderView(MasterView):
             'product_weighed': row.product_weighed,
             'department_display': 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),
+            'case_size': self.app.render_quantity(row.case_size),
+            'order_qty': self.app.render_quantity(row.order_qty),
             'order_uom': row.order_uom,
             'order_uom_choices': self.get_default_uom_choices(),
             'unit_price_quoted': float(row.unit_price_quoted) if row.unit_price_quoted is not None else None,
@@ -742,33 +523,6 @@ class OrderView(MasterView):
             'status_text': row.status_text,
         }
 
-        use_local = self.batch_handler.use_local_products()
-
-        # product_id
-        if use_local:
-            if row.local_product:
-                data['product_id'] = row.local_product.uuid.hex
-        else:
-            data['product_id'] = row.product_id
-
-        # product_full_description
-        if use_local:
-            if row.local_product:
-                data['product_full_description'] = row.local_product.full_description
-        else: # use external
-            pass                # TODO
-        if not data.get('product_id') and row.pending_product:
-            data['product_full_description'] = row.pending_product.full_description
-
-        # vendor_name
-        if use_local:
-            if row.local_product:
-                data['vendor_name'] = row.local_product.vendor_name
-        else: # use external
-            pass                # TODO
-        if not data.get('product_id') and row.pending_product:
-            data['vendor_name'] = row.pending_product.vendor_name
-
         if row.unit_price_reg:
             data['unit_price_reg'] = float(row.unit_price_reg)
             data['unit_price_reg_display'] = self.app.render_currency(row.unit_price_reg)
@@ -781,8 +535,21 @@ class OrderView(MasterView):
             data['sale_ends'] = str(row.sale_ends)
             data['sale_ends_display'] = self.app.render_date(row.sale_ends)
 
+        # if row.unit_price_sale and row.unit_price_quoted == row.unit_price_sale:
+        #     data['pricing_reflects_sale'] = True
+
+        # TODO
+        if row.pending_product:
+            data['product_full_description'] = row.pending_product.full_description
+        # else:
+        #     data['product_full_description'] = row.product_description
+
+        # if row.pending_product:
+        #     data['vendor_display'] = row.pending_product.vendor_name
+
         if row.pending_product:
             pending = row.pending_product
+            # data['vendor_display'] = pending.vendor_name
             data['pending_product'] = {
                 'uuid': pending.uuid.hex,
                 'scancode': pending.scancode,
@@ -800,17 +567,19 @@ class OrderView(MasterView):
                 'special_order': pending.special_order,
             }
 
+        # TODO: remove this
+        data['product_key'] = row.product_scancode
+
         # display text for order qty/uom
         if row.order_uom == enum.ORDER_UOM_CASE:
-            order_qty = self.app.render_quantity(row.order_qty)
             if row.case_size is None:
                 case_qty = unit_qty = '??'
             else:
-                case_qty = self.app.render_quantity(row.case_size)
+                case_qty = data['case_size']
                 unit_qty = self.app.render_quantity(row.order_qty * row.case_size)
             CS = enum.ORDER_UOM[enum.ORDER_UOM_CASE]
             EA = enum.ORDER_UOM[enum.ORDER_UOM_UNIT]
-            data['order_qty_display'] = (f"{order_qty} {CS} "
+            data['order_qty_display'] = (f"{data['order_qty']} {CS} "
                                          f"(&times; {case_qty} = {unit_qty} {EA})")
         else:
             unit_qty = self.app.render_quantity(row.order_qty)
@@ -826,16 +595,9 @@ class OrderView(MasterView):
     def configure_form(self, f):
         """ """
         super().configure_form(f)
-        order = f.model_instance
-
-        # local_customer
-        f.set_node('local_customer', LocalCustomerRef(self.request))
 
         # pending_customer
-        if order.customer_id or order.local_customer:
-            f.remove('pending_customer')
-        else:
-            f.set_node('pending_customer', PendingCustomerRef(self.request))
+        f.set_node('pending_customer', PendingCustomerRef(self.request))
 
         # total_price
         f.set_node('total_price', WuttaMoney(self.request))
@@ -908,75 +670,6 @@ class OrderView(MasterView):
         """ """
         return self.request.route_url('order_items.view', uuid=item.uuid)
 
-    def configure_get_simple_settings(self):
-        """ """
-        settings = [
-
-            # products
-            {'name': 'sideshow.orders.allow_unknown_products',
-             'type': bool,
-             'default': True},
-        ]
-
-        # required fields for new product entry
-        for field in self.PENDING_PRODUCT_ENTRY_FIELDS:
-            setting = {'name': f'sideshow.orders.unknown_product.fields.{field}.required',
-                       'type': bool}
-            if field == 'description':
-                setting['default'] = True
-            settings.append(setting)
-
-        return settings
-
-    def configure_get_context(self, **kwargs):
-        """ """
-        context = super().configure_get_context(**kwargs)
-
-        context['pending_product_fields'] = self.PENDING_PRODUCT_ENTRY_FIELDS
-
-        return context
-
-    @classmethod
-    def defaults(cls, config):
-        cls._order_defaults(config)
-        cls._defaults(config)
-
-    @classmethod
-    def _order_defaults(cls, config):
-        route_prefix = cls.get_route_prefix()
-        permission_prefix = cls.get_permission_prefix()
-        url_prefix = cls.get_url_prefix()
-        model_title = cls.get_model_title()
-        model_title_plural = cls.get_model_title_plural()
-
-        # fix perm group
-        config.add_wutta_permission_group(permission_prefix,
-                                          model_title_plural,
-                                          overwrite=False)
-
-        # extra perm required to create order with unknown/pending product
-        config.add_wutta_permission(permission_prefix,
-                                    f'{permission_prefix}.create_unknown_product',
-                                    f"Create new {model_title} for unknown/pending product")
-
-        # customer autocomplete
-        config.add_route(f'{route_prefix}.customer_autocomplete',
-                         f'{url_prefix}/customer-autocomplete',
-                         request_method='GET')
-        config.add_view(cls, attr='customer_autocomplete',
-                        route_name=f'{route_prefix}.customer_autocomplete',
-                        renderer='json',
-                        permission=f'{permission_prefix}.list')
-
-        # product autocomplete
-        config.add_route(f'{route_prefix}.product_autocomplete',
-                         f'{url_prefix}/product-autocomplete',
-                         request_method='GET')
-        config.add_view(cls, attr='product_autocomplete',
-                        route_name=f'{route_prefix}.product_autocomplete',
-                        renderer='json',
-                        permission=f'{permission_prefix}.list')
-
 
 class OrderItemView(MasterView):
     """
@@ -1006,8 +699,7 @@ class OrderItemView(MasterView):
         'product_brand': "Brand",
         'product_description': "Description",
         'product_size': "Size",
-        'product_weighed': "Sold by Weight",
-        'department_id': "Department ID",
+        'department_name': "Department",
         'order_uom': "Order UOM",
         'status_code': "Status",
     }
@@ -1035,7 +727,6 @@ class OrderItemView(MasterView):
         # 'customer_name',
         'sequence',
         'product_id',
-        'local_product',
         'pending_product',
         'product_scancode',
         'product_brand',
@@ -1115,46 +806,27 @@ class OrderItemView(MasterView):
         enum = self.app.enum
         return enum.ORDER_ITEM_STATUS[value]
 
-    def get_instance_title(self, item):
-        """ """
-        enum = self.app.enum
-        title = str(item)
-        status = enum.ORDER_ITEM_STATUS[item.status_code]
-        return f"({status}) {title}"
-
     def configure_form(self, f):
         """ """
         super().configure_form(f)
         enum = self.app.enum
-        item = f.model_instance
 
         # order
         f.set_node('order', OrderRef(self.request))
 
-        # local_product
-        f.set_node('local_product', LocalProductRef(self.request))
-
         # pending_product
-        if item.product_id or item.local_product:
-            f.remove('pending_product')
-        else:
-            f.set_node('pending_product', PendingProductRef(self.request))
+        f.set_node('pending_product', PendingProductRef(self.request))
 
         # order_qty
         f.set_node('order_qty', WuttaQuantity(self.request))
 
         # order_uom
-        f.set_node('order_uom', WuttaDictEnum(self.request, enum.ORDER_UOM))
+        # TODO
+        #f.set_node('order_uom', WuttaEnum(self.request, enum.OrderUOM))
 
         # case_size
         f.set_node('case_size', WuttaQuantity(self.request))
 
-        # unit_cost
-        f.set_node('unit_cost', WuttaMoney(self.request, scale=4))
-
-        # unit_price_reg
-        f.set_node('unit_price_reg', WuttaMoney(self.request))
-
         # unit_price_quoted
         f.set_node('unit_price_quoted', WuttaMoney(self.request))
 
@@ -1164,21 +836,18 @@ class OrderItemView(MasterView):
         # total_price
         f.set_node('total_price', WuttaMoney(self.request))
 
-        # status
-        f.set_node('status_code', WuttaDictEnum(self.request, enum.ORDER_ITEM_STATUS))
-
         # paid_amount
         f.set_node('paid_amount', WuttaMoney(self.request))
 
     def get_xref_buttons(self, item):
         """ """
         buttons = super().get_xref_buttons(item)
+        model = self.app.model
 
         if self.request.has_perm('orders.view'):
             url = self.request.route_url('orders.view', uuid=item.order_uuid)
             buttons.append(
-                self.make_button("View the Order", url=url,
-                                 primary=True, icon_left='eye'))
+                self.make_button("View the Order", primary=True, icon_left='eye', url=url))
 
         return buttons
 
diff --git a/src/sideshow/web/views/products.py b/src/sideshow/web/views/products.py
index 98341b7..90b94ab 100644
--- a/src/sideshow/web/views/products.py
+++ b/src/sideshow/web/views/products.py
@@ -25,194 +25,9 @@ Views for Products
 """
 
 from wuttaweb.views import MasterView
-from wuttaweb.forms.schema import UserRef, WuttaEnum, WuttaMoney, WuttaQuantity
+from wuttaweb.forms.schema import UserRef, WuttaEnum, WuttaMoney
 
-from sideshow.db.model import LocalProduct, PendingProduct
-
-
-class LocalProductView(MasterView):
-    """
-    Master view for :class:`~sideshow.db.model.products.LocalProduct`;
-    route prefix is ``local_products``.
-
-    Notable URLs provided by this class:
-
-    * ``/local/products/``
-    * ``/local/products/new``
-    * ``/local/products/XXX``
-    * ``/local/products/XXX/edit``
-    * ``/local/products/XXX/delete``
-    """
-    model_class = LocalProduct
-    model_title = "Local Product"
-    route_prefix = 'local_products'
-    url_prefix = '/local/products'
-
-    labels = {
-        'external_id': "External ID",
-        'department_id': "Department ID",
-    }
-
-    grid_columns = [
-        'scancode',
-        'brand_name',
-        'description',
-        'size',
-        'department_name',
-        'special_order',
-        'case_size',
-        'unit_cost',
-        'unit_price_reg',
-    ]
-
-    sort_defaults = 'scancode'
-
-    form_fields = [
-        'external_id',
-        'scancode',
-        'brand_name',
-        'description',
-        'size',
-        'department_id',
-        'department_name',
-        'special_order',
-        'vendor_name',
-        'vendor_item_code',
-        'case_size',
-        'unit_cost',
-        'unit_price_reg',
-        'notes',
-        'orders',
-        'new_order_batches',
-    ]
-
-    def configure_grid(self, g):
-        """ """
-        super().configure_grid(g)
-
-        # unit_cost
-        g.set_renderer('unit_cost', 'currency', scale=4)
-
-        # unit_price_reg
-        g.set_label('unit_price_reg', "Reg. Price", column_only=True)
-        g.set_renderer('unit_price_reg', 'currency')
-
-        # links
-        g.set_link('scancode')
-        g.set_link('brand_name')
-        g.set_link('description')
-        g.set_link('size')
-
-    def configure_form(self, f):
-        """ """
-        super().configure_form(f)
-        enum = self.app.enum
-        product = f.model_instance
-
-        # external_id
-        if self.creating:
-            f.remove('external_id')
-        else:
-            f.set_readonly('external_id')
-
-        # TODO: should not have to explicitly mark these nodes
-        # as required=False.. i guess i do for now b/c i am
-        # totally overriding the node from colanderlachemy
-
-        # case_size
-        f.set_node('case_size', WuttaQuantity(self.request))
-        f.set_required('case_size', False)
-
-        # unit_cost
-        f.set_node('unit_cost', WuttaMoney(self.request, scale=4))
-        f.set_required('unit_cost', False)
-
-        # unit_price_reg
-        f.set_node('unit_price_reg', WuttaMoney(self.request))
-        f.set_required('unit_price_reg', False)
-
-        # notes
-        f.set_widget('notes', 'notes')
-
-        # orders
-        if self.creating or self.editing:
-            f.remove('orders')
-        else:
-            f.set_grid('orders', self.make_orders_grid(product))
-
-        # new_order_batches
-        if self.creating or self.editing:
-            f.remove('new_order_batches')
-        else:
-            f.set_grid('new_order_batches', self.make_new_order_batches_grid(product))
-
-    def make_orders_grid(self, product):
-        """
-        Make and return the grid for the Orders field.
-        """
-        model = self.app.model
-        route_prefix = self.get_route_prefix()
-
-        orders = set([item.order for item in product.order_items])
-        orders = sorted(orders, key=lambda order: order.order_id)
-
-        grid = self.make_grid(key=f'{route_prefix}.view.orders',
-                              model_class=model.Order,
-                              data=orders,
-                              columns=[
-                                  'order_id',
-                                  'total_price',
-                                  'created',
-                                  'created_by',
-                              ],
-                              labels={
-                                  'order_id': "Order ID",
-                              },
-                              renderers={
-                                  'total_price': 'currency',
-                              })
-
-        if self.request.has_perm('orders.view'):
-            url = lambda order, i: self.request.route_url('orders.view', uuid=order.uuid)
-            grid.add_action('view', icon='eye', url=url)
-            grid.set_link('order_id')
-
-        return grid
-
-    def make_new_order_batches_grid(self, product):
-        """
-        Make and return the grid for the New Order Batches field.
-        """
-        model = self.app.model
-        route_prefix = self.get_route_prefix()
-
-        batches = set([row.batch for row in product.new_order_batch_rows])
-        batches = sorted(batches, key=lambda batch: batch.id)
-
-        grid = self.make_grid(key=f'{route_prefix}.view.new_order_batches',
-                              model_class=model.NewOrderBatch,
-                              data=batches,
-                              columns=[
-                                  'id',
-                                  'total_price',
-                                  'created',
-                                  'created_by',
-                                  'executed',
-                              ],
-                              labels={
-                                  'id': "Batch ID",
-                                  'status_code': "Status",
-                              },
-                              renderers={
-                                  'id': 'batch_id',
-                              })
-
-        if self.request.has_perm('neworder_batches.view'):
-            url = lambda batch, i: self.request.route_url('neworder_batches.view', uuid=batch.uuid)
-            grid.add_action('view', icon='eye', url=url)
-            grid.set_link('id')
-
-        return grid
+from sideshow.db.model import PendingProduct
 
 
 class PendingProductView(MasterView):
@@ -434,9 +249,6 @@ class PendingProductView(MasterView):
 def defaults(config, **kwargs):
     base = globals()
 
-    LocalProductView = kwargs.get('LocalProductView', base['LocalProductView'])
-    LocalProductView.defaults(config)
-
     PendingProductView = kwargs.get('PendingProductView', base['PendingProductView'])
     PendingProductView.defaults(config)
 
diff --git a/tests/batch/test_neworder.py b/tests/batch/test_neworder.py
index 719efcb..66e625e 100644
--- a/tests/batch/test_neworder.py
+++ b/tests/batch/test_neworder.py
@@ -1,8 +1,6 @@
 # -*- coding: utf-8; -*-
 
-import datetime
 import decimal
-from unittest.mock import patch
 
 from wuttjamaican.testing import DataTestCase
 
@@ -20,133 +18,61 @@ class TestNewOrderBatchHandler(DataTestCase):
     def make_handler(self):
         return mod.NewOrderBatchHandler(self.config)
 
-    def tets_use_local_customers(self):
-        handler = self.make_handler()
-
-        # true by default
-        self.assertTrue(handler.use_local_customers())
-
-        # config can disable
-        config.setdefault('sideshow.orders.use_local_customers', 'false')
-        self.assertFalse(handler.use_local_customers())
-
-    def tets_use_local_products(self):
-        handler = self.make_handler()
-
-        # true by default
-        self.assertTrue(handler.use_local_products())
-
-        # config can disable
-        config.setdefault('sideshow.orders.use_local_products', 'false')
-        self.assertFalse(handler.use_local_products())
-
-    def tets_allow_unknown_products(self):
-        handler = self.make_handler()
-
-        # true by default
-        self.assertTrue(handler.allow_unknown_products())
-
-        # config can disable
-        config.setdefault('sideshow.orders.allow_unknown_products', 'false')
-        self.assertFalse(handler.allow_unknown_products())
-
-    def test_set_customer(self):
+    def test_set_pending_customer(self):
         model = self.app.model
         handler = self.make_handler()
 
         user = model.User(username='barney')
         self.session.add(user)
 
-        # customer starts blank
-        batch = handler.make_batch(self.session, created_by=user)
-        self.session.add(batch)
-        self.session.flush()
-        self.assertIsNone(batch.customer_id)
-        self.assertIsNone(batch.local_customer)
+        batch = handler.make_batch(self.session, created_by=user, customer_id=42)
+        self.assertEqual(batch.customer_id, 42)
         self.assertIsNone(batch.pending_customer)
         self.assertIsNone(batch.customer_name)
         self.assertIsNone(batch.phone_number)
         self.assertIsNone(batch.email_address)
 
-        # pending, typical (nb. full name is automatic)
-        handler.set_customer(batch, {
+        # auto full_name
+        handler.set_pending_customer(batch, {
             'first_name': "Fred",
             'last_name': "Flintstone",
             'phone_number': '555-1234',
             'email_address': 'fred@mailinator.com',
         })
         self.assertIsNone(batch.customer_id)
-        self.assertIsNone(batch.local_customer)
         self.assertIsInstance(batch.pending_customer, model.PendingCustomer)
         customer = batch.pending_customer
+        self.assertEqual(customer.full_name, "Fred Flintstone")
         self.assertEqual(customer.first_name, "Fred")
         self.assertEqual(customer.last_name, "Flintstone")
-        self.assertEqual(customer.full_name, "Fred Flintstone")
         self.assertEqual(customer.phone_number, '555-1234')
         self.assertEqual(customer.email_address, 'fred@mailinator.com')
         self.assertEqual(batch.customer_name, "Fred Flintstone")
         self.assertEqual(batch.phone_number, '555-1234')
         self.assertEqual(batch.email_address, 'fred@mailinator.com')
 
-        # pending, minimal
-        last_customer = customer # save ref to prev record
-        handler.set_customer(batch, {'full_name': "Wilma Flintstone"})
+        # explicit full_name
+        batch = handler.make_batch(self.session, created_by=user, customer_id=42)
+        handler.set_pending_customer(batch, {
+            'full_name': "Freddy Flintstone",
+            'first_name': "Fred",
+            'last_name': "Flintstone",
+            'phone_number': '555-1234',
+            'email_address': 'fred@mailinator.com',
+        })
         self.assertIsNone(batch.customer_id)
-        self.assertIsNone(batch.local_customer)
         self.assertIsInstance(batch.pending_customer, model.PendingCustomer)
         customer = batch.pending_customer
-        self.assertIs(customer, last_customer)
-        self.assertEqual(customer.full_name, "Wilma Flintstone")
-        self.assertIsNone(customer.first_name)
-        self.assertIsNone(customer.last_name)
-        self.assertIsNone(customer.phone_number)
-        self.assertIsNone(customer.email_address)
-        self.assertEqual(batch.customer_name, "Wilma Flintstone")
-        self.assertIsNone(batch.phone_number)
-        self.assertIsNone(batch.email_address)
+        self.assertEqual(customer.full_name, "Freddy Flintstone")
+        self.assertEqual(customer.first_name, "Fred")
+        self.assertEqual(customer.last_name, "Flintstone")
+        self.assertEqual(customer.phone_number, '555-1234')
+        self.assertEqual(customer.email_address, 'fred@mailinator.com')
+        self.assertEqual(batch.customer_name, "Freddy Flintstone")
+        self.assertEqual(batch.phone_number, '555-1234')
+        self.assertEqual(batch.email_address, 'fred@mailinator.com')
 
-        # local customer
-        local = model.LocalCustomer(full_name="Bam Bam",
-                                    first_name="Bam", last_name="Bam",
-                                    phone_number='555-4321')
-        self.session.add(local)
-        self.session.flush()
-        handler.set_customer(batch, local.uuid.hex)
-        self.session.flush()
-        self.assertIsNone(batch.customer_id)
-        # nb. pending customer does not get removed
-        self.assertIsInstance(batch.pending_customer, model.PendingCustomer)
-        self.assertIsInstance(batch.local_customer, model.LocalCustomer)
-        customer = batch.local_customer
-        self.assertEqual(customer.full_name, "Bam Bam")
-        self.assertEqual(customer.first_name, "Bam")
-        self.assertEqual(customer.last_name, "Bam")
-        self.assertEqual(customer.phone_number, '555-4321')
-        self.assertIsNone(customer.email_address)
-        self.assertEqual(batch.customer_name, "Bam Bam")
-        self.assertEqual(batch.phone_number, '555-4321')
-        self.assertIsNone(batch.email_address)
-
-        # local customer, not found
-        mock_uuid = self.app.make_true_uuid()
-        self.assertRaises(ValueError, handler.set_customer, batch, mock_uuid.hex)
-
-        # external lookup not implemented
-        self.config.setdefault('sideshow.orders.use_local_customers', 'false')
-        self.assertRaises(NotImplementedError, handler.set_customer, batch, '42')
-
-        # null
-        handler.set_customer(batch, None)
-        self.session.flush()
-        self.assertIsNone(batch.customer_id)
-        # nb. pending customer does not get removed
-        self.assertIsInstance(batch.pending_customer, model.PendingCustomer)
-        self.assertIsNone(batch.local_customer)
-        self.assertIsNone(batch.customer_name)
-        self.assertIsNone(batch.phone_number)
-        self.assertIsNone(batch.email_address)
-
-    def test_add_item(self):
+    def test_add_pending_product(self):
         model = self.app.model
         enum = self.app.enum
         handler = self.make_handler()
@@ -158,115 +84,42 @@ class TestNewOrderBatchHandler(DataTestCase):
         self.session.add(batch)
         self.assertEqual(len(batch.rows), 0)
 
-        # pending, typical
         kw = dict(
-            scancode='07430500001',
+            scancode='07430500132',
             brand_name='Bragg',
             description='Vinegar',
-            size='1oz',
+            size='32oz',
             case_size=12,
-            unit_cost=decimal.Decimal('1.99'),
-            unit_price_reg=decimal.Decimal('2.99'),
+            unit_cost=decimal.Decimal('3.99'),
+            unit_price_reg=decimal.Decimal('5.99'),
+            created_by=user,
         )
-        row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_UNIT)
-        # nb. this is the first row in batch
+        row = handler.add_pending_product(batch, kw, 1, enum.ORDER_UOM_UNIT)
         self.assertEqual(len(batch.rows), 1)
         self.assertIs(batch.rows[0], row)
-        self.assertIsNone(row.product_id)
-        self.assertIsNone(row.local_product)
-        product = row.pending_product
-        self.assertIsInstance(product, model.PendingProduct)
-        self.assertEqual(product.scancode, '07430500001')
-        self.assertEqual(product.brand_name, 'Bragg')
-        self.assertEqual(product.description, 'Vinegar')
-        self.assertEqual(product.size, '1oz')
-        self.assertEqual(product.case_size, 12)
-        self.assertEqual(product.unit_cost, decimal.Decimal('1.99'))
-        self.assertEqual(product.unit_price_reg, decimal.Decimal('2.99'))
-        self.assertEqual(row.product_scancode, '07430500001')
+
+        self.assertEqual(row.product_scancode, '07430500132')
         self.assertEqual(row.product_brand, 'Bragg')
         self.assertEqual(row.product_description, 'Vinegar')
-        self.assertEqual(row.product_size, '1oz')
+        self.assertEqual(row.product_size, '32oz')
         self.assertEqual(row.case_size, 12)
-        self.assertEqual(row.unit_cost, decimal.Decimal('1.99'))
-        self.assertEqual(row.unit_price_reg, decimal.Decimal('2.99'))
-        self.assertEqual(row.unit_price_quoted, decimal.Decimal('2.99'))
-        self.assertEqual(row.case_price_quoted, decimal.Decimal('35.88'))
-        self.assertEqual(row.total_price, decimal.Decimal('2.99'))
+        self.assertEqual(row.unit_cost, decimal.Decimal('3.99'))
+        self.assertEqual(row.unit_price_reg, decimal.Decimal('5.99'))
+        self.assertEqual(row.unit_price_quoted, decimal.Decimal('5.99'))
+        self.assertEqual(row.case_price_quoted, decimal.Decimal('71.88'))
 
-        # pending, minimal
-        row = handler.add_item(batch, {'description': "Tangerines"}, 1, enum.ORDER_UOM_UNIT)
-        self.assertIsNone(row.product_id)
-        self.assertIsNone(row.local_product)
         product = row.pending_product
         self.assertIsInstance(product, model.PendingProduct)
-        self.assertIsNone(product.scancode)
-        self.assertIsNone(product.brand_name)
-        self.assertEqual(product.description, 'Tangerines')
-        self.assertIsNone(product.size)
-        self.assertIsNone(product.case_size)
-        self.assertIsNone(product.unit_cost)
-        self.assertIsNone(product.unit_price_reg)
-        self.assertIsNone(row.product_scancode)
-        self.assertIsNone(row.product_brand)
-        self.assertEqual(row.product_description, 'Tangerines')
-        self.assertIsNone(row.product_size)
-        self.assertIsNone(row.case_size)
-        self.assertIsNone(row.unit_cost)
-        self.assertIsNone(row.unit_price_reg)
-        self.assertIsNone(row.unit_price_quoted)
-        self.assertIsNone(row.case_price_quoted)
-        self.assertIsNone(row.total_price)
-
-        # error if unknown products not allowed
-        self.config.setdefault('sideshow.orders.allow_unknown_products', 'false')
-        self.assertRaises(TypeError, handler.add_item, batch, kw, 1, enum.ORDER_UOM_UNIT)
-
-        # local product
-        local = model.LocalProduct(scancode='07430500002',
-                                   description='Vinegar',
-                                   size='2oz',
-                                   unit_price_reg=2.99,
-                                   case_size=12)
-        self.session.add(local)
-        self.session.flush()
-        row = handler.add_item(batch, local.uuid.hex, 1, enum.ORDER_UOM_CASE)
-        self.session.flush()
-        self.session.refresh(row)
-        self.session.refresh(local)
-        self.assertIsNone(row.product_id)
-        self.assertIsNone(row.pending_product)
-        product = row.local_product
-        self.assertIsInstance(product, model.LocalProduct)
-        self.assertEqual(product.scancode, '07430500002')
-        self.assertIsNone(product.brand_name)
+        self.assertEqual(product.scancode, '07430500132')
+        self.assertEqual(product.brand_name, 'Bragg')
         self.assertEqual(product.description, 'Vinegar')
-        self.assertEqual(product.size, '2oz')
+        self.assertEqual(product.size, '32oz')
         self.assertEqual(product.case_size, 12)
-        self.assertIsNone(product.unit_cost)
-        self.assertEqual(product.unit_price_reg, decimal.Decimal('2.99'))
-        self.assertEqual(row.product_scancode, '07430500002')
-        self.assertIsNone(row.product_brand)
-        self.assertEqual(row.product_description, 'Vinegar')
-        self.assertEqual(row.product_size, '2oz')
-        self.assertEqual(row.case_size, 12)
-        self.assertIsNone(row.unit_cost)
-        self.assertEqual(row.unit_price_reg, decimal.Decimal('2.99'))
-        self.assertEqual(row.unit_price_quoted, decimal.Decimal('2.99'))
-        self.assertEqual(row.case_price_quoted, decimal.Decimal('35.88'))
-        self.assertEqual(row.total_price, decimal.Decimal('35.88'))
+        self.assertEqual(product.unit_cost, decimal.Decimal('3.99'))
+        self.assertEqual(product.unit_price_reg, decimal.Decimal('5.99'))
+        self.assertIs(product.created_by, user)
 
-        # local product, not found
-        mock_uuid = self.app.make_true_uuid()
-        self.assertRaises(ValueError, handler.add_item,
-                          batch, mock_uuid.hex, 1, enum.ORDER_UOM_CASE)
-
-        # external lookup not implemented
-        self.config.setdefault('sideshow.orders.use_local_products', 'false')
-        self.assertRaises(NotImplementedError, handler.add_item,
-                          batch, '42', 1, enum.ORDER_UOM_CASE)
-
-    def test_update_item(self):
+    def test_set_pending_product(self):
         model = self.app.model
         enum = self.app.enum
         handler = self.make_handler()
@@ -278,170 +131,84 @@ class TestNewOrderBatchHandler(DataTestCase):
         self.session.add(batch)
         self.assertEqual(len(batch.rows), 0)
 
-        # start with typical pending product
-        kw = dict(
-            scancode='07430500001',
-            brand_name='Bragg',
-            description='Vinegar',
-            size='1oz',
-            case_size=12,
-            unit_cost=decimal.Decimal('1.99'),
-            unit_price_reg=decimal.Decimal('2.99'),
-        )
-        row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_CASE)
-        self.assertIsNone(row.product_id)
-        self.assertIsNone(row.local_product)
-        product = row.pending_product
-        self.assertIsInstance(product, model.PendingProduct)
-        self.assertEqual(product.scancode, '07430500001')
-        self.assertEqual(product.brand_name, 'Bragg')
-        self.assertEqual(product.description, 'Vinegar')
-        self.assertEqual(product.size, '1oz')
-        self.assertEqual(product.case_size, 12)
-        self.assertEqual(product.unit_cost, decimal.Decimal('1.99'))
-        self.assertEqual(product.unit_price_reg, decimal.Decimal('2.99'))
-        self.assertEqual(row.product_scancode, '07430500001')
-        self.assertEqual(row.product_brand, 'Bragg')
-        self.assertEqual(row.product_description, 'Vinegar')
-        self.assertEqual(row.product_size, '1oz')
-        self.assertEqual(row.case_size, 12)
-        self.assertEqual(row.unit_cost, decimal.Decimal('1.99'))
-        self.assertEqual(row.unit_price_reg, decimal.Decimal('2.99'))
-        self.assertEqual(row.unit_price_quoted, decimal.Decimal('2.99'))
-        self.assertEqual(row.case_price_quoted, decimal.Decimal('35.88'))
-        self.assertEqual(row.order_qty, 1)
-        self.assertEqual(row.order_uom, enum.ORDER_UOM_CASE)
-        self.assertEqual(row.total_price, decimal.Decimal('35.88'))
-
-        # set pending, minimal
-        handler.update_item(row, {'description': 'Vinegar'}, 1, enum.ORDER_UOM_UNIT)
-        # self.session.flush()
-        self.assertIsNone(row.product_id)
-        self.assertIsNone(row.local_product)
-        product = row.pending_product
-        self.assertIsInstance(product, model.PendingProduct)
-        self.assertIsNone(product.scancode)
-        self.assertIsNone(product.brand_name)
-        self.assertEqual(product.description, 'Vinegar')
-        self.assertIsNone(product.size)
-        self.assertIsNone(product.case_size)
-        self.assertIsNone(product.unit_cost)
-        self.assertIsNone(product.unit_price_reg)
+        # start with mock product_id
+        row = handler.make_row(product_id=42, order_qty=1, order_uom=enum.ORDER_UOM_UNIT)
+        handler.add_row(batch, row)
+        self.session.flush()
+        self.assertEqual(row.product_id, 42)
+        self.assertIsNone(row.pending_product)
         self.assertIsNone(row.product_scancode)
         self.assertIsNone(row.product_brand)
-        self.assertEqual(row.product_description, 'Vinegar')
+        self.assertIsNone(row.product_description)
         self.assertIsNone(row.product_size)
         self.assertIsNone(row.case_size)
         self.assertIsNone(row.unit_cost)
         self.assertIsNone(row.unit_price_reg)
         self.assertIsNone(row.unit_price_quoted)
-        self.assertIsNone(row.case_price_quoted)
-        self.assertEqual(row.order_qty, 1)
-        self.assertEqual(row.order_uom, enum.ORDER_UOM_UNIT)
-        self.assertIsNone(row.total_price)
 
-        # start over, new row w/ local product
-        local = model.LocalProduct(scancode='07430500002',
-                                   description='Vinegar',
-                                   size='2oz',
-                                   unit_price_reg=3.99,
-                                   case_size=12)
-        self.session.add(local)
+        # set pending, which clears product_id
+        handler.set_pending_product(row, dict(
+            scancode='07430500132',
+            brand_name='Bragg',
+            description='Vinegar',
+            size='32oz',
+            case_size=12,
+            unit_cost=decimal.Decimal('3.99'),
+            unit_price_reg=decimal.Decimal('5.99'),
+            created_by=user,
+        ))
         self.session.flush()
-        row = handler.add_item(batch, local.uuid.hex, 1, enum.ORDER_UOM_CASE)
-        self.session.flush()
-        self.session.refresh(row)
-        self.session.refresh(local)
         self.assertIsNone(row.product_id)
-        self.assertIsNone(row.pending_product)
-        product = row.local_product
-        self.assertIsInstance(product, model.LocalProduct)
-        self.assertEqual(product.scancode, '07430500002')
-        self.assertIsNone(product.brand_name)
-        self.assertEqual(product.description, 'Vinegar')
-        self.assertEqual(product.size, '2oz')
-        self.assertEqual(product.case_size, 12)
-        self.assertIsNone(product.unit_cost)
-        self.assertEqual(product.unit_price_reg, decimal.Decimal('3.99'))
-        self.assertEqual(row.product_scancode, '07430500002')
-        self.assertIsNone(row.product_brand)
-        self.assertEqual(row.product_description, 'Vinegar')
-        self.assertEqual(row.product_size, '2oz')
-        self.assertEqual(row.case_size, 12)
-        self.assertIsNone(row.unit_cost)
-        self.assertEqual(row.unit_price_reg, decimal.Decimal('3.99'))
-        self.assertEqual(row.unit_price_quoted, decimal.Decimal('3.99'))
-        self.assertEqual(row.case_price_quoted, decimal.Decimal('47.88'))
-        self.assertEqual(row.order_qty, 1)
-        self.assertEqual(row.order_uom, enum.ORDER_UOM_CASE)
-        self.assertEqual(row.total_price, decimal.Decimal('47.88'))
-
-        # update w/ pending product
-        handler.update_item(row, kw, 2, enum.ORDER_UOM_CASE)
-        self.assertIsNone(row.product_id)
-        self.assertIsNone(row.local_product)
-        product = row.pending_product
-        self.assertIsInstance(product, model.PendingProduct)
-        self.assertEqual(product.scancode, '07430500001')
-        self.assertEqual(product.brand_name, 'Bragg')
-        self.assertEqual(product.description, 'Vinegar')
-        self.assertEqual(product.size, '1oz')
-        self.assertEqual(product.case_size, 12)
-        self.assertEqual(product.unit_cost, decimal.Decimal('1.99'))
-        self.assertEqual(product.unit_price_reg, decimal.Decimal('2.99'))
-        self.assertEqual(row.product_scancode, '07430500001')
+        self.assertIsInstance(row.pending_product, model.PendingProduct)
+        self.assertEqual(row.product_scancode, '07430500132')
         self.assertEqual(row.product_brand, 'Bragg')
         self.assertEqual(row.product_description, 'Vinegar')
-        self.assertEqual(row.product_size, '1oz')
+        self.assertEqual(row.product_size, '32oz')
         self.assertEqual(row.case_size, 12)
-        self.assertEqual(row.unit_cost, decimal.Decimal('1.99'))
-        self.assertEqual(row.unit_price_reg, decimal.Decimal('2.99'))
-        self.assertEqual(row.unit_price_quoted, decimal.Decimal('2.99'))
-        self.assertEqual(row.case_price_quoted, decimal.Decimal('35.88'))
-        self.assertEqual(row.order_qty, 2)
-        self.assertEqual(row.order_uom, enum.ORDER_UOM_CASE)
-        self.assertEqual(row.total_price, decimal.Decimal('71.76'))
-
-        # update w/ pending, error if not allowed
-        self.config.setdefault('sideshow.orders.allow_unknown_products', 'false')
-        self.assertRaises(TypeError, handler.update_item, row, kw, 1, enum.ORDER_UOM_UNIT)
-
-        # update w/ local product
-        handler.update_item(row, local.uuid.hex, 1, enum.ORDER_UOM_CASE)
-        self.assertIsNone(row.product_id)
-        # nb. pending remains intact here
-        self.assertIsNotNone(row.pending_product)
-        product = row.local_product
-        self.assertIsInstance(product, model.LocalProduct)
-        self.assertEqual(product.scancode, '07430500002')
-        self.assertIsNone(product.brand_name)
+        self.assertEqual(row.unit_cost, decimal.Decimal('3.99'))
+        self.assertEqual(row.unit_price_reg, decimal.Decimal('5.99'))
+        self.assertEqual(row.unit_price_quoted, decimal.Decimal('5.99'))
+        self.assertEqual(row.case_price_quoted, decimal.Decimal('71.88'))
+        product = row.pending_product
+        self.assertIsInstance(product, model.PendingProduct)
+        self.assertEqual(product.scancode, '07430500132')
+        self.assertEqual(product.brand_name, 'Bragg')
         self.assertEqual(product.description, 'Vinegar')
-        self.assertEqual(product.size, '2oz')
+        self.assertEqual(product.size, '32oz')
         self.assertEqual(product.case_size, 12)
-        self.assertIsNone(product.unit_cost)
-        self.assertEqual(product.unit_price_reg, decimal.Decimal('3.99'))
-        self.assertEqual(row.product_scancode, '07430500002')
-        self.assertIsNone(row.product_brand)
+        self.assertEqual(product.unit_cost, decimal.Decimal('3.99'))
+        self.assertEqual(product.unit_price_reg, decimal.Decimal('5.99'))
+        self.assertIs(product.created_by, user)
+
+        # set again to update pending
+        handler.set_pending_product(row, dict(
+            scancode='07430500116',
+            size='16oz',
+            unit_cost=decimal.Decimal('2.19'),
+            unit_price_reg=decimal.Decimal('3.59'),
+        ))
+        self.session.flush()
+        self.assertIsNone(row.product_id)
+        self.assertIsInstance(row.pending_product, model.PendingProduct)
+        self.assertEqual(row.product_scancode, '07430500116')
+        self.assertEqual(row.product_brand, 'Bragg')
         self.assertEqual(row.product_description, 'Vinegar')
-        self.assertEqual(row.product_size, '2oz')
+        self.assertEqual(row.product_size, '16oz')
         self.assertEqual(row.case_size, 12)
-        self.assertIsNone(row.unit_cost)
-        self.assertEqual(row.unit_price_reg, decimal.Decimal('3.99'))
-        self.assertEqual(row.unit_price_quoted, decimal.Decimal('3.99'))
-        self.assertEqual(row.case_price_quoted, decimal.Decimal('47.88'))
-        self.assertEqual(row.order_qty, 1)
-        self.assertEqual(row.order_uom, enum.ORDER_UOM_CASE)
-        self.assertEqual(row.total_price, decimal.Decimal('47.88'))
-
-        # update w/ local, not found
-        mock_uuid = self.app.make_true_uuid()
-        self.assertRaises(ValueError, handler.update_item,
-                          batch, mock_uuid.hex, 1, enum.ORDER_UOM_CASE)
-
-        # external lookup not implemented
-        self.config.setdefault('sideshow.orders.use_local_products', 'false')
-        self.assertRaises(NotImplementedError, handler.update_item,
-                          row, '42', 1, enum.ORDER_UOM_CASE)
+        self.assertEqual(row.unit_cost, decimal.Decimal('2.19'))
+        self.assertEqual(row.unit_price_reg, decimal.Decimal('3.59'))
+        self.assertEqual(row.unit_price_quoted, decimal.Decimal('3.59'))
+        self.assertEqual(row.case_price_quoted, decimal.Decimal('43.08'))
+        product = row.pending_product
+        self.assertIsInstance(product, model.PendingProduct)
+        self.assertEqual(product.scancode, '07430500116')
+        self.assertEqual(product.brand_name, 'Bragg')
+        self.assertEqual(product.description, 'Vinegar')
+        self.assertEqual(product.size, '16oz')
+        self.assertEqual(product.case_size, 12)
+        self.assertEqual(product.unit_cost, decimal.Decimal('2.19'))
+        self.assertEqual(product.unit_price_reg, decimal.Decimal('3.59'))
+        self.assertIs(product.created_by, user)
 
     def test_refresh_row(self):
         model = self.app.model
@@ -543,37 +310,6 @@ class TestNewOrderBatchHandler(DataTestCase):
         self.assertEqual(row.case_price_quoted, decimal.Decimal('71.88'))
         self.assertEqual(row.total_price, decimal.Decimal('143.76'))
 
-        # refreshed from pending product (sale price)
-        product = model.PendingProduct(scancode='07430500132',
-                                       brand_name='Bragg',
-                                       description='Vinegar',
-                                       size='32oz',
-                                       case_size=12,
-                                       unit_cost=decimal.Decimal('3.99'),
-                                       unit_price_reg=decimal.Decimal('5.99'),
-                                       created_by=user,
-                                       status=enum.PendingProductStatus.PENDING)
-        row = handler.make_row(pending_product=product, order_qty=2, order_uom=enum.ORDER_UOM_CASE,
-                               unit_price_sale=decimal.Decimal('5.19'),
-                               sale_ends=datetime.datetime(2099, 1, 1))
-        self.assertIsNone(row.status_code)
-        handler.add_row(batch, row)
-        self.assertEqual(row.status_code, row.STATUS_OK)
-        self.assertIsNone(row.product_id)
-        self.assertIs(row.pending_product, product)
-        self.assertEqual(row.product_scancode, '07430500132')
-        self.assertEqual(row.product_brand, 'Bragg')
-        self.assertEqual(row.product_description, 'Vinegar')
-        self.assertEqual(row.product_size, '32oz')
-        self.assertEqual(row.case_size, 12)
-        self.assertEqual(row.unit_cost, decimal.Decimal('3.99'))
-        self.assertEqual(row.unit_price_reg, decimal.Decimal('5.99'))
-        self.assertEqual(row.unit_price_sale, decimal.Decimal('5.19'))
-        self.assertEqual(row.sale_ends, datetime.datetime(2099, 1, 1))
-        self.assertEqual(row.unit_price_quoted, decimal.Decimal('5.19'))
-        self.assertEqual(row.case_price_quoted, decimal.Decimal('62.28'))
-        self.assertEqual(row.total_price, decimal.Decimal('124.56'))
-
     def test_remove_row(self):
         model = self.app.model
         enum = self.app.enum
@@ -596,7 +332,7 @@ class TestNewOrderBatchHandler(DataTestCase):
             unit_price_reg=decimal.Decimal('5.99'),
             created_by=user,
         )
-        row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_CASE)
+        row = handler.add_pending_product(batch, kw, 1, enum.ORDER_UOM_CASE)
         self.session.add(row)
         self.session.flush()
         self.assertEqual(batch.row_count, 1)
@@ -632,70 +368,25 @@ class TestNewOrderBatchHandler(DataTestCase):
         self.assertNotIn(batch, self.session)
         self.assertEqual(self.session.query(model.PendingCustomer).count(), 0)
 
-        # make new pending customer, assigned to batch + order
-        customer = model.PendingCustomer(full_name="Wilma Flintstone",
+        # make new pending customer
+        customer = model.PendingCustomer(full_name="Fred Flintstone",
                                          status=enum.PendingCustomerStatus.PENDING,
                                          created_by=user)
         self.session.add(customer)
-        batch = handler.make_batch(self.session, created_by=user, pending_customer=customer)
-        self.session.add(batch)
-        order = model.Order(order_id=77, created_by=user, pending_customer=customer)
-        self.session.add(order)
-        self.session.flush()
 
-        # deleting batch will *not* delete pending customer
-        self.assertIn(batch, self.session)
-        self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
-        handler.do_delete(batch, user)
+        # make 2 batches with same pending customer
+        batch1 = handler.make_batch(self.session, created_by=user, pending_customer=customer)
+        batch2 = handler.make_batch(self.session, created_by=user, pending_customer=customer)
+        self.session.add(batch1)
+        self.session.add(batch2)
         self.session.commit()
-        self.assertNotIn(batch, self.session)
+
+        # deleting 1 will not delete pending customer
         self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
-
-        # make new pending product, associate w/ batch + order
-        batch = handler.make_batch(self.session, created_by=user)
-        self.session.add(batch)
-        handler.set_customer(batch, {'full_name': "Jack Black"})
-        row = handler.add_item(batch, dict(
-            scancode='07430500132',
-            brand_name='Bragg',
-            description='Vinegar',
-            size='32oz',
-            unit_price_reg=5.99,
-        ), 1, enum.ORDER_UOM_UNIT)
-        product = row.pending_product
-        order = model.Order(order_id=33, created_by=user)
-        item = model.OrderItem(pending_product=product, order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
-                               status_code=enum.ORDER_ITEM_STATUS_INITIATED)
-        order.items.append(item)
-        self.session.add(order)
-        self.session.flush()
-
-        # deleting batch will *not* delete pending product
-        self.assertIn(batch, self.session)
-        self.assertEqual(self.session.query(model.PendingProduct).count(), 1)
-        handler.do_delete(batch, user)
+        handler.do_delete(batch1, user)
         self.session.commit()
-        self.assertNotIn(batch, self.session)
         self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
-
-        # make another batch w/ same pending product
-        batch = handler.make_batch(self.session, created_by=user)
-        self.session.add(batch)
-        row = handler.make_row(pending_product=product,
-                               order_qty=1, order_uom=enum.ORDER_UOM_UNIT)
-        handler.add_row(batch, row)
-
-        # also delete the associated order
-        self.session.delete(order)
-        self.session.flush()
-
-        # deleting this batch *will* delete pending product
-        self.assertIn(batch, self.session)
-        self.assertEqual(self.session.query(model.PendingProduct).count(), 1)
-        handler.do_delete(batch, user)
-        self.session.commit()
-        self.assertNotIn(batch, self.session)
-        self.assertEqual(self.session.query(model.PendingProduct).count(), 0)
+        self.assertIs(batch2.pending_customer, customer)
 
     def test_get_effective_rows(self):
         model = self.app.model
@@ -746,12 +437,6 @@ class TestNewOrderBatchHandler(DataTestCase):
         self.assertEqual(reason, "Must assign the customer")
 
         batch.customer_id = 42
-        batch.customer_name = "Fred Flintstone"
-
-        reason = handler.why_not_execute(batch)
-        self.assertEqual(reason, "Customer phone number is required")
-
-        batch.phone_number = '555-1234'
 
         reason = handler.why_not_execute(batch)
         self.assertEqual(reason, "Must add at least one valid item")
@@ -766,206 +451,13 @@ class TestNewOrderBatchHandler(DataTestCase):
             unit_price_reg=decimal.Decimal('5.99'),
             created_by=user,
         )
-        row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_CASE)
+        row = handler.add_pending_product(batch, kw, 1, enum.ORDER_UOM_CASE)
         self.session.add(row)
         self.session.flush()
 
         reason = handler.why_not_execute(batch)
         self.assertIsNone(reason)
 
-    def test_make_local_customer(self):
-        model = self.app.model
-        enum = self.app.enum
-        handler = self.make_handler()
-
-        user = model.User(username='barney')
-        self.session.add(user)
-
-        # make a typical batch
-        batch = handler.make_batch(self.session, created_by=user)
-        self.session.add(batch)
-        handler.set_customer(batch, {'first_name': "John", 'last_name': "Doe",
-                                     'phone_number': '555-1234'})
-        row = handler.add_item(batch, dict(
-            scancode='07430500132',
-            brand_name='Bragg',
-            description='Vinegar',
-            size='32oz',
-            unit_price_reg=5.99,
-        ), 1, enum.ORDER_UOM_UNIT)
-        self.session.flush()
-
-        # making local customer removes pending customer
-        self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
-        self.assertEqual(self.session.query(model.LocalCustomer).count(), 0)
-        self.assertIsNotNone(batch.pending_customer)
-        self.assertIsNone(batch.local_customer)
-        handler.make_local_customer(batch)
-        self.assertEqual(self.session.query(model.PendingCustomer).count(), 0)
-        self.assertEqual(self.session.query(model.LocalCustomer).count(), 1)
-        self.assertIsNone(batch.pending_customer)
-        local = batch.local_customer
-        self.assertIsNotNone(local)
-        self.assertEqual(local.first_name, "John")
-        self.assertEqual(local.last_name, "Doe")
-        self.assertEqual(local.full_name, "John Doe")
-        self.assertEqual(local.phone_number, '555-1234')
-
-        # trying again does nothing
-        handler.make_local_customer(batch)
-        self.assertEqual(self.session.query(model.PendingCustomer).count(), 0)
-        self.assertEqual(self.session.query(model.LocalCustomer).count(), 1)
-        self.assertIsNone(batch.pending_customer)
-        local = batch.local_customer
-        self.assertIsNotNone(local)
-        self.assertEqual(local.first_name, "John")
-        self.assertEqual(local.last_name, "Doe")
-        self.assertEqual(local.full_name, "John Doe")
-        self.assertEqual(local.phone_number, '555-1234')
-
-        # make another typical batch
-        batch = handler.make_batch(self.session, created_by=user)
-        self.session.add(batch)
-        handler.set_customer(batch, {'first_name': "Chuck", 'last_name': "Norris",
-                                     'phone_number': '555-1234'})
-        row = handler.add_item(batch, dict(
-            scancode='07430500132',
-            brand_name='Bragg',
-            description='Vinegar',
-            size='32oz',
-            unit_price_reg=5.99,
-        ), 1, enum.ORDER_UOM_UNIT)
-        self.session.flush()
-
-        # should do nothing if local customers disabled
-        with patch.object(handler, 'use_local_customers', return_value=False):
-            self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
-            self.assertEqual(self.session.query(model.LocalCustomer).count(), 1)
-            self.assertIsNotNone(batch.pending_customer)
-            self.assertIsNone(batch.local_customer)
-            handler.make_local_customer(batch)
-            self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
-            self.assertEqual(self.session.query(model.LocalCustomer).count(), 1)
-            self.assertIsNotNone(batch.pending_customer)
-            self.assertIsNone(batch.local_customer)
-
-        # but things happen by default, since local customers enabled
-        self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
-        self.assertEqual(self.session.query(model.LocalCustomer).count(), 1)
-        self.assertIsNotNone(batch.pending_customer)
-        self.assertIsNone(batch.local_customer)
-        handler.make_local_customer(batch)
-        self.assertEqual(self.session.query(model.PendingCustomer).count(), 0)
-        self.assertEqual(self.session.query(model.LocalCustomer).count(), 2)
-        self.assertIsNone(batch.pending_customer)
-        local = batch.local_customer
-        self.assertIsNotNone(local)
-        self.assertEqual(local.first_name, "Chuck")
-        self.assertEqual(local.last_name, "Norris")
-        self.assertEqual(local.full_name, "Chuck Norris")
-        self.assertEqual(local.phone_number, '555-1234')
-
-    def test_make_local_products(self):
-        model = self.app.model
-        enum = self.app.enum
-        handler = self.make_handler()
-
-        user = model.User(username='barney')
-        self.session.add(user)
-
-        # make a batch w/ one each local + pending products
-        batch = handler.make_batch(self.session, created_by=user)
-        self.session.add(batch)
-        handler.set_customer(batch, {'full_name': "John Doe"})
-        local = model.LocalProduct(scancode='07430500116',
-                                   brand_name='Bragg',
-                                   description='Vinegar',
-                                   size='16oz',
-                                   unit_price_reg=3.59)
-        self.session.add(local)
-        self.session.flush()
-        row1 = handler.add_item(batch, local.uuid.hex, 1, enum.ORDER_UOM_UNIT)
-        row2 = handler.add_item(batch, dict(
-            scancode='07430500132',
-            brand_name='Bragg',
-            description='Vinegar',
-            size='32oz',
-            unit_price_reg=5.99,
-        ), 1, enum.ORDER_UOM_UNIT)
-        self.session.flush()
-
-        # making local product removes pending product
-        self.assertEqual(self.session.query(model.PendingProduct).count(), 1)
-        self.assertEqual(self.session.query(model.LocalProduct).count(), 1)
-        self.assertIsNotNone(row2.pending_product)
-        self.assertIsNone(row2.local_product)
-        handler.make_local_products(batch, batch.rows)
-        self.assertEqual(self.session.query(model.PendingProduct).count(), 0)
-        self.assertEqual(self.session.query(model.LocalProduct).count(), 2)
-        self.assertIsNone(row2.pending_product)
-        local = row2.local_product
-        self.assertIsNotNone(local)
-        self.assertEqual(local.scancode, '07430500132')
-        self.assertEqual(local.brand_name, 'Bragg')
-        self.assertEqual(local.description, 'Vinegar')
-        self.assertEqual(local.size, '32oz')
-        self.assertEqual(local.unit_price_reg, decimal.Decimal('5.99'))
-
-        # trying again does nothing
-        handler.make_local_products(batch, batch.rows)
-        self.assertEqual(self.session.query(model.PendingProduct).count(), 0)
-        self.assertEqual(self.session.query(model.LocalProduct).count(), 2)
-        self.assertIsNone(row2.pending_product)
-        local = row2.local_product
-        self.assertIsNotNone(local)
-        self.assertEqual(local.scancode, '07430500132')
-        self.assertEqual(local.brand_name, 'Bragg')
-        self.assertEqual(local.description, 'Vinegar')
-        self.assertEqual(local.size, '32oz')
-        self.assertEqual(local.unit_price_reg, decimal.Decimal('5.99'))
-
-        # make another typical batch
-        batch = handler.make_batch(self.session, created_by=user)
-        self.session.add(batch)
-        handler.set_customer(batch, {'full_name': "Chuck Norris"})
-        row = handler.add_item(batch, dict(
-            scancode='07430500164',
-            brand_name='Bragg',
-            description='Vinegar',
-            size='64oz',
-            unit_price_reg=9.99,
-        ), 1, enum.ORDER_UOM_UNIT)
-        self.session.flush()
-
-        # should do nothing if local products disabled
-        with patch.object(handler, 'use_local_products', return_value=False):
-            self.assertEqual(self.session.query(model.PendingProduct).count(), 1)
-            self.assertEqual(self.session.query(model.LocalProduct).count(), 2)
-            self.assertIsNotNone(row.pending_product)
-            self.assertIsNone(row.local_product)
-            handler.make_local_products(batch, batch.rows)
-            self.assertEqual(self.session.query(model.PendingProduct).count(), 1)
-            self.assertEqual(self.session.query(model.LocalProduct).count(), 2)
-            self.assertIsNotNone(row.pending_product)
-            self.assertIsNone(row.local_product)
-
-        # but things happen by default, since local products enabled
-        self.assertEqual(self.session.query(model.PendingProduct).count(), 1)
-        self.assertEqual(self.session.query(model.LocalProduct).count(), 2)
-        self.assertIsNotNone(row.pending_product)
-        self.assertIsNone(row.local_product)
-        handler.make_local_products(batch, batch.rows)
-        self.assertEqual(self.session.query(model.PendingProduct).count(), 0)
-        self.assertEqual(self.session.query(model.LocalProduct).count(), 3)
-        self.assertIsNone(row.pending_product)
-        local = row.local_product
-        self.assertIsNotNone(local)
-        self.assertEqual(local.scancode, '07430500164')
-        self.assertEqual(local.brand_name, 'Bragg')
-        self.assertEqual(local.description, 'Vinegar')
-        self.assertEqual(local.size, '64oz')
-        self.assertEqual(local.unit_price_reg, decimal.Decimal('9.99'))
-
     def test_make_new_order(self):
         model = self.app.model
         enum = self.app.enum
@@ -987,7 +479,7 @@ class TestNewOrderBatchHandler(DataTestCase):
             unit_price_reg=decimal.Decimal('5.99'),
             created_by=user,
         )
-        row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_CASE)
+        row = handler.add_pending_product(batch, kw, 1, enum.ORDER_UOM_CASE)
         self.session.add(row)
         self.session.flush()
 
@@ -1027,7 +519,7 @@ class TestNewOrderBatchHandler(DataTestCase):
             unit_price_reg=decimal.Decimal('5.99'),
             created_by=user,
         )
-        row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_CASE)
+        row = handler.add_pending_product(batch, kw, 1, enum.ORDER_UOM_CASE)
         self.session.add(row)
         self.session.flush()
 
diff --git a/tests/db/model/test_orders.py b/tests/db/model/test_orders.py
index 7169991..b0ad9f4 100644
--- a/tests/db/model/test_orders.py
+++ b/tests/db/model/test_orders.py
@@ -19,19 +19,6 @@ class TestOrder(DataTestCase):
 
 class TestOrderItem(DataTestCase):
 
-    def test_full_description(self):
-
-        item = mod.OrderItem()
-        self.assertEqual(item.full_description, "")
-
-        item = mod.OrderItem(product_description="Vinegar")
-        self.assertEqual(item.full_description, "Vinegar")
-
-        item = mod.OrderItem(product_brand='Bragg',
-                             product_description='Vinegar',
-                             product_size='32oz')
-        self.assertEqual(item.full_description, "Bragg Vinegar 32oz")
-
     def test_str(self):
 
         item = mod.OrderItem()
@@ -40,7 +27,8 @@ class TestOrderItem(DataTestCase):
         item = mod.OrderItem(product_description="Vinegar")
         self.assertEqual(str(item), "Vinegar")
 
-        item = mod.OrderItem(product_brand='Bragg',
-                             product_description='Vinegar',
-                             product_size='32oz')
+        product = PendingProduct(brand_name="Bragg",
+                                 description="Vinegar",
+                                 size="32oz")
+        item = mod.OrderItem(pending_product=product)
         self.assertEqual(str(item), "Bragg Vinegar 32oz")
diff --git a/tests/web/forms/test_schema.py b/tests/web/forms/test_schema.py
index 3dd838a..38ff106 100644
--- a/tests/web/forms/test_schema.py
+++ b/tests/web/forms/test_schema.py
@@ -32,31 +32,6 @@ class TestOrderRef(WebTestCase):
         self.assertIn(f'/orders/{order.uuid}', url)
 
 
-class TestLocalCustomerRef(WebTestCase):
-
-    def test_sort_query(self):
-        typ = mod.LocalCustomerRef(self.request, session=self.session)
-        query = typ.get_query()
-        self.assertIsInstance(query, orm.Query)
-        sorted_query = typ.sort_query(query)
-        self.assertIsInstance(sorted_query, orm.Query)
-        self.assertIsNot(sorted_query, query)
-
-    def test_get_object_url(self):
-        self.pyramid_config.add_route('local_customers.view', '/local/customers/{uuid}')
-        model = self.app.model
-        enum = self.app.enum
-
-        customer = model.LocalCustomer()
-        self.session.add(customer)
-        self.session.commit()
-
-        typ = mod.LocalCustomerRef(self.request, session=self.session)
-        url = typ.get_object_url(customer)
-        self.assertIsNotNone(url)
-        self.assertIn(f'/local/customers/{customer.uuid}', url)
-
-
 class TestPendingCustomerRef(WebTestCase):
 
     def test_sort_query(self):
@@ -85,31 +60,6 @@ class TestPendingCustomerRef(WebTestCase):
         self.assertIn(f'/pending/customers/{customer.uuid}', url)
 
 
-class TestLocalProductRef(WebTestCase):
-
-    def test_sort_query(self):
-        typ = mod.LocalProductRef(self.request, session=self.session)
-        query = typ.get_query()
-        self.assertIsInstance(query, orm.Query)
-        sorted_query = typ.sort_query(query)
-        self.assertIsInstance(sorted_query, orm.Query)
-        self.assertIsNot(sorted_query, query)
-
-    def test_get_object_url(self):
-        self.pyramid_config.add_route('local_products.view', '/local/products/{uuid}')
-        model = self.app.model
-        enum = self.app.enum
-
-        product = model.LocalProduct()
-        self.session.add(product)
-        self.session.commit()
-
-        typ = mod.LocalProductRef(self.request, session=self.session)
-        url = typ.get_object_url(product)
-        self.assertIsNotNone(url)
-        self.assertIn(f'/local/products/{product.uuid}', url)
-
-
 class TestPendingProductRef(WebTestCase):
 
     def test_sort_query(self):
diff --git a/tests/web/test_menus.py b/tests/web/test_menus.py
index 36fdc56..bff33cd 100644
--- a/tests/web/test_menus.py
+++ b/tests/web/test_menus.py
@@ -9,4 +9,4 @@ class TestSideshowMenuHandler(WebTestCase):
     def test_make_menus(self):
         handler = mod.SideshowMenuHandler(self.config)
         menus = handler.make_menus(self.request)
-        self.assertEqual(len(menus), 5)
+        self.assertEqual(len(menus), 4)
diff --git a/tests/web/views/test_customers.py b/tests/web/views/test_customers.py
index d68e48f..b8f1db1 100644
--- a/tests/web/views/test_customers.py
+++ b/tests/web/views/test_customers.py
@@ -16,114 +16,6 @@ class TestIncludeme(WebTestCase):
         mod.includeme(self.pyramid_config)
 
 
-class TestLocalCustomerView(WebTestCase):
-
-    def make_view(self):
-        return mod.LocalCustomerView(self.request)
-
-    def test_configure_grid(self):
-        model = self.app.model
-        view = self.make_view()
-        grid = view.make_grid(model_class=model.LocalCustomer)
-        self.assertNotIn('full_name', grid.linked_columns)
-        view.configure_grid(grid)
-        self.assertIn('full_name', grid.linked_columns)
-
-    def test_configure_form(self):
-        model = self.app.model
-        view = self.make_view()
-
-        # creating
-        with patch.object(view, 'creating', new=True):
-            form = view.make_form(model_class=model.LocalCustomer)
-            view.configure_form(form)
-            self.assertNotIn('external_id', form)
-            self.assertNotIn('full_name', form)
-            self.assertNotIn('orders', form)
-            self.assertNotIn('new_order_batches', form)
-
-        user = model.User(username='barney')
-        self.session.add(user)
-        customer = model.LocalCustomer()
-        self.session.add(customer)
-        self.session.commit()
-
-        # viewing
-        with patch.object(view, 'viewing', new=True):
-            form = view.make_form(model_instance=customer)
-            view.configure_form(form)
-            self.assertIn('external_id', form)
-            self.assertIn('full_name', form)
-            self.assertIn('orders', form)
-            self.assertIn('new_order_batches', form)
-
-    def test_make_orders_grid(self):
-        model = self.app.model
-        view = self.make_view()
-
-        user = model.User(username='barney')
-        self.session.add(user)
-        customer = model.LocalCustomer()
-        self.session.add(customer)
-        order = model.Order(order_id=42, local_customer=customer, created_by=user)
-        self.session.add(order)
-        self.session.commit()
-
-        # no view perm
-        grid = view.make_orders_grid(customer)
-        self.assertEqual(len(grid.actions), 0)
-
-        # with view perm
-        with patch.object(self.request, 'is_root', new=True):
-            grid = view.make_orders_grid(customer)
-            self.assertEqual(len(grid.actions), 1)
-            self.assertEqual(grid.actions[0].key, 'view')
-
-    def test_make_new_order_batches_grid(self):
-        model = self.app.model
-        handler = NewOrderBatchHandler(self.config)
-        view = self.make_view()
-
-        user = model.User(username='barney')
-        self.session.add(user)
-        customer = model.LocalCustomer()
-        self.session.add(customer)
-        batch = handler.make_batch(self.session, local_customer=customer, created_by=user)
-        self.session.add(batch)
-        self.session.commit()
-
-        # no view perm
-        grid = view.make_new_order_batches_grid(customer)
-        self.assertEqual(len(grid.actions), 0)
-
-        # with view perm
-        with patch.object(self.request, 'is_root', new=True):
-            grid = view.make_new_order_batches_grid(customer)
-            self.assertEqual(len(grid.actions), 1)
-            self.assertEqual(grid.actions[0].key, 'view')
-
-    def test_objectify(self):
-        model = self.app.model
-        view = self.make_view()
-
-        user = model.User(username='barney')
-        self.session.add(user)
-        self.session.commit()
-
-        with patch.object(view, 'creating', new=True):
-            with patch.object(self.request, 'user', new=user):
-                form = view.make_model_form()
-                with patch.object(form, 'validated', create=True, new={
-                        'first_name': 'Chuck',
-                        'last_name': 'Norris',
-                }):
-                    customer = view.objectify(form)
-                    self.assertIsInstance(customer, model.LocalCustomer)
-                    self.assertEqual(customer.first_name, 'Chuck')
-                    self.assertEqual(customer.last_name, 'Norris')
-                    self.assertEqual(customer.full_name, 'Chuck Norris')
-
-
 class TestPendingCustomerView(WebTestCase):
 
     def make_view(self):
diff --git a/tests/web/views/test_orders.py b/tests/web/views/test_orders.py
index ab996f1..3925832 100644
--- a/tests/web/views/test_orders.py
+++ b/tests/web/views/test_orders.py
@@ -13,7 +13,7 @@ from wuttaweb.forms.schema import WuttaMoney
 from sideshow.batch.neworder import NewOrderBatchHandler
 from sideshow.testing import WebTestCase
 from sideshow.web.views import orders as mod
-from sideshow.web.forms.schema import OrderRef, PendingProductRef
+from sideshow.web.forms.schema import OrderRef
 
 
 class TestIncludeme(WebTestCase):
@@ -27,9 +27,6 @@ class TestOrderView(WebTestCase):
     def make_view(self):
         return mod.OrderView(self.request)
 
-    def make_handler(self):
-        return NewOrderBatchHandler(self.config)
-
     def test_configure_grid(self):
         model = self.app.model
         view = self.make_view()
@@ -43,7 +40,6 @@ class TestOrderView(WebTestCase):
     def test_create(self):
         self.pyramid_config.include('sideshow.web.views')
         model = self.app.model
-        enum = self.app.enum
         view = self.make_view()
 
         user = model.User(username='barney')
@@ -95,7 +91,7 @@ class TestOrderView(WebTestCase):
                             'customer_name': 'Fred Flintstone',
                             'phone_number': '555-1234',
                             'email_address': 'fred@mailinator.com',
-                            'new_customer_full_name': 'Fred Flintstone',
+                            'new_customer_name': 'Fred Flintstone',
                             'new_customer_first_name': 'Fred',
                             'new_customer_last_name': 'Flintstone',
                             'new_customer_phone': '555-1234',
@@ -112,40 +108,6 @@ class TestOrderView(WebTestCase):
                         self.assertEqual(response.content_type, 'application/json')
                         self.assertEqual(response.json_body, {'error': 'unknown form action'})
 
-                    # add item
-                    with patch.multiple(self.request, create=True,
-                                        method='POST',
-                                        json_body={'action': 'add_item',
-                                                   'product_info': {
-                                                       'scancode': '07430500132',
-                                                       'description': 'Vinegar',
-                                                       'unit_price_reg': 5.99,
-                                                   },
-                                                   'order_qty': 1,
-                                                   'order_uom': enum.ORDER_UOM_UNIT}):
-                        response = view.create()
-                        self.assertIsInstance(response, Response)
-                        self.assertEqual(response.content_type, 'application/json')
-                        data = response.json_body
-                        self.assertEqual(sorted(data), ['batch', 'row'])
-
-                    # add item, w/ error
-                    with patch.object(NewOrderBatchHandler, 'add_item', side_effect=RuntimeError):
-                        with patch.multiple(self.request, create=True,
-                                            method='POST',
-                                            json_body={'action': 'add_item',
-                                                       'product_info': {
-                                                           'scancode': '07430500116',
-                                                           'description': 'Vinegar',
-                                                           'unit_price_reg': 3.59,
-                                                       },
-                                                       'order_qty': 1,
-                                                       'order_uom': enum.ORDER_UOM_UNIT}):
-                            response = view.create()
-                            self.assertIsInstance(response, Response)
-                            self.assertEqual(response.content_type, 'application/json')
-                            self.assertEqual(response.json_body, {'error': 'RuntimeError'})
-
     def test_get_current_batch(self):
         model = self.app.model
         handler = NewOrderBatchHandler(self.config)
@@ -175,75 +137,6 @@ class TestOrderView(WebTestCase):
                     self.assertEqual(self.session.query(model.NewOrderBatch).count(), 1)
                     self.assertIs(batch2, batch)
 
-    def test_customer_autocomplete(self):
-        model = self.app.model
-        view = self.make_view()
-
-        with patch.object(view, 'Session', return_value=self.session):
-
-            # empty results by default
-            self.assertEqual(view.customer_autocomplete(), [])
-            with patch.object(self.request, 'GET', new={'term': 'foo'}, create=True):
-                self.assertEqual(view.customer_autocomplete(), [])
-
-            # add a customer
-            customer = model.LocalCustomer(full_name="Chuck Norris")
-            self.session.add(customer)
-            self.session.flush()
-
-            # search for chuck finds chuck
-            with patch.object(self.request, 'GET', new={'term': 'chuck'}, create=True):
-                result = view.customer_autocomplete()
-                self.assertEqual(len(result), 1)
-                self.assertEqual(result[0], {
-                    'value': customer.uuid.hex,
-                    'label': "Chuck Norris",
-                })
-
-            # search for sally finds nothing
-            with patch.object(self.request, 'GET', new={'term': 'sally'}, create=True):
-                result = view.customer_autocomplete()
-                self.assertEqual(result, [])
-
-    def test_product_autocomplete(self):
-        model = self.app.model
-        view = self.make_view()
-
-        with patch.object(view, 'Session', return_value=self.session):
-
-            # empty results by default
-            self.assertEqual(view.product_autocomplete(), [])
-            with patch.object(self.request, 'GET', new={'term': 'foo'}, create=True):
-                self.assertEqual(view.product_autocomplete(), [])
-
-            # add a product
-            product = model.LocalProduct(brand_name="Bragg's", description="Vinegar")
-            self.session.add(product)
-            self.session.flush()
-
-            # search for vinegar finds product
-            with patch.object(self.request, 'GET', new={'term': 'vinegar'}, create=True):
-                result = view.product_autocomplete()
-                self.assertEqual(len(result), 1)
-                self.assertEqual(result[0], {
-                    'value': product.uuid.hex,
-                    'label': "Bragg's Vinegar",
-                })
-
-            # search for brag finds product
-            with patch.object(self.request, 'GET', new={'term': 'brag'}, create=True):
-                result = view.product_autocomplete()
-                self.assertEqual(len(result), 1)
-                self.assertEqual(result[0], {
-                    'value': product.uuid.hex,
-                    'label': "Bragg's Vinegar",
-                })
-
-            # search for juice finds nothing
-            with patch.object(self.request, 'GET', new={'term': 'juice'}, create=True):
-                result = view.product_autocomplete()
-                self.assertEqual(result, [])
-
     def test_get_pending_product_required_fields(self):
         model = self.app.model
         view = self.make_view()
@@ -265,51 +158,38 @@ class TestOrderView(WebTestCase):
         model = self.app.model
         handler = NewOrderBatchHandler(self.config)
         view = self.make_view()
-        view.batch_handler = handler
 
         user = model.User(username='barney')
         self.session.add(user)
 
-        # with external customer
-        with patch.object(handler, 'use_local_customers', return_value=False):
-            batch = handler.make_batch(self.session, created_by=user,
-                                       customer_id=42, customer_name='Fred Flintstone',
-                                       phone_number='555-1234', email_address='fred@mailinator.com')
-            self.session.add(batch)
-            self.session.flush()
-            context = view.get_context_customer(batch)
-            self.assertEqual(context, {
-                'customer_is_known': True,
-                'customer_id': 42,
-                'customer_name': 'Fred Flintstone',
-                'phone_number': '555-1234',
-                'email_address': 'fred@mailinator.com',
-            })
-
-        # with local customer
-        local = model.LocalCustomer(full_name="Betty Boop")
-        self.session.add(local)
+        # with true customer
         batch = handler.make_batch(self.session, created_by=user,
-                                   local_customer=local, customer_name='Betty Boop',
-                                   phone_number='555-8888')
+                                   customer_id=42, customer_name='Fred Flintstone',
+                                   phone_number='555-1234', email_address='fred@mailinator.com')
         self.session.add(batch)
         self.session.flush()
         context = view.get_context_customer(batch)
         self.assertEqual(context, {
             'customer_is_known': True,
-            'customer_id': local.uuid.hex,
-            'customer_name': 'Betty Boop',
-            'phone_number': '555-8888',
-            'email_address': None,
+            'customer_id': 42,
+            'customer_name': 'Fred Flintstone',
+            'phone_number': '555-1234',
+            'email_address': 'fred@mailinator.com',
+            'new_customer_name': None,
+            'new_customer_first_name': None,
+            'new_customer_last_name': None,
+            'new_customer_phone': None,
+            'new_customer_email': None,
         })
 
         # with pending customer
         batch = handler.make_batch(self.session, created_by=user)
         self.session.add(batch)
-        handler.set_customer(batch, dict(
+        handler.set_pending_customer(batch, dict(
             full_name="Fred Flintstone",
             first_name="Fred", last_name="Flintstone",
             phone_number='555-1234', email_address='fred@mailinator.com',
+            created_by=user,
         ))
         self.session.flush()
         context = view.get_context_customer(batch)
@@ -319,7 +199,7 @@ class TestOrderView(WebTestCase):
             'customer_name': 'Fred Flintstone',
             'phone_number': '555-1234',
             'email_address': 'fred@mailinator.com',
-            'new_customer_full_name': 'Fred Flintstone',
+            'new_customer_name': 'Fred Flintstone',
             'new_customer_first_name': 'Fred',
             'new_customer_last_name': 'Flintstone',
             'new_customer_phone': '555-1234',
@@ -337,6 +217,11 @@ class TestOrderView(WebTestCase):
             'customer_name': None,
             'phone_number': None,
             'email_address': None,
+            'new_customer_name': None,
+            'new_customer_first_name': None,
+            'new_customer_last_name': None,
+            'new_customer_phone': None,
+            'new_customer_email': None,
         })
 
     def test_start_over(self):
@@ -383,80 +268,6 @@ class TestOrderView(WebTestCase):
                     self.session.flush()
                     self.assertEqual(self.session.query(model.NewOrderBatch).count(), 0)
 
-    def test_assign_customer(self):
-        self.pyramid_config.add_route('orders.create', '/orders/new')
-        model = self.app.model
-        handler = NewOrderBatchHandler(self.config)
-        view = self.make_view()
-
-        user = model.User(username='barney')
-        self.session.add(user)
-        weirdal = model.LocalCustomer(full_name="Weird Al")
-        self.session.add(weirdal)
-        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()
-
-                    # normal
-                    self.assertIsNone(batch.local_customer)
-                    self.assertIsNone(batch.pending_customer)
-                    context = view.assign_customer(batch, {'customer_id': weirdal.uuid.hex})
-                    self.assertIsNone(batch.pending_customer)
-                    self.assertIs(batch.local_customer, weirdal)
-                    self.assertEqual(context, {
-                        'customer_is_known': True,
-                        'customer_id': weirdal.uuid.hex,
-                        'customer_name': 'Weird Al',
-                        'phone_number': None,
-                        'email_address': None,
-                    })
-
-                    # missing customer_id
-                    context = view.assign_customer(batch, {})
-                    self.assertEqual(context, {'error': "Must provide customer_id"})
-
-    def test_unassign_customer(self):
-        self.pyramid_config.add_route('orders.create', '/orders/new')
-        model = self.app.model
-        handler = NewOrderBatchHandler(self.config)
-        view = self.make_view()
-
-        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()
-                    view.set_pending_customer(batch, {'first_name': 'Jack',
-                                                      'last_name': 'Black'})
-
-                    # normal
-                    self.assertIsNone(batch.local_customer)
-                    self.assertIsNotNone(batch.pending_customer)
-                    self.assertEqual(batch.customer_name, 'Jack Black')
-                    context = view.unassign_customer(batch, {})
-                    # nb. pending record remains, but not used
-                    self.assertIsNotNone(batch.pending_customer)
-                    self.assertIsNone(batch.customer_name)
-                    self.assertIsNone(batch.local_customer)
-                    self.assertEqual(context, {
-                        'customer_is_known': True,
-                        'customer_id': None,
-                        'customer_name': None,
-                        'phone_number': None,
-                        'email_address': None,
-                        'new_customer_full_name': 'Jack Black',
-                        'new_customer_first_name': 'Jack',
-                        'new_customer_last_name': 'Black',
-                        'new_customer_phone': None,
-                        'new_customer_email': None,
-                    })
-
     def test_set_pending_customer(self):
         self.pyramid_config.add_route('orders.create', '/orders/new')
         model = self.app.model
@@ -490,58 +301,19 @@ class TestOrderView(WebTestCase):
                         'customer_name': 'Fred Flintstone',
                         'phone_number': '555-1234',
                         'email_address': 'fred@mailinator.com',
-                        'new_customer_full_name': 'Fred Flintstone',
+                        'new_customer_name': 'Fred Flintstone',
                         'new_customer_first_name': 'Fred',
                         'new_customer_last_name': 'Flintstone',
                         'new_customer_phone': '555-1234',
                         'new_customer_email': 'fred@mailinator.com',
                     })
 
-    def test_get_product_info(self):
-        model = self.app.model
-        handler = self.make_handler()
-        view = self.make_view()
-
-        user = model.User(username='barney')
-        self.session.add(user)
-        local = model.LocalProduct(scancode='07430500132',
-                                   brand_name='Bragg',
-                                   description='Vinegar',
-                                   size='32oz',
-                                   case_size=12,
-                                   unit_price_reg=decimal.Decimal('5.99'))
-        self.session.add(local)
-        self.session.flush()
-
-        with patch.object(view, 'Session', return_value=self.session):
-            with patch.object(view, 'batch_handler', create=True, new=handler):
-                with patch.object(self.request, 'user', new=user):
-                    batch = view.get_current_batch()
-
-                    # typical, for local product
-                    context = view.get_product_info(batch, {'product_id': local.uuid.hex})
-                    self.assertEqual(context['product_id'], local.uuid.hex)
-                    self.assertEqual(context['scancode'], '07430500132')
-                    self.assertEqual(context['brand_name'], 'Bragg')
-                    self.assertEqual(context['description'], 'Vinegar')
-                    self.assertEqual(context['size'], '32oz')
-                    self.assertEqual(context['full_description'], 'Bragg Vinegar 32oz')
-                    self.assertEqual(context['case_size'], 12)
-                    self.assertEqual(context['unit_price_reg'], 5.99)
-
-                    # error if local product missing
-                    mock_uuid = self.app.make_true_uuid()
-                    context = view.get_product_info(batch, {'product_id': mock_uuid.hex})
-                    self.assertEqual(context, {'error': "Product not found"})
-
-                    # error if no product_id
-                    context = view.get_product_info(batch, {})
-                    self.assertEqual(context, {'error': "Must specify a product ID"})
-
-                    # external lookup not implemented (yet)
-                    with patch.object(handler, 'use_local_products', return_value=False):
-                        self.assertRaises(NotImplementedError, view.get_product_info,
-                                          batch, {'product_id': '42'})
+                    # error
+                    with patch.object(handler, 'set_pending_customer', side_effect=RuntimeError):
+                        context = view.set_pending_customer(batch, data)
+                        self.assertEqual(context, {
+                            'error': 'RuntimeError',
+                        })
 
     def test_add_item(self):
         model = self.app.model
@@ -554,7 +326,7 @@ class TestOrderView(WebTestCase):
         self.session.commit()
 
         data = {
-            'product_info': {
+            'pending_product': {
                 'scancode': '07430500132',
                 'brand_name': 'Bragg',
                 'description': 'Vinegar',
@@ -581,10 +353,16 @@ class TestOrderView(WebTestCase):
                     row = batch.rows[0]
                     self.assertIsInstance(row.pending_product, model.PendingProduct)
 
-                    # external product not yet supported
-                    with patch.object(handler, 'use_local_products', return_value=False):
-                        with patch.dict(data, product_info='42'):
-                            self.assertRaises(NotImplementedError, view.add_item, batch, data)
+                    # pending w/ invalid price
+                    with patch.dict(data['pending_product'], unit_price_reg='invalid'):
+                        result = view.add_item(batch, data)
+                        self.assertEqual(result, {'error': "Invalid entry for field: unit_price_reg"})
+                        self.session.flush()
+                        self.assertEqual(len(batch.rows), 1) # still just the 1st row
+
+                    # true product not yet supported
+                    with patch.dict(data, product_is_known=True):
+                        self.assertRaises(NotImplementedError, view.add_item, batch, data)
 
     def test_update_item(self):
         model = self.app.model
@@ -597,7 +375,7 @@ class TestOrderView(WebTestCase):
         self.session.commit()
 
         data = {
-            'product_info': {
+            'pending_product': {
                 'scancode': '07430500132',
                 'brand_name': 'Bragg',
                 'description': 'Vinegar',
@@ -625,7 +403,7 @@ class TestOrderView(WebTestCase):
 
                     # missing row uuid
                     result = view.update_item(batch, data)
-                    self.assertEqual(result, {'error': "Must specify row UUID"})
+                    self.assertEqual(result, {'error': "Must specify a row UUID"})
 
                     # row not found
                     with patch.dict(data, uuid=self.app.make_true_uuid()):
@@ -642,18 +420,16 @@ class TestOrderView(WebTestCase):
                         result = view.update_item(batch, data)
                         self.assertEqual(result, {'error': "Row is for wrong batch"})
 
+                    # set row for remaining tests
+                    data['uuid'] = row.uuid
+
                     # true product not yet supported
-                    with patch.object(handler, 'use_local_products', return_value=False):
-                        self.assertRaises(NotImplementedError, view.update_item, batch, {
-                            'uuid': row.uuid,
-                            'product_info': '42',
-                            'order_qty': 1,
-                            'order_uom': enum.ORDER_UOM_UNIT,
-                        })
+                    with patch.dict(data, product_is_known=True):
+                        self.assertRaises(NotImplementedError, view.update_item, batch, data)
 
                     # update row, pending product
-                    with patch.dict(data, uuid=row.uuid, order_qty=2):
-                        with patch.dict(data['product_info'], scancode='07430500116'):
+                    with patch.dict(data, order_qty=2):
+                        with patch.dict(data['pending_product'], scancode='07430500116'):
                             self.assertEqual(row.product_scancode, '07430500132')
                             self.assertEqual(row.order_qty, 1)
                             result = view.update_item(batch, data)
@@ -662,7 +438,7 @@ class TestOrderView(WebTestCase):
                             self.assertEqual(row.order_qty, 2)
                             self.assertEqual(row.pending_product.scancode, '07430500116')
                             self.assertEqual(result['row']['product_scancode'], '07430500116')
-                            self.assertEqual(result['row']['order_qty'], 2)
+                            self.assertEqual(result['row']['order_qty'], '2')
 
     def test_delete_item(self):
         model = self.app.model
@@ -675,7 +451,7 @@ class TestOrderView(WebTestCase):
         self.session.commit()
 
         data = {
-            'product_info': {
+            'pending_product': {
                 'scancode': '07430500132',
                 'brand_name': 'Bragg',
                 'description': 'Vinegar',
@@ -730,7 +506,7 @@ class TestOrderView(WebTestCase):
                     self.assertEqual(len(batch.rows), 0)
                     self.assertEqual(batch.row_count, 0)
 
-    def test_submit_order(self):
+    def test_submit_new_order(self):
         self.pyramid_config.add_route('orders.view', '/orders/{uuid}')
         model = self.app.model
         enum = self.app.enum
@@ -742,7 +518,7 @@ class TestOrderView(WebTestCase):
         self.session.commit()
 
         data = {
-            'product_info': {
+            'pending_product': {
                 'scancode': '07430500132',
                 'brand_name': 'Bragg',
                 'description': 'Vinegar',
@@ -758,33 +534,28 @@ class TestOrderView(WebTestCase):
             with patch.object(view, 'Session', return_value=self.session):
                 with patch.object(self.request, 'user', new=user):
                     batch = view.get_current_batch()
+                    self.session.flush()
                     self.assertEqual(len(batch.rows), 0)
 
                     # add row w/ pending product
                     view.add_item(batch, data)
-                    self.assertEqual(len(batch.rows), 1)
+                    self.session.flush()
                     row = batch.rows[0]
                     self.assertIsInstance(row.pending_product, model.PendingProduct)
                     self.assertEqual(row.unit_price_quoted, decimal.Decimal('5.99'))
 
                     # execute not allowed yet (no customer)
-                    result = view.submit_order(batch, {})
+                    result = view.submit_new_order(batch, {})
                     self.assertEqual(result, {'error': "Must assign the customer"})
 
-                    # execute not allowed yet (no phone number)
-                    view.set_pending_customer(batch, {'full_name': 'John Doe'})
-                    result = view.submit_order(batch, {})
-                    self.assertEqual(result, {'error': "Customer phone number is required"})
-
                     # submit/execute ok
-                    view.set_pending_customer(batch, {'full_name': 'John Doe',
-                                                      'phone_number': '555-1234'})
-                    result = view.submit_order(batch, {})
+                    batch.customer_id = 42
+                    result = view.submit_new_order(batch, {})
                     self.assertEqual(sorted(result), ['next_url'])
                     self.assertIn('/orders/', result['next_url'])
 
                     # error (already executed)
-                    result = view.submit_order(batch, {})
+                    result = view.submit_new_order(batch, {})
                     self.assertEqual(result, {
                         'error': f"ValueError: batch has already been executed: {batch}",
                     })
@@ -814,8 +585,9 @@ class TestOrderView(WebTestCase):
             'size': '32oz',
             'unit_price_reg': 5.99,
             'case_size': 12,
+            'created_by': user,
         }
-        row = handler.add_item(batch, pending, 1, enum.ORDER_UOM_CASE)
+        row = handler.add_pending_product(batch, pending, 1, enum.ORDER_UOM_CASE)
         self.session.commit()
 
         data = view.normalize_batch(batch)
@@ -832,15 +604,11 @@ class TestOrderView(WebTestCase):
         enum = self.app.enum
         handler = NewOrderBatchHandler(self.config)
         view = self.make_view()
-        view.batch_handler = 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()
-
-        # add 1st row w/ pending product
         pending = {
             'scancode': '07430500132',
             'brand_name': 'Bragg',
@@ -848,22 +616,19 @@ class TestOrderView(WebTestCase):
             'size': '32oz',
             'unit_price_reg': 5.99,
             'case_size': 12,
-            'vendor_name': 'Acme Warehouse',
-            'vendor_item_code': '1234',
+            'created_by': user,
         }
-        row1 = handler.add_item(batch, pending, 2, enum.ORDER_UOM_CASE)
+        row = handler.add_pending_product(batch, pending, 2, enum.ORDER_UOM_CASE)
+        self.session.commit()
 
-        # typical, pending product
-        data = view.normalize_row(row1)
+        # normal
+        data = view.normalize_row(row)
         self.assertIsInstance(data, dict)
-        self.assertEqual(data['uuid'], row1.uuid.hex)
+        self.assertEqual(data['uuid'], row.uuid.hex)
         self.assertEqual(data['sequence'], 1)
-        self.assertIsNone(data['product_id'])
         self.assertEqual(data['product_scancode'], '07430500132')
-        self.assertEqual(data['product_full_description'], 'Bragg Vinegar 32oz')
-        self.assertEqual(data['case_size'], 12)
-        self.assertEqual(data['vendor_name'], 'Acme Warehouse')
-        self.assertEqual(data['order_qty'], 2)
+        self.assertEqual(data['case_size'], '12')
+        self.assertEqual(data['order_qty'], '2')
         self.assertEqual(data['order_uom'], 'CS')
         self.assertEqual(data['order_qty_display'], '2 Cases (&times; 12 = 24 Units)')
         self.assertEqual(data['unit_price_reg'], 5.99)
@@ -879,9 +644,9 @@ class TestOrderView(WebTestCase):
         self.assertEqual(data['total_price'], 143.76)
         self.assertEqual(data['total_price_display'], '$143.76')
         self.assertIsNone(data['special_order'])
-        self.assertEqual(data['status_code'], row1.STATUS_OK)
+        self.assertEqual(data['status_code'], row.STATUS_OK)
         self.assertEqual(data['pending_product'], {
-            'uuid': row1.pending_product_uuid.hex,
+            'uuid': row.pending_product_uuid.hex,
             'scancode': '07430500132',
             'brand_name': 'Bragg',
             'description': 'Vinegar',
@@ -889,117 +654,44 @@ class TestOrderView(WebTestCase):
             'department_id': None,
             'department_name': None,
             'unit_price_reg': 5.99,
-            'vendor_name': 'Acme Warehouse',
-            'vendor_item_code': '1234',
+            'vendor_name': None,
+            'vendor_item_code': None,
             'unit_cost': None,
             'case_size': 12.0,
             'notes': None,
             'special_order': None,
         })
 
-        # the next few tests will morph 1st row..
-
         # unknown case size
-        row1.pending_product.case_size = None
-        handler.refresh_row(row1)
+        row.pending_product.case_size = None
+        handler.refresh_row(row)
         self.session.flush()
-        data = view.normalize_row(row1)
-        self.assertIsNone(data['case_size'])
+        data = view.normalize_row(row)
         self.assertEqual(data['order_qty_display'], '2 Cases (&times; ?? = ?? Units)')
 
         # order by unit
-        row1.order_uom = enum.ORDER_UOM_UNIT
-        handler.refresh_row(row1)
+        row.order_uom = enum.ORDER_UOM_UNIT
+        handler.refresh_row(row)
         self.session.flush()
-        data = view.normalize_row(row1)
-        self.assertEqual(data['order_uom'], enum.ORDER_UOM_UNIT)
+        data = view.normalize_row(row)
         self.assertEqual(data['order_qty_display'], '2 Units')
 
         # item on sale
-        row1.pending_product.case_size = 12
-        row1.unit_price_sale = decimal.Decimal('5.19')
-        row1.sale_ends = datetime.datetime(2099, 1, 5, 20, 32)
-        handler.refresh_row(row1)
+        row.pending_product.case_size = 12
+        row.unit_price_sale = decimal.Decimal('5.19')
+        row.sale_ends = datetime.datetime(2025, 1, 5, 20, 32)
+        handler.refresh_row(row, now=datetime.datetime(2025, 1, 5, 19))
         self.session.flush()
-        data = view.normalize_row(row1)
+        data = view.normalize_row(row)
         self.assertEqual(data['unit_price_sale'], 5.19)
         self.assertEqual(data['unit_price_sale_display'], '$5.19')
-        self.assertEqual(data['sale_ends'], '2099-01-05 20:32:00')
-        self.assertEqual(data['sale_ends_display'], '2099-01-05')
+        self.assertEqual(data['sale_ends'], '2025-01-05 20:32:00')
+        self.assertEqual(data['sale_ends_display'], '2025-01-05')
         self.assertEqual(data['unit_price_quoted'], 5.19)
         self.assertEqual(data['unit_price_quoted_display'], '$5.19')
         self.assertEqual(data['case_price_quoted'], 62.28)
         self.assertEqual(data['case_price_quoted_display'], '$62.28')
 
-        # add 2nd row w/ local product
-        local = model.LocalProduct(brand_name="Lay's",
-                                   description="Potato Chips",
-                                   vendor_name='Acme Distribution',
-                                   unit_price_reg=3.29)
-        self.session.add(local)
-        self.session.flush()
-        row2 = handler.add_item(batch, local.uuid.hex, 1, enum.ORDER_UOM_UNIT)
-
-        # typical, local product
-        data = view.normalize_row(row2)
-        self.assertEqual(data['uuid'], row2.uuid.hex)
-        self.assertEqual(data['sequence'], 2)
-        self.assertEqual(data['product_id'], local.uuid.hex)
-        self.assertIsNone(data['product_scancode'])
-        self.assertEqual(data['product_full_description'], "Lay's Potato Chips")
-        self.assertIsNone(data['case_size'])
-        self.assertEqual(data['vendor_name'], 'Acme Distribution')
-        self.assertEqual(data['order_qty'], 1)
-        self.assertEqual(data['order_uom'], 'EA')
-        self.assertEqual(data['order_qty_display'], '1 Units')
-        self.assertEqual(data['unit_price_reg'], 3.29)
-        self.assertEqual(data['unit_price_reg_display'], '$3.29')
-        self.assertNotIn('unit_price_sale', data)
-        self.assertNotIn('unit_price_sale_display', data)
-        self.assertNotIn('sale_ends', data)
-        self.assertNotIn('sale_ends_display', data)
-        self.assertEqual(data['unit_price_quoted'], 3.29)
-        self.assertEqual(data['unit_price_quoted_display'], '$3.29')
-        self.assertIsNone(data['case_price_quoted'])
-        self.assertEqual(data['case_price_quoted_display'], '')
-        self.assertEqual(data['total_price'], 3.29)
-        self.assertEqual(data['total_price_display'], '$3.29')
-        self.assertIsNone(data['special_order'])
-        self.assertEqual(data['status_code'], row2.STATUS_OK)
-        self.assertNotIn('pending_product', data)
-
-        # the next few tests will morph 2nd row..
-
-        # typical, external product
-        row2.product_id = '42'
-        with patch.object(handler, 'use_local_products', return_value=False):
-            data = view.normalize_row(row2)
-        self.assertEqual(data['uuid'], row2.uuid.hex)
-        self.assertEqual(data['sequence'], 2)
-        self.assertEqual(data['product_id'], '42')
-        self.assertIsNone(data['product_scancode'])
-        self.assertNotIn('product_full_description', data) # TODO
-        self.assertIsNone(data['case_size'])
-        self.assertNotIn('vendor_name', data) # TODO
-        self.assertEqual(data['order_qty'], 1)
-        self.assertEqual(data['order_uom'], 'EA')
-        self.assertEqual(data['order_qty_display'], '1 Units')
-        self.assertEqual(data['unit_price_reg'], 3.29)
-        self.assertEqual(data['unit_price_reg_display'], '$3.29')
-        self.assertNotIn('unit_price_sale', data)
-        self.assertNotIn('unit_price_sale_display', data)
-        self.assertNotIn('sale_ends', data)
-        self.assertNotIn('sale_ends_display', data)
-        self.assertEqual(data['unit_price_quoted'], 3.29)
-        self.assertEqual(data['unit_price_quoted_display'], '$3.29')
-        self.assertIsNone(data['case_price_quoted'])
-        self.assertEqual(data['case_price_quoted_display'], '')
-        self.assertEqual(data['total_price'], 3.29)
-        self.assertEqual(data['total_price_display'], '$3.29')
-        self.assertIsNone(data['special_order'])
-        self.assertEqual(data['status_code'], row2.STATUS_OK)
-        self.assertNotIn('pending_product', data)
-
     def test_get_instance_title(self):
         model = self.app.model
         view = self.make_view()
@@ -1023,31 +715,13 @@ class TestOrderView(WebTestCase):
         self.session.add(order)
         self.session.commit()
 
-        # viewing (no customer)
+        # viewing
         with patch.object(view, 'viewing', new=True):
             form = view.make_form(model_instance=order)
             # nb. this is to avoid include/exclude ambiguity
             form.remove('items')
             view.configure_form(form)
             schema = form.get_schema()
-            self.assertIn('pending_customer', form)
-            self.assertIsInstance(schema['total_price'].typ, WuttaMoney)
-
-        # assign local customer
-        local = model.LocalCustomer(first_name='Jack', last_name='Black',
-                                    phone_number='555-1234')
-        self.session.add(local)
-        order.local_customer = local
-        self.session.flush()
-
-        # viewing (local customer)
-        with patch.object(view, 'viewing', new=True):
-            form = view.make_form(model_instance=order)
-            # nb. this is to avoid include/exclude ambiguity
-            form.remove('items')
-            view.configure_form(form)
-            self.assertNotIn('pending_customer', form)
-            schema = form.get_schema()
             self.assertIsInstance(schema['total_price'].typ, WuttaMoney)
 
     def test_get_xref_buttons(self):
@@ -1157,46 +831,6 @@ class TestOrderView(WebTestCase):
         url = view.get_row_action_url_view(item, 0)
         self.assertIn(f'/order-items/{item.uuid}', url)
 
-    def test_configure(self):
-        self.pyramid_config.add_route('home', '/')
-        self.pyramid_config.add_route('login', '/auth/login')
-        self.pyramid_config.add_route('orders', '/orders/')
-        model = self.app.model
-        view = self.make_view()
-
-        with patch.object(view, 'Session', return_value=self.session):
-            with patch.multiple(self.config, usedb=True, preferdb=True):
-
-                # sanity check
-                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)
-
-                # fetch initial page
-                response = view.configure()
-                self.assertIsInstance(response, Response)
-                self.assertNotIsInstance(response, HTTPFound)
-                self.session.flush()
-                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)
-
-                # post new settings
-                with patch.multiple(self.request, create=True,
-                                    method='POST',
-                                    POST={
-                                        'sideshow.orders.allow_unknown_products': 'true',
-                                    }):
-                    response = view.configure()
-                self.assertIsInstance(response, HTTPFound)
-                self.session.flush()
-                allowed = self.config.get_bool('sideshow.orders.allow_unknown_products',
-                                               session=self.session)
-                self.assertTrue(allowed)
-                self.assertTrue(self.session.query(model.Setting).count() > 1)
-
 
 class TestOrderItemView(WebTestCase):
 
@@ -1230,18 +864,6 @@ class TestOrderItemView(WebTestCase):
         self.assertEqual(view.render_status_code(None, None, enum.ORDER_ITEM_STATUS_INITIATED),
                          'initiated')
 
-    def test_get_instance_title(self):
-        model = self.app.model
-        enum = self.app.enum
-        view = self.make_view()
-
-        item = model.OrderItem(product_brand='Bragg',
-                               product_description='Vinegar',
-                               product_size='32oz',
-                               status_code=enum.ORDER_ITEM_STATUS_INITIATED)
-        title = view.get_instance_title(item)
-        self.assertEqual(title, "(initiated) Bragg Vinegar 32oz")
-
     def test_configure_form(self):
         model = self.app.model
         enum = self.app.enum
@@ -1249,24 +871,12 @@ class TestOrderItemView(WebTestCase):
 
         item = model.OrderItem(status_code=enum.ORDER_ITEM_STATUS_INITIATED)
 
-        # viewing, w/ pending product
+        # viewing
         with patch.object(view, 'viewing', new=True):
             form = view.make_form(model_instance=item)
             view.configure_form(form)
             schema = form.get_schema()
             self.assertIsInstance(schema['order'].typ, OrderRef)
-            self.assertIn('pending_product', form)
-            self.assertIsInstance(schema['pending_product'].typ, PendingProductRef)
-
-        # viewing, w/ local product
-        local = model.LocalProduct()
-        item.local_product = local
-        with patch.object(view, 'viewing', new=True):
-            form = view.make_form(model_instance=item)
-            view.configure_form(form)
-            schema = form.get_schema()
-            self.assertIsInstance(schema['order'].typ, OrderRef)
-            self.assertNotIn('pending_product', form)
 
     def test_get_xref_buttons(self):
         self.pyramid_config.add_route('orders.view', '/orders/{uuid}')
diff --git a/tests/web/views/test_products.py b/tests/web/views/test_products.py
index c9782dd..e7e61fe 100644
--- a/tests/web/views/test_products.py
+++ b/tests/web/views/test_products.py
@@ -16,104 +16,6 @@ class TestIncludeme(WebTestCase):
         mod.includeme(self.pyramid_config)
 
 
-class TestLocalProductView(WebTestCase):
-
-    def make_view(self):
-        return mod.LocalProductView(self.request)
-
-    def test_configure_grid(self):
-        model = self.app.model
-        view = self.make_view()
-        grid = view.make_grid(model_class=model.LocalProduct)
-        self.assertNotIn('scancode', grid.linked_columns)
-        self.assertNotIn('brand_name', grid.linked_columns)
-        self.assertNotIn('description', grid.linked_columns)
-        view.configure_grid(grid)
-        self.assertIn('scancode', grid.linked_columns)
-        self.assertIn('brand_name', grid.linked_columns)
-        self.assertIn('description', grid.linked_columns)
-
-    def test_configure_form(self):
-        model = self.app.model
-        view = self.make_view()
-
-        # creating
-        with patch.object(view, 'creating', new=True):
-            form = view.make_form(model_class=model.LocalProduct)
-            self.assertIn('external_id', form)
-            view.configure_form(form)
-            self.assertNotIn('external_id', form)
-
-        user = model.User(username='barney')
-        self.session.add(user)
-        product = model.LocalProduct()
-        self.session.add(product)
-        self.session.commit()
-
-        # viewing
-        with patch.object(view, 'viewing', new=True):
-            form = view.make_form(model_instance=product)
-            self.assertNotIn('external_id', form.readonly_fields)
-            self.assertNotIn('local_products.view.orders', form.grid_vue_context)
-            view.configure_form(form)
-            self.assertIn('external_id', form.readonly_fields)
-            self.assertIn('local_products.view.orders', form.grid_vue_context)
-
-    def test_make_orders_grid(self):
-        model = self.app.model
-        enum = self.app.enum
-        view = self.make_view()
-
-        user = model.User(username='barney')
-        self.session.add(user)
-        order = model.Order(order_id=42, customer_id=42, created_by=user)
-        product = model.LocalProduct()
-        self.session.add(product)
-        item = model.OrderItem(local_product=product,
-                               order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
-                               status_code=enum.ORDER_ITEM_STATUS_INITIATED)
-        order.items.append(item)
-        self.session.add(order)
-        self.session.commit()
-
-        # no view perm
-        grid = view.make_orders_grid(product)
-        self.assertEqual(len(grid.actions), 0)
-
-        # with view perm
-        with patch.object(self.request, 'is_root', new=True):
-            grid = view.make_orders_grid(product)
-            self.assertEqual(len(grid.actions), 1)
-            self.assertEqual(grid.actions[0].key, 'view')
-
-    def test_make_new_order_batches_grid(self):
-        model = self.app.model
-        enum = self.app.enum
-        handler = NewOrderBatchHandler(self.config)
-        view = self.make_view()
-
-        user = model.User(username='barney')
-        self.session.add(user)
-        batch = handler.make_batch(self.session, created_by=user)
-        self.session.add(batch)
-        product = model.LocalProduct()
-        self.session.add(product)
-        row = handler.make_row(local_product=product,
-                               order_qty=1, order_uom=enum.ORDER_UOM_UNIT)
-        handler.add_row(batch, row)
-        self.session.commit()
-
-        # no view perm
-        grid = view.make_new_order_batches_grid(product)
-        self.assertEqual(len(grid.actions), 0)
-
-        # with view perm
-        with patch.object(self.request, 'is_root', new=True):
-            grid = view.make_new_order_batches_grid(product)
-            self.assertEqual(len(grid.actions), 1)
-            self.assertEqual(grid.actions[0].key, 'view')
-
-
 class TestPendingProductView(WebTestCase):
 
     def make_view(self):