feat: move lookup logic to handler; improve support for external lookup
This commit is contained in:
		
							parent
							
								
									f8f4933ca1
								
							
						
					
					
						commit
						811a37995d
					
				
					 7 changed files with 553 additions and 151 deletions
				
			
		| 
						 | 
				
			
			@ -77,6 +77,65 @@ class NewOrderBatchHandler(BatchHandler):
 | 
			
		|||
        return self.config.get_bool('sideshow.orders.allow_unknown_products',
 | 
			
		||||
                                    default=True)
 | 
			
		||||
 | 
			
		||||
    def autocomplete_customers_external(self, session, term, user=None):
 | 
			
		||||
        """
 | 
			
		||||
        Return autocomplete search results for :term:`external
 | 
			
		||||
        customer` records.
 | 
			
		||||
 | 
			
		||||
        There is no default logic here; subclass must implement.
 | 
			
		||||
 | 
			
		||||
        :param session: Current app :term:`db session`.
 | 
			
		||||
 | 
			
		||||
        :param term: Search term string from user input.
 | 
			
		||||
 | 
			
		||||
        :param user:
 | 
			
		||||
           :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
 | 
			
		||||
           is doing the search, if known.
 | 
			
		||||
 | 
			
		||||
        :returns: List of search results; each should be a dict with
 | 
			
		||||
           ``value`` and ``label`` keys.
 | 
			
		||||
        """
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def autocomplete_customers_local(self, session, term, user=None):
 | 
			
		||||
        """
 | 
			
		||||
        Return autocomplete search results for
 | 
			
		||||
        :class:`~sideshow.db.model.customers.LocalCustomer` records.
 | 
			
		||||
 | 
			
		||||
        :param session: Current app :term:`db session`.
 | 
			
		||||
 | 
			
		||||
        :param term: Search term string from user input.
 | 
			
		||||
 | 
			
		||||
        :param user:
 | 
			
		||||
           :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
 | 
			
		||||
           is doing the search, if known.
 | 
			
		||||
 | 
			
		||||
        :returns: List of search results; each should be a dict with
 | 
			
		||||
           ``value`` and ``label`` keys.
 | 
			
		||||
        """
 | 
			
		||||
        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 set_customer(self, batch, customer_info, user=None):
 | 
			
		||||
        """
 | 
			
		||||
        Set/update customer info for the batch.
 | 
			
		||||
| 
						 | 
				
			
			@ -91,14 +150,14 @@ class NewOrderBatchHandler(BatchHandler):
 | 
			
		|||
        :class:`~sideshow.db.model.customers.PendingCustomer` record
 | 
			
		||||
        is created if necessary.
 | 
			
		||||
 | 
			
		||||
        And then it will update these accordingly:
 | 
			
		||||
        And then it will update customer-related attributes via one of:
 | 
			
		||||
 | 
			
		||||
        * :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`
 | 
			
		||||
        * :meth:`refresh_batch_from_external_customer()`
 | 
			
		||||
        * :meth:`refresh_batch_from_local_customer()`
 | 
			
		||||
        * :meth:`refresh_batch_from_pending_customer()`
 | 
			
		||||
 | 
			
		||||
        Note that ``customer_info`` may be ``None``, which will cause
 | 
			
		||||
        all the above to be set to ``None`` also.
 | 
			
		||||
        customer attributes to be set to ``None`` also.
 | 
			
		||||
 | 
			
		||||
        :param batch:
 | 
			
		||||
           :class:`~sideshow.db.model.batch.neworder.NewOrderBatch` to
 | 
			
		||||
| 
						 | 
				
			
			@ -129,13 +188,11 @@ class NewOrderBatchHandler(BatchHandler):
 | 
			
		|||
                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
 | 
			
		||||
                self.refresh_batch_from_local_customer(batch)
 | 
			
		||||
 | 
			
		||||
            else: # external customer_id
 | 
			
		||||
                #batch.customer_id = customer_info
 | 
			
		||||
                raise NotImplementedError
 | 
			
		||||
                batch.customer_id = customer_info
 | 
			
		||||
                self.refresh_batch_from_external_customer(batch)
 | 
			
		||||
 | 
			
		||||
        elif customer_info:
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -160,9 +217,7 @@ class NewOrderBatchHandler(BatchHandler):
 | 
			
		|||
            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
 | 
			
		||||
            self.refresh_batch_from_pending_customer(batch)
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -175,6 +230,203 @@ class NewOrderBatchHandler(BatchHandler):
 | 
			
		|||
 | 
			
		||||
        session.flush()
 | 
			
		||||
 | 
			
		||||
    def refresh_batch_from_external_customer(self, batch):
 | 
			
		||||
        """
 | 
			
		||||
        Update customer-related attributes on the batch, from its
 | 
			
		||||
        :term:`external customer` record.
 | 
			
		||||
 | 
			
		||||
        This is called automatically from :meth:`set_customer()`.
 | 
			
		||||
 | 
			
		||||
        There is no default logic here; subclass must implement.
 | 
			
		||||
        """
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def refresh_batch_from_local_customer(self, batch):
 | 
			
		||||
        """
 | 
			
		||||
        Update customer-related attributes on the batch, from its
 | 
			
		||||
        :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.local_customer`
 | 
			
		||||
        record.
 | 
			
		||||
 | 
			
		||||
        This is called automatically from :meth:`set_customer()`.
 | 
			
		||||
        """
 | 
			
		||||
        customer = batch.local_customer
 | 
			
		||||
        batch.customer_name = customer.full_name
 | 
			
		||||
        batch.phone_number = customer.phone_number
 | 
			
		||||
        batch.email_address = customer.email_address
 | 
			
		||||
 | 
			
		||||
    def refresh_batch_from_pending_customer(self, batch):
 | 
			
		||||
        """
 | 
			
		||||
        Update customer-related attributes on the batch, from its
 | 
			
		||||
        :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`
 | 
			
		||||
        record.
 | 
			
		||||
 | 
			
		||||
        This is called automatically from :meth:`set_customer()`.
 | 
			
		||||
        """
 | 
			
		||||
        customer = batch.pending_customer
 | 
			
		||||
        batch.customer_name = customer.full_name
 | 
			
		||||
        batch.phone_number = customer.phone_number
 | 
			
		||||
        batch.email_address = customer.email_address
 | 
			
		||||
 | 
			
		||||
    def autocomplete_products_external(self, session, term, user=None):
 | 
			
		||||
        """
 | 
			
		||||
        Return autocomplete search results for :term:`external
 | 
			
		||||
        product` records.
 | 
			
		||||
 | 
			
		||||
        There is no default logic here; subclass must implement.
 | 
			
		||||
 | 
			
		||||
        :param session: Current app :term:`db session`.
 | 
			
		||||
 | 
			
		||||
        :param term: Search term string from user input.
 | 
			
		||||
 | 
			
		||||
        :param user:
 | 
			
		||||
           :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
 | 
			
		||||
           is doing the search, if known.
 | 
			
		||||
 | 
			
		||||
        :returns: List of search results; each should be a dict with
 | 
			
		||||
           ``value`` and ``label`` keys.
 | 
			
		||||
        """
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def autocomplete_products_local(self, session, term, user=None):
 | 
			
		||||
        """
 | 
			
		||||
        Return autocomplete search results for
 | 
			
		||||
        :class:`~sideshow.db.model.products.LocalProduct` records.
 | 
			
		||||
 | 
			
		||||
        :param session: Current app :term:`db session`.
 | 
			
		||||
 | 
			
		||||
        :param term: Search term string from user input.
 | 
			
		||||
 | 
			
		||||
        :param user:
 | 
			
		||||
           :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
 | 
			
		||||
           is doing the search, if known.
 | 
			
		||||
 | 
			
		||||
        :returns: List of search results; each should be a dict with
 | 
			
		||||
           ``value`` and ``label`` keys.
 | 
			
		||||
        """
 | 
			
		||||
        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_product_info_external(self, session, product_id, user=None):
 | 
			
		||||
        """
 | 
			
		||||
        Returns basic info for an :term:`external product` as pertains
 | 
			
		||||
        to ordering.
 | 
			
		||||
 | 
			
		||||
        When user has located a product via search, and must then
 | 
			
		||||
        choose order quantity and UOM based on case size, pricing
 | 
			
		||||
        etc., this method is called to retrieve the product info.
 | 
			
		||||
 | 
			
		||||
        There is no default logic here; subclass must implement.
 | 
			
		||||
 | 
			
		||||
        :param session: Current app :term:`db session`.
 | 
			
		||||
 | 
			
		||||
        :param product_id: Product ID string for which to retrieve
 | 
			
		||||
           info.
 | 
			
		||||
 | 
			
		||||
        :param user:
 | 
			
		||||
           :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
 | 
			
		||||
           is performing the action, if known.
 | 
			
		||||
 | 
			
		||||
        :returns: Dict of product info.  Should raise error instead of
 | 
			
		||||
           returning ``None`` if product not found.
 | 
			
		||||
 | 
			
		||||
        This method should only be called after a product has been
 | 
			
		||||
        identified via autocomplete/search lookup; therefore the
 | 
			
		||||
        ``product_id`` should be valid, and the caller can expect this
 | 
			
		||||
        method to *always* return a dict.  If for some reason the
 | 
			
		||||
        product cannot be found here, an error should be raised.
 | 
			
		||||
 | 
			
		||||
        The dict should contain as much product info as is available
 | 
			
		||||
        and needed; if some are missing it should not cause too much
 | 
			
		||||
        trouble in the app.  Here is a basic example::
 | 
			
		||||
 | 
			
		||||
           def get_product_info_external(self, session, product_id, user=None):
 | 
			
		||||
               ext_model = get_external_model()
 | 
			
		||||
               ext_session = make_external_session()
 | 
			
		||||
 | 
			
		||||
               ext_product = ext_session.get(ext_model.Product, product_id)
 | 
			
		||||
               if not ext_product:
 | 
			
		||||
                   ext_session.close()
 | 
			
		||||
                   raise ValueError(f"external product not found: {product_id}")
 | 
			
		||||
 | 
			
		||||
               info = {
 | 
			
		||||
                   'product_id': product_id,
 | 
			
		||||
                   'scancode': product.scancode,
 | 
			
		||||
                   'brand_name': product.brand_name,
 | 
			
		||||
                   'description': product.description,
 | 
			
		||||
                   'size': product.size,
 | 
			
		||||
                   'weighed': product.sold_by_weight,
 | 
			
		||||
                   'special_order': False,
 | 
			
		||||
                   'department_id': str(product.department_number),
 | 
			
		||||
                   '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,
 | 
			
		||||
               }
 | 
			
		||||
 | 
			
		||||
               ext_session.close()
 | 
			
		||||
               return info
 | 
			
		||||
        """
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def get_product_info_local(self, session, uuid, user=None):
 | 
			
		||||
        """
 | 
			
		||||
        Returns basic info for a
 | 
			
		||||
        :class:`~sideshow.db.model.products.LocalProduct` as pertains
 | 
			
		||||
        to ordering.
 | 
			
		||||
 | 
			
		||||
        When user has located a product via search, and must then
 | 
			
		||||
        choose order quantity and UOM based on case size, pricing
 | 
			
		||||
        etc., this method is called to retrieve the product info.
 | 
			
		||||
 | 
			
		||||
        See :meth:`get_product_info_external()` for more explanation.
 | 
			
		||||
        """
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        product = session.get(model.LocalProduct, uuid)
 | 
			
		||||
        if not product:
 | 
			
		||||
            raise ValueError(f"Local Product not found: {uuid}")
 | 
			
		||||
 | 
			
		||||
        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,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def add_item(self, batch, product_info, order_qty, order_uom, user=None):
 | 
			
		||||
        """
 | 
			
		||||
        Add a new item/row to the batch, for given product and quantity.
 | 
			
		||||
| 
						 | 
				
			
			@ -224,8 +476,7 @@ class NewOrderBatchHandler(BatchHandler):
 | 
			
		|||
                row.local_product = local
 | 
			
		||||
 | 
			
		||||
            else: # external product_id
 | 
			
		||||
                #row.product_id = product_info
 | 
			
		||||
                raise NotImplementedError
 | 
			
		||||
                row.product_id = product_info
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            # pending_product
 | 
			
		||||
| 
						 | 
				
			
			@ -313,8 +564,7 @@ class NewOrderBatchHandler(BatchHandler):
 | 
			
		|||
                row.local_product = local
 | 
			
		||||
 | 
			
		||||
            else: # external product_id
 | 
			
		||||
                #row.product_id = product_info
 | 
			
		||||
                raise NotImplementedError
 | 
			
		||||
                row.product_id = product_info
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            # pending_product
 | 
			
		||||
| 
						 | 
				
			
			@ -493,6 +743,7 @@ class NewOrderBatchHandler(BatchHandler):
 | 
			
		|||
        There is no default logic here; subclass must implement as
 | 
			
		||||
        needed.
 | 
			
		||||
        """
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def remove_row(self, row):
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
# -*- coding: utf-8; -*-
 | 
			
		||||
################################################################################
 | 
			
		||||
#
 | 
			
		||||
#  Sideshow -- Case/Special Order Tracker
 | 
			
		||||
#  Copyright © 2024 Lance Edgar
 | 
			
		||||
#
 | 
			
		||||
#  This file is part of Sideshow.
 | 
			
		||||
#
 | 
			
		||||
#  Sideshow is free software: you can redistribute it and/or modify it
 | 
			
		||||
#  under the terms of the GNU General Public License as published by
 | 
			
		||||
#  the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
#  (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
#  Sideshow is distributed in the hope that it will be useful, but
 | 
			
		||||
#  WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 | 
			
		||||
#  General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
#  You should have received a copy of the GNU General Public License
 | 
			
		||||
#  along with Sideshow.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
#
 | 
			
		||||
################################################################################
 | 
			
		||||
"""
 | 
			
		||||
