Clean up the product selection UI for new custorder

still needs some work but this is much better, more like the customer
selection now w/ "multi-faceted" autocomplete
This commit is contained in:
Lance Edgar 2021-10-19 18:14:50 -05:00
parent 8b044dbb22
commit 8d16a5f110
2 changed files with 98 additions and 105 deletions

View file

@ -504,41 +504,22 @@
style="padding-left: 5rem;"> style="padding-left: 5rem;">
<b-field grouped> <b-field grouped>
<b-field label="Description" horizontal expanded> <p class="label control">
<tailbone-autocomplete Product
ref="productDescriptionAutocomplete" </p>
v-model="productUUID" <b-field :expanded="!productUUID">
:assigned-value="productUUID" <tailbone-autocomplete ref="productAutocomplete"
:assigned-label="productDisplay" v-model="productUUID"
serviceUrl="${product_autocomplete_url}" placeholder="Enter UPC or brand, description etc."
@input="productChanged"> :initial-label="productDisplay"
serviceUrl="${url('{}.product_autocomplete'.format(route_prefix))}"
@input="productChanged">
</tailbone-autocomplete> </tailbone-autocomplete>
</b-field> </b-field>
</b-field>
<b-field grouped>
<b-field label="UPC" horizontal expanded>
<b-input v-if="!productUUID"
v-model="productUPC"
ref="productUPCInput"
@keydown.native="productUPCKeyDown">
</b-input>
<b-button v-if="!productUUID"
type="is-primary"
icon-pack="fas"
icon-left="search"
@click="fetchProductByUPC()">
Fetch
</b-button>
<b-button v-if="productUUID"
@click="clearProduct(true)">
{{ productUPC }} (click to change)
</b-button>
</b-field>
<b-button v-if="productUUID" <b-button v-if="productUUID"
type="is-primary" type="is-primary"
tag="a" target="_blank" tag="a" target="_blank"
:href="'${request.route_url('products')}/' + productUUID" :href="productURL"
icon-pack="fas" icon-pack="fas"
icon-left="external-link-alt"> icon-left="external-link-alt">
View Product View Product
@ -546,18 +527,26 @@
</b-field> </b-field>
<div v-if="productUUID"> <div v-if="productUUID">
<b-field grouped
v-if="productUUID"> <div class="is-pulled-right has-text-centered">
<img :src="productImageURL"
style="height: 150px; width: 150px; "/>
<p>{{ productKey }}</p>
</div>
<b-field grouped>
<b-field label="Unit Price"> <b-field label="Unit Price">
<span :class="productPriceNeedsConfirmation ? 'has-background-warning' : ''"> <span :class="productPriceNeedsConfirmation ? 'has-background-warning' : ''">
$4.20 / EA {{ productUnitPriceDisplay }}
</span> </span>
</b-field> </b-field>
<b-field label="Last Changed"> <!-- <b-field label="Last Changed"> -->
<span>2021-01-01</span> <!-- <span>2021-01-01</span> -->
</b-field> <!-- </b-field> -->
</b-field> </b-field>
<b-checkbox v-model="productPriceNeedsConfirmation" <b-checkbox v-model="productPriceNeedsConfirmation"
type="is-warning"
size="is-small"> size="is-small">
This price is questionable and should be confirmed This price is questionable and should be confirmed
by someone before order proceeds. by someone before order proceeds.
@ -759,6 +748,10 @@
productUUID: null, productUUID: null,
productDisplay: null, productDisplay: null,
productUPC: null, productUPC: null,
productKey: null,
productUnitPriceDisplay: null,
productURL: null,
productImageURL: null,
productQuantity: null, productQuantity: null,
defaultUnitChoices: defaultUnitChoices, defaultUnitChoices: defaultUnitChoices,
productUnitChoices: defaultUnitChoices, productUnitChoices: defaultUnitChoices,
@ -1292,13 +1285,15 @@
this.productUUID = null this.productUUID = null
this.productDisplay = null this.productDisplay = null
this.productUPC = null this.productUPC = null
this.productKey = null
this.productUnitPriceDisplay = null
this.productQuantity = 1 this.productQuantity = 1
this.productUnitChoices = this.defaultUnitChoices this.productUnitChoices = this.defaultUnitChoices
this.productUOM = this.defaultUOM this.productUOM = this.defaultUOM
this.productPriceNeedsConfirmation = false this.productPriceNeedsConfirmation = false
this.showingItemDialog = true this.showingItemDialog = true
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.productDescriptionAutocomplete.focus() this.$refs.productAutocomplete.focus()
}) })
}, },
@ -1309,6 +1304,10 @@
this.productUUID = row.product_uuid this.productUUID = row.product_uuid
this.productDisplay = row.product_full_description this.productDisplay = row.product_full_description
this.productUPC = row.product_upc_pretty || row.product_upc this.productUPC = row.product_upc_pretty || row.product_upc
this.productKey = row.product_key
this.productURL = row.product_url
this.productUnitPriceDisplay = row.unit_price_display
this.productImageURL = row.product_image_url
this.productQuantity = row.order_quantity this.productQuantity = row.order_quantity
this.productUnitChoices = row.order_uom_choices this.productUnitChoices = row.order_uom_choices
this.productUOM = row.order_uom this.productUOM = row.order_uom
@ -1340,17 +1339,16 @@
}) })
}, },
clearProduct(autofocus) { clearProduct() {
this.productUUID = null this.productUUID = null
this.productDisplay = null this.productDisplay = null
this.productUPC = null this.productUPC = null
this.productKey = null
this.productUnitPriceDisplay = null
this.productURL = null
this.productImageURL = null
this.productUnitChoices = this.defaultUnitChoices this.productUnitChoices = this.defaultUnitChoices
this.productPriceNeedsConfirmation = false this.productPriceNeedsConfirmation = false
if (autofocus) {
this.$nextTick(() => {
this.$refs.productUPCInput.focus()
})
}
}, },
setProductUnitChoices(choices) { setProductUnitChoices(choices) {
@ -1368,34 +1366,6 @@
} }
}, },
fetchProductByUPC() {
let params = {
action: 'find_product_by_upc',
upc: this.productUPC,
}
this.submitBatchData(params, response => {
if (response.data.error) {
this.$buefy.toast.open({
message: "Fetch failed: " + response.data.error,
type: 'is-warning',
duration: 2000, // 2 seconds
})
} else {
this.productUUID = response.data.uuid
this.productUPC = response.data.upc_pretty
this.productDisplay = response.data.full_description
this.setProductUnitChoices(response.data.uom_choices)
this.productPriceNeedsConfirmation = false
}
})
},
productUPCKeyDown(event) {
if (event.which == 13) { // Enter
this.fetchProductByUPC()
}
},
productChanged(uuid) { productChanged(uuid) {
if (uuid) { if (uuid) {
this.productUUID = uuid this.productUUID = uuid
@ -1405,7 +1375,11 @@
} }
this.submitBatchData(params, response => { this.submitBatchData(params, response => {
this.productUPC = response.data.upc_pretty this.productUPC = response.data.upc_pretty
this.productKey = response.data.key
this.productDisplay = response.data.full_description this.productDisplay = response.data.full_description
this.productUnitPriceDisplay = response.data.unit_price_display
this.productURL = response.data.url
this.productImageURL = response.data.image_url
this.setProductUnitChoices(response.data.uom_choices) this.setProductUnitChoices(response.data.uom_choices)
this.productPriceNeedsConfirmation = false this.productPriceNeedsConfirmation = false
}) })

View file

@ -31,7 +31,6 @@ import decimal
import six import six
from sqlalchemy import orm from sqlalchemy import orm
from rattail import pod
from rattail.db import model from rattail.db import model
from rattail.util import pretty_quantity from rattail.util import pretty_quantity
from rattail.batch import get_batch_handler from rattail.batch import get_batch_handler
@ -259,7 +258,6 @@ class CustomerOrderView(MasterView):
'update_pending_customer', 'update_pending_customer',
'get_customer_info', 'get_customer_info',
# 'set_customer_data', # 'set_customer_data',
'find_product_by_upc',
'get_product_info', 'get_product_info',
'add_item', 'add_item',
'update_item', 'update_item',
@ -273,12 +271,6 @@ class CustomerOrderView(MasterView):
items = [self.normalize_row(row) items = [self.normalize_row(row)
for row in batch.active_rows()] for row in batch.active_rows()]
if self.handler.has_custom_product_autocomplete:
route_prefix = self.get_route_prefix()
product_autocomplete = '{}.product_autocomplete'.format(route_prefix)
else:
product_autocomplete = 'products.autocomplete'
context = self.get_context_contact(batch) context = self.get_context_contact(batch)
context.update({ context.update({
@ -288,7 +280,6 @@ class CustomerOrderView(MasterView):
'allow_contact_info_choice': self.handler.allow_contact_info_choice(), 'allow_contact_info_choice': self.handler.allow_contact_info_choice(),
'restrict_contact_info': self.handler.should_restrict_contact_info(), 'restrict_contact_info': self.handler.should_restrict_contact_info(),
'order_items': items, 'order_items': items,
'product_autocomplete_url': self.request.route_url(product_autocomplete),
}) })
return self.render_to_response(template, context) return self.render_to_response(template, context)
@ -535,31 +526,18 @@ class CustomerOrderView(MasterView):
""" """
Custom product autocomplete logic, which invokes the handler. Custom product autocomplete logic, which invokes the handler.
""" """
self.handler = self.get_batch_handler()
term = self.request.GET['term'] term = self.request.GET['term']
return self.handler.custom_product_autocomplete(self.Session(), term,
user=self.request.user)
def find_product_by_upc(self, batch, data): # if handler defines custom autocomplete, use that
upc = data.get('upc') handler = self.get_batch_handler()
if not upc: if handler.has_custom_product_autocomplete:
return {'error': "Must specify a product UPC"} return handler.custom_product_autocomplete(self.Session(), term,
user=self.request.user)
product = self.handler.locate_product_for_entry( # otherwise we use 'products.neworder' autocomplete
self.Session(), upc, product_key='upc', app = self.get_rattail_app()
# nb. let handler know "why" we're doing this, so that it autocomplete = app.get_autocompleter('products.neworder')
# can "modify" the result accordingly, i.e. return the return autocomplete.autocomplete(self.Session(), term)
# appropriate item when a "different" scancode is entered
# by the user (e.g. PLU, and/or units vs. packs)
variation='new_custorder')
if not product:
return {'error': "Product not found"}
reason = self.handler.why_not_add_product(product, batch)
if reason:
return {'error': reason}
return self.info_for_product(batch, data, product)
def get_product_info(self, batch, data): def get_product_info(self, batch, data):
uuid = data.get('uuid') uuid = data.get('uuid')
@ -608,15 +586,27 @@ class CustomerOrderView(MasterView):
return choices return choices
def info_for_product(self, batch, data, product): def info_for_product(self, batch, data, product):
return { app = self.get_rattail_app()
products = app.get_products_handler()
data = {
'uuid': product.uuid, 'uuid': product.uuid,
'upc': six.text_type(product.upc), 'upc': six.text_type(product.upc),
'upc_pretty': product.upc.pretty(), 'upc_pretty': product.upc.pretty(),
'unit_price_display': self.get_unit_price_display(product),
'full_description': product.full_description, 'full_description': product.full_description,
'image_url': pod.get_image_url(self.rattail_config, product.upc), 'url': self.request.route_url('products.view', uuid=product.uuid),
'image_url': products.get_image_url(product),
'uom_choices': self.uom_choices_for_product(product), 'uom_choices': self.uom_choices_for_product(product),
} }
key = self.rattail_config.product_key()
if key == 'upc':
data['key'] = data['upc_pretty']
else:
data['key'] = getattr(product, key, data['upc_pretty'])
return data
def normalize_batch(self, batch): def normalize_batch(self, batch):
return { return {
'uuid': batch.uuid, 'uuid': batch.uuid,
@ -626,7 +616,24 @@ class CustomerOrderView(MasterView):
'status_text': batch.status_text, 'status_text': batch.status_text,
} }
def get_unit_price_display(self, obj):
"""
Returns a display string for the given object's unit price.
The object can be either a ``Product`` instance, or a batch
row.
"""
app = self.get_rattail_app()
model = self.model
if isinstance(obj, model.Product):
products = app.get_products_handler()
return products.render_price(obj.regular_price)
else: # row
return app.render_currency(obj.unit_price)
def normalize_row(self, row): def normalize_row(self, row):
app = self.get_rattail_app()
products = app.get_products_handler()
product = row.product product = row.product
department = product.department if product else None department = product.department if product else None
cost = product.cost if product else None cost = product.cost if product else None
@ -654,7 +661,7 @@ class CustomerOrderView(MasterView):
'vendor_display': cost.vendor.name if cost else None, 'vendor_display': cost.vendor.name if cost else None,
'unit_price': six.text_type(row.unit_price) if row.unit_price is not None else None, 'unit_price': six.text_type(row.unit_price) if row.unit_price is not None else None,
'unit_price_display': "${:0.2f}".format(row.unit_price) if row.unit_price is not None else None, 'unit_price_display': self.get_unit_price_display(row),
'total_price': six.text_type(row.total_price) if row.total_price is not None else None, 'total_price': six.text_type(row.total_price) if row.total_price is not None else None,
'total_price_display': "${:0.2f}".format(row.total_price) if row.total_price is not None else None, 'total_price_display': "${:0.2f}".format(row.total_price) if row.total_price is not None else None,
'price_needs_confirmation': row.price_needs_confirmation, 'price_needs_confirmation': row.price_needs_confirmation,
@ -663,6 +670,18 @@ class CustomerOrderView(MasterView):
'status_text': row.status_text, 'status_text': row.status_text,
} }
key = self.rattail_config.product_key()
if key == 'upc':
data['product_key'] = data['product_upc_pretty']
else:
data['product_key'] = getattr(product, key, data['product_upc_pretty'])
if row.product:
data.update({
'product_url': self.request.route_url('products.view', uuid=row.product.uuid),
'product_image_url': products.get_image_url(row.product),
})
unit_uom = self.enum.UNIT_OF_MEASURE_POUND if data['product_weighed'] else self.enum.UNIT_OF_MEASURE_EACH unit_uom = self.enum.UNIT_OF_MEASURE_POUND if data['product_weighed'] else self.enum.UNIT_OF_MEASURE_EACH
if row.order_uom == self.enum.UNIT_OF_MEASURE_CASE: if row.order_uom == self.enum.UNIT_OF_MEASURE_CASE:
if row.case_quantity is None: if row.case_quantity is None: