diff --git a/pyproject.toml b/pyproject.toml index 4b04493..f250b63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,15 +49,9 @@ sideshow_libcache = "sideshow.web.static:libcache" [project.entry-points."paste.app_factory"] "main" = "sideshow.web.app:main" -[project.entry-points."wutta.batch.neworder"] -"sideshow" = "sideshow.batch.neworder:NewOrderBatchHandler" - [project.entry-points."wutta.config.extensions"] "sideshow" = "sideshow.config:SideshowConfig" -[project.entry-points."wutta.web.menus"] -sideshow = "sideshow.web.menus:SideshowMenuHandler" - [project.urls] Homepage = "https://wuttaproject.org/" diff --git a/src/sideshow/batch/neworder.py b/src/sideshow/batch/neworder.py index e2ce3a4..353af9c 100644 --- a/src/sideshow/batch/neworder.py +++ b/src/sideshow/batch/neworder.py @@ -77,65 +77,6 @@ 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. @@ -150,14 +91,14 @@ class NewOrderBatchHandler(BatchHandler): :class:`~sideshow.db.model.customers.PendingCustomer` record is created if necessary. - And then it will update customer-related attributes via one of: + And then it will update these accordingly: - * :meth:`refresh_batch_from_external_customer()` - * :meth:`refresh_batch_from_local_customer()` - * :meth:`refresh_batch_from_pending_customer()` + * :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 - customer attributes to be set to ``None`` also. + all the above to be set to ``None`` also. :param batch: :class:`~sideshow.db.model.batch.neworder.NewOrderBatch` to @@ -188,11 +129,13 @@ class NewOrderBatchHandler(BatchHandler): if not customer: raise ValueError("local customer not found") batch.local_customer = customer - self.refresh_batch_from_local_customer(batch) + batch.customer_name = customer.full_name + batch.phone_number = customer.phone_number + batch.email_address = customer.email_address else: # external customer_id - batch.customer_id = customer_info - self.refresh_batch_from_external_customer(batch) + #batch.customer_id = customer_info + raise NotImplementedError elif customer_info: @@ -217,7 +160,9 @@ class NewOrderBatchHandler(BatchHandler): if 'full_name' not in customer_info: customer.full_name = self.app.make_full_name(customer.first_name, customer.last_name) - self.refresh_batch_from_pending_customer(batch) + batch.customer_name = customer.full_name + batch.phone_number = customer.phone_number + batch.email_address = customer.email_address else: @@ -230,203 +175,6 @@ 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. @@ -476,7 +224,8 @@ class NewOrderBatchHandler(BatchHandler): row.local_product = local else: # external product_id - row.product_id = product_info + #row.product_id = product_info + raise NotImplementedError else: # pending_product @@ -564,7 +313,8 @@ class NewOrderBatchHandler(BatchHandler): row.local_product = local else: # external product_id - row.product_id = product_info + #row.product_id = product_info + raise NotImplementedError else: # pending_product @@ -743,7 +493,6 @@ 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/config.py b/src/sideshow/config.py index a010262..2a414aa 100644 --- a/src/sideshow/config.py +++ b/src/sideshow/config.py @@ -46,12 +46,8 @@ class SideshowConfig(WuttaConfigExtension): config.setdefault(f'{config.appname}.model_spec', 'sideshow.db.model') config.setdefault(f'{config.appname}.enum_spec', 'sideshow.enum') - # batch handlers - config.setdefault(f'{config.appname}.batch.neworder.handler.default_spec', - 'sideshow.batch.neworder:NewOrderBatchHandler') - # web app menu - config.setdefault(f'{config.appname}.web.menus.handler.default_spec', + config.setdefault(f'{config.appname}.web.menus.handler_spec', 'sideshow.web.menus:SideshowMenuHandler') # web app libcache diff --git a/src/sideshow/web/__init__.py b/src/sideshow/web/__init__.py index b86b498..e69de29 100644 --- a/src/sideshow/web/__init__.py +++ b/src/sideshow/web/__init__.py @@ -1,31 +0,0 @@ -# -*- 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 77c28ab..66ff8c3 100644 --- a/src/sideshow/web/app.py +++ b/src/sideshow/web/app.py @@ -42,7 +42,9 @@ def main(global_config, **settings): pyramid_config = base.make_pyramid_config(settings) # bring in the rest of Sideshow - pyramid_config.include('sideshow.web') + pyramid_config.include('sideshow.web.static') + pyramid_config.include('wuttaweb.subscribers') + pyramid_config.include('sideshow.web.views') return pyramid_config.make_wsgi_app() diff --git a/src/sideshow/web/menus.py b/src/sideshow/web/menus.py index c8256fc..2c319cc 100644 --- a/src/sideshow/web/menus.py +++ b/src/sideshow/web/menus.py @@ -39,7 +39,6 @@ class SideshowMenuHandler(base.MenuHandler): self.make_customers_menu(request), self.make_products_menu(request), self.make_batch_menu(request), - self.make_other_menu(request), self.make_admin_menu(request), ] @@ -128,16 +127,6 @@ class SideshowMenuHandler(base.MenuHandler): ], } - def make_other_menu(self, request, **kwargs): - """ - Generate the "Other" menu. - """ - return { - 'title': "Other", - 'type': 'menu', - 'items': [], - } - def make_admin_menu(self, request, **kwargs): """ """ kwargs['include_people'] = True diff --git a/src/sideshow/web/templates/orders/configure.mako b/src/sideshow/web/templates/orders/configure.mako index 10d58fa..4dc23f4 100644 --- a/src/sideshow/web/templates/orders/configure.mako +++ b/src/sideshow/web/templates/orders/configure.mako @@ -3,38 +3,15 @@ <%def name="form_content()"> -