Sideshow web app
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def includeme(config):
 | 
			
		||||
    config.include('sideshow.web.static')
 | 
			
		||||
    config.include('wuttaweb.subscribers')
 | 
			
		||||
    config.include('sideshow.web.views')
 | 
			
		||||
| 
						 | 
				
			
			@ -42,9 +42,7 @@ def main(global_config, **settings):
 | 
			
		|||
    pyramid_config = base.make_pyramid_config(settings)
 | 
			
		||||
 | 
			
		||||
    # bring in the rest of Sideshow
 | 
			
		||||
    pyramid_config.include('sideshow.web.static')
 | 
			
		||||
    pyramid_config.include('wuttaweb.subscribers')
 | 
			
		||||
    pyramid_config.include('sideshow.web.views')
 | 
			
		||||
    pyramid_config.include('sideshow.web')
 | 
			
		||||
 | 
			
		||||
    return pyramid_config.make_wsgi_app()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,15 +3,38 @@
 | 
			
		|||
 | 
			
		||||
<%def name="form_content()">
 | 
			
		||||
 | 
			
		||||
  <h3 class="block is-size-3">Customers</h3>
 | 
			
		||||
  <div class="block" style="padding-left: 2rem;">
 | 
			
		||||
 | 
			
		||||
    <b-field label="Customer Source">
 | 
			
		||||
      <b-select name="sideshow.orders.use_local_customers"
 | 
			
		||||
                  v-model="simpleSettings['sideshow.orders.use_local_customers']"
 | 
			
		||||
                  @input="settingsNeedSaved = true">
 | 
			
		||||
        <option value="true">Local Customers (in Sideshow)</option>
 | 
			
		||||
        <option value="false">External Customers (e.g. in POS)</option>
 | 
			
		||||
      </b-select>
 | 
			
		||||
    </b-field>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <h3 class="block is-size-3">Products</h3>
 | 
			
		||||
  <div class="block" style="padding-left: 2rem;">
 | 
			
		||||
 | 
			
		||||
    <b-field message="If set, user can enter details of an arbitrary new "pending" product.">
 | 
			
		||||
    <b-field label="Product Source">
 | 
			
		||||
      <b-select name="sideshow.orders.use_local_products"
 | 
			
		||||
                  v-model="simpleSettings['sideshow.orders.use_local_products']"
 | 
			
		||||
                  @input="settingsNeedSaved = true">
 | 
			
		||||
        <option value="true">Local Products (in Sideshow)</option>
 | 
			
		||||
        <option value="false">External Products (e.g. in POS)</option>
 | 
			
		||||
      </b-select>
 | 
			
		||||
    </b-field>
 | 
			
		||||
 | 
			
		||||
    <b-field label="New/Unknown Products"
 | 
			
		||||
             message="If set, user can enter details of an arbitrary new "pending" 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
 | 
			
		||||
        Allow creating orders for new/unknown products
 | 
			
		||||
      </b-checkbox>
 | 
			
		||||
    </b-field>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -57,6 +57,11 @@ class OrderView(MasterView):
 | 
			
		|||
 | 
			
		||||
    Note that the "edit" view is not exposed here; user must perform
 | 
			
		||||
    various other workflow actions to modify the order.
 | 
			
		||||
 | 
			
		||||
    .. attribute:: batch_handler
 | 
			
		||||
 | 
			
		||||
       Reference to the new order batch handler, as returned by
 | 
			
		||||
       :meth:`get_batch_handler()`.  This gets set in the constructor.
 | 
			
		||||
    """
 | 
			
		||||
    model_class = Order
 | 
			
		||||
    editable = False
 | 
			
		||||
| 
						 | 
				
			
			@ -153,6 +158,24 @@ class OrderView(MasterView):
 | 
			
		|||
        # total_price
 | 
			
		||||
        g.set_renderer('total_price', g.render_currency)
 | 
			
		||||
 | 
			
		||||
    def get_batch_handler(self):
 | 
			
		||||
        """
 | 
			
		||||
        Returns the configured :term:`handler` for :term:`new order
 | 
			
		||||
        batches <new order batch>`.
 | 
			
		||||
 | 
			
		||||
        You normally would not need to call this; just use
 | 
			
		||||
        :attr:`batch_handler` instead.
 | 
			
		||||
 | 
			
		||||
        :returns:
 | 
			
		||||
           :class:`~sideshow.batch.neworder.NewOrderBatchHandler`
 | 
			
		||||
           instance.
 | 
			
		||||
        """
 | 
			
		||||
        if hasattr(self, 'batch_handler'):
 | 
			
		||||
            return self.batch_handler
 | 
			
		||||
 | 
			
		||||
        # TODO
 | 
			
		||||
        return NewOrderBatchHandler(self.config)
 | 
			
		||||
 | 
			
		||||
    def create(self):
 | 
			
		||||
        """
 | 
			
		||||
        Instead of the typical "create" view, this displays a "wizard"
 | 
			
		||||
