Add "full lookup" product search modal for new custorder page
This commit is contained in:
parent
025cabd1ad
commit
072f5da69d
|
@ -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
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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()}
|
||||
|
||||
<script type="text/x-template" id="customer-order-creator-template">
|
||||
<div>
|
||||
|
@ -524,6 +526,15 @@
|
|||
@input="productChanged">
|
||||
</tailbone-autocomplete>
|
||||
</b-field>
|
||||
|
||||
<b-button type="is-primary"
|
||||
v-if="!productUUID"
|
||||
@click="productFullLookup()"
|
||||
icon-pack="fas"
|
||||
icon-left="search">
|
||||
Full Lookup
|
||||
</b-button>
|
||||
|
||||
<b-button v-if="productUUID"
|
||||
type="is-primary"
|
||||
tag="a" target="_blank"
|
||||
|
@ -822,6 +833,11 @@
|
|||
</div>
|
||||
</b-modal>
|
||||
|
||||
<tailbone-product-lookup ref="productLookup"
|
||||
@canceled="productLookupCanceled"
|
||||
@selected="productLookupSelected">
|
||||
</tailbone-product-lookup>
|
||||
|
||||
<b-modal :active.sync="pastItemsShowDialog">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
|
@ -1017,6 +1033,7 @@
|
|||
|
||||
<%def name="make_this_page_component()">
|
||||
${parent.make_this_page_component()}
|
||||
${product_lookup.tailbone_product_lookup_component()}
|
||||
<script type="text/javascript">
|
||||
|
||||
const CustomerOrderCreator = {
|
||||
|
@ -1096,7 +1113,6 @@
|
|||
productIsKnown: true,
|
||||
productUUID: null,
|
||||
productDisplay: null,
|
||||
productUPC: null,
|
||||
productKey: null,
|
||||
productKeyField: ${json.dumps(product_key_field)|n},
|
||||
productKeyLabel: ${json.dumps(product_key_label)|n},
|
||||
|
@ -1716,6 +1732,22 @@
|
|||
}
|
||||
},
|
||||
|
||||
productFullLookup() {
|
||||
this.showingItemDialog = false
|
||||
let term = this.$refs.productAutocomplete.getUserInput()
|
||||
this.$refs.productLookup.showDialog(term)
|
||||
},
|
||||
|
||||
productLookupCanceled() {
|
||||
this.showingItemDialog = true
|
||||
},
|
||||
|
||||
productLookupSelected(selected) {
|
||||
this.clearProduct()
|
||||
this.productChanged(selected.uuid)
|
||||
this.showingItemDialog = true
|
||||
},
|
||||
|
||||
copyPendingProductAttrs(from, to) {
|
||||
to.upc = from.upc
|
||||
to.item_id = from.item_id
|
||||
|
@ -1738,7 +1770,6 @@
|
|||
this.productIsKnown = true
|
||||
this.productUUID = null
|
||||
this.productDisplay = null
|
||||
this.productUPC = null
|
||||
this.productKey = null
|
||||
this.productSize = null
|
||||
this.productCaseQuantity = null
|
||||
|
@ -1794,7 +1825,6 @@
|
|||
this.productIsKnown = true
|
||||
this.productUUID = selected.uuid
|
||||
this.productDisplay = selected.full_description
|
||||
this.productUPC = selected.upc_pretty || selected.upc
|
||||
this.productKey = selected.key
|
||||
this.productSize = selected.size
|
||||
this.productCaseQuantity = selected.case_quantity
|
||||
|
@ -1833,7 +1863,6 @@
|
|||
}
|
||||
|
||||
this.productDisplay = row.product_full_description
|
||||
this.productUPC = row.product_upc_pretty || row.product_upc
|
||||
this.productKey = row.product_key
|
||||
this.productSize = row.product_size
|
||||
this.productCaseQuantity = row.case_quantity
|
||||
|
@ -1886,7 +1915,6 @@
|
|||
clearProduct() {
|
||||
this.productUUID = null
|
||||
this.productDisplay = null
|
||||
this.productUPC = null
|
||||
this.productKey = null
|
||||
this.productSize = null
|
||||
this.productCaseQuantity = null
|
||||
|
@ -1936,7 +1964,6 @@
|
|||
// whatever came back from handler
|
||||
this.submitBatchData(params, response => {
|
||||
this.productUUID = response.data.uuid
|
||||
this.productUPC = response.data.upc_pretty
|
||||
this.productKey = response.data.key
|
||||
this.productDisplay = response.data.full_description
|
||||
this.productSize = response.data.size
|
||||
|
|
257
tailbone/templates/products/lookup.mako
Normal file
257
tailbone/templates/products/lookup.mako
Normal file
|
@ -0,0 +1,257 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
|
||||
<%def name="tailbone_product_lookup_template()">
|
||||
<script type="text/x-template" id="tailbone-product-lookup-template">
|
||||
<div>
|
||||
<b-modal :active.sync="showingDialog">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
|
||||
<b-field grouped>
|
||||
|
||||
<b-input v-model="searchTerm"
|
||||
ref="searchTermInput"
|
||||
@keydown.native="searchTermInputKeydown">
|
||||
</b-input>
|
||||
|
||||
<b-button class="control"
|
||||
type="is-primary"
|
||||
@click="performSearch()">
|
||||
Search
|
||||
</b-button>
|
||||
|
||||
<b-checkbox v-model="searchProductKey"
|
||||
native-value="true">
|
||||
${request.rattail_config.product_key_title()}
|
||||
</b-checkbox>
|
||||
|
||||
<b-checkbox v-model="searchVendorItemCode"
|
||||
native-value="true">
|
||||
Vendor Code
|
||||
</b-checkbox>
|
||||
|
||||
<b-checkbox v-model="searchAlternateCode"
|
||||
native-value="true">
|
||||
Alt Code
|
||||
</b-checkbox>
|
||||
|
||||
<b-checkbox v-model="searchProductBrand"
|
||||
native-value="true">
|
||||
Brand
|
||||
</b-checkbox>
|
||||
|
||||
<b-checkbox v-model="searchProductDescription"
|
||||
native-value="true">
|
||||
Description
|
||||
</b-checkbox>
|
||||
|
||||
</b-field>
|
||||
|
||||
<b-table :data="searchResults"
|
||||
narrowed
|
||||
icon-pack="fas"
|
||||
:loading="searchResultsLoading"
|
||||
:selected.sync="searchResultSelected">
|
||||
<template slot-scope="props">
|
||||
|
||||
<b-table-column label="${request.rattail_config.product_key_title()}"
|
||||
field="product_key">
|
||||
{{ props.row.product_key }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column label="Brand"
|
||||
field="brand_name">
|
||||
{{ props.row.brand_name }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column label="Description"
|
||||
field="description">
|
||||
{{ props.row.description }}
|
||||
{{ props.row.size }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column label="Unit Price"
|
||||
field="unit_price">
|
||||
{{ props.row.unit_price_display }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column label="Sale Price"
|
||||
field="sale_price">
|
||||
<span class="has-background-warning">
|
||||
{{ props.row.sale_price_display }}
|
||||
</span>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column label="Sale Ends"
|
||||
field="sale_ends">
|
||||
<span class="has-background-warning">
|
||||
{{ props.row.sale_ends_display }}
|
||||
</span>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column label="Department"
|
||||
field="department_name">
|
||||
{{ props.row.department_name }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column label="Vendor"
|
||||
field="vendor_name">
|
||||
{{ props.row.vendor_name }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column label="Actions">
|
||||
<a :href="props.row.url"
|
||||
target="_blank"
|
||||
class="grid-action">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
View
|
||||
</a>
|
||||
</b-table-column>
|
||||
|
||||
</template>
|
||||
<template slot="empty">
|
||||
<div class="content has-text-grey has-text-centered">
|
||||
<p>
|
||||
<b-icon
|
||||
pack="fas"
|
||||
icon="fas fa-sad-tear"
|
||||
size="is-large">
|
||||
</b-icon>
|
||||
</p>
|
||||
<p>Nothing here.</p>
|
||||
</div>
|
||||
</template>
|
||||
</b-table>
|
||||
|
||||
<br />
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item buttons">
|
||||
<b-button @click="cancelDialog()">
|
||||
Cancel
|
||||
</b-button>
|
||||
<b-button type="is-primary"
|
||||
@click="selectResult()"
|
||||
:disabled="!searchResultSelected">
|
||||
Choose Selected
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<span v-if="searchResultsElided"
|
||||
class="has-text-danger">
|
||||
{{ searchResultsElided }} results are not shown
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
<%def name="tailbone_product_lookup_component()">
|
||||
<script type="text/javascript">
|
||||
|
||||
const TailboneProductLookup = {
|
||||
template: '#tailbone-product-lookup-template',
|
||||
data() {
|
||||
return {
|
||||
showingDialog: false,
|
||||
|
||||
searchTerm: null,
|
||||
searchTermLastUsed: null,
|
||||
|
||||
searchProductKey: true,
|
||||
searchVendorItemCode: true,
|
||||
searchProductBrand: true,
|
||||
searchProductDescription: true,
|
||||
searchAlternateCode: true,
|
||||
|
||||
searchResults: [],
|
||||
searchResultsLoading: false,
|
||||
searchResultsElided: 0,
|
||||
searchResultSelected: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
showDialog(term) {
|
||||
|
||||
this.searchResultSelected = null
|
||||
|
||||
if (term !== undefined) {
|
||||
this.searchTerm = term
|
||||
// perform search if invoked with new term
|
||||
if (term != this.searchTermLastUsed) {
|
||||
this.searchTermLastUsed = null
|
||||
this.performSearch()
|
||||
}
|
||||
} else {
|
||||
this.searchTerm = this.searchTermLastUsed
|
||||
}
|
||||
|
||||
this.showingDialog = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.searchTermInput.focus()
|
||||
})
|
||||
},
|
||||
|
||||
searchTermInputKeydown(event) {
|
||||
if (event.which == 13) {
|
||||
this.performSearch()
|
||||
}
|
||||
},
|
||||
|
||||
cancelDialog() {
|
||||
this.searchResultSelected = null
|
||||
this.showingDialog = false
|
||||
this.$emit('canceled')
|
||||
},
|
||||
|
||||
selectResult() {
|
||||
this.showingDialog = false
|
||||
this.$emit('selected', this.searchResultSelected)
|
||||
},
|
||||
|
||||
performSearch() {
|
||||
if (this.searchResultsLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.searchTerm || !this.searchTerm.length) {
|
||||
this.$refs.searchTermInput.focus()
|
||||
return
|
||||
}
|
||||
|
||||
this.searchResultsLoading = true
|
||||
this.searchResultSelected = null
|
||||
|
||||
let url = '${url('products.search')}'
|
||||
let params = {
|
||||
term: this.searchTerm,
|
||||
search_product_key: this.searchProductKey,
|
||||
search_vendor_code: this.searchVendorItemCode,
|
||||
search_brand_name: this.searchProductBrand,
|
||||
search_description: this.searchProductDescription,
|
||||
search_alt_code: this.searchAlternateCode,
|
||||
}
|
||||
|
||||
this.$http.get(url, {params: params}).then((response) => {
|
||||
this.searchTermLastUsed = params.term
|
||||
this.searchResults = response.data.results
|
||||
this.searchResultsElided = response.data.elided
|
||||
this.searchResultsLoading = false
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Vue.component('tailbone-product-lookup', TailboneProductLookup)
|
||||
|
||||
</script>
|
||||
</%def>
|
|
@ -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
|
||||
``<b-table>`` 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')
|
||||
|
|
Loading…
Reference in a new issue