diff --git a/src/sideshow/batch/neworder.py b/src/sideshow/batch/neworder.py
index 353af9c..e2ce3a4 100644
--- a/src/sideshow/batch/neworder.py
+++ b/src/sideshow/batch/neworder.py
@@ -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):
"""
diff --git a/src/sideshow/web/__init__.py b/src/sideshow/web/__init__.py
index e69de29..b86b498 100644
--- a/src/sideshow/web/__init__.py
+++ b/src/sideshow/web/__init__.py
@@ -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 .
+#
+################################################################################
+"""
+Sideshow web app
+"""
+
+
+def includeme(config):
+ config.include('sideshow.web.static')
+ config.include('wuttaweb.subscribers')
+ config.include('sideshow.web.views')
diff --git a/src/sideshow/web/app.py b/src/sideshow/web/app.py
index 66ff8c3..77c28ab 100644
--- a/src/sideshow/web/app.py
+++ b/src/sideshow/web/app.py
@@ -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()
diff --git a/src/sideshow/web/templates/orders/configure.mako b/src/sideshow/web/templates/orders/configure.mako
index 4dc23f4..a7ffccc 100644
--- a/src/sideshow/web/templates/orders/configure.mako
+++ b/src/sideshow/web/templates/orders/configure.mako
@@ -3,15 +3,38 @@
<%def name="form_content()">
+
Customers
+
+
+
+
+
+
+
+
+
+
Products
-
+
+
+
+
+
+
+
+
- Allow creating orders for "unknown" products
+ Allow creating orders for new/unknown products
diff --git a/src/sideshow/web/views/orders.py b/src/sideshow/web/views/orders.py
index bbd5811..80c9d24 100644
--- a/src/sideshow/web/views/orders.py
+++ b/src/sideshow/web/views/orders.py
@@ -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 `.
+
+ 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',
diff --git a/tests/batch/test_neworder.py b/tests/batch/test_neworder.py
index 719efcb..b3fbf4a 100644
--- a/tests/batch/test_neworder.py
+++ b/tests/batch/test_neworder.py
@@ -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)
diff --git a/tests/web/views/test_orders.py b/tests/web/views/test_orders.py
index ab996f1..278c674 100644
--- a/tests/web/views/test_orders.py
+++ b/tests/web/views/test_orders.py
@@ -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)