Customers

-
- - - - - - - -
-

Products

- - - - - - - - + - Allow creating orders for new/unknown products + Allow creating orders for "unknown" products @@ -61,32 +38,4 @@
- -

Batches

-
- - - - - - - - -
- - -<%def name="modify_vue_vars()"> - ${parent.modify_vue_vars()} - diff --git a/src/sideshow/web/templates/orders/create.mako b/src/sideshow/web/templates/orders/create.mako index 7ed78db..72345b5 100644 --- a/src/sideshow/web/templates/orders/create.mako +++ b/src/sideshow/web/templates/orders/create.mako @@ -836,11 +836,11 @@ orderEmailAddress: ${json.dumps(email_address)|n}, refreshingCustomer: false, - newCustomerFullName: ${json.dumps(new_customer_full_name or None)|n}, - newCustomerFirstName: ${json.dumps(new_customer_first_name or None)|n}, - newCustomerLastName: ${json.dumps(new_customer_last_name or None)|n}, - newCustomerPhone: ${json.dumps(new_customer_phone or None)|n}, - newCustomerEmail: ${json.dumps(new_customer_email or None)|n}, + 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}, + newCustomerEmail: ${json.dumps(new_customer_email)|n}, editNewCustomerShowDialog: false, editNewCustomerFirstName: null, diff --git a/src/sideshow/web/views/orders.py b/src/sideshow/web/views/orders.py index a48546e..bbd5811 100644 --- a/src/sideshow/web/views/orders.py +++ b/src/sideshow/web/views/orders.py @@ -57,11 +57,6 @@ 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 @@ -158,22 +153,6 @@ 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, and can use - :attr:`batch_handler` instead. - - :returns: - :class:`~sideshow.batch.neworder.NewOrderBatchHandler` - instance. - """ - if hasattr(self, 'batch_handler'): - return self.batch_handler - return self.app.get_batch_handler('neworder') - def create(self): """ Instead of the typical "create" view, this displays a "wizard" @@ -206,7 +185,7 @@ class OrderView(MasterView): """ enum = self.app.enum self.creating = True - self.batch_handler = self.get_batch_handler() + self.batch_handler = NewOrderBatchHandler(self.config) batch = self.get_current_batch() context = self.get_context_customer(batch) @@ -244,7 +223,6 @@ 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) @@ -301,49 +279,91 @@ class OrderView(MasterView): """ AJAX view for customer autocomplete, when entering new order. - 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. + 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) - 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) + # 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 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. + 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) - 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) + # 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): """ """ @@ -511,12 +531,11 @@ 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.batch_handler.get_product_info_local(session, product_id) + data = self.get_local_product_info(product_id) else: - data = self.batch_handler.get_product_info_external(session, product_id) + raise NotImplementedError("TODO: add integration handler") if 'error' in data: return data @@ -552,6 +571,32 @@ 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. @@ -680,9 +725,6 @@ 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, @@ -709,6 +751,15 @@ 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: @@ -861,20 +912,7 @@ class OrderView(MasterView): """ """ settings = [ - # batches - {'name': 'wutta.batch.neworder.handler.spec'}, - - # 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}, @@ -896,10 +934,6 @@ class OrderView(MasterView): context['pending_product_fields'] = self.PENDING_PRODUCT_ENTRY_FIELDS - handlers = self.app.get_batch_handler_specs('neworder') - handlers = [{'spec': spec} for spec in handlers] - context['batch_handlers'] = handlers - return context @classmethod @@ -1011,6 +1045,8 @@ class OrderItemView(MasterView): 'department_id', 'department_name', 'special_order', + 'order_qty', + 'order_uom', 'case_size', 'unit_cost', 'unit_price_reg', @@ -1018,8 +1054,6 @@ 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 b3fbf4a..719efcb 100644 --- a/tests/batch/test_neworder.py +++ b/tests/batch/test_neworder.py @@ -50,34 +50,6 @@ 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() @@ -174,83 +146,6 @@ 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 @@ -824,8 +719,10 @@ class TestNewOrderBatchHandler(DataTestCase): self.session.add(row) self.session.flush() # STATUS_OK - row = handler.add_item(batch, {'scancode': '07430500132'}, 1, enum.ORDER_UOM_UNIT) - self.session.flush() + 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() # only 1 effective row rows = handler.get_effective_rows(batch) diff --git a/tests/web/test_menus.py b/tests/web/test_menus.py index ed64232..36fdc56 100644 --- a/tests/web/test_menus.py +++ b/tests/web/test_menus.py @@ -9,12 +9,4 @@ class TestSideshowMenuHandler(WebTestCase): def test_make_menus(self): handler = mod.SideshowMenuHandler(self.config) menus = handler.make_menus(self.request) - titles = [menu['title'] for menu in menus] - self.assertEqual(titles, [ - 'Orders', - 'Customers', - 'Products', - 'Batches', - 'Other', - 'Admin', - ]) + self.assertEqual(len(menus), 5) diff --git a/tests/web/views/test_orders.py b/tests/web/views/test_orders.py index 98d00cd..ab996f1 100644 --- a/tests/web/views/test_orders.py +++ b/tests/web/views/test_orders.py @@ -42,8 +42,6 @@ class TestOrderView(WebTestCase): def test_create(self): self.pyramid_config.include('sideshow.web.views') - self.config.setdefault('wutta.batch.neworder.handler.spec', - 'sideshow.batch.neworder:NewOrderBatchHandler') model = self.app.model enum = self.app.enum view = self.make_view() @@ -179,9 +177,7 @@ 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): @@ -209,16 +205,9 @@ 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): @@ -255,11 +244,6 @@ 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() @@ -545,27 +529,20 @@ 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"}) - # 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}) - + # external lookup not implemented (yet) 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 @@ -993,47 +970,33 @@ 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): - 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) + data = view.normalize_row(row2) self.assertEqual(data['uuid'], row2.uuid.hex) self.assertEqual(data['sequence'], 2) self.assertEqual(data['product_id'], '42') - self.assertEqual(data['product_scancode'], '012345') - self.assertEqual(data['product_full_description'], 'Acme Bricks 1 ton') + 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'], 999.99) - self.assertEqual(data['unit_price_reg_display'], '$999.99') + 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'], 999.99) - self.assertEqual(data['unit_price_quoted_display'], '$999.99') + 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'], 999.99) - self.assertEqual(data['total_price_display'], '$999.99') - self.assertFalse(data['special_order']) + 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)