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:
parent
8b044dbb22
commit
8d16a5f110
|
@ -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>
|
||||||
|
<b-field :expanded="!productUUID">
|
||||||
|
<tailbone-autocomplete ref="productAutocomplete"
|
||||||
v-model="productUUID"
|
v-model="productUUID"
|
||||||
:assigned-value="productUUID"
|
placeholder="Enter UPC or brand, description etc."
|
||||||
:assigned-label="productDisplay"
|
:initial-label="productDisplay"
|
||||||
serviceUrl="${product_autocomplete_url}"
|
serviceUrl="${url('{}.product_autocomplete'.format(route_prefix))}"
|
||||||
@input="productChanged">
|
@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
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
|
||||||
|
# if handler defines custom autocomplete, use that
|
||||||
|
handler = self.get_batch_handler()
|
||||||
|
if handler.has_custom_product_autocomplete:
|
||||||
|
return handler.custom_product_autocomplete(self.Session(), term,
|
||||||
user=self.request.user)
|
user=self.request.user)
|
||||||
|
|
||||||
def find_product_by_upc(self, batch, data):
|
# otherwise we use 'products.neworder' autocomplete
|
||||||
upc = data.get('upc')
|
app = self.get_rattail_app()
|
||||||
if not upc:
|
autocomplete = app.get_autocompleter('products.neworder')
|
||||||
return {'error': "Must specify a product UPC"}
|
return autocomplete.autocomplete(self.Session(), term)
|
||||||
|
|
||||||
product = self.handler.locate_product_for_entry(
|
|
||||||
self.Session(), upc, product_key='upc',
|
|
||||||
# nb. let handler know "why" we're doing this, so that it
|
|
||||||
# can "modify" the result accordingly, i.e. return the
|
|
||||||
# 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:
|
||||||
|
|
Loading…
Reference in a new issue