From 072f5da69d8af620a420b5cc37a61879ad99ec5b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 8 Feb 2022 12:21:24 -0600 Subject: [PATCH] Add "full lookup" product search modal for new custorder page --- .../static/js/tailbone.buefy.autocomplete.js | 6 + tailbone/templates/custorders/create.mako | 39 ++- tailbone/templates/products/lookup.mako | 257 ++++++++++++++++++ tailbone/views/products.py | 114 +++++++- 4 files changed, 403 insertions(+), 13 deletions(-) create mode 100644 tailbone/templates/products/lookup.mako diff --git a/tailbone/static/js/tailbone.buefy.autocomplete.js b/tailbone/static/js/tailbone.buefy.autocomplete.js index f615c2a9..b4070fab 100644 --- a/tailbone/static/js/tailbone.buefy.autocomplete.js +++ b/tailbone/static/js/tailbone.buefy.autocomplete.js @@ -223,6 +223,12 @@ const TailboneAutocomplete = { // we have nothing to go on here.. return "" }, + + // returns the "raw" user input from the underlying buefy + // autocomplete component + getUserInput() { + return this.buefyValue + }, }, } diff --git a/tailbone/templates/custorders/create.mako b/tailbone/templates/custorders/create.mako index c3aed270..ddabfc4d 100644 --- a/tailbone/templates/custorders/create.mako +++ b/tailbone/templates/custorders/create.mako @@ -1,5 +1,6 @@ ## -*- coding: utf-8; -*- <%inherit file="/master/create.mako" /> +<%namespace name="product_lookup" file="/products/lookup.mako" /> <%def name="extra_styles()"> ${parent.extra_styles()} @@ -54,6 +55,7 @@ <%def name="render_this_page_template()"> ${parent.render_this_page_template()} + ${product_lookup.tailbone_product_lookup_template()} + + +<%def name="tailbone_product_lookup_component()"> + + diff --git a/tailbone/views/products.py b/tailbone/views/products.py index 15b2083f..752a996d 100644 --- a/tailbone/views/products.py +++ b/tailbone/views/products.py @@ -179,9 +179,10 @@ class ProductView(MasterView): 'tailbone', 'products.print_labels', default=False) app = self.get_rattail_app() - self.product_handler = app.get_products_handler() - # TODO: deprecate / remove this - self.handler = self.product_handler + self.products_handler = app.get_products_handler() + # TODO: deprecate / remove these + self.product_handler = self.products_handler + self.handler = self.products_handler def query(self, session): user = self.request.user @@ -535,7 +536,7 @@ class ProductView(MasterView): if not product.not_for_sale: price = product[field] if price: - return self.product_handler.render_price(price) + return self.products_handler.render_price(price) def render_current_price_for_grid(self, product, field): text = self.render_price(product, field) or "" @@ -1173,7 +1174,7 @@ class ProductView(MasterView): key = self.rattail_config.product_key() kwargs['product_key_field'] = self.product_key_fields.get(key, key) - kwargs['image_url'] = self.product_handler.get_image_url(product) + kwargs['image_url'] = self.products_handler.get_image_url(product) # add price history, if user has access if self.rattail_config.versioning_enabled() and self.has_perm('versions'): @@ -1743,6 +1744,105 @@ class ProductView(MasterView): return {'ok': True} def search(self): + """ + Perform a product search across multiple fields, and return + the results as JSON suitable for row data for a Buefy + ```` component. + """ + if 'term' not in self.request.GET: + # TODO: deprecate / remove this? not sure if/where it is used + return self.search_v1() + + term = self.request.GET.get('term') + if not term: + return {'ok': True, 'results': []} + + supported_fields = [ + 'product_key', + 'vendor_code', + 'alt_code', + 'brand_name', + 'description', + ] + + search_fields = [] + for field in supported_fields: + key = 'search_{}'.format(field) + if self.request.GET.get(key) == 'true': + search_fields.append(field) + + final_results = [] + session = self.Session() + model = self.model + + lookup_fields = [] + if 'product_key' in search_fields: + lookup_fields.append('_product_key_') + if 'vendor_code' in search_fields: + lookup_fields.append('vendor_code') + if 'alt_code' in search_fields: + lookup_fields.append('alt_code') + if lookup_fields: + product = self.products_handler.locate_product_for_entry( + session, term, lookup_fields=lookup_fields) + if product: + final_results.append(self.search_normalize_result(product)) + + # base wildcard query + query = session.query(model.Product) + if 'brand_name' in search_fields: + query = query.outerjoin(model.Brand) + + # now figure out wildcard criteria + criteria = [] + for word in term.split(): + if 'brand_name' in search_fields and 'description' in search_fields: + criteria.append(sa.or_( + model.Brand.name.ilike('%{}%'.format(word)), + model.Product.description.ilike('%{}%'.format(word)))) + elif 'brand_name' in search_fields: + criteria.append(model.Brand.name.ilike('%{}%'.format(word))) + elif 'description' in search_fields: + criteria.append(model.Product.description.ilike('%{}%'.format(word))) + + # execute wildcard query if applicable + max_results = 30 # TODO: make conifgurable? + elided = 0 + if criteria: + query = query.filter(sa.and_(*criteria)) + count = query.count() + if count > max_results: + elided = count - max_results + for product in query[:max_results]: + final_results.append(self.search_normalize_result(product)) + + return {'ok': True, 'results': final_results, 'elided': elided} + + def search_normalize_result(self, product, **kwargs): + return self.products_handler.normalize_product(product, fields=[ + 'product_key', + 'url', + 'image_url', + 'brand_name', + 'description', + 'size', + 'full_description', + 'department_name', + 'unit_price', + 'unit_price_display', + 'sale_price', + 'sale_price_display', + 'sale_ends_display', + 'vendor_name', + # TODO: should be case_size + 'case_quantity', + 'case_price', + 'case_price_display', + 'uom_choices', + ]) + + # TODO: deprecate / remove this? not sure if/where it is used + def search_v1(self): """ Locate a product(s) by UPC. @@ -2027,10 +2127,10 @@ class ProductView(MasterView): renderer='{}/batch.mako'.format(template_prefix), permission='{}.make_batch'.format(permission_prefix)) - # search (by upc) + # search config.add_route('products.search', '/products/search') config.add_view(cls, attr='search', route_name='products.search', - renderer='json', permission='products.view') + renderer='json', permission='products.list') # product image config.add_route('products.image', '/products/{uuid}/image')