| 
						 | 
				
			
			@ -185,7 +208,7 @@ class OrderView(MasterView):
 | 
			
		|||
        """
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
        self.creating = True
 | 
			
		||||
        self.batch_handler = NewOrderBatchHandler(self.config)
 | 
			
		||||
        self.batch_handler = self.get_batch_handler()
 | 
			
		||||
        batch = self.get_current_batch()
 | 
			
		||||
 | 
			
		||||
        context = self.get_context_customer(batch)
 | 
			
		||||
| 
						 | 
				
			
			@ -223,6 +246,7 @@ class OrderView(MasterView):
 | 
			
		|||
                try:
 | 
			
		||||
                    result = getattr(self, action)(batch, data)
 | 
			
		||||
                except Exception as error:
 | 
			
		||||
                    log.warning("error calling json action for order", exc_info=True)
 | 
			
		||||
                    result = {'error': self.app.render_error(error)}
 | 
			
		||||
                return self.json_response(result)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -279,91 +303,49 @@ class OrderView(MasterView):
 | 
			
		|||
        """
 | 
			
		||||
        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.
 | 
			
		||||
        This invokes one of the following on the
 | 
			
		||||
        :attr:`batch_handler`:
 | 
			
		||||
 | 
			
		||||
        * :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.autocomplete_customers_external()`
 | 
			
		||||
        * :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.autocomplete_customers_local()`
 | 
			
		||||
 | 
			
		||||
        :returns: List of search results; each should be a dict with
 | 
			
		||||
           ``value`` and ``label`` keys.
 | 
			
		||||
        """
 | 
			
		||||
        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]
 | 
			
		||||
        handler = self.get_batch_handler()
 | 
			
		||||
        if handler.use_local_customers():
 | 
			
		||||
            return handler.autocomplete_customers_local(session, term, user=self.request.user)
 | 
			
		||||
        else:
 | 
			
		||||
            return handler.autocomplete_customers_external(session, term, user=self.request.user)
 | 
			
		||||
 | 
			
		||||
    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.
 | 
			
		||||
        This invokes one of the following on the
 | 
			
		||||
        :attr:`batch_handler`:
 | 
			
		||||
 | 
			
		||||
        * :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.autocomplete_products_external()`
 | 
			
		||||
        * :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.autocomplete_products_local()`
 | 
			
		||||
 | 
			
		||||
        :returns: List of search results; each should be a dict with
 | 
			
		||||
           ``value`` and ``label`` keys.
 | 
			
		||||
        """
 | 
			
		||||
        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]
 | 
			
		||||
        handler = self.get_batch_handler()
 | 
			
		||||
        if handler.use_local_products():
 | 
			
		||||
            return handler.autocomplete_products_local(session, term, user=self.request.user)
 | 
			
		||||
        else:
 | 
			
		||||
            return handler.autocomplete_products_external(session, term, user=self.request.user)
 | 
			
		||||
 | 
			
		||||
    def get_pending_product_required_fields(self):
 | 
			
		||||
        """ """
 | 
			
		||||
| 
						 | 
				
			
			@ -531,11 +513,12 @@ class OrderView(MasterView):
 | 
			
		|||
        if not product_id:
 | 
			
		||||
            return {'error': "Must specify a product ID"}
 | 
			
		||||
 | 
			
		||||
        session = self.Session()
 | 
			
		||||
        use_local = self.batch_handler.use_local_products()
 | 
			
		||||
        if use_local:
 | 
			
		||||
            data = self.get_local_product_info(product_id)
 | 
			
		||||
            data = self.batch_handler.get_product_info_local(session, product_id)
 | 
			
		||||
        else:
 | 
			
		||||
            raise NotImplementedError("TODO: add integration handler")
 | 
			
		||||
            data = self.batch_handler.get_product_info_external(session, product_id)
 | 
			
		||||
 | 
			
		||||
        if 'error' in data:
 | 
			
		||||
            return data
 | 
			
		||||
| 
						 | 
				
			
			@ -571,32 +554,6 @@ class OrderView(MasterView):
 | 
			
		|||
 | 
			
		||||
        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,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def add_item(self, batch, data):
 | 
			
		||||
        """
 | 
			
		||||
        This adds a row to the user's current new order batch.
 | 
			
		||||
| 
						 | 
				
			
			@ -725,6 +682,9 @@ class OrderView(MasterView):
 | 
			
		|||
            'product_brand': row.product_brand,
 | 
			
		||||
            'product_description': row.product_description,
 | 
			
		||||
            'product_size': row.product_size,
 | 
			
		||||
            'product_full_description': self.app.make_full_name(row.product_brand,
 | 
			
		||||
                                                                row.product_description,
 | 
			
		||||
                                                                row.product_size),
 | 
			
		||||
            'product_weighed': row.product_weighed,
 | 
			
		||||
            'department_display': row.department_name,
 | 
			
		||||
            'special_order': row.special_order,
 | 
			
		||||
| 
						 | 
				
			
			@ -751,15 +711,6 @@ class OrderView(MasterView):
 | 
			
		|||
        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:
 | 
			
		||||
| 
						 | 
				
			
			@ -912,7 +863,17 @@ class OrderView(MasterView):
 | 
			
		|||
        """ """
 | 
			
		||||
        settings = [
 | 
			
		||||
 | 
			
		||||
            # customers
 | 
			
		||||
            {'name': 'sideshow.orders.use_local_customers',
 | 
			
		||||
             # nb. this is really a bool but we present as string in config UI
 | 
			
		||||
             #'type': bool,
 | 
			
		||||
             'default': 'true'},
 | 
			
		||||
 | 
			
		||||
            # products
 | 
			
		||||
            {'name': 'sideshow.orders.use_local_products',
 | 
			
		||||
             # nb. this is really a bool but we present as string in config UI
 | 
			
		||||
             #'type': bool,
 | 
			
		||||
             'default': 'true'},
 | 
			
		||||
            {'name': 'sideshow.orders.allow_unknown_products',
 | 
			
		||||
             'type': bool,
 | 
			
		||||
             'default': True},
 | 
			
		||||
| 
						 | 
				
			
			@ -1045,8 +1006,6 @@ class OrderItemView(MasterView):
 | 
			
		|||
        'department_id',
 | 
			
		||||
        'department_name',
 | 
			
		||||
        'special_order',
 | 
			
		||||
        'order_qty',
 | 
			
		||||
        'order_uom',
 | 
			
		||||
        'case_size',
 | 
			
		||||
        'unit_cost',
 | 
			
		||||
        'unit_price_reg',
 | 
			
		||||
| 
						 | 
				
			
			@ -1054,6 +1013,8 @@ class OrderItemView(MasterView):
 | 
			
		|||
        'sale_ends',
 | 
			
		||||
        'unit_price_quoted',
 | 
			
		||||
        'case_price_quoted',
 | 
			
		||||
        'order_qty',
 | 
			
		||||
        'order_uom',
 | 
			
		||||
        'discount_percent',
 | 
			
		||||
        'total_price',
 | 
			
		||||
        'status_code',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,6 +50,34 @@ class TestNewOrderBatchHandler(DataTestCase):
 | 
			
		|||
        config.setdefault('sideshow.orders.allow_unknown_products', 'false')
 | 
			
		||||
        self.assertFalse(handler.allow_unknown_products())
 | 
			
		||||
 | 
			
		||||
    def test_autocomplete_customers_external(self):
 | 
			
		||||
        handler = self.make_handler()
 | 
			
		||||
        self.assertRaises(NotImplementedError, handler.autocomplete_customers_external,
 | 
			
		||||
                          self.session, 'jack')
 | 
			
		||||
 | 
			
		||||
    def test_autocomplete_cutomers_local(self):
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        handler = self.make_handler()
 | 
			
		||||
 | 
			
		||||
        # empty results by default
 | 
			
		||||
        self.assertEqual(handler.autocomplete_customers_local(self.session, 'foo'), [])
 | 
			
		||||
 | 
			
		||||
        # add a customer
 | 
			
		||||
        customer = model.LocalCustomer(full_name="Chuck Norris")
 | 
			
		||||
        self.session.add(customer)
 | 
			
		||||
        self.session.flush()
 | 
			
		||||
 | 
			
		||||
        # search for chuck finds chuck
 | 
			
		||||
        results = handler.autocomplete_customers_local(self.session, 'chuck')
 | 
			
		||||
        self.assertEqual(len(results), 1)
 | 
			
		||||
        self.assertEqual(results[0], {
 | 
			
		||||
            'value': customer.uuid.hex,
 | 
			
		||||
            'label': "Chuck Norris",
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        # search for sally finds nothing
 | 
			
		||||
        self.assertEqual(handler.autocomplete_customers_local(self.session, 'sally'), [])
 | 
			
		||||
 | 
			
		||||
    def test_set_customer(self):
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        handler = self.make_handler()
 | 
			
		||||
| 
						 | 
				
			
			@ -146,6 +174,83 @@ class TestNewOrderBatchHandler(DataTestCase):
 | 
			
		|||
        self.assertIsNone(batch.phone_number)
 | 
			
		||||
        self.assertIsNone(batch.email_address)
 | 
			
		||||
 | 
			
		||||
    def test_autocomplete_products_external(self):
 | 
			
		||||
        handler = self.make_handler()
 | 
			
		||||
        self.assertRaises(NotImplementedError, handler.autocomplete_products_external,
 | 
			
		||||
                          self.session, 'cheese')
 | 
			
		||||
 | 
			
		||||
    def test_autocomplete_products_local(self):
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        handler = self.make_handler()
 | 
			
		||||
 | 
			
		||||
        # empty results by default
 | 
			
		||||
        self.assertEqual(handler.autocomplete_products_local(self.session, 'foo'), [])
 | 
			
		||||
 | 
			
		||||
        # add a product
 | 
			
		||||
        product = model.LocalProduct(brand_name="Bragg's", description="Vinegar")
 | 
			
		||||
        self.session.add(product)
 | 
			
		||||
        self.session.flush()
 | 
			
		||||
 | 
			
		||||
        # search for vinegar finds product
 | 
			
		||||
        results = handler.autocomplete_products_local(self.session, 'vinegar')
 | 
			
		||||
        self.assertEqual(len(results), 1)
 | 
			
		||||
        self.assertEqual(results[0], {
 | 
			
		||||
            'value': product.uuid.hex,
 | 
			
		||||
            'label': "Bragg's Vinegar",
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        # search for brag finds product
 | 
			
		||||
        results = handler.autocomplete_products_local(self.session, 'brag')
 | 
			
		||||
        self.assertEqual(len(results), 1)
 | 
			
		||||
        self.assertEqual(results[0], {
 | 
			
		||||
            'value': product.uuid.hex,
 | 
			
		||||
            'label': "Bragg's Vinegar",
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        # search for juice finds nothing
 | 
			
		||||
        self.assertEqual(handler.autocomplete_products_local(self.session, 'juice'), [])
 | 
			
		||||
 | 
			
		||||
    def test_get_product_info_external(self):
 | 
			
		||||
        handler = self.make_handler()
 | 
			
		||||
        self.assertRaises(NotImplementedError, handler.get_product_info_external,
 | 
			
		||||
                          self.session, '07430500132')
 | 
			
		||||
 | 
			
		||||
    def test_get_product_info_local(self):
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        handler = self.make_handler()
 | 
			
		||||
 | 
			
		||||
        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()
 | 
			
		||||
 | 
			
		||||
        batch = handler.make_batch(self.session, created_by=user)
 | 
			
		||||
        self.session.add(batch)
 | 
			
		||||
 | 
			
		||||
        # typical, for local product
 | 
			
		||||
        info = handler.get_product_info_local(self.session, local.uuid.hex)
 | 
			
		||||
        self.assertEqual(info['product_id'], local.uuid.hex)
 | 
			
		||||
        self.assertEqual(info['scancode'], '07430500132')
 | 
			
		||||
        self.assertEqual(info['brand_name'], 'Bragg')
 | 
			
		||||
        self.assertEqual(info['description'], 'Vinegar')
 | 
			
		||||
        self.assertEqual(info['size'], '32oz')
 | 
			
		||||
        self.assertEqual(info['full_description'], 'Bragg Vinegar 32oz')
 | 
			
		||||
        self.assertEqual(info['case_size'], 12)
 | 
			
		||||
        self.assertEqual(info['unit_price_reg'], decimal.Decimal('5.99'))
 | 
			
		||||
 | 
			
		||||
        # error if no product_id
 | 
			
		||||
        self.assertRaises(ValueError, handler.get_product_info_local, self.session, None)
 | 
			
		||||
 | 
			
		||||
        # error if product not found
 | 
			
		||||
        mock_uuid = self.app.make_true_uuid()
 | 
			
		||||
        self.assertRaises(ValueError, handler.get_product_info_local, self.session, mock_uuid.hex)
 | 
			
		||||
 | 
			
		||||
    def test_add_item(self):
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
| 
						 | 
				
			
			@ -719,10 +824,8 @@ class TestNewOrderBatchHandler(DataTestCase):
 | 
			
		|||
        self.session.add(row)
 | 
			
		||||
        self.session.flush()
 | 
			
		||||
        # STATUS_OK
 | 
			
		||||
        row = handler.make_row(product_id=42, order_qty=1, order_uom=enum.ORDER_UOM_UNIT)
 | 
			
		||||
        handler.add_row(batch, row)
 | 
			
		||||
        self.session.add(row)
 | 
			
		||||
        self.session.commit()
 | 
			
		||||
        row = handler.add_item(batch, {'scancode': '07430500132'}, 1, enum.ORDER_UOM_UNIT)
 | 
			
		||||
        self.session.flush()
 | 
			
		||||
 | 
			
		||||
        # only 1 effective row
 | 
			
		||||
        rows = handler.get_effective_rows(batch)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -177,7 +177,9 @@ class TestOrderView(WebTestCase):
 | 
			
		|||
 | 
			
		||||
    def test_customer_autocomplete(self):
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        handler = self.make_handler()
 | 
			
		||||
        view = self.make_view()
 | 
			
		||||
        view.batch_handler = handler
 | 
			
		||||
 | 
			
		||||
        with patch.object(view, 'Session', return_value=self.session):
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -205,9 +207,16 @@ class TestOrderView(WebTestCase):
 | 
			
		|||
                result = view.customer_autocomplete()
 | 
			
		||||
                self.assertEqual(result, [])
 | 
			
		||||
 | 
			
		||||
            # external lookup not implemented by default
 | 
			
		||||
            with patch.object(handler, 'use_local_customers', return_value=False):
 | 
			
		||||
                with patch.object(self.request, 'GET', new={'term': 'sally'}, create=True):
 | 
			
		||||
                    self.assertRaises(NotImplementedError, view.customer_autocomplete)
 | 
			
		||||
 | 
			
		||||
    def test_product_autocomplete(self):
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        handler = self.make_handler()
 | 
			
		||||
        view = self.make_view()
 | 
			
		||||
        view.batch_handler = handler
 | 
			
		||||
 | 
			
		||||
        with patch.object(view, 'Session', return_value=self.session):
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -244,6 +253,11 @@ class TestOrderView(WebTestCase):
 | 
			
		|||
                result = view.product_autocomplete()
 | 
			
		||||
                self.assertEqual(result, [])
 | 
			
		||||
 | 
			
		||||
            # external lookup not implemented by default
 | 
			
		||||
            with patch.object(handler, 'use_local_products', return_value=False):
 | 
			
		||||
                with patch.object(self.request, 'GET', new={'term': 'juice'}, create=True):
 | 
			
		||||
                    self.assertRaises(NotImplementedError, view.product_autocomplete)
 | 
			
		||||
 | 
			
		||||
    def test_get_pending_product_required_fields(self):
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        view = self.make_view()
 | 
			
		||||
| 
						 | 
				
			
			@ -529,20 +543,27 @@ class TestOrderView(WebTestCase):
 | 
			
		|||
                    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)
 | 
			
		||||
                    # error if product not found
 | 
			
		||||
                    mock_uuid = self.app.make_true_uuid()
 | 
			
		||||
                    self.assertRaises(ValueError, view.get_product_info,
 | 
			
		||||
                                      batch, {'product_id': mock_uuid.hex})
 | 
			
		||||
 | 
			
		||||
                    with patch.object(handler, 'use_local_products', return_value=False):
 | 
			
		||||
 | 
			
		||||
                        # external lookup not implemented by default
 | 
			
		||||
                        self.assertRaises(NotImplementedError, view.get_product_info,
 | 
			
		||||
                                          batch, {'product_id': '42'})
 | 
			
		||||
 | 
			
		||||
                        # external lookup may return its own error
 | 
			
		||||
                        with patch.object(handler, 'get_product_info_external',
 | 
			
		||||
                                          return_value={'error': "something smells fishy"}):
 | 
			
		||||
                            context = view.get_product_info(batch, {'product_id': '42'})
 | 
			
		||||
                            self.assertEqual(context, {'error': "something smells fishy"})
 | 
			
		||||
 | 
			
		||||
    def test_add_item(self):
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
| 
						 | 
				
			
			@ -970,33 +991,47 @@ class TestOrderView(WebTestCase):
 | 
			
		|||
 | 
			
		||||
        # the next few tests will morph 2nd row..
 | 
			
		||||
 | 
			
		||||
        def refresh_external(row):
 | 
			
		||||
            row.product_scancode = '012345'
 | 
			
		||||
            row.product_brand = 'Acme'
 | 
			
		||||
            row.product_description = 'Bricks'
 | 
			
		||||
            row.product_size = '1 ton'
 | 
			
		||||
            row.product_weighed = True
 | 
			
		||||
            row.department_id = 1
 | 
			
		||||
            row.department_name = "Bricks & Mortar"
 | 
			
		||||
            row.special_order = False
 | 
			
		||||
            row.case_size = None
 | 
			
		||||
            row.unit_cost = decimal.Decimal('599.99')
 | 
			
		||||
            row.unit_price_reg = decimal.Decimal('999.99')
 | 
			
		||||
 | 
			
		||||
        # typical, external product
 | 
			
		||||
        row2.product_id = '42'
 | 
			
		||||
        with patch.object(handler, 'use_local_products', return_value=False):
 | 
			
		||||
            data = view.normalize_row(row2)
 | 
			
		||||
            with patch.object(handler, 'refresh_row_from_external_product', new=refresh_external):
 | 
			
		||||
                handler.update_item(row2, '42', 1, enum.ORDER_UOM_UNIT)
 | 
			
		||||
                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.assertEqual(data['product_scancode'], '012345')
 | 
			
		||||
        self.assertEqual(data['product_full_description'], 'Acme Bricks 1 ton')
 | 
			
		||||
        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.assertEqual(data['unit_price_reg'], 999.99)
 | 
			
		||||
        self.assertEqual(data['unit_price_reg_display'], '$999.99')
 | 
			
		||||
        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.assertEqual(data['unit_price_quoted'], 999.99)
 | 
			
		||||
        self.assertEqual(data['unit_price_quoted_display'], '$999.99')
 | 
			
		||||
        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['total_price'], 999.99)
 | 
			
		||||
        self.assertEqual(data['total_price_display'], '$999.99')
 | 
			
		||||
        self.assertFalse(data['special_order'])
 | 
			
		||||
        self.assertEqual(data['status_code'], row2.STATUS_OK)
 | 
			
		||||
        self.assertNotIn('pending_product', data)